From fd1f48c03e9ffe605cb03714a22359c467fe753e Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Mon, 26 Aug 2024 13:19:40 -0700 Subject: [PATCH 01/13] fix files in forms --- .../waku/src/lib/renderers/rsc-renderer.ts | 7 ++-- packages/waku/src/lib/utils/form.ts | 12 +++++-- packages/waku/src/lib/utils/stream.ts | 32 +++++++++++++++++++ 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/packages/waku/src/lib/renderers/rsc-renderer.ts b/packages/waku/src/lib/renderers/rsc-renderer.ts index 496d3db96..a73a8ee9b 100644 --- a/packages/waku/src/lib/renderers/rsc-renderer.ts +++ b/packages/waku/src/lib/renderers/rsc-renderer.ts @@ -10,7 +10,7 @@ import type { import type { ResolvedConfig } from '../config.js'; import { filePathToFileURL } from '../utils/path.js'; import { parseFormData } from '../utils/form.js'; -import { streamToString } from '../utils/stream.js'; +import { streamToArrayBuffer, arrayBufferToString } from '../utils/stream.js'; import { decodeActionId } from '../renderers/utils.js'; export const SERVER_MODULE_MAP = { @@ -194,13 +194,14 @@ export async function renderRsc( let decodedBody: unknown | undefined = args.decodedBody; if (body) { - const bodyStr = await streamToString(body); + const bodyBuf = await streamToArrayBuffer(body); + const bodyStr = arrayBufferToString(bodyBuf); if ( typeof contentType === 'string' && contentType.startsWith('multipart/form-data') ) { // XXX This doesn't support streaming unlike busboy - const formData = parseFormData(bodyStr, contentType); + const formData = parseFormData(bodyBuf, contentType); decodedBody = await decodeReply(formData, serverBundlerConfig); } else if (bodyStr) { decodedBody = await decodeReply(bodyStr, serverBundlerConfig); diff --git a/packages/waku/src/lib/utils/form.ts b/packages/waku/src/lib/utils/form.ts index a33c9214a..424c4cc9c 100644 --- a/packages/waku/src/lib/utils/form.ts +++ b/packages/waku/src/lib/utils/form.ts @@ -1,8 +1,10 @@ // TODO is this correct? better to use a library? -export const parseFormData = (body: string, contentType: string) => { +export const parseFormData = (body: ArrayBuffer, contentType: string) => { const boundary = contentType.split('boundary=')[1]; - const parts = body.split(`--${boundary}`); + const parts = new TextDecoder().decode(body).split(`--${boundary}`); + const formData = new FormData(); + for (const part of parts) { if (part.trim() === '' || part === '--') continue; const [rawHeaders, content] = part.split('\r\n\r\n', 2); @@ -22,7 +24,11 @@ export const parseFormData = (body: string, contentType: string) => { if (filenameMatch) { const filename = filenameMatch[1]; const type = headers['content-type'] || 'application/octet-stream'; - const blob = new Blob([content!], { type }); + + const encoder = new TextEncoder(); + const uint8Array = encoder.encode(content!); + + const blob = new Blob([uint8Array], { type }); formData.append(name!, blob, filename); } else { formData.append(name!, content!.trim()); diff --git a/packages/waku/src/lib/utils/stream.ts b/packages/waku/src/lib/utils/stream.ts index d70ccc5a4..304383d39 100644 --- a/packages/waku/src/lib/utils/stream.ts +++ b/packages/waku/src/lib/utils/stream.ts @@ -11,6 +11,31 @@ export const concatUint8Arrays = (arrs: Uint8Array[]): Uint8Array => { return array; }; +export const streamToArrayBuffer = async (stream: ReadableStream) => { + const reader = stream.getReader(); + const chunks = []; + let totalSize = 0; + let done = false; + let value: Uint8Array | undefined; + + do { + ({ done, value } = await reader.read()); + if (!done && value) { + chunks.push(value); + totalSize += value.length; + } + } while (!done); + + const result = new Uint8Array(totalSize); + let offset = 0; + for (const chunk of chunks) { + result.set(chunk, offset); + offset += chunk.length; + } + + return result.buffer; +}; + export const streamToString = async ( stream: ReadableStream, ): Promise => { @@ -31,6 +56,13 @@ export const streamToString = async ( return outs.join(''); }; +export function arrayBufferToString(buffer: ArrayBuffer): string { + const uint8Array = new Uint8Array(buffer); + return Array.from(uint8Array) + .map((byte) => String.fromCharCode(byte)) + .join(''); +} + export const stringToStream = (str: string): ReadableStream => { const encoder = new TextEncoder(); return new ReadableStream({ From 6f819c08f9e116514124f3e75608726dad3a6027 Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Mon, 26 Aug 2024 14:07:46 -0700 Subject: [PATCH 02/13] fix bad decoding --- packages/waku/src/lib/utils/form.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/waku/src/lib/utils/form.ts b/packages/waku/src/lib/utils/form.ts index 424c4cc9c..bac774aec 100644 --- a/packages/waku/src/lib/utils/form.ts +++ b/packages/waku/src/lib/utils/form.ts @@ -1,7 +1,13 @@ // TODO is this correct? better to use a library? export const parseFormData = (body: ArrayBuffer, contentType: string) => { const boundary = contentType.split('boundary=')[1]; - const parts = new TextDecoder().decode(body).split(`--${boundary}`); + const parts = new Uint8Array(body) + .reduce((acc, byte) => { + acc.push(String.fromCharCode(byte)); + return acc; + }, []) + .join('') + .split(`--${boundary}`); const formData = new FormData(); @@ -25,8 +31,10 @@ export const parseFormData = (body: ArrayBuffer, contentType: string) => { const filename = filenameMatch[1]; const type = headers['content-type'] || 'application/octet-stream'; - const encoder = new TextEncoder(); - const uint8Array = encoder.encode(content!); + const uint8Array = Uint8Array.from( + content!, + (char) => char.charCodeAt(0) & 0xff, + ); const blob = new Blob([uint8Array], { type }); formData.append(name!, blob, filename); From b6a773f3a46ebf14db67b9af30c37cd261323842 Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Mon, 26 Aug 2024 16:47:47 -0700 Subject: [PATCH 03/13] move conversion into conditional --- packages/waku/src/lib/renderers/rsc-renderer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/waku/src/lib/renderers/rsc-renderer.ts b/packages/waku/src/lib/renderers/rsc-renderer.ts index a73a8ee9b..0ee42b44f 100644 --- a/packages/waku/src/lib/renderers/rsc-renderer.ts +++ b/packages/waku/src/lib/renderers/rsc-renderer.ts @@ -195,7 +195,6 @@ export async function renderRsc( let decodedBody: unknown | undefined = args.decodedBody; if (body) { const bodyBuf = await streamToArrayBuffer(body); - const bodyStr = arrayBufferToString(bodyBuf); if ( typeof contentType === 'string' && contentType.startsWith('multipart/form-data') @@ -203,7 +202,8 @@ export async function renderRsc( // XXX This doesn't support streaming unlike busboy const formData = parseFormData(bodyBuf, contentType); decodedBody = await decodeReply(formData, serverBundlerConfig); - } else if (bodyStr) { + } else if (bodyBuf.byteLength > 0) { + const bodyStr = arrayBufferToString(bodyBuf); decodedBody = await decodeReply(bodyStr, serverBundlerConfig); } } From 4868377652485ca141c5a8d4ac81f59d75c3ca50 Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Mon, 26 Aug 2024 18:29:25 -0700 Subject: [PATCH 04/13] wip tests --- packages/waku/tests/form.test.ts | 81 ++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 packages/waku/tests/form.test.ts diff --git a/packages/waku/tests/form.test.ts b/packages/waku/tests/form.test.ts new file mode 100644 index 000000000..e23657ef5 --- /dev/null +++ b/packages/waku/tests/form.test.ts @@ -0,0 +1,81 @@ +import { describe, it, expect } from 'vitest'; +import { parseFormData } from '../src/lib/utils/form'; + +describe('parseFormData', () => { + it('should parse text fields correctly', () => { + const boundary = 'boundary123'; + const body = `--${boundary}\r\nContent-Disposition: form-data; name="field1"\r\n\r\nvalue1\r\n--${boundary}--`; + const contentType = `multipart/form-data; boundary=${boundary}`; + + const formData = parseFormData(new TextEncoder().encode(body), contentType); + + expect(formData.get('field1')).toBe('value1'); + }); + + it('should parse multiple text fields', () => { + const boundary = 'boundary123'; + const body = `--${boundary}\r\nContent-Disposition: form-data; name="field1"\r\n\r\nvalue1\r\n--${boundary}\r\nContent-Disposition: form-data; name="field2"\r\n\r\nvalue2\r\n--${boundary}--`; + const contentType = `multipart/form-data; boundary=${boundary}`; + + const formData = parseFormData(new TextEncoder().encode(body), contentType); + + expect(formData.get('field1')).toBe('value1'); + expect(formData.get('field2')).toBe('value2'); + }); + + it('should parse file fields correctly', () => { + const boundary = 'boundary123'; + const body = `--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="test.txt"\r\nContent-Type: text/plain\r\n\r\nfile content\r\n--${boundary}--`; + const contentType = `multipart/form-data; boundary=${boundary}`; + + const formData = parseFormData(new TextEncoder().encode(body), contentType); + + const file = formData.get('file') as File; + expect(file).toBeInstanceOf(File); + expect(file.name).toBe('test.txt'); + expect(file.type).toBe('text/plain'); + }); + + it('should handle mixed text and file fields', () => { + const boundary = 'boundary123'; + const body = `--${boundary}\r\nContent-Disposition: form-data; name="field1"\r\n\r\nvalue1\r\n--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="test.txt"\r\nContent-Type: text/plain\r\n\r\nfile content\r\n--${boundary}--`; + const contentType = `multipart/form-data; boundary=${boundary}`; + + const formData = parseFormData(new TextEncoder().encode(body), contentType); + + expect(formData.get('field1')).toBe('value1'); + const file = formData.get('file') as File; + expect(file).toBeInstanceOf(File); + expect(file.name).toBe('test.txt'); + }); + + it('should handle empty fields', () => { + const boundary = 'boundary123'; + const body = `--${boundary}\r\nContent-Disposition: form-data; name="emptyField"\r\n\r\n\r\n--${boundary}--`; + const contentType = `multipart/form-data; boundary=${boundary}`; + + const formData = parseFormData(new TextEncoder().encode(body), contentType); + + expect(formData.get('emptyField')).toBe(''); + }); + + it('should handle fields with special characters', () => { + const boundary = 'boundary123'; + const body = `--${boundary}\r\nContent-Disposition: form-data; name="special"\r\n\r\n!@#$%^&*()\r\n--${boundary}--`; + const contentType = `multipart/form-data; boundary=${boundary}`; + + const formData = parseFormData(new TextEncoder().encode(body), contentType); + + expect(formData.get('special')).toBe('!@#$%^&*()'); + }); + + it('should handle fields with line breaks', () => { + const boundary = 'boundary123'; + const body = `--${boundary}\r\nContent-Disposition: form-data; name="multiline"\r\n\r\nLine 1\r\nLine 2\r\nLine 3\r\n--${boundary}--`; + const contentType = `multipart/form-data; boundary=${boundary}`; + + const formData = parseFormData(new TextEncoder().encode(body), contentType); + + expect(formData.get('multiline')).toBe('Line 1\r\nLine 2\r\nLine 3'); + }); +}); From 219f5f4d02c30fca9757144dd4790d78d3766150 Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Mon, 26 Aug 2024 19:05:26 -0700 Subject: [PATCH 05/13] more comprehensive tests, more work to be done --- packages/waku/tests/form.test.ts | 153 +++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) diff --git a/packages/waku/tests/form.test.ts b/packages/waku/tests/form.test.ts index e23657ef5..c2b82e70e 100644 --- a/packages/waku/tests/form.test.ts +++ b/packages/waku/tests/form.test.ts @@ -1,6 +1,16 @@ import { describe, it, expect } from 'vitest'; import { parseFormData } from '../src/lib/utils/form'; +// Minimal valid 1x1 pixel PNG image +const PNG_HEADER = new Uint8Array([ + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, + 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, + 0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4, 0x89, 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44, + 0x41, 0x54, 0x08, 0xd7, 0x63, 0x60, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0xe2, + 0x21, 0xbc, 0x33, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, + 0x60, 0x82, +]); + describe('parseFormData', () => { it('should parse text fields correctly', () => { const boundary = 'boundary123'; @@ -78,4 +88,147 @@ describe('parseFormData', () => { expect(formData.get('multiline')).toBe('Line 1\r\nLine 2\r\nLine 3'); }); + + it('should parse text file fields correctly and match input', async () => { + const boundary = 'boundary123'; + const fileContent = 'file content'; + const body = `--${boundary}\r\nContent-Disposition: form-data; name="textFile"; filename="test.txt"\r\nContent-Type: text/plain\r\n\r\n${fileContent}\r\n--${boundary}--`; + const contentType = `multipart/form-data; boundary=${boundary}`; + + const formData = parseFormData(new TextEncoder().encode(body), contentType); + + const file = formData.get('textFile') as File; + expect(file).toBeInstanceOf(File); + expect(file.name).toBe('test.txt'); + expect(file.type).toBe('text/plain'); + + const fileText = await file.text(); + expect(fileText).toBe(fileContent); + }); + + it('should parse PNG file fields correctly and match input', async () => { + const boundary = 'boundary123'; + const pngContent = String.fromCharCode.apply(null, Array.from(PNG_HEADER)); + const body = `--${boundary}\r\nContent-Disposition: form-data; name="pngFile"; filename="test.png"\r\nContent-Type: image/png\r\n\r\n${pngContent}\r\n--${boundary}--`; + const contentType = `multipart/form-data; boundary=${boundary}`; + + const formData = parseFormData(new TextEncoder().encode(body), contentType); + + const file = formData.get('pngFile') as File; + expect(file).toBeInstanceOf(File); + expect(file.name).toBe('test.png'); + expect(file.type).toBe('image/png'); + expect(file.size).toBe(PNG_HEADER.length); + + const fileArrayBuffer = await file.arrayBuffer(); + const fileUint8Array = new Uint8Array(fileArrayBuffer); + expect(fileUint8Array).toEqual(PNG_HEADER); + }); + + it('should handle mixed text, text file, and PNG file fields and match input', async () => { + const boundary = 'boundary123'; + const textFieldContent = 'Hello, World!'; + const textFileContent = 'This is a text file.'; + const pngContent = String.fromCharCode.apply(null, Array.from(PNG_HEADER)); + + const body = `--${boundary}\r\nContent-Disposition: form-data; name="textField"\r\n\r\n${textFieldContent}\r\n--${boundary}\r\nContent-Disposition: form-data; name="textFile"; filename="test.txt"\r\nContent-Type: text/plain\r\n\r\n${textFileContent}\r\n--${boundary}\r\nContent-Disposition: form-data; name="pngFile"; filename="test.png"\r\nContent-Type: image/png\r\n\r\n${pngContent}\r\n--${boundary}--`; + const contentType = `multipart/form-data; boundary=${boundary}`; + + const formData = parseFormData(new TextEncoder().encode(body), contentType); + + expect(formData.get('textField')).toBe(textFieldContent); + + const textFile = formData.get('textFile') as File; + expect(textFile).toBeInstanceOf(File); + expect(textFile.name).toBe('test.txt'); + expect(textFile.type).toBe('text/plain'); + const textFileText = await textFile.text(); + expect(textFileText).toBe(textFileContent); + + const pngFile = formData.get('pngFile') as File; + expect(pngFile).toBeInstanceOf(File); + expect(pngFile.name).toBe('test.png'); + expect(pngFile.type).toBe('image/png'); + expect(pngFile.size).toBe(PNG_HEADER.length); + const pngFileArrayBuffer = await pngFile.arrayBuffer(); + const pngFileUint8Array = new Uint8Array(pngFileArrayBuffer); + expect(pngFileUint8Array).toEqual(PNG_HEADER); + }); + + it('should handle base64 encoded content', async () => { + const boundary = 'boundary123'; + const originalContent = 'Hello, World!'; + const base64Content = Buffer.from(originalContent).toString('base64'); + const body = `--${boundary}\r\nContent-Disposition: form-data; name="base64Field"\r\nContent-Transfer-Encoding: base64\r\n\r\n${base64Content}\r\n--${boundary}--`; + const contentType = `multipart/form-data; boundary=${boundary}`; + + const formData = parseFormData(new TextEncoder().encode(body), contentType); + + const decodedContent = Buffer.from( + formData.get('base64Field') as string, + 'base64', + ).toString(); + expect(decodedContent).toBe(originalContent); + }); + + it('should handle quoted-printable encoded content', async () => { + const boundary = 'boundary123'; + const originalContent = 'Hello, World! Special chars: =?!'; + const quotedPrintableContent = 'Hello, World! Special chars: =3D=3F=21'; + const body = `--${boundary}\r\nContent-Disposition: form-data; name="qpField"\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\n${quotedPrintableContent}\r\n--${boundary}--`; + const contentType = `multipart/form-data; boundary=${boundary}`; + + const formData = parseFormData(new TextEncoder().encode(body), contentType); + + expect(formData.get('qpField')).toBe(originalContent); + }); + + it('should handle binary content', async () => { + const boundary = 'boundary123'; + const binaryContent = String.fromCharCode.apply( + null, + Array.from(PNG_HEADER), + ); + const body = `--${boundary}\r\nContent-Disposition: form-data; name="binaryField"; filename="test.bin"\r\nContent-Type: application/octet-stream\r\nContent-Transfer-Encoding: binary\r\n\r\n${binaryContent}\r\n--${boundary}--`; + const contentType = `multipart/form-data; boundary=${boundary}`; + + const formData = parseFormData(new TextEncoder().encode(body), contentType); + + const file = formData.get('binaryField') as File; + expect(file).toBeInstanceOf(File); + expect(file.name).toBe('test.bin'); + expect(file.type).toBe('application/octet-stream'); + + const fileArrayBuffer = await file.arrayBuffer(); + const fileUint8Array = new Uint8Array(fileArrayBuffer); + expect(fileUint8Array).toEqual(PNG_HEADER); + }); + + it('should handle mixed encodings in a single request', async () => { + const boundary = 'boundary123'; + const plainContent = 'Plain text'; + const base64Content = Buffer.from('Base64 encoded text').toString('base64'); + const quotedPrintableContent = 'Quoted-Printable: Hello=20World=21'; + const binaryContent = String.fromCharCode.apply( + null, + Array.from(PNG_HEADER), + ); + + const body = `--${boundary}\r\nContent-Disposition: form-data; name="plainField"\r\n\r\n${plainContent}\r\n--${boundary}\r\nContent-Disposition: form-data; name="base64Field"\r\nContent-Transfer-Encoding: base64\r\n\r\n${base64Content}\r\n--${boundary}\r\nContent-Disposition: form-data; name="qpField"\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\n${quotedPrintableContent}\r\n--${boundary}\r\nContent-Disposition: form-data; name="binaryField"; filename="test.bin"\r\nContent-Type: application/octet-stream\r\nContent-Transfer-Encoding: binary\r\n\r\n${binaryContent}\r\n--${boundary}--`; + const contentType = `multipart/form-data; boundary=${boundary}`; + + const formData = parseFormData(new TextEncoder().encode(body), contentType); + + expect(formData.get('plainField')).toBe(plainContent); + expect( + Buffer.from(formData.get('base64Field') as string, 'base64').toString(), + ).toBe('Base64 encoded text'); + expect(formData.get('qpField')).toBe('Quoted-Printable: Hello World!'); + + const file = formData.get('binaryField') as File; + expect(file).toBeInstanceOf(File); + const fileArrayBuffer = await file.arrayBuffer(); + const fileUint8Array = new Uint8Array(fileArrayBuffer); + expect(fileUint8Array).toEqual(PNG_HEADER); + }); }); From ec5a4bb8c1149cba4a091900ee981d7839d9b9cb Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Tue, 27 Aug 2024 10:31:00 -0700 Subject: [PATCH 06/13] use hono provided libs for decoding forms --- .../waku/src/lib/renderers/rsc-renderer.ts | 9 +- packages/waku/src/lib/utils/form.ts | 47 ----------- packages/waku/src/lib/utils/stream.ts | 7 -- packages/waku/tests/form.test.ts | 84 ++++++++++++++----- 4 files changed, 68 insertions(+), 79 deletions(-) delete mode 100644 packages/waku/src/lib/utils/form.ts diff --git a/packages/waku/src/lib/renderers/rsc-renderer.ts b/packages/waku/src/lib/renderers/rsc-renderer.ts index 0ee42b44f..254b28575 100644 --- a/packages/waku/src/lib/renderers/rsc-renderer.ts +++ b/packages/waku/src/lib/renderers/rsc-renderer.ts @@ -9,10 +9,11 @@ import type { } from '../../server.js'; import type { ResolvedConfig } from '../config.js'; import { filePathToFileURL } from '../utils/path.js'; -import { parseFormData } from '../utils/form.js'; -import { streamToArrayBuffer, arrayBufferToString } from '../utils/stream.js'; +import { streamToArrayBuffer } from '../utils/stream.js'; import { decodeActionId } from '../renderers/utils.js'; +import { bufferToFormData, bufferToString } from 'hono/utils/buffer'; + export const SERVER_MODULE_MAP = { 'rsdw-server': 'react-server-dom-webpack/server.edge', 'waku-server': 'waku/server', @@ -200,10 +201,10 @@ export async function renderRsc( contentType.startsWith('multipart/form-data') ) { // XXX This doesn't support streaming unlike busboy - const formData = parseFormData(bodyBuf, contentType); + const formData = await bufferToFormData(bodyBuf, contentType); decodedBody = await decodeReply(formData, serverBundlerConfig); } else if (bodyBuf.byteLength > 0) { - const bodyStr = arrayBufferToString(bodyBuf); + const bodyStr = bufferToString(bodyBuf); decodedBody = await decodeReply(bodyStr, serverBundlerConfig); } } diff --git a/packages/waku/src/lib/utils/form.ts b/packages/waku/src/lib/utils/form.ts deleted file mode 100644 index bac774aec..000000000 --- a/packages/waku/src/lib/utils/form.ts +++ /dev/null @@ -1,47 +0,0 @@ -// TODO is this correct? better to use a library? -export const parseFormData = (body: ArrayBuffer, contentType: string) => { - const boundary = contentType.split('boundary=')[1]; - const parts = new Uint8Array(body) - .reduce((acc, byte) => { - acc.push(String.fromCharCode(byte)); - return acc; - }, []) - .join('') - .split(`--${boundary}`); - - const formData = new FormData(); - - for (const part of parts) { - if (part.trim() === '' || part === '--') continue; - const [rawHeaders, content] = part.split('\r\n\r\n', 2); - const headers = rawHeaders!.split('\r\n').reduce( - (acc, currentHeader) => { - const [key, value] = currentHeader.split(': '); - acc[key!.toLowerCase()] = value!; - return acc; - }, - {} as Record, - ); - const contentDisposition = headers['content-disposition']; - const nameMatch = /name="([^"]+)"/.exec(contentDisposition!); - const filenameMatch = /filename="([^"]+)"/.exec(contentDisposition!); - if (nameMatch) { - const name = nameMatch[1]; - if (filenameMatch) { - const filename = filenameMatch[1]; - const type = headers['content-type'] || 'application/octet-stream'; - - const uint8Array = Uint8Array.from( - content!, - (char) => char.charCodeAt(0) & 0xff, - ); - - const blob = new Blob([uint8Array], { type }); - formData.append(name!, blob, filename); - } else { - formData.append(name!, content!.trim()); - } - } - } - return formData; -}; diff --git a/packages/waku/src/lib/utils/stream.ts b/packages/waku/src/lib/utils/stream.ts index 304383d39..3730957d0 100644 --- a/packages/waku/src/lib/utils/stream.ts +++ b/packages/waku/src/lib/utils/stream.ts @@ -56,13 +56,6 @@ export const streamToString = async ( return outs.join(''); }; -export function arrayBufferToString(buffer: ArrayBuffer): string { - const uint8Array = new Uint8Array(buffer); - return Array.from(uint8Array) - .map((byte) => String.fromCharCode(byte)) - .join(''); -} - export const stringToStream = (str: string): ReadableStream => { const encoder = new TextEncoder(); return new ReadableStream({ diff --git a/packages/waku/tests/form.test.ts b/packages/waku/tests/form.test.ts index c2b82e70e..bd2aadfef 100644 --- a/packages/waku/tests/form.test.ts +++ b/packages/waku/tests/form.test.ts @@ -12,33 +12,42 @@ const PNG_HEADER = new Uint8Array([ ]); describe('parseFormData', () => { - it('should parse text fields correctly', () => { + it('should parse text fields correctly', async () => { const boundary = 'boundary123'; const body = `--${boundary}\r\nContent-Disposition: form-data; name="field1"\r\n\r\nvalue1\r\n--${boundary}--`; const contentType = `multipart/form-data; boundary=${boundary}`; - const formData = parseFormData(new TextEncoder().encode(body), contentType); + const formData = await parseFormData( + new TextEncoder().encode(body), + contentType, + ); expect(formData.get('field1')).toBe('value1'); }); - it('should parse multiple text fields', () => { + it('should parse multiple text fields', async () => { const boundary = 'boundary123'; const body = `--${boundary}\r\nContent-Disposition: form-data; name="field1"\r\n\r\nvalue1\r\n--${boundary}\r\nContent-Disposition: form-data; name="field2"\r\n\r\nvalue2\r\n--${boundary}--`; const contentType = `multipart/form-data; boundary=${boundary}`; - const formData = parseFormData(new TextEncoder().encode(body), contentType); + const formData = await parseFormData( + new TextEncoder().encode(body), + contentType, + ); expect(formData.get('field1')).toBe('value1'); expect(formData.get('field2')).toBe('value2'); }); - it('should parse file fields correctly', () => { + it('should parse file fields correctly', async () => { const boundary = 'boundary123'; const body = `--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="test.txt"\r\nContent-Type: text/plain\r\n\r\nfile content\r\n--${boundary}--`; const contentType = `multipart/form-data; boundary=${boundary}`; - const formData = parseFormData(new TextEncoder().encode(body), contentType); + const formData = await parseFormData( + new TextEncoder().encode(body), + contentType, + ); const file = formData.get('file') as File; expect(file).toBeInstanceOf(File); @@ -46,12 +55,15 @@ describe('parseFormData', () => { expect(file.type).toBe('text/plain'); }); - it('should handle mixed text and file fields', () => { + it('should handle mixed text and file fields', async () => { const boundary = 'boundary123'; const body = `--${boundary}\r\nContent-Disposition: form-data; name="field1"\r\n\r\nvalue1\r\n--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="test.txt"\r\nContent-Type: text/plain\r\n\r\nfile content\r\n--${boundary}--`; const contentType = `multipart/form-data; boundary=${boundary}`; - const formData = parseFormData(new TextEncoder().encode(body), contentType); + const formData = await parseFormData( + new TextEncoder().encode(body), + contentType, + ); expect(formData.get('field1')).toBe('value1'); const file = formData.get('file') as File; @@ -59,32 +71,41 @@ describe('parseFormData', () => { expect(file.name).toBe('test.txt'); }); - it('should handle empty fields', () => { + it('should handle empty fields', async () => { const boundary = 'boundary123'; const body = `--${boundary}\r\nContent-Disposition: form-data; name="emptyField"\r\n\r\n\r\n--${boundary}--`; const contentType = `multipart/form-data; boundary=${boundary}`; - const formData = parseFormData(new TextEncoder().encode(body), contentType); + const formData = await parseFormData( + new TextEncoder().encode(body), + contentType, + ); expect(formData.get('emptyField')).toBe(''); }); - it('should handle fields with special characters', () => { + it('should handle fields with special characters', async () => { const boundary = 'boundary123'; const body = `--${boundary}\r\nContent-Disposition: form-data; name="special"\r\n\r\n!@#$%^&*()\r\n--${boundary}--`; const contentType = `multipart/form-data; boundary=${boundary}`; - const formData = parseFormData(new TextEncoder().encode(body), contentType); + const formData = await parseFormData( + new TextEncoder().encode(body), + contentType, + ); expect(formData.get('special')).toBe('!@#$%^&*()'); }); - it('should handle fields with line breaks', () => { + it('should handle fields with line breaks', async () => { const boundary = 'boundary123'; const body = `--${boundary}\r\nContent-Disposition: form-data; name="multiline"\r\n\r\nLine 1\r\nLine 2\r\nLine 3\r\n--${boundary}--`; const contentType = `multipart/form-data; boundary=${boundary}`; - const formData = parseFormData(new TextEncoder().encode(body), contentType); + const formData = await parseFormData( + new TextEncoder().encode(body), + contentType, + ); expect(formData.get('multiline')).toBe('Line 1\r\nLine 2\r\nLine 3'); }); @@ -95,7 +116,10 @@ describe('parseFormData', () => { const body = `--${boundary}\r\nContent-Disposition: form-data; name="textFile"; filename="test.txt"\r\nContent-Type: text/plain\r\n\r\n${fileContent}\r\n--${boundary}--`; const contentType = `multipart/form-data; boundary=${boundary}`; - const formData = parseFormData(new TextEncoder().encode(body), contentType); + const formData = await parseFormData( + new TextEncoder().encode(body), + contentType, + ); const file = formData.get('textFile') as File; expect(file).toBeInstanceOf(File); @@ -112,7 +136,10 @@ describe('parseFormData', () => { const body = `--${boundary}\r\nContent-Disposition: form-data; name="pngFile"; filename="test.png"\r\nContent-Type: image/png\r\n\r\n${pngContent}\r\n--${boundary}--`; const contentType = `multipart/form-data; boundary=${boundary}`; - const formData = parseFormData(new TextEncoder().encode(body), contentType); + const formData = await parseFormData( + new TextEncoder().encode(body), + contentType, + ); const file = formData.get('pngFile') as File; expect(file).toBeInstanceOf(File); @@ -134,7 +161,10 @@ describe('parseFormData', () => { const body = `--${boundary}\r\nContent-Disposition: form-data; name="textField"\r\n\r\n${textFieldContent}\r\n--${boundary}\r\nContent-Disposition: form-data; name="textFile"; filename="test.txt"\r\nContent-Type: text/plain\r\n\r\n${textFileContent}\r\n--${boundary}\r\nContent-Disposition: form-data; name="pngFile"; filename="test.png"\r\nContent-Type: image/png\r\n\r\n${pngContent}\r\n--${boundary}--`; const contentType = `multipart/form-data; boundary=${boundary}`; - const formData = parseFormData(new TextEncoder().encode(body), contentType); + const formData = await parseFormData( + new TextEncoder().encode(body), + contentType, + ); expect(formData.get('textField')).toBe(textFieldContent); @@ -162,7 +192,10 @@ describe('parseFormData', () => { const body = `--${boundary}\r\nContent-Disposition: form-data; name="base64Field"\r\nContent-Transfer-Encoding: base64\r\n\r\n${base64Content}\r\n--${boundary}--`; const contentType = `multipart/form-data; boundary=${boundary}`; - const formData = parseFormData(new TextEncoder().encode(body), contentType); + const formData = await parseFormData( + new TextEncoder().encode(body), + contentType, + ); const decodedContent = Buffer.from( formData.get('base64Field') as string, @@ -178,7 +211,10 @@ describe('parseFormData', () => { const body = `--${boundary}\r\nContent-Disposition: form-data; name="qpField"\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\n${quotedPrintableContent}\r\n--${boundary}--`; const contentType = `multipart/form-data; boundary=${boundary}`; - const formData = parseFormData(new TextEncoder().encode(body), contentType); + const formData = await parseFormData( + new TextEncoder().encode(body), + contentType, + ); expect(formData.get('qpField')).toBe(originalContent); }); @@ -192,7 +228,10 @@ describe('parseFormData', () => { const body = `--${boundary}\r\nContent-Disposition: form-data; name="binaryField"; filename="test.bin"\r\nContent-Type: application/octet-stream\r\nContent-Transfer-Encoding: binary\r\n\r\n${binaryContent}\r\n--${boundary}--`; const contentType = `multipart/form-data; boundary=${boundary}`; - const formData = parseFormData(new TextEncoder().encode(body), contentType); + const formData = await parseFormData( + new TextEncoder().encode(body), + contentType, + ); const file = formData.get('binaryField') as File; expect(file).toBeInstanceOf(File); @@ -217,7 +256,10 @@ describe('parseFormData', () => { const body = `--${boundary}\r\nContent-Disposition: form-data; name="plainField"\r\n\r\n${plainContent}\r\n--${boundary}\r\nContent-Disposition: form-data; name="base64Field"\r\nContent-Transfer-Encoding: base64\r\n\r\n${base64Content}\r\n--${boundary}\r\nContent-Disposition: form-data; name="qpField"\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\n${quotedPrintableContent}\r\n--${boundary}\r\nContent-Disposition: form-data; name="binaryField"; filename="test.bin"\r\nContent-Type: application/octet-stream\r\nContent-Transfer-Encoding: binary\r\n\r\n${binaryContent}\r\n--${boundary}--`; const contentType = `multipart/form-data; boundary=${boundary}`; - const formData = parseFormData(new TextEncoder().encode(body), contentType); + const formData = await parseFormData( + new TextEncoder().encode(body), + contentType, + ); expect(formData.get('plainField')).toBe(plainContent); expect( From c8dd4687447954160b6ccdd9bcee8b20e9edde78 Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Tue, 27 Aug 2024 19:52:50 -0700 Subject: [PATCH 07/13] add buffer.ts to replace hono/utils implementation --- .../waku/src/lib/renderers/rsc-renderer.ts | 5 ++--- packages/waku/src/lib/utils/buffer.ts | 19 +++++++++++++++++++ .../tests/{form.test.ts => buffer.test.ts} | 2 +- 3 files changed, 22 insertions(+), 4 deletions(-) create mode 100644 packages/waku/src/lib/utils/buffer.ts rename packages/waku/tests/{form.test.ts => buffer.test.ts} (99%) diff --git a/packages/waku/src/lib/renderers/rsc-renderer.ts b/packages/waku/src/lib/renderers/rsc-renderer.ts index 254b28575..d5ffea1a6 100644 --- a/packages/waku/src/lib/renderers/rsc-renderer.ts +++ b/packages/waku/src/lib/renderers/rsc-renderer.ts @@ -11,8 +11,7 @@ import type { ResolvedConfig } from '../config.js'; import { filePathToFileURL } from '../utils/path.js'; import { streamToArrayBuffer } from '../utils/stream.js'; import { decodeActionId } from '../renderers/utils.js'; - -import { bufferToFormData, bufferToString } from 'hono/utils/buffer'; +import { bufferToString, parseFormData } from '../utils/buffer.js'; export const SERVER_MODULE_MAP = { 'rsdw-server': 'react-server-dom-webpack/server.edge', @@ -201,7 +200,7 @@ export async function renderRsc( contentType.startsWith('multipart/form-data') ) { // XXX This doesn't support streaming unlike busboy - const formData = await bufferToFormData(bodyBuf, contentType); + const formData = await parseFormData(bodyBuf, contentType); decodedBody = await decodeReply(formData, serverBundlerConfig); } else if (bodyBuf.byteLength > 0) { const bodyStr = bufferToString(bodyBuf); diff --git a/packages/waku/src/lib/utils/buffer.ts b/packages/waku/src/lib/utils/buffer.ts new file mode 100644 index 000000000..e4b425f88 --- /dev/null +++ b/packages/waku/src/lib/utils/buffer.ts @@ -0,0 +1,19 @@ +export const parseFormData = async ( + buffer: ArrayBuffer, + contentType: string, +): Promise => { + const response = new Response(buffer, { + headers: { + 'Content-Type': contentType, + }, + }); + return response.formData(); +}; + +export const bufferToString = (buffer: ArrayBuffer): string => { + if (buffer instanceof ArrayBuffer) { + const enc = new TextDecoder('utf-8'); + return enc.decode(buffer); + } + return buffer; +}; diff --git a/packages/waku/tests/form.test.ts b/packages/waku/tests/buffer.test.ts similarity index 99% rename from packages/waku/tests/form.test.ts rename to packages/waku/tests/buffer.test.ts index bd2aadfef..df4d53f39 100644 --- a/packages/waku/tests/form.test.ts +++ b/packages/waku/tests/buffer.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { parseFormData } from '../src/lib/utils/form'; +import { parseFormData } from '../src/lib/utils/buffer'; // Minimal valid 1x1 pixel PNG image const PNG_HEADER = new Uint8Array([ From 70bf54d24f9d35e3dbb9eafe65c221d0964a13a7 Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Tue, 27 Aug 2024 20:14:54 -0700 Subject: [PATCH 08/13] remove qp test, rewrite png based tests using request object --- packages/waku/tests/buffer.test.ts | 171 ++++++++++++++++++----------- 1 file changed, 108 insertions(+), 63 deletions(-) diff --git a/packages/waku/tests/buffer.test.ts b/packages/waku/tests/buffer.test.ts index df4d53f39..03fe39687 100644 --- a/packages/waku/tests/buffer.test.ts +++ b/packages/waku/tests/buffer.test.ts @@ -131,17 +131,31 @@ describe('parseFormData', () => { }); it('should parse PNG file fields correctly and match input', async () => { - const boundary = 'boundary123'; - const pngContent = String.fromCharCode.apply(null, Array.from(PNG_HEADER)); - const body = `--${boundary}\r\nContent-Disposition: form-data; name="pngFile"; filename="test.png"\r\nContent-Type: image/png\r\n\r\n${pngContent}\r\n--${boundary}--`; - const contentType = `multipart/form-data; boundary=${boundary}`; - - const formData = await parseFormData( - new TextEncoder().encode(body), - contentType, + const formData = new FormData(); + formData.append( + 'pngFile', + new Blob([PNG_HEADER], { type: 'image/png' }), + 'test.png', ); - const file = formData.get('pngFile') as File; + // Create a Request object + const request = new Request('http://example.com/upload', { + method: 'POST', + body: formData, + }); + + const contentType = request.headers.get('Content-Type'); + + // Get the body as ArrayBuffer + const arrayBuffer = await request.arrayBuffer(); + + // Convert ArrayBuffer to Uint8Array + const body = new Uint8Array(arrayBuffer); + + // Now use your parseFormData function + const parsedFormData = await parseFormData(body, contentType!); + + const file = parsedFormData.get('pngFile') as File; expect(file).toBeInstanceOf(File); expect(file.name).toBe('test.png'); expect(file.type).toBe('image/png'); @@ -153,29 +167,46 @@ describe('parseFormData', () => { }); it('should handle mixed text, text file, and PNG file fields and match input', async () => { - const boundary = 'boundary123'; - const textFieldContent = 'Hello, World!'; - const textFileContent = 'This is a text file.'; - const pngContent = String.fromCharCode.apply(null, Array.from(PNG_HEADER)); + const formData = new FormData(); + formData.append('textField', 'Hello, World!'); + formData.append( + 'textFile', + new Blob(['This is a text file.'], { type: 'text/plain' }), + 'test.txt', + ); + formData.append( + 'pngFile', + new Blob([PNG_HEADER], { type: 'image/png' }), + 'test.png', + ); - const body = `--${boundary}\r\nContent-Disposition: form-data; name="textField"\r\n\r\n${textFieldContent}\r\n--${boundary}\r\nContent-Disposition: form-data; name="textFile"; filename="test.txt"\r\nContent-Type: text/plain\r\n\r\n${textFileContent}\r\n--${boundary}\r\nContent-Disposition: form-data; name="pngFile"; filename="test.png"\r\nContent-Type: image/png\r\n\r\n${pngContent}\r\n--${boundary}--`; - const contentType = `multipart/form-data; boundary=${boundary}`; + // Create a Request object + const request = new Request('http://example.com/upload', { + method: 'POST', + body: formData, + }); - const formData = await parseFormData( - new TextEncoder().encode(body), - contentType, - ); + const contentType = request.headers.get('Content-Type'); + + // Get the body as ArrayBuffer + const arrayBuffer = await request.arrayBuffer(); - expect(formData.get('textField')).toBe(textFieldContent); + // Convert ArrayBuffer to Uint8Array + const body = new Uint8Array(arrayBuffer); - const textFile = formData.get('textFile') as File; + // Now use your parseFormData function + const parsedFormData = await parseFormData(body, contentType!); + + expect(parsedFormData.get('textField')).toBe('Hello, World!'); + + const textFile = parsedFormData.get('textFile') as File; expect(textFile).toBeInstanceOf(File); expect(textFile.name).toBe('test.txt'); expect(textFile.type).toBe('text/plain'); const textFileText = await textFile.text(); - expect(textFileText).toBe(textFileContent); + expect(textFileText).toBe('This is a text file.'); - const pngFile = formData.get('pngFile') as File; + const pngFile = parsedFormData.get('pngFile') as File; expect(pngFile).toBeInstanceOf(File); expect(pngFile.name).toBe('test.png'); expect(pngFile.type).toBe('image/png'); @@ -204,36 +235,32 @@ describe('parseFormData', () => { expect(decodedContent).toBe(originalContent); }); - it('should handle quoted-printable encoded content', async () => { - const boundary = 'boundary123'; - const originalContent = 'Hello, World! Special chars: =?!'; - const quotedPrintableContent = 'Hello, World! Special chars: =3D=3F=21'; - const body = `--${boundary}\r\nContent-Disposition: form-data; name="qpField"\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\n${quotedPrintableContent}\r\n--${boundary}--`; - const contentType = `multipart/form-data; boundary=${boundary}`; - - const formData = await parseFormData( - new TextEncoder().encode(body), - contentType, + it('should handle binary content', async () => { + const formData = new FormData(); + formData.append( + 'binaryField', + new Blob([PNG_HEADER], { type: 'application/octet-stream' }), + 'test.bin', ); - expect(formData.get('qpField')).toBe(originalContent); - }); + // Create a Request object + const request = new Request('http://example.com/upload', { + method: 'POST', + body: formData, + }); - it('should handle binary content', async () => { - const boundary = 'boundary123'; - const binaryContent = String.fromCharCode.apply( - null, - Array.from(PNG_HEADER), - ); - const body = `--${boundary}\r\nContent-Disposition: form-data; name="binaryField"; filename="test.bin"\r\nContent-Type: application/octet-stream\r\nContent-Transfer-Encoding: binary\r\n\r\n${binaryContent}\r\n--${boundary}--`; - const contentType = `multipart/form-data; boundary=${boundary}`; + const contentType = request.headers.get('Content-Type'); - const formData = await parseFormData( - new TextEncoder().encode(body), - contentType, - ); + // Get the body as ArrayBuffer + const arrayBuffer = await request.arrayBuffer(); + + // Convert ArrayBuffer to Uint8Array + const body = new Uint8Array(arrayBuffer); - const file = formData.get('binaryField') as File; + // Now use your parseFormData function + const parsedFormData = await parseFormData(body, contentType!); + + const file = parsedFormData.get('binaryField') as File; expect(file).toBeInstanceOf(File); expect(file.name).toBe('test.bin'); expect(file.type).toBe('application/octet-stream'); @@ -244,31 +271,49 @@ describe('parseFormData', () => { }); it('should handle mixed encodings in a single request', async () => { - const boundary = 'boundary123'; const plainContent = 'Plain text'; const base64Content = Buffer.from('Base64 encoded text').toString('base64'); - const quotedPrintableContent = 'Quoted-Printable: Hello=20World=21'; - const binaryContent = String.fromCharCode.apply( - null, - Array.from(PNG_HEADER), + const binaryContent = new Uint8Array(PNG_HEADER); + + const formData = new FormData(); + formData.append('plainField', plainContent); + formData.append('base64Field', base64Content); + formData.append( + 'binaryField', + new Blob([binaryContent], { type: 'application/octet-stream' }), + 'test.bin', ); - const body = `--${boundary}\r\nContent-Disposition: form-data; name="plainField"\r\n\r\n${plainContent}\r\n--${boundary}\r\nContent-Disposition: form-data; name="base64Field"\r\nContent-Transfer-Encoding: base64\r\n\r\n${base64Content}\r\n--${boundary}\r\nContent-Disposition: form-data; name="qpField"\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\n${quotedPrintableContent}\r\n--${boundary}\r\nContent-Disposition: form-data; name="binaryField"; filename="test.bin"\r\nContent-Type: application/octet-stream\r\nContent-Transfer-Encoding: binary\r\n\r\n${binaryContent}\r\n--${boundary}--`; - const contentType = `multipart/form-data; boundary=${boundary}`; + // Create a Request object + const request = new Request('http://example.com/upload', { + method: 'POST', + body: formData, + }); - const formData = await parseFormData( - new TextEncoder().encode(body), - contentType, - ); + const contentType = request.headers.get('Content-Type'); - expect(formData.get('plainField')).toBe(plainContent); + // Get the body as ArrayBuffer + const arrayBuffer = await request.arrayBuffer(); + + // Convert ArrayBuffer to Uint8Array + const body = new Uint8Array(arrayBuffer); + + // Now use your parseFormData function + const parsedFormData = await parseFormData(body, contentType!); + + expect(parsedFormData.get('plainField')).toBe(plainContent); expect( - Buffer.from(formData.get('base64Field') as string, 'base64').toString(), + Buffer.from( + parsedFormData.get('base64Field') as string, + 'base64', + ).toString(), ).toBe('Base64 encoded text'); - expect(formData.get('qpField')).toBe('Quoted-Printable: Hello World!'); - const file = formData.get('binaryField') as File; + const file = parsedFormData.get('binaryField') as File; expect(file).toBeInstanceOf(File); + expect(file.name).toBe('test.bin'); + expect(file.type).toBe('application/octet-stream'); + const fileArrayBuffer = await file.arrayBuffer(); const fileUint8Array = new Uint8Array(fileArrayBuffer); expect(fileUint8Array).toEqual(PNG_HEADER); From e0816bd8e3faaf3ba0baa77556081559113f1c6c Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Tue, 27 Aug 2024 20:17:21 -0700 Subject: [PATCH 09/13] basic tests for buffer to string --- packages/waku/tests/buffer.test.ts | 39 +++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/packages/waku/tests/buffer.test.ts b/packages/waku/tests/buffer.test.ts index 03fe39687..57b71e061 100644 --- a/packages/waku/tests/buffer.test.ts +++ b/packages/waku/tests/buffer.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { parseFormData } from '../src/lib/utils/buffer'; +import { bufferToString, parseFormData } from '../src/lib/utils/buffer'; // Minimal valid 1x1 pixel PNG image const PNG_HEADER = new Uint8Array([ @@ -319,3 +319,40 @@ describe('parseFormData', () => { expect(fileUint8Array).toEqual(PNG_HEADER); }); }); + +describe('bufferToString', () => { + it('should convert an empty ArrayBuffer to an empty string', () => { + const buffer = new ArrayBuffer(0); + expect(bufferToString(buffer)).toBe(''); + }); + + it('should convert a simple ASCII string', () => { + const text = 'Hello, World!'; + const buffer = new TextEncoder().encode(text).buffer; + expect(bufferToString(buffer)).toBe(text); + }); + + it('should handle Unicode characters', () => { + const text = '你好,世界!😊'; + const buffer = new TextEncoder().encode(text).buffer; + expect(bufferToString(buffer)).toBe(text); + }); + + it('should handle a mix of ASCII and Unicode characters', () => { + const text = 'Hello, 世界! 🌍'; + const buffer = new TextEncoder().encode(text).buffer; + expect(bufferToString(buffer)).toBe(text); + }); + + it('should handle a large string', () => { + const text = 'a'.repeat(1000000); // 1 million 'a' characters + const buffer = new TextEncoder().encode(text).buffer; + expect(bufferToString(buffer)).toBe(text); + }); + + it('should handle null characters', () => { + const text = 'Hello\0World'; + const buffer = new TextEncoder().encode(text).buffer; + expect(bufferToString(buffer)).toBe(text); + }); +}); From 41cd666c8a90623580a5b131bc2166f3dfc677bc Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Wed, 28 Aug 2024 09:37:06 -0700 Subject: [PATCH 10/13] remove conditional check for buffer to string --- packages/waku/src/lib/utils/buffer.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/waku/src/lib/utils/buffer.ts b/packages/waku/src/lib/utils/buffer.ts index e4b425f88..5457d762f 100644 --- a/packages/waku/src/lib/utils/buffer.ts +++ b/packages/waku/src/lib/utils/buffer.ts @@ -11,9 +11,6 @@ export const parseFormData = async ( }; export const bufferToString = (buffer: ArrayBuffer): string => { - if (buffer instanceof ArrayBuffer) { - const enc = new TextDecoder('utf-8'); - return enc.decode(buffer); - } - return buffer; + const enc = new TextDecoder('utf-8'); + return enc.decode(buffer); }; From fb44400c9aa73cf4204a3819dc7095a665ab2db0 Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Wed, 28 Aug 2024 09:39:37 -0700 Subject: [PATCH 11/13] add fixme --- packages/waku/src/lib/utils/stream.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/waku/src/lib/utils/stream.ts b/packages/waku/src/lib/utils/stream.ts index 3730957d0..5dfa4f152 100644 --- a/packages/waku/src/lib/utils/stream.ts +++ b/packages/waku/src/lib/utils/stream.ts @@ -11,6 +11,7 @@ export const concatUint8Arrays = (arrs: Uint8Array[]): Uint8Array => { return array; }; +// FIXME remove the two loops if possible, if it is more efficient export const streamToArrayBuffer = async (stream: ReadableStream) => { const reader = stream.getReader(); const chunks = []; From 0d5e4e658c5238e259d64b8c92731b7618d70124 Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Wed, 28 Aug 2024 09:45:37 -0700 Subject: [PATCH 12/13] clean up test suite --- packages/waku/tests/buffer.test.ts | 34 ++++++------------------------ 1 file changed, 7 insertions(+), 27 deletions(-) diff --git a/packages/waku/tests/buffer.test.ts b/packages/waku/tests/buffer.test.ts index 57b71e061..b99c8d283 100644 --- a/packages/waku/tests/buffer.test.ts +++ b/packages/waku/tests/buffer.test.ts @@ -50,9 +50,10 @@ describe('parseFormData', () => { ); const file = formData.get('file') as File; - expect(file).toBeInstanceOf(File); expect(file.name).toBe('test.txt'); expect(file.type).toBe('text/plain'); + expect(file.size).toBe(12); + expect(await file.text()).toBe('file content'); }); it('should handle mixed text and file fields', async () => { @@ -67,8 +68,10 @@ describe('parseFormData', () => { expect(formData.get('field1')).toBe('value1'); const file = formData.get('file') as File; - expect(file).toBeInstanceOf(File); expect(file.name).toBe('test.txt'); + expect(file.type).toBe('text/plain'); + expect(file.size).toBe(12); + expect(await file.text()).toBe('file content'); }); it('should handle empty fields', async () => { @@ -110,26 +113,6 @@ describe('parseFormData', () => { expect(formData.get('multiline')).toBe('Line 1\r\nLine 2\r\nLine 3'); }); - it('should parse text file fields correctly and match input', async () => { - const boundary = 'boundary123'; - const fileContent = 'file content'; - const body = `--${boundary}\r\nContent-Disposition: form-data; name="textFile"; filename="test.txt"\r\nContent-Type: text/plain\r\n\r\n${fileContent}\r\n--${boundary}--`; - const contentType = `multipart/form-data; boundary=${boundary}`; - - const formData = await parseFormData( - new TextEncoder().encode(body), - contentType, - ); - - const file = formData.get('textFile') as File; - expect(file).toBeInstanceOf(File); - expect(file.name).toBe('test.txt'); - expect(file.type).toBe('text/plain'); - - const fileText = await file.text(); - expect(fileText).toBe(fileContent); - }); - it('should parse PNG file fields correctly and match input', async () => { const formData = new FormData(); formData.append( @@ -156,7 +139,6 @@ describe('parseFormData', () => { const parsedFormData = await parseFormData(body, contentType!); const file = parsedFormData.get('pngFile') as File; - expect(file).toBeInstanceOf(File); expect(file.name).toBe('test.png'); expect(file.type).toBe('image/png'); expect(file.size).toBe(PNG_HEADER.length); @@ -200,14 +182,12 @@ describe('parseFormData', () => { expect(parsedFormData.get('textField')).toBe('Hello, World!'); const textFile = parsedFormData.get('textFile') as File; - expect(textFile).toBeInstanceOf(File); expect(textFile.name).toBe('test.txt'); expect(textFile.type).toBe('text/plain'); const textFileText = await textFile.text(); expect(textFileText).toBe('This is a text file.'); const pngFile = parsedFormData.get('pngFile') as File; - expect(pngFile).toBeInstanceOf(File); expect(pngFile.name).toBe('test.png'); expect(pngFile.type).toBe('image/png'); expect(pngFile.size).toBe(PNG_HEADER.length); @@ -261,9 +241,9 @@ describe('parseFormData', () => { const parsedFormData = await parseFormData(body, contentType!); const file = parsedFormData.get('binaryField') as File; - expect(file).toBeInstanceOf(File); expect(file.name).toBe('test.bin'); expect(file.type).toBe('application/octet-stream'); + expect(file.size).toBe(PNG_HEADER.length); const fileArrayBuffer = await file.arrayBuffer(); const fileUint8Array = new Uint8Array(fileArrayBuffer); @@ -310,9 +290,9 @@ describe('parseFormData', () => { ).toBe('Base64 encoded text'); const file = parsedFormData.get('binaryField') as File; - expect(file).toBeInstanceOf(File); expect(file.name).toBe('test.bin'); expect(file.type).toBe('application/octet-stream'); + expect(file.size).toBe(PNG_HEADER.length); const fileArrayBuffer = await file.arrayBuffer(); const fileUint8Array = new Uint8Array(fileArrayBuffer); From 4a9f9d8bba4520b451b1da1a026300ffa3c8eca5 Mon Sep 17 00:00:00 2001 From: Daishi Kato Date: Thu, 29 Aug 2024 08:58:53 +0900 Subject: [PATCH 13/13] Apply suggestions from code review --- packages/waku/src/lib/utils/buffer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/waku/src/lib/utils/buffer.ts b/packages/waku/src/lib/utils/buffer.ts index 5457d762f..7cd9c5ce2 100644 --- a/packages/waku/src/lib/utils/buffer.ts +++ b/packages/waku/src/lib/utils/buffer.ts @@ -4,13 +4,13 @@ export const parseFormData = async ( ): Promise => { const response = new Response(buffer, { headers: { - 'Content-Type': contentType, + 'content-type': contentType, }, }); return response.formData(); }; export const bufferToString = (buffer: ArrayBuffer): string => { - const enc = new TextDecoder('utf-8'); + const enc = new TextDecoder(); return enc.decode(buffer); };