mirror of
https://git.ethanthesleepy.one/ethanaobrien/ew
synced 2026-10-12 01:24:27 +08:00
Some design fixes in custom data handling
This commit is contained in:
@@ -41,11 +41,40 @@ pub fn build(music_id: i64) -> Result<Vec<u8>, String> {
|
||||
Ok(zip.finish().map_err(|e| e.to_string())?.into_inner())
|
||||
}
|
||||
|
||||
fn read_entry<R: Read + Seek>(archive: &mut zip::ZipArchive<R>, name: &str) -> Option<Vec<u8>> {
|
||||
let mut file = archive.by_name(name).ok()?;
|
||||
// Reads one entry, capped. Deflate's ceiling is about 1032:1, so an uncapped
|
||||
// read_to_end here is a zip bomb: a one-megabyte entry inflates to a gigabyte and
|
||||
// grows the Vec until the allocator or the OOM killer stops it. Every entry is
|
||||
// bounded by the upload form's own per-file cap, and by what is left of the
|
||||
// per-request budget across all entries.
|
||||
//
|
||||
// The central directory's declared size rejects the obvious case without
|
||||
// inflating anything; take(cap + 1) makes that declaration untrusted - a lying
|
||||
// header runs out of budget one byte past the cap and stops there.
|
||||
//
|
||||
// Ok(None) is "the package does not carry this entry", which is a normal outcome
|
||||
// for the optional ones
|
||||
fn read_entry<R: Read + Seek>(archive: &mut zip::ZipArchive<R>, name: &str, remaining: &mut usize) -> Result<Option<Vec<u8>>, String> {
|
||||
let Ok(mut file) = archive.by_name(name) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let cap = std::cmp::min(super::MAX_FILE_BYTES, *remaining);
|
||||
// Which limit the entry actually ran into, so the message names the right one
|
||||
let too_big = if cap >= super::MAX_FILE_BYTES {
|
||||
super::over_file_limit(name)
|
||||
} else {
|
||||
super::over_request_limit()
|
||||
};
|
||||
if file.size() > cap as u64 {
|
||||
return Err(too_big);
|
||||
}
|
||||
let mut bytes = Vec::new();
|
||||
file.read_to_end(&mut bytes).ok()?;
|
||||
Some(bytes)
|
||||
file.by_ref().take(cap as u64 + 1).read_to_end(&mut bytes)
|
||||
.map_err(|_| format!("Package entry '{}' could not be read", name))?;
|
||||
if bytes.len() > cap {
|
||||
return Err(too_big);
|
||||
}
|
||||
*remaining -= bytes.len();
|
||||
Ok(Some(bytes))
|
||||
}
|
||||
|
||||
// Expands a package into the same field map the upload form produces - the zip
|
||||
@@ -55,8 +84,10 @@ fn read_entry<R: Read + Seek>(archive: &mut zip::ZipArchive<R>, name: &str) -> O
|
||||
// visibility/shared_with/downloads_disabled aren't packaged and stay untouched
|
||||
pub fn expand(package: &[u8], fields: &mut HashMap<String, Vec<u8>>) -> Result<(), String> {
|
||||
let mut archive = zip::ZipArchive::new(Cursor::new(package)).map_err(|_| String::from("Package is not a valid zip file"))?;
|
||||
// The decompressed budget for the whole package, shared by every entry
|
||||
let mut remaining = super::MAX_REQUEST_BYTES;
|
||||
|
||||
let manifest = read_entry(&mut archive, "manifest.json").ok_or(String::from("Package has no manifest.json"))?;
|
||||
let manifest = read_entry(&mut archive, "manifest.json", &mut remaining)?.ok_or(String::from("Package has no manifest.json"))?;
|
||||
let manifest = jzon::parse(&String::from_utf8_lossy(&manifest)).map_err(|_| String::from("Package manifest is not valid JSON"))?;
|
||||
if manifest["format"].as_i64() != Some(1) {
|
||||
return Err(String::from("Unsupported package format"));
|
||||
@@ -74,11 +105,11 @@ pub fn expand(package: &[u8], fields: &mut HashMap<String, Vec<u8>>) -> Result<(
|
||||
}
|
||||
}
|
||||
|
||||
fields.insert(String::from("jacket"), read_entry(&mut archive, "jacket").ok_or(String::from("Package has no jacket"))?);
|
||||
fields.insert(String::from("audio"), read_entry(&mut archive, "audio").ok_or(String::from("Package has no audio"))?);
|
||||
fields.insert(String::from("jacket"), read_entry(&mut archive, "jacket", &mut remaining)?.ok_or(String::from("Package has no jacket"))?);
|
||||
fields.insert(String::from("audio"), read_entry(&mut archive, "audio", &mut remaining)?.ok_or(String::from("Package has no audio"))?);
|
||||
let mut has_chart = false;
|
||||
for level in 1..=LEVEL_COUNT {
|
||||
if let Some(chart) = read_entry(&mut archive, &format!("chart_{}.json", level)) {
|
||||
if let Some(chart) = read_entry(&mut archive, &format!("chart_{}.json", level), &mut remaining)? {
|
||||
fields.insert(format!("chart_{}", level), chart);
|
||||
has_chart = true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user