mirror of
https://github.com/storytold/deckcraft.git
synced 2026-11-06 18:39:31 -06:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ad81f944f | ||
|
|
d021e2baed | ||
|
|
bb0048e78d |
@@ -811,10 +811,32 @@ fn clamp_selection(st: &mut DocState) {
|
||||
/// Default for [`History::byte_limit`].
|
||||
pub const UNDO_BYTE_BUDGET: usize = 256 << 20;
|
||||
|
||||
/// A rough size of a text body in memory: its paragraphs and runs, at their inline struct sizes.
|
||||
fn text_cost(t: &deckcraft_model::text::TextBody) -> usize {
|
||||
use deckcraft_model::text::{Paragraph, Run};
|
||||
let (para, run) = (std::mem::size_of::<Paragraph>(), std::mem::size_of::<Run>());
|
||||
t.paragraphs.iter().map(|p| para + p.runs.iter().map(|r| run + r.text.len()).sum::<usize>()).sum()
|
||||
}
|
||||
|
||||
/// A rough size of comments in memory, replies included.
|
||||
fn comments_cost(cs: &[deckcraft_model::Comment], depth: usize) -> usize {
|
||||
let kids = |c: &deckcraft_model::Comment| if depth < 64 { comments_cost(&c.replies, depth + 1) } else { 0 };
|
||||
cs.iter().map(|c| 256 + c.author.len() + c.initials.len() + c.text.len() + c.date.len() + kids(c)).sum()
|
||||
}
|
||||
|
||||
/// A rough size of a slide's own data besides its shapes: notes, comments, tags and kept XML,
|
||||
/// all copied with the slide when an edit changes it.
|
||||
fn slide_extra_cost(s: &deckcraft_model::Slide) -> usize {
|
||||
text_cost(&s.notes)
|
||||
+ comments_cost(&s.comments, 0)
|
||||
+ s.name.len()
|
||||
+ s.tags.iter().map(|(k, v)| 48 + k.len() + v.len()).sum::<usize>()
|
||||
+ s.raw_ext.as_ref().map_or(0, String::len)
|
||||
}
|
||||
|
||||
/// A rough size of a shape in memory: the struct, its text and kept XML, its children.
|
||||
fn shape_cost(sh: &deckcraft_model::Shape, depth: usize) -> usize {
|
||||
let text: usize =
|
||||
sh.text.as_ref().map_or(0, |t| t.paragraphs.iter().map(|p| 96 + p.runs.iter().map(|r| 128 + r.text.len()).sum::<usize>()).sum());
|
||||
let text: usize = sh.text.as_ref().map_or(0, text_cost);
|
||||
let kids: usize = if depth < 64 { sh.children().iter().map(|c| shape_cost(c, depth + 1)).sum() } else { 0 };
|
||||
768 + text + sh.raw_ext.as_ref().map_or(0, String::len) + kids
|
||||
}
|
||||
@@ -832,7 +854,7 @@ pub fn undo_cost(doc: &Presentation, newer: &Presentation) -> usize {
|
||||
let media: HashSet<*const Vec<u8>> = newer.media.iter().map(|m| Arc::as_ptr(&m.data)).collect();
|
||||
let mut n = 64 * doc.slides.len();
|
||||
for s in doc.slides.iter().filter(|s| !slides.contains(&Arc::as_ptr(s))) {
|
||||
n += 512 + shapes_cost(&s.shapes);
|
||||
n += 512 + shapes_cost(&s.shapes) + slide_extra_cost(s);
|
||||
}
|
||||
for m in doc.masters.iter().filter(|m| !masters.contains(&Arc::as_ptr(m))) {
|
||||
n += 4096 + shapes_cost(&m.shapes) + m.layouts.iter().map(|l| 512 + shapes_cost(&l.shapes)).sum::<usize>();
|
||||
|
||||
@@ -3338,6 +3338,25 @@ fn undo_history_is_capped_by_memory_as_well_as_steps() {
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), 120);
|
||||
}
|
||||
|
||||
/// A slide's notes count toward the undo budget: small edits to a slide with large notes keep a
|
||||
/// copy of those notes per step, and must not slip past the budget as if the slide were empty.
|
||||
#[test]
|
||||
fn undo_history_counts_slide_notes() {
|
||||
let mut s = Session::with_new();
|
||||
let notes = "n".repeat(1 << 20);
|
||||
s.edit(|doc, _| {
|
||||
std::sync::Arc::make_mut(&mut doc.slides[0]).notes = deckcraft_model::text::TextBody::from_text(¬es);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
s.doc_mut().unwrap().history = History { limit: 500, byte_limit: 8 << 20, ..Default::default() };
|
||||
for i in 0..40 {
|
||||
s.execute("slide.rename", &json!({"name": format!("v{i}"), "index": 0})).unwrap();
|
||||
}
|
||||
let n = s.doc().unwrap().history.undo.len();
|
||||
assert!((1..=8).contains(&n), "{n} steps of 1 MB notes kept under an 8 MB budget");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn picture_styles_replace_each_other_in_one_step() {
|
||||
const PNG: &[u8] = &[
|
||||
|
||||
@@ -81,9 +81,7 @@ pub fn decode(bytes: &Bytes) -> Result<Pcm> {
|
||||
Some(r) => r,
|
||||
None => contained(|| decode_symphonia(bytes, "mkv")),
|
||||
},
|
||||
Container::Mp4 | Container::Unknown | Container::Mp3 if !crate::mp4guard::probe_fits(bytes) => {
|
||||
Err(MediaError::Corrupt("the MP4's sample tables do not fit the file".into()))
|
||||
}
|
||||
Container::Mp4 if !crate::mp4guard::tables_fit(bytes) => Err(MediaError::Corrupt("the MP4's sample tables do not fit the file".into())),
|
||||
c => contained(|| decode_symphonia(bytes, c.name())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,29 +12,6 @@ pub(crate) fn tables_fit(b: &[u8]) -> bool {
|
||||
boxes_fit(b, b.len() as u64, 0)
|
||||
}
|
||||
|
||||
/// Bytes symphonia's probe scans for a format marker before giving up.
|
||||
const PROBE_SEARCH_LIMIT: usize = 1024 * 1024;
|
||||
|
||||
/// Whether symphonia can be given `b`, whatever the sniffer made of it: its probe takes the first
|
||||
/// `ftyp` within its search window as the start of an MP4 (the size field 4 bytes before it), so
|
||||
/// the table check must start where that parser will, not at offset 0.
|
||||
pub(crate) fn probe_fits(b: &[u8]) -> bool {
|
||||
if crate::Container::sniff(b) == crate::Container::Mp4 && !tables_fit(b) {
|
||||
return false;
|
||||
}
|
||||
// The probe reads leading ID3v2 tags as metadata and searches again after the last one.
|
||||
let mut rest = b;
|
||||
while rest.starts_with(b"ID3") {
|
||||
let size = rest.get(6..10).map(|s| s.iter().fold(0usize, |a, &x| (a << 7) | (x & 0x7f) as usize)).unwrap_or(0);
|
||||
rest = rest.get(10 + size..).unwrap_or(&[]);
|
||||
}
|
||||
let head = &rest[..rest.len().min(PROBE_SEARCH_LIMIT + 16)];
|
||||
match head.windows(4).position(|w| w == b"ftyp") {
|
||||
Some(p) => tables_fit(&rest[p.saturating_sub(4)..]),
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn be32(b: &[u8], at: usize) -> Option<u64> {
|
||||
b.get(at..at.checked_add(4)?).map(|s| u32::from_be_bytes([s[0], s[1], s[2], s[3]]) as u64)
|
||||
}
|
||||
@@ -97,29 +74,3 @@ fn table_fits(kind: &[u8], body: &[u8], file_len: u64) -> bool {
|
||||
};
|
||||
be32(body, 4).is_some_and(|count| count <= body.len().saturating_sub(8) as u64 / entry)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// An MP4 whose `stsc` count asks for far more entries than the file holds.
|
||||
fn mp4_with_huge_stsc() -> Vec<u8> {
|
||||
let stsc = [&24u32.to_be_bytes()[..], b"stsc", &[0, 0, 0, 0], &u32::MAX.to_be_bytes(), &[0; 8]].concat();
|
||||
let stbl = [&(stsc.len() as u32 + 8).to_be_bytes()[..], b"stbl", &stsc].concat();
|
||||
let moov = [&(stbl.len() as u32 + 8).to_be_bytes()[..], b"moov", &stbl].concat();
|
||||
[&16u32.to_be_bytes()[..], b"ftyp", b"isom", &[0; 4], &moov].concat()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_prefixed_mp4_is_checked_where_symphonia_starts_it() {
|
||||
let mp4 = mp4_with_huge_stsc();
|
||||
assert!(!tables_fit(&mp4));
|
||||
assert!(!probe_fits(&mp4));
|
||||
let prefixed = [&b"junk before the file "[..], &mp4].concat();
|
||||
assert_eq!(crate::Container::sniff(&prefixed), crate::Container::Unknown);
|
||||
assert!(tables_fit(&prefixed), "offset 0 sees no MP4 boxes");
|
||||
assert!(!probe_fits(&prefixed));
|
||||
let far = [&vec![0u8; PROBE_SEARCH_LIMIT + 16][..], &mp4].concat();
|
||||
assert!(probe_fits(&far), "past the probe window symphonia never reads it as an MP4");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,8 +71,7 @@ pub fn probe_headers(bytes: &Bytes) -> Result<MediaInfo> {
|
||||
};
|
||||
if info.audio.is_none() && info.video.is_none() || info.audio.as_ref().is_some_and(|a| a.sample_rate == 0) {
|
||||
// Plain audio files (and anything our demuxers didn't describe): symphonia's probe.
|
||||
// Symphonia opens an MP4 wherever its probe finds an `ftyp` (after leading ID3v2 tags too); other known containers (a WAV's audio bytes can spell an `ftyp`) skip the check.
|
||||
if matches!(c, Container::Mp4 | Container::Unknown | Container::Mp3) && !crate::mp4guard::probe_fits(bytes) {
|
||||
if c == Container::Mp4 && !crate::mp4guard::tables_fit(bytes) {
|
||||
return Err(MediaError::Corrupt("the MP4's sample tables do not fit the file".into()));
|
||||
}
|
||||
let (codec, rate, channels, frames) = std::panic::catch_unwind(|| crate::audio::probe_symphonia(bytes, c.name()))
|
||||
|
||||
Reference in New Issue
Block a user