use std::collections::BTreeMap;
use std::fmt::{Display, Formatter, Write};
use std::path::{Path, PathBuf};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
mod pubtime_format {
use chrono::{DateTime, Utc};
use serde::{self, Deserialize, Deserializer, Serializer};
const FORMAT: &str = "%Y-%m-%dT%H:%M:%SZ";
#[allow(clippy::ref_option)] pub fn serialize<S>(date: &Option<DateTime<Utc>>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match date {
Some(dt) => serializer.serialize_str(&dt.format(FORMAT).to_string()),
None => serializer.serialize_none(),
}
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<DateTime<Utc>>, D::Error>
where
D: Deserializer<'de>,
{
let s: Option<String> = Option::deserialize(deserializer)?;
match s {
Some(s) => DateTime::parse_from_rfc3339(&s)
.map(|dt| Some(dt.with_timezone(&Utc)))
.map_err(serde::de::Error::custom),
None => Ok(None),
}
}
}
use tokio::fs::File;
use tokio::io::AsyncReadExt;
use crate::publish_metadata::{PublishMetadata, RegistryDep};
use crate::version::Version;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct IndexMetadata {
pub name: String,
pub vers: String,
pub deps: Vec<IndexDep>,
pub cksum: String,
pub features: BTreeMap<String, Vec<String>>,
pub yanked: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub links: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "pubtime_format"
)]
pub pubtime: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub v: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub features2: Option<BTreeMap<String, Vec<String>>>,
}
impl IndexMetadata {
pub async fn from_max_version(path: &Path) -> Result<Self, std::io::Error> {
let mut file = File::open(path).await?;
let mut content = String::new();
file.read_to_string(&mut content).await?;
let mut metadata: Vec<IndexMetadata> = content
.lines()
.filter_map(|m| serde_json::from_str::<IndexMetadata>(m).ok())
.collect();
metadata.sort_by(|a, b| {
let sv1 = Version::from_unchecked_str(&a.vers);
let sv2 = Version::from_unchecked_str(&b.vers);
sv1.cmp(&sv2)
});
metadata.last().cloned().ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
"Unable to read metadata file.",
)
})
}
pub async fn from_version(path: &Path, version: &Version) -> Result<Self, std::io::Error> {
let mut file = File::open(path).await?;
let mut content = String::new();
file.read_to_string(&mut content).await?;
let metadata: Vec<IndexMetadata> = content
.lines()
.filter_map(|m| serde_json::from_str::<IndexMetadata>(m).ok())
.collect();
metadata
.iter()
.find(|m| {
let sv = Version::try_from(&m.vers).unwrap_or_default();
sv == *version
})
.cloned()
.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
"Unable to read metadata file.",
)
})
}
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string(&self)
}
pub fn metadata_path(&self, index_path: &Path) -> PathBuf {
metadata_path(index_path, &self.name)
}
pub fn from_reg_meta(registry_metadata: &PublishMetadata, cksum: &str) -> Self {
IndexMetadata {
name: registry_metadata.name.clone(),
vers: registry_metadata.vers.clone(),
deps: registry_metadata
.deps
.clone()
.into_iter()
.map(IndexDep::from)
.collect(),
cksum: cksum.to_string(),
pubtime: Some(Utc::now()),
features: registry_metadata.features.clone(),
yanked: false,
links: registry_metadata.links.clone(),
v: Some(1),
features2: None,
}
}
pub fn minimal(name: &str, vers: &str, cksum: &str) -> Self {
Self {
name: name.to_string(),
vers: vers.to_string(),
cksum: cksum.to_string(),
deps: vec![],
features: BTreeMap::default(),
yanked: false,
links: None,
pubtime: None,
v: Some(1),
features2: None,
}
}
pub fn serialize_indices(indices: &[IndexMetadata]) -> Result<String, serde_json::Error> {
let indices = indices
.iter()
.map(serde_json::to_string)
.collect::<Result<Vec<_>, serde_json::Error>>()?;
let mut index = String::new();
for (i, ix) in indices.iter().enumerate() {
if i == indices.len() - 1 {
write!(&mut index, "{ix}").unwrap();
} else {
writeln!(&mut index, "{ix}").unwrap();
}
}
Ok(index)
}
}
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
pub struct IndexDep {
pub name: String,
pub req: String,
pub features: Vec<String>,
pub optional: bool,
pub default_features: bool,
pub target: Option<String>,
pub kind: Option<DependencyKind>,
pub registry: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub package: Option<String>,
}
#[derive(Clone, Debug, PartialEq, PartialOrd, Ord, Eq)]
pub enum DependencyKind {
Normal,
Build,
Dev,
Other(String),
}
impl<'de> Deserialize<'de> for DependencyKind {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
match s.as_str() {
"normal" => Ok(DependencyKind::Normal),
"build" => Ok(DependencyKind::Build),
"dev" => Ok(DependencyKind::Dev),
_ => Ok(DependencyKind::Other(s)),
}
}
}
impl Serialize for DependencyKind {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
DependencyKind::Normal => serializer.serialize_str("normal"),
DependencyKind::Build => serializer.serialize_str("build"),
DependencyKind::Dev => serializer.serialize_str("dev"),
DependencyKind::Other(s) => serializer.serialize_str(s),
}
}
}
impl Display for DependencyKind {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
DependencyKind::Normal => write!(f, "normal"),
DependencyKind::Build => write!(f, "build"),
DependencyKind::Dev => write!(f, "dev"),
DependencyKind::Other(s) => write!(f, "{s}"),
}
}
}
impl From<String> for DependencyKind {
fn from(kind: String) -> Self {
match kind.as_str() {
"normal" => DependencyKind::Normal,
"build" => DependencyKind::Build,
"dev" => DependencyKind::Dev,
_ => DependencyKind::Other(kind),
}
}
}
impl From<RegistryDep> for IndexDep {
fn from(registry_dep: RegistryDep) -> Self {
IndexDep {
name: match registry_dep.explicit_name_in_toml {
Some(ref name) => name.clone(),
None => registry_dep.name.clone(),
},
req: registry_dep.version_req,
features: registry_dep.features.unwrap_or_default(),
optional: registry_dep.optional,
default_features: registry_dep.default_features,
target: registry_dep.target,
kind: registry_dep.kind.map(DependencyKind::from),
registry: registry_dep.registry,
package: match registry_dep.explicit_name_in_toml {
Some(_) => Some(registry_dep.name),
None => None,
},
}
}
}
pub fn metadata_path(index_path: &Path, name: &str) -> PathBuf {
if name.len() == 1 {
index_path.join("1").join(name.to_lowercase())
} else if name.len() == 2 {
index_path.join("2").join(name.to_lowercase())
} else if name.len() == 3 {
let first_char = &name[0..1].to_lowercase();
index_path
.join("3")
.join(first_char)
.join(name.to_lowercase())
} else {
let first_two = &name[0..2].to_lowercase();
let second_two = &name[2..4].to_lowercase();
index_path
.join(first_two)
.join(second_two)
.join(name.to_lowercase())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn transitive_dependency_rename() {
let reg_meta = PublishMetadata {
name: "foo".to_string(),
vers: "0.1.0".to_string(),
deps: vec![
RegistryDep {
name: "bar".to_string(),
version_req: "^0.1.0".to_string(),
features: None,
optional: false,
default_features: true,
target: None,
kind: None,
registry: None,
explicit_name_in_toml: None,
},
RegistryDep {
name: "baz".to_string(),
version_req: "^0.1.0".to_string(),
features: None,
optional: false,
default_features: true,
target: None,
kind: None,
registry: None,
explicit_name_in_toml: Some("qux".to_string()),
},
],
features: BTreeMap::default(),
links: None,
description: None,
authors: None,
documentation: None,
homepage: None,
readme: None,
readme_file: None,
keywords: Vec::default(),
categories: Vec::default(),
license: None,
license_file: None,
repository: None,
badges: None,
rust_version: None,
};
let index_meta = IndexMetadata::from_reg_meta(®_meta, "1234");
assert_eq!(index_meta.deps.len(), 2);
assert_eq!(index_meta.deps[0].name, "bar");
assert_eq!(index_meta.deps[0].package, None);
assert_eq!(index_meta.deps[1].name, "qux");
assert_eq!(index_meta.deps[1].package, Some("baz".to_string()));
}
#[test]
fn metadata_path_one_letter() {
let name = "A";
assert_eq!(
metadata_path(&PathBuf::from("ip"), name),
Path::new("ip").join("1").join("a")
);
}
#[test]
fn metadata_path_two_letters() {
let name = "cB";
assert_eq!(
metadata_path(&PathBuf::from("ip"), name),
Path::new("ip").join("2").join("cb")
);
}
#[test]
fn metadata_path_three_letters() {
let name = "cAb";
assert_eq!(
metadata_path(&PathBuf::from("ip"), name),
Path::new("ip").join("3").join("c").join("cab")
);
}
#[test]
fn metadata_path_four_or_more_letters() {
let name = "foo_bAr";
assert_eq!(
metadata_path(&PathBuf::from("ip"), name),
Path::new("ip").join("fo").join("o_").join("foo_bar")
);
}
#[test]
fn pubtime_serializes_without_fractional_seconds() {
use chrono::TimeZone;
let pubtime = Utc.with_ymd_and_hms(2025, 1, 2, 9, 5, 7).unwrap();
let metadata = IndexMetadata {
name: "test".to_string(),
vers: "1.0.0".to_string(),
deps: vec![],
cksum: "abc123".to_string(),
features: BTreeMap::new(),
yanked: false,
links: None,
pubtime: Some(pubtime),
v: Some(1),
features2: None,
};
let json = metadata.to_json().unwrap();
assert!(
json.contains(r#""pubtime":"2025-01-02T09:05:07Z""#),
"Expected pubtime to be serialized as '2025-01-02T09:05:07Z', got: {json}"
);
}
#[test]
fn pubtime_none_is_omitted_from_serialization() {
let metadata = IndexMetadata {
name: "test".to_string(),
vers: "1.0.0".to_string(),
deps: vec![],
cksum: "abc123".to_string(),
features: BTreeMap::new(),
yanked: false,
links: None,
pubtime: None,
v: Some(1),
features2: None,
};
let json = metadata.to_json().unwrap();
assert!(
!json.contains("pubtime"),
"Expected pubtime to be omitted when None, got: {json}"
);
}
#[test]
fn pubtime_deserializes_from_rfc3339() {
use chrono::{Datelike, Timelike};
let json = r#"{"name":"test","vers":"1.0.0","deps":[],"cksum":"abc","features":{},"yanked":false,"pubtime":"2025-01-02T09:05:07Z","v":1}"#;
let metadata: IndexMetadata = serde_json::from_str(json).unwrap();
assert!(metadata.pubtime.is_some());
let pubtime = metadata.pubtime.unwrap();
assert_eq!(pubtime.year(), 2025);
assert_eq!(pubtime.month(), 1);
assert_eq!(pubtime.day(), 2);
assert_eq!(pubtime.hour(), 9);
assert_eq!(pubtime.minute(), 5);
assert_eq!(pubtime.second(), 7);
}
#[test]
fn pubtime_deserializes_from_rfc3339_with_fractional_seconds() {
use chrono::{Datelike, Timelike};
let json = r#"{"name":"test","vers":"1.0.0","deps":[],"cksum":"abc","features":{},"yanked":false,"pubtime":"2025-01-02T09:05:07.123456Z","v":1}"#;
let metadata: IndexMetadata = serde_json::from_str(json).unwrap();
assert!(metadata.pubtime.is_some());
let pubtime = metadata.pubtime.unwrap();
assert_eq!(pubtime.year(), 2025);
assert_eq!(pubtime.month(), 1);
assert_eq!(pubtime.day(), 2);
assert_eq!(pubtime.hour(), 9);
assert_eq!(pubtime.minute(), 5);
assert_eq!(pubtime.second(), 7);
}
}