1use std::ops::RangeInclusive;
4
5use crate::stream_web::{
6 readable_byte_stream_controller_close_stream,
7 readable_byte_stream_controller_enqueue_bytes_borrowed, utils::promise::PromisePrimordials,
8 CancelAlgorithm, PullAlgorithm, ReadableStream, ReadableStreamControllerClass,
9};
10use crate::utils::{
11 array_buffer::shared_array_buffer_view,
12 bytes::{get_lossy_string, ObjectBytes},
13 object::not_a_object_error,
14 primordials::Primordial,
15 result::ResultExt,
16 string::get_coerced_defined_string,
17};
18use rquickjs::{
19 atom::PredefinedAtom, class::Trace, function::Opt, prelude::This, Array, ArrayBuffer, Class,
20 Coerced, Ctx, Exception, FromJs, IntoJs, JsIterator, Result, TypedArray, Value,
21};
22
23use super::file::File;
24
25struct ArrayPartsIter<'js> {
26 array: Array<'js>,
27 index: usize,
28}
29
30impl<'js> ArrayPartsIter<'js> {
31 fn new(array: Array<'js>) -> Self {
32 Self { array, index: 0 }
33 }
34}
35
36impl<'js> Iterator for ArrayPartsIter<'js> {
37 type Item = Result<Value<'js>>;
38
39 fn next(&mut self) -> Option<Self::Item> {
40 let len: usize = match self.array.as_object().get(PredefinedAtom::Length) {
41 Ok(v) => v,
42 Err(e) => return Some(Err(e)),
43 };
44 if self.index >= len {
45 return None;
46 }
47 let result = self.array.get(self.index);
48 self.index += 1;
49 Some(result)
50 }
51}
52
53enum EndingType {
54 Native,
55 Transparent,
56}
57
58#[cfg(windows)]
59const LINE_ENDING: &[u8] = b"\r\n";
60#[cfg(not(windows))]
61const LINE_ENDING: &[u8] = b"\n";
62
63#[rquickjs::class]
64#[derive(Trace, Clone, rquickjs::JsLifetime)]
65pub struct Blob<'js> {
66 data: ArrayBuffer<'js>,
69 mime_type: String,
70}
71
72fn normalize_type(mut mime_type: String) -> String {
73 static INVALID_RANGE: RangeInclusive<u8> = 0x0020..=0x007E;
74
75 let bytes = unsafe { mime_type.as_bytes_mut() };
76 for byte in bytes {
77 if !INVALID_RANGE.contains(byte) {
78 return String::new();
79 }
80 byte.make_ascii_lowercase();
81 }
82 mime_type
83}
84
85#[rquickjs::methods]
86impl<'js> Blob<'js> {
87 #[qjs(constructor)]
88 pub fn new(
89 ctx: Ctx<'js>,
90 this: This<Value<'js>>,
91 parts: Opt<Value<'js>>,
92 options: Opt<Value<'js>>,
93 ) -> Result<Self> {
94 if this.as_function().is_none() {
95 return Err(Exception::throw_type(
96 &ctx,
97 "Failed to construct 'Blob': Please use the 'new' operator",
98 ));
99 }
100
101 Self::from_parts(ctx, parts, options)
102 }
103
104 #[qjs(get)]
105 pub fn size(&self) -> usize {
106 self.data.len()
107 }
108
109 #[qjs(get, rename = "type")]
110 pub fn mime_type(&self) -> String {
111 self.mime_type.clone()
112 }
113
114 pub async fn text(&self) -> String {
115 String::from_utf8_lossy(self.as_bytes()).to_string()
116 }
117
118 #[qjs(rename = "arrayBuffer")]
119 pub async fn array_buffer(&self, ctx: Ctx<'js>) -> Result<ArrayBuffer<'js>> {
120 ArrayBuffer::new_copy(ctx, self.as_bytes())
122 }
123
124 pub async fn bytes(&self, ctx: Ctx<'js>) -> Result<Value<'js>> {
125 let ab = ArrayBuffer::new_copy(ctx, self.as_bytes())?;
127 TypedArray::<u8>::from_arraybuffer(ab).map(|t| t.into_value())
128 }
129
130 pub fn slice(
131 &self,
132 ctx: Ctx<'js>,
133 start: Opt<Value<'js>>,
134 end: Opt<Value<'js>>,
135 content_type: Opt<Value<'js>>,
136 ) -> Result<Blob<'js>> {
137 let start = start.0.and_then(|v| v.as_number()).map(clamp_long_long);
138 let end = end.0.and_then(|v| v.as_number()).map(clamp_long_long);
139 Self::slice_blob(self, &ctx, start, end, content_type.0)
140 }
141
142 pub fn stream(&self, ctx: Ctx<'js>) -> Result<Value<'js>> {
143 let data = self.get_bytes();
150 let pull = PullAlgorithm::from_fn_once(
151 move |ctx: Ctx<'js>, controller: ReadableStreamControllerClass<'js>| {
152 let ctrl = match controller {
153 ReadableStreamControllerClass::ReadableStreamByteController(c) => c,
154 _ => return Err(Exception::throw_type(&ctx, "Expected byte controller")),
155 };
156 let len = data.len();
157 if len != 0 {
158 let buffer = ArrayBuffer::new(ctx.clone(), data.clone())?;
159 let view = shared_array_buffer_view(&ctx, &buffer, 0, len)?;
160 readable_byte_stream_controller_enqueue_bytes_borrowed(
161 ctx.clone(),
162 ctrl.clone(),
163 view,
164 )?;
165 }
166 readable_byte_stream_controller_close_stream(ctx.clone(), ctrl)?;
167 Ok(PromisePrimordials::get(&ctx)?
168 .promise_resolved_with_undefined
169 .clone())
170 },
171 );
172 let stream = ReadableStream::from_byte_pull_algorithm(
175 ctx,
176 pull,
177 CancelAlgorithm::ReturnPromiseUndefined,
178 )?;
179 Ok(stream.into_value())
180 }
181
182 #[qjs(prop, rename = PredefinedAtom::SymbolToStringTag, configurable)]
183 pub fn to_string_tag() -> &'static str {
184 stringify!(Blob)
185 }
186
187 #[qjs(static, rename = PredefinedAtom::SymbolHasInstance)]
188 pub fn has_instance(value: Value<'js>) -> bool {
189 if let Some(obj) = value.as_object() {
190 return obj.instance_of::<Self>() || obj.instance_of::<File>();
191 }
192 false
193 }
194
195 #[qjs(skip)]
196 pub fn slice_blob(
197 &self,
198 ctx: &Ctx<'js>,
199 start: Option<isize>,
200 end: Option<isize>,
201 content_type: Option<Value<'js>>,
202 ) -> Result<Blob<'js>> {
203 let bytes = self.as_bytes();
204 let len = bytes.len();
205 let start = start.unwrap_or_default();
206 let start = if start < 0 {
207 (len as isize + start).max(0) as usize
208 } else {
209 len.min(start as usize)
210 };
211 let end = end.unwrap_or(len as isize);
212 let end = if end < 0 {
213 (len as isize + end).max(0) as usize
214 } else {
215 len.min(end as usize)
216 };
217 let data = shared_array_buffer_view(ctx, &self.data, start, end.saturating_sub(start))?;
218 let mime_type = get_coerced_defined_string(&content_type);
219 let mime_type = mime_type.map(normalize_type).unwrap_or_default();
220 Ok(Blob { mime_type, data })
221 }
222}
223
224impl<'js> Blob<'js> {
225 pub fn from_bytes(ctx: &Ctx<'js>, data: Vec<u8>, content_type: Option<String>) -> Result<Self> {
226 let mime_type = content_type.map(normalize_type).unwrap_or_default();
227 let data = ArrayBuffer::new(ctx.clone(), data)?;
228 Ok(Self { mime_type, data })
229 }
230
231 pub fn from_parts(
232 ctx: Ctx<'js>,
233 parts: Opt<Value<'js>>,
234 options: Opt<Value<'js>>,
235 ) -> Result<Self> {
236 if let Some(options) = options.0.as_ref() {
237 if !options.is_null() && !options.is_undefined() && options.as_object().is_none() {
238 return Err(not_a_object_error(&ctx, "options"));
239 }
240 }
241
242 let mut endings = EndingType::Transparent;
243 if let Some(options) = options.0.as_ref() {
244 if let Some(opts) = options.as_object() {
245 if opts.contains_key("endings")? {
246 if let Some(parsed) = parse_endings(&ctx, opts.get("endings")?)? {
247 endings = parsed;
248 }
249 }
250 }
251 }
252
253 let bytes = if let Some(parts) = parts.0 {
254 bytes_from_parts(&ctx, parts, endings)?
255 } else {
256 Vec::new()
257 };
258
259 let mut mime_type = String::new();
260 if let Some(options) = options.0.as_ref() {
261 if let Some(opts) = options.as_object() {
262 if let Some(x) = opts.get::<_, Option<Coerced<String>>>("type")? {
263 mime_type = normalize_type(x.to_string());
264 }
265 }
266 }
267
268 let data = ArrayBuffer::new(ctx, bytes)?;
271
272 Ok(Self { data, mime_type })
273 }
274
275 pub fn get_bytes(&self) -> Vec<u8> {
276 self.as_bytes().to_vec()
277 }
278
279 pub fn array_buffer_ref(&self) -> ArrayBuffer<'js> {
284 self.data.clone()
285 }
286
287 pub fn as_bytes(&self) -> &[u8] {
290 self.data.as_bytes().unwrap_or(&[])
291 }
292}
293
294fn bytes_from_parts<'js>(
295 ctx: &Ctx<'js>,
296 parts: Value<'js>,
297 endings: EndingType,
298) -> Result<Vec<u8>> {
299 if parts.is_undefined() {
300 return Ok(Vec::new());
301 }
302
303 if let Some(array) = parts.clone().into_array() {
304 return process_parts(ctx, ArrayPartsIter::new(array), endings);
305 }
306
307 process_parts(ctx, JsIterator::from_js(ctx, parts)?, endings)
308}
309
310fn process_parts<'js, I>(ctx: &Ctx<'js>, iter: I, endings: EndingType) -> Result<Vec<u8>>
311where
312 I: IntoIterator<Item = Result<Value<'js>>>,
313{
314 let mut data = Vec::new();
315 for elem in iter {
316 let elem = elem?;
317 if let Some(arr) = elem.as_array() {
318 let string = array_to_string(arr)?;
319 data.extend_from_slice(string.as_bytes());
320 continue;
321 }
322 if let Some(object) = elem.as_object() {
323 if let Some(x) = Class::<Blob>::from_object(object) {
324 data.extend_from_slice(x.borrow().as_bytes());
325 continue;
326 }
327 if let Some(x) = Class::<File>::from_object(object) {
328 let file = x.borrow();
329 let end = Some(file.size().try_into().or_throw(ctx)?);
330 let mime_type = Some(file.mime_type().into_js(ctx)?);
331 let sub = file.slice(ctx.clone(), Opt(Some(0)), Opt(end), Opt(mime_type))?;
332 data.extend_from_slice(sub.as_bytes());
333 continue;
334 }
335 if let Ok(x) = ObjectBytes::from(ctx, object) {
336 data.extend_from_slice(x.as_bytes(ctx).map_err(|_| {
337 Exception::throw_type(ctx, "Cannot create a blob with detached buffer")
338 })?);
339 continue;
340 }
341 if let Some(x) = ArrayBuffer::from_object(object.clone()) {
342 data.extend_from_slice(x.as_bytes().ok_or_else(|| {
343 Exception::throw_type(ctx, "Cannot create a blob with detached buffer")
344 })?);
345 continue;
346 }
347 }
348
349 let string = if elem.is_string() {
350 get_lossy_string(elem)?
351 } else {
352 Coerced::<String>::from_js(ctx, elem)?.0
353 };
354 if let EndingType::Transparent = endings {
355 data.extend_from_slice(string.as_bytes());
356 } else {
357 let len = string.len();
358 data.reserve(len);
359
360 let bytes = string.as_bytes();
361 let mut iter = bytes.iter();
362
363 let mut start = 0usize;
364 let mut i = 0usize;
365 let line_ending_is_n = LINE_ENDING[0] == b'\n';
366
367 while let Some(byte) = iter.next() {
368 if byte == &b'\r' {
369 if let Some(next_byte) = iter.next() {
370 data.extend(&bytes[start..i]);
371 i += 1;
372 start = i + 1;
373 if next_byte != &b'\n' {
374 data.extend([b'\r', *next_byte]);
375 } else {
376 data.extend(LINE_ENDING);
377 }
378 }
379 } else if byte == &b'\n' && !line_ending_is_n {
380 data.extend(&bytes[start..i]);
381 data.extend(LINE_ENDING);
382 start = i + 1;
383 };
384 i += 1;
385 }
386
387 if start < len {
388 data.extend(&bytes[start..len]);
389 }
390 }
391 }
392 Ok(data)
393}
394
395fn parse_endings<'js>(ctx: &Ctx<'js>, value: Value<'js>) -> Result<Option<EndingType>> {
396 if value.is_undefined() {
397 return Ok(None);
398 }
399 let endings = match Coerced::<String>::from_js(ctx, value)?.0.as_str() {
400 "transparent" => Some(EndingType::Transparent),
401 "native" => Some(EndingType::Native),
402 _ => {
403 return Err(Exception::throw_type(
404 ctx,
405 r#"expected 'endings' to be either 'transparent' or 'native'"#,
406 ));
407 },
408 };
409 Ok(endings)
410}
411
412fn array_to_string(array: &Array) -> Result<String> {
413 let mut itoa_buffer = itoa::Buffer::new();
414 let mut ryu_buffer = ryu::Buffer::new();
415
416 let parts = array
417 .clone()
418 .into_iter()
419 .map(|value| {
420 let value = value?;
421 if let Some(string) = value.as_string() {
422 Ok(string.to_string()?)
423 } else if let Some(number) = value.as_int() {
424 Ok(itoa_buffer.format(number).to_string())
425 } else if let Some(number) = value.as_float() {
426 Ok(ryu_buffer.format(number).to_string())
427 } else {
428 Ok(String::new())
429 }
430 })
431 .collect::<Result<Vec<_>>>()?;
432
433 Ok(parts.join(","))
434}
435
436fn clamp_long_long(value: f64) -> isize {
437 if value.is_nan() {
438 return 0;
439 }
440 let rounded = value.round_ties_even();
441 rounded.clamp(isize::MIN as f64, isize::MAX as f64) as isize
442}