Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions packages/react-native/Libraries/Blob/FileReader.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ class FileReader extends EventTarget {
_result: ?ReaderResult;
_aborted: boolean = false;
_readId: number = 0;
// The blob being read must stay strongly referenced until the native read
// settles. Otherwise, if the caller drops its own reference (as
// whatwg-fetch does in `readBlobAsText`), the Blob can be garbage
// collected while the read is still in flight, and its BlobCollector
// finalizer deallocates the underlying native buffer, failing the read
// with "The specified blob is invalid".
_blob: ?Blob;

constructor() {
super();
Expand All @@ -56,6 +63,7 @@ class FileReader extends EventTarget {
this._readyState = EMPTY;
this._error = null;
this._result = null;
this._blob = null;
}

_startRead(methodName: string): number {
Expand Down Expand Up @@ -110,12 +118,14 @@ class FileReader extends EventTarget {
}

const readId = this._startRead('readAsArrayBuffer');
this._blob = blob;

NativeFileReaderModule.readAsDataURL(blob.data).then(
(text: string) => {
if (readId !== this._readId) {
return;
}
this._blob = null;

const base64 = text.split(',')[1];
const typedArray = toByteArray(base64);
Expand All @@ -127,6 +137,7 @@ class FileReader extends EventTarget {
if (readId !== this._readId) {
return;
}
this._blob = null;
this._error = this._toDOMException(error);
this._setReadyState(DONE);
},
Expand All @@ -141,19 +152,22 @@ class FileReader extends EventTarget {
}

const readId = this._startRead('readAsDataURL');
this._blob = blob;

NativeFileReaderModule.readAsDataURL(blob.data).then(
(text: string) => {
if (readId !== this._readId) {
return;
}
this._blob = null;
this._result = text;
this._setReadyState(DONE);
},
error => {
if (readId !== this._readId) {
return;
}
this._blob = null;
this._error = this._toDOMException(error);
this._setReadyState(DONE);
},
Expand All @@ -168,19 +182,22 @@ class FileReader extends EventTarget {
}

const readId = this._startRead('readAsText');
this._blob = blob;

NativeFileReaderModule.readAsText(blob.data, encoding).then(
(text: string) => {
if (readId !== this._readId) {
return;
}
this._blob = null;
this._result = text;
this._setReadyState(DONE);
},
error => {
if (readId !== this._readId) {
return;
}
this._blob = null;
this._error = this._toDOMException(error);
this._setReadyState(DONE);
},
Expand All @@ -192,6 +209,10 @@ class FileReader extends EventTarget {
if (this._readyState === LOADING) {
this._aborted = true;
this._readId++;
// The abandoned read's callbacks bail out on the readId check without
// clearing _blob, so release it here — before dispatching the abort
// event, whose handler may start a new read that sets _blob again.
this._blob = null;
this._setReadyState(DONE);
}
}
Expand Down
104 changes: 104 additions & 0 deletions packages/react-native/Libraries/Blob/__tests__/FileReader-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -275,4 +275,108 @@ describe('FileReader', function () {
expect(() => reader.readAsText(null)).toThrow(TypeError);
expect(reader.readyState).toBe(FileReader.EMPTY);
});

it('should retain the blob until the read resolves', async () => {
let resolveRead: string => void = () => {};
const spy = jest
.spyOn(FileReaderModuleMock, 'readAsText')
.mockImplementation(
() =>
new Promise(resolve => {
resolveRead = resolve;
}),
);

const reader = new FileReader();
const blob = new Blob();
const loadend = new Promise<Event>(resolve => {
reader.onloadend = resolve;
});
reader.readAsText(blob);
// $FlowFixMe[prop-missing] - accessing private state for the test
expect(reader._blob).toBe(blob);

resolveRead('');
await loadend;
// $FlowFixMe[prop-missing] - accessing private state for the test
expect(reader._blob).toBe(null);

spy.mockRestore();
});

it('should release the blob when the read rejects', async () => {
let rejectRead: Error => void = () => {};
const spy = jest
.spyOn(FileReaderModuleMock, 'readAsText')
.mockImplementation(
() =>
new Promise((resolve, reject) => {
rejectRead = reject;
}),
);

const reader = new FileReader();
const blob = new Blob();
const loadend = new Promise<Event>(resolve => {
reader.onloadend = resolve;
});
reader.readAsText(blob);
// $FlowFixMe[prop-missing] - accessing private state for the test
expect(reader._blob).toBe(blob);

rejectRead(new Error('nope'));
await loadend;
// $FlowFixMe[prop-missing] - accessing private state for the test
expect(reader._blob).toBe(null);

spy.mockRestore();
});

it('should release the blob when a pending read is aborted', () => {
const spy = jest
.spyOn(FileReaderModuleMock, 'readAsText')
.mockImplementation(() => new Promise(() => {}));

const reader = new FileReader();
const blob = new Blob();
reader.readAsText(blob);
// $FlowFixMe[prop-missing] - accessing private state for the test
expect(reader._blob).toBe(blob);

reader.abort();
// $FlowFixMe[prop-missing] - accessing private state for the test
expect(reader._blob).toBe(null);

spy.mockRestore();
});

it('should keep retaining the new blob when a stale read settles after abort', async () => {
const resolvers: Array<(string) => void> = [];
const spy = jest
.spyOn(FileReaderModuleMock, 'readAsText')
.mockImplementation(
() =>
new Promise(resolve => {
resolvers.push(resolve);
}),
);

const reader = new FileReader();
const staleBlob = new Blob();
reader.readAsText(staleBlob);
reader.abort();

const newBlob = new Blob();
reader.readAsText(newBlob);
// $FlowFixMe[prop-missing] - accessing private state for the test
expect(reader._blob).toBe(newBlob);

// Settle the first (aborted) read; it must not drop the new blob.
resolvers[0]('');
await Promise.resolve();
// $FlowFixMe[prop-missing] - accessing private state for the test
expect(reader._blob).toBe(newBlob);

spy.mockRestore();
});
});