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
15pub(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 std::borrow::Cow::Borrowed(_) => Ok(()),
37 std::borrow::Cow::Owned(sanitized) => Err(Error::UnacceptableFilename {
38 filename: sanitized,
39 }),
40 }
41}
42
43pub(crate) fn insecure_no_validate() -> bool {
45 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 ("normal.txt", true),
65 ("__init__.py", true),
66 ("fine i guess.py", true),
67 ("🌈.py", true),
68 ("", 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}