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