|
| 1 | +#include "File.h" |
| 2 | +#include "FileReader.h" |
| 3 | + |
| 4 | +#include <Babylon/Polyfills/File.h> |
| 5 | + |
| 6 | +#include <chrono> |
| 7 | +#include <string> |
| 8 | + |
| 9 | +namespace Babylon::Polyfills::Internal |
| 10 | +{ |
| 11 | + namespace |
| 12 | + { |
| 13 | + constexpr auto JS_FILE_CONSTRUCTOR_NAME = "File"; |
| 14 | + constexpr auto JS_BLOB_CONSTRUCTOR_NAME = "Blob"; |
| 15 | + } |
| 16 | + |
| 17 | + void File::Initialize(Napi::Env env) |
| 18 | + { |
| 19 | + auto global = env.Global(); |
| 20 | + |
| 21 | + // No-op if the runtime already provides a global File. Cheapest |
| 22 | + // check, and the common path on platforms with a native File. |
| 23 | + if (!global.Get(JS_FILE_CONSTRUCTOR_NAME).IsUndefined()) |
| 24 | + { |
| 25 | + return; |
| 26 | + } |
| 27 | + |
| 28 | + // Require the native Blob polyfill: File delegates byte storage to |
| 29 | + // a Blob, so without it the constructor cannot produce useful |
| 30 | + // instances. Use IsUndefined() rather than IsFunction() because |
| 31 | + // some JavaScriptCore builds (notably libjavascriptcoregtk on |
| 32 | + // Linux) classify constructors created via JSObjectMakeConstructor |
| 33 | + // as typeof 'object', not 'function', so napi_typeof returns |
| 34 | + // napi_object for them. |
| 35 | + auto blob = global.Get(JS_BLOB_CONSTRUCTOR_NAME); |
| 36 | + if (blob.IsUndefined() || blob.IsNull()) |
| 37 | + { |
| 38 | + throw Napi::Error::New(env, |
| 39 | + "File polyfill requires the Blob polyfill to be installed first."); |
| 40 | + } |
| 41 | + |
| 42 | + Napi::Function func = DefineClass( |
| 43 | + env, |
| 44 | + JS_FILE_CONSTRUCTOR_NAME, |
| 45 | + { |
| 46 | + InstanceAccessor("size", &File::GetSize, nullptr), |
| 47 | + InstanceAccessor("type", &File::GetType, nullptr), |
| 48 | + InstanceAccessor("name", &File::GetName, nullptr), |
| 49 | + InstanceAccessor("lastModified", &File::GetLastModified, nullptr), |
| 50 | + InstanceMethod("arrayBuffer", &File::ArrayBuffer), |
| 51 | + InstanceMethod("text", &File::Text), |
| 52 | + InstanceMethod("bytes", &File::Bytes), |
| 53 | + }); |
| 54 | + |
| 55 | + global.Set(JS_FILE_CONSTRUCTOR_NAME, func); |
| 56 | + |
| 57 | + // Wire File.prototype's [[Prototype]] to Blob.prototype so |
| 58 | + // `new File(...) instanceof Blob === true`. WHATWG specs File as |
| 59 | + // a Blob subtype; BJS core (fileTools, Offline/database, |
| 60 | + // abstractEngine, thinNativeEngine) branches on `instanceof Blob` |
| 61 | + // and needs File inputs to satisfy that check. |
| 62 | + auto setPrototypeOf = env.Global().Get("Object").As<Napi::Object>() |
| 63 | + .Get("setPrototypeOf").As<Napi::Function>(); |
| 64 | + setPrototypeOf.Call({ |
| 65 | + func.Get("prototype"), |
| 66 | + blob.As<Napi::Function>().Get("prototype"), |
| 67 | + }); |
| 68 | + } |
| 69 | + |
| 70 | + File::File(const Napi::CallbackInfo& info) |
| 71 | + : Napi::ObjectWrap<File>{info} |
| 72 | + { |
| 73 | + auto env = info.Env(); |
| 74 | + |
| 75 | + // The WHATWG File constructor takes (fileBits, fileName, [options]). |
| 76 | + // Both fileBits and fileName are required (USVString without |
| 77 | + // `optional`), so missing either is a TypeError per WebIDL bindings. |
| 78 | + if (info.Length() < 2) |
| 79 | + { |
| 80 | + throw Napi::TypeError::New(env, |
| 81 | + "Failed to construct 'File': 2 arguments required, but only " + |
| 82 | + std::to_string(info.Length()) + " present."); |
| 83 | + } |
| 84 | + |
| 85 | + Napi::Value parts = info[0]; |
| 86 | + Napi::Value name = info[1]; |
| 87 | + Napi::Value options = info.Length() > 2 ? info[2] : env.Undefined(); |
| 88 | + |
| 89 | + // USVString conversion: undefined -> "undefined", null -> "null", |
| 90 | + // numbers/objects -> their .toString() representation. Napi::Value:: |
| 91 | + // ToString() routes through napi_coerce_to_string, which matches |
| 92 | + // these semantics on all three engines. |
| 93 | + m_name = name.ToString().Utf8Value(); |
| 94 | + |
| 95 | + // Default lastModified to the current wall clock in milliseconds, |
| 96 | + // matching Date.now() semantics used by the JS File constructor. |
| 97 | + m_lastModified = static_cast<double>( |
| 98 | + std::chrono::duration_cast<std::chrono::milliseconds>( |
| 99 | + std::chrono::system_clock::now().time_since_epoch()) |
| 100 | + .count()); |
| 101 | + |
| 102 | + auto blobOptions = Napi::Object::New(env); |
| 103 | + |
| 104 | + if (options.IsObject()) |
| 105 | + { |
| 106 | + auto optsObj = options.As<Napi::Object>(); |
| 107 | + if (optsObj.Has("type")) |
| 108 | + { |
| 109 | + blobOptions.Set("type", optsObj.Get("type")); |
| 110 | + } |
| 111 | + if (optsObj.Has("lastModified")) |
| 112 | + { |
| 113 | + auto lm = optsObj.Get("lastModified"); |
| 114 | + if (lm.IsNumber()) |
| 115 | + { |
| 116 | + m_lastModified = lm.As<Napi::Number>().DoubleValue(); |
| 117 | + } |
| 118 | + } |
| 119 | + } |
| 120 | + |
| 121 | + Napi::Value partsArray; |
| 122 | + if (parts.IsArray()) |
| 123 | + { |
| 124 | + partsArray = parts; |
| 125 | + } |
| 126 | + else |
| 127 | + { |
| 128 | + partsArray = Napi::Array::New(env, 0); |
| 129 | + } |
| 130 | + |
| 131 | + // Delegate byte-buffer construction to the native Blob polyfill so |
| 132 | + // we benefit from its existing BlobPart handling (ArrayBuffer, |
| 133 | + // typed array, string, Blob). |
| 134 | + auto blobCtor = env.Global().Get(JS_BLOB_CONSTRUCTOR_NAME).As<Napi::Function>(); |
| 135 | + auto blobInstance = blobCtor.New({partsArray, blobOptions}); |
| 136 | + m_blob = Napi::Persistent(blobInstance); |
| 137 | + } |
| 138 | + |
| 139 | + Napi::Value File::GetSize(const Napi::CallbackInfo&) |
| 140 | + { |
| 141 | + return m_blob.Value().Get("size"); |
| 142 | + } |
| 143 | + |
| 144 | + Napi::Value File::GetType(const Napi::CallbackInfo&) |
| 145 | + { |
| 146 | + return m_blob.Value().Get("type"); |
| 147 | + } |
| 148 | + |
| 149 | + Napi::Value File::GetName(const Napi::CallbackInfo& info) |
| 150 | + { |
| 151 | + return Napi::String::New(info.Env(), m_name); |
| 152 | + } |
| 153 | + |
| 154 | + Napi::Value File::GetLastModified(const Napi::CallbackInfo& info) |
| 155 | + { |
| 156 | + return Napi::Number::New(info.Env(), m_lastModified); |
| 157 | + } |
| 158 | + |
| 159 | + Napi::Value File::ArrayBuffer(const Napi::CallbackInfo&) |
| 160 | + { |
| 161 | + auto blob = m_blob.Value(); |
| 162 | + return blob.Get("arrayBuffer").As<Napi::Function>().Call(blob, {}); |
| 163 | + } |
| 164 | + |
| 165 | + Napi::Value File::Text(const Napi::CallbackInfo&) |
| 166 | + { |
| 167 | + auto blob = m_blob.Value(); |
| 168 | + return blob.Get("text").As<Napi::Function>().Call(blob, {}); |
| 169 | + } |
| 170 | + |
| 171 | + Napi::Value File::Bytes(const Napi::CallbackInfo&) |
| 172 | + { |
| 173 | + auto blob = m_blob.Value(); |
| 174 | + return blob.Get("bytes").As<Napi::Function>().Call(blob, {}); |
| 175 | + } |
| 176 | +} |
| 177 | + |
| 178 | +namespace Babylon::Polyfills::File |
| 179 | +{ |
| 180 | + void BABYLON_API Initialize(Napi::Env env) |
| 181 | + { |
| 182 | + Internal::File::Initialize(env); |
| 183 | + Internal::FileReader::Initialize(env); |
| 184 | + } |
| 185 | +} |
0 commit comments