1use nom::{
2 IResult, Parser,
3 branch::alt,
4 bytes::complete::{is_not, tag, take, take_till, take_until},
5 character::complete::{
6 digit1, line_ending, multispace0, not_line_ending, one_of, space0, space1,
7 },
8 combinator::{cond, eof, map, map_res, opt, recognize},
9 error::{ParseError, context},
10 multi::{count, many_till, many0, many1, separated_list1},
11 sequence::{delimited, preceded, terminated},
12};
13use nom_language::error::VerboseError;
14
15use thiserror::Error;
16
17type Res<T, U> = IResult<T, U, VerboseError<T>>;
18
19#[derive(Debug, Error)]
21#[error("yarn.lock error")]
22pub enum YarnLockError {
23 #[error("Error parsing yarn.lock file")]
24 Parser {
25 #[from]
26 source: nom::Err<VerboseError<String>>,
27 },
28}
29
30#[derive(Debug)]
32#[non_exhaustive]
33pub struct Lockfile<'a> {
34 pub entries: Vec<Entry<'a>>,
35 pub generator: Generator,
36 pub version: u8,
37 pub cache_key: Option<&'a str>,
38}
39
40#[derive(Debug, PartialEq, Eq, Clone, Copy)]
41#[non_exhaustive]
42pub enum Generator {
43 Yarn,
44 Bun,
45}
46
47#[derive(Debug, PartialEq, Eq, Default)]
50#[non_exhaustive]
51pub struct Entry<'a> {
52 pub name: &'a str,
53 pub version: &'a str,
54 pub resolved: &'a str,
55 pub integrity: &'a str,
56 pub dependencies: Vec<(&'a str, &'a str)>,
57 pub optional_dependencies: Vec<(&'a str, &'a str)>,
58 pub dependencies_meta: Vec<(&'a str, DepMeta)>,
59 pub peer_dependencies: Vec<(&'a str, &'a str)>,
60 pub peer_dependencies_meta: Vec<(&'a str, DepMeta)>,
61 pub descriptors: Vec<(&'a str, &'a str)>,
62}
63
64pub fn parse_str(content: &str) -> Result<Lockfile<'_>, YarnLockError> {
68 parse(content).map(|(_, entries)| entries).map_err(|e| {
69 e.map(|ve| {
70 let errors = ve
71 .errors
72 .into_iter()
73 .map(|v| (v.0.to_string(), v.1))
74 .collect();
75 VerboseError { errors }
76 })
77 .into()
78 })
79}
80
81fn parse(input: &str) -> Res<&str, Lockfile<'_>> {
82 let (i, (is_bun, is_v1)) = yarn_lock_header(input)?;
83 let (i, metadata) = cond(!is_v1, yarn_lock_metadata).parse(i)?;
84 let (i, mut entries) = many0(entry).parse(i)?;
85
86 let generator = if is_bun {
87 Generator::Bun
88 } else {
89 Generator::Yarn
90 };
91 let (version, cache_key) = match (is_v1, metadata) {
92 (true, None) => (1, None),
93 (false, Some(m)) => m,
94 (true, Some(_)) | (false, None) => unreachable!(),
96 };
97
98 if i.is_empty() {
100 return Ok((
101 i,
102 Lockfile {
103 entries,
104 generator,
105 version,
106 cache_key,
107 },
108 ));
109 }
110
111 let (i, final_entry) = entry_final(i)?;
112 entries.push(final_entry);
113
114 Ok((
115 i,
116 Lockfile {
117 entries,
118 generator,
119 version,
120 cache_key,
121 },
122 ))
123}
124
125fn take_till_line_end(input: &str) -> Res<&str, &str> {
126 recognize((alt((take_until("\n"), take_until("\r\n"))), take(1usize))).parse(input)
127}
128
129fn take_till_optional_line_end(input: &str) -> Res<&str, &str> {
130 recognize((
131 alt((take_until("\n"), take_until("\r\n"), space0)),
132 take(1usize),
133 ))
134 .parse(input)
135}
136
137fn yarn_lock_header(input: &str) -> Res<&str, (bool, bool)> {
138 let is_bun = input
139 .lines()
140 .skip(2)
141 .take(1)
142 .any(|l| l.starts_with("# bun"));
143 let is_v1 = input
144 .lines()
145 .skip(1)
146 .take(1)
147 .any(|l| l.starts_with("# yarn lockfile v1"));
148 let lines = if is_bun { 3 } else { 2 };
151 let (input, _) = recognize((count(take_till_line_end, lines), multispace0)).parse(input)?;
152 Ok((input, (is_bun, is_v1)))
153}
154
155fn yarn_lock_metadata(input: &str) -> Res<&str, (u8, Option<&str>)> {
156 context(
157 "metadata",
158 terminated(
159 (
160 delimited(
161 (tag("__metadata:"), line_ending, space1, tag("version: ")),
162 map_res(digit1, |d: &str| d.parse()),
163 line_ending,
164 ),
165 opt(preceded(
166 (space1, tag("cacheKey: ")),
167 recognize((digit1, opt((tag("c"), digit1)))),
168 )),
169 ),
170 (
171 many_till(take_till_line_end, (space0, line_ending)),
172 multispace0,
173 ),
174 ),
175 )
176 .parse(input)
177}
178
179fn entry_final(input: &str) -> Res<&str, Entry<'_>> {
180 recognize(many_till(take_till_optional_line_end, eof))
181 .parse(input)
182 .and_then(|(i, capture)| {
183 let (_, my_entry) = parse_entry(capture)?;
184 Ok((i, my_entry))
185 })
186}
187
188fn entry(input: &str) -> Res<&str, Entry<'_>> {
189 recognize(many_till(
190 take_till_line_end,
191 recognize((space0, line_ending)),
192 ))
193 .parse(input)
194 .and_then(|(i, capture)| {
195 let (_, my_entry) = parse_entry(capture)?;
196 Ok((i, my_entry))
197 })
198}
199
200#[derive(PartialEq, Debug)]
201enum EntryItem<'a> {
202 Version(&'a str),
203 Resolved(&'a str),
204 Dependencies(Vec<(&'a str, &'a str)>),
205 OptionalDependencies(Vec<(&'a str, &'a str)>),
206 PeerDependencies(Vec<(&'a str, &'a str)>),
207 DepsMeta(Vec<(&'a str, DepMeta)>),
208 PeersMeta(Vec<(&'a str, DepMeta)>),
209 Integrity(&'a str),
210 Unknown(&'a str),
211}
212
213fn unknown_line(input: &str) -> Res<&str, EntryItem<'_>> {
214 take_till_line_end(input).map(|(i, res)| (i, EntryItem::Unknown(res)))
215}
216
217fn integrity(input: &str) -> Res<&str, EntryItem<'_>> {
218 context(
219 "integrity",
220 (
221 space1,
222 opt(tag("\"")),
223 alt((tag("checksum"), tag("integrity"))),
224 opt(tag("\"")),
225 opt(tag(":")),
226 space1,
227 opt(tag("\"")),
228 take_till(|c| c == '"' || c == '\n' || c == '\r'),
229 ),
230 )
231 .parse(input)
232 .map(|(i, (_, _, _, _, _, _, _, integrity))| (i, EntryItem::Integrity(integrity)))
233}
234
235fn entry_item(input: &str) -> Res<&str, EntryItem<'_>> {
236 alt((
237 entry_version,
238 parse_dependencies,
239 integrity,
240 entry_resolved,
241 parse_deps_meta,
242 unknown_line,
243 ))
244 .parse(input)
245}
246
247fn parse_entry(input: &str) -> Res<&str, Entry<'_>> {
248 context("entry", (entry_descriptors, many1(entry_item)))
249 .parse(input)
250 .and_then(|(next_input, res)| {
251 let (descriptors, entry_items) = res;
252
253 let first_descriptor = descriptors.first().expect("Somehow descriptors is empty");
255
256 let name = first_descriptor.0;
257
258 let mut version = "";
259 let mut resolved = "";
260 let mut dependencies = Vec::new();
261 let mut optional_dependencies = Vec::new();
262 let mut dependencies_meta = Vec::new();
263 let mut peer_dependencies = Vec::new();
264 let mut peer_dependencies_meta = Vec::new();
265 let mut integrity = "";
266
267 for ei in entry_items {
268 match ei {
269 EntryItem::Version(v) => version = v,
270 EntryItem::Resolved(r) => resolved = r,
271 EntryItem::Dependencies(d) => dependencies = d,
272 EntryItem::OptionalDependencies(d) => optional_dependencies = d,
273 EntryItem::PeerDependencies(d) => peer_dependencies = d,
274 EntryItem::Integrity(c) => integrity = c,
275 EntryItem::DepsMeta(m) => dependencies_meta = m,
276 EntryItem::PeersMeta(m) => peer_dependencies_meta = m,
277 EntryItem::Unknown(_) => (),
278 }
279 }
280
281 if version.is_empty() {
282 return Err(nom::Err::Failure(VerboseError::from_error_kind(
283 "version is empty for an entry",
284 nom::error::ErrorKind::Fail,
285 )));
286 }
287
288 Ok((
289 next_input,
290 Entry {
291 name,
292 version,
293 resolved,
294 integrity,
295 dependencies,
296 optional_dependencies,
297 dependencies_meta,
298 peer_dependencies,
299 peer_dependencies_meta,
300 descriptors,
301 },
302 ))
303 })
304}
305
306fn dependency_version(input: &str) -> Res<&str, &str> {
307 alt((double_quoted_text, not_line_ending)).parse(input)
308}
309
310fn parse_dependencies(input: &str) -> Res<&str, EntryItem<'_>> {
311 enum DepsKind {
312 Deps,
313 Optional,
315 Peer,
317 }
318 let (input, (indent, key, _)) = (
319 space1,
320 alt((
321 map(tag("dependencies:"), |_| DepsKind::Deps),
322 map(tag("optionalDependencies:"), |_| DepsKind::Optional),
323 map(tag("peerDependencies:"), |_| DepsKind::Peer),
324 )),
325 line_ending,
326 )
327 .parse(input)?;
328
329 let dependencies_parser = many1(move |i| {
330 (
331 tag(indent), space1, is_not(": "), one_of(": "),
335 space0,
336 dependency_version, alt((line_ending, space0)), )
339 .parse(i)
340 .map(|(i, (_, _, p, _, _, v, _))| (i, (p.trim_matches('"'), v)))
341 });
342 context("dependencies", dependencies_parser)
343 .parse(input)
344 .map(|(i, res)| {
345 (
346 i,
347 match key {
348 DepsKind::Deps => EntryItem::Dependencies(res),
349 DepsKind::Optional => EntryItem::OptionalDependencies(res),
350 DepsKind::Peer => EntryItem::PeerDependencies(res),
351 },
352 )
353 })
354}
355
356#[derive(Debug, PartialEq, Eq, Default)]
357#[non_exhaustive]
358pub struct DepMeta {
359 pub optional: Option<bool>,
360}
361
362fn parse_deps_meta(input: &str) -> Res<&str, EntryItem<'_>> {
363 enum MetaKind {
364 Deps,
365 Peers,
366 }
367 let (input, indent_top) = space1(input)?;
368 let (input, key) = terminated(
369 alt((
370 map(tag("dependenciesMeta:"), |_| MetaKind::Deps),
371 map(tag("peerDependenciesMeta:"), |_| MetaKind::Peers),
372 )),
373 line_ending,
374 )
375 .parse(input)?;
376 many1(|i| deps_meta_dep(i, indent_top))
377 .parse(input)
378 .map(|(i, dm)| {
379 (
380 i,
381 match key {
382 MetaKind::Deps => EntryItem::DepsMeta(dm),
383 MetaKind::Peers => EntryItem::PeersMeta(dm),
384 },
385 )
386 })
387}
388
389fn deps_meta_dep<'a>(input: &'a str, indent_top: &'a str) -> Res<&'a str, (&'a str, DepMeta)> {
390 let (input, indent_dep) = recognize((tag(indent_top), space1)).parse(input)?;
391 let (input, dep_name) = alt((double_quoted_text, take_until(":"))).parse(input)?;
392 let (input, _) = (tag(":"), line_ending).parse(input)?;
393 many1(|i| peers_meta_dep_prop(i, indent_dep))
394 .parse(input)
395 .and_then(|(i, props)| {
396 let mut meta = DepMeta { optional: None };
397 for (prop_key, prop_val) in props {
398 #[allow(clippy::single_match)]
399 match prop_key {
400 "optional" => {
401 meta.optional = Some(match prop_val {
402 "true" => true,
403 "false" => false,
404 _ => {
405 return Err(nom::Err::Failure(VerboseError::from_error_kind(
406 "bool property not 'true' or 'false'",
407 nom::error::ErrorKind::Fail,
408 )));
409 }
410 });
411 }
412 _ => {}
413 }
414 }
415 Ok((i, (dep_name, meta)))
416 })
417}
418
419fn peers_meta_dep_prop<'a>(
420 input: &'a str,
421 indent_dep: &'a str,
422) -> Res<&'a str, (&'a str, &'a str)> {
423 let (input, _) = recognize((tag(indent_dep), space1)).parse(input)?;
424 let (input, (prop_key, _, prop_val)) = (
425 take_until(":"),
426 tag(": "),
427 map(take_till_line_end, |v| {
428 v.strip_suffix("\r\n")
429 .or_else(|| v.strip_suffix("\n"))
430 .unwrap()
431 }),
432 )
433 .parse(input)?;
434 Ok((input, (prop_key, prop_val)))
435}
436
437fn double_quoted_text(input: &str) -> Res<&str, &str> {
442 delimited(tag("\""), take_until("\""), tag("\"")).parse(input)
443}
444
445fn entry_single_descriptor<'a>(input: &'a str) -> Res<&'a str, (&'a str, &'a str)> {
446 let (i, (_, desc)) = (opt(tag("\"")), is_not(",\"\n")).parse(input)?;
447 let i = i.strip_prefix('"').unwrap_or(i);
448
449 let (_, (name, version)) = context("entry single descriptor", |i: &'a str| {
450 #[allow(clippy::manual_strip)]
451 let name_end_idx = if i.starts_with('@') {
452 i[1..].find('@').map(|idx| idx + 1)
453 } else {
454 i.find('@')
455 };
456
457 let Some(name_end_idx) = name_end_idx else {
458 return Err(nom::Err::Failure(VerboseError::from_error_kind(
459 "version format error: @ not found",
460 nom::error::ErrorKind::Fail,
461 )));
462 };
463
464 let (name, version) = (&i[..name_end_idx], &i[name_end_idx + 1..]);
465
466 Ok((i, (name, version)))
467 })
468 .parse(desc)?;
469
470 Ok((i, (name, version)))
471}
472
473fn entry_descriptors<'a>(input: &'a str) -> Res<&'a str, Vec<(&'a str, &'a str)>> {
474 context(
481 "descriptors",
482 |input: &'a str| -> Res<&str, Vec<(&str, &str)>> {
483 let (input, line) = take_till_line_end(input)?;
484
485 let line = line
486 .strip_suffix(":\r\n")
487 .or_else(|| line.strip_suffix(":\n"));
488
489 if line.is_none() {
490 return Err(nom::Err::Failure(VerboseError::from_error_kind(
491 "descriptor does not end with : followed by newline",
492 nom::error::ErrorKind::Fail,
493 )));
494 }
495 let line = line.unwrap();
496
497 let (_, res) = separated_list1((opt(tag("\"")), tag(", ")), entry_single_descriptor)
498 .parse(line)?;
499
500 Ok((input, res))
501 },
502 )
503 .parse(input)
504}
505
506fn entry_resolved(input: &str) -> Res<&str, EntryItem<'_>> {
507 context(
511 "resolved",
512 preceded(
513 (
514 space1,
515 opt(tag("\"")),
516 alt((tag("resolved"), tag("resolution"))),
517 opt(tag("\"")),
518 opt(tag(":")),
519 space1,
520 tag("\""),
521 ),
522 terminated(
523 map(is_not("\"\r\n"), EntryItem::Resolved),
524 (tag("\""), line_ending),
525 ),
526 ),
527 )
528 .parse(input)
529}
530
531fn entry_version(input: &str) -> Res<&str, EntryItem<'_>> {
532 context(
537 "version",
538 (
539 space1,
540 opt(tag("\"")),
541 tag("version"),
542 opt(tag("\"")),
543 opt(tag(":")),
544 space1,
545 opt(tag("\"")),
546 is_version,
547 opt(tag("\"")),
548 line_ending,
549 ),
550 )
551 .parse(input)
552 .map(|(i, (_, _, _, _, _, _, _, version, _, _))| (i, EntryItem::Version(version)))
553}
554
555fn is_version(input: &str) -> Res<&str, &str> {
556 for (idx, byte) in input.as_bytes().iter().enumerate() {
557 if !matches!(
558 byte,
559 b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z'
561 | b'.' | b'-' | b'+'
562 | b'@' | b':' | b'/' | b'#' | b'%'
564 | b'!' | b'~' | b'*' | b'\'' | b'(' | b')'
566 ) {
567 return Ok((&input[idx..], &input[..idx]));
568 }
569 }
570 Err(nom::Err::Error(VerboseError::from_error_kind(
571 input,
572 nom::error::ErrorKind::AlphaNumeric,
573 )))
574}
575
576#[cfg(test)]
577mod tests {
578 use super::*;
579
580 fn assert_v1(res: &(&str, Lockfile)) {
581 assert_eq!(res.0, "");
582 assert_eq!(res.1.generator, Generator::Yarn);
583 assert_eq!(res.1.version, 1);
584 assert_eq!(
585 res.1.entries.first().unwrap(),
586 &Entry {
587 name: "@babel/code-frame",
588 version: "7.12.13",
589 resolved: "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658",
590 descriptors: vec![("@babel/code-frame", "^7.0.0")],
591 dependencies: vec![("@babel/highlight", "^7.12.13")],
592 integrity: "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==",
593 ..Default::default()
594 }
595 );
596
597 assert_eq!(
598 res.1.entries.last().unwrap(),
599 &Entry {
600 name: "yargs",
601 version: "9.0.1",
602 resolved: "https://registry.yarnpkg.com/yargs/-/yargs-9.0.1.tgz#52acc23feecac34042078ee78c0c007f5085db4c",
603 descriptors: vec![("yargs", "^9.0.0")],
604 dependencies: vec![
605 ("camelcase", "^4.1.0"),
606 ("cliui", "^3.2.0"),
607 ("decamelize", "^1.1.1"),
608 ("get-caller-file", "^1.0.1"),
609 ("os-locale", "^2.0.0"),
610 ("read-pkg-up", "^2.0.0"),
611 ("require-directory", "^2.1.1"),
612 ("require-main-filename", "^1.0.1"),
613 ("set-blocking", "^2.0.0"),
614 ("string-width", "^2.0.0"),
615 ("which-module", "^2.0.0"),
616 ("y18n", "^3.2.1"),
617 ("yargs-parser", "^7.0.0"),
618 ],
619 integrity: "sha1-UqzCP+7Kw0BCB47njAwAf1CF20w=",
620 ..Default::default()
621 }
622 );
623 }
624
625 #[test]
626 fn parse_windows_server_from_memory_works() {
627 let content = "# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.\r\n# yarn lockfile v1\r\n\r\n\r\n\"@babel/code-frame@^7.0.0\":\r\n version \"7.12.13\"\r\n resolved \"https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658\"\r\n integrity sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==\r\n dependencies:\r\n \"@babel/highlight\" \"^7.12.13\"\r\n\r\nyargs-parser@^7.0.0:\r\n version \"7.0.0\"\r\n resolved \"https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-7.0.0.tgz#8d0ac42f16ea55debd332caf4c4038b3e3f5dfd9\"\r\n integrity sha1-jQrELxbqVd69MyyvTEA4s+P139k=\r\n dependencies:\r\n camelcase \"^4.1.0\"\r\n\r\nyargs@^9.0.0:\r\n version \"9.0.1\"\r\n resolved \"https://registry.yarnpkg.com/yargs/-/yargs-9.0.1.tgz#52acc23feecac34042078ee78c0c007f5085db4c\"\r\n integrity sha1-UqzCP+7Kw0BCB47njAwAf1CF20w=\r\n dependencies:\r\n camelcase \"^4.1.0\"\r\n cliui \"^3.2.0\"\r\n decamelize \"^1.1.1\"\r\n get-caller-file \"^1.0.1\"\r\n os-locale \"^2.0.0\"\r\n read-pkg-up \"^2.0.0\"\r\n require-directory \"^2.1.1\"\r\n require-main-filename \"^1.0.1\"\r\n set-blocking \"^2.0.0\"\r\n string-width \"^2.0.0\"\r\n which-module \"^2.0.0\"\r\n y18n \"^3.2.1\"\r\n yargs-parser \"^7.0.0\"\r\n";
628 let res = parse(content).unwrap();
629 assert_v1(&res);
630 }
631
632 #[test]
633 fn parse_bun_basic_v1() {
634 let content = std::fs::read_to_string("tests/bun_basic/yarn.lock").unwrap();
635 let (_, res) = parse(&content).unwrap();
636
637 assert_eq!(res.generator, Generator::Bun);
638 assert_eq!(res.entries.len(), 1);
639 }
640
641 #[test]
642 fn parse_bun_workspaces_v1() {
643 let content = std::fs::read_to_string("tests/bun_workspaces/yarn.lock").unwrap();
644 let (_, res) = parse(&content).unwrap();
645
646 assert_eq!(res.generator, Generator::Bun);
647 assert_eq!(res.entries.len(), 19);
648 }
649
650 #[test]
651 fn parse_v1_extra_end_line_from_file_works() {
652 let content = std::fs::read_to_string("tests/v1_extra_end_line/yarn.lock").unwrap();
653 let res = parse(&content).unwrap();
654 assert_v1(&res);
655 }
656
657 #[test]
658 fn parse_v1_bad_format_doc_from_file_does_not_panic() {
659 let content = std::fs::read_to_string("tests/v1_bad_format/yarn.lock").unwrap();
660 let res = parse(&content);
661 assert!(res.is_err());
662 }
663
664 #[test]
665 fn parse_v1_doc_from_file_works() {
666 let content = std::fs::read_to_string("tests/v1/yarn.lock").unwrap();
667 let res = parse(&content).unwrap();
668 assert_v1(&res);
669 }
670
671 #[test]
672 fn parse_v1_doc_from_file_without_endline_works() {
673 let content = std::fs::read_to_string("tests/v1_without_endline/yarn.lock").unwrap();
674 let res = parse(&content).unwrap();
675 assert_v1(&res);
676 }
677
678 #[test]
679 fn parse_v1_doc_from_file_with_npm_bug_works() {
680 let content = std::fs::read_to_string("tests/v1_with_npm_bug/yarn.lock").unwrap();
682 let res = parse(&content).unwrap();
683 assert_v6(&res, true);
686 }
687
688 #[test]
689 fn parse_v1_doc_from_memory_works_v1() {
690 fn assert(input: &str, expect: &[Entry]) {
691 let res = parse(input).unwrap();
692 assert_eq!(res.1.entries, expect);
693 }
694
695 assert(
696 r#"# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
697# yarn lockfile v1
698
699
700"@babel/code-frame@^7.0.0":
701 version "7.12.13"
702 resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658"
703 integrity sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==
704 dependencies:
705 "@babel/highlight" "^7.12.13"
706
707cli-table3@~0.6.1:
708 version "0.6.5"
709 resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.5.tgz#013b91351762739c16a9567c21a04632e449bf2f"
710 integrity sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==
711 dependencies:
712 string-width "^4.2.0"
713 optionalDependencies:
714 "@colors/colors" "1.5.0"
715
716"@babel/helper-validator-identifier@^7.12.11":
717 version "7.12.11"
718 resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed"
719 integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==
720"#,
721 &[
722 Entry {
723 name: "@babel/code-frame",
724 version: "7.12.13",
725 resolved: "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658",
726 descriptors: vec![("@babel/code-frame", "^7.0.0")],
727 dependencies: vec![("@babel/highlight", "^7.12.13")],
728 integrity: "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==",
729 ..Default::default()
730 },
731 Entry {
732 name: "cli-table3",
733 version: "0.6.5",
734 resolved: "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.5.tgz#013b91351762739c16a9567c21a04632e449bf2f",
735 descriptors: vec![("cli-table3", "~0.6.1")],
736 dependencies: vec![("string-width", "^4.2.0")],
737 optional_dependencies: vec![("@colors/colors", "1.5.0")],
738 integrity: "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==",
739 ..Default::default()
740 },
741 Entry {
742 name: "@babel/helper-validator-identifier",
743 version: "7.12.11",
744 resolved: "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed",
745 descriptors: vec![("@babel/helper-validator-identifier", "^7.12.11")],
746 integrity: "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==",
747 ..Default::default()
748 },
749 ],
750 );
751 }
752
753 fn assert_v6(res: &(&str, Lockfile), with_bug: bool) {
754 assert_eq!(res.0, "");
755 assert_eq!(res.1.generator, Generator::Yarn);
756 assert_eq!(res.1.version, if with_bug { 1 } else { 6 });
757 assert_eq!(
758 res.1.entries.first().unwrap(),
759 &Entry {
760 name: "@babel/code-frame",
761 version: "7.18.6",
762 resolved: if with_bug {
763 "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz"
764 } else {
765 "@babel/code-frame@npm:7.18.6"
766 },
767 descriptors: vec![(
768 "@babel/code-frame",
769 if with_bug { "^7.18.6" } else { "npm:^7.18.6" }
770 )],
771 dependencies: vec![("@babel/highlight", "^7.18.6")],
772 integrity: if with_bug {
773 "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q=="
774 } else {
775 "195e2be3172d7684bf95cff69ae3b7a15a9841ea9d27d3c843662d50cdd7d6470fd9c8e64be84d031117e4a4083486effba39f9aef6bbb2c89f7f21bcfba33ba"
776 },
777 ..Default::default()
778 }
779 );
780
781 assert_eq!(
782 res.1.entries.last().unwrap(),
783 &Entry {
784 name: "yargs",
785 version: "17.5.1",
786 resolved: if with_bug {
787 "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz"
788 } else {
789 "yargs@npm:17.5.1"
790 },
791 descriptors: vec![("yargs", if with_bug { "^17.5.1" } else { "npm:^17.5.1" })],
792 dependencies: vec![
793 ("cliui", "^7.0.2"),
794 ("escalade", "^3.1.1"),
795 ("get-caller-file", "^2.0.5"),
796 ("require-directory", "^2.1.1"),
797 ("string-width", "^4.2.3"),
798 ("y18n", "^5.0.5"),
799 ("yargs-parser", "^21.0.0"),
800 ],
801 integrity: if with_bug {
802 "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA=="
803 } else {
804 "00d58a2c052937fa044834313f07910fd0a115dec5ee35919e857eeee3736b21a4eafa8264535800ba8bac312991ce785ecb8a51f4d2cc8c4676d865af1cfbde"
805 },
806 ..Default::default()
807 }
808 );
809 }
810
811 #[test]
812 fn parse_v6_doc_from_file_works() {
813 let content = std::fs::read_to_string("tests/v2/yarn.lock").unwrap();
814 let res = parse(&content).unwrap();
815 assert_v6(&res, false);
816 }
817
818 #[test]
819 fn parse_v6_doc_from_file_without_endline_works() {
820 let content = std::fs::read_to_string("tests/v2_without_endline/yarn.lock").unwrap();
821 let res = parse(&content).unwrap();
822 assert_v6(&res, false);
823 }
824
825 #[test]
826 fn parse_v6_doc_from_memory_works() {
827 fn assert(input: &str, expect: &[Entry]) {
828 let res = parse(input).unwrap();
829 assert_eq!(res.1.entries, expect);
830 }
831
832 assert(
833 r#"# This file is generated by running "yarn install" inside your project.
834# Manual changes might be lost - proceed with caution!
835
836__metadata:
837 version: 6
838 cacheKey: 8
839
840"@babel/helper-plugin-utils@npm:^7.16.7":
841 version: 7.16.7
842 resolution: "@babel/helper-plugin-utils@npm:7.16.7"
843 checksum: d08dd86554a186c2538547cd537552e4029f704994a9201d41d82015c10ed7f58f9036e8d1527c3760f042409163269d308b0b3706589039c5f1884619c6d4ce
844 languageName: node
845 linkType: hard
846
847"@babel/plugin-transform-for-of@npm:^7.12.1":
848 version: 7.16.7
849 resolution: "@babel/plugin-transform-for-of@npm:7.16.7"
850 dependencies:
851 "@babel/helper-plugin-utils": ^7.16.7
852 peerDependencies:
853 "@babel/core": ^7.0.0-0
854 checksum: 35c9264ee4bef814818123d70afe8b2f0a85753a0a9dc7b73f93a71cadc5d7de852f1a3e300a7c69a491705805704611de1e2ccceb5686f7828d6bca2e5a7306
855 languageName: node
856 linkType: hard
857
858"@babel/runtime@npm:^7.12.5":
859 version: 7.17.9
860 resolution: "@babel/runtime@npm:7.17.9"
861 dependencies:
862 regenerator-runtime: ^0.13.4
863 checksum: 4d56bdb82890f386d5a57c40ef985a0ed7f0a78f789377a2d0c3e8826819e0f7f16ba0fe906d9b2241c5f7ca56630ef0653f5bb99f03771f7b87ff8af4bf5fe3
864 languageName: node
865 linkType: hard
866"#,
867 &[
868 Entry {
869 name: "@babel/helper-plugin-utils",
870 version: "7.16.7",
871 resolved: "@babel/helper-plugin-utils@npm:7.16.7",
872 descriptors: vec![("@babel/helper-plugin-utils", "npm:^7.16.7")],
873 integrity: "d08dd86554a186c2538547cd537552e4029f704994a9201d41d82015c10ed7f58f9036e8d1527c3760f042409163269d308b0b3706589039c5f1884619c6d4ce",
874 ..Default::default()
875 },
876 Entry {
877 name: "@babel/plugin-transform-for-of",
878 version: "7.16.7",
879 resolved: "@babel/plugin-transform-for-of@npm:7.16.7",
880 descriptors: vec![("@babel/plugin-transform-for-of", "npm:^7.12.1")],
881 dependencies: vec![("@babel/helper-plugin-utils", "^7.16.7")],
882 peer_dependencies: vec![("@babel/core", "^7.0.0-0")],
883 integrity: "35c9264ee4bef814818123d70afe8b2f0a85753a0a9dc7b73f93a71cadc5d7de852f1a3e300a7c69a491705805704611de1e2ccceb5686f7828d6bca2e5a7306",
884 ..Default::default()
885 },
886 Entry {
887 name: "@babel/runtime",
888 version: "7.17.9",
889 resolved: "@babel/runtime@npm:7.17.9",
890 descriptors: vec![("@babel/runtime", "npm:^7.12.5")],
891 dependencies: vec![("regenerator-runtime", "^0.13.4")],
892 integrity: "4d56bdb82890f386d5a57c40ef985a0ed7f0a78f789377a2d0c3e8826819e0f7f16ba0fe906d9b2241c5f7ca56630ef0653f5bb99f03771f7b87ff8af4bf5fe3",
893 ..Default::default()
894 },
895 ],
896 );
897 }
898
899 #[test]
900 fn parse_v6_doc_from_memory_with_npm_in_dependencies_works() {
901 fn assert(input: &str, expect: &[Entry]) {
902 let res = parse(input).unwrap();
903 assert_eq!(res.1.entries, expect);
904 }
905
906 assert(
907 r#"# This file is generated by running "yarn install" inside your project.
908# Manual changes might be lost - proceed with caution!
909
910__metadata:
911 version: 6
912 cacheKey: 8
913
914"foo@workspace:.":
915 version: 0.0.0-use.local
916 resolution: "foo@workspace:."
917 dependencies:
918 valib-aliased: "npm:valib@1.0.0 || 1.0.1"
919 languageName: unknown
920 linkType: soft
921
922"valib-aliased@npm:valib@1.0.0 || 1.0.1":
923 version: 1.0.0
924 resolution: "valib@npm:1.0.0"
925 checksum: ad4f5a0b5dde5ab5e3cc87050fad4d7096c32797454d8e37c7dadf3455a43a7221a3caaa0ad9e72b8cd96668168e5a25d5f0072e21990f7f80a64b1a4e34e921
926 languageName: node
927 linkType: hard
928"#,
929 &[
930 Entry {
931 name: "foo",
932 version: "0.0.0-use.local",
933 resolved: "foo@workspace:.",
934 integrity: "",
935 descriptors: vec![("foo", "workspace:.")],
936 dependencies: vec![("valib-aliased", "npm:valib@1.0.0 || 1.0.1")],
937 ..Default::default()
938 },
939 Entry {
940 name: "valib-aliased",
941 version: "1.0.0",
942 resolved: "valib@npm:1.0.0",
943 integrity: "ad4f5a0b5dde5ab5e3cc87050fad4d7096c32797454d8e37c7dadf3455a43a7221a3caaa0ad9e72b8cd96668168e5a25d5f0072e21990f7f80a64b1a4e34e921",
944 descriptors: vec![("valib-aliased", "npm:valib@1.0.0 || 1.0.1")],
945 dependencies: vec![],
946 ..Default::default()
947 },
948 ],
949 );
950 }
951
952 #[test]
953 fn entry_works() {
954 #[allow(clippy::needless_pass_by_value)]
955 fn assert(input: &str, expect: Entry) {
956 let res = entry(input).unwrap();
957 assert_eq!(res.1, expect);
958 }
959
960 assert(
961 "\"@babel/code-frame@^7.0.0\":\r\n version \"7.12.13\"\r\n resolved \"https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658\"\r\n integrity sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==\r\n dependencies:\r\n \"@babel/highlight\" \"^7.12.13\"\r\n\r\n",
962 Entry {
963 name: "@babel/code-frame",
964 version: "7.12.13",
965 resolved: "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658",
966 descriptors: vec![("@babel/code-frame", "^7.0.0")],
967 dependencies: vec![("@babel/highlight", "^7.12.13")],
968 integrity: "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==",
969 ..Default::default()
970 },
971 );
972
973 assert(
974 "\"@babel/code-frame@^7.0.0\":\n version \"7.12.13\"\n resolved \"https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658\"\n integrity sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==\n dependencies:\n \"@babel/highlight\" \"^7.12.13\"\n\n",
975 Entry {
976 name: "@babel/code-frame",
977 version: "7.12.13",
978 resolved: "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658",
979 descriptors: vec![("@babel/code-frame", "^7.0.0")],
980 dependencies: vec![("@babel/highlight", "^7.12.13")],
981 integrity: "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==",
982 ..Default::default()
983 },
984 );
985
986 assert(
987 r#""@babel/code-frame@^7.0.0":
988 version "7.12.13"
989 resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658"
990 integrity sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==
991 dependencies:
992 "@babel/highlight" "^7.12.13"
993
994 "#,
995 Entry {
996 name: "@babel/code-frame",
997 version: "7.12.13",
998 resolved: "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658",
999 descriptors: vec![("@babel/code-frame", "^7.0.0")],
1000 dependencies: vec![("@babel/highlight", "^7.12.13")],
1001 integrity: "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==",
1002 ..Default::default()
1003 },
1004 );
1005
1006 assert(
1008 r#""@babel/helper-validator-identifier@^7.12.11":
1009 version "7.12.11"
1010 resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed"
1011 integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==
1012
1013 "#,
1014 Entry {
1015 name: "@babel/helper-validator-identifier",
1016 version: "7.12.11",
1017 resolved: "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed",
1018 descriptors: vec![("@babel/helper-validator-identifier", "^7.12.11")],
1019 integrity: "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==",
1020 ..Default::default()
1021 },
1022 );
1023
1024 assert(
1026 r#""@babel/helper-validator-identifier@^7.12.11":
1027 version "7.12.11"
1028 resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed"
1029 integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==
1030
1031 "#,
1032 Entry {
1033 name: "@babel/helper-validator-identifier",
1034 version: "7.12.11",
1035 resolved: "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed",
1036 descriptors: vec![("@babel/helper-validator-identifier", "^7.12.11")],
1037 integrity: "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==",
1038 ..Default::default()
1039 },
1040 );
1041 }
1042
1043 #[test]
1044 fn parse_entry_works() {
1045 #[allow(clippy::needless_pass_by_value)]
1046 fn assert(input: &str, expect: Entry) {
1047 let res = parse_entry(input).unwrap();
1048 assert_eq!(res.1, expect);
1049 }
1050 assert(
1052 "\"@babel/code-frame@^7.0.0\":\n version \"7.12.13\"\n resolved \"https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658\"\n integrity sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==\n dependencies:\n \"@babel/highlight\" \"^7.12.13\"\n\n",
1053 Entry {
1054 name: "@babel/code-frame",
1055 version: "7.12.13",
1056 resolved: "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658",
1057 descriptors: vec![("@babel/code-frame", "^7.0.0")],
1058 dependencies: vec![("@babel/highlight", "^7.12.13")],
1059 integrity: "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==",
1060 ..Default::default()
1061 },
1062 );
1063
1064 assert(
1066 "\"@babel/code-frame@^7.0.0\":\r\n version \"7.12.13\"\r\n resolved \"https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658\"\r\n integrity sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==\r\n dependencies:\r\n \"@babel/highlight\" \"^7.12.13\"\r\n\r\n",
1067 Entry {
1068 name: "@babel/code-frame",
1069 version: "7.12.13",
1070 resolved: "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658",
1071 descriptors: vec![("@babel/code-frame", "^7.0.0")],
1072 dependencies: vec![("@babel/highlight", "^7.12.13")],
1073 integrity: "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==",
1074 ..Default::default()
1075 },
1076 );
1077
1078 assert(
1080 r#""@babel/code-frame@^7.0.0":
1081 version "7.12.13"
1082 resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658"
1083 integrity sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==
1084 dependencies:
1085 "@babel/highlight" "^7.12.13"
1086
1087 "#,
1088 Entry {
1089 name: "@babel/code-frame",
1090 version: "7.12.13",
1091 resolved: "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658",
1092 descriptors: vec![("@babel/code-frame", "^7.0.0")],
1093 dependencies: vec![("@babel/highlight", "^7.12.13")],
1094 integrity: "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==",
1095 ..Default::default()
1096 },
1097 );
1098 }
1099
1100 #[test]
1101 fn parse_entry_without_endline_works() {
1102 #[allow(clippy::needless_pass_by_value)]
1103 fn assert(input: &str, expect: Entry) {
1104 let res = parse_entry(input).unwrap();
1105 assert_eq!(res.1, expect);
1106 }
1107
1108 assert(
1109 r#""@babel/code-frame@^7.0.0":
1110 version "7.12.13"
1111 resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658"
1112 integrity sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==
1113 dependencies:
1114 "@babel/highlight" "^7.12.13""#,
1115 Entry {
1116 name: "@babel/code-frame",
1117 version: "7.12.13",
1118 resolved: "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658",
1119 descriptors: vec![("@babel/code-frame", "^7.0.0")],
1120 dependencies: vec![("@babel/highlight", "^7.12.13")],
1121 integrity: "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==",
1122 ..Default::default()
1123 },
1124 );
1125 }
1126
1127 #[test]
1128 fn entry_version_works() {
1129 assert_eq!(
1130 entry_version(" version \"1.2.3\"\r\n"),
1131 Ok(("", EntryItem::Version("1.2.3")))
1132 );
1133 assert_eq!(
1134 entry_version(" version \"1.2.3\"\n"),
1135 Ok(("", EntryItem::Version("1.2.3")))
1136 );
1137 assert_eq!(
1138 entry_version(" version \"1.2.3-beta1\"\n"),
1139 Ok(("", EntryItem::Version("1.2.3-beta1")))
1140 );
1141 assert_eq!(
1142 entry_version(" version: 1.2.3\n"),
1143 Ok(("", EntryItem::Version("1.2.3")))
1144 );
1145 assert!(entry_version(" node-version: 1.0.0\n").is_err());
1146
1147 assert_eq!(
1149 entry_version(" version: \"workspace:foo\"\n"),
1150 Ok(("", EntryItem::Version("workspace:foo")))
1151 );
1152 assert_eq!(
1153 entry_version(" version: \"workspace:@bar/baz\"\r\n"),
1154 Ok(("", EntryItem::Version("workspace:@bar/baz")))
1155 );
1156
1157 assert_eq!(
1159 entry_version(" version \"github:settlemint/node-http-proxy\"\r\n"),
1160 Ok(("", EntryItem::Version("github:settlemint/node-http-proxy")))
1161 );
1162 assert_eq!(
1163 entry_version(" version \"github:settlemint/node-http-proxy#master\"\n"),
1164 Ok((
1165 "",
1166 EntryItem::Version("github:settlemint/node-http-proxy#master")
1167 ))
1168 );
1169
1170 assert_eq!(
1172 entry_version(" version \"npm:foo-bar\"\r\n"),
1173 Ok(("", EntryItem::Version("npm:foo-bar")))
1174 );
1175 assert_eq!(
1176 entry_version(" version \"npm:@scope/foo-bar\"\r\n"),
1177 Ok(("", EntryItem::Version("npm:@scope/foo-bar")))
1178 );
1179 }
1180
1181 #[test]
1182 fn entry_descriptors_works() {
1183 #[allow(clippy::needless_pass_by_value)]
1184 fn assert(input: &str, expect: Vec<(&str, &str)>) {
1185 let res = entry_descriptors(input).unwrap();
1186 assert_eq!(res.1, expect);
1187 }
1188
1189 assert(
1190 r#"abab@^1.0.3:
1191 version "1.0.4"
1192 "#,
1193 vec![("abab", "^1.0.3")],
1194 );
1195
1196 assert(
1197 r#""@nodelib/fs.stat@2.0.3":
1198 version "2.0.3"
1199 "#,
1200 vec![("@nodelib/fs.stat", "2.0.3")],
1201 );
1202
1203 assert(
1204 r#"abab@^1.0.3, abab@^1.0.4:
1205 version "1.0.4"
1206 "#,
1207 vec![("abab", "^1.0.3"), ("abab", "^1.0.4")],
1208 );
1209
1210 assert(
1211 r#""@nodelib/fs.stat@2.0.3", "@nodelib/fs.stat@^2.0.2":
1212 version "2.0.3"
1213 "#,
1214 vec![
1215 ("@nodelib/fs.stat", "2.0.3"),
1216 ("@nodelib/fs.stat", "^2.0.2"),
1217 ],
1218 );
1219
1220 assert(
1222 r#""@nodelib/fs.stat@npm:2.0.3, @nodelib/fs.stat@npm:^2.0.2":
1223 version "2.0.3"
1224 "#,
1225 vec![
1226 ("@nodelib/fs.stat", "npm:2.0.3"),
1227 ("@nodelib/fs.stat", "npm:^2.0.2"),
1228 ],
1229 );
1230
1231 assert(
1232 r#"foolib@npm:1.2.3 || ^2.0.0":
1233 version "1.2.3"
1234 "#,
1235 vec![("foolib", "npm:1.2.3 || ^2.0.0")],
1236 );
1237 }
1238
1239 #[test]
1240 fn unknown_line_works() {
1241 let res = unknown_line("foo\nbar").unwrap();
1242 assert_eq!(res, ("bar", EntryItem::Unknown("foo\n")));
1243 }
1244
1245 #[test]
1246 fn integrity_works() {
1247 #[allow(clippy::needless_pass_by_value)]
1248 fn assert(input: &str, expect: EntryItem) {
1249 let res = integrity(input).unwrap();
1250 assert_eq!(res.1, expect);
1251 }
1252
1253 assert(
1254 r#" "integrity" "sha1-jQrELxbqVd69MyyvTEA4s+P139k="
1255 "#,
1256 EntryItem::Integrity("sha1-jQrELxbqVd69MyyvTEA4s+P139k="),
1257 );
1258
1259 assert(
1260 r" integrity sha1-jQrELxbqVd69MyyvTEA4s+P139k=
1261 ",
1262 EntryItem::Integrity("sha1-jQrELxbqVd69MyyvTEA4s+P139k="),
1263 );
1264
1265 assert(
1266 r" checksum: fb47e70bf0001fdeabdc0429d431863e9475e7e43ea5f94ad86503d918423c1543361cc5166d713eaa7029dd7a3d34775af04764bebff99ef413111a5af18c80
1267 ",
1268 EntryItem::Integrity(
1269 "fb47e70bf0001fdeabdc0429d431863e9475e7e43ea5f94ad86503d918423c1543361cc5166d713eaa7029dd7a3d34775af04764bebff99ef413111a5af18c80",
1270 ),
1271 );
1272 }
1273
1274 #[test]
1275 fn parse_dependencies_work() {
1276 #[allow(clippy::needless_pass_by_value)]
1277 fn assert(input: &str, expect: EntryItem) {
1278 let res = parse_dependencies(input).unwrap();
1279 assert_eq!(res.1, expect);
1280 }
1281
1282 assert(
1283 r#" dependencies:
1284 foo "1.0"
1285 "bar" "0.3-alpha1"
1286 "#,
1287 EntryItem::Dependencies(vec![("foo", "1.0"), ("bar", "0.3-alpha1")]),
1288 );
1289
1290 assert(
1291 r#" dependencies:
1292 foo "1.0 || 2.0"
1293 "bar" "0.3-alpha1"
1294 "#,
1295 EntryItem::Dependencies(vec![("foo", "1.0 || 2.0"), ("bar", "0.3-alpha1")]),
1296 );
1297
1298 assert(
1299 r#" dependencies:
1300 foo: 1.0 || 2.0
1301 "bar": "0.3-alpha1"
1302 "#,
1303 EntryItem::Dependencies(vec![("foo", "1.0 || 2.0"), ("bar", "0.3-alpha1")]),
1304 );
1305
1306 assert(
1307 r#" peerDependencies:
1308 foo: 1.0 || 2.0
1309 "bar": "0.3-alpha1"
1310 "#,
1311 EntryItem::PeerDependencies(vec![("foo", "1.0 || 2.0"), ("bar", "0.3-alpha1")]),
1312 );
1313
1314 assert(
1315 r#" optionalDependencies:
1316 foo "1.0"
1317 "bar" "0.3-alpha1"
1318 "#,
1319 EntryItem::OptionalDependencies(vec![("foo", "1.0"), ("bar", "0.3-alpha1")]),
1320 );
1321
1322 assert(
1323 r#" optionalDependencies:
1324 foo "1.0 || 2.0"
1325 "bar" "0.3-alpha1"
1326 "#,
1327 EntryItem::OptionalDependencies(vec![("foo", "1.0 || 2.0"), ("bar", "0.3-alpha1")]),
1328 );
1329
1330 assert(
1331 r#" optionalDependencies:
1332 foo: 1.0 || 2.0
1333 "bar": "0.3-alpha1"
1334 "#,
1335 EntryItem::OptionalDependencies(vec![("foo", "1.0 || 2.0"), ("bar", "0.3-alpha1")]),
1336 );
1337 }
1338
1339 #[test]
1340 fn take_till_the_end_works() {
1341 let k = take_till_line_end("foo\r\nbar").unwrap();
1342 assert_eq!(k.0, "bar");
1343 assert_eq!(k.1, "foo\r\n");
1344 }
1345
1346 #[test]
1347 fn supports_github_version_protocol() {
1348 let content = std::fs::read_to_string("tests/github_version/yarn.lock").unwrap();
1350 let res = parse(&content);
1351 assert!(res.is_ok());
1352
1353 let content = std::fs::read_to_string("tests/github_version/yarn1.lock").unwrap();
1355 let res = parse(&content);
1356 assert!(res.is_ok());
1357
1358 let content = std::fs::read_to_string("tests/github_version/bun.lock").unwrap();
1360 let res = parse(&content);
1361 assert!(res.is_ok());
1362 }
1363
1364 #[test]
1365 fn supports_git_url_descriptor() {
1366 let content = std::fs::read_to_string("tests/v1_git_url/yarn.lock").unwrap();
1367 let res = parse_str(&content).unwrap();
1368
1369 assert_eq!(
1370 res.entries.last().unwrap(),
1371 &Entry {
1372 name: "minimatch",
1373 version: "10.0.1",
1374 resolved: "https://github.com/isaacs/minimatch.git#0569cd3373408f9d701d3aab187b3f43a24a0db7",
1375 integrity: "",
1376 dependencies: vec![("brace-expansion", "^2.0.1")],
1377 descriptors: vec![(
1378 "minimatch",
1379 "https://github.com/isaacs/minimatch.git#v10.0.1"
1380 )],
1381 ..Default::default()
1382 }
1383 );
1384 }
1385
1386 #[test]
1387 fn supports_at_in_version_descriptor() {
1388 let content = std::fs::read_to_string("tests/v1_git_ssh/yarn.lock").unwrap();
1389 let res = parse_str(&content).unwrap();
1390
1391 assert_eq!(
1392 res.entries.last().unwrap(),
1393 &Entry {
1394 name: "node-semver",
1395 version: "7.6.3",
1396 resolved: "ssh://git@github.com/npm/node-semver.git#0a12d6c7debb1dc82d8645c770e77c47bac5e1ea",
1397 integrity: "",
1398 dependencies: vec![],
1399 descriptors: vec![(
1400 "node-semver",
1401 "ssh://git@github.com/npm/node-semver.git#semver:^7.5.0"
1402 )],
1403 ..Default::default()
1404 }
1405 );
1406 }
1407
1408 #[test]
1409 fn supports_dependencies_meta() {
1410 let content = std::fs::read_to_string("tests/v2_deps_meta/yarn.lock").unwrap();
1411 let res = parse_str(&content).unwrap();
1412
1413 assert_eq!(
1414 res.entries[2],
1415 Entry {
1416 name: "jsonfile",
1417 version: "4.0.0",
1418 resolved: "jsonfile@npm:4.0.0",
1419 dependencies: vec![("graceful-fs", "^4.1.6")],
1420 dependencies_meta: vec![(
1421 "graceful-fs",
1422 DepMeta {
1423 optional: Some(true),
1424 }
1425 )],
1426 integrity: "a40b7b64da41c84b0dc7ad753737ba240bb0dc50a94be20ec0b73459707dede69a6f89eb44b4d29e6994ed93ddf8c9b6e57f6b1f09dd707567959880ad6cee7f",
1427 descriptors: vec![("jsonfile", "npm:4.0.0")],
1428 ..Default::default()
1429 }
1430 );
1431
1432 let content = std::fs::read_to_string("tests/v2_deps_meta_quoted/yarn.lock").unwrap();
1433 let res = parse_str(&content).unwrap();
1434 assert_eq!(
1435 res.entries[2],
1436 Entry {
1437 name: "@bufbuild/protoc-gen-es",
1438 version: "1.10.1",
1439 integrity: "ac75c370aeeac43e835e679e7cd8c7f3ed746648ab59fc2538dacf7702a918a7dc8d2d53babe08f43e505c01bdcd5cf4bc0d3d2ef23d50e0cede67c82a742489",
1440 resolved: "@bufbuild/protoc-gen-es@npm:1.10.1",
1441 dependencies: vec![
1442 ("@bufbuild/protobuf", "^1.10.1"),
1443 ("@bufbuild/protoplugin", "1.10.1"),
1444 ],
1445 peer_dependencies: vec![("@bufbuild/protobuf", "1.10.1"),],
1446 peer_dependencies_meta: vec![(
1447 "@bufbuild/protobuf",
1448 DepMeta {
1449 optional: Some(true),
1450 }
1451 )],
1452 descriptors: vec![("@bufbuild/protoc-gen-es", "npm:^1.8")],
1453 ..Default::default()
1454 }
1455 );
1456 }
1457
1458 #[test]
1459 fn supports_peer_dependencies() {
1460 let content = std::fs::read_to_string("tests/peer_dependencies/yarn.lock").unwrap();
1461 let res = parse_str(&content).unwrap();
1462
1463 assert_eq!(
1464 res.entries[4],
1465 Entry {
1466 name: "react-router",
1467 version: "7.2.0",
1468 resolved: "react-router@npm:7.2.0",
1469 descriptors: vec![("react-router", "npm:^7.2.0")],
1470 integrity: "05c79d86639f146aafc64351bb042acd785dbb69c7874ad8e0a3f5f3e70890b1b3ee07d0e18f8cebaffd62bca47e58d0645b07d1cc428a73ba449ce378cbef01",
1471 dependencies: vec![
1472 ("@types/cookie", "^0.6.0"),
1473 ("cookie", "^1.0.1"),
1474 ("set-cookie-parser", "^2.6.0"),
1475 ("turbo-stream", "2.4.0"),
1476 ],
1477 peer_dependencies: vec![("react", ">=18"), ("react-dom", ">=18"),],
1478 peer_dependencies_meta: vec![(
1479 "react-dom",
1480 DepMeta {
1481 optional: Some(true),
1482 ..Default::default()
1483 }
1484 )],
1485 ..Default::default()
1486 }
1487 );
1488 }
1489
1490 #[test]
1491 fn supports_version_url() {
1492 let content = std::fs::read_to_string("tests/bun_version_url/yarn.lock").unwrap();
1494 let res = parse_str(&content).unwrap();
1495
1496 assert_eq!(
1497 res.entries.last().unwrap(),
1498 &Entry {
1499 name: "@a/verboden(name~'!*)",
1500 version: "https://s.lnl.gay/@a/verboden(name~'!*)/-/verboden(name~'!*)-1.0.0.tgz",
1501 resolved: "https://s.lnl.gay/@a/verboden(name~'!*)/-/verboden(name~'!*)-1.0.0.tgz",
1502 integrity: "",
1503 dependencies: vec![],
1504 descriptors: vec![(
1505 "@a/verboden(name~'!*)",
1506 "https://s.lnl.gay/@a/verboden(name~'!*)/-/verboden(name~'!*)-1.0.0.tgz"
1507 ),],
1508 ..Default::default()
1509 }
1510 );
1511 }
1512
1513 #[test]
1514
1515 fn empty_lockfile() {
1516 let content = std::fs::read_to_string("tests/v1_empty/yarn.lock").unwrap();
1517 let res = parse_str(&content).unwrap();
1518 assert_eq!(res.entries, []);
1519 assert_eq!(res.generator, Generator::Yarn);
1520 assert_eq!(res.version, 1);
1521 }
1522
1523 #[test]
1524 fn parses_cache_key() {
1525 let content = std::fs::read_to_string("tests/v2/yarn.lock").unwrap();
1526 let res = parse_str(&content).unwrap();
1527 assert_eq!(res.cache_key, Some("8"));
1528
1529 let content = std::fs::read_to_string("tests/v2_cache_key/yarn.lock").unwrap();
1530 let res = parse_str(&content).unwrap();
1531 assert_eq!(res.cache_key, Some("10c0"));
1532 }
1533}