Skip to main content

structured_asic/
uri.rs

1//! Package references retain ZIP identity without a filesystem interpretation.
2
3use structured_asn1::memory::{MemoryError, OwnedVec, Work};
4
5/// A query or fragment cannot prove whole-member coverage merely by selecting
6/// the same physical ZIP entry. Its original XML location remains the selector.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum Part {
9    /// Neither component was written.
10    Whole,
11    /// A query was written, including an explicitly empty query.
12    Query,
13    /// A fragment was written, including an explicitly empty fragment.
14    Fragment,
15    /// Both components were written.
16    QueryAndFragment,
17}
18
19/// Completed resolution facts, independent of signature mathematics and trust.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum Negative {
22    /// Scheme, authority or a relative path escaping the package base.
23    Outside,
24    /// Invalid percent encoding, control character or relative first segment.
25    Malformed,
26    /// Percent-decoded name is not UTF-8.
27    Encoding,
28}
29
30/// A lexical reference after package resolution. Exact lookup may still find
31/// zero or several original members; this value never chooses a duplicate.
32#[derive(Debug)]
33pub enum Target<'uri> {
34    /// A path-empty reference selects its original containing document.
35    SameDocument(Part),
36    /// The package root, which is not an empty-name member.
37    Root(Part),
38    /// A complete exact ZIP filename with independently retained capacity.
39    Member {
40        /// ZIP bytes after exactly one URI decoding, without case folding.
41        name: MemberName<'uri>,
42        /// Presence of a query/fragment, not a fabricated whole-object proof.
43        part: Part,
44    },
45    /// A completed negative outcome, never a resource refusal or missing entry.
46    Negative(Negative),
47}
48
49/// The common root-relative, unescaped form borrows the owner's existing URI
50/// view. Joining/decoding alone needs a funded buffer; lookup need not retain it.
51#[derive(Debug)]
52pub enum MemberName<'uri> {
53    /// No allocation, copy or account reference-count increment.
54    Borrowed(&'uri [u8]),
55    /// Actual retained capacity for a joined or percent-decoded name.
56    Owned(OwnedVec<u8>),
57}
58impl AsRef<[u8]> for MemberName<'_> {
59    fn as_ref(&self) -> &[u8] {
60        match self {
61            Self::Borrowed(bytes) => bytes,
62            Self::Owned(bytes) => bytes,
63        }
64    }
65}
66
67/// Resolves an original schema-normalized URI against its original ZIP name.
68/// EN319162-1 V1.1.1 A.6 overrides ODF1.2 part3 §3.7 for META-INF references:
69/// their base is the package root. Its explicit `/document.xml` form is allowed.
70/// <https://www.etsi.org/deliver/etsi_en/319100_319199/31916201/01.01.01_60/en_31916201v010101p.pdf>
71/// <https://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part3.html>
72pub fn resolve<'uri, W: Work>(
73    source_path: &[u8],
74    uri: &'uri str,
75    work: &W,
76) -> Result<Target<'uri>, MemoryError> {
77    let cost = source_path
78        .len()
79        .checked_add(uri.len())
80        .and_then(|n| n.checked_mul(16))
81        .and_then(|n| n.checked_add(1))
82        .ok_or(MemoryError::WorkExceeded)?;
83    work.charge_work(u64::try_from(cost).map_err(|_| MemoryError::WorkExceeded)?)?;
84    let bytes = uri.as_bytes();
85    if !lexical(bytes) {
86        return Ok(Target::Negative(Negative::Malformed));
87    }
88    // RFC3986 §2.4: split components before decoding. An escaped delimiter
89    // must not become a query/fragment or initiate a second path resolution.
90    let fragment = bytes.iter().position(|byte| *byte == b'#');
91    let before_fragment = &bytes[..fragment.unwrap_or(bytes.len())];
92    let query = before_fragment.iter().position(|byte| *byte == b'?');
93    let path = &before_fragment[..query.unwrap_or(before_fragment.len())];
94    let part = match (query.is_some(), fragment.is_some()) {
95        (false, false) => Part::Whole,
96        (true, false) => Part::Query,
97        (false, true) => Part::Fragment,
98        (true, true) => Part::QueryAndFragment,
99    };
100    if path.is_empty() {
101        return Ok(Target::SameDocument(part));
102    }
103    if path.starts_with(b"//") {
104        return Ok(Target::Negative(Negative::Outside));
105    }
106    let first = &path[..match path.iter().position(|byte| *byte == b'/') {
107        Some(at) => at,
108        None => path.len(),
109    }];
110    if first.contains(&b':') {
111        return Ok(Target::Negative(Negative::Outside));
112    }
113    let mut base = if source_path.starts_with(b"META-INF/") || path[0] == b'/' {
114        &[][..]
115    } else {
116        match source_path.iter().rposition(|byte| *byte == b'/') {
117            Some(at) => &source_path[..=at],
118            None => &[],
119        }
120    };
121    let mut path = if path[0] == b'/' { &path[1..] } else { path };
122    // ODF §3.7 steps7-12 process these leading lexical components before
123    // converting IRI bytes. Existing ZIP base components are never URI-decoded.
124    loop {
125        if path.starts_with(b"./") {
126            path = &path[2..];
127            continue;
128        }
129        if path == b"." {
130            path = &[];
131            break;
132        }
133        if path == b".." || path.starts_with(b"../") {
134            if base.is_empty() {
135                return Ok(Target::Negative(Negative::Outside));
136            }
137            let without_separator = &base[..base.len() - 1];
138            base = match without_separator.iter().rposition(|byte| *byte == b'/') {
139                Some(at) => &base[..=at],
140                None => &[],
141            };
142            path = if path.len() == 2 { &[] } else { &path[3..] };
143            continue;
144        }
145        break;
146    }
147    if base.is_empty() && !path.contains(&b'%') {
148        if path.is_empty() {
149            return Ok(Target::Root(part));
150        }
151        return Ok(Target::Member {
152            name: MemberName::Borrowed(path),
153            part,
154        });
155    }
156    decoded_name(base, path, part, work)
157}
158
159fn decoded_name<'uri, W: Work>(
160    base: &[u8],
161    path: &'uri [u8],
162    part: Part,
163    work: &W,
164) -> Result<Target<'uri>, MemoryError> {
165    let maximum = base
166        .len()
167        .checked_add(path.len())
168        .ok_or(MemoryError::LimitExceeded)?;
169    let mut name = work.vector();
170    name.try_reserve_exact(maximum)?;
171    name.try_extend_from_slice(base)?;
172    let mut rest = path;
173    while let Some(at) = rest.iter().position(|byte| *byte == b'%') {
174        name.try_extend_from_slice(&rest[..at])?;
175        if rest.len() - at < 3 {
176            return Ok(Target::Negative(Negative::Malformed));
177        }
178        let (Some(high), Some(low)) = (hex(rest[at + 1]), hex(rest[at + 2])) else {
179            return Ok(Target::Negative(Negative::Malformed));
180        };
181        // '+' is an ordinary URI path octet, never form-encoded whitespace.
182        name.try_push(high * 16 + low)?;
183        rest = &rest[at + 3..];
184    }
185    name.try_extend_from_slice(rest)?;
186    if core::str::from_utf8(&name).is_err() {
187        return Ok(Target::Negative(Negative::Encoding));
188    }
189    if name.is_empty() {
190        return Ok(Target::Root(part));
191    }
192    Ok(Target::Member {
193        name: MemberName::Owned(name.into_owned()),
194        part,
195    })
196}
197
198fn hex(byte: u8) -> Option<u8> {
199    match byte {
200        b'0'..=b'9' => Some(byte - b'0'),
201        b'a'..=b'f' => Some(byte - b'a' + 10),
202        b'A'..=b'F' => Some(byte - b'A' + 10),
203        _ => None,
204    }
205}
206
207fn lexical(bytes: &[u8]) -> bool {
208    let mut at = 0;
209    while at < bytes.len() {
210        let byte = bytes[at];
211        if byte < 0x20 || byte == 0x7f || byte == b'\\' {
212            return false;
213        }
214        if byte == b'%' {
215            if bytes.len() - at < 3 {
216                return false;
217            }
218            if hex(bytes[at + 1]).is_none() || hex(bytes[at + 2]).is_none() {
219                return false;
220            }
221            at += 3;
222        } else {
223            at += 1;
224        }
225    }
226    true
227}
228
229#[cfg(test)]
230mod tests;