Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Creating new Blob from Uint8Array produces garbage data #41079

Open
stefan-schweiger opened this issue Oct 19, 2023 · 15 comments
Open

Creating new Blob from Uint8Array produces garbage data #41079

stefan-schweiger opened this issue Oct 19, 2023 · 15 comments
Labels
Issue: Author Provided Repro This issue can be reproduced in Snack or an attached project. Never gets stale Prevent those issues and PRs from getting stale Newer Patch Available

Comments

@stefan-schweiger
Copy link

stefan-schweiger commented Oct 19, 2023

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:
- DriverKit 23.0
- iOS 17.0
- macOS 14.0
- tvOS 17.0
- watchOS 10.0
Android SDK: Not Found
IDEs:
Android Studio: 2021.2 AI-212.5712.43.2112.8609683
Xcode:
version: 15.0/15A240d
path: /usr/bin/xcodebuild
Languages:
Java:
version: 11.0.20.1
path: /usr/bin/javac
Ruby:
version: 2.6.10
path: /usr/bin/ruby
npmPackages:
"@react-native-community/cli": Not Found
react:
installed: 18.2.0
wanted: 18.2.0
react-native:
installed: 0.72.5
wanted: 0.72.5
react-native-macos: Not Found
npmGlobalPackages:
"react-native": Not Found
Android:
hermesEnabled: Not found
newArchEnabled: Not found
iOS:
hermesEnabled: Not found
newArchEnabled: Not found

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
Copy link

⚠️ Newer Version of React Native is Available!
ℹ️ 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 stefan-schweiger changed the title Creating new Blob from Uint8Arrays produces garbage data Creating new Blob from Uint8Array produces garbage data Oct 19, 2023
@stefan-schweiger
Copy link
Author

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

@stefan-schweiger
Copy link
Author

stefan-schweiger commented Oct 19, 2023

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
Copy link
Author

stefan-schweiger commented Oct 20, 2023

@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:

if (part instanceof ArrayBuffer || ArrayBuffer.isView(part)) {
return {
// $FlowFixMe[incompatible-cast]
data: fromByteArray(new Uint8Array((part: ArrayBuffer))),
type: 'string',
};
} else if (part instanceof Blob) {
return {
data: part.data,
type: 'blob',
};
} else {
return {
data: String(part),
type: 'string',
};
}

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
Copy link
Author

stefan-schweiger commented Oct 20, 2023

@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
Copy link
Author

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
Copy link
Contributor

CodyJasonBennett commented Oct 21, 2023

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
Copy link
Author

@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
Copy link
Contributor

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
Copy link
Author

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",
  );
}

facebook-github-bot pushed a commit that referenced this issue Oct 31, 2023
Summary:
As per #41079, we're outputting ASCII encoded data URIs to `FileReader.readAsDataURL` due to lack of native `ArrayBuffer` support and unclear use of encoding to align with web. I'll revisit this at a later point with a better testing strategy once we have a good idea of how this should behave internally.

Aside from purely reverting #39276, I've kept the use of `ArrayBuffer.isView(part)` to the previous `part instanceof global.ArrayBufferView` since it is more correct.

## Changelog:

[INTERNAL] [REMOVED] - Revert Blob from ArrayBuffer

Pull Request resolved: #41170

Test Plan:
Run the following at the project root to selectively test changes:

`jest packages/react-native/Libraries/Blob`

Reviewed By: cipolleschi

Differential Revision: D50601036

Pulled By: dmytrorykun

fbshipit-source-id: 0ef5c960c253db255c2f8532ea1f44111093706c
Othinn pushed a commit to Othinn/react-native that referenced this issue Jan 9, 2024
facebook#41170)

Summary:
As per facebook#41079, we're outputting ASCII encoded data URIs to `FileReader.readAsDataURL` due to lack of native `ArrayBuffer` support and unclear use of encoding to align with web. I'll revisit this at a later point with a better testing strategy once we have a good idea of how this should behave internally.

Aside from purely reverting facebook#39276, I've kept the use of `ArrayBuffer.isView(part)` to the previous `part instanceof global.ArrayBufferView` since it is more correct.

## Changelog:

[INTERNAL] [REMOVED] - Revert Blob from ArrayBuffer

Pull Request resolved: facebook#41170

Test Plan:
Run the following at the project root to selectively test changes:

`jest packages/react-native/Libraries/Blob`

Reviewed By: cipolleschi

Differential Revision: D50601036

Pulled By: dmytrorykun

fbshipit-source-id: 0ef5c960c253db255c2f8532ea1f44111093706c
Copy link

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.

@github-actions github-actions bot added the Stale There has been a lack of activity on this issue and it may be closed soon. label Apr 23, 2024
@stefan-schweiger
Copy link
Author

not stale

@github-actions github-actions bot removed the Stale There has been a lack of activity on this issue and it may be closed soon. label Apr 23, 2024
@cortinico cortinico added Issue: Author Provided Repro This issue can be reproduced in Snack or an attached project. Never gets stale Prevent those issues and PRs from getting stale and removed Needs: Triage 🔍 labels Apr 23, 2024
@maksimlya
Copy link

maksimlya commented Aug 27, 2024

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
Copy link
Contributor

LinusU commented Oct 14, 2024

related: #44125

@bot-leo
Copy link

bot-leo commented Oct 25, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Issue: Author Provided Repro This issue can be reproduced in Snack or an attached project. Never gets stale Prevent those issues and PRs from getting stale Newer Patch Available
Projects
None yet
Development

No branches or pull requests

6 participants