Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 24 additions & 4 deletions lp-app/lpa-fs-opfs/src/opfs_read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,13 @@ pub async fn load_tree_filtered(
}
FileSystemHandleKind::File => {
let file_handle: FileSystemFileHandle = handle.unchecked_into();
let bytes = read_file_bytes(&file_handle, &child_path).await?;
out.push((LpPathBuf::from(child_path.as_str()), bytes));
match read_file_bytes(&file_handle, &child_path).await? {
Some(bytes) => out.push((LpPathBuf::from(child_path.as_str()), bytes)),
None => log::warn!(
"opfs: skipping implausibly large file at {child_path} \
(> {MAX_FILE_BYTES} bytes; corrupt-store recovery)"
),
}
}
_ => {
log::warn!("opfs: unknown handle kind at {child_path}, skipping");
Expand Down Expand Up @@ -105,15 +110,30 @@ pub async fn list_child_dirs(dir: &FileSystemDirectoryHandle) -> Result<Vec<Stri
Ok(out)
}

async fn read_file_bytes(handle: &FileSystemFileHandle, path: &str) -> Result<Vec<u8>, OpfsError> {
/// Refuse to load files no legitimate package member can reach. Library
/// content is device-bound (JSON, GLSL, SVG — kilobytes); the only known
/// way to exceed this is store corruption (pre-fix WebKit wrote the whole
/// wasm heap into every file — see `opfs_write::write_file`). Reading such
/// a file into the memory-primary tree would OOM the tab all over again,
/// so the mount skips it instead.
const MAX_FILE_BYTES: f64 = 16.0 * 1024.0 * 1024.0;

/// `Ok(None)` when the file exceeds [`MAX_FILE_BYTES`].
async fn read_file_bytes(
handle: &FileSystemFileHandle,
path: &str,
) -> Result<Option<Vec<u8>>, OpfsError> {
let file = JsFuture::from(handle.get_file())
.await
.map_err(|e| OpfsError::new("get_file", path.to_string(), e))?;
let file: File = file
.dyn_into()
.map_err(|e| OpfsError::new("get_file", path.to_string(), e))?;
if file.size() > MAX_FILE_BYTES {
return Ok(None);
}
let buffer = JsFuture::from(file.array_buffer())
.await
.map_err(|e| OpfsError::new("array_buffer", path.to_string(), e))?;
Ok(Uint8Array::new(&buffer).to_vec())
Ok(Some(Uint8Array::new(&buffer).to_vec()))
}
9 changes: 8 additions & 1 deletion lp-app/lpa-fs-opfs/src/opfs_write.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,15 @@ async fn write_via_writable(
.dyn_into()
.map_err(|e| OpfsError::new("create_writable", path.as_str().to_string(), e))?;

// Copy into a JS-owned array before writing. `write_with_u8_array`
// passes a Uint8Array VIEW over wasm linear memory, and WebKit's
// `write()` ignores the view's offset/length and writes the entire
// underlying buffer (observed on WebKit 26.5; Chrome honors the view)
// — passing wasm memory directly dumps the whole wasm heap into every
// file. The copy's buffer is exactly `bytes`, so either reading works.
let copy = js_sys::Uint8Array::from(bytes);
let write_promise = stream
.write_with_u8_array(bytes)
.write_with_buffer_source(&copy)
.map_err(|e| OpfsError::new("write", path.as_str().to_string(), e))?;
JsFuture::from(write_promise)
.await
Expand Down
39 changes: 39 additions & 0 deletions lp-app/lpa-fs-opfs/tests/opfs_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,42 @@ async fn remove_missing_errors() {
let result = remove_path(&dir, LpPath::new("/nope.txt")).await;
assert!(result.is_err());
}

/// The write must persist exactly the bytes passed — no view-length
/// confusion. Guards the WebKit whole-buffer bug (`write()` on a wasm
/// memory view wrote the entire heap): with the JS-owned copy in
/// `write_file` this size is exact on every engine.
#[wasm_bindgen_test]
async fn write_persists_exact_length() {
let dir = fresh_test_dir("t-exact-length").await;
let payload = vec![0x42u8; 4096];
write_file(&dir, LpPath::new("/exact.bin"), &payload)
.await
.unwrap();
let tree = tree_map(load_tree(&dir).await.unwrap());
assert_eq!(tree["/exact.bin"].len(), payload.len());
assert_eq!(tree["/exact.bin"], payload);
}

/// Corrupt-store recovery: a file no legitimate package could contain
/// (pre-fix WebKit wrote the whole wasm heap into every file) is skipped
/// by the mount instead of being loaded into memory.
#[wasm_bindgen_test]
async fn load_tree_skips_implausibly_large_files() {
let dir = fresh_test_dir("t-oversize").await;
write_file(&dir, LpPath::new("/ok.json"), b"{}")
.await
.unwrap();
// 17 MiB of zeros — just over the 16 MiB cap.
let huge = vec![0u8; 17 * 1024 * 1024];
write_file(&dir, LpPath::new("/heap-dump.json"), &huge)
.await
.unwrap();

let tree = tree_map(load_tree(&dir).await.unwrap());
assert!(tree.contains_key("/ok.json"));
assert!(
!tree.contains_key("/heap-dump.json"),
"oversized file must be skipped, not loaded"
);
}
Loading