@@ -638,3 +638,153 @@ func TestDecoderRawDict(t *testing.T) {
638638 t .Errorf ("mismatch: got %q, wanted %q" , out , ref )
639639 }
640640}
641+
642+ // TestEncoderDictResetDifferentContent verifies that ResetWithOptions correctly
643+ // handles switching between raw dicts that share the same ID but have different
644+ // content lengths. Previously, the encoder cached dict tables by ID only, so a
645+ // shorter dict reusing the same ID would leave stale table entries pointing
646+ // beyond the new (shorter) history, causing an out-of-bounds panic in matchlen.
647+ func TestEncoderDictResetDifferentContent (t * testing.T ) {
648+ // Two raw dicts: same ID, different content lengths.
649+ longDict := make ([]byte , 700 )
650+ for i := range longDict {
651+ longDict [i ] = byte (i * 3 )
652+ }
653+ shortDict := make ([]byte , 120 )
654+ for i := range shortDict {
655+ shortDict [i ] = byte (i * 7 )
656+ }
657+
658+ const dictID = 42
659+ // Payload reuses bytes from the tail of longDict (beyond shortDict's length).
660+ // This makes stale dict table entries match during encoding, triggering the
661+ // out-of-bounds access when the encoder uses the stale offset.
662+ payload := make ([]byte , 200 )
663+ copy (payload , longDict [500 :])
664+
665+ for level := SpeedFastest ; level < speedLast ; level ++ {
666+ t .Run (level .String (), func (t * testing.T ) {
667+ enc , err := NewWriter (nil , WithEncoderConcurrency (1 ), WithEncoderLevel (level ), WithEncoderDictRaw (dictID , longDict ))
668+ if err != nil {
669+ t .Fatal (err )
670+ }
671+
672+ // Encode with long dict first to populate table entries at high offsets.
673+ enc .EncodeAll (payload , nil )
674+
675+ // Switch to shorter dict with same ID. This must rebuild the tables.
676+ if err := enc .ResetWithOptions (nil , WithEncoderDictRaw (dictID , shortDict )); err != nil {
677+ t .Fatal (err )
678+ }
679+ compressed := enc .EncodeAll (payload , nil )
680+
681+ // Verify round-trip with matching dict.
682+ dec , err := NewReader (nil , WithDecoderConcurrency (1 ), WithDecoderDictRaw (dictID , shortDict ))
683+ if err != nil {
684+ t .Fatal (err )
685+ }
686+ defer dec .Close ()
687+ got , err := dec .DecodeAll (compressed , nil )
688+ if err != nil {
689+ t .Fatal (err )
690+ }
691+ if ! bytes .Equal (got , payload ) {
692+ t .Errorf ("round-trip mismatch: got %q, want %q" , got , payload )
693+ }
694+ })
695+ }
696+ }
697+
698+ // TestEncoderDictAddViaReset verifies that adding/removing a dict via
699+ // ResetWithOptions works (requires recreating the encoder type).
700+ func TestEncoderDictAddViaReset (t * testing.T ) {
701+ dict := make ([]byte , 120 )
702+ for i := range dict {
703+ dict [i ] = byte (i )
704+ }
705+ payload := []byte ("hello world, this is a test payload!!" )
706+
707+ for level := SpeedFastest ; level < speedLast ; level ++ {
708+ t .Run ("nil-to-dict/" + level .String (), func (t * testing.T ) {
709+ enc , err := NewWriter (nil , WithEncoderConcurrency (1 ), WithEncoderLevel (level ))
710+ if err != nil {
711+ t .Fatal (err )
712+ }
713+ if err := enc .ResetWithOptions (nil , WithEncoderDictRaw (42 , dict )); err != nil {
714+ t .Fatal (err )
715+ }
716+ compressed := enc .EncodeAll (payload , nil )
717+
718+ dec , err := NewReader (nil , WithDecoderConcurrency (1 ), WithDecoderDictRaw (42 , dict ))
719+ if err != nil {
720+ t .Fatal (err )
721+ }
722+ defer dec .Close ()
723+ got , err := dec .DecodeAll (compressed , nil )
724+ if err != nil {
725+ t .Fatal (err )
726+ }
727+ if ! bytes .Equal (got , payload ) {
728+ t .Errorf ("round-trip mismatch: got %q, want %q" , got , payload )
729+ }
730+ })
731+
732+ t .Run ("dict-to-nil/" + level .String (), func (t * testing.T ) {
733+ enc , err := NewWriter (nil , WithEncoderConcurrency (1 ), WithEncoderLevel (level ), WithEncoderDictRaw (42 , dict ))
734+ if err != nil {
735+ t .Fatal (err )
736+ }
737+ if err := enc .ResetWithOptions (nil , WithEncoderDictDelete ()); err != nil {
738+ t .Fatal (err )
739+ }
740+ compressed := enc .EncodeAll (payload , nil )
741+
742+ dec , err := NewReader (nil , WithDecoderConcurrency (1 ))
743+ if err != nil {
744+ t .Fatal (err )
745+ }
746+ defer dec .Close ()
747+ got , err := dec .DecodeAll (compressed , nil )
748+ if err != nil {
749+ t .Fatal (err )
750+ }
751+ if ! bytes .Equal (got , payload ) {
752+ t .Errorf ("round-trip mismatch: got %q, want %q" , got , payload )
753+ }
754+ })
755+
756+ t .Run ("streaming-dict-to-nil/" + level .String (), func (t * testing.T ) {
757+ var buf bytes.Buffer
758+ enc , err := NewWriter (& buf , WithEncoderConcurrency (2 ), WithEncoderLevel (level ), WithEncoderDictRaw (42 , dict ))
759+ if err != nil {
760+ t .Fatal (err )
761+ }
762+ enc .Close ()
763+
764+ buf .Reset ()
765+ if err := enc .ResetWithOptions (& buf , WithEncoderDictDelete ()); err != nil {
766+ t .Fatal (err )
767+ }
768+ _ , err = enc .Write (payload )
769+ if err != nil {
770+ t .Fatal (err )
771+ }
772+ if err := enc .Close (); err != nil {
773+ t .Fatal (err )
774+ }
775+
776+ dec , err := NewReader (nil , WithDecoderConcurrency (1 ))
777+ if err != nil {
778+ t .Fatal (err )
779+ }
780+ defer dec .Close ()
781+ got , err := dec .DecodeAll (buf .Bytes (), nil )
782+ if err != nil {
783+ t .Fatal (err )
784+ }
785+ if ! bytes .Equal (got , payload ) {
786+ t .Errorf ("round-trip mismatch: got %q, want %q" , got , payload )
787+ }
788+ })
789+ }
790+ }
0 commit comments