use std::collections::BTreeMap;
use std::error::Error as StdError;
use std::fmt;
use std::io;
use std::path::Path;
const MAX_ERROR_BYTES: usize = 12 * 1024;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct File {
pub path: String,
pub contents: String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RustBinInput {
pub text: String,
pub objects: Vec<Vec<u8>>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RustBinOutput {
pub text: String,
pub objects: Vec<Vec<u8>>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RunRequest {
pub text: String,
pub objects: Vec<Vec<u8>>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RunResult {
pub version: String,
pub text: String,
pub objects: Vec<Vec<u8>>,
pub diagnostics: String,
}
pub struct Error {
category: &'static str,
message: String,
}
pub type Result<T> = std::result::Result<T, Error>;
impl Error {
pub(crate) fn new(category: &'static str, message: impl Into<String>) -> Self {
Self {
category,
message: bound(message.into()),
}
}
pub(crate) fn io(operation: &'static str, path: impl AsRef<Path>, source: io::Error) -> Self {
Self::new(
"io",
format!("{operation} at {}: {source}", path.as_ref().display()),
)
}
}
impl fmt::Display for Error {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}: {}", self.category, self.message)
}
}
impl fmt::Debug for Error {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("Error")
.field("category", &self.category)
.field("message", &self.message)
.finish()
}
}
impl StdError for Error {}
pub(crate) struct ValidSource {
pub(crate) files: Vec<File>,
pub(crate) version: String,
}
pub(crate) fn validate_name(name: &str) -> Result<()> {
let mut bytes = name.bytes();
if !bytes
.next()
.is_some_and(|byte| byte.is_ascii_alphanumeric())
|| !bytes.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
{
return Err(Error::new(
"invalid_name",
format!("invalid managed-binary name {name:?}"),
));
}
Ok(())
}
pub(crate) fn validate_source(files: &[File], expected_name: &str) -> Result<ValidSource> {
let mut ordered = BTreeMap::new();
for file in files {
validate_path(&file.path)?;
if file.path == "Cargo.lock" {
return Err(Error::new(
"invalid_source",
"Cargo.lock is ephemeral and cannot be managed source",
));
}
if ordered
.insert(file.path.clone(), file.contents.clone())
.is_some()
{
return Err(Error::new(
"invalid_source",
format!("duplicate source path {:?}", file.path),
));
}
}
let manifest = ordered
.get("Cargo.toml")
.ok_or_else(|| Error::new("invalid_source", "root Cargo.toml is required"))?;
if !ordered.contains_key("Documentation.md") {
return Err(Error::new(
"invalid_source",
"root Documentation.md is required",
));
}
let (package_name, version) = manifest_metadata(manifest)?;
if package_name != expected_name {
return Err(Error::new(
"invalid_metadata",
format!("[package].name must be {expected_name:?}, found {package_name:?}"),
));
}
Ok(ValidSource {
files: ordered
.into_iter()
.map(|(path, contents)| File { path, contents })
.collect(),
version,
})
}
pub(crate) fn manifest_metadata(manifest: &str) -> Result<(String, String)> {
let mut section = String::new();
let mut name = None;
let mut version = None;
for raw_line in manifest.lines() {
let line = strip_comment(raw_line).trim();
if line.is_empty() {
continue;
}
if line.starts_with('[') && line.ends_with(']') {
section.clear();
section.push_str(line[1..line.len() - 1].trim());
continue;
}
if section != "package" {
continue;
}
let Some((raw_key, raw_value)) = line.split_once('=') else {
continue;
};
match raw_key.trim() {
"name" => {
if name.is_some() {
return Err(Error::new("invalid_metadata", "duplicate [package].name"));
}
name = Some(parse_basic_string(raw_value.trim(), "name")?);
}
"version" => {
if version.is_some() {
return Err(Error::new(
"invalid_metadata",
"duplicate [package].version",
));
}
version = Some(parse_basic_string(raw_value.trim(), "version")?);
}
_ => {}
}
}
let name = name.ok_or_else(|| {
Error::new(
"invalid_metadata",
"literal [package].name is required in root Cargo.toml",
)
})?;
validate_name(&name)?;
let version = version.ok_or_else(|| {
Error::new(
"invalid_metadata",
"literal [package].version is required in root Cargo.toml",
)
})?;
validate_version(&version)?;
Ok((name, version))
}
fn validate_path(path: &str) -> Result<()> {
if path.is_empty()
|| path.starts_with('/')
|| path.ends_with('/')
|| path.contains('\\')
|| path.contains(':')
|| path.contains('\0')
|| path
.split('/')
.any(|component| component.is_empty() || matches!(component, "." | ".."))
{
return Err(Error::new(
"unsafe_path",
format!("invalid relative source path {path:?}"),
));
}
Ok(())
}
fn validate_version(version: &str) -> Result<()> {
let mut count = 0;
for component in version.split('.') {
count += 1;
if component.is_empty()
|| !component.bytes().all(|byte| byte.is_ascii_digit())
|| (component.len() > 1 && component.starts_with('0'))
|| component.parse::<u64>().is_err()
{
return Err(Error::new(
"invalid_metadata",
format!("noncanonical stable version {version:?}"),
));
}
}
if count != 3 {
return Err(Error::new(
"invalid_metadata",
format!("noncanonical stable version {version:?}"),
));
}
Ok(())
}
fn parse_basic_string(value: &str, field: &str) -> Result<String> {
if value.len() < 2 || !value.starts_with('"') || !value.ends_with('"') {
return Err(Error::new(
"invalid_metadata",
format!("[package].{field} must be a literal basic string"),
));
}
let inner = &value[1..value.len() - 1];
if inner.contains(['"', '\\', '\n', '\r']) {
return Err(Error::new(
"invalid_metadata",
format!("[package].{field} must not contain escapes or newlines"),
));
}
Ok(inner.to_owned())
}
fn strip_comment(line: &str) -> &str {
let mut quoted = false;
let mut escaped = false;
for (index, character) in line.char_indices() {
if escaped {
escaped = false;
continue;
}
if character == '\\' && quoted {
escaped = true;
} else if character == '"' {
quoted = !quoted;
} else if character == '#' && !quoted {
return &line[..index];
}
}
line
}
fn bound(mut message: String) -> String {
if message.len() <= MAX_ERROR_BYTES {
return message;
}
let mut end = MAX_ERROR_BYTES;
while !message.is_char_boundary(end) {
end -= 1;
}
message.truncate(end);
message.push_str("… [truncated]");
message
}
#[cfg(test)]
mod tests {
use super::{File, manifest_metadata, validate_source};
#[test]
fn parses_canonical_metadata() {
let manifest = "[package]\nname = \"demo\"\nversion = \"12.3.4\" # current\n";
assert_eq!(
manifest_metadata(manifest).unwrap(),
("demo".to_owned(), "12.3.4".to_owned())
);
}
#[test]
fn rejects_noncanonical_versions_and_lockfiles() {
for version in ["1.2", "01.2.3", "1.2.3-beta", "1.2.3+build"] {
let files = [
File {
path: "Cargo.toml".to_owned(),
contents: format!("[package]\nname = \"demo\"\nversion = \"{version}\"\n"),
},
File {
path: "Documentation.md".to_owned(),
contents: String::new(),
},
];
assert!(validate_source(&files, "demo").is_err());
}
let files = [
File {
path: "Cargo.toml".to_owned(),
contents: "[package]\nname = \"demo\"\nversion = \"1.2.3\"\n".to_owned(),
},
File {
path: "Documentation.md".to_owned(),
contents: String::new(),
},
File {
path: "Cargo.lock".to_owned(),
contents: "version = 4\n".to_owned(),
},
];
assert!(validate_source(&files, "demo").is_err());
}
}