use std::error::Error as StdError;
use std::fmt;
use std::io;
use std::path::PathBuf;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct RustLibPath(String);
impl RustLibPath {
pub fn new(path: impl Into<String>) -> Result<Self> {
let path = path.into();
validate_path(&path)?;
Ok(Self(path))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for RustLibPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
fn validate_path(path: &str) -> Result<()> {
let invalid = |reason| Error::InvalidRustLibPath {
path: path.to_owned(),
reason,
};
if path.is_empty() {
return Err(invalid("path is empty"));
}
if path.starts_with('/') || path.ends_with('/') {
return Err(invalid("path must not begin or end with '/'"));
}
if path.contains('\\') {
return Err(invalid("backslashes are not allowed"));
}
if path.contains(':') {
return Err(invalid("colons are not allowed"));
}
if path.contains('\0') {
return Err(invalid("NUL is not allowed"));
}
if path
.split('/')
.any(|component| component.is_empty() || component == "." || component == "..")
{
return Err(invalid("empty, '.' and '..' components are not allowed"));
}
Ok(())
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RustLibFile {
pub path: RustLibPath,
pub contents: String,
}
impl RustLibFile {
pub fn new(path: RustLibPath, contents: impl Into<String>) -> Self {
Self {
path,
contents: contents.into(),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CheckStage {
Fetch,
Format,
Build,
Clippy,
Test,
DocTest,
}
impl CheckStage {
pub fn name(self) -> &'static str {
match self {
Self::Fetch => "fetch",
Self::Format => "format",
Self::Build => "build",
Self::Clippy => "clippy",
Self::Test => "test",
Self::DocTest => "doc-test",
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CheckStageResult {
pub stage: CheckStage,
pub command: String,
pub exit_code: Option<i32>,
pub stdout: String,
pub stderr: String,
}
impl CheckStageResult {
pub fn passed(&self) -> bool {
self.exit_code == Some(0)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CheckResult {
pub stages: Vec<CheckStageResult>,
}
impl CheckResult {
pub fn passed(&self) -> bool {
self.stages.len() == 6 && self.stages.iter().all(CheckStageResult::passed)
}
pub fn failure(&self) -> Option<&CheckStageResult> {
self.stages.iter().find(|stage| !stage.passed())
}
}
#[derive(Debug)]
pub enum Error {
InvalidRustLibName(String),
InvalidRustLibPath {
path: String,
reason: &'static str,
},
RustLibAlreadyExists(String),
RustLibNotFound(String),
RustLibIsNotDirectory(String),
DuplicateWritePath(String),
NonUtf8Path(PathBuf),
NonUtf8File(PathBuf),
SymlinkNotAllowed(PathBuf),
UnsupportedFileType(PathBuf),
RootsOverlap {
rust_libs_root: PathBuf,
work_root: PathBuf,
},
MissingRequiredFile(&'static str),
InvalidVersion(String),
InvalidCargoManifest(String),
InvalidRegistryToken,
CheckFailed(CheckResult),
Io {
action: &'static str,
path: PathBuf,
source: io::Error,
},
Sandbox {
stage: String,
message: String,
},
Publish(String),
}
impl Error {
pub(crate) fn io(action: &'static str, path: impl Into<PathBuf>, source: io::Error) -> Self {
Self::Io {
action,
path: path.into(),
source,
}
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidRustLibName(name) => write!(
f,
"invalid Rust library name {name:?}; use ASCII letters, digits, '-' or '_'"
),
Self::InvalidRustLibPath { path, reason } => {
write!(f, "invalid Rust library path {path:?}: {reason}")
}
Self::RustLibAlreadyExists(name) => {
write!(f, "Rust library {name:?} already exists")
}
Self::RustLibNotFound(name) => write!(f, "Rust library {name:?} was not found"),
Self::RustLibIsNotDirectory(name) => {
write!(f, "Rust library {name:?} is not a directory")
}
Self::DuplicateWritePath(path) => {
write!(f, "write batch contains duplicate path {path:?}")
}
Self::NonUtf8Path(path) => {
write!(
f,
"Rust library contains a non-UTF-8 path: {}",
path.display()
)
}
Self::NonUtf8File(path) => {
write!(f, "Rust library file is not UTF-8: {}", path.display())
}
Self::SymlinkNotAllowed(path) => {
write!(
f,
"Rust library symlinks are not allowed: {}",
path.display()
)
}
Self::UnsupportedFileType(path) => write!(
f,
"Rust library entry is not a regular file or directory: {}",
path.display()
),
Self::RootsOverlap {
rust_libs_root,
work_root,
} => write!(
f,
"Rust libraries root ({}) and work root ({}) must not overlap",
rust_libs_root.display(),
work_root.display()
),
Self::MissingRequiredFile(path) => {
write!(f, "Rust library is missing required file {path}")
}
Self::InvalidVersion(value) => write!(
f,
"invalid Cargo.toml [package].version {value:?}; expected canonical major.minor.patch"
),
Self::InvalidCargoManifest(message) => {
write!(f, "invalid Cargo.toml: {message}")
}
Self::InvalidRegistryToken => f.write_str("crates.io registry token is empty"),
Self::CheckFailed(result) => {
if let Some(stage) = result.failure() {
write!(f, "Rust library failed the {} stage", stage.stage.name())
} else {
f.write_str("Rust library did not complete every check stage")
}
}
Self::Io {
action,
path,
source,
} => write!(f, "could not {action} {}: {source}", path.display()),
Self::Sandbox { stage, message } => {
write!(f, "sandbox failure during {stage}: {message}")
}
Self::Publish(message) => write!(f, "publication failed: {message}"),
}
}
}
impl StdError for Error {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
match self {
Self::Io { source, .. } => Some(source),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::{CheckResult, CheckStage, CheckStageResult, RustLibPath};
#[test]
fn validates_canonical_relative_paths() {
assert_eq!(
RustLibPath::new("src/lib.rs").unwrap().as_str(),
"src/lib.rs"
);
for path in ["", "/root", "tail/", "a//b", ".", "../x", "a\\b", "C:x"] {
assert!(RustLibPath::new(path).is_err(), "accepted {path:?}");
}
}
#[test]
fn a_check_passes_only_after_all_six_stages() {
let stages = [
CheckStage::Fetch,
CheckStage::Format,
CheckStage::Build,
CheckStage::Clippy,
CheckStage::Test,
CheckStage::DocTest,
]
.into_iter()
.map(|stage| CheckStageResult {
stage,
command: stage.name().to_owned(),
exit_code: Some(0),
stdout: String::new(),
stderr: String::new(),
})
.collect();
assert!(CheckResult { stages }.passed());
}
}