1pub use error::Error;
2use regex::regex;
3pub use sync::*;
4use uv_static::EnvVars;
5
6mod archive_path;
7pub mod dirhash;
8mod error;
9pub mod hash;
10pub mod stream;
11mod sync;
12mod vendor;
13
14static REPLACEMENT_CHARACTER: &str = "\u{FFFD}";
15
16pub(crate) fn validate_archive_member_name(name: &str) -> Result<(), Error> {
31 if name.is_empty() {
32 return Err(Error::EmptyFilename);
33 }
34
35 match regex!(r"\p{C}").replace_all(name, REPLACEMENT_CHARACTER) {
36 std::borrow::Cow::Borrowed(_) => Ok(()),
38 std::borrow::Cow::Owned(sanitized) => Err(Error::UnacceptableFilename {
39 filename: sanitized,
40 }),
41 }
42}
43
44pub fn insecure_no_validate() -> bool {
46 let Some(value) = std::env::var_os(EnvVars::UV_INSECURE_NO_ZIP_VALIDATION) else {
48 return false;
49 };
50 let Some(value) = value.to_str() else {
51 return false;
52 };
53 matches!(
54 value.to_lowercase().as_str(),
55 "y" | "yes" | "t" | "true" | "on" | "1"
56 )
57}
58
59#[cfg(test)]
60mod tests {
61 #[test]
62 fn test_validate_archive_member_name() {
63 for (testcase, ok) in &[
64 ("normal.txt", true),
66 ("__init__.py", true),
67 ("fine i guess.py", true),
68 ("🌈.py", true),
69 ("", false),
71 ("new\nline.py", false),
72 ("carriage\rreturn.py", false),
73 ("tab\tcharacter.py", false),
74 ("null\0byte.py", false),
75 ("control\x01code.py", false),
76 ("control\x02code.py", false),
77 ("control\x03code.py", false),
78 ("control\x04code.py", false),
79 ("backspace\x08code.py", false),
80 ("delete\x7fcode.py", false),
81 ] {
82 assert_eq!(
83 super::validate_archive_member_name(testcase).is_ok(),
84 *ok,
85 "testcase: {testcase}"
86 );
87 }
88 }
89
90 #[test]
91 fn test_unacceptable_filename_error_replaces_control_characters() {
92 let err = super::validate_archive_member_name("bad\nname").unwrap_err();
93 match err {
94 super::Error::UnacceptableFilename { filename } => {
95 assert_eq!(filename, "bad�name");
96 }
97 _ => panic!("expected UnacceptableFilename error"),
98 }
99 }
100}