@@ -196,4 +196,117 @@ describe('parseURL', () => {
196196 // includes BOM bytes, independently of stripBOM
197197 expect ( result [ 0 ] ?. meta . byteCount ) . toBe ( fileSize )
198198 } )
199+
200+ it . for ( [
201+ { firstByte : 0 , expected : { row : [ '👉🏿' , '1' ] , charCount : 6 } } ,
202+ // There is no way to know that '👉' and '🏿' were part of a combined emoji.
203+ { firstByte : 0 , lastByte : 3 , expected : { row : [ '👉' ] , charCount : 2 } } ,
204+ { firstByte : 1 , expected : { row : [ '🏿' , '1' ] , charCount : 4 , invalidByteCount : 3 } } ,
205+ { firstByte : 2 , expected : { row : [ '🏿' , '1' ] , charCount : 4 , invalidByteCount : 2 } } ,
206+ { firstByte : 3 , expected : { row : [ '🏿' , '1' ] , charCount : 4 , invalidByteCount : 1 } } ,
207+ { firstByte : 4 , expected : { row : [ '🏿' , '1' ] , charCount : 4 } } ,
208+ { firstByte : 4 , lastByte : 7 , expected : { row : [ '🏿' ] , charCount : 2 } } ,
209+ { firstByte : 5 , expected : { row : [ '' , '1' ] , charCount : 2 , invalidByteCount : 3 } } ,
210+ { firstByte : 6 , expected : { row : [ '' , '1' ] , charCount : 2 , invalidByteCount : 2 } } ,
211+ { firstByte : 7 , expected : { row : [ '' , '1' ] , charCount : 2 , invalidByteCount : 1 } } ,
212+ { firstByte : 8 , expected : { row : [ '' , '1' ] , charCount : 2 } } ,
213+ ] ) ( 'should support cutting 👉🏿 emoji: firstByte=$firstByte, lastByte=$lastByte' , async ( { firstByte, lastByte, expected : { row, charCount, invalidByteCount } } ) => {
214+ // 👉🏿 uses 8 bytes in UTF-8.
215+ const text = '👉🏿,1'
216+ const { url, fileSize, revoke } = toUrl ( text )
217+ lastByte ??= fileSize - 1
218+ const result = [ ]
219+ for await ( const r of parseURL ( url , { firstByte, lastByte, delimiter : ',' , newline : '\n' } ) ) {
220+ result . push ( r )
221+ }
222+ revoke ( )
223+
224+ const expectedByteOffset = firstByte + ( invalidByteCount ?? 0 )
225+ expect ( result . length ) . toBe ( 1 )
226+ expect ( result ) . toEqual ( [ {
227+ errors : invalidByteCount === undefined
228+ ? [ ]
229+ : [ {
230+ type : 'Decoding' ,
231+ code : 'InvalidData' ,
232+ message : `Skipped ${ invalidByteCount } invalid byte(s) at the start of the range` ,
233+ } ] ,
234+ row,
235+ meta : {
236+ byteOffset : expectedByteOffset ,
237+ byteCount : lastByte - expectedByteOffset + 1 ,
238+ charCount, // TODO(SL): define what is a "character" (UTF-16 code point? grapheme?)
239+ delimiter : ',' ,
240+ newline : '\n' ,
241+ } ,
242+ } ] )
243+ expect ( ( result [ 0 ] ?. meta . byteCount ?? - Infinity ) + ( result [ 0 ] ?. meta . byteOffset ?? - Infinity ) ) . toBe ( lastByte + 1 )
244+ } )
245+
246+ it ( 'should search invalid bytes over multiple chunks if needed' , async ( ) => {
247+ const text = '👉,1'
248+ const { url, fileSize, revoke } = toUrl ( text )
249+ const result = [ ]
250+ // There are 3 invalid bytes at start, when starting at byte 1. Using chunkSize=1 to force multiple iterations.
251+ for await ( const r of parseURL ( url , { chunkSize : 1 , firstByte : 1 , lastByte : fileSize - 1 , delimiter : ',' , newline : '\n' } ) ) {
252+ result . push ( r )
253+ }
254+ revoke ( )
255+
256+ expect ( result . length ) . toBe ( 1 )
257+ expect ( result ) . toEqual ( [ {
258+ errors : [ {
259+ type : 'Decoding' ,
260+ code : 'InvalidData' ,
261+ message : 'Skipped 3 invalid byte(s) at the start of the range' ,
262+ } ] ,
263+ row : [ '' , '1' ] ,
264+ meta : {
265+ byteOffset : 4 ,
266+ byteCount : fileSize - 4 ,
267+ charCount : 2 ,
268+ delimiter : ',' ,
269+ newline : '\n' ,
270+ } ,
271+ } ] )
272+ } )
273+
274+ it ( 'should report invalid data in multiple rows' , async ( ) => {
275+ const text = '👉a,b\n1,2'
276+ const { url, fileSize, revoke } = toUrl ( text )
277+ const result = [ ]
278+ for await ( const r of parseURL ( url , { firstByte : 3 , lastByte : fileSize - 1 , delimiter : ',' , newline : '\n' } ) ) {
279+ result . push ( r )
280+ }
281+ revoke ( )
282+ expect ( result . length ) . toBe ( 2 )
283+ expect ( result ) . toEqual ( [
284+ {
285+ errors : [ {
286+ type : 'Decoding' ,
287+ code : 'InvalidData' ,
288+ message : 'Skipped 1 invalid byte(s) at the start of the range' ,
289+ } ] ,
290+ row : [ 'a' , 'b' ] ,
291+ meta : {
292+ byteOffset : 4 ,
293+ byteCount : 4 ,
294+ charCount : 4 ,
295+ delimiter : ',' ,
296+ newline : '\n' ,
297+ } ,
298+ } ,
299+ {
300+ errors : [ ] ,
301+ row : [ '1' , '2' ] ,
302+ meta : {
303+ byteOffset : 8 ,
304+ byteCount : 3 ,
305+ charCount : 3 ,
306+ delimiter : ',' ,
307+ newline : '\n' ,
308+ } ,
309+ } ,
310+ ] )
311+ } )
199312} )
0 commit comments