From e2a5d3bb5401f7165f6bc9a94a56502957a162ed Mon Sep 17 00:00:00 2001 From: Marc Alvarez Date: Sun, 15 Mar 2026 01:40:13 -0600 Subject: [PATCH] fix concurrent QR decode by cloning BitMatrix in parser --- bit_matrix.go | 7 +++++- bit_matrix_test.go | 31 ++++++++++++++++++++++++ qrcode/decoder/bit_matrix_parser.go | 5 +++- qrcode/decoder/bit_matrix_parser_test.go | 14 +++++------ qrcode/decoder/decoder_test.go | 31 ++++++++++++++++++++++++ 5 files changed, 79 insertions(+), 9 deletions(-) diff --git a/bit_matrix.go b/bit_matrix.go index 7184831..040bfb1 100644 --- a/bit_matrix.go +++ b/bit_matrix.go @@ -389,4 +389,9 @@ func (b *BitMatrix) ToStringWithLineSeparator(setString, unsetString, lineSepara return string(result) } -// public BitMatrix clone() +// Clone returns a deep copy of the BitMatrix. +func (b *BitMatrix) Clone() *BitMatrix { + bits := make([]uint32, len(b.bits)) + copy(bits, b.bits) + return &BitMatrix{b.width, b.height, b.rowSize, bits} +} diff --git a/bit_matrix_test.go b/bit_matrix_test.go index e745e2a..11b45e9 100644 --- a/bit_matrix_test.go +++ b/bit_matrix_test.go @@ -449,3 +449,34 @@ func TestBitMatrix_String(t *testing.T) { t.Fatalf("String is\n%s\nexpect:\n%s", r, s2) } } + +func TestBitMatrix_Clone(t *testing.T) { + original, _ := NewBitMatrix(8, 8) + original.Set(0, 0) + original.Set(3, 4) + original.Set(7, 7) + + clone := original.Clone() + + if clone.GetWidth() != original.GetWidth() || clone.GetHeight() != original.GetHeight() { + t.Fatalf("Clone dimensions %dx%d != original %dx%d", + clone.GetWidth(), clone.GetHeight(), original.GetWidth(), original.GetHeight()) + } + for y := 0; y < 8; y++ { + for x := 0; x < 8; x++ { + if clone.Get(x, y) != original.Get(x, y) { + t.Errorf("Clone differs at (%d, %d): got %v, want %v", + x, y, clone.Get(x, y), original.Get(x, y)) + } + } + } + + clone.Flip(0, 0) + clone.Set(1, 1) + if !original.Get(0, 0) { + t.Error("Mutating clone affected original at (0, 0)") + } + if original.Get(1, 1) { + t.Error("Mutating clone affected original at (1, 1)") + } +} diff --git a/qrcode/decoder/bit_matrix_parser.go b/qrcode/decoder/bit_matrix_parser.go index 670f03c..a777554 100644 --- a/qrcode/decoder/bit_matrix_parser.go +++ b/qrcode/decoder/bit_matrix_parser.go @@ -16,7 +16,10 @@ func NewBitMatrixParser(bitMatrix *gozxing.BitMatrix) (*BitMatrixParser, error) if dimension < 21 || (dimension&0x03) != 1 { return nil, gozxing.NewFormatException("dimension = %v", dimension) } - return &BitMatrixParser{bitMatrix: bitMatrix}, nil + // Clone the matrix so that mutations during decoding (UnmaskBitMatrix, + // Remask, Mirror) do not corrupt the shared cached copy returned by + // BinaryBitmap.GetBlackMatrix(). + return &BitMatrixParser{bitMatrix: bitMatrix.Clone()}, nil } func (this *BitMatrixParser) ReadFormatInformation() (*FormatInformation, error) { diff --git a/qrcode/decoder/bit_matrix_parser_test.go b/qrcode/decoder/bit_matrix_parser_test.go index 1dcc87f..8d78439 100644 --- a/qrcode/decoder/bit_matrix_parser_test.go +++ b/qrcode/decoder/bit_matrix_parser_test.go @@ -51,8 +51,8 @@ func TestNewBitMatrixParser(t *testing.T) { if e != nil { t.Fatalf("NewBitMatrixParser(21x21) returns error, %v", e) } - if p.bitMatrix != img { - t.Fatalf("p.bitMatrix = %p, expect %p", p.bitMatrix, img) + if p.bitMatrix == img { + t.Fatalf("p.bitMatrix must be a clone, not the same pointer") } if p.parsedVersion != nil { t.Fatalf("p.parsedVersion is not nil, %p", p.parsedVersion) @@ -313,14 +313,14 @@ func TestBitMatrixParser_Remask(t *testing.T) { p, _ := NewBitMatrixParser(img) p.Remask() - compareBitMatrix(t, img, unmasked) + compareBitMatrix(t, p.bitMatrix, unmasked) p.ReadFormatInformation() p.Remask() - compareBitMatrix(t, img, masked) + compareBitMatrix(t, p.bitMatrix, masked) p.Remask() - compareBitMatrix(t, img, unmasked) + compareBitMatrix(t, p.bitMatrix, unmasked) } func TestBitMatrixParser_Mirror(t *testing.T) { @@ -352,8 +352,8 @@ func TestBitMatrixParser_Mirror(t *testing.T) { p, _ := NewBitMatrixParser(img) p.Mirror() - compareBitMatrix(t, img, mirrored) + compareBitMatrix(t, p.bitMatrix, mirrored) p.Mirror() - compareBitMatrix(t, img, unmirrored) + compareBitMatrix(t, p.bitMatrix, unmirrored) } diff --git a/qrcode/decoder/decoder_test.go b/qrcode/decoder/decoder_test.go index c5ab57f..c29f640 100644 --- a/qrcode/decoder/decoder_test.go +++ b/qrcode/decoder/decoder_test.go @@ -1,6 +1,7 @@ package decoder import ( + "sync" "testing" "github.com/makiuchi-d/gozxing" @@ -152,3 +153,33 @@ func TestDecoder_decode(t *testing.T) { t.Fatalf("decoder result text=\"%v\", expect \"hello\"", r) } } + +func TestDecoder_ConcurrentDecode(t *testing.T) { + bits, _ := gozxing.ParseStringToBitMatrix(qrstr, "##", " ") + + var wg sync.WaitGroup + errs := make(chan error, 10) + + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + defer wg.Done() + decoder := NewDecoder() + result, err := decoder.Decode(bits, nil) + if err != nil { + errs <- err + return + } + if r := result.GetText(); r != "hello" { + errs <- gozxing.NewFormatException("got %q, want %q", r, "hello") + } + }() + } + + wg.Wait() + close(errs) + + for err := range errs { + t.Errorf("Concurrent decode error: %v", err) + } +}