facebook / react-native

A framework for building native applications using React
https://reactnative.dev
MIT License
119.17k stars 24.32k forks source link

Creating new `Blob` from `Uint8Array` produces garbage data #41079

Open stefan-schweiger opened 1 year ago

stefan-schweiger commented 1 year ago

Description

I'm trying to manually convert base64 data to a Blob and vice versa. To do this I create a Uint8Array from the base64 data (with atob - polyfilled with base-64).

For example for a very simple PNG the resulting blob is about 3-4x as large as expected and the data is unrecognizable.

const base64Uri = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=";

const base64ToBlob = (b64Data, contentType, sliceSize = 512) => {
  const byteCharacters = atob(b64Data);
  const byteArray = new Uint8Array(byteCharacters.length);

  for (let n = 0; n < byteCharacters.length; n++) {
    byteArray[n] = byteCharacters.charCodeAt(n);
  }

  return new Blob([byteArray], { type: contentType });
};
const blobToBase64Uri = (blob) => {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => {
      resolve(reader.result);
    };

    reader.readAsDataURL(blob);
  });
};

const [_, type, base64] = base64Uri.match(/^data:(.*);base64,(.*)/) ?? [];

console.log(base64Uri)
// result: data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=
blobToBase64Uri(base64ToBlob(base64, type)).then(console.log)
// result: data:image/png;base64,MTM3LDgwLDc4LDcxLDEzLDEwLDI2LDEwLDAsMCwwLDEzLDczLDcyLDY4LDgyLDAsMCwwLDgsMCwwLDAsOCwxLDMsMCwwLDAsMjU0LDE5Myw0NCwyMDAsMCwwLDAsNiw4MCw3Niw4NCw2OSwyNTUsMjU1LDI1NSwxOTEsMTkxLDE5MSwxNjMsNjcsMTE4LDU3LDAsMCwwLDE0LDczLDY4LDY1LDg0LDgsMjE1LDk5LDI0OCwwLDEzMywyNTIsMTYsOCwwLDQ2LDAsMywyNTMsMTYzLDEwNSwxMTAsMjA5LDAsMCwwLDAsNzMsNjksNzgsNjgsMTc0LDY2LDk2LDEzMA==

The same code works fine on the web and outputs the same result for both log statements.

React Native Version

0.72.6

Output of npx react-native info

System: OS: macOS 14.0 CPU: (10) arm64 Apple M1 Pro Memory: 551.34 MB / 32.00 GB Shell: version: "5.9" path: /bin/zsh Binaries: Node: version: 20.8.0 path: /opt/homebrew/bin/node Yarn: version: 1.22.19 path: /usr/local/bin/yarn npm: version: 10.2.0 path: /opt/homebrew/bin/npm Watchman: version: 2023.10.09.00 path: /opt/homebrew/bin/watchman Managers: CocoaPods: version: 1.13.0 path: /opt/homebrew/bin/pod SDKs: iOS SDK: Platforms:

Steps to reproduce

  1. Install base-64
  2. run the code given in the instructions somewhere in your app

Snack, screenshot, or link to a repository

https://snack.expo.dev/@stefan-5gpay/base64blobbug

On the web version both are the same:

image

On device (same for iOS and Android) data is different:

IMG_3631

github-actions[bot] commented 1 year ago
:warning: Newer Version of React Native is Available!
:information_source: You are on a supported minor version, but it looks like there's a newer patch available - 0.72.6. Please upgrade to the highest patch for your minor or latest and verify if the issue persists (alternatively, create a new project and repro the issue in it). If it does not repro, please let us know so we can close out this issue. This helps us ensure we are looking at issues that still exist in the most recent releases.
stefan-schweiger commented 1 year ago

Still happens for patch 0.72.6, I have updated the issue, please remove the tag

stefan-schweiger commented 1 year ago

After a bit more tinkering I think it's really that the Blob implementation does not understand Uint8Arrays, because if I do this (effectively constructing a normal array) I get the same result on the web as I do on a native device: new Blob([b64.decode(b64Data).split('').map(x => x.charCodeAt(0))], { type: contentType })

stefan-schweiger commented 1 year ago

@CodyJasonBennett I saw that you implemented some additional Blob code which is not yet released that allows for Blobs to be constructed from ArrayBuffer. From what I was able to gather I think that the changes you did would also allow for Blobs to be constructed from a Uint8Array, but there is no explicit code branch for it here:

https://github.com/facebook/react-native/blob/c47bef6ae0767937cad4a75254be088118fc3dd1/packages/react-native/Libraries/Blob/BlobManager.js#L70-L86

Is that assessment correct?

EDIT: I just tried to create a small patch myself which uses 'base64-js'. But this still produces an unusable blob which just contains the base64 content as a string...

if (part instanceof Uint8Array) {
  return {
    data: fromByteArray(part),
    type: 'string'
  }
}
stefan-schweiger commented 1 year ago

@CodyJasonBennett how confident are you in your implementation? I just applied all your changes as a patch and tried to construct a blob from an ArrayBuffer and it just gave me a blob which is a basically just the buffer content as text.

image

(The output is the value the blob actually should have if it was base64 encoded)

stefan-schweiger commented 1 year ago

For a second I though that this might at least be a workaround return new Blob([b64.decode(b64Data)], { type: contentType }); but for an PDF this actually still creates a corrupted blob which when viewed in a hex editor has the correct PDF headers and metadata but the actual binary data within seems corrupted. I assume this is because of some wrong interpretation of the string encoding (e.g. UTF8 vs ASCII).

CodyJasonBennett commented 1 year ago

I assume this is because of some wrong interpretation of the string encoding (e.g. UTF8 vs ASCII).

I'm pretty sure this assumption is correct, and consequently, I'm inclined to revert #39276 until we do this properly. My previous code example did not run into this since text was incidentally decoded by FileReader. We'll need better coverage to okay a PR like that, and I noted that it is still divergent from web from byte size alone since there is no native ArrayBuffer support thus I had to workaround it with base64 (string for JSI) in the first place.


const data = await new Promise((res, rej) => {
  const reader = new FileReader()
  reader.onload = () => res(reader.result)
  reader.onerror = rej
  reader.readAsText(blob)
})

// `data:${blob.type};base64,${data}`
``
stefan-schweiger commented 1 year ago

@CodyJasonBennett If you decide to revert the changes maybe there should also be an Error throwing if a part is Uint8Array similar to the "old" check for ArrayBuffer.

CodyJasonBennett commented 1 year ago

If you decide to revert the changes maybe there should also be an Error throwing if a part is Uint8Array similar to the "old" check for ArrayBuffer.

I'm not sure what you mean. The old and current checks consider all typed arrays -- that's what ArrayBuffer.isView does.

how confident are you in your implementation? I just applied all your changes as a patch and tried to construct a blob from an ArrayBuffer and it just gave me a blob which is a basically just the buffer content as text.

Upon further review that PR is working as I initially intended even if incomplete -- this is the equivalent code for web btoa(String.fromCharCode(...new Uint8Array())) which converts from UTF-16 to base64-encoded ASCII. Notably, readAsText as used in the above example defaults to UTF-8 for encoding which hides this discrepancy since it converts under the hood.

That's obviously not desirable here in this issue though. I'm not keen on handling encoding for arbitrary binary data, but I wonder if we can get away with UTF or some other encoding to bring it closer to web. I can't find anything concrete on what the internal behavior should look like as everything states that ASCII should be used for safe transmission.

stefan-schweiger commented 1 year ago

I'm not sure what you mean. The old and current checks consider all typed arrays -- that's what ArrayBuffer.isView does.

In the "old" implementation this was the check, which did not throw an Errror if a part is Uint8Array:

if (
  part instanceof ArrayBuffer ||
  (global.ArrayBufferView && part instanceof global.ArrayBufferView)
) {
  throw new Error(
    "Creating blobs from 'ArrayBuffer' and 'ArrayBufferView' are not supported",
  );
}
github-actions[bot] commented 6 months ago

This issue is stale because it has been open 180 days with no activity. Remove stale label or comment or this will be closed in 7 days.

stefan-schweiger commented 6 months ago

not stale

maksimlya commented 2 months ago

On react native 0.75.1 after enabling new arch + bridgeless this got completely broken. The above example throws:

{"stack":"Error: Creating blobs from 'ArrayBuffer' and 'ArrayBufferView' are not supported\n    at anonymous (http://localhost:8081/index.bundle//&platform=ios&dev=true&lazy=true&minify=false&inlineSourceMap=false&modulesOnly=false&runModule=true&app=me.bluemail.comm:59350:28)\n    at map (native)\n    at createFromParts (http://localhost:8081/index.bundle//&platform=ios&dev=true&lazy=true&minify=false&inlineSourceMap=false&modulesOnly=false&runModule=true&app=me.bluemail.comm:59348:30)\n    at Blob (http://localhost:8081/index.bundle//&platform=ios&dev=true&lazy=true&minify=false&inlineSourceMap=false&modulesOnly=false&runModule=true&app=me.bluemail.comm:59610:46)\n    at base64ToBlob (JavaScript:9:18)\n    at eval (JavaScript:1:29)\n    at __tickleJs (__tickleJsHackUrl:1:43)","message":"Creating blobs from 'ArrayBuffer' and 'ArrayBufferView' are not supported"}

I added workaround in form of turning the chunk into base64 before:

function chunkToBase64(chunk) { let binary = ''; let bytes = new Uint8Array(chunk); for (let i = 0; i < bytes.byteLength; i++) { binary += String.fromCharCode(bytes[i]); } return btoa(binary); // Convert to base64 }

tho this could potentially affect performance. Does anyone has a better solution / will this be fixed?

LinusU commented 3 weeks ago

related: #44125

bot-leo commented 2 weeks ago

Hello everyone, I'm facing the same problem when working with new Uint8Array, I'm using expo in its latest version (51.0.36) and react native in version 0.74.5, I need to do this conversion because I have an api in .Net which only receives a specific type of file, is there anything to solve this problem?

image

image