Skip to main content

iri_rs_core/
mutate.rs

1//! Mutation methods on owning buffer types (`IriBuf`, `IriRefBuf`, `UriBuf`, `UriRefBuf`).
2
3use crate::{
4    components::first_segment_has_colon,
5    error::{InvalidIri, InvalidUri, IriParseError},
6    parse::find_iri_ref_positions,
7    resolve::resolve,
8    types::{Iri, IriBuf, IriRef, IriRefBuf, Uri, UriBuf, UriRef, UriRefBuf},
9    validate::{validate_authority_body, validate_fragment, validate_iri_ref, validate_path, validate_query, validate_resolved_path, validate_scheme},
10};
11
12impl IriRefBuf {
13    /// # Errors
14    ///
15    /// Returns an error when the new scheme is not a valid scheme, or when removing it would leave a reference that cannot be reparsed. The value is left untouched.
16    pub fn set_scheme(&mut self, scheme: Option<&str>) -> Result<(), IriParseError> {
17        if let Some(s) = scheme {
18            validate_scheme(s)?;
19        }
20        rebuild(self, &Edit::Scheme(scheme.map(str::to_string)), true)
21    }
22
23    /// # Errors
24    ///
25    /// Returns an error when the new authority is not valid in that position. The value is left untouched.
26    pub fn set_authority(&mut self, authority: Option<&str>) -> Result<(), IriParseError> {
27        if let Some(a) = authority {
28            validate_authority_body(a, true)?;
29        }
30        rebuild(self, &Edit::Authority(authority.map(str::to_string)), true)
31    }
32
33    /// # Errors
34    ///
35    /// Returns an error when the new path is not valid in that position. The value is left untouched.
36    pub fn set_path(&mut self, path: &str) -> Result<(), IriParseError> {
37        validate_path(path, true)?;
38        rebuild(self, &Edit::Path(path.to_string()), true)
39    }
40
41    /// # Errors
42    ///
43    /// Returns an error when the new query is not valid in that position. The value is left untouched.
44    pub fn set_query(&mut self, query: Option<&str>) -> Result<(), IriParseError> {
45        if let Some(q) = query {
46            validate_query(q, true)?;
47        }
48        rebuild(self, &Edit::Query(query.map(str::to_string)), true)
49    }
50
51    /// # Errors
52    ///
53    /// Returns an error when the new fragment is not valid in that position. The value is left untouched.
54    pub fn set_fragment(&mut self, fragment: Option<&str>) -> Result<(), IriParseError> {
55        if let Some(f) = fragment {
56            validate_fragment(f, true)?;
57        }
58        rebuild(self, &Edit::Fragment(fragment.map(str::to_string)), true)
59    }
60
61    /// # Errors
62    ///
63    /// Returns an error when the reference and base do not recompose into a valid IRI reference.
64    pub fn resolve<T: std::ops::Deref<Target = str>>(&mut self, base: &Iri<T>) -> Result<(), IriParseError> {
65        let mut out = String::with_capacity(self.as_str().len() + base.as_str().len());
66        let pos = resolve((base.as_str(), base.positions()), (self.as_str(), self.positions), &mut out);
67        validate_resolved_path(&out, pos)?;
68        *self = IriRefBuf::from_raw_parts(out, pos);
69        Ok(())
70    }
71
72    /// # Errors
73    ///
74    /// Returns the reference unchanged when it has no scheme, since an IRI is by definition absolute.
75    pub fn try_into_iri(self) -> Result<IriBuf, InvalidIri<IriRefBuf>> {
76        if self.is_absolute() {
77            let p = self.positions;
78            Ok(Iri::from_raw_parts(self.into_inner(), p))
79        } else {
80            Err(InvalidIri(self))
81        }
82    }
83
84    #[must_use]
85    pub fn as_iri(&self) -> Option<Iri<&str>> {
86        if self.is_absolute() {
87            Some(Iri::from_raw_parts(self.as_str(), self.positions))
88        } else {
89            None
90        }
91    }
92}
93
94impl Default for IriRefBuf {
95    fn default() -> Self {
96        Self::parse_unchecked(String::new())
97    }
98}
99
100impl Default for UriRefBuf {
101    fn default() -> Self {
102        Self::parse_unchecked(String::new())
103    }
104}
105
106impl IriBuf {
107    /// # Errors
108    ///
109    /// Returns an error when the new scheme is not a valid scheme, or when removing it would leave a reference that cannot be reparsed. The value is left untouched.
110    pub fn set_scheme(&mut self, scheme: &str) -> Result<(), IriParseError> {
111        validate_scheme(scheme)?;
112        let p = self.positions();
113        let iri = std::mem::take(&mut self.0.iri);
114        let mut r = IriRefBuf::from_raw_parts(iri, p);
115        rebuild(&mut r, &Edit::Scheme(Some(scheme.to_string())), true)?;
116        *self = Iri::from_raw_parts(r.iri, r.positions);
117        Ok(())
118    }
119
120    /// # Errors
121    ///
122    /// Returns an error when the new authority is not valid in that position. The value is left untouched.
123    pub fn set_authority(&mut self, authority: Option<&str>) -> Result<(), IriParseError> {
124        let p = self.positions();
125        let iri = std::mem::take(&mut self.0.iri);
126        let mut r = IriRefBuf::from_raw_parts(iri, p);
127        r.set_authority(authority)?;
128        *self = Iri::from_raw_parts(r.iri, r.positions);
129        Ok(())
130    }
131
132    /// # Errors
133    ///
134    /// Returns an error when the new path is not valid in that position. The value is left untouched.
135    pub fn set_path(&mut self, path: &str) -> Result<(), IriParseError> {
136        let p = self.positions();
137        let iri = std::mem::take(&mut self.0.iri);
138        let mut r = IriRefBuf::from_raw_parts(iri, p);
139        r.set_path(path)?;
140        *self = Iri::from_raw_parts(r.iri, r.positions);
141        Ok(())
142    }
143
144    /// # Errors
145    ///
146    /// Returns an error when the new query is not valid in that position. The value is left untouched.
147    pub fn set_query(&mut self, query: Option<&str>) -> Result<(), IriParseError> {
148        let p = self.positions();
149        let iri = std::mem::take(&mut self.0.iri);
150        let mut r = IriRefBuf::from_raw_parts(iri, p);
151        r.set_query(query)?;
152        *self = Iri::from_raw_parts(r.iri, r.positions);
153        Ok(())
154    }
155
156    /// # Errors
157    ///
158    /// Returns an error when the new fragment is not valid in that position. The value is left untouched.
159    pub fn set_fragment(&mut self, fragment: Option<&str>) -> Result<(), IriParseError> {
160        let p = self.positions();
161        let iri = std::mem::take(&mut self.0.iri);
162        let mut r = IriRefBuf::from_raw_parts(iri, p);
163        r.set_fragment(fragment)?;
164        *self = Iri::from_raw_parts(r.iri, r.positions);
165        Ok(())
166    }
167}
168
169impl UriRefBuf {
170    /// # Errors
171    ///
172    /// Returns an error when the new scheme is not a valid scheme, or when removing it would leave a reference that cannot be reparsed. The value is left untouched.
173    pub fn set_scheme(&mut self, scheme: Option<&str>) -> Result<(), IriParseError> {
174        if let Some(s) = scheme {
175            validate_scheme(s)?;
176        }
177        rebuild_uri(self, &Edit::Scheme(scheme.map(str::to_string)))
178    }
179    /// # Errors
180    ///
181    /// Returns an error when the new authority is not valid in that position. The value is left untouched.
182    pub fn set_authority(&mut self, authority: Option<&str>) -> Result<(), IriParseError> {
183        if let Some(a) = authority {
184            validate_authority_body(a, false)?;
185        }
186        rebuild_uri(self, &Edit::Authority(authority.map(str::to_string)))
187    }
188    /// # Errors
189    ///
190    /// Returns an error when the new path is not valid in that position. The value is left untouched.
191    pub fn set_path(&mut self, path: &str) -> Result<(), IriParseError> {
192        validate_path(path, false)?;
193        rebuild_uri(self, &Edit::Path(path.to_string()))
194    }
195    /// # Errors
196    ///
197    /// Returns an error when the new query is not valid in that position. The value is left untouched.
198    pub fn set_query(&mut self, query: Option<&str>) -> Result<(), IriParseError> {
199        if let Some(q) = query {
200            validate_query(q, false)?;
201        }
202        rebuild_uri(self, &Edit::Query(query.map(str::to_string)))
203    }
204    /// # Errors
205    ///
206    /// Returns an error when the new fragment is not valid in that position. The value is left untouched.
207    pub fn set_fragment(&mut self, fragment: Option<&str>) -> Result<(), IriParseError> {
208        if let Some(f) = fragment {
209            validate_fragment(f, false)?;
210        }
211        rebuild_uri(self, &Edit::Fragment(fragment.map(str::to_string)))
212    }
213    /// # Errors
214    ///
215    /// Returns an error when the reference and base do not recompose into a valid IRI reference.
216    pub fn resolve<T: std::ops::Deref<Target = str>>(&mut self, base: &Uri<T>) -> Result<(), IriParseError> {
217        let mut out = String::with_capacity(self.as_str().len() + base.as_str().len());
218        let pos = resolve((base.as_str(), base.positions()), (self.as_str(), self.positions), &mut out);
219        validate_resolved_path(&out, pos)?;
220        *self = UriRefBuf::from_raw_parts(out, pos);
221        Ok(())
222    }
223
224    /// # Errors
225    ///
226    /// Returns the reference unchanged when it has no scheme, since a URI is by definition absolute.
227    pub fn try_into_uri(self) -> Result<UriBuf, InvalidUri<UriRefBuf>> {
228        if self.is_absolute() {
229            let p = self.positions;
230            Ok(Uri::from_raw_parts(self.into_inner(), p))
231        } else {
232            Err(InvalidUri(self))
233        }
234    }
235
236    #[must_use]
237    pub fn as_uri(&self) -> Option<Uri<&str>> {
238        if self.is_absolute() {
239            Some(Uri::from_raw_parts(self.as_str(), self.positions))
240        } else {
241            None
242        }
243    }
244}
245
246impl UriBuf {
247    /// # Errors
248    ///
249    /// Returns an error when the new scheme is not a valid scheme, or when removing it would leave a reference that cannot be reparsed. The value is left untouched.
250    pub fn set_scheme(&mut self, scheme: &str) -> Result<(), IriParseError> {
251        let p = self.positions();
252        let uri = std::mem::take(&mut self.0.uri);
253        let mut r = UriRefBuf::from_raw_parts(uri, p);
254        r.set_scheme(Some(scheme))?;
255        *self = Uri::from_raw_parts(r.uri, r.positions);
256        Ok(())
257    }
258    /// # Errors
259    ///
260    /// Returns an error when the new authority is not valid in that position. The value is left untouched.
261    pub fn set_authority(&mut self, authority: Option<&str>) -> Result<(), IriParseError> {
262        let p = self.positions();
263        let uri = std::mem::take(&mut self.0.uri);
264        let mut r = UriRefBuf::from_raw_parts(uri, p);
265        r.set_authority(authority)?;
266        *self = Uri::from_raw_parts(r.uri, r.positions);
267        Ok(())
268    }
269    /// # Errors
270    ///
271    /// Returns an error when the new path is not valid in that position. The value is left untouched.
272    pub fn set_path(&mut self, path: &str) -> Result<(), IriParseError> {
273        let p = self.positions();
274        let uri = std::mem::take(&mut self.0.uri);
275        let mut r = UriRefBuf::from_raw_parts(uri, p);
276        r.set_path(path)?;
277        *self = Uri::from_raw_parts(r.uri, r.positions);
278        Ok(())
279    }
280    /// # Errors
281    ///
282    /// Returns an error when the new query is not valid in that position. The value is left untouched.
283    pub fn set_query(&mut self, query: Option<&str>) -> Result<(), IriParseError> {
284        let p = self.positions();
285        let uri = std::mem::take(&mut self.0.uri);
286        let mut r = UriRefBuf::from_raw_parts(uri, p);
287        r.set_query(query)?;
288        *self = Uri::from_raw_parts(r.uri, r.positions);
289        Ok(())
290    }
291    /// # Errors
292    ///
293    /// Returns an error when the new fragment is not valid in that position. The value is left untouched.
294    pub fn set_fragment(&mut self, fragment: Option<&str>) -> Result<(), IriParseError> {
295        let p = self.positions();
296        let uri = std::mem::take(&mut self.0.uri);
297        let mut r = UriRefBuf::from_raw_parts(uri, p);
298        r.set_fragment(fragment)?;
299        *self = Uri::from_raw_parts(r.uri, r.positions);
300        Ok(())
301    }
302}
303
304enum Edit {
305    Scheme(Option<String>),
306    Authority(Option<String>),
307    Path(String),
308    Query(Option<String>),
309    Fragment(Option<String>),
310}
311
312fn rebuild(buf: &mut IriRefBuf, edit: &Edit, is_iri: bool) -> Result<(), IriParseError> {
313    let out = {
314        let s = buf.as_str();
315        let (scheme, authority, path, query, fragment) = destructure(s, buf.positions);
316        let mut out = String::with_capacity(s.len() + 8);
317        match edit {
318            Edit::Scheme(new) => write_iri(&mut out, new.as_deref(), authority, path, query, fragment),
319            Edit::Authority(new) => write_iri(&mut out, scheme, new.as_deref(), path, query, fragment),
320            Edit::Path(new) => write_iri(&mut out, scheme, authority, new, query, fragment),
321            Edit::Query(new) => write_iri(&mut out, scheme, authority, path, new.as_deref(), fragment),
322            Edit::Fragment(new) => write_iri(&mut out, scheme, authority, path, query, new.as_deref()),
323        }
324        out
325    };
326    let positions = find_iri_ref_positions(&out);
327    validate_iri_ref(&out, positions, is_iri)?;
328    *buf = IriRef::from_raw_parts(out, positions);
329    Ok(())
330}
331
332fn write_iri(out: &mut String, scheme: Option<&str>, authority: Option<&str>, path: &str, query: Option<&str>, fragment: Option<&str>) {
333    if let Some(s) = scheme {
334        out.push_str(s);
335        out.push(':');
336    }
337    if let Some(a) = authority {
338        out.push_str("//");
339        out.push_str(a);
340    } else {
341        // Removing a component can leave the path looking like the component
342        // that is now gone. Both cases are recoverable with a dot segment,
343        // which denotes the same path but cannot be misread on the way back in.
344        if path.starts_with("//") {
345            // RFC 3986 §3.3: with no authority, a path may not begin with `//`.
346            out.push_str("/.");
347        } else if scheme.is_none() && first_segment_has_colon(path) {
348            // RFC 3986 §4.2: the first segment of a relative-path reference
349            // may not hold a `:`, or it reads as a scheme.
350            out.push_str("./");
351        }
352    }
353    out.push_str(path);
354    if let Some(q) = query {
355        out.push('?');
356        out.push_str(q);
357    }
358    if let Some(f) = fragment {
359        out.push('#');
360        out.push_str(f);
361    }
362}
363
364fn rebuild_uri(buf: &mut UriRefBuf, edit: &Edit) -> Result<(), IriParseError> {
365    let mut tmp: IriRefBuf = IriRef::from_raw_parts(buf.as_str().to_string(), buf.positions);
366    rebuild(&mut tmp, edit, false)?;
367    let p = tmp.positions;
368    *buf = UriRef::from_raw_parts(tmp.iri, p);
369    Ok(())
370}
371
372fn destructure(s: &str, p: crate::parse::Positions) -> (Option<&str>, Option<&str>, &str, Option<&str>, Option<&str>) {
373    let scheme = if p.scheme_end > 0 { Some(&s[..p.scheme_end - 1]) } else { None };
374    let authority = if p.authority_end > p.scheme_end + 2
375        || (p.authority_end == p.scheme_end + 2 && p.scheme_end + 2 <= s.len() && &s[p.scheme_end..p.scheme_end + 2] == "//")
376    {
377        Some(&s[p.scheme_end + 2..p.authority_end])
378    } else {
379        None
380    };
381    let path = &s[p.authority_end..p.path_end];
382    let query = if p.query_end > p.path_end {
383        Some(&s[p.path_end + 1..p.query_end])
384    } else {
385        None
386    };
387    let fragment = if s.len() > p.query_end { Some(&s[p.query_end + 1..]) } else { None };
388    (scheme, authority, path, query, fragment)
389}
390
391#[cfg(test)]
392mod tests {
393    use super::*;
394
395    fn iri_ref(s: &str) -> IriRefBuf {
396        IriRef::parse(s.to_owned()).unwrap()
397    }
398
399    /// Removing a scheme can leave the first path segment holding a `:`, which
400    /// would be read back as a scheme of its own (RFC 3986 §4.2).
401    #[test]
402    fn removing_a_scheme_disambiguates_a_first_segment_with_a_colon() {
403        let mut buf = iri_ref("scheme:a:b/c");
404        buf.set_scheme(None).unwrap();
405
406        assert_eq!(buf.as_str(), "./a:b/c");
407        assert_eq!(buf.scheme(), None, "the path must not be read back as a scheme");
408        assert_eq!(buf.path(), "./a:b/c");
409    }
410
411    /// Removing an authority can leave the path starting with `//`, which
412    /// would be read back as an authority (RFC 3986 §3.3).
413    #[test]
414    fn removing_an_authority_disambiguates_a_path_starting_with_two_slashes() {
415        let mut buf = iri_ref("//host//path");
416        buf.set_authority(None).unwrap();
417
418        assert_eq!(buf.as_str(), "/.//path");
419        assert_eq!(buf.authority(), None, "the path must not be read back as an authority");
420        assert_eq!(buf.path(), "/.//path");
421    }
422
423    #[test]
424    fn removing_an_authority_disambiguates_under_a_scheme_too() {
425        let mut buf = iri_ref("http://host//path");
426        buf.set_authority(None).unwrap();
427
428        assert_eq!(buf.as_str(), "http:/.//path");
429        assert_eq!(buf.scheme(), Some("http"));
430        assert_eq!(buf.authority(), None);
431        assert_eq!(buf.path(), "/.//path");
432    }
433
434    #[test]
435    fn setting_an_ambiguous_path_disambiguates_it() {
436        let mut buf = iri_ref("http:x");
437        buf.set_path("//foo").unwrap();
438        assert_eq!(buf.as_str(), "http:/.//foo");
439        assert_eq!(buf.authority(), None);
440
441        let mut buf = iri_ref("x");
442        buf.set_path("a:b").unwrap();
443        assert_eq!(buf.as_str(), "./a:b");
444        assert_eq!(buf.scheme(), None);
445    }
446
447    #[test]
448    fn unambiguous_removals_gain_no_dot_segment() {
449        let mut buf = iri_ref("http://host/path");
450        buf.set_authority(None).unwrap();
451        assert_eq!(buf.as_str(), "http:/path");
452
453        let mut buf = iri_ref("scheme:/a/b");
454        buf.set_scheme(None).unwrap();
455        assert_eq!(buf.as_str(), "/a/b");
456
457        // Only the *first* segment may not hold a `:`.
458        let mut buf = iri_ref("scheme:a/b:c");
459        buf.set_scheme(None).unwrap();
460        assert_eq!(buf.as_str(), "a/b:c");
461
462        // With an authority present the `//` is unambiguous.
463        let mut buf = iri_ref("http://host//path");
464        buf.set_scheme(None).unwrap();
465        assert_eq!(buf.as_str(), "//host//path");
466    }
467
468    #[test]
469    fn uri_buffers_disambiguate_the_same_way() {
470        let mut buf: UriRefBuf = UriRef::parse("//host//path".to_owned()).unwrap();
471        buf.set_authority(None).unwrap();
472        assert_eq!(buf.as_str(), "/.//path");
473        assert_eq!(buf.authority(), None);
474
475        let mut buf: UriRefBuf = UriRef::parse("scheme:a:b/c".to_owned()).unwrap();
476        buf.set_scheme(None).unwrap();
477        assert_eq!(buf.as_str(), "./a:b/c");
478        assert_eq!(buf.scheme(), None);
479    }
480
481    /// Upstream `iref` 4.0.1 fixed panics and silent corruption when a
482    /// percent-encoded value reached a `set_*` method: it sized buffers with
483    /// the decoded character count instead of the encoded byte length. This
484    /// recomposes the whole string rather than splicing bytes, so the bug
485    /// cannot occur — which stays true only as long as that holds.
486    #[test]
487    fn percent_encoded_values_survive_mutation() {
488        let mut buf = iri_ref("scheme://user:pass@example.com:8080/path");
489        buf.set_authority(Some("%6Eew_long_host.org")).unwrap();
490        assert_eq!(buf.as_str(), "scheme://%6Eew_long_host.org/path");
491        assert_eq!(buf.authority(), Some("%6Eew_long_host.org"));
492
493        let mut buf = iri_ref("scheme://%65xample.com:8080/path");
494        buf.set_authority(Some("example.com")).unwrap();
495        assert_eq!(buf.authority(), Some("example.com"));
496
497        let mut buf = iri_ref("scheme://authority/path");
498        buf.set_query(Some("%71uery")).unwrap();
499        buf.set_fragment(Some("%66rag")).unwrap();
500        assert_eq!(buf.as_str(), "scheme://authority/path?%71uery#%66rag");
501        assert_eq!(buf.query(), Some("%71uery"));
502        assert_eq!(buf.fragment(), Some("%66rag"));
503
504        buf.set_fragment(None).unwrap();
505        assert_eq!(buf.as_str(), "scheme://authority/path?%71uery");
506    }
507
508    /// The inserted dot segments denote the same path, so resolving the result
509    /// against a base reproduces the components the edit intended.
510    #[test]
511    fn disambiguated_results_resolve_to_the_intended_path() {
512        let mut buf = iri_ref("//host//path");
513        buf.set_authority(None).unwrap();
514
515        let base = Iri::parse("http://example.org/a/b").unwrap();
516        let resolved = buf.resolved(&base).unwrap();
517        assert_eq!(resolved.as_str(), "http://example.org//path");
518        assert_eq!(resolved.authority(), Some("example.org"));
519        assert_eq!(resolved.path(), "//path");
520    }
521}