mirror of
https://github.com/storytold/deckcraft.git
synced 2026-11-10 04:19:33 -06:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a02d1d386 | ||
|
|
6af917e869 |
@@ -171,7 +171,9 @@ impl Dictionary {
|
|||||||
["COMPOUNDWORDMAX", n, ..] => d.compound_max = n.parse().unwrap_or(0),
|
["COMPOUNDWORDMAX", n, ..] => d.compound_max = n.parse().unwrap_or(0),
|
||||||
["REP", n] => {
|
["REP", n] => {
|
||||||
for _ in 0..n.parse::<usize>().unwrap_or(0) {
|
for _ in 0..n.parse::<usize>().unwrap_or(0) {
|
||||||
let r: Vec<&str> = lines.next().unwrap_or("").split_whitespace().collect();
|
// The count is the file's claim: stop when the records run out.
|
||||||
|
let Some(line) = lines.next() else { break };
|
||||||
|
let r: Vec<&str> = line.split_whitespace().collect();
|
||||||
if let [_, a, b, ..] = r.as_slice() {
|
if let [_, a, b, ..] = r.as_slice() {
|
||||||
d.rep.push((a.replace('_', " "), b.replace('_', " ")));
|
d.rep.push((a.replace('_', " "), b.replace('_', " ")));
|
||||||
}
|
}
|
||||||
@@ -182,7 +184,8 @@ impl Dictionary {
|
|||||||
let Some(flag) = one_flag(Some(flag), mode) else { continue };
|
let Some(flag) = one_flag(Some(flag), mode) else { continue };
|
||||||
let cross = cross.eq_ignore_ascii_case("y");
|
let cross = cross.eq_ignore_ascii_case("y");
|
||||||
for _ in 0..n.parse::<usize>().unwrap_or(0) {
|
for _ in 0..n.parse::<usize>().unwrap_or(0) {
|
||||||
let r: Vec<&str> = lines.next().unwrap_or("").split_whitespace().collect();
|
let Some(line) = lines.next() else { break };
|
||||||
|
let r: Vec<&str> = line.split_whitespace().collect();
|
||||||
let [_, _, strip, add, rest @ ..] = r.as_slice() else { continue };
|
let [_, _, strip, add, rest @ ..] = r.as_slice() else { continue };
|
||||||
let (add, cont) = match add.split_once('/') {
|
let (add, cont) = match add.split_once('/') {
|
||||||
Some((a, c)) => (a, parse_flags(c, mode)),
|
Some((a, c)) => (a, parse_flags(c, mode)),
|
||||||
@@ -576,6 +579,15 @@ fn cache() -> &'static Cache {
|
|||||||
CACHE.get_or_init(Default::default)
|
CACHE.get_or_init(Default::default)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Language strings remembered by [`dictionary`]; a string past it is resolved again on each call.
|
||||||
|
const MAX_CACHED_LANGS: usize = 256;
|
||||||
|
|
||||||
|
/// Parsed dictionaries by their affix file, so aliases of one language share it.
|
||||||
|
fn by_files() -> &'static Mutex<HashMap<PathBuf, Arc<Dictionary>>> {
|
||||||
|
static M: OnceLock<Mutex<HashMap<PathBuf, Arc<Dictionary>>>> = OnceLock::new();
|
||||||
|
M.get_or_init(Default::default)
|
||||||
|
}
|
||||||
|
|
||||||
/// The dictionary for `lang` (a BCP-47 tag such as `en-US`), loaded once per process.
|
/// The dictionary for `lang` (a BCP-47 tag such as `en-US`), loaded once per process.
|
||||||
pub fn dictionary(lang: &str) -> Option<Arc<Dictionary>> {
|
pub fn dictionary(lang: &str) -> Option<Arc<Dictionary>> {
|
||||||
let cache = cache();
|
let cache = cache();
|
||||||
@@ -586,10 +598,23 @@ pub fn dictionary(lang: &str) -> Option<Arc<Dictionary>> {
|
|||||||
return d.clone();
|
return d.clone();
|
||||||
}
|
}
|
||||||
let loaded = find_in(&search_dirs(), lang).and_then(|(a, d)| {
|
let loaded = find_in(&search_dirs(), lang).and_then(|(a, d)| {
|
||||||
let (a, d) = (std::fs::read(a).ok()?, std::fs::read(d).ok()?);
|
// Tags that resolve to the same files share one parsed dictionary.
|
||||||
Some(Arc::new(Dictionary::parse(&a, &d)))
|
if let Ok(m) = by_files().lock()
|
||||||
|
&& let Some(dict) = m.get(&a)
|
||||||
|
{
|
||||||
|
return Some(dict.clone());
|
||||||
|
}
|
||||||
|
let (bytes_a, bytes_d) = (std::fs::read(&a).ok()?, std::fs::read(d).ok()?);
|
||||||
|
let dict = Arc::new(Dictionary::parse(&bytes_a, &bytes_d));
|
||||||
|
// A concurrent loader of the same files may have inserted first: share its copy.
|
||||||
|
if let Ok(mut m) = by_files().lock() {
|
||||||
|
return Some(m.entry(a).or_insert(dict).clone());
|
||||||
|
}
|
||||||
|
Some(dict)
|
||||||
});
|
});
|
||||||
if let Ok(mut c) = cache.lock() {
|
if let Ok(mut c) = cache.lock()
|
||||||
|
&& c.len() < MAX_CACHED_LANGS
|
||||||
|
{
|
||||||
c.insert(key, loaded.clone());
|
c.insert(key, loaded.clone());
|
||||||
}
|
}
|
||||||
loaded
|
loaded
|
||||||
@@ -880,6 +905,14 @@ SFX D Y 1\nSFX D 0 ed/S .\n";
|
|||||||
assert!(d.suggest("workingslides", 5).contains(&"working slides".to_string()));
|
assert!(d.suggest("workingslides", 5).contains(&"working slides".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An affix header can claim billions of records; parsing stops where the file does.
|
||||||
|
#[test]
|
||||||
|
fn a_header_count_past_the_records_ends_with_them() {
|
||||||
|
let aff = b"SFX S Y 4000000000\nSFX S 0 s .\nREP 4000000000\nREP a b\n";
|
||||||
|
let d = Dictionary::parse(aff, b"1\ncat/S\n".as_slice());
|
||||||
|
assert!(d.check("cats"));
|
||||||
|
}
|
||||||
|
|
||||||
/// #1175: compound splitting is quadratic in the word's length, so a word longer than
|
/// #1175: compound splitting is quadratic in the word's length, so a word longer than
|
||||||
/// `MAX_CHECK_CHARS` is not checked; shorter ones still are.
|
/// `MAX_CHECK_CHARS` is not checked; shorter ones still are.
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -38,8 +38,6 @@ pub struct Imp<'a> {
|
|||||||
pub vml_previews: HashMap<String, HashMap<String, String>>,
|
pub vml_previews: HashMap<String, HashMap<String, String>>,
|
||||||
/// Aggregate byte count of retained chart sidecars across the presentation.
|
/// Aggregate byte count of retained chart sidecars across the presentation.
|
||||||
pub kept_sidecar_bytes: usize,
|
pub kept_sidecar_bytes: usize,
|
||||||
/// Data slots of the charts decoded so far (see `chart::MAX_PRESENTATION_CHART_POINTS`).
|
|
||||||
pub chart_points: usize,
|
|
||||||
/// Cached kept sidecars by normalized chart part name.
|
/// Cached kept sidecars by normalized chart part name.
|
||||||
pub kept_charts: HashMap<String, deckcraft_model::chart::ChartKept>,
|
pub kept_charts: HashMap<String, deckcraft_model::chart::ChartKept>,
|
||||||
/// Add-in tags parts, parsed once each, by normalized part name.
|
/// Add-in tags parts, parsed once each, by normalized part name.
|
||||||
@@ -444,7 +442,6 @@ pub fn import_theme(bytes: &[u8]) -> Result<(deckcraft_model::theme::Theme, Vec<
|
|||||||
table_style_ids: Vec::new(),
|
table_style_ids: Vec::new(),
|
||||||
vml_previews: HashMap::new(),
|
vml_previews: HashMap::new(),
|
||||||
kept_sidecar_bytes: 0,
|
kept_sidecar_bytes: 0,
|
||||||
chart_points: 0,
|
|
||||||
kept_charts: HashMap::new(),
|
kept_charts: HashMap::new(),
|
||||||
tags_by_part: HashMap::new(),
|
tags_by_part: HashMap::new(),
|
||||||
tag_bytes: 0,
|
tag_bytes: 0,
|
||||||
@@ -486,7 +483,6 @@ pub fn import_checked(bytes: &[u8]) -> Result<(Presentation, Vec<String>), PptxE
|
|||||||
table_style_ids: Vec::new(),
|
table_style_ids: Vec::new(),
|
||||||
vml_previews: HashMap::new(),
|
vml_previews: HashMap::new(),
|
||||||
kept_sidecar_bytes: 0,
|
kept_sidecar_bytes: 0,
|
||||||
chart_points: 0,
|
|
||||||
kept_charts: HashMap::new(),
|
kept_charts: HashMap::new(),
|
||||||
tags_by_part: HashMap::new(),
|
tags_by_part: HashMap::new(),
|
||||||
tag_bytes: 0,
|
tag_bytes: 0,
|
||||||
|
|||||||
@@ -39,24 +39,6 @@ pub(crate) fn rich_text(t: &El) -> Option<String> {
|
|||||||
/// Most category/value/size slots one chart may decode across all its series and caches, so
|
/// Most category/value/size slots one chart may decode across all its series and caches, so
|
||||||
/// sparse `ptCount`/`idx` declarations can't expand far beyond the XML they came from.
|
/// sparse `ptCount`/`idx` declarations can't expand far beyond the XML they came from.
|
||||||
pub(crate) const MAX_CHART_POINTS: usize = 1_000_000;
|
pub(crate) const MAX_CHART_POINTS: usize = 1_000_000;
|
||||||
/// Data slots (values and categories) the charts of one presentation may hold in all; a chart past
|
|
||||||
/// it is read as a placeholder frame. Each frame that references a chart decodes its own copy.
|
|
||||||
pub(crate) const MAX_PRESENTATION_CHART_POINTS: usize = 4_000_000;
|
|
||||||
|
|
||||||
/// Slots a decoded chart holds, counting the chart itself as at least one.
|
|
||||||
pub(crate) fn point_count(c: &Chart) -> usize {
|
|
||||||
let series = |s: &deckcraft_model::chart::Series| {
|
|
||||||
let bars = s.error_bars.as_ref().map_or(0, |e| e.plus.len().saturating_add(e.minus.len()));
|
|
||||||
s.values.len().saturating_add(s.x_values.len()).saturating_add(s.sizes.len()).saturating_add(bars)
|
|
||||||
};
|
|
||||||
let levels: usize = c.category_levels.iter().map(Vec::len).sum();
|
|
||||||
1usize.saturating_add(c.categories.len()).saturating_add(levels).saturating_add(c.series.iter().map(series).fold(0, usize::saturating_add))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Bytes of the strings a kept relationship owns.
|
|
||||||
fn rel_bytes(r: &deckcraft_model::chart::ChartRel) -> usize {
|
|
||||||
r.id.len() + r.rel_type.len() + r.target.len() + r.part.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Most slots one cache may hold.
|
/// Most slots one cache may hold.
|
||||||
const MAX_CACHE_POINTS: usize = 100_000;
|
const MAX_CACHE_POINTS: usize = 100_000;
|
||||||
@@ -484,13 +466,6 @@ pub(crate) fn capture_kept(imp: &mut super::Imp, chart_part: &str) -> deckcraft_
|
|||||||
|
|
||||||
let key = crate::opc::norm(chart_part);
|
let key = crate::opc::norm(chart_part);
|
||||||
if let Some(cached) = imp.kept_charts.get(&key) {
|
if let Some(cached) = imp.kept_charts.get(&key) {
|
||||||
// Every frame that shows this chart gets its own copy, so each copy is charged like the first.
|
|
||||||
let bytes: usize = cached.parts.iter().map(|p| p.bytes.len()).sum::<usize>() + cached.rels.iter().map(rel_bytes).sum::<usize>();
|
|
||||||
if imp.kept_sidecar_bytes.saturating_add(bytes) > MAX_KEPT_TOTAL_BYTES {
|
|
||||||
log::warn!("pptx: chart sidecar retained byte budget exceeded for {chart_part}");
|
|
||||||
return ChartKept::default();
|
|
||||||
}
|
|
||||||
imp.kept_sidecar_bytes += bytes;
|
|
||||||
return cached.clone();
|
return cached.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -510,13 +485,6 @@ pub(crate) fn capture_kept(imp: &mut super::Imp, chart_part: &str) -> deckcraft_
|
|||||||
kept.rels.push(ChartRel { id: r.id, rel_type: r.ty, target: r.target_raw, part: r.target, external: false });
|
kept.rels.push(ChartRel { id: r.id, rel_type: r.ty, target: r.target_raw, part: r.target, external: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// The first capture's relationship strings are retained too (the parts were charged as copied).
|
|
||||||
let rel_total: usize = kept.rels.iter().map(rel_bytes).sum();
|
|
||||||
if imp.kept_sidecar_bytes.saturating_add(rel_total) > MAX_KEPT_TOTAL_BYTES {
|
|
||||||
log::warn!("pptx: chart sidecar retained byte budget exceeded for {chart_part}");
|
|
||||||
return ChartKept::default();
|
|
||||||
}
|
|
||||||
imp.kept_sidecar_bytes += rel_total;
|
|
||||||
imp.kept_charts.insert(key, kept.clone());
|
imp.kept_charts.insert(key, kept.clone());
|
||||||
kept
|
kept
|
||||||
}
|
}
|
||||||
@@ -622,16 +590,6 @@ fn text_size(e: Option<&El>) -> Option<f64> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn point_count_charges_every_retained_vector() {
|
|
||||||
let mut c = Chart { categories: vec!["a".into()], category_levels: vec![vec!["x".into(); 2], vec!["y".into(); 3]], ..Default::default() };
|
|
||||||
let mut s =
|
|
||||||
deckcraft_model::chart::Series { values: vec![Some(1.0); 4], x_values: vec![None; 5], sizes: vec![None; 6], ..Default::default() };
|
|
||||||
s.error_bars = Some(deckcraft_model::chart::ErrorBars { plus: vec![None; 7], minus: vec![None; 8], ..Default::default() });
|
|
||||||
c.series = vec![s];
|
|
||||||
assert_eq!(point_count(&c), 1 + 1 + 5 + 4 + 5 + 6 + 15);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sparse_caches_share_one_chart_wide_budget() {
|
fn sparse_caches_share_one_chart_wide_budget() {
|
||||||
// Every series declares 100000 points but writes one: without a chart-wide budget each
|
// Every series declares 100000 points but writes one: without a chart-wide budget each
|
||||||
|
|||||||
@@ -428,14 +428,10 @@ fn frame(imp: &mut Imp, part: &Part, ctx: &mut IdCtx, e: &El, depth: usize) -> S
|
|||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
if let Some(c) = gd.child("chart")
|
if let Some(c) = gd.child("chart")
|
||||||
&& imp.chart_points < super::chart::MAX_PRESENTATION_CHART_POINTS
|
|
||||||
&& let Some(rel) = part.rels.get(c.attr("r:id").unwrap_or(""))
|
&& let Some(rel) = part.rels.get(c.attr("r:id").unwrap_or(""))
|
||||||
&& let Some(bytes) = imp.pkg.xml_bytes(&rel.target)
|
&& let Some(bytes) = imp.pkg.xml_bytes(&rel.target)
|
||||||
&& let Some(mut chart) = if uri.ends_with("/chartex") { super::read_chartex_xml(bytes) } else { super::read_chart_xml(bytes) }
|
&& let Some(mut chart) = if uri.ends_with("/chartex") { super::read_chartex_xml(bytes) } else { super::read_chart_xml(bytes) }
|
||||||
// A chart that would cross the presentation limit is read as the placeholder frame instead.
|
|
||||||
&& imp.chart_points.saturating_add(super::chart::point_count(&chart)) <= super::chart::MAX_PRESENTATION_CHART_POINTS
|
|
||||||
{
|
{
|
||||||
imp.chart_points = imp.chart_points.saturating_add(super::chart::point_count(&chart));
|
|
||||||
chart.kept = super::chart::capture_kept(imp, &rel.target);
|
chart.kept = super::chart::capture_kept(imp, &rel.target);
|
||||||
s.kind = ShapeKind::Chart(Box::new(chart));
|
s.kind = ShapeKind::Chart(Box::new(chart));
|
||||||
return s;
|
return s;
|
||||||
|
|||||||
Reference in New Issue
Block a user