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] {
292 unsafe { self.data.as_bytes() }.unwrap_or(&[])
293 }
294}
295
296fn bytes_from_parts<'js>(
297 ctx: &Ctx<'js>,
298 parts: Value<'js>,
299 endings: EndingType,
300) -> Result<Vec<u8>> {
301 if parts.is_undefined() {
302 return Ok(Vec::new());
303 }
304
305 if let Some(array) = parts.clone().into_array() {
306 return process_parts(ctx, ArrayPartsIter::new(array), endings);
307 }
308
309 process_parts(ctx, JsIterator::from_js(ctx, parts)?, endings)
310}
311
312fn process_parts<'js, I>(ctx: &Ctx<'js>, iter: I, endings: EndingType) -> Result<Vec<u8>>
313where
314 I: IntoIterator<Item = Result<Value<'js>>>,
315{
316 let mut data = Vec::new();
317 for elem in iter {
318 let elem = elem?;
319 if let Some(arr) = elem.as_array() {
320 let string = array_to_string(arr)?;
321 data.extend_from_slice(string.as_bytes());
322 continue;
323 }
324 if let Some(object) = elem.as_object() {
325 if let Some(x) = Class::<Blob>::from_object(object) {
326 data.extend_from_slice(x.borrow().as_bytes());
327 continue;
328 }
329 if let Some(x) = Class::<File>::from_object(object) {
330 let file = x.borrow();
331 let end = Some(file.size().try_into().or_throw(ctx)?);
332 let mime_type = Some(file.mime_type().into_js(ctx)?);
333 let sub = file.slice(ctx.clone(), Opt(Some(0)), Opt(end), Opt(mime_type))?;
334 data.extend_from_slice(sub.as_bytes());
335 continue;
336 }
337 if let Ok(x) = ObjectBytes::from(ctx, object) {
338 data.extend_from_slice(x.as_bytes(ctx).map_err(|_| {
339 Exception::throw_type(ctx, "Cannot create a blob with detached buffer")
340 })?);
341 continue;
342 }
343 if let Some(x) = ArrayBuffer::from_object(object.clone()) {
344 data.extend_from_slice(unsafe { x.as_bytes() }.ok_or_else(|| {
346 Exception::throw_type(ctx, "Cannot create a blob with detached buffer")
347 })?);
348 continue;
349 }
350 }
351
352 let string = if elem.is_string() {
353 get_lossy_string(elem)?
354 } else {
355 Coerced::<String>::from_js(ctx, elem)?.0
356 };
357 if let EndingType::Transparent = endings {
358 data.extend_from_slice(string.as_bytes());
359 } else {
360 let len = string.len();
361 data.reserve(len);
362
363 let bytes = string.as_bytes();
364 let mut iter = bytes.iter();
365
366 let mut start = 0usize;
367 let mut i = 0usize;
368 let line_ending_is_n = LINE_ENDING[0] == b'\n';
369
370 while let Some(byte) = iter.next() {
371 if byte == &b'\r' {
372 if let Some(next_byte) = iter.next() {
373 data.extend(&bytes[start..i]);
374 i += 1;
375 start = i + 1;
376 if next_byte != &b'\n' {
377 data.extend([b'\r', *next_byte]);
378 } else {
379 data.extend(LINE_ENDING);
380 }
381 }
382 } else if byte == &b'\n' && !line_ending_is_n {
383 data.extend(&bytes[start..i]);
384 data.extend(LINE_ENDING);
385 start = i + 1;
386 };
387 i += 1;
388 }
389
390 if start < len {
391 data.extend(&bytes[start..len]);
392 }
393 }
394 }
395 Ok(data)
396}
397
398fn parse_endings<'js>(ctx: &Ctx<'js>, value: Value<'js>) -> Result<Option<EndingType>> {
399 if value.is_undefined() {
400 return Ok(None);
401 }
402 let endings = match Coerced::<String>::from_js(ctx, value)?.0.as_str() {
403 "transparent" => Some(EndingType::Transparent),
404 "native" => Some(EndingType::Native),
405 _ => {
406 return Err(Exception::throw_type(
407 ctx,
408 r#"expected 'endings' to be either 'transparent' or 'native'"#,
409 ));
410 },
411 };
412 Ok(endings)
413}
414
415fn array_to_string(array: &Array) -> Result<String> {
416 let mut itoa_buffer = itoa::Buffer::new();
417 let mut ryu_buffer = ryu::Buffer::new();
418
419 let parts = array
420 .clone()
421 .into_iter()
422 .map(|value| {
423 let value = value?;
424 if let Some(string) = value.as_string() {
425 Ok(string.to_string()?)
426 } else if let Some(number) = value.as_int() {
427 Ok(itoa_buffer.format(number).to_string())
428 } else if let Some(number) = value.as_float() {
429 Ok(ryu_buffer.format(number).to_string())
430 } else {
431 Ok(String::new())
432 }
433 })
434 .collect::<Result<Vec<_>>>()?;
435
436 Ok(parts.join(","))
437}
438
439fn clamp_long_long(value: f64) -> isize {
440 if value.is_nan() {
441 return 0;
442 }
443 let rounded = value.round_ties_even();
444 rounded.clamp(isize::MIN as f64, isize::MAX as f64) as isize
445}