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