1use std::collections::BTreeMap;
4use std::fmt;
5use std::{cmp::Ordering, str::FromStr};
6
7use serde::{Deserialize, Serialize, de, ser};
8use url::Url;
9
10use crate::core::{GitReference, SourceKind};
11
12#[derive(Serialize, Deserialize, Debug)]
14#[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))]
15pub struct TomlLockfile {
16 pub version: Option<u32>,
21 pub package: Option<Vec<TomlLockfileDependency>>,
23 pub root: Option<TomlLockfileDependency>,
28 pub metadata: Option<TomlLockfileMetadata>,
34 #[serde(default, skip_serializing_if = "TomlLockfilePatch::is_empty")]
38 pub patch: TomlLockfilePatch,
39}
40
41pub type TomlLockfileMetadata = BTreeMap<String, String>;
45
46#[derive(Serialize, Deserialize, Debug, Default)]
50#[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))]
51pub struct TomlLockfilePatch {
52 pub unused: Vec<TomlLockfileDependency>,
54}
55
56impl TomlLockfilePatch {
57 fn is_empty(&self) -> bool {
58 self.unused.is_empty()
59 }
60}
61
62#[derive(Serialize, Deserialize, Debug, PartialOrd, Ord, PartialEq, Eq)]
64#[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))]
65pub struct TomlLockfileDependency {
66 pub name: String,
68 pub version: String,
70 pub source: Option<TomlLockfileSourceId>,
74 pub checksum: Option<String>,
79 pub dependencies: Option<Vec<TomlLockfilePackageId>>,
81 pub replace: Option<TomlLockfilePackageId>,
83}
84
85#[derive(Debug, Clone)]
87#[cfg_attr(
88 feature = "unstable-schema",
89 derive(schemars::JsonSchema),
90 schemars(with = "String")
91)]
92pub struct TomlLockfileSourceId {
93 source_str: String,
95 kind: SourceKind,
99 url: Url,
103}
104
105impl TomlLockfileSourceId {
106 pub fn new(source: String) -> Result<Self, EncodableSourceIdError> {
107 let source_str = source.clone();
108 let (kind, url) = source.split_once('+').ok_or_else(|| {
109 EncodableSourceIdError(EncodableSourceIdErrorKind::InvalidSource(source.clone()).into())
110 })?;
111
112 let url = Url::parse(if kind == "sparse" { &source } else { url }).map_err(|msg| {
115 EncodableSourceIdErrorKind::InvalidUrl {
116 url: url.to_string(),
117 msg: msg.to_string(),
118 }
119 })?;
120
121 let kind = match kind {
122 "git" => {
123 let reference = GitReference::from_query(url.query_pairs());
124 SourceKind::Git(reference)
125 }
126 "registry" => SourceKind::Registry,
127 "sparse" => SourceKind::SparseRegistry,
128 "path" => SourceKind::Path,
129 kind => {
130 return Err(EncodableSourceIdErrorKind::UnsupportedSource(kind.to_string()).into());
131 }
132 };
133
134 Ok(Self {
135 source_str,
136 kind,
137 url,
138 })
139 }
140
141 pub fn kind(&self) -> &SourceKind {
142 &self.kind
143 }
144
145 pub fn url(&self) -> &Url {
146 &self.url
147 }
148
149 pub fn source_str(&self) -> &String {
150 &self.source_str
151 }
152
153 pub fn as_url(&self) -> impl fmt::Display + '_ {
154 self.source_str.clone()
155 }
156}
157
158impl ser::Serialize for TomlLockfileSourceId {
159 fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
160 where
161 S: ser::Serializer,
162 {
163 s.collect_str(&self.as_url())
164 }
165}
166
167impl<'de> de::Deserialize<'de> for TomlLockfileSourceId {
168 fn deserialize<D>(d: D) -> Result<Self, D::Error>
169 where
170 D: de::Deserializer<'de>,
171 {
172 let s = String::deserialize(d)?;
173 Ok(TomlLockfileSourceId::new(s).map_err(de::Error::custom)?)
174 }
175}
176
177impl std::hash::Hash for TomlLockfileSourceId {
178 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
179 self.kind.hash(state);
180 self.url.hash(state);
181 }
182}
183
184impl std::cmp::PartialEq for TomlLockfileSourceId {
185 fn eq(&self, other: &Self) -> bool {
186 self.kind == other.kind && self.url == other.url
187 }
188}
189
190impl std::cmp::Eq for TomlLockfileSourceId {}
191
192impl PartialOrd for TomlLockfileSourceId {
193 fn partial_cmp(&self, other: &TomlLockfileSourceId) -> Option<Ordering> {
194 Some(self.cmp(other))
195 }
196}
197
198impl Ord for TomlLockfileSourceId {
199 fn cmp(&self, other: &TomlLockfileSourceId) -> Ordering {
200 self.kind
201 .cmp(&other.kind)
202 .then_with(|| self.url.cmp(&other.url))
203 }
204}
205
206#[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Hash, Clone)]
212#[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))]
213pub struct TomlLockfilePackageId {
214 pub name: String,
215 pub version: Option<String>,
216 pub source: Option<TomlLockfileSourceId>,
217}
218
219impl fmt::Display for TomlLockfilePackageId {
220 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
221 write!(f, "{}", self.name)?;
222 if let Some(s) = &self.version {
223 write!(f, " {}", s)?;
224 }
225 if let Some(s) = &self.source {
226 write!(f, " ({})", s.as_url())?;
227 }
228 Ok(())
229 }
230}
231
232impl FromStr for TomlLockfilePackageId {
233 type Err = EncodablePackageIdError;
234
235 fn from_str(s: &str) -> Result<TomlLockfilePackageId, Self::Err> {
236 let mut s = s.splitn(3, ' ');
237 let name = s.next().unwrap();
238 let version = s.next();
239 let source_id = match s.next() {
240 Some(s) => {
241 if let Some(s) = s.strip_prefix('(').and_then(|s| s.strip_suffix(')')) {
242 Some(TomlLockfileSourceId::new(s.to_string())?)
243 } else {
244 return Err(EncodablePackageIdErrorKind::InvalidSerializedPackageId.into());
245 }
246 }
247 None => None,
248 };
249
250 Ok(TomlLockfilePackageId {
251 name: name.to_string(),
252 version: version.map(|v| v.to_string()),
253 source: source_id,
254 })
255 }
256}
257
258impl ser::Serialize for TomlLockfilePackageId {
259 fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
260 where
261 S: ser::Serializer,
262 {
263 s.collect_str(self)
264 }
265}
266
267impl<'de> de::Deserialize<'de> for TomlLockfilePackageId {
268 fn deserialize<D>(d: D) -> Result<TomlLockfilePackageId, D::Error>
269 where
270 D: de::Deserializer<'de>,
271 {
272 String::deserialize(d).and_then(|string| {
273 string
274 .parse::<TomlLockfilePackageId>()
275 .map_err(de::Error::custom)
276 })
277 }
278}
279
280#[derive(Debug, thiserror::Error)]
281#[error(transparent)]
282pub struct EncodableSourceIdError(#[from] EncodableSourceIdErrorKind);
283
284#[non_exhaustive]
285#[derive(Debug, thiserror::Error)]
286enum EncodableSourceIdErrorKind {
287 #[error("invalid source `{0}`")]
288 InvalidSource(String),
289
290 #[error("invalid url `{url}`: {msg}")]
291 InvalidUrl { url: String, msg: String },
292
293 #[error("unsupported source protocol: {0}")]
294 UnsupportedSource(String),
295}
296
297#[derive(Debug, thiserror::Error)]
298#[error(transparent)]
299pub struct EncodablePackageIdError(#[from] EncodablePackageIdErrorKind);
300
301impl From<EncodableSourceIdError> for EncodablePackageIdError {
302 fn from(value: EncodableSourceIdError) -> Self {
303 EncodablePackageIdErrorKind::Source(value).into()
304 }
305}
306
307#[non_exhaustive]
308#[derive(Debug, thiserror::Error)]
309enum EncodablePackageIdErrorKind {
310 #[error("invalid serialied PackageId")]
311 InvalidSerializedPackageId,
312
313 #[error(transparent)]
314 Source(#[from] EncodableSourceIdError),
315}
316
317#[cfg(feature = "unstable-schema")]
318#[test]
319fn dump_lockfile_schema() {
320 let schema = schemars::schema_for!(crate::lockfile::TomlLockfile);
321 let dump = serde_json::to_string_pretty(&schema).unwrap();
322 snapbox::assert_data_eq!(dump, snapbox::file!("../lockfile.schema.json").raw());
323}
324
325#[cfg(test)]
326mod tests {
327 use crate::core::{GitReference, SourceKind};
328 use crate::lockfile::{EncodableSourceIdErrorKind, TomlLockfileSourceId};
329
330 #[track_caller]
331 fn ok(source_str: &str, source_kind: SourceKind, url: &str) {
332 let source_str = source_str.to_owned();
333 let source_id = TomlLockfileSourceId::new(source_str).unwrap();
334 assert_eq!(source_id.kind, source_kind);
335 assert_eq!(source_id.url().to_string(), url);
336 }
337
338 macro_rules! err {
339 ($src:expr, $expected:pat) => {
340 let kind = TomlLockfileSourceId::new($src.to_owned()).unwrap_err().0;
341 assert!(
342 matches!(kind, $expected),
343 "`{}` parse error mismatch, got {kind:?}",
344 $src,
345 );
346 };
347 }
348
349 #[test]
350 fn good_sources() {
351 ok(
352 "sparse+https://my-crates.io",
353 SourceKind::SparseRegistry,
354 "sparse+https://my-crates.io",
355 );
356 ok(
357 "registry+https://github.com/rust-lang/crates.io-index",
358 SourceKind::Registry,
359 "https://github.com/rust-lang/crates.io-index",
360 );
361 ok(
362 "git+https://github.com/rust-lang/cargo",
363 SourceKind::Git(GitReference::DefaultBranch),
364 "https://github.com/rust-lang/cargo",
365 );
366 ok(
367 "git+https://github.com/rust-lang/cargo?branch=dev",
368 SourceKind::Git(GitReference::Branch("dev".to_owned())),
369 "https://github.com/rust-lang/cargo?branch=dev",
370 );
371 ok(
372 "git+https://github.com/rust-lang/cargo?tag=v1.0",
373 SourceKind::Git(GitReference::Tag("v1.0".to_owned())),
374 "https://github.com/rust-lang/cargo?tag=v1.0",
375 );
376 ok(
377 "git+https://github.com/rust-lang/cargo?rev=refs/pull/493/head",
378 SourceKind::Git(GitReference::Rev("refs/pull/493/head".to_owned())),
379 "https://github.com/rust-lang/cargo?rev=refs/pull/493/head",
380 );
381 ok(
382 "path+file:///path/to/root",
383 SourceKind::Path,
384 "file:///path/to/root",
385 );
386 }
387
388 #[test]
389 fn bad_sources() {
390 err!(
391 "unknown+https://my-crates.io",
392 EncodableSourceIdErrorKind::UnsupportedSource(..)
393 );
394 err!(
395 "registry+https//github.com/rust-lang/crates.io-index",
396 EncodableSourceIdErrorKind::InvalidUrl { .. }
397 );
398 err!(
399 "https//github.com/rust-lang/crates.io-index",
400 EncodableSourceIdErrorKind::InvalidSource(..)
401 );
402 }
403}