pkgsrc/
pkgpath.rs

1/*
2 * Copyright (c) 2024 Jonathan Perkin <jonathan@perkin.org.uk>
3 *
4 * Permission to use, copy, modify, and distribute this software for any
5 * purpose with or without fee is hereby granted, provided that the above
6 * copyright notice and this permission notice appear in all copies.
7 *
8 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15 */
16
17use std::path::{Component, Path, PathBuf};
18use std::str::FromStr;
19use thiserror::Error;
20
21#[cfg(feature = "serde")]
22use serde_with::{DeserializeFromStr, SerializeDisplay};
23
24/**
25 * An invalid path was specified trying to create a new [`PkgPath`].
26 */
27#[derive(Debug, Eq, Error, Ord, PartialEq, PartialOrd)]
28pub enum PkgPathError {
29    /**
30     * Contains an invalid path.
31     */
32    #[error("Invalid path specified")]
33    InvalidPath,
34}
35
36/**
37 * Handling for `PKGPATH` metadata and relative package directory locations.
38 *
39 * [`PkgPath`] is a struct for storing the path to a package within pkgsrc.
40 *
41 * Binary packages contain the `PKGPATH` metadata, for example
42 * `pkgtools/pkg_install`, while across pkgsrc dependencies are referred to by
43 * their relative location, for example `../../pkgtools/pkg_install`.
44 *
45 * [`PkgPath`] takes either format as input, validates it for correctness,
46 * then stores both internally as [`PathBuf`] entries.
47 *
48 * Once stored, [`as_path`] returns the short path as a [`Path`], while
49 * [`as_full_path`] returns the full relative path as a [`Path`].
50 *
51 * As [`PkgPath`] uses [`PathBuf`] under the hood, there is a small amount of
52 * normalisation performed, for example trailing or double slashes, but
53 * otherwise input strings are expected to be precisely formatted, and a
54 * [`PkgPathError`] is raised otherwise.
55 *
56 * ## Examples
57 *
58 * ```
59 * use pkgsrc::PkgPath;
60 * use std::ffi::OsStr;
61 *
62 * let p = PkgPath::new("pkgtools/pkg_install").unwrap();
63 * assert_eq!(p.as_path(), OsStr::new("pkgtools/pkg_install"));
64 * assert_eq!(p.as_full_path(), OsStr::new("../../pkgtools/pkg_install"));
65 *
66 * let p = PkgPath::new("../../pkgtools/pkg_install").unwrap();
67 * assert_eq!(p.as_path(), OsStr::new("pkgtools/pkg_install"));
68 * assert_eq!(p.as_full_path(), OsStr::new("../../pkgtools/pkg_install"));
69 *
70 * // Missing category path.
71 * assert!(PkgPath::new("../../pkg_install").is_err());
72 *
73 * // Must traverse back to the pkgsrc root directory.
74 * assert!(PkgPath::new("../pkg_install").is_err());
75 *
76 * // Not fully formed.
77 * assert!(PkgPath::new("/pkgtools/pkg_install").is_err());;
78 * ```
79 *
80 * [`as_full_path`]: PkgPath::as_full_path
81 * [`as_path`]: PkgPath::as_path
82 */
83#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
84#[cfg_attr(feature = "serde", derive(SerializeDisplay, DeserializeFromStr))]
85pub struct PkgPath {
86    short: PathBuf,
87    full: PathBuf,
88}
89
90impl PkgPath {
91    /**
92     * Create a new PkgPath
93     */
94    pub fn new(path: &str) -> Result<Self, PkgPathError> {
95        let p = PathBuf::from(path);
96        let c: Vec<_> = p.components().collect();
97
98        match c.len() {
99            //
100            // Handle the "category/package" case, adding "../../" to the full
101            // PathBuf if the rest is valid.
102            //
103            2 => match (c[0], c[1]) {
104                (Component::Normal(_), Component::Normal(_)) => {
105                    let mut f = PathBuf::from("../../");
106                    f.push(p.clone());
107                    Ok(PkgPath { short: p, full: f })
108                }
109                _ => Err(PkgPathError::InvalidPath),
110            },
111            //
112            // Handle the "../../category/package" case, removing "../../"
113            // from the short PathBuf if it's valid.
114            //
115            4 => match (c[0], c[1], c[2], c[3]) {
116                (
117                    Component::ParentDir,
118                    Component::ParentDir,
119                    Component::Normal(_),
120                    Component::Normal(_),
121                ) => {
122                    let mut s = PathBuf::from(c[2].as_os_str());
123                    s.push(c[3].as_os_str());
124                    Ok(PkgPath { short: s, full: p })
125                }
126                _ => Err(PkgPathError::InvalidPath),
127            },
128            //
129            // All other forms of input are invalid.
130            //
131            _ => Err(PkgPathError::InvalidPath),
132        }
133    }
134
135    /**
136     * Return a [`Path`] reference containing the short version of a PkgPath,
137     * for example `pkgtools/pkg_install`.
138     */
139    pub fn as_path(&self) -> &Path {
140        &self.short
141    }
142
143    /**
144     * Return a [`Path`] reference containing the full version of a PkgPath,
145     * for example `../../pkgtools/pkg_install`.
146     */
147    pub fn as_full_path(&self) -> &Path {
148        &self.full
149    }
150}
151
152impl std::fmt::Display for PkgPath {
153    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154        write!(f, "{}", self.short.display())
155    }
156}
157
158impl FromStr for PkgPath {
159    type Err = PkgPathError;
160
161    fn from_str(s: &str) -> Result<Self, PkgPathError> {
162        PkgPath::new(s)
163    }
164}
165
166impl crate::kv::FromKv for PkgPath {
167    fn from_kv(value: &str, span: crate::kv::Span) -> crate::kv::Result<Self> {
168        Self::new(value).map_err(|e| crate::kv::Error::Parse {
169            message: e.to_string(),
170            span,
171        })
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178    use std::ffi::OsStr;
179
180    fn assert_valid_foobar(s: &str) {
181        let p = PkgPath::new(s).unwrap();
182        assert_eq!(p.as_path(), OsStr::new("foo/bar"));
183        assert_eq!(p.as_full_path(), OsStr::new("../../foo/bar"));
184    }
185
186    #[test]
187    fn pkgpath_test_good_input() {
188        assert_valid_foobar("foo/bar");
189        assert_valid_foobar("foo//bar");
190        assert_valid_foobar("foo//bar//");
191        assert_valid_foobar("../../foo/bar");
192        assert_valid_foobar("../../foo/bar/");
193        assert_valid_foobar("..//..//foo//bar//");
194    }
195
196    #[test]
197    fn pkgpath_test_bad_input() {
198        let err = Err(PkgPathError::InvalidPath);
199        assert_eq!(PkgPath::new(""), err);
200        assert_eq!(PkgPath::new("\0"), err);
201        assert_eq!(PkgPath::new("foo"), err);
202        assert_eq!(PkgPath::new("foo/"), err);
203        assert_eq!(PkgPath::new("./foo"), err);
204        assert_eq!(PkgPath::new("./foo/"), err);
205        assert_eq!(PkgPath::new("../foo"), err);
206        assert_eq!(PkgPath::new("../foo/"), err);
207        assert_eq!(PkgPath::new("../foo/bar"), err);
208        assert_eq!(PkgPath::new("../foo/bar/"), err);
209        assert_eq!(PkgPath::new("../foo/bar/ojnk"), err);
210        assert_eq!(PkgPath::new("../foo/bar/ojnk/"), err);
211        assert_eq!(PkgPath::new("../.."), err);
212        assert_eq!(PkgPath::new("../../"), err);
213        assert_eq!(PkgPath::new("../../foo"), err);
214        assert_eq!(PkgPath::new("../../foo/"), err);
215        assert_eq!(PkgPath::new("../../foo/bar/ojnk"), err);
216        assert_eq!(PkgPath::new("../../foo/bar/ojnk/"), err);
217        // ".. /" gets parsed as a Normal file named ".. ".
218        assert_eq!(PkgPath::new(".. /../foo/bar"), err);
219    }
220}