diff --git a/src/appview/ldraw.rs b/src/appview/ldraw.rs index a2f3c5c..2ea037e 100644 --- a/src/appview/ldraw.rs +++ b/src/appview/ldraw.rs @@ -16,6 +16,10 @@ use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use std::collections::HashSet; +const MAX_COMPOUND_DESCRIPTORS: usize = 16_384; +const MAX_COMPOUND_BYTES: u64 = 128 * 1024 * 1024; +const MAX_COMPOUND_METADATA_BYTES: u64 = 16 * 1024 * 1024; + fn verify_chunk_cid(cid_text: &str, bytes: &[u8]) -> Result<(), &'static str> { let cid = cid::Cid::try_from(cid_text).map_err(|_| "malformed CID")?; if cid.hash().code() != 0x12 { @@ -118,6 +122,8 @@ pub(super) async fn compound( let mut resources = Vec::new(); let mut seen_paths = HashSet::new(); let mut pending = vec![scoped.path.clone()]; + let mut total_bytes = 0_u64; + let mut total_metadata = 0_u64; while let Some(path) = pending.pop() { let resource = ensure_verified(&state, &resolver, Some(&principal), &request.project, &path) @@ -129,9 +135,40 @@ pub(super) async fn compound( if !seen_paths.insert((resource.root, resource.path.clone())) { continue; } - if resources.len() >= 16_384 { + if resources.len() >= MAX_COMPOUND_DESCRIPTORS { return Err(invalid_request("compound LDraw descriptor bound exceeded")); } + let byte_length = u64::try_from(resource.byte_length) + .map_err(|_| invalid_request("compound LDraw byte length is invalid"))?; + total_bytes = total_bytes + .checked_add(byte_length) + .ok_or_else(|| invalid_request("compound LDraw byte bound overflow"))?; + if total_bytes > MAX_COMPOUND_BYTES { + return Err(invalid_request("compound LDraw byte bound exceeded")); + } + let metadata = resource + .path + .as_str() + .len() + .saturating_add(resource.root.as_str().len()) + .saturating_add(resource.mime_type.len()) + .saturating_add( + resource + .target + .blob_cids + .iter() + .map(String::len) + .sum::(), + ); + total_metadata = total_metadata + .checked_add( + u64::try_from(metadata) + .map_err(|_| invalid_request("compound LDraw metadata bound overflow"))?, + ) + .ok_or_else(|| invalid_request("compound LDraw metadata bound overflow"))?; + if total_metadata > MAX_COMPOUND_METADATA_BYTES { + return Err(invalid_request("compound LDraw metadata bound exceeded")); + } if resource.root == crate::ldraw::RootId::Mpd { let bytes = fetch_verified_bytes(&state, &resource).await?; let paths = collect_mpd_type1_paths(&bytes).map_err(|_| not_found())?; @@ -143,6 +180,9 @@ pub(super) async fn compound( ).fetch_all(&state.pool).await)?; for row in manifest_rows.into_iter().rev() { let path = CanonicalPath::parse(&row.canonical_path).map_err(|_| not_found())?; + if !scoped.contains(&path, resource.root) { + return Err(not_found()); + } pending.push(path); } resources.push(resource); @@ -153,11 +193,15 @@ pub(super) async fn compound( .await .map_err(|error| internal(format!("LDraw route transaction begin failed: {error}")))?; let mut descriptors = Vec::with_capacity(resources.len()); - for resource in resources { - descriptors.push( + for (index, resource) in resources.into_iter().enumerate() { + let mut descriptor = issue_descriptor_in_transaction(&mut tx, &principal, &request.project, resource) - .await?, - ); + .await?; + if index == 0 && scoped.subroot.is_some() { + descriptor.key = scoped.canonical(); + descriptor.path = descriptor.key.clone(); + } + descriptors.push(descriptor); } tx.commit() .await