From 0fad233ee2ba019fd6ad0e1f074c7b68e3dad86c Mon Sep 17 00:00:00 2001 From: Cody Bennett Date: Sun, 22 Oct 2023 07:10:20 -0500 Subject: [PATCH 1/7] experiment(native): harden Blob URI check --- packages/fiber/src/native/polyfills.ts | 66 +++++++------------------- 1 file changed, 18 insertions(+), 48 deletions(-) diff --git a/packages/fiber/src/native/polyfills.ts b/packages/fiber/src/native/polyfills.ts index 4e8de6be70..4109ddc850 100644 --- a/packages/fiber/src/native/polyfills.ts +++ b/packages/fiber/src/native/polyfills.ts @@ -15,6 +15,8 @@ export function polyfills() { }) } + let BLOB_URL_PREFIX: string | null = null + // Patch Blob for ArrayBuffer if unsupported // https://github.com/facebook/react-native/pull/39276 if (Platform.OS !== 'web') { @@ -23,16 +25,10 @@ export function polyfills() { const url = URL.createObjectURL(blob) URL.revokeObjectURL(url) } catch (_) { - const BlobManager = require('react-native/Libraries/Blob/BlobManager.js') - - let BLOB_URL_PREFIX: string | null = null - - const { BlobModule } = NativeModules - - if (BlobModule && typeof BlobModule.BLOB_URI_SCHEME === 'string') { - BLOB_URL_PREFIX = BlobModule.BLOB_URI_SCHEME + ':' - if (typeof BlobModule.BLOB_URI_HOST === 'string') { - BLOB_URL_PREFIX += `//${BlobModule.BLOB_URI_HOST}/` + if (NativeModules.BlobModule && typeof NativeModules.BlobModule.BLOB_URI_SCHEME === 'string') { + BLOB_URL_PREFIX = NativeModules.BlobModule.BLOB_URI_SCHEME + ':' + if (typeof NativeModules.BlobModule.BLOB_URI_HOST === 'string') { + BLOB_URL_PREFIX += `//${NativeModules.BlobModule.BLOB_URI_HOST}/` } } @@ -48,50 +44,24 @@ export function polyfills() { return `${BLOB_URL_PREFIX}${data.blobId}?offset=${data.offset}&size=${blob.size}` } - BlobManager.createFromParts = function createFromParts(parts: Array, options: any) { - const blobId = uuidv4() - - const items = parts.map((part) => { + const BlobManager = require('react-native/Libraries/Blob/BlobManager.js') + const createFromParts = BlobManager.createFromParts + BlobManager.createFromParts = function (parts: Array, options: any) { + parts = parts.map((part) => { if (part instanceof ArrayBuffer || ArrayBuffer.isView(part)) { - const data = fromByteArray(new Uint8Array(part as ArrayBuffer)) - return { - data, - type: 'string', - } - } else if (part instanceof Blob) { - return { - data: (part as any).data, - type: 'blob', - } - } else { - return { - data: String(part), - type: 'string', - } - } - }) - const size = items.reduce((acc, curr) => { - if (curr.type === 'string') { - return acc + global.unescape(encodeURI(curr.data)).length - } else { - return acc + curr.data.size + part = fromByteArray(new Uint8Array(part as ArrayBuffer)) } - }, 0) - NativeModules.BlobModule.createFromParts(items, blobId) - - const blob = BlobManager.createFromOptions({ - blobId, - offset: 0, - size, - type: options ? options.type : '', - lastModified: options ? options.lastModified : Date.now(), + return part }) + const blob = createFromParts(parts) + if (BLOB_URL_PREFIX === null) { let data = '' - for (const item of items) { - data += item.data._base64 ?? item.data + for (const part of parts) { + if (part instanceof Blob) data += (part as any).data._base64 + else data += part } blob.data._base64 = data } @@ -107,7 +77,7 @@ export function polyfills() { if (input.startsWith('file:')) return input // Unpack Blobs from react-native BlobManager - if (input.startsWith('blob:')) { + if (input.startsWith('blob:') || (typeof BLOB_URL_PREFIX === 'string' && input.startsWith(BLOB_URL_PREFIX))) { const blob = await new Promise((res, rej) => { const xhr = new XMLHttpRequest() xhr.open('GET', input as string) From 1302aaadae7e4e16023e6d67f53011745d3e60bd Mon Sep 17 00:00:00 2001 From: Cody Bennett Date: Sun, 22 Oct 2023 07:22:51 -0500 Subject: [PATCH 2/7] chore: cleanup --- packages/fiber/src/native/polyfills.ts | 54 ++++++++------------------ 1 file changed, 16 insertions(+), 38 deletions(-) diff --git a/packages/fiber/src/native/polyfills.ts b/packages/fiber/src/native/polyfills.ts index 4109ddc850..e26ac282e6 100644 --- a/packages/fiber/src/native/polyfills.ts +++ b/packages/fiber/src/native/polyfills.ts @@ -15,8 +15,6 @@ export function polyfills() { }) } - let BLOB_URL_PREFIX: string | null = null - // Patch Blob for ArrayBuffer if unsupported // https://github.com/facebook/react-native/pull/39276 if (Platform.OS !== 'web') { @@ -25,46 +23,35 @@ export function polyfills() { const url = URL.createObjectURL(blob) URL.revokeObjectURL(url) } catch (_) { - if (NativeModules.BlobModule && typeof NativeModules.BlobModule.BLOB_URI_SCHEME === 'string') { - BLOB_URL_PREFIX = NativeModules.BlobModule.BLOB_URI_SCHEME + ':' - if (typeof NativeModules.BlobModule.BLOB_URI_HOST === 'string') { - BLOB_URL_PREFIX += `//${NativeModules.BlobModule.BLOB_URI_HOST}/` - } - } - - URL.createObjectURL = function createObjectURL(blob: Blob): string { - const data = (blob as any).data + const BlobManager = require('react-native/Libraries/Blob/BlobManager.js') - if (BLOB_URL_PREFIX === null) { - // https://github.com/pmndrs/react-three-fiber/issues/3058 - // throw new Error('Cannot create URL for blob!') - return `data:${blob.type};base64,${data._base64}` + const createObjectURL = URL.createObjectURL + URL.createObjectURL = function (blob: Blob): string { + if ((blob as any)._base64) { + return `data:${blob.type};base64,${(blob as any)._base64}` } - return `${BLOB_URL_PREFIX}${data.blobId}?offset=${data.offset}&size=${blob.size}` + return createObjectURL(blob) } - const BlobManager = require('react-native/Libraries/Blob/BlobManager.js') const createFromParts = BlobManager.createFromParts BlobManager.createFromParts = function (parts: Array, options: any) { + let base64 = '' + parts = parts.map((part) => { if (part instanceof ArrayBuffer || ArrayBuffer.isView(part)) { part = fromByteArray(new Uint8Array(part as ArrayBuffer)) } + if (!NativeModules.BlobModule?.BLOB_URI_SCHEME) { + base64 += (part as any)._base64 ?? part + } + return part }) - const blob = createFromParts(parts) - - if (BLOB_URL_PREFIX === null) { - let data = '' - for (const part of parts) { - if (part instanceof Blob) data += (part as any).data._base64 - else data += part - } - blob.data._base64 = data - } + const blob = createFromParts(parts, options) + blob._base64 = base64 return blob } @@ -77,7 +64,7 @@ export function polyfills() { if (input.startsWith('file:')) return input // Unpack Blobs from react-native BlobManager - if (input.startsWith('blob:') || (typeof BLOB_URL_PREFIX === 'string' && input.startsWith(BLOB_URL_PREFIX))) { + if (input.startsWith('blob:') || input.startsWith(NativeModules.BlobModule?.BLOB_URI_SCHEME)) { const blob = await new Promise((res, rej) => { const xhr = new XMLHttpRequest() xhr.open('GET', input as string) @@ -131,8 +118,6 @@ export function polyfills() { THREE.TextureLoader.prototype.load = function load(this: THREE.TextureLoader, url, onLoad, onProgress, onError) { if (this.path && typeof url === 'string') url = this.path + url - this.manager.itemStart(url) - const texture = new THREE.Texture() getAsset(url) @@ -148,7 +133,6 @@ export function polyfills() { width, height, } - texture.flipY = true // Since expo-gl@12.4.0 texture.needsUpdate = true // Force non-DOM upload for EXGL texImage2D @@ -157,13 +141,7 @@ export function polyfills() { onLoad?.(texture) }) - .catch((error) => { - onError?.(error) - this.manager.itemError(url) - }) - .finally(() => { - this.manager.itemEnd(url) - }) + .catch(onError) return texture } From 13eaaf82e402009b0bb58c7cafd018b7a388a985 Mon Sep 17 00:00:00 2001 From: Cody Bennett Date: Sun, 22 Oct 2023 07:27:26 -0500 Subject: [PATCH 3/7] chore: update mock --- packages/fiber/__mocks__/react-native.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/fiber/__mocks__/react-native.ts b/packages/fiber/__mocks__/react-native.ts index 5b2aedbf9c..129eed47d5 100644 --- a/packages/fiber/__mocks__/react-native.ts +++ b/packages/fiber/__mocks__/react-native.ts @@ -49,3 +49,5 @@ export const Image = { export const Platform = { OS: 'web', } + +export const NativeModules = {} From 6c3649b3193752051f9d1aee7fb778777bdd33d0 Mon Sep 17 00:00:00 2001 From: Cody Bennett Date: Sun, 22 Oct 2023 07:36:28 -0500 Subject: [PATCH 4/7] chore: cleanup --- packages/fiber/src/native/polyfills.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/fiber/src/native/polyfills.ts b/packages/fiber/src/native/polyfills.ts index e26ac282e6..1560872cad 100644 --- a/packages/fiber/src/native/polyfills.ts +++ b/packages/fiber/src/native/polyfills.ts @@ -28,7 +28,7 @@ export function polyfills() { const createObjectURL = URL.createObjectURL URL.createObjectURL = function (blob: Blob): string { if ((blob as any)._base64) { - return `data:${blob.type};base64,${(blob as any)._base64}` + return `data:${blob.type};base64,${(blob as any).data._base64}` } return createObjectURL(blob) @@ -44,14 +44,14 @@ export function polyfills() { } if (!NativeModules.BlobModule?.BLOB_URI_SCHEME) { - base64 += (part as any)._base64 ?? part + base64 += (part as any).data._base64 ?? part } return part }) const blob = createFromParts(parts, options) - blob._base64 = base64 + blob.data._base64 = base64 return blob } @@ -64,6 +64,7 @@ export function polyfills() { if (input.startsWith('file:')) return input // Unpack Blobs from react-native BlobManager + // https://github.com/facebook/react-native/issues/22681#issuecomment-523258955 if (input.startsWith('blob:') || input.startsWith(NativeModules.BlobModule?.BLOB_URI_SCHEME)) { const blob = await new Promise((res, rej) => { const xhr = new XMLHttpRequest() From aabacb5ec70b588554c686115a167885ea132419 Mon Sep 17 00:00:00 2001 From: Cody Bennett Date: Sun, 22 Oct 2023 07:45:13 -0500 Subject: [PATCH 5/7] chore: cleanup --- packages/fiber/src/native/polyfills.ts | 143 +++++++++++++------------ 1 file changed, 72 insertions(+), 71 deletions(-) diff --git a/packages/fiber/src/native/polyfills.ts b/packages/fiber/src/native/polyfills.ts index 1560872cad..2dcc21a1f3 100644 --- a/packages/fiber/src/native/polyfills.ts +++ b/packages/fiber/src/native/polyfills.ts @@ -5,18 +5,72 @@ import * as fs from 'expo-file-system' import { fromByteArray } from 'base64-js' import { Buffer } from 'buffer' -export function polyfills() { - // http://stackoverflow.com/questions/105034 - function uuidv4() { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { - const r = (Math.random() * 16) | 0, - v = c == 'x' ? r : (r & 0x3) | 0x8 - return v.toString(16) - }) +// http://stackoverflow.com/questions/105034 +function uuidv4() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + const r = (Math.random() * 16) | 0, + v = c == 'x' ? r : (r & 0x3) | 0x8 + return v.toString(16) + }) +} + +async function getAsset(input: string | number): Promise { + if (typeof input === 'string') { + // Don't process storage + if (input.startsWith('file:')) return input + + // Unpack Blobs from react-native BlobManager + // https://github.com/facebook/react-native/issues/22681#issuecomment-523258955 + if (input.startsWith('blob:') || input.startsWith(NativeModules.BlobModule?.BLOB_URI_SCHEME)) { + const blob = await new Promise((res, rej) => { + const xhr = new XMLHttpRequest() + xhr.open('GET', input as string) + xhr.responseType = 'blob' + xhr.onload = () => res(xhr.response) + xhr.onerror = rej + xhr.send() + }) + + const data = await new Promise((res, rej) => { + const reader = new FileReader() + reader.onload = () => res(reader.result as string) + reader.onerror = rej + reader.readAsText(blob) + }) + + input = `data:${blob.type};base64,${data}` + } + + // Create safe URI for JSI + if (input.startsWith('data:')) { + const [header, data] = input.split(';base64,') + const [, type] = header.split('/') + + const uri = fs.cacheDirectory + uuidv4() + `.${type}` + await fs.writeAsStringAsync(uri, data, { encoding: fs.EncodingType.Base64 }) + + return uri + } + } + + // Download bundler module or external URL + const asset = await Asset.fromModule(input).downloadAsync() + let uri = asset.localUri || asset.uri + + // Unpack assets in Android Release Mode + if (!uri.includes(':')) { + const file = `${fs.cacheDirectory}ExponentAsset-${asset.hash}.${asset.type}` + await fs.copyAsync({ from: uri, to: file }) + uri = file } - // Patch Blob for ArrayBuffer if unsupported - // https://github.com/facebook/react-native/pull/39276 + return uri +} + +export function polyfills() { + // Patch Blob for ArrayBuffer and URL if unsupported + // https://github.com/pmndrs/react-three-fiber/pull/3059 + // https://github.com/pmndrs/react-three-fiber/issues/3058 if (Platform.OS !== 'web') { try { const blob = new Blob([new ArrayBuffer(4) as any]) @@ -27,7 +81,7 @@ export function polyfills() { const createObjectURL = URL.createObjectURL URL.createObjectURL = function (blob: Blob): string { - if ((blob as any)._base64) { + if ((blob as any).data._base64) { return `data:${blob.type};base64,${(blob as any).data._base64}` } @@ -36,79 +90,26 @@ export function polyfills() { const createFromParts = BlobManager.createFromParts BlobManager.createFromParts = function (parts: Array, options: any) { - let base64 = '' - parts = parts.map((part) => { if (part instanceof ArrayBuffer || ArrayBuffer.isView(part)) { part = fromByteArray(new Uint8Array(part as ArrayBuffer)) } - if (!NativeModules.BlobModule?.BLOB_URI_SCHEME) { - base64 += (part as any).data._base64 ?? part - } - return part }) const blob = createFromParts(parts, options) - blob.data._base64 = base64 - - return blob - } - } - } - - async function getAsset(input: string | number): Promise { - if (typeof input === 'string') { - // Don't process storage - if (input.startsWith('file:')) return input - - // Unpack Blobs from react-native BlobManager - // https://github.com/facebook/react-native/issues/22681#issuecomment-523258955 - if (input.startsWith('blob:') || input.startsWith(NativeModules.BlobModule?.BLOB_URI_SCHEME)) { - const blob = await new Promise((res, rej) => { - const xhr = new XMLHttpRequest() - xhr.open('GET', input as string) - xhr.responseType = 'blob' - xhr.onload = () => res(xhr.response) - xhr.onerror = rej - xhr.send() - }) - - const data = await new Promise((res, rej) => { - const reader = new FileReader() - reader.onload = () => res(reader.result as string) - reader.onerror = rej - reader.readAsText(blob) - }) - input = `data:${blob.type};base64,${data}` - } - - // Create safe URI for JSI - if (input.startsWith('data:')) { - const [header, data] = input.split(';base64,') - const [, type] = header.split('/') - - const uri = fs.cacheDirectory + uuidv4() + `.${type}` - await fs.writeAsStringAsync(uri, data, { encoding: fs.EncodingType.Base64 }) + if (!NativeModules.BlobModule?.BLOB_URI_SCHEME) { + blob.data._base64 = '' + for (const part of parts) { + blob.data._base64 += (part as any).data._base64 ?? part + } + } - return uri + return blob } } - - // Download bundler module or external URL - const asset = await Asset.fromModule(input).downloadAsync() - let uri = asset.localUri || asset.uri - - // Unpack assets in Android Release Mode - if (!uri.includes(':')) { - const file = `${fs.cacheDirectory}ExponentAsset-${asset.hash}.${asset.type}` - await fs.copyAsync({ from: uri, to: file }) - uri = file - } - - return uri } // Don't pre-process urls, let expo-asset generate an absolute URL From 0dd661ff024628e332e619d575e50566daeb191d Mon Sep 17 00:00:00 2001 From: Cody Bennett Date: Sun, 22 Oct 2023 07:52:42 -0500 Subject: [PATCH 6/7] chore: cleanup conflicts --- packages/fiber/src/native/polyfills.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/fiber/src/native/polyfills.ts b/packages/fiber/src/native/polyfills.ts index 2dcc21a1f3..6accce0266 100644 --- a/packages/fiber/src/native/polyfills.ts +++ b/packages/fiber/src/native/polyfills.ts @@ -103,7 +103,7 @@ export function polyfills() { if (!NativeModules.BlobModule?.BLOB_URI_SCHEME) { blob.data._base64 = '' for (const part of parts) { - blob.data._base64 += (part as any).data._base64 ?? part + blob.data._base64 += (part as any).data?._base64 ?? part } } @@ -135,6 +135,7 @@ export function polyfills() { width, height, } + texture.flipY = true // Since expo-gl@12.4.0 texture.needsUpdate = true // Force non-DOM upload for EXGL texImage2D From d552fe93913e8e0fbb4d5d2946654f604cab0967 Mon Sep 17 00:00:00 2001 From: Cody Bennett Date: Sun, 22 Oct 2023 07:56:58 -0500 Subject: [PATCH 7/7] chore: bump CI --- packages/fiber/src/native/polyfills.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/fiber/src/native/polyfills.ts b/packages/fiber/src/native/polyfills.ts index 6accce0266..7d9f6532bf 100644 --- a/packages/fiber/src/native/polyfills.ts +++ b/packages/fiber/src/native/polyfills.ts @@ -41,7 +41,7 @@ async function getAsset(input: string | number): Promise { input = `data:${blob.type};base64,${data}` } - // Create safe URI for JSI + // Create safe URI for JSI serialization if (input.startsWith('data:')) { const [header, data] = input.split(';base64,') const [, type] = header.split('/')