use std::ffi::OsStr;
use std::fmt;
use std::path::Path;
use std::str::FromStr;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
#[derive(Debug, thiserror::Error)]
pub enum IdentityError {
#[error("invalid crate_name `{0}`: must be 1..=128 ASCII alphanumeric, `-` or `_`")]
InvalidCrateName(String),
#[error("invalid c_metadata `{0}`: must be 1..=64 ASCII hex digits")]
InvalidCMetadata(String),
#[error("invalid target `{0}`: must be 1..=128 alphanumeric, `-` or `_`")]
InvalidTarget(String),
#[error("invalid version `{0}`: must be a semver release like 1.0.219")]
InvalidCrateVersion(String),
#[error("invalid rustc_version `{0}`: must be 1..=64 alphanumeric, `.`, `-`, or `_`")]
InvalidRustcVersion(String),
#[error(
"invalid feature name `{0}`: cargo requires an XID start, `_`, or digit first character, then XID continue, `-`, `+`, or `.` (no `dep:` prefix, no `/`), 1..=128 chars"
)]
InvalidFeatureName(String),
#[error("features must be strictly sorted and deduplicated")]
UnsortedFeatures,
#[error("invalid emit entry `{0}`: must be 1..=32 alphanumeric, `-`, or `_`")]
InvalidEmitEntry(String),
#[error("emit entries must be strictly sorted and deduplicated")]
UnsortedEmit,
#[error("invalid JSON wrapper: {0}")]
InvalidJsonWrapper(String),
#[error("dependency_c_metadata_json must be sorted by (crate_name, c_metadata)")]
UnsortedDependencyIdentities,
}
fn validate_crate_name(value: &str) -> Result<(), IdentityError> {
if value.is_empty()
|| value.len() > 128
|| !value
.chars()
.all(|ch| ch.is_ascii_alphanumeric() || ch == '-' || ch == '_')
{
return Err(IdentityError::InvalidCrateName(value.to_owned()));
}
Ok(())
}
fn validate_c_metadata(value: &str) -> Result<(), IdentityError> {
if value.is_empty() || value.len() > 64 || !value.chars().all(|ch| ch.is_ascii_hexdigit()) {
return Err(IdentityError::InvalidCMetadata(value.to_owned()));
}
Ok(())
}
fn validate_target(value: &str) -> Result<(), IdentityError> {
if value.is_empty()
|| value.len() > 128
|| !value
.chars()
.all(|ch| ch.is_ascii_alphanumeric() || ch == '-' || ch == '_')
{
return Err(IdentityError::InvalidTarget(value.to_owned()));
}
Ok(())
}
fn validate_rustc_version(value: &str) -> Result<(), IdentityError> {
if value.is_empty()
|| value.len() > 64
|| !value
.chars()
.all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '.' | '-' | '_'))
{
return Err(IdentityError::InvalidRustcVersion(value.to_owned()));
}
Ok(())
}
pub fn validate_feature_name(value: &str) -> Result<(), IdentityError> {
let reject = || IdentityError::InvalidFeatureName(value.to_owned());
if value.is_empty() || value.len() > 128 || value.starts_with("dep:") || value.contains('/') {
return Err(reject());
}
let mut chars = value.chars();
if let Some(first) = chars.next()
&& !(unicode_ident::is_xid_start(first) || first == '_' || first.is_ascii_digit())
{
return Err(reject());
}
if !chars.all(|ch| unicode_ident::is_xid_continue(ch) || matches!(ch, '-' | '+' | '.')) {
return Err(reject());
}
Ok(())
}
fn validate_features_sorted(features: &[String]) -> Result<(), IdentityError> {
let mut previous: Option<&str> = None;
for feature in features {
validate_feature_name(feature)?;
if previous.is_some_and(|last| last >= feature.as_str()) {
return Err(IdentityError::UnsortedFeatures);
}
previous = Some(feature.as_str());
}
Ok(())
}
fn validate_emit_entry(value: &str) -> Result<(), IdentityError> {
if value.is_empty()
|| value.len() > 32
|| !value
.chars()
.all(|ch| ch.is_ascii_alphanumeric() || ch == '-' || ch == '_')
{
return Err(IdentityError::InvalidEmitEntry(value.to_owned()));
}
Ok(())
}
pub fn validate_emit_sorted(emit: &[String]) -> Result<(), IdentityError> {
let mut previous: Option<&str> = None;
for entry in emit {
validate_emit_entry(entry)?;
if previous.is_some_and(|last| last >= entry.as_str()) {
return Err(IdentityError::UnsortedEmit);
}
previous = Some(entry.as_str());
}
Ok(())
}
macro_rules! string_newtype {
(
$(#[$meta:meta])*
$name:ident, $validate:ident, $error:ident
) => {
$(#[$meta])*
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, utoipa::ToSchema)]
pub struct $name(String);
impl $name {
pub fn parse<S: Into<String>>(value: S) -> Result<Self, IdentityError> {
let value = value.into();
$validate(&value)?;
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_inner(self) -> String {
self.0
}
}
impl AsRef<str> for $name {
fn as_ref(&self) -> &str {
&self.0
}
}
impl AsRef<OsStr> for $name {
fn as_ref(&self) -> &OsStr {
self.0.as_ref()
}
}
impl AsRef<Path> for $name {
fn as_ref(&self) -> &Path {
self.0.as_ref()
}
}
impl PartialEq<str> for $name {
fn eq(&self, other: &str) -> bool {
self.0.as_str() == other
}
}
impl PartialEq<&str> for $name {
fn eq(&self, other: &&str) -> bool {
self.0.as_str() == *other
}
}
impl PartialEq<$name> for &str {
fn eq(&self, other: &$name) -> bool {
*self == other.0.as_str()
}
}
impl PartialEq<$name> for str {
fn eq(&self, other: &$name) -> bool {
self == other.0.as_str()
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl FromStr for $name {
type Err = IdentityError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::parse(value)
}
}
impl Serialize for $name {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
self.0.serialize(s)
}
}
impl<'de> Deserialize<'de> for $name {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let raw = String::deserialize(d)?;
Self::parse(raw).map_err(serde::de::Error::custom)
}
}
};
}
string_newtype!(
CrateName,
validate_crate_name,
InvalidCrateName
);
string_newtype!(
CMetadata,
validate_c_metadata,
InvalidCMetadata
);
string_newtype!(
TargetTriple,
validate_target,
InvalidTarget
);
string_newtype!(
WireRustcVersion,
validate_rustc_version,
InvalidRustcVersion
);
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)]
#[serde(transparent)]
pub struct CrateVersion(pub semver::Version);
impl<'de> Deserialize<'de> for CrateVersion {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let raw = String::deserialize(deserializer)?;
raw.parse().map_err(serde::de::Error::custom)
}
}
impl utoipa::PartialSchema for CrateVersion {
fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
<String as utoipa::PartialSchema>::schema()
}
}
impl utoipa::ToSchema for CrateVersion {}
impl CrateVersion {
#[must_use]
pub const fn new(version: semver::Version) -> Self {
Self(version)
}
#[must_use]
pub const fn as_semver(&self) -> &semver::Version {
&self.0
}
#[must_use]
pub fn into_inner(self) -> semver::Version {
self.0
}
}
impl fmt::Display for CrateVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl FromStr for CrateVersion {
type Err = IdentityError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
semver::Version::parse(value)
.map(Self)
.map_err(|_| IdentityError::InvalidCrateVersion(value.to_owned()))
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct FeaturesJson(Vec<String>);
impl utoipa::PartialSchema for FeaturesJson {
fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
<String as utoipa::PartialSchema>::schema()
}
}
impl utoipa::ToSchema for FeaturesJson {}
impl FeaturesJson {
pub fn from_sorted(features: Vec<String>) -> Result<Self, IdentityError> {
validate_features_sorted(&features)?;
Ok(Self(features))
}
pub fn canonicalize(mut features: Vec<String>) -> Result<Self, IdentityError> {
features.sort();
features.dedup();
Self::from_sorted(features)
}
#[must_use]
pub fn features(&self) -> &[String] {
&self.0
}
#[must_use]
pub fn raw(&self) -> String {
serde_json::to_string(&self.0).expect("Vec<String> always serializes")
}
}
impl fmt::Display for FeaturesJson {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.raw())
}
}
impl Serialize for FeaturesJson {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
self.raw().serialize(s)
}
}
impl<'de> Deserialize<'de> for FeaturesJson {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let raw = String::deserialize(d)?;
let features: Vec<String> = serde_json::from_str(&raw).map_err(|error| {
serde::de::Error::custom(IdentityError::InvalidJsonWrapper(error.to_string()))
})?;
Self::from_sorted(features).map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct DependencyCompileKeyIdentity {
pub crate_name: CrateName,
pub compile_key: String,
}
impl DependencyCompileKeyIdentity {
#[must_use]
pub fn canonicalize_list(mut identities: Vec<Self>) -> Vec<Self> {
identities.sort();
identities.dedup();
identities
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct DependencyCMetadataIdentity {
pub crate_name: CrateName,
pub c_metadata: CMetadata,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct DependencyCMetadataJson(Vec<DependencyCMetadataIdentity>);
impl utoipa::PartialSchema for DependencyCMetadataJson {
fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
<String as utoipa::PartialSchema>::schema()
}
}
impl utoipa::ToSchema for DependencyCMetadataJson {}
impl DependencyCMetadataJson {
pub fn from_sorted(
identities: Vec<DependencyCMetadataIdentity>,
) -> Result<Self, IdentityError> {
let mut previous: Option<(&str, &str)> = None;
for identity in &identities {
let current = (identity.crate_name.as_str(), identity.c_metadata.as_str());
if previous.is_some_and(|last| last >= current) {
return Err(IdentityError::UnsortedDependencyIdentities);
}
previous = Some(current);
}
Ok(Self(identities))
}
pub fn canonicalize(
mut identities: Vec<DependencyCMetadataIdentity>,
) -> Result<Self, IdentityError> {
identities.sort();
identities.dedup();
Self::from_sorted(identities)
}
#[must_use]
pub fn entries(&self) -> &[DependencyCMetadataIdentity] {
&self.0
}
#[must_use]
pub fn raw(&self) -> String {
serde_json::to_string(&self.0).expect("dependency identity list always serializes")
}
}
impl fmt::Display for DependencyCMetadataJson {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.raw())
}
}
impl Serialize for DependencyCMetadataJson {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
self.raw().serialize(s)
}
}
impl<'de> Deserialize<'de> for DependencyCMetadataJson {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let raw = String::deserialize(d)?;
let entries: Vec<DependencyCMetadataIdentity> =
serde_json::from_str(&raw).map_err(|error| {
serde::de::Error::custom(IdentityError::InvalidJsonWrapper(error.to_string()))
})?;
Self::from_sorted(entries).map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn crate_name_accepts_typical() {
CrateName::parse("serde_json").unwrap();
CrateName::parse("proc-macro2").unwrap();
}
#[test]
fn crate_name_rejects_empty_and_overlong() {
assert!(CrateName::parse("").is_err());
assert!(CrateName::parse("x".repeat(129)).is_err());
assert!(CrateName::parse("a b").is_err());
}
#[test]
fn c_metadata_must_be_hex() {
CMetadata::parse("deadbeef").unwrap();
assert!(CMetadata::parse("xyz").is_err());
}
#[test]
fn rustc_version_shape() {
WireRustcVersion::parse("1.83.0").unwrap();
WireRustcVersion::parse("1.91.0-nightly").unwrap();
assert!(WireRustcVersion::parse("").is_err());
}
#[test]
fn features_json_round_trip() {
let raw = "[\"default\",\"std\"]";
let features: FeaturesJson =
serde_json::from_value(serde_json::Value::String(raw.to_owned())).unwrap();
assert_eq!(features.features(), &["default", "std"]);
let serialized = serde_json::to_value(&features).unwrap();
assert_eq!(serialized, serde_json::Value::String(raw.to_owned()));
}
#[test]
fn features_json_rejects_unsorted() {
let raw = "[\"std\",\"default\"]";
let result: Result<FeaturesJson, _> =
serde_json::from_value(serde_json::Value::String(raw.to_owned()));
assert!(result.is_err());
}
#[test]
fn feature_names_follow_cargo_grammar() {
for name in [
"c++20",
"foo+bar",
"128bit",
"_foo",
"feat-name",
"feat_name",
"foo.bar",
] {
assert!(validate_feature_name(name).is_ok(), "{name} must validate");
}
for name in [
"",
"+foo",
"-foo",
".foo",
"dep:bar",
"foo/bar",
"foo:bar",
"foo?",
"?foo",
"ⒶⒷⒸ",
"a¼",
&"x".repeat(129),
] {
assert!(
validate_feature_name(name).is_err(),
"{name} must not validate"
);
}
}
#[test]
fn dependency_identities_round_trip() {
let raw = "[{\"crate_name\":\"a\",\"c_metadata\":\"deadbeef\"}]";
let value: DependencyCMetadataJson =
serde_json::from_value(serde_json::Value::String(raw.to_owned())).unwrap();
assert_eq!(value.entries().len(), 1);
let serialized = serde_json::to_value(&value).unwrap();
assert_eq!(serialized, serde_json::Value::String(raw.to_owned()));
}
}