1use std::ffi::OsStr;
21use std::fmt;
22use std::path::Path;
23use std::str::FromStr;
24
25use serde::{Deserialize, Deserializer, Serialize, Serializer};
26
27#[derive(Debug, thiserror::Error)]
29pub enum IdentityError {
30 #[error("invalid crate_name `{0}`: must be 1..=128 ASCII alphanumeric, `-` or `_`")]
32 InvalidCrateName(String),
33 #[error("invalid c_metadata `{0}`: must be 1..=64 ASCII hex digits")]
35 InvalidCMetadata(String),
36 #[error("invalid target `{0}`: must be 1..=128 alphanumeric, `-` or `_`")]
38 InvalidTarget(String),
39 #[error("invalid version `{0}`: must be a semver release like 1.0.219")]
41 InvalidCrateVersion(String),
42 #[error("invalid rustc_version `{0}`: must be 1..=64 alphanumeric, `.`, `-`, or `_`")]
44 InvalidRustcVersion(String),
45 #[error("invalid feature name `{0}`: must be 1..=128 alphanumeric, `-`, `_`, or `.`")]
47 InvalidFeatureName(String),
48 #[error("features must be strictly sorted and deduplicated")]
50 UnsortedFeatures,
51 #[error("invalid emit entry `{0}`: must be 1..=32 alphanumeric, `-`, or `_`")]
53 InvalidEmitEntry(String),
54 #[error("emit entries must be strictly sorted and deduplicated")]
56 UnsortedEmit,
57 #[error("invalid JSON wrapper: {0}")]
59 InvalidJsonWrapper(String),
60 #[error("dependency_c_metadata_json must be sorted by (crate_name, c_metadata)")]
62 UnsortedDependencyIdentities,
63}
64
65fn validate_crate_name(value: &str) -> Result<(), IdentityError> {
66 if value.is_empty()
67 || value.len() > 128
68 || !value
69 .chars()
70 .all(|ch| ch.is_ascii_alphanumeric() || ch == '-' || ch == '_')
71 {
72 return Err(IdentityError::InvalidCrateName(value.to_owned()));
73 }
74 Ok(())
75}
76
77fn validate_c_metadata(value: &str) -> Result<(), IdentityError> {
78 if value.is_empty() || value.len() > 64 || !value.chars().all(|ch| ch.is_ascii_hexdigit()) {
79 return Err(IdentityError::InvalidCMetadata(value.to_owned()));
80 }
81 Ok(())
82}
83
84fn validate_target(value: &str) -> Result<(), IdentityError> {
85 if value.is_empty()
86 || value.len() > 128
87 || !value
88 .chars()
89 .all(|ch| ch.is_ascii_alphanumeric() || ch == '-' || ch == '_')
90 {
91 return Err(IdentityError::InvalidTarget(value.to_owned()));
92 }
93 Ok(())
94}
95
96fn validate_rustc_version(value: &str) -> Result<(), IdentityError> {
97 if value.is_empty()
98 || value.len() > 64
99 || !value
100 .chars()
101 .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '.' | '-' | '_'))
102 {
103 return Err(IdentityError::InvalidRustcVersion(value.to_owned()));
104 }
105 Ok(())
106}
107
108fn validate_feature_name(value: &str) -> Result<(), IdentityError> {
109 if value.is_empty()
110 || value.len() > 128
111 || !value
112 .chars()
113 .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.'))
114 {
115 return Err(IdentityError::InvalidFeatureName(value.to_owned()));
116 }
117 Ok(())
118}
119
120fn validate_features_sorted(features: &[String]) -> Result<(), IdentityError> {
121 let mut previous: Option<&str> = None;
122 for feature in features {
123 validate_feature_name(feature)?;
124 if previous.is_some_and(|last| last >= feature.as_str()) {
125 return Err(IdentityError::UnsortedFeatures);
126 }
127 previous = Some(feature.as_str());
128 }
129 Ok(())
130}
131
132fn validate_emit_entry(value: &str) -> Result<(), IdentityError> {
133 if value.is_empty()
134 || value.len() > 32
135 || !value
136 .chars()
137 .all(|ch| ch.is_ascii_alphanumeric() || ch == '-' || ch == '_')
138 {
139 return Err(IdentityError::InvalidEmitEntry(value.to_owned()));
140 }
141 Ok(())
142}
143
144pub fn validate_emit_sorted(emit: &[String]) -> Result<(), IdentityError> {
151 let mut previous: Option<&str> = None;
152 for entry in emit {
153 validate_emit_entry(entry)?;
154 if previous.is_some_and(|last| last >= entry.as_str()) {
155 return Err(IdentityError::UnsortedEmit);
156 }
157 previous = Some(entry.as_str());
158 }
159 Ok(())
160}
161
162macro_rules! string_newtype {
163 (
164 $(#[$meta:meta])*
165 $name:ident, $validate:ident, $error:ident
166 ) => {
167 $(#[$meta])*
168 #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, utoipa::ToSchema)]
169 pub struct $name(String);
170
171 impl $name {
172 pub fn parse<S: Into<String>>(value: S) -> Result<Self, IdentityError> {
178 let value = value.into();
179 $validate(&value)?;
180 Ok(Self(value))
181 }
182
183 pub fn as_str(&self) -> &str {
185 &self.0
186 }
187
188 pub fn into_inner(self) -> String {
190 self.0
191 }
192 }
193
194 impl AsRef<str> for $name {
195 fn as_ref(&self) -> &str {
196 &self.0
197 }
198 }
199
200 impl AsRef<OsStr> for $name {
201 fn as_ref(&self) -> &OsStr {
202 self.0.as_ref()
203 }
204 }
205
206 impl AsRef<Path> for $name {
207 fn as_ref(&self) -> &Path {
208 self.0.as_ref()
209 }
210 }
211
212 impl PartialEq<str> for $name {
213 fn eq(&self, other: &str) -> bool {
214 self.0.as_str() == other
215 }
216 }
217
218 impl PartialEq<&str> for $name {
219 fn eq(&self, other: &&str) -> bool {
220 self.0.as_str() == *other
221 }
222 }
223
224 impl PartialEq<$name> for &str {
225 fn eq(&self, other: &$name) -> bool {
226 *self == other.0.as_str()
227 }
228 }
229
230 impl PartialEq<$name> for str {
231 fn eq(&self, other: &$name) -> bool {
232 self == other.0.as_str()
233 }
234 }
235
236 impl fmt::Display for $name {
237 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
238 f.write_str(&self.0)
239 }
240 }
241
242 impl FromStr for $name {
243 type Err = IdentityError;
244 fn from_str(value: &str) -> Result<Self, Self::Err> {
245 Self::parse(value)
246 }
247 }
248
249 impl Serialize for $name {
250 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
251 self.0.serialize(s)
252 }
253 }
254
255 impl<'de> Deserialize<'de> for $name {
256 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
257 let raw = String::deserialize(d)?;
258 Self::parse(raw).map_err(serde::de::Error::custom)
259 }
260 }
261 };
262}
263
264string_newtype!(
265 CrateName,
267 validate_crate_name,
268 InvalidCrateName
269);
270string_newtype!(
271 CMetadata,
273 validate_c_metadata,
274 InvalidCMetadata
275);
276string_newtype!(
277 TargetTriple,
279 validate_target,
280 InvalidTarget
281);
282string_newtype!(
283 WireRustcVersion,
285 validate_rustc_version,
286 InvalidRustcVersion
287);
288
289#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)]
294#[serde(transparent)]
295pub struct CrateVersion(pub semver::Version);
296
297impl<'de> Deserialize<'de> for CrateVersion {
302 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
303 let raw = String::deserialize(deserializer)?;
304 raw.parse().map_err(serde::de::Error::custom)
305 }
306}
307
308impl utoipa::PartialSchema for CrateVersion {
310 fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
311 <String as utoipa::PartialSchema>::schema()
312 }
313}
314
315impl utoipa::ToSchema for CrateVersion {}
316
317impl CrateVersion {
318 #[must_use]
320 pub const fn new(version: semver::Version) -> Self {
321 Self(version)
322 }
323
324 #[must_use]
326 pub const fn as_semver(&self) -> &semver::Version {
327 &self.0
328 }
329
330 #[must_use]
332 pub fn into_inner(self) -> semver::Version {
333 self.0
334 }
335}
336
337impl fmt::Display for CrateVersion {
338 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
339 self.0.fmt(f)
340 }
341}
342
343impl FromStr for CrateVersion {
344 type Err = IdentityError;
345 fn from_str(value: &str) -> Result<Self, Self::Err> {
346 semver::Version::parse(value)
347 .map(Self)
348 .map_err(|_| IdentityError::InvalidCrateVersion(value.to_owned()))
349 }
350}
351
352#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
358pub struct FeaturesJson(Vec<String>);
359
360impl utoipa::PartialSchema for FeaturesJson {
362 fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
363 <String as utoipa::PartialSchema>::schema()
364 }
365}
366
367impl utoipa::ToSchema for FeaturesJson {}
368
369impl FeaturesJson {
370 pub fn from_sorted(features: Vec<String>) -> Result<Self, IdentityError> {
377 validate_features_sorted(&features)?;
378 Ok(Self(features))
379 }
380
381 pub fn canonicalize(mut features: Vec<String>) -> Result<Self, IdentityError> {
387 features.sort();
388 features.dedup();
389 Self::from_sorted(features)
390 }
391
392 #[must_use]
394 pub fn features(&self) -> &[String] {
395 &self.0
396 }
397
398 #[must_use]
404 pub fn raw(&self) -> String {
405 serde_json::to_string(&self.0).expect("Vec<String> always serializes")
406 }
407}
408
409impl fmt::Display for FeaturesJson {
410 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
412 f.write_str(&self.raw())
413 }
414}
415
416impl Serialize for FeaturesJson {
417 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
418 self.raw().serialize(s)
419 }
420}
421
422impl<'de> Deserialize<'de> for FeaturesJson {
423 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
424 let raw = String::deserialize(d)?;
425 let features: Vec<String> = serde_json::from_str(&raw).map_err(|error| {
426 serde::de::Error::custom(IdentityError::InvalidJsonWrapper(error.to_string()))
427 })?;
428 Self::from_sorted(features).map_err(serde::de::Error::custom)
429 }
430}
431
432#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
438pub struct DependencyCompileKeyIdentity {
439 pub crate_name: CrateName,
441 pub compile_key: String,
443}
444
445impl DependencyCompileKeyIdentity {
446 #[must_use]
448 pub fn canonicalize_list(mut identities: Vec<Self>) -> Vec<Self> {
449 identities.sort();
450 identities.dedup();
451 identities
452 }
453}
454
455#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
458pub struct DependencyCMetadataIdentity {
459 pub crate_name: CrateName,
461 pub c_metadata: CMetadata,
463}
464
465#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
471pub struct DependencyCMetadataJson(Vec<DependencyCMetadataIdentity>);
472
473impl utoipa::PartialSchema for DependencyCMetadataJson {
475 fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
476 <String as utoipa::PartialSchema>::schema()
477 }
478}
479
480impl utoipa::ToSchema for DependencyCMetadataJson {}
481
482impl DependencyCMetadataJson {
483 pub fn from_sorted(
489 identities: Vec<DependencyCMetadataIdentity>,
490 ) -> Result<Self, IdentityError> {
491 let mut previous: Option<(&str, &str)> = None;
492 for identity in &identities {
493 let current = (identity.crate_name.as_str(), identity.c_metadata.as_str());
494 if previous.is_some_and(|last| last >= current) {
495 return Err(IdentityError::UnsortedDependencyIdentities);
496 }
497 previous = Some(current);
498 }
499 Ok(Self(identities))
500 }
501
502 pub fn canonicalize(
508 mut identities: Vec<DependencyCMetadataIdentity>,
509 ) -> Result<Self, IdentityError> {
510 identities.sort();
511 identities.dedup();
512 Self::from_sorted(identities)
513 }
514
515 #[must_use]
517 pub fn entries(&self) -> &[DependencyCMetadataIdentity] {
518 &self.0
519 }
520
521 #[must_use]
527 pub fn raw(&self) -> String {
528 serde_json::to_string(&self.0).expect("dependency identity list always serializes")
529 }
530}
531
532impl fmt::Display for DependencyCMetadataJson {
533 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
535 f.write_str(&self.raw())
536 }
537}
538
539impl Serialize for DependencyCMetadataJson {
540 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
541 self.raw().serialize(s)
542 }
543}
544
545impl<'de> Deserialize<'de> for DependencyCMetadataJson {
546 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
547 let raw = String::deserialize(d)?;
548 let entries: Vec<DependencyCMetadataIdentity> =
549 serde_json::from_str(&raw).map_err(|error| {
550 serde::de::Error::custom(IdentityError::InvalidJsonWrapper(error.to_string()))
551 })?;
552 Self::from_sorted(entries).map_err(serde::de::Error::custom)
553 }
554}
555
556#[cfg(test)]
557mod tests {
558 use super::*;
559
560 #[test]
561 fn crate_name_accepts_typical() {
562 CrateName::parse("serde_json").unwrap();
563 CrateName::parse("proc-macro2").unwrap();
564 }
565
566 #[test]
567 fn crate_name_rejects_empty_and_overlong() {
568 assert!(CrateName::parse("").is_err());
569 assert!(CrateName::parse("x".repeat(129)).is_err());
570 assert!(CrateName::parse("a b").is_err());
571 }
572
573 #[test]
574 fn c_metadata_must_be_hex() {
575 CMetadata::parse("deadbeef").unwrap();
576 assert!(CMetadata::parse("xyz").is_err());
577 }
578
579 #[test]
580 fn rustc_version_shape() {
581 WireRustcVersion::parse("1.83.0").unwrap();
582 WireRustcVersion::parse("1.91.0-nightly").unwrap();
583 assert!(WireRustcVersion::parse("").is_err());
584 }
585
586 #[test]
587 fn features_json_round_trip() {
588 let raw = "[\"default\",\"std\"]";
589 let features: FeaturesJson =
590 serde_json::from_value(serde_json::Value::String(raw.to_owned())).unwrap();
591 assert_eq!(features.features(), &["default", "std"]);
592 let serialized = serde_json::to_value(&features).unwrap();
593 assert_eq!(serialized, serde_json::Value::String(raw.to_owned()));
594 }
595
596 #[test]
597 fn features_json_rejects_unsorted() {
598 let raw = "[\"std\",\"default\"]";
599 let result: Result<FeaturesJson, _> =
600 serde_json::from_value(serde_json::Value::String(raw.to_owned()));
601 assert!(result.is_err());
602 }
603
604 #[test]
605 fn dependency_identities_round_trip() {
606 let raw = "[{\"crate_name\":\"a\",\"c_metadata\":\"deadbeef\"}]";
607 let value: DependencyCMetadataJson =
608 serde_json::from_value(serde_json::Value::String(raw.to_owned())).unwrap();
609 assert_eq!(value.entries().len(), 1);
610 let serialized = serde_json::to_value(&value).unwrap();
611 assert_eq!(serialized, serde_json::Value::String(raw.to_owned()));
612 }
613}