Skip to main content

umya_spreadsheet/reader/
driver.rs

1use std::{
2    io,
3    path::{
4        Component,
5        Path,
6        PathBuf,
7    },
8    string::FromUtf8Error,
9};
10
11use quick_xml::events::attributes::Attribute;
12
13#[macro_export]
14macro_rules! xml_read_loop {
15    ($reader:ident $(,$pat:pat => $result:expr)+ $(,)?) => {
16        let mut buf = Vec::new();
17        loop {
18            let ev = match $reader.read_event_into(&mut buf) {
19                Ok(v) => v,
20                Err(e) => panic!("Error at position {}: {e:?}", $reader.buffer_position()),
21            };
22
23            match ev {
24                $($pat => $result,)+
25                _ => (),
26            }
27
28            buf.clear();
29        }
30    };
31}
32
33pub(crate) use crate::xml_read_loop;
34
35#[macro_export]
36macro_rules! set_string_from_xml {
37    ($self:ident, $e:ident, $attr:ident, $xml_attr:expr) => {{
38        if let Some(v) = get_attribute($e, $xml_attr.as_bytes()) {
39            $self.$attr.set_value_string(v);
40        }
41    }};
42}
43
44pub(crate) use crate::set_string_from_xml;
45
46pub(crate) fn normalize_path(path: &str) -> PathBuf {
47    let path = Path::new(path);
48    let mut components = path.components().peekable();
49    let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().copied() {
50        components.next();
51        PathBuf::from(c.as_os_str())
52    } else {
53        PathBuf::new()
54    };
55
56    for component in components {
57        match component {
58            Component::Prefix(..) => unreachable!(),
59            Component::RootDir => {
60                ret.push(component.as_os_str());
61            }
62            Component::CurDir => {}
63            Component::ParentDir => {
64                ret.pop();
65            }
66            Component::Normal(c) => {
67                ret.push(c);
68            }
69        }
70    }
71    ret
72}
73
74#[inline]
75pub(crate) fn join_paths(base_path: &str, target: &str) -> String {
76    match target.split_once('/') {
77        Some(("", target)) => normalize_path_to_str(target),
78        _ => normalize_path_to_str(&format!("{base_path}/{target}")),
79    }
80}
81
82#[inline]
83pub(crate) fn normalize_path_to_str(path: &str) -> String {
84    let ret = normalize_path(path);
85    ret.to_str().unwrap_or("").replace('\\', "/")
86}
87
88/// Look up a zip entry by name, tolerating backslash path separators and
89/// case differences that appear in XLSX files generated by some Windows tools.
90///
91/// Try order:
92/// 1. Exact match (fast path — zero overhead for well-formed files).
93/// 2. Backslash-separated variant of the requested name.
94/// 3. Case-insensitive scan with separator normalization (last resort).
95pub(crate) fn zip_by_name<'a, R: io::Read + io::Seek>(
96    arv: &'a mut zip::ZipArchive<R>,
97    name: &str,
98) -> Result<zip::read::ZipFile<'a, R>, zip::result::ZipError> {
99    // 1. Exact match (common case)
100    if arv.by_name(name).is_ok() {
101        // Re-borrow: `by_name` returns a ZipFile that borrows `arv`, so we
102        // must call it again after the probe to satisfy the borrow checker.
103        return arv.by_name(name);
104    }
105
106    // 2. Backslash variant: "xl/workbook.xml" → "xl\\workbook.xml"
107    let backslash_name = name.replace('/', "\\");
108    if backslash_name != name && arv.by_name(&backslash_name).is_ok() {
109        return arv.by_name(&backslash_name);
110    }
111
112    // 3. Case-insensitive scan with separator normalization
113    let normalized = name.replace('\\', "/").to_ascii_lowercase();
114    for i in 0..arv.len() {
115        let entry_name = match arv.by_index(i) {
116            Ok(f) => f.name().to_owned(),
117            Err(_) => continue,
118        };
119        let entry_normalized = entry_name.replace('\\', "/").to_ascii_lowercase();
120        if entry_normalized == normalized {
121            return arv.by_name(&entry_name);
122        }
123    }
124
125    Err(zip::result::ZipError::FileNotFound)
126}
127
128/// Look up an XML attribute by key, falling back to a match on just the
129/// local name (after the colon) when the exact prefixed key is not found.
130///
131/// This handles XLSX files that use non-standard namespace prefixes
132/// (e.g. `d3p1:id` instead of `r:id`).
133#[inline]
134pub(crate) fn get_attribute(e: &quick_xml::events::BytesStart<'_>, key: &[u8]) -> Option<String> {
135    // 1. Exact match (fast path)
136    let result = e
137        .attributes()
138        .with_checks(false)
139        .find_map(|attr| match attr {
140            Ok(ref attr) if attr.key.into_inner() == key => {
141                Some(get_attribute_value(attr).unwrap())
142            }
143            _ => None,
144        });
145    if result.is_some() {
146        return result;
147    }
148
149    // 2. For namespaced keys like "r:id", fall back to matching the local name
150    //    ("id") against any attribute whose local name matches. Returns early if
151    //    ':' is not found.
152    let local_name = {
153        let pos = key.iter().position(|&b| b == b':')?;
154        &key[pos + 1..]
155    };
156
157    e.attributes()
158        .with_checks(false)
159        .find_map(|attr| match attr {
160            Ok(ref attr) => {
161                let attr_key = attr.key.into_inner();
162                let attr_local = match attr_key.iter().position(|&b| b == b':') {
163                    Some(pos) => &attr_key[pos + 1..],
164                    None => attr_key,
165                };
166                if attr_local == local_name {
167                    Some(get_attribute_value(attr).unwrap())
168                } else {
169                    None
170                }
171            }
172            _ => None,
173        })
174}
175
176#[inline]
177pub(crate) fn get_attribute_value(attr: &Attribute) -> Result<String, FromUtf8Error> {
178    String::from_utf8(attr.value.to_vec())
179}