1use std::collections::{BTreeMap, HashSet};
19use std::fmt::Write as _;
20
21use crate::model::{DefinedName, Workbook};
22use serde_json::Value as Json;
23
24use crate::engine::{EngineError, Result};
25
26const PREFIX: &str = "_gc_";
27const LEGACY_PREFIX: &str = "_sc_";
28const LITERAL_UNITS: usize = 255;
30const FORMULA_UNITS: usize = 8_192;
32const MAX_KEY: usize = 200;
34pub const MAX_ITEM_CHARS: usize = 4 * 1024 * 1024;
36const MAX_STORED_BYTES: usize = MAX_ITEM_CHARS * 7;
39pub const ESCAPE: char = '\u{FDD0}';
41pub const MAX_ITEMS: usize = 10_000;
44
45#[derive(Default)]
48struct Parts<'a> {
49 chunks: BTreeMap<u64, &'a str>,
50 legacy: Option<&'a str>,
51}
52
53impl<'a> Parts<'a> {
54 fn add(&mut self, chunk: Option<u64>, formula: &'a str) {
55 match chunk {
56 Some(i) => {
57 self.chunks.entry(i).or_insert(formula);
58 }
59 None => self.legacy = self.legacy.or(Some(formula)),
60 }
61 }
62
63 fn is_item(&self) -> bool {
65 self.chunks.contains_key(&0) || self.legacy.is_some()
66 }
67}
68
69#[cfg(test)]
70thread_local! {
71 static NAME_VISITS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
73}
74
75fn group<'a>(wb: &'a Workbook, prefix: &str) -> BTreeMap<String, Parts<'a>> {
78 let prefix = prefix.to_ascii_lowercase();
79 let mut items: BTreeMap<String, Parts<'a>> = BTreeMap::new();
80 for n in wb.names.iter() {
81 #[cfg(test)]
82 NAME_VISITS.with(|v| v.set(v.get().saturating_add(1)));
83 let Some((k, chunk)) = item_of(n) else { continue };
84 let k = k.to_ascii_lowercase();
85 if k.starts_with(&prefix) {
86 items.entry(k).or_default().add(chunk, &n.formula);
87 }
88 }
89 items
90}
91
92pub fn get(wb: &Workbook, key: &str) -> Option<String> {
94 let mut parts = Parts::default();
95 for n in wb.names.iter() {
96 if let Some((k, chunk)) = item_of(n)
97 && k.eq_ignore_ascii_case(key)
98 {
99 parts.add(chunk, &n.formula);
100 }
101 }
102 assemble(&parts)
103}
104
105#[derive(Clone, Debug, Default, PartialEq, Eq)]
107pub struct Listing<T> {
108 pub found: T,
109 pub more: usize,
111}
112
113pub fn items(wb: &Workbook, prefix: &str) -> Listing<BTreeMap<String, String>> {
118 let mut listing: Listing<BTreeMap<String, String>> = Listing::default();
119 for (k, parts) in group(wb, prefix) {
120 let Some(text) = parts.is_item().then(|| assemble(&parts)).flatten() else { continue };
121 if listing.found.len() < MAX_ITEMS {
122 listing.found.insert(k, text);
123 } else {
124 listing.more = listing.more.saturating_add(1);
125 }
126 }
127 listing
128}
129
130fn assemble(parts: &Parts) -> Option<String> {
132 let chunks = &parts.chunks;
133 let mut out = String::new();
134 if !chunks.contains_key(&0) {
135 out = string_literals(parts.legacy?);
136 } else {
137 for (want, (i, f)) in chunks.iter().enumerate() {
139 if *i != want as u64 {
140 break;
141 }
142 out.push_str(&string_literals(f));
143 if out.len() > MAX_STORED_BYTES {
144 return None;
145 }
146 }
147 out = unescape(&out);
148 }
149 (out.chars().count() <= MAX_ITEM_CHARS).then_some(out)
150}
151
152fn needs_escape(c: char) -> bool {
154 c == ESCAPE || !matches!(c, '\t' | '\n' | '\r' | '\u{20}'..='\u{D7FF}' | '\u{E000}'..='\u{FFFD}' | '\u{10000}'..='\u{10FFFF}')
155}
156
157fn unescape(s: &str) -> String {
160 if !s.contains(ESCAPE) {
161 return s.to_string();
162 }
163 let mut out = String::with_capacity(s.len());
164 let mut rest = s;
165 while let Some(at) = rest.find(ESCAPE) {
166 out.push_str(rest.get(..at).unwrap_or(""));
167 let after = rest.get(at + ESCAPE.len_utf8()..).unwrap_or("");
168 let code = after.get(..4).filter(|h| h.bytes().all(|b| b.is_ascii_hexdigit())).and_then(|h| u32::from_str_radix(h, 16).ok());
169 match code.and_then(char::from_u32) {
170 Some(c) => {
171 out.push(c);
172 rest = after.get(4..).unwrap_or("");
173 }
174 None => {
175 out.push(ESCAPE);
176 rest = after;
177 }
178 }
179 }
180 out.push_str(rest);
181 out
182}
183
184pub fn set(wb: &mut Workbook, key: &str, text: &str) -> Result<()> {
186 set_many(wb, &[(key.to_string(), text.to_string())])
187}
188
189pub fn set_many(wb: &mut Workbook, items: &[(String, String)]) -> Result<()> {
193 let mut planned = Vec::new();
194 let mut keys: HashSet<String> = HashSet::new();
195 for (key, text) in items {
196 check_key(key)?;
197 if text.chars().count() > MAX_ITEM_CHARS {
198 return Err(EngineError::Other(format!(
199 "The data is too large to keep in the workbook ({} MB at most).",
200 MAX_ITEM_CHARS / (1024 * 1024)
201 )));
202 }
203 if !keys.insert(key.to_ascii_lowercase()) {
206 return Err(EngineError::Other(format!("`{key}` is given twice.")));
207 }
208 for (i, formula) in chunk_formulas(text).into_iter().enumerate() {
209 planned.push(DefinedName { name: format!("{PREFIX}{key}_{i}"), scope: None, formula, comment: "GridCraft data".into(), hidden: true });
210 }
211 }
212 let names: HashSet<String> = planned.iter().map(|n| n.name.to_ascii_lowercase()).collect();
215 if let Some(n) = wb.names.iter().find(|n| n.scope.is_none() && !n.hidden && names.contains(&n.name.to_ascii_lowercase())) {
216 return Err(EngineError::Other(format!(
217 "The workbook has a defined name '{}', which GridCraft needs for its own data. Rename it first.",
218 n.name
219 )));
220 }
221 wb.names.retain(|n| !item_of(n).is_some_and(|(k, _)| keys.contains(&k.to_ascii_lowercase())));
223 wb.names.extend(planned);
224 Ok(())
225}
226
227pub fn remove(wb: &mut Workbook, key: &str) {
229 wb.names.retain(|n| !item_of(n).is_some_and(|(k, _)| k.eq_ignore_ascii_case(key)));
230}
231
232pub fn get_json(wb: &Workbook, key: &str) -> Option<Json> {
234 serde_json::from_str(&get(wb, key)?).ok()
235}
236
237pub fn set_json(wb: &mut Workbook, key: &str, v: &Json) -> Result<()> {
238 set(wb, key, &v.to_string())
239}
240
241pub fn keys(wb: &Workbook, prefix: &str) -> Listing<Vec<String>> {
245 let mut found: Vec<String> = group(wb, prefix).into_iter().filter(|(_, parts)| parts.is_item()).map(|(k, _)| k).collect();
246 let more = found.len().saturating_sub(MAX_ITEMS);
247 found.truncate(MAX_ITEMS);
248 Listing { found, more }
249}
250
251pub fn holds_item(wb: &Workbook, name: &str) -> bool {
255 wb.names.iter().any(|n| n.name.eq_ignore_ascii_case(name) && item_of(n).is_some())
256}
257
258pub fn item_key(name: &str) -> Option<String> {
260 parse_name(name).map(|(k, _)| k.to_ascii_lowercase())
261}
262
263fn item_of(n: &DefinedName) -> Option<(&str, Option<u64>)> {
267 if n.scope.is_some() {
268 return None;
269 }
270 parse_name(&n.name).filter(|(_, chunk)| chunk.is_none() || n.hidden)
271}
272
273fn parse_name(name: &str) -> Option<(&str, Option<u64>)> {
275 if let Some(rest) = strip_prefix_ci(name, PREFIX) {
276 let (key, k) = rest.rsplit_once('_')?;
277 let canonical = !k.is_empty() && k.len() <= 9 && k.bytes().all(|b| b.is_ascii_digit()) && (k == "0" || !k.starts_with('0'));
280 if key.is_empty() || !canonical {
281 return None;
282 }
283 return k.parse().ok().map(|i| (key, Some(i)));
284 }
285 strip_prefix_ci(name, LEGACY_PREFIX).filter(|k| !k.is_empty()).map(|k| (k, None))
286}
287
288fn strip_prefix_ci<'a>(s: &'a str, prefix: &str) -> Option<&'a str> {
289 let head = s.get(..prefix.len())?;
290 head.eq_ignore_ascii_case(prefix).then(|| s.get(prefix.len()..)).flatten()
291}
292
293fn check_key(key: &str) -> Result<()> {
294 let ok = !key.is_empty() && key.len() <= MAX_KEY && key.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'.');
295 if ok { Ok(()) } else { Err(EngineError::Other(format!("`{key}` can't name workbook data: use letters, digits, `_` and `.`"))) }
296}
297
298pub fn chunk_formulas(text: &str) -> Vec<String> {
302 let mut formulas = Vec::new();
303 let (mut formula, mut units) = (String::new(), 0usize);
304 let (mut literal, mut lit_units) = (String::from("\""), 0usize);
305 let mut flush_literal = |literal: &mut String, lit_units: &mut usize, formula: &mut String, units: &mut usize| {
306 literal.push('"');
307 let cost = literal.encode_utf16().count() + usize::from(!formula.is_empty());
308 if *units + cost > FORMULA_UNITS && !formula.is_empty() {
309 formulas.push(std::mem::take(formula));
310 *units = 0;
311 }
312 if !formula.is_empty() {
313 formula.push('&');
314 *units += 1;
315 }
316 *units += literal.encode_utf16().count();
317 formula.push_str(literal);
318 *literal = String::from("\"");
319 *lit_units = 0;
320 };
321 for ch in text.chars() {
322 let escaped = needs_escape(ch);
324 let cost = if escaped { ESCAPE.len_utf16() + 4 } else { ch.len_utf16() };
325 if lit_units + cost > LITERAL_UNITS {
326 flush_literal(&mut literal, &mut lit_units, &mut formula, &mut units);
327 }
328 lit_units += cost;
329 if escaped {
330 literal.push(ESCAPE);
331 let _ = write!(literal, "{:04X}", u32::from(ch));
332 } else {
333 if ch == '"' {
334 literal.push('"');
335 }
336 literal.push(ch);
337 }
338 }
339 if lit_units > 0 {
340 flush_literal(&mut literal, &mut lit_units, &mut formula, &mut units);
341 }
342 if !formula.is_empty() {
343 formulas.push(formula);
344 }
345 formulas
346}
347
348pub fn string_literals(f: &str) -> String {
350 let mut out = String::new();
351 let mut it = f.chars().peekable();
352 let mut in_str = false;
353 while let Some(c) = it.next() {
354 if !in_str {
355 if c == '"' {
356 in_str = true;
357 }
358 continue;
359 }
360 if c == '"' {
361 if it.peek() == Some(&'"') {
362 it.next();
363 out.push('"');
364 } else {
365 in_str = false;
366 }
367 } else {
368 out.push(c);
369 }
370 }
371 out
372}
373
374#[cfg(test)]
375mod tests {
376 use super::*;
377
378 fn name(n: &str, formula: &str) -> DefinedName {
379 DefinedName { name: n.into(), scope: None, formula: formula.into(), comment: String::new(), hidden: true }
380 }
381
382 fn excel_limits_hold(wb: &Workbook) {
383 for n in &wb.names {
384 assert!(n.formula.encode_utf16().count() <= FORMULA_UNITS, "{} is {} long", n.name, n.formula.len());
385 for lit in n.formula.split("\"&\"") {
387 let inner = lit.trim_start_matches('"').trim_end_matches('"').replace("\"\"", "\"");
388 assert!(inner.encode_utf16().count() <= LITERAL_UNITS, "{}", inner.len());
389 }
390 }
391 }
392
393 #[test]
394 fn long_values_split_over_names_and_come_back() {
395 let mut wb = Workbook::new();
396 let text: String = (0..30_000).map(|i| ['a', '"', 'é', '\u{1D11E}', '&', '\n'][i % 6]).collect();
398 set(&mut wb, "big", &text).unwrap();
399 let names: Vec<&str> = wb.names.iter().map(|n| n.name.as_str()).collect();
400 assert!(names.len() > 5, "{names:?}");
401 assert_eq!(names[0], "_gc_big_0");
402 assert!(wb.names.iter().all(|n| n.hidden && n.scope.is_none()));
403 excel_limits_hold(&wb);
404 assert_eq!(get(&wb, "big").as_deref(), Some(text.as_str()));
405 assert_eq!(get(&wb, "BIG").as_deref(), Some(text.as_str()), "names are case-insensitive");
406 set(&mut wb, "big", "short").unwrap();
408 assert_eq!(wb.names.len(), 1);
409 assert_eq!(wb.names[0].formula, "\"short\"");
410 set(&mut wb, "big", "").unwrap();
411 assert!(wb.names.is_empty());
412 assert_eq!(get(&wb, "big"), None);
413 }
414
415 #[test]
416 fn chunks_and_literals_respect_the_boundaries() {
417 for len in [0, 1, 254, 255, 256, 8_000, 8_192, 8_193, 20_000] {
418 let text = "q".repeat(len);
419 let fs = chunk_formulas(&text);
420 assert_eq!(fs.iter().map(|f| string_literals(f)).collect::<String>(), text, "{len}");
421 assert!(fs.iter().all(|f| f.len() <= FORMULA_UNITS), "{len}");
422 }
423 let text = "\"".repeat(9_000);
425 let fs = chunk_formulas(&text);
426 assert_eq!(fs.iter().map(|f| string_literals(f)).collect::<String>(), text);
427 assert!(fs.iter().all(|f| f.len() <= FORMULA_UNITS));
428 }
429
430 #[test]
431 fn the_old_single_name_still_reads_and_is_replaced_on_write() {
432 let mut wb = Workbook::new();
433 wb.names.push(name("_sc_scripts", "\"{\"\"A\"\":[1]}\""));
434 wb.names.push(name("_sc_scenarios", "\"[1,\"&\"2]\""));
435 assert_eq!(get_json(&wb, "scripts"), Some(serde_json::json!({"A": [1]})));
436 assert_eq!(get_json(&wb, "scenarios"), Some(serde_json::json!([1, 2])));
437 set_json(&mut wb, "scripts", &serde_json::json!({"B": []})).unwrap();
438 let names: Vec<&str> = wb.names.iter().map(|n| n.name.as_str()).collect();
439 assert_eq!(names, ["_sc_scenarios", "_gc_scripts_0"]);
440 assert_eq!(get_json(&wb, "scripts"), Some(serde_json::json!({"B": []})));
441 }
442
443 #[test]
444 fn hostile_names_read_safely() {
445 let mut wb = Workbook::new();
446 wb.names.push(name("_gc_gap_0", "\"a\""));
448 wb.names.push(name("_gc_gap_2", "\"c\""));
449 assert_eq!(get(&wb, "gap").as_deref(), Some("a"));
450 wb.names.push(name("_gc_orphan_2", "\"c\""));
452 assert_eq!(get(&wb, "orphan"), None);
453 assert_eq!(keys(&wb, "orphan").found, Vec::<String>::new());
454 wb.names.push(name("_sc_orphan", "\"old\""));
455 assert_eq!(get(&wb, "orphan").as_deref(), Some("old"));
456 assert_eq!(keys(&wb, "orphan").found, ["orphan"]);
457 wb.names.push(name("_gc_mix_1", "\"b\"&A1&"));
459 wb.names.push(name("_gc_mix_0", "SUM(\"a\")"));
460 wb.names.push(name("_gc_mix_0", "\"ignored\""));
461 wb.names.push(name("_gc_mix_2", "\"c"));
462 assert_eq!(get(&wb, "mix").as_deref(), Some("abc"));
463 for odd in ["_gc_", "_gc__0", "_gc_x_", "_gc_x_-1", "_gc_x_99999999999999999999", "_sc_", "_gcx_0"] {
465 wb.names.push(name(odd, "\"zzz\""));
466 }
467 assert_eq!(get(&wb, "x"), None);
468 assert_eq!(get(&wb, ""), None);
469 wb.names.push(name("_gc_notjson_0", "\"{\""));
471 assert_eq!(get_json(&wb, "notjson"), None);
472 let deep = format!("\"{}{}\"", "[".repeat(10_000), "]".repeat(10_000));
473 wb.names.push(name("_gc_deep_0", &deep));
474 assert_eq!(get_json(&wb, "deep"), None);
475 wb.names.push(DefinedName { scope: Some(0), ..name("_gc_scoped_0", "\"s\"") });
477 assert_eq!(get(&wb, "scoped"), None);
478 for bad in ["", "has space", "quote\"", "a/b", &"k".repeat(201)] {
480 assert!(set(&mut wb, bad, "v").is_err(), "{bad}");
481 }
482 assert!(set(&mut wb, "big", &"x".repeat(MAX_ITEM_CHARS + 1)).is_err());
483 }
484
485 #[test]
486 fn oversized_items_are_not_read() {
487 let mut wb = Workbook::new();
488 let chunk = format!("\"{}\"", "x".repeat(250));
489 let big = vec![chunk.as_str(); 32].join("&");
490 for i in 0..(MAX_ITEM_CHARS / 8_000 + 2) {
491 wb.names.push(name(&format!("_gc_huge_{i}"), &big));
492 }
493 assert_eq!(get(&wb, "huge"), None);
494 }
495
496 #[test]
497 fn a_visible_name_of_the_item_shape_is_not_an_item() {
498 let mut wb = Workbook::new();
501 let own = DefinedName { hidden: false, ..name("_gc_watch_0", "\"mine\"") };
502 wb.names.push(own.clone());
503 assert_eq!(get(&wb, "watch"), None);
504 assert!(keys(&wb, "").found.is_empty());
505 let err = set(&mut wb, "watch", "theirs").unwrap_err().to_string();
508 assert!(err.contains("_gc_watch_0"), "{err}");
509 assert_eq!(wb.names, vec![own.clone()]);
510 assert_eq!(get(&wb, "watch"), None);
511 set(&mut wb, "other", "theirs").unwrap();
512 assert_eq!(get(&wb, "other").as_deref(), Some("theirs"));
513 remove(&mut wb, "watch");
514 remove(&mut wb, "other");
515 assert_eq!(wb.names, vec![own]);
516 }
517
518 #[test]
519 fn set_many_refuses_one_key_twice() {
520 let mut wb = Workbook::new();
522 wb.names.push(name("_gc_other_0", "\"stays\""));
523 let err = set_many(&mut wb, &[("k".into(), "a".into()), ("K".into(), "b".into())]).unwrap_err().to_string();
524 assert!(err.contains("given twice"), "{err}");
525 assert_eq!(wb.names.len(), 1, "nothing changed");
526 set_many(&mut wb, &[("k".into(), "a".into()), ("k2".into(), "b".into())]).unwrap();
527 assert_eq!((get(&wb, "k").as_deref(), get(&wb, "K2").as_deref(), get(&wb, "other").as_deref()), (Some("a"), Some("b"), Some("stays")));
528 }
529
530 #[test]
531 fn a_chunk_number_with_leading_zeros_is_another_name() {
532 let mut wb = Workbook::new();
535 wb.names.push(name("_gc_watch_00", "\"theirs\""));
536 assert_eq!(get(&wb, "watch"), None);
537 assert!(keys(&wb, "").found.is_empty());
538 let own = DefinedName { hidden: false, ..name("_gc_watch_00", "\"mine\"") };
539 wb.names.push(own.clone());
540 set(&mut wb, "watch", "ours").unwrap();
541 assert_eq!(get(&wb, "watch").as_deref(), Some("ours"));
542 assert_eq!(wb.names.len(), 3, "both other names stay");
543 remove(&mut wb, "watch");
544 assert_eq!(wb.names, vec![name("_gc_watch_00", "\"theirs\""), own]);
545 }
546
547 #[test]
548 fn keys_list_items_by_prefix() {
549 let mut wb = Workbook::new();
550 set(&mut wb, "macro_2", "b").unwrap();
551 set(&mut wb, "macro_1", &"a".repeat(9_000)).unwrap();
552 set(&mut wb, "scripts", "{}").unwrap();
553 assert_eq!(keys(&wb, "macro_").found, ["macro_1", "macro_2"]);
554 assert_eq!(item_key("_gc_macro_1_3").as_deref(), Some("macro_1"));
555 assert_eq!(item_key("_SC_Scripts").as_deref(), Some("scripts"));
556 assert_eq!(item_key("Total"), None);
557 }
558
559 #[test]
560 fn listing_many_items_takes_one_pass_and_is_capped() {
561 let mut wb = Workbook::new();
564 let total = 20_000;
565 for i in 0..total {
566 wb.names.push(name(&format!("_gc_macro_{i:05}_0"), &format!("\"m{i}\"")));
567 }
568 set(&mut wb, "macro_big", &"b".repeat(9_000)).unwrap();
569 wb.names.push(name("_sc_macro_legacy", "\"old\""));
570 wb.names.push(name("_gc_macro_orphan_1", "\"no chunk 0\""));
571 wb.names.push(name("_gc_other_0", "\"not a macro\""));
572 NAME_VISITS.with(|v| v.set(0));
573 let listing = items(&wb, "MACRO_");
574 let all = &listing.found;
575 assert_eq!(NAME_VISITS.with(|v| v.get()), wb.names.len(), "one pass over the names");
576 assert_eq!(all.len(), MAX_ITEMS);
577 let first: Vec<&String> = all.keys().take(2).collect();
578 assert_eq!(first, ["macro_00000", "macro_00001"]);
579 assert_eq!(all.get("macro_09999").map(String::as_str), Some("m9999"));
580 assert!(!all.contains_key("macro_10000"), "past the cap");
581 NAME_VISITS.with(|v| v.set(0));
582 let listed = keys(&wb, "macro_");
583 assert_eq!(NAME_VISITS.with(|v| v.get()), wb.names.len());
584 assert_eq!(listed.found, all.keys().cloned().collect::<Vec<_>>(), "keys and items agree");
585 assert_eq!((listing.more, listed.more), (10_002, 10_002), "the rest are counted, not lost");
586
587 wb.names.retain(|n| !n.name.starts_with("_gc_macro_0") && !n.name.starts_with("_gc_macro_1"));
589 let few = items(&wb, "macro_").found;
590 let want: BTreeMap<String, String> =
591 [("macro_big", "b".repeat(9_000)), ("macro_legacy", "old".into())].into_iter().map(|(k, t)| (k.to_string(), t)).collect();
592 assert_eq!(few, want);
593 for (k, text) in &few {
594 assert_eq!(get(&wb, k).as_ref(), Some(text));
595 }
596 assert_eq!(keys(&wb, "macro_").found, ["macro_big", "macro_legacy"]);
597 assert_eq!((items(&wb, "").found.len(), items(&wb, "").more), (3, 0), "the other item too, none past the cap");
598 }
599
600 #[test]
601 fn an_item_over_the_size_limit_takes_no_place_in_a_listing() {
602 let mut wb = Workbook::new();
605 let oversized = |wb: &mut Workbook, key: &str| {
606 for (i, formula) in chunk_formulas(&"x".repeat(MAX_ITEM_CHARS + 1)).into_iter().enumerate() {
607 wb.names.push(name(&format!("_gc_{key}_{i}"), &formula));
608 }
609 };
610 oversized(&mut wb, "macro_0");
611 for i in 0..MAX_ITEMS {
612 wb.names.push(name(&format!("_gc_macro_{i:05}_0"), &format!("\"m{i}\"")));
613 }
614 oversized(&mut wb, "macro_z");
615 assert_eq!(get(&wb, "macro_0"), None, "too large to read");
616 let listing = items(&wb, "macro_");
617 assert_eq!((listing.found.len(), listing.more), (MAX_ITEMS, 0));
618 assert!(listing.found.contains_key("macro_09999"));
619 }
620
621 #[test]
622 fn items_round_trip_through_xlsx() {
623 let mut wb = Workbook::new();
624 let text: String = (0..20_000).map(|i| ['x', '"', 'ü', '\u{1D11E}', ' '][i % 5]).collect();
625 set(&mut wb, "blob", &text).unwrap();
626 wb.names.push(name("_sc_watch", "\"[\"\"A1\"\"]\""));
627 let bytes = crate::xlsx::write_xlsx(&wb).unwrap();
628 let (back, warnings) = crate::engine::io::open_bytes("data.xlsx", &bytes).unwrap();
629 assert!(warnings.is_empty(), "{warnings:?}");
630 assert_eq!(get(&back, "blob").as_deref(), Some(text.as_str()));
631 assert_eq!(get_json(&back, "watch"), Some(serde_json::json!(["A1"])));
632
633 let mut wb = Workbook::new();
636 let odd: String = (0u32..0x20).filter_map(char::from_u32).chain(['\u{FFFE}', '\u{FFFF}', ESCAPE, 'a', '\u{7F}', '\u{FDD1}']).collect();
637 let text: String = odd.chars().cycle().take(odd.chars().count() * 300).collect();
638 set(&mut wb, "odd", &text).unwrap();
639 excel_limits_hold(&wb);
640 let json = serde_json::json!({"s": "x\u{FFFF}y\u{1}z\u{FDD0}0041"});
641 set_json(&mut wb, "json", &json).unwrap();
642 let controls = "\u{1}".repeat(9_000);
644 set(&mut wb, "controls", &controls).unwrap();
645 excel_limits_hold(&wb);
646 let bytes = crate::xlsx::write_xlsx(&wb).unwrap();
647 let (back, warnings) = crate::engine::io::open_bytes("data.xlsx", &bytes).unwrap();
648 assert!(warnings.is_empty(), "{warnings:?}");
649 assert_eq!(get(&back, "odd").as_deref(), Some(text.as_str()));
650 assert_eq!(get_json(&back, "json"), Some(json));
651 assert_eq!(get(&back, "controls").as_deref(), Some(controls.as_str()));
652 }
653
654 #[test]
655 fn escapes_count_against_the_limits_and_odd_markers_read_as_they_are() {
656 for len in [1, 50, 51, 52, 1_638, 1_639, 5_000] {
659 let text = "\u{2}".repeat(len);
660 let fs = chunk_formulas(&text);
661 assert_eq!(unescape(&fs.iter().map(|f| string_literals(f)).collect::<String>()), text, "{len}");
662 assert!(fs.iter().all(|f| f.encode_utf16().count() <= FORMULA_UNITS), "{len}");
663 }
664 let mut wb = Workbook::new();
665 let longest = "\u{0}".repeat(MAX_ITEM_CHARS);
666 set(&mut wb, "longest", &longest).unwrap();
667 excel_limits_hold(&wb);
668 assert_eq!(get(&wb, "longest").map(|t| t.len()), Some(MAX_ITEM_CHARS));
669 let mut wb = Workbook::new();
671 wb.names.push(name("_gc_hand_0", "\"a\u{FDD0}zz\u{FDD0}D800\u{FDD0}004\u{FDD0}0041\u{FDD0}\""));
672 assert_eq!(get(&wb, "hand").as_deref(), Some("a\u{FDD0}zz\u{FDD0}D800\u{FDD0}004A\u{FDD0}"));
673 }
674}