diff --git a/native/expo-modules/comm-expo-package/expo-module.config.json b/native/expo-modules/comm-expo-package/expo-module.config.json --- a/native/expo-modules/comm-expo-package/expo-module.config.json +++ b/native/expo-modules/comm-expo-package/expo-module.config.json @@ -1,7 +1,7 @@ { "platforms": ["ios", "android"], "ios": { - "modules": ["AESCryptoModule", "ThumbhashModule"] + "modules": ["AESCryptoModule", "BlobUtilsModule", "ThumbhashModule"] }, "android": { "modules": [ diff --git a/native/expo-modules/comm-expo-package/ios/AESCryptoModule.swift b/native/expo-modules/comm-expo-package/ios/AESCryptoModule.swift --- a/native/expo-modules/comm-expo-package/ios/AESCryptoModule.swift +++ b/native/expo-modules/comm-expo-package/ios/AESCryptoModule.swift @@ -66,19 +66,6 @@ plaintext.copyBytes(to: destination.rawBufferPtr()) } -// MARK: - Utilities - -extension TypedArray { - func data() -> Data { - Data(bytes: self.rawPointer, count: self.byteLength) - } - - func rawBufferPtr() -> UnsafeMutableRawBufferPointer { - UnsafeMutableRawBufferPointer(start: self.rawPointer, - count: self.byteLength) - } -} - // MARK: - Exception definitions private class InvalidKeyLengthException: Exception { diff --git a/native/expo-modules/comm-expo-package/ios/BlobUtilsModule.swift b/native/expo-modules/comm-expo-package/ios/BlobUtilsModule.swift new file mode 100644 --- /dev/null +++ b/native/expo-modules/comm-expo-package/ios/BlobUtilsModule.swift @@ -0,0 +1,70 @@ +import ExpoModulesCore + +// This type corresponds to the BlobData interface in react-native +struct BlobMetadata: Record { + @Field var blobId: String + @Field var size: Int + @Field var offset: Int +} + +public class BlobUtilsModule: Module { + public func definition() -> ModuleDefinition { + Name("BlobUtils") + + Function("copyBlobToTypedArray") { + (blob: BlobMetadata, destination: Uint8Array) throws in + let blobManager = try self.getReactBlobManager() + guard let blobData = blobManager.resolve(blob.blobId, + offset: blob.offset, + size: blob.size) else { + throw NoSuchBlobException(blob.blobId) + } + blobData.copyBytes(to: destination.rawBufferPtr()) + } + + Function("blobFromTypedArray") { (source: TypedArray) throws -> String in + let blobManager = try self.getReactBlobManager() + guard let blobID = blobManager.store(source.data()) else { + throw BlobCreationFailedException() + } + return blobID + } + } + + private func getReactBlobManager() throws -> RCTBlobManager { + guard let bridge = self.appContext?.reactBridge else { + throw BridgeNotFoundException() + } + guard let blobManager = bridge.module(for: RCTBlobManager.self) + as? RCTBlobManager else { + throw BlobManagerNotFoundException() + } + return blobManager + } +} + +// MARK: Exception definitions + +class BridgeNotFoundException: Exception { + override var reason: String { + "React bridge is null" + } +} + +class BlobManagerNotFoundException: Exception { + override var reason: String { + "Module RCTBlobManager not found" + } +} + +class NoSuchBlobException: GenericException { + override var reason: String { + "No blob data found for blob id=\(param)" + } +} + +class BlobCreationFailedException: Exception { + override var reason: String { + "Failed to store blob" + } +} diff --git a/native/expo-modules/comm-expo-package/ios/TypedArray+data.swift b/native/expo-modules/comm-expo-package/ios/TypedArray+data.swift new file mode 100644 --- /dev/null +++ b/native/expo-modules/comm-expo-package/ios/TypedArray+data.swift @@ -0,0 +1,12 @@ +import ExpoModulesCore + +extension TypedArray { + func data() -> Data { + Data(bytes: self.rawPointer, count: self.byteLength) + } + + func rawBufferPtr() -> UnsafeMutableRawBufferPointer { + UnsafeMutableRawBufferPointer(start: self.rawPointer, + count: self.byteLength) + } +} diff --git a/native/utils/blob-utils-module.js b/native/utils/blob-utils-module.js new file mode 100644 --- /dev/null +++ b/native/utils/blob-utils-module.js @@ -0,0 +1,44 @@ +// @flow + +import { requireNativeModule } from 'expo-modules-core'; +import RNBlob from 'react-native/Libraries/Blob/Blob.js'; +import BlobManager from 'react-native/Libraries/Blob/BlobManager.js'; +import type { BlobData } from 'react-native/Libraries/Blob/BlobTypes.js'; + +const BlobUtilsModule: { + +copyBlobToTypedArray: (blob: BlobData, destination: Uint8Array) => void, + +blobFromTypedArray: (data: $TypedArray) => string, +} = requireNativeModule('BlobUtils'); + +function arrayBufferFromBlob(blob: Blob): ArrayBuffer { + // $FlowFixMe: react-native Blob type is incompatible with global Blob type + const rnBlob = (blob: RNBlob); + if (!(rnBlob instanceof RNBlob)) { + throw new Error( + 'Given blob is not a React Native blob. Missing "data" property.', + ); + } + const blobData = rnBlob.data; + + const resultArray = new Uint8Array(blob.size); + BlobUtilsModule.copyBlobToTypedArray(blobData, resultArray); + return resultArray.buffer; +} + +function blobFromArrayBuffer(arrayBuffer: ArrayBuffer, type?: string): Blob { + const typedArray = new Uint8Array(arrayBuffer); + const blobID = BlobUtilsModule.blobFromTypedArray(typedArray); + const reactNativeBlob = BlobManager.createFromOptions({ + blobId: blobID, + offset: 0, + size: arrayBuffer.byteLength, + type: type || '', + lastModified: Date.now(), + }); + + // $FlowFixMe: react-native Blob type is incompatible with global Blob type + // $FlowFixMe: even though they have the same properties + return (reactNativeBlob: Blob); +} + +export { arrayBufferFromBlob, blobFromArrayBuffer };