1use std::fmt;
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
9#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
10#[cfg_attr(feature = "typescript", ts(export_to = "controller-api.ts"))]
11#[serde(try_from = "String", into = "String")]
12#[cfg_attr(feature = "typescript", ts(type = "string"))]
13pub struct SandboxPath(String);
14
15impl SandboxPath {
16 const MOST_BYTES: usize = 4096;
18
19 pub fn as_str(&self) -> &str {
20 &self.0
21 }
22}
23
24impl TryFrom<String> for SandboxPath {
25 type Error = String;
26
27 fn try_from(path: String) -> Result<Self, String> {
29 if path.is_empty() {
30 return Err("a file's path is empty".to_owned());
31 }
32 if path.contains('\0') {
33 return Err("a file's path can't hold a NUL character".to_owned());
34 }
35 if path.len() > Self::MOST_BYTES {
36 return Err(format!(
37 "a file's path is {} bytes, past {}",
38 path.len(),
39 Self::MOST_BYTES
40 ));
41 }
42 Ok(Self(path))
43 }
44}
45
46impl From<SandboxPath> for String {
47 fn from(path: SandboxPath) -> Self {
48 path.0
49 }
50}
51
52impl fmt::Display for SandboxPath {
53 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54 f.write_str(&self.0)
55 }
56}
57
58#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
61#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
62#[cfg_attr(feature = "typescript", ts(export_to = "controller-api.ts"))]
63#[serde(try_from = "u32", into = "u32")]
64#[cfg_attr(feature = "typescript", ts(type = "number"))]
65pub struct FileMode(u32);
66
67impl FileMode {
68 const MOST: u32 = 0o7777;
69
70 pub fn bits(self) -> u32 {
71 self.0
72 }
73
74 pub fn from_octal(digits: &str) -> Result<Self, String> {
76 let bits = u32::from_str_radix(digits, 8)
77 .map_err(|_| format!("a mode is octal digits, as chmod takes them: {digits:?}"))?;
78 Self::try_from(bits)
79 }
80}
81
82impl Default for FileMode {
83 fn default() -> Self {
85 Self(0o644)
86 }
87}
88
89impl TryFrom<u32> for FileMode {
90 type Error = String;
91
92 fn try_from(bits: u32) -> Result<Self, String> {
93 if bits > Self::MOST {
94 return Err(format!("a mode is at most 7777, not {bits:o}"));
95 }
96 Ok(Self(bits))
97 }
98}
99
100impl From<FileMode> for u32 {
101 fn from(mode: FileMode) -> Self {
102 mode.0
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 #[test]
111 fn a_path_is_checked() {
112 assert_eq!(
113 SandboxPath::try_from("/repo/fix.diff".to_owned())
114 .unwrap()
115 .as_str(),
116 "/repo/fix.diff"
117 );
118 assert!(SandboxPath::try_from("out.txt".to_owned()).is_ok());
119 for wrong in [String::new(), "a\0b".to_owned(), "a".repeat(4097)] {
120 assert!(SandboxPath::try_from(wrong).is_err());
121 }
122 assert!(serde_json::from_str::<SandboxPath>(r#""""#).is_err());
123 }
124
125 #[test]
126 fn a_mode_is_octal_as_chmod_reads_it() {
127 assert_eq!(FileMode::from_octal("755").unwrap().bits(), 0o755);
128 assert_eq!(FileMode::from_octal("0600").unwrap().bits(), 0o600);
129 assert_eq!(FileMode::default().bits(), 0o644);
130 for wrong in ["", "9", "17777", "rw"] {
131 assert!(FileMode::from_octal(wrong).is_err(), "{wrong}");
132 }
133 }
134}