1use js_sys::{Array, Reflect};
5use wasm_bindgen::JsValue;
6
7use crate::error::LibfwError;
8use libfw_core::metadata::{etag_from_size_mtime, FileMeta, TransferPlan};
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct FileEntry {
13 pub path: String,
15 pub size: u64,
17 pub mtime: u64,
19}
20
21impl FileEntry {
22 pub fn to_meta(&self) -> FileMeta {
24 FileMeta {
25 path: self.path.clone(),
26 size: self.size,
27 mtime: self.mtime,
28 etag: etag_from_size_mtime(self.size, self.mtime),
29 }
30 }
31
32 pub fn plan(&self, chunk_size: u64) -> TransferPlan {
34 TransferPlan::with_chunk_size(self.to_meta(), chunk_size)
35 }
36}
37
38pub fn parse_file_entries(value: &JsValue) -> Result<Vec<FileEntry>, LibfwError> {
40 let arr = Array::from(value);
41 let mut out = Vec::with_capacity(arr.length() as usize);
42 for item in arr.iter() {
43 let path = Reflect::get(&item, &JsValue::from_str("path"))
44 .map_err(|e| LibfwError::Js(format!("missing `path`: {e:?}")))?
45 .as_string()
46 .ok_or_else(|| LibfwError::Js("`path` must be a string".into()))?;
47 let size = Reflect::get(&item, &JsValue::from_str("size"))
48 .ok()
49 .and_then(|v| v.as_f64())
50 .unwrap_or(0.0) as u64;
51 let mtime = Reflect::get(&item, &JsValue::from_str("mtime"))
52 .ok()
53 .and_then(|v| v.as_f64())
54 .unwrap_or(0.0) as u64;
55 out.push(FileEntry { path, size, mtime });
56 }
57 Ok(out)
58}
59
60pub fn total_bytes(files: &[FileEntry]) -> u64 {
62 files.iter().map(|f| f.size).sum()
63}
64
65#[allow(dead_code)]
71pub fn chunk_bounds(file: &FileEntry, chunk_size: u64, from: u64) -> Vec<(u64, u64)> {
72 let mut bounds = Vec::new();
73 let mut offset = from.min(file.size);
74 while offset < file.size {
75 let end = (offset + chunk_size).min(file.size);
76 bounds.push((offset, end));
77 offset = end;
78 }
79 bounds
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85
86 #[test]
87 fn chunk_bounds_cover_file_from_resume() {
88 let f = FileEntry {
89 path: "a.bin".into(),
90 size: 10,
91 mtime: 1,
92 };
93 let bounds = chunk_bounds(&f, 4, 0);
94 assert_eq!(bounds, vec![(0, 4), (4, 8), (8, 10)]);
95
96 let resumed = chunk_bounds(&f, 4, 4);
97 assert_eq!(resumed, vec![(4, 8), (8, 10)]);
98
99 let done = chunk_bounds(&f, 4, 10);
100 assert!(done.is_empty());
101 }
102
103 #[test]
104 fn plan_meta_has_etag() {
105 let f = FileEntry {
106 path: "x".into(),
107 size: 5,
108 mtime: 42,
109 };
110 let plan = f.plan(4);
111 assert_eq!(plan.chunks.len(), 2);
112 assert!(!plan.file.etag.is_empty());
113 assert_eq!(plan.total_bytes(), 5);
114 }
115
116 #[test]
117 #[cfg(target_arch = "wasm32")]
118 fn parses_js_file_entries() {
119 let arr = Array::new();
120 let a = js_sys::Object::new();
121 js_sys::Reflect::set(&a, &JsValue::from_str("path"), &JsValue::from_str("d/f.txt"))
122 .unwrap();
123 js_sys::Reflect::set(&a, &JsValue::from_str("size"), &JsValue::from_f64(12.0)).unwrap();
124 arr.push(&a);
125 let entries = parse_file_entries(&arr.into()).unwrap();
126 assert_eq!(entries.len(), 1);
127 assert_eq!(entries[0].path, "d/f.txt");
128 assert_eq!(entries[0].size, 12);
129 }
130}