Skip to main content

uv_extract/
lib.rs

1pub use error::Error;
2use regex::regex;
3pub use sync::*;
4use uv_static::EnvVars;
5
6pub mod dirhash;
7mod error;
8pub mod hash;
9pub mod stream;
10mod sync;
11mod vendor;
12
13static REPLACEMENT_CHARACTER: &str = "\u{FFFD}";
14
15/// Validate that a given filename (e.g. reported by a ZIP archive's
16/// local file entries or central directory entries) is "safe" to use.
17///
18/// "Safe" in this context doesn't refer to directory traversal
19/// risk, but whether we believe that other ZIP implementations
20/// handle the name correctly and consistently.
21///
22/// Specifically, we want to avoid names that:
23///
24/// - Contain *any* non-printable characters
25/// - Are empty
26///
27/// In the future, we may also want to check for names that contain
28/// leading/trailing whitespace, or names that are exceedingly long.
29pub(crate) fn validate_archive_member_name(name: &str) -> Result<(), Error> {
30    if name.is_empty() {
31        return Err(Error::EmptyFilename);
32    }
33
34    match regex!(r"\p{C}").replace_all(name, REPLACEMENT_CHARACTER) {
35        // No replacements mean no control characters.
36        std::borrow::Cow::Borrowed(_) => Ok(()),
37        std::borrow::Cow::Owned(sanitized) => Err(Error::UnacceptableFilename {
38            filename: sanitized,
39        }),
40    }
41}
42
43/// Returns `true` if ZIP validation is disabled.
44pub(crate) fn insecure_no_validate() -> bool {
45    // TODO(charlie) Parse this in `EnvironmentOptions`.
46    let Some(value) = std::env::var_os(EnvVars::UV_INSECURE_NO_ZIP_VALIDATION) else {
47        return false;
48    };
49    let Some(value) = value.to_str() else {
50        return false;
51    };
52    matches!(
53        value.to_lowercase().as_str(),
54        "y" | "yes" | "t" | "true" | "on" | "1"
55    )
56}
57
58#[cfg(test)]
59mod tests {
60    #[test]
61    fn test_validate_archive_member_name() {
62        for (testcase, ok) in &[
63            // Valid cases.
64            ("normal.txt", true),
65            ("__init__.py", true),
66            ("fine i guess.py", true),
67            ("🌈.py", true),
68            // Invalid cases.
69            ("", false),
70            ("new\nline.py", false),
71            ("carriage\rreturn.py", false),
72            ("tab\tcharacter.py", false),
73            ("null\0byte.py", false),
74            ("control\x01code.py", false),
75            ("control\x02code.py", false),
76            ("control\x03code.py", false),
77            ("control\x04code.py", false),
78            ("backspace\x08code.py", false),
79            ("delete\x7fcode.py", false),
80        ] {
81            assert_eq!(
82                super::validate_archive_member_name(testcase).is_ok(),
83                *ok,
84                "testcase: {testcase}"
85            );
86        }
87    }
88
89    #[test]
90    fn test_unacceptable_filename_error_replaces_control_characters() {
91        let err = super::validate_archive_member_name("bad\nname").unwrap_err();
92        match err {
93            super::Error::UnacceptableFilename { filename } => {
94                assert_eq!(filename, "bad�name");
95            }
96            _ => panic!("expected UnacceptableFilename error"),
97        }
98    }
99}