1use std::io::{Cursor, Read};
7use std::sync::Arc;
8
9use crate::base::date::{days_in_month, serial_from_ymd};
10use crate::base::{CellRef, DateSystem, MAX_COLS, MAX_ROWS, RangeRef, Value};
11use crate::model::{Cell, NumFmt, Sheet, StyleId, Workbook};
12use quick_xml::NsReader;
13use quick_xml::XmlVersion;
14use quick_xml::events::{BytesStart, Event};
15use quick_xml::name::ResolveResult;
16
17use crate::xlsx::package::{MAX_ENTRIES, Package};
18use crate::xlsx::{IoError, ReadReport};
19
20const MIME: &[u8] = b"application/vnd.oasis.opendocument.spreadsheet";
21const OFFICE: &[u8] = b"urn:oasis:names:tc:opendocument:xmlns:office:1.0";
22const TABLE: &[u8] = b"urn:oasis:names:tc:opendocument:xmlns:table:1.0";
23const TEXT: &[u8] = b"urn:oasis:names:tc:opendocument:xmlns:text:1.0";
24const CALC_EXT: &[u8] = b"urn:org:documentfoundation:names:experimental:calc:xmlns:calcext:1.0";
25const MANIFEST: &[u8] = b"urn:oasis:names:tc:opendocument:xmlns:manifest:1.0";
26const MAX_CELLS: u64 = 1_000_000;
27const MAX_MERGES: u64 = 100_000;
28const MAX_CELL_TEXT: usize = 1024 * 1024;
29const MAX_TEXT: u64 = 64 * 1024 * 1024;
30const MAX_SHEETS: usize = 1024;
31
32pub(crate) fn has_mimetype(bytes: &[u8]) -> bool {
35 let Ok(mut zip) = zip::ZipArchive::new(Cursor::new(bytes)) else { return false };
36 if zip.len() > MAX_ENTRIES {
37 return false;
38 }
39 let Ok(mut file) = zip.by_name("mimetype") else { return false };
40 if file.size() != MIME.len() as u64 {
41 return false;
42 }
43 let mut value = Vec::new();
44 (&mut file).take(MIME.len() as u64 + 1).read_to_end(&mut value).is_ok() && value == MIME
45}
46
47pub fn read_ods(bytes: &[u8]) -> Result<(Workbook, ReadReport), IoError> {
51 let mut package = Package::open(bytes)?;
52 if !has_mimetype(bytes) {
53 return Err(IoError::Format("missing or invalid ODS mimetype".into()));
54 }
55 if let Some(manifest) = package.read("META-INF/manifest.xml")? {
56 check_manifest(&manifest)?;
57 }
58 let content = package.read("content.xml")?.ok_or_else(|| IoError::Format("ODS content.xml is missing".into()))?;
59 let mut import = Import::new();
60 import.parse(&content)?;
61 if import.wb.sheets.is_empty() {
62 return Err(IoError::Format("ODS contains no worksheets".into()));
63 }
64 Ok((import.wb, import.report))
65}
66
67fn is_ns(ns: ResolveResult<'_>, expected: &[u8]) -> bool {
68 matches!(ns, ResolveResult::Bound(n) if n.as_ref() == expected)
69}
70
71fn attr(reader: &mut NsReader<&[u8]>, e: &BytesStart<'_>, ns: &[u8], name: &[u8]) -> Result<Option<String>, IoError> {
72 let mut found = None;
73 for a in e.attributes() {
74 let a = a.map_err(|e| IoError::Xml(e.to_string()))?;
75 let (resolved, local) = reader.resolver_mut().resolve_attribute(a.key);
76 if is_ns(resolved, ns) && local.as_ref() == name {
77 let value = a.normalized_value(XmlVersion::Explicit1_0).map_err(|e| IoError::Xml(e.to_string()))?;
78 if value.len() > MAX_CELL_TEXT {
79 return Err(IoError::TooLarge("ODS attribute exceeds 1 MiB".into()));
80 }
81 found = Some(value.into_owned());
82 }
83 }
84 Ok(found)
85}
86
87fn positive(value: Option<String>, default: u32, limit: u32) -> Result<u32, IoError> {
88 let n = match value {
89 Some(s) => s.parse::<u32>().map_err(|_| IoError::Format("invalid ODS repeat or span count".into()))?,
90 None => default,
91 };
92 if n == 0 || n > limit {
93 return Err(IoError::TooLarge("ODS repeat or span exceeds worksheet bounds".into()));
94 }
95 Ok(n)
96}
97
98fn xml_error(e: impl std::fmt::Display) -> IoError {
99 IoError::Xml(format!("ODS: {e}"))
100}
101
102fn check_manifest(bytes: &[u8]) -> Result<(), IoError> {
103 let mut reader = NsReader::from_reader(bytes);
104 let mut depth = 0usize;
105 let mut root_seen = false;
106 loop {
107 let event = reader.read_event().map_err(xml_error)?;
108 match &event {
109 Event::Start(e) | Event::Empty(e) => {
110 let (ns, name) = reader.resolver_mut().resolve_element(e.name());
111 if is_ns(ns.clone(), MANIFEST) && name.as_ref() == b"encryption-data" {
112 return Err(IoError::Format("encrypted ODS files are not supported".into()));
113 }
114 if depth == 0 {
115 if root_seen || !is_ns(ns, MANIFEST) || name.as_ref() != b"manifest" {
116 return Err(xml_error("expected one manifest:manifest root"));
117 }
118 root_seen = true;
119 }
120 if matches!(event, Event::Start(_)) {
121 depth += 1;
122 if depth > crate::xlsx::xml::MAX_DEPTH {
123 return Err(xml_error("manifest nested too deeply"));
124 }
125 }
126 }
127 Event::End(_) => depth = depth.checked_sub(1).ok_or_else(|| xml_error("unexpected manifest closing element"))?,
128 Event::DocType(_) => return Err(xml_error("DOCTYPE is not supported")),
129 Event::Eof => break,
130 _ => {}
131 }
132 }
133 if !root_seen || depth != 0 {
134 return Err(xml_error("incomplete manifest"));
135 }
136 Ok(())
137}
138
139#[derive(Clone, Copy, Debug, PartialEq, Eq)]
140enum Tag {
141 Document,
142 Body,
143 Spreadsheet,
144 Table,
145 Rows,
146 Row,
147 Cell,
148 Covered,
149 Paragraph,
150 Inline,
151 Space,
152 Tab,
153 Break,
154 Other,
155}
156
157fn tag(reader: &mut NsReader<&[u8]>, e: &BytesStart<'_>) -> Tag {
158 let (ns, name) = reader.resolver_mut().resolve_element(e.name());
159 let local = name.as_ref();
160 if is_ns(ns.clone(), OFFICE) {
161 return match local {
162 b"document-content" => Tag::Document,
163 b"body" => Tag::Body,
164 b"spreadsheet" => Tag::Spreadsheet,
165 _ => Tag::Other,
166 };
167 }
168 if is_ns(ns.clone(), TABLE) {
169 return match local {
170 b"table" => Tag::Table,
171 b"table-row-group" | b"table-header-rows" | b"table-rows" => Tag::Rows,
172 b"table-row" => Tag::Row,
173 b"table-cell" => Tag::Cell,
174 b"covered-table-cell" => Tag::Covered,
175 _ => Tag::Other,
176 };
177 }
178 if is_ns(ns, TEXT) {
179 return match local {
180 b"p" | b"h" => Tag::Paragraph,
181 b"span" | b"a" | b"meta" | b"meta-field" | b"ruby" | b"ruby-base" => Tag::Inline,
182 b"s" => Tag::Space,
183 b"tab" => Tag::Tab,
184 b"line-break" => Tag::Break,
185 _ => Tag::Other,
186 };
187 }
188 Tag::Other
189}
190
191struct SheetDraft {
192 sheet: Sheet,
193 depth: usize,
194 row: u32,
195}
196struct RowDraft {
197 depth: usize,
198 repeat: u32,
199 col: u32,
200 cells: Vec<RepeatedCell>,
201}
202struct RepeatedCell {
203 col: u32,
204 repeat: u32,
205 width: u32,
206 height: u32,
207 cell: Cell,
208}
209struct CellDraft {
210 depth: usize,
211 repeat: u32,
212 width: u32,
213 height: u32,
214 covered: bool,
215 kind: Option<String>,
216 raw: Option<String>,
217 formula: bool,
218 text: String,
219 paragraph: Option<usize>,
220 paragraphs: usize,
221 paragraph_started: bool,
222 pending_space: bool,
223}
224struct Import {
225 wb: Workbook,
226 report: ReadReport,
227 stack: Vec<Tag>,
228 sheet: Option<SheetDraft>,
229 row: Option<RowDraft>,
230 cell: Option<CellDraft>,
231 cells: u64,
232 merges: u64,
233 text: u64,
234 formats: Vec<(&'static str, StyleId)>,
235 spreadsheet_seen: bool,
236}
237
238impl Import {
239 fn new() -> Self {
240 let mut wb = Workbook::new();
241 wb.sheets.clear();
242 let mut report = ReadReport::default();
243 report.warn("ODS data-only import: formulas become their saved results and will not recalculate. Source formatting, charts, images and other features are not imported. Save a new .xlsx file; ODS saving is not supported.");
244 Self { wb, report, stack: vec![], sheet: None, row: None, cell: None, cells: 0, merges: 0, text: 0, formats: vec![], spreadsheet_seen: false }
245 }
246
247 fn parse(&mut self, bytes: &[u8]) -> Result<(), IoError> {
248 let mut reader = NsReader::from_reader(bytes);
249 let mut root_seen = false;
250 loop {
251 match reader.read_event().map_err(xml_error)? {
252 Event::Start(e) => {
253 if self.stack.len() >= crate::xlsx::xml::MAX_DEPTH {
254 return Err(xml_error("XML nested too deeply"));
255 }
256 if self.stack.is_empty() {
257 if root_seen || tag(&mut reader, &e) != Tag::Document {
258 return Err(xml_error("expected one office:document-content root"));
259 }
260 root_seen = true;
261 }
262 self.start(&mut reader, &e)?;
263 }
264 Event::Empty(e) => {
265 if self.stack.is_empty() {
266 return Err(xml_error("expected office:document-content with a spreadsheet"));
267 }
268 self.start(&mut reader, &e)?;
269 self.end()?;
270 }
271 Event::End(_) => self.end()?,
272 Event::Text(t) => {
273 let text = t.xml_content(XmlVersion::Explicit1_0).map_err(xml_error)?;
274 if self.stack.is_empty() && !text.trim().is_empty() {
275 return Err(xml_error("text outside the document"));
276 }
277 self.append(&text)?;
278 }
279 Event::CData(t) => self.append(&t.decode().map_err(xml_error)?)?,
280 Event::GeneralRef(r) => {
281 let ch = match r.resolve_char_ref().map_err(xml_error)? {
282 Some(c) => c,
283 None => match &*r {
284 b"amp" => '&',
285 b"lt" => '<',
286 b"gt" => '>',
287 b"apos" => '\'',
288 b"quot" => '"',
289 _ => return Err(xml_error("unsupported XML entity")),
290 },
291 };
292 self.append(ch.encode_utf8(&mut [0; 4]))?;
293 }
294 Event::DocType(_) => return Err(xml_error("DOCTYPE is not supported")),
295 Event::Eof => break,
296 _ => {}
297 }
298 }
299 if !root_seen || !self.stack.is_empty() || !self.spreadsheet_seen {
300 return Err(xml_error("incomplete ODS document"));
301 }
302 Ok(())
303 }
304
305 fn start(&mut self, reader: &mut NsReader<&[u8]>, e: &BytesStart<'_>) -> Result<(), IoError> {
306 let t = tag(reader, e);
307 let depth = self.stack.len();
308 match t {
309 Tag::Spreadsheet if self.stack == [Tag::Document, Tag::Body] => {
310 if self.spreadsheet_seen {
311 return Err(xml_error("multiple spreadsheets in one document"));
312 }
313 self.spreadsheet_seen = true;
314 }
315 Tag::Table if self.stack == [Tag::Document, Tag::Body, Tag::Spreadsheet] => {
316 if self.wb.sheets.len() >= MAX_SHEETS {
317 return Err(IoError::TooLarge("ODS contains more than 1024 sheets".into()));
318 }
319 let original = attr(reader, e, TABLE, b"name")?.unwrap_or_default();
320 let mut name = crate::xlsx::read::SheetNames::of(&self.wb).unique(original.trim_matches('\''), self.wb.sheets.len());
321 if self.wb.check_sheet_name(&name, None).is_err() {
322 name = self.wb.next_sheet_name();
323 }
324 if name != original {
325 self.report.warn("Some ODS sheet names were shortened or renamed to valid, unique XLSX sheet names.");
326 }
327 self.sheet = Some(SheetDraft { sheet: Sheet::new(name), depth, row: 0 });
328 }
329 Tag::Row if self.sheet.as_ref().is_some_and(|s| self.stack.get(s.depth + 1..).is_some_and(|p| p.iter().all(|t| *t == Tag::Rows))) => {
330 self.row =
331 Some(RowDraft { depth, repeat: positive(attr(reader, e, TABLE, b"number-rows-repeated")?, 1, u32::MAX)?, col: 0, cells: vec![] });
332 }
333 Tag::Cell | Tag::Covered if self.row.as_ref().is_some_and(|r| depth == r.depth + 1) => {
334 let kind = if attr(reader, e, CALC_EXT, b"value-type")?.as_deref() == Some("error") {
335 Some("error".into())
336 } else {
337 attr(reader, e, OFFICE, b"value-type")?
338 };
339 let key: &[u8] = match kind.as_deref() {
340 Some("boolean") => b"boolean-value",
341 Some("date") => b"date-value",
342 Some("time") => b"time-value",
343 Some("string") => b"string-value",
344 _ => b"value",
345 };
346 self.cell = Some(CellDraft {
347 depth,
348 repeat: positive(attr(reader, e, TABLE, b"number-columns-repeated")?, 1, u32::MAX)?,
349 width: positive(attr(reader, e, TABLE, b"number-columns-spanned")?, 1, MAX_COLS)?,
350 height: positive(attr(reader, e, TABLE, b"number-rows-spanned")?, 1, MAX_ROWS)?,
351 covered: t == Tag::Covered,
352 kind,
353 raw: attr(reader, e, OFFICE, key)?,
354 formula: attr(reader, e, TABLE, b"formula")?.is_some(),
355 text: String::new(),
356 paragraph: None,
357 paragraphs: 0,
358 paragraph_started: false,
359 pending_space: false,
360 });
361 }
362 Tag::Paragraph if self.cell.as_ref().is_some_and(|c| depth == c.depth + 1) => {
363 if let Some(cell) = self.cell.as_mut() {
364 if cell.paragraphs > 0 {
365 push_text(&mut cell.text, "\n")?;
366 }
367 cell.paragraph = Some(depth);
368 cell.paragraphs += 1;
369 cell.paragraph_started = false;
370 cell.pending_space = false;
371 }
372 }
373 Tag::Space | Tag::Tab | Tag::Break if self.captures_text() => {
374 let count = if t == Tag::Space {
375 let n = attr(reader, e, TEXT, b"c")?.map(|s| s.parse::<u32>()).transpose().map_err(xml_error)?.unwrap_or(1);
376 if n as usize > MAX_CELL_TEXT {
377 return Err(IoError::TooLarge("ODS repeated spaces exceed 1 MiB".into()));
378 }
379 n
380 } else {
381 1
382 };
383 let value = match t {
384 Tag::Space => " ",
385 Tag::Tab => "\t",
386 _ => "\n",
387 };
388 if let Some(cell) = self.cell.as_mut() {
389 if cell.pending_space {
390 push_text(&mut cell.text, " ")?;
391 cell.pending_space = false;
392 }
393 cell.paragraph_started = true;
394 if cell.text.len().saturating_add(count as usize) > MAX_CELL_TEXT {
395 return Err(IoError::TooLarge("ODS cell text exceeds 1 MiB".into()));
396 }
397 for _ in 0..count {
398 cell.text.push_str(value);
399 }
400 }
401 }
402 _ => {}
403 }
404 self.stack.push(t);
405 Ok(())
406 }
407
408 fn captures_text(&self) -> bool {
409 self.cell
410 .as_ref()
411 .and_then(|c| c.paragraph)
412 .is_some_and(|depth| self.stack.get(depth..).is_some_and(|p| p.iter().all(|t| matches!(t, Tag::Paragraph | Tag::Inline))))
413 }
414
415 fn append(&mut self, text: &str) -> Result<(), IoError> {
416 if self.captures_text()
417 && let Some(cell) = self.cell.as_mut()
418 {
419 for ch in text.chars() {
422 if matches!(ch, ' ' | '\t' | '\n' | '\r') {
423 cell.pending_space = cell.paragraph_started;
424 } else {
425 if cell.pending_space {
426 push_text(&mut cell.text, " ")?;
427 cell.pending_space = false;
428 }
429 push_text(&mut cell.text, ch.encode_utf8(&mut [0; 4]))?;
430 cell.paragraph_started = true;
431 }
432 }
433 }
434 Ok(())
435 }
436
437 fn end(&mut self) -> Result<(), IoError> {
438 self.stack.pop().ok_or_else(|| xml_error("unexpected closing element"))?;
439 let depth = self.stack.len();
440 if let Some(cell) = self.cell.as_mut()
441 && cell.paragraph == Some(depth)
442 {
443 cell.paragraph = None;
444 }
445 if self.cell.as_ref().is_some_and(|c| c.depth == depth) {
446 self.finish_cell()?;
447 }
448 if self.row.as_ref().is_some_and(|r| r.depth == depth) {
449 self.finish_row()?;
450 }
451 if self.sheet.as_ref().is_some_and(|s| s.depth == depth)
452 && let Some(sheet) = self.sheet.take()
453 {
454 self.wb.sheets.push(Arc::new(sheet.sheet));
455 }
456 Ok(())
457 }
458
459 fn finish_cell(&mut self) -> Result<(), IoError> {
460 let Some(draft) = self.cell.take() else { return Ok(()) };
461 let cell = self.convert(&draft);
462 let (width, height) = if draft.covered { (1, 1) } else { (draft.width, draft.height) };
463 let Some(row) = self.row.as_mut() else { return Ok(()) };
464 let kept = !cell.is_blank() || width > 1 || height > 1;
465 let end = match row.col.checked_add(draft.repeat).filter(|n| *n <= MAX_COLS) {
468 Some(end) => end,
469 None if !kept => MAX_COLS,
470 None => return Err(IoError::TooLarge("ODS row exceeds worksheet width".into())),
471 };
472 if kept {
473 if let Value::Text(t) = &cell.value {
476 self.text = self.text.saturating_add(u64::from(draft.repeat).saturating_mul(t.len() as u64));
477 if self.text > MAX_TEXT {
478 return Err(IoError::TooLarge("ODS expands beyond the import limit (64 MiB of text)".into()));
479 }
480 }
481 row.cells.push(RepeatedCell { col: row.col, repeat: draft.repeat, width, height, cell });
482 }
483 row.col = end;
484 Ok(())
485 }
486
487 fn convert(&mut self, draft: &CellDraft) -> Cell {
488 let raw = draft.raw.as_deref();
489 let mut format = None;
490 let value = match draft.kind.as_deref() {
491 Some("float" | "percentage" | "currency") => raw.and_then(|s| s.parse::<f64>().ok()).filter(|n| n.is_finite()).map(|n| {
492 if draft.kind.as_deref() == Some("percentage") {
493 format = Some("0.00%");
494 }
495 Value::Number(n)
496 }),
497 Some("boolean") => match raw {
498 Some("true" | "1") => Some(Value::Bool(true)),
499 Some("false" | "0") => Some(Value::Bool(false)),
500 _ => None,
501 },
502 Some("string") if raw.is_some() || draft.paragraphs > 0 || !draft.formula => Some(Value::Text(Arc::from(raw.unwrap_or(&draft.text)))),
503 Some("void") if !draft.formula => Some(Value::Empty),
504 Some("date") => raw.and_then(parse_date).map(|(n, f)| {
505 format = Some(f);
506 Value::Number(n)
507 }),
508 Some("time") => raw.and_then(parse_duration).map(|n| {
509 format = Some("[h]:mm:ss.000");
510 Value::Number(n)
511 }),
512 None if !draft.formula => Some(if draft.text.is_empty() { Value::Empty } else { Value::Text(Arc::from(draft.text.as_str())) }),
513 _ => None,
514 };
515 let value = value.unwrap_or_else(|| {
516 self.report.warn("Some ODS values or formula caches were unsupported or missing; their displayed text (or raw value) was kept as text. A formula without either is marked [Unavailable ODS formula result].");
517 let text = if !draft.text.is_empty() { draft.text.as_str() } else if let Some(raw) = raw { raw } else if draft.formula { "[Unavailable ODS formula result]" } else { "[Unsupported ODS value]" };
518 Value::Text(Arc::from(text))
519 });
520 let style = match format {
521 Some(code) => match self.formats.iter().find(|(f, _)| *f == code) {
522 Some((_, id)) => *id,
523 None => {
524 let id = self.wb.styles.derive(StyleId::DEFAULT, |s| s.num_fmt = NumFmt::new(code));
525 self.formats.push((code, id));
526 id
527 }
528 },
529 None => StyleId::DEFAULT,
530 };
531 Cell { style, ..Cell::value(value) }
532 }
533
534 fn finish_row(&mut self) -> Result<(), IoError> {
535 let Some(row) = self.row.take() else { return Ok(()) };
536 let Some(sheet) = self.sheet.as_mut() else { return Ok(()) };
537 let end = match sheet.row.checked_add(row.repeat).filter(|n| *n <= MAX_ROWS) {
539 Some(end) => end,
540 None if row.cells.is_empty() => MAX_ROWS,
541 None => return Err(IoError::TooLarge("ODS exceeds worksheet height".into())),
542 };
543 for entry in row.cells {
544 let count = u64::from(entry.repeat) * u64::from(row.repeat);
545 if !entry.cell.is_blank() {
546 self.cells = self.cells.saturating_add(count);
547 if let Value::Text(t) = &entry.cell.value {
548 let repeats = count.saturating_sub(u64::from(entry.repeat));
549 self.text = self.text.saturating_add(repeats.saturating_mul(t.len() as u64));
550 }
551 }
552 let merged = entry.width > 1 || entry.height > 1;
553 if merged {
554 self.merges = self.merges.saturating_add(count);
555 }
556 if self.cells > MAX_CELLS || self.merges > MAX_MERGES || self.text > MAX_TEXT {
557 return Err(IoError::TooLarge("ODS expands beyond the import limit (1 million cells, 100000 merges or 64 MiB of text)".into()));
558 }
559 let last_col = entry.col.checked_add(entry.repeat - 1).and_then(|c| c.checked_add(entry.width - 1));
560 let last_row = end.checked_add(entry.height - 1);
561 if last_col.is_none_or(|c| c >= MAX_COLS) || last_row.is_none_or(|r| r > MAX_ROWS) {
562 return Err(IoError::TooLarge("ODS merged cell exceeds worksheet bounds".into()));
563 }
564 if merged && ((entry.repeat > 1 && entry.width > 1) || (row.repeat > 1 && entry.height > 1)) {
565 return Err(IoError::Format("repeated ODS cells have overlapping merges".into()));
566 }
567 for r in sheet.row..end {
568 for c in entry.col..entry.col + entry.repeat {
569 let pos = CellRef::new(r, c);
570 if !entry.cell.is_blank() {
571 sheet.sheet.cells.set(pos, entry.cell.clone());
572 }
573 if merged {
574 sheet.sheet.merges.push(RangeRef::new(pos, CellRef::new(r + entry.height - 1, c + entry.width - 1)));
575 }
576 }
577 }
578 }
579 sheet.row = end;
580 Ok(())
581 }
582}
583
584fn push_text(out: &mut String, text: &str) -> Result<(), IoError> {
585 if out.len().saturating_add(text.len()) > MAX_CELL_TEXT {
586 return Err(IoError::TooLarge("ODS cell text exceeds 1 MiB".into()));
587 }
588 out.push_str(text);
589 Ok(())
590}
591
592fn parse_date(s: &str) -> Option<(f64, &'static str)> {
595 let (date, time) = s.split_once('T').map_or((s, None), |(d, t)| (d, Some(t)));
596 if date.len() != 10 || date.get(4..5)? != "-" || date.get(7..8)? != "-" {
597 return None;
598 }
599 let year: i32 = digits(date.get(..4)?)?.parse().ok()?;
600 let month: u32 = digits(date.get(5..7)?)?.parse().ok()?;
601 let day: u32 = digits(date.get(8..10)?)?.parse().ok()?;
602 if !(1900..=9999).contains(&year) || !(1..=12).contains(&month) || day == 0 || day > days_in_month(year, month) {
603 return None;
604 }
605 let day = serial_from_ymd(DateSystem::D1900, year.into(), month.into(), day.into())?;
606 let Some(time) = time else { return Some((day, "yyyy-mm-dd")) };
607 let mut parts = time.split(':');
608 let h = parts.next()?;
609 let m = parts.next()?;
610 let sec = parts.next()?;
611 if parts.next().is_some() || h.len() != 2 || m.len() != 2 || sec.split('.').next()?.len() != 2 {
612 return None;
613 }
614 let h: u32 = digits(h)?.parse().ok()?;
615 let m: u32 = digits(m)?.parse().ok()?;
616 let sec = decimal(sec)?;
617 if h >= 24 || m >= 60 || sec >= 60.0 {
618 return None;
619 }
620 Some((day + (h as f64 * 3600.0 + m as f64 * 60.0 + sec) / 86400.0, "yyyy-mm-dd hh:mm:ss.000"))
621}
622
623fn digits(s: &str) -> Option<&str> {
624 (!s.is_empty() && s.bytes().all(|b| b.is_ascii_digit())).then_some(s)
625}
626fn decimal(s: &str) -> Option<f64> {
627 match s.split_once('.') {
628 Some((a, b)) => {
629 digits(a)?;
630 digits(b)?;
631 }
632 None => {
633 digits(s)?;
634 }
635 }
636 s.parse::<f64>().ok().filter(|n| n.is_finite())
637}
638
639fn parse_duration(s: &str) -> Option<f64> {
641 let (negative, s) = s.strip_prefix('-').map_or((false, s), |s| (true, s));
642 let mut rest = s.strip_prefix('P')?;
643 let mut total = 0.0;
644 let mut any = false;
645 if let Some((days, tail)) = rest.split_once('D') {
646 total += digits(days)?.parse::<f64>().ok()? * 86400.0;
647 rest = tail;
648 any = true;
649 }
650 if let Some(mut time) = rest.strip_prefix('T') {
651 let mut time_any = false;
652 for (unit, factor) in [('H', 3600.0), ('M', 60.0), ('S', 1.0)] {
653 if let Some((n, tail)) = time.split_once(unit) {
654 let value = if unit == 'S' { decimal(n)? } else { digits(n)?.parse::<f64>().ok()? };
655 total += value * factor;
656 time = tail;
657 time_any = true;
658 }
659 }
660 if !time.is_empty() || !time_any {
661 return None;
662 }
663 any = true;
664 } else if !rest.is_empty() {
665 return None;
666 }
667 let days = total / 86400.0;
668 (any && days.is_finite() && days <= 2_958_465.0).then_some(if negative { -days } else { days })
669}