@@ -100,90 +100,159 @@ func (r *Reader) PeekReplyType() (byte, error) {
100100 return b [0 ], nil
101101}
102102
103+ // PeekPushNotificationName returns the notification name of the next RESP3
104+ // push frame without consuming it. The caller is expected to have already
105+ // verified that the next reply is a push notification (e.g. via PeekReplyType
106+ // returning RespPush).
107+ //
108+ // To identify the name the method may block briefly reading more bytes from
109+ // the underlying connection. That is safe: once the push marker '>' has been
110+ // observed, the server is committed to sending the rest of the frame, so
111+ // fetching the next few header bytes does not race with anything the caller
112+ // could be waiting on. Blocking is preferred to a truncated peek, which would
113+ // silently misidentify the notification and cause the caller's ReadReply to
114+ // consume (and drop) the frame; see issue #3839.
103115func (r * Reader ) PeekPushNotificationName () (string , error ) {
104- // "prime" the buffer by peeking at the next byte
105- c , err := r .Peek (1 )
116+ c , err := r .rd .Peek (1 )
106117 if err != nil {
107118 return "" , err
108119 }
109120 if c [0 ] != RespPush {
110121 return "" , fmt .Errorf ("redis: can't peek push notification name, next reply is not a push notification" )
111122 }
112123
113- // peek 36 bytes at most, should be enough to read the push notification name
114- toPeek := 36
115- buffered := r .Buffered ()
116- if buffered == 0 {
117- return "" , fmt .Errorf ("redis: can't peek push notification name, no data available" )
118- }
119- if buffered < toPeek {
120- toPeek = buffered
124+ // Start with a peek window that covers every Redis-defined notification
125+ // header (MOVING, MIGRATING, FAILED_OVER, message, pmessage, smessage,
126+ // subscribe, unsubscribe, ...). If a longer name is encountered, grow
127+ // the window up to maxPushHeaderPeek before giving up.
128+ const initialPeek = 36
129+ const maxPushHeaderPeek = 4096
130+
131+ peekSize := initialPeek
132+ for {
133+ buf , peekErr := r .rd .Peek (peekSize )
134+ name , complete , parseErr := parsePushNotificationName (buf )
135+ if parseErr != nil {
136+ return "" , parseErr
137+ }
138+ if complete {
139+ return name , nil
140+ }
141+ // Parser ran out of bytes. Surface a failed underlying read before
142+ // growing further; otherwise grow the peek window and retry.
143+ if peekErr != nil {
144+ return "" , peekErr
145+ }
146+ if peekSize >= maxPushHeaderPeek {
147+ return "" , fmt .Errorf ("redis: push notification header exceeds %d bytes" , maxPushHeaderPeek )
148+ }
149+ peekSize *= 2
150+ if peekSize > maxPushHeaderPeek {
151+ peekSize = maxPushHeaderPeek
152+ }
121153 }
122- buf , err := r .rd .Peek (toPeek )
123- if err != nil {
124- return "" , err
154+ }
155+
156+ // parsePushNotificationName extracts the notification name from a buffered
157+ // RESP3 push frame prefix. The three return values are:
158+ //
159+ // - (name, true, nil): the full name is in buf.
160+ // - ("", false, nil): buf is a valid prefix but too short to determine the
161+ // name; the caller should fetch more bytes and retry.
162+ // - ("", _, err): buf is malformed.
163+ //
164+ // This split lets PeekPushNotificationName tell "incomplete header" apart
165+ // from "corrupt frame" without ever returning a truncated string.
166+ func parsePushNotificationName (buf []byte ) (string , bool , error ) {
167+ // Need at least ">N\r" before any meaningful work.
168+ if len (buf ) < 3 {
169+ return "" , false , nil
125170 }
126171 if buf [0 ] != RespPush {
127- return "" , fmt .Errorf ("redis: can't parse push notification: %q" , buf )
172+ return "" , false , fmt .Errorf ("redis: can't parse push notification: %q" , buf )
128173 }
129174
130- if len (buf ) < 3 {
131- return "" , fmt .Errorf ("redis: can't parse push notification: %q" , buf )
175+ // Skip the array length line ">N\r\n".
176+ const arrayLenStart = 1 // first byte after the '>' marker
177+ pos , ok , err := skipDigitsThenCRLF (buf , arrayLenStart )
178+ if err != nil {
179+ return "" , false , fmt .Errorf ("redis: can't parse push notification: %w" , err )
132180 }
133-
134- // remove push notification type
135- buf = buf [1 :]
136- // remove first line - e.g. >2\r\n
137- for i := 0 ; i < len (buf )- 1 ; i ++ {
138- if buf [i ] == '\r' && buf [i + 1 ] == '\n' {
139- buf = buf [i + 2 :]
140- break
141- } else {
142- if buf [i ] < '0' || buf [i ] > '9' {
143- return "" , fmt .Errorf ("redis: can't parse push notification: %q" , buf )
144- }
145- }
181+ if ! ok {
182+ return "" , false , nil
183+ }
184+ // Reject ">\r\n": RESP requires at least one digit for the array length.
185+ // Without this check the empty length looks like a valid prefix and the
186+ // caller would block fetching more bytes for a frame that is already
187+ // malformed.
188+ if pos - 2 == arrayLenStart {
189+ return "" , false , fmt .Errorf ("redis: empty push notification array length" )
146190 }
147- if len (buf ) < 2 {
148- return "" , fmt .Errorf ("redis: can't parse push notification: %q" , buf )
191+
192+ // First element type byte: '$' (bulk) or '+' (simple-string).
193+ if pos >= len (buf ) {
194+ return "" , false , nil
149195 }
150- // next line should be $<length><string>\r\n or +<length><string>\r\n
151- // should have the type of the push notification name and it's length
152- if buf [0 ] != RespString && buf [0 ] != RespStatus {
153- return "" , fmt .Errorf ("redis: can't parse push notification name: %q" , buf )
196+ typeOfName := buf [pos ]
197+ if typeOfName != RespString && typeOfName != RespStatus {
198+ return "" , false , fmt .Errorf ("redis: can't parse push notification name: %q" , buf [pos :])
154199 }
155- typeOfName := buf [0 ]
156- // remove the type of the push notification name
157- buf = buf [1 :]
200+ pos ++
201+
158202 if typeOfName == RespString {
159- // remove the length of the string
160- if len (buf ) < 2 {
161- return "" , fmt .Errorf ("redis: can't parse push notification name: %q" , buf )
162- }
163- for i := 0 ; i < len (buf )- 1 ; i ++ {
164- if buf [i ] == '\r' && buf [i + 1 ] == '\n' {
165- buf = buf [i + 2 :]
166- break
167- } else {
168- if buf [i ] < '0' || buf [i ] > '9' {
169- return "" , fmt .Errorf ("redis: can't parse push notification name: %q" , buf )
170- }
171- }
203+ // Read "$M\r\n" then the M-byte name.
204+ lenStart := pos
205+ next , ok , err := skipDigitsThenCRLF (buf , pos )
206+ if err != nil {
207+ return "" , false , fmt .Errorf ("redis: can't parse push notification name length: %w" , err )
208+ }
209+ if ! ok {
210+ return "" , false , nil
172211 }
212+ if next - 2 == lenStart {
213+ return "" , false , fmt .Errorf ("redis: empty push notification name length" )
214+ }
215+ nameLen , err := util .Atoi (buf [lenStart : next - 2 ])
216+ if err != nil {
217+ return "" , false , fmt .Errorf ("redis: invalid push notification name length %q: %w" , buf [lenStart :next - 2 ], err )
218+ }
219+ if nameLen < 0 {
220+ return "" , false , fmt .Errorf ("redis: negative push notification name length: %d" , nameLen )
221+ }
222+ // Compare against the remaining bytes instead of computing
223+ // next+nameLen: a hugely advertised length on malformed input could
224+ // overflow int, wrap negative, slip past an "end > len(buf)" guard and
225+ // panic the slice below. next <= len(buf) here, so the subtraction is
226+ // safe.
227+ if nameLen > len (buf )- next {
228+ return "" , false , nil
229+ }
230+ return util .BytesToString (buf [next : next + nameLen ]), true , nil
173231 }
174232
175- if len (buf ) < 2 {
176- return "" , fmt .Errorf ("redis: can't parse push notification name: %q" , buf )
177- }
178- // keep only the notification name
179- for i := 0 ; i < len (buf )- 1 ; i ++ {
233+ // RespStatus: scan for the terminating CRLF.
234+ for i := pos ; i < len (buf )- 1 ; i ++ {
180235 if buf [i ] == '\r' && buf [i + 1 ] == '\n' {
181- buf = buf [:i ]
182- break
236+ return util .BytesToString (buf [pos :i ]), true , nil
183237 }
184238 }
239+ return "" , false , nil
240+ }
185241
186- return util .BytesToString (buf ), nil
242+ // skipDigitsThenCRLF advances past zero-or-more ASCII digits and the
243+ // terminating "\r\n" starting at offset start in buf. It returns the position
244+ // after the "\r\n" and true on success; (pos, false, nil) if buf is too
245+ // short; or an error if a non-digit non-CR byte is encountered before the CRLF.
246+ func skipDigitsThenCRLF (buf []byte , start int ) (int , bool , error ) {
247+ for pos := start ; pos < len (buf )- 1 ; pos ++ {
248+ if buf [pos ] == '\r' && buf [pos + 1 ] == '\n' {
249+ return pos + 2 , true , nil
250+ }
251+ if buf [pos ] < '0' || buf [pos ] > '9' {
252+ return pos , false , fmt .Errorf ("expected digit or CRLF, got %q" , buf [pos ])
253+ }
254+ }
255+ return len (buf ), false , nil
187256}
188257
189258// ReadLine Return a valid reply, it will check the protocol or redis error,
0 commit comments