1use std::error::Error as StdError;
2use std::fmt;
3use std::io;
4use std::path::PathBuf;
5
6pub type Result<T> = std::result::Result<T, Error>;
8
9#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
11pub struct RustLibPath(String);
12
13impl RustLibPath {
14 pub fn new(path: impl Into<String>) -> Result<Self> {
16 let path = path.into();
17 validate_path(&path)?;
18 Ok(Self(path))
19 }
20
21 pub fn as_str(&self) -> &str {
23 &self.0
24 }
25}
26
27impl fmt::Display for RustLibPath {
28 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29 f.write_str(&self.0)
30 }
31}
32
33fn validate_path(path: &str) -> Result<()> {
34 let invalid = |reason| Error::InvalidRustLibPath {
35 path: path.to_owned(),
36 reason,
37 };
38 if path.is_empty() {
39 return Err(invalid("path is empty"));
40 }
41 if path.starts_with('/') || path.ends_with('/') {
42 return Err(invalid("path must not begin or end with '/'"));
43 }
44 if path.contains('\\') {
45 return Err(invalid("backslashes are not allowed"));
46 }
47 if path.contains(':') {
48 return Err(invalid("colons are not allowed"));
49 }
50 if path.contains('\0') {
51 return Err(invalid("NUL is not allowed"));
52 }
53 if path
54 .split('/')
55 .any(|component| component.is_empty() || component == "." || component == "..")
56 {
57 return Err(invalid("empty, '.' and '..' components are not allowed"));
58 }
59 Ok(())
60}
61
62#[derive(Clone, Debug, Eq, PartialEq)]
64pub struct RustLibFile {
65 pub path: RustLibPath,
66 pub contents: String,
67}
68
69impl RustLibFile {
70 pub fn new(path: RustLibPath, contents: impl Into<String>) -> Self {
71 Self {
72 path,
73 contents: contents.into(),
74 }
75 }
76}
77
78#[derive(Clone, Copy, Debug, Eq, PartialEq)]
80pub enum CheckStage {
81 Fetch,
82 Format,
83 Build,
84 Clippy,
85 Test,
86 DocTest,
87}
88
89impl CheckStage {
90 pub fn name(self) -> &'static str {
91 match self {
92 Self::Fetch => "fetch",
93 Self::Format => "format",
94 Self::Build => "build",
95 Self::Clippy => "clippy",
96 Self::Test => "test",
97 Self::DocTest => "doc-test",
98 }
99 }
100}
101
102#[derive(Clone, Debug, Eq, PartialEq)]
104pub struct CheckStageResult {
105 pub stage: CheckStage,
106 pub command: String,
107 pub exit_code: Option<i32>,
108 pub stdout: String,
109 pub stderr: String,
110}
111
112impl CheckStageResult {
113 pub fn passed(&self) -> bool {
114 self.exit_code == Some(0)
115 }
116}
117
118#[derive(Clone, Debug, Eq, PartialEq)]
120pub struct CheckResult {
121 pub stages: Vec<CheckStageResult>,
122}
123
124impl CheckResult {
125 pub fn passed(&self) -> bool {
126 self.stages.len() == 6 && self.stages.iter().all(CheckStageResult::passed)
127 }
128
129 pub fn failure(&self) -> Option<&CheckStageResult> {
130 self.stages.iter().find(|stage| !stage.passed())
131 }
132}
133
134#[derive(Debug)]
136pub enum Error {
137 InvalidRustLibName(String),
138 InvalidRustLibPath {
139 path: String,
140 reason: &'static str,
141 },
142 RustLibAlreadyExists(String),
143 RustLibNotFound(String),
144 RustLibIsNotDirectory(String),
145 DuplicateWritePath(String),
146 NonUtf8Path(PathBuf),
147 NonUtf8File(PathBuf),
148 SymlinkNotAllowed(PathBuf),
149 UnsupportedFileType(PathBuf),
150 RootsOverlap {
151 rust_libs_root: PathBuf,
152 work_root: PathBuf,
153 },
154 MissingRequiredFile(&'static str),
155 InvalidVersion(String),
156 InvalidCargoManifest(String),
157 InvalidRegistryToken,
158 CheckFailed(CheckResult),
159 Io {
160 action: &'static str,
161 path: PathBuf,
162 source: io::Error,
163 },
164 Sandbox {
165 stage: String,
166 message: String,
167 },
168 Publish(String),
169}
170
171impl Error {
172 pub(crate) fn io(action: &'static str, path: impl Into<PathBuf>, source: io::Error) -> Self {
173 Self::Io {
174 action,
175 path: path.into(),
176 source,
177 }
178 }
179}
180
181impl fmt::Display for Error {
182 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183 match self {
184 Self::InvalidRustLibName(name) => write!(
185 f,
186 "invalid Rust library name {name:?}; use ASCII letters, digits, '-' or '_'"
187 ),
188 Self::InvalidRustLibPath { path, reason } => {
189 write!(f, "invalid Rust library path {path:?}: {reason}")
190 }
191 Self::RustLibAlreadyExists(name) => {
192 write!(f, "Rust library {name:?} already exists")
193 }
194 Self::RustLibNotFound(name) => write!(f, "Rust library {name:?} was not found"),
195 Self::RustLibIsNotDirectory(name) => {
196 write!(f, "Rust library {name:?} is not a directory")
197 }
198 Self::DuplicateWritePath(path) => {
199 write!(f, "write batch contains duplicate path {path:?}")
200 }
201 Self::NonUtf8Path(path) => {
202 write!(
203 f,
204 "Rust library contains a non-UTF-8 path: {}",
205 path.display()
206 )
207 }
208 Self::NonUtf8File(path) => {
209 write!(f, "Rust library file is not UTF-8: {}", path.display())
210 }
211 Self::SymlinkNotAllowed(path) => {
212 write!(
213 f,
214 "Rust library symlinks are not allowed: {}",
215 path.display()
216 )
217 }
218 Self::UnsupportedFileType(path) => write!(
219 f,
220 "Rust library entry is not a regular file or directory: {}",
221 path.display()
222 ),
223 Self::RootsOverlap {
224 rust_libs_root,
225 work_root,
226 } => write!(
227 f,
228 "Rust libraries root ({}) and work root ({}) must not overlap",
229 rust_libs_root.display(),
230 work_root.display()
231 ),
232 Self::MissingRequiredFile(path) => {
233 write!(f, "Rust library is missing required file {path}")
234 }
235 Self::InvalidVersion(value) => write!(
236 f,
237 "invalid Cargo.toml [package].version {value:?}; expected canonical major.minor.patch"
238 ),
239 Self::InvalidCargoManifest(message) => {
240 write!(f, "invalid Cargo.toml: {message}")
241 }
242 Self::InvalidRegistryToken => f.write_str("crates.io registry token is empty"),
243 Self::CheckFailed(result) => {
244 if let Some(stage) = result.failure() {
245 write!(f, "Rust library failed the {} stage", stage.stage.name())
246 } else {
247 f.write_str("Rust library did not complete every check stage")
248 }
249 }
250 Self::Io {
251 action,
252 path,
253 source,
254 } => write!(f, "could not {action} {}: {source}", path.display()),
255 Self::Sandbox { stage, message } => {
256 write!(f, "sandbox failure during {stage}: {message}")
257 }
258 Self::Publish(message) => write!(f, "publication failed: {message}"),
259 }
260 }
261}
262
263impl StdError for Error {
264 fn source(&self) -> Option<&(dyn StdError + 'static)> {
265 match self {
266 Self::Io { source, .. } => Some(source),
267 _ => None,
268 }
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 use super::{CheckResult, CheckStage, CheckStageResult, RustLibPath};
275
276 #[test]
277 fn validates_canonical_relative_paths() {
278 assert_eq!(
279 RustLibPath::new("src/lib.rs").unwrap().as_str(),
280 "src/lib.rs"
281 );
282 for path in ["", "/root", "tail/", "a//b", ".", "../x", "a\\b", "C:x"] {
283 assert!(RustLibPath::new(path).is_err(), "accepted {path:?}");
284 }
285 }
286
287 #[test]
288 fn a_check_passes_only_after_all_six_stages() {
289 let stages = [
290 CheckStage::Fetch,
291 CheckStage::Format,
292 CheckStage::Build,
293 CheckStage::Clippy,
294 CheckStage::Test,
295 CheckStage::DocTest,
296 ]
297 .into_iter()
298 .map(|stage| CheckStageResult {
299 stage,
300 command: stage.name().to_owned(),
301 exit_code: Some(0),
302 stdout: String::new(),
303 stderr: String::new(),
304 })
305 .collect();
306 assert!(CheckResult { stages }.passed());
307 }
308}