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 let (input, indent_top) = space1(input)?;
364 enum MetaKind {
365 Deps,
366 Peers,
367 }
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) = take_until(":")(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 fn assert(input: &str, expect: Entry) {
955 let res = entry(input).unwrap();
956 assert_eq!(res.1, expect);
957 }
958
959 assert(
960 "\"@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",
961 Entry {
962 name: "@babel/code-frame",
963 version: "7.12.13",
964 resolved: "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658",
965 descriptors: vec![("@babel/code-frame", "^7.0.0")],
966 dependencies: vec![("@babel/highlight", "^7.12.13")],
967 integrity: "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==",
968 ..Default::default()
969 },
970 );
971
972 assert(
973 "\"@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",
974 Entry {
975 name: "@babel/code-frame",
976 version: "7.12.13",
977 resolved: "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658",
978 descriptors: vec![("@babel/code-frame", "^7.0.0")],
979 dependencies: vec![("@babel/highlight", "^7.12.13")],
980 integrity: "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==",
981 ..Default::default()
982 },
983 );
984
985 assert(
986 r#""@babel/code-frame@^7.0.0":
987 version "7.12.13"
988 resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658"
989 integrity sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==
990 dependencies:
991 "@babel/highlight" "^7.12.13"
992
993 "#,
994 Entry {
995 name: "@babel/code-frame",
996 version: "7.12.13",
997 resolved: "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658",
998 descriptors: vec![("@babel/code-frame", "^7.0.0")],
999 dependencies: vec![("@babel/highlight", "^7.12.13")],
1000 integrity: "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==",
1001 ..Default::default()
1002 },
1003 );
1004
1005 assert(
1007 r#""@babel/helper-validator-identifier@^7.12.11":
1008 version "7.12.11"
1009 resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed"
1010 integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==
1011
1012 "#,
1013 Entry {
1014 name: "@babel/helper-validator-identifier",
1015 version: "7.12.11",
1016 resolved: "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed",
1017 descriptors: vec![("@babel/helper-validator-identifier", "^7.12.11")],
1018 integrity: "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==",
1019 ..Default::default()
1020 },
1021 );
1022
1023 assert(
1025 r#""@babel/helper-validator-identifier@^7.12.11":
1026 version "7.12.11"
1027 resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed"
1028 integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==
1029
1030 "#,
1031 Entry {
1032 name: "@babel/helper-validator-identifier",
1033 version: "7.12.11",
1034 resolved: "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed",
1035 descriptors: vec![("@babel/helper-validator-identifier", "^7.12.11")],
1036 integrity: "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==",
1037 ..Default::default()
1038 },
1039 );
1040 }
1041
1042 #[test]
1043 fn parse_entry_works() {
1044 fn assert(input: &str, expect: Entry) {
1045 let res = parse_entry(input).unwrap();
1046 assert_eq!(res.1, expect);
1047 }
1048 assert(
1050 "\"@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",
1051 Entry {
1052 name: "@babel/code-frame",
1053 version: "7.12.13",
1054 resolved: "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658",
1055 descriptors: vec![("@babel/code-frame", "^7.0.0")],
1056 dependencies: vec![("@babel/highlight", "^7.12.13")],
1057 integrity: "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==",
1058 ..Default::default()
1059 },
1060 );
1061
1062 assert(
1064 "\"@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",
1065 Entry {
1066 name: "@babel/code-frame",
1067 version: "7.12.13",
1068 resolved: "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658",
1069 descriptors: vec![("@babel/code-frame", "^7.0.0")],
1070 dependencies: vec![("@babel/highlight", "^7.12.13")],
1071 integrity: "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==",
1072 ..Default::default()
1073 },
1074 );
1075
1076 assert(
1078 r#""@babel/code-frame@^7.0.0":
1079 version "7.12.13"
1080 resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658"
1081 integrity sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==
1082 dependencies:
1083 "@babel/highlight" "^7.12.13"
1084
1085 "#,
1086 Entry {
1087 name: "@babel/code-frame",
1088 version: "7.12.13",
1089 resolved: "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658",
1090 descriptors: vec![("@babel/code-frame", "^7.0.0")],
1091 dependencies: vec![("@babel/highlight", "^7.12.13")],
1092 integrity: "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==",
1093 ..Default::default()
1094 },
1095 );
1096 }
1097
1098 #[test]
1099 fn parse_entry_without_endline_works() {
1100 fn assert(input: &str, expect: Entry) {
1101 let res = parse_entry(input).unwrap();
1102 assert_eq!(res.1, expect);
1103 }
1104
1105 assert(
1106 r#""@babel/code-frame@^7.0.0":
1107 version "7.12.13"
1108 resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658"
1109 integrity sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==
1110 dependencies:
1111 "@babel/highlight" "^7.12.13""#,
1112 Entry {
1113 name: "@babel/code-frame",
1114 version: "7.12.13",
1115 resolved: "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658",
1116 descriptors: vec![("@babel/code-frame", "^7.0.0")],
1117 dependencies: vec![("@babel/highlight", "^7.12.13")],
1118 integrity: "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==",
1119 ..Default::default()
1120 },
1121 );
1122 }
1123
1124 #[test]
1125 fn entry_version_works() {
1126 assert_eq!(
1127 entry_version(" version \"1.2.3\"\r\n"),
1128 Ok(("", EntryItem::Version("1.2.3")))
1129 );
1130 assert_eq!(
1131 entry_version(" version \"1.2.3\"\n"),
1132 Ok(("", EntryItem::Version("1.2.3")))
1133 );
1134 assert_eq!(
1135 entry_version(" version \"1.2.3-beta1\"\n"),
1136 Ok(("", EntryItem::Version("1.2.3-beta1")))
1137 );
1138 assert_eq!(
1139 entry_version(" version: 1.2.3\n"),
1140 Ok(("", EntryItem::Version("1.2.3")))
1141 );
1142 assert!(entry_version(" node-version: 1.0.0\n").is_err());
1143
1144 assert_eq!(
1146 entry_version(" version: \"workspace:foo\"\n"),
1147 Ok(("", EntryItem::Version("workspace:foo")))
1148 );
1149 assert_eq!(
1150 entry_version(" version: \"workspace:@bar/baz\"\r\n"),
1151 Ok(("", EntryItem::Version("workspace:@bar/baz")))
1152 );
1153
1154 assert_eq!(
1156 entry_version(" version \"github:settlemint/node-http-proxy\"\r\n"),
1157 Ok(("", EntryItem::Version("github:settlemint/node-http-proxy")))
1158 );
1159 assert_eq!(
1160 entry_version(" version \"github:settlemint/node-http-proxy#master\"\n"),
1161 Ok((
1162 "",
1163 EntryItem::Version("github:settlemint/node-http-proxy#master")
1164 ))
1165 );
1166
1167 assert_eq!(
1169 entry_version(" version \"npm:foo-bar\"\r\n"),
1170 Ok(("", EntryItem::Version("npm:foo-bar")))
1171 );
1172 assert_eq!(
1173 entry_version(" version \"npm:@scope/foo-bar\"\r\n"),
1174 Ok(("", EntryItem::Version("npm:@scope/foo-bar")))
1175 );
1176 }
1177
1178 #[test]
1179 fn entry_descriptors_works() {
1180 fn assert(input: &str, expect: Vec<(&str, &str)>) {
1181 let res = entry_descriptors(input).unwrap();
1182 assert_eq!(res.1, expect);
1183 }
1184
1185 assert(
1186 r#"abab@^1.0.3:
1187 version "1.0.4"
1188 "#,
1189 vec![("abab", "^1.0.3")],
1190 );
1191
1192 assert(
1193 r#""@nodelib/fs.stat@2.0.3":
1194 version "2.0.3"
1195 "#,
1196 vec![("@nodelib/fs.stat", "2.0.3")],
1197 );
1198
1199 assert(
1200 r#"abab@^1.0.3, abab@^1.0.4:
1201 version "1.0.4"
1202 "#,
1203 vec![("abab", "^1.0.3"), ("abab", "^1.0.4")],
1204 );
1205
1206 assert(
1207 r#""@nodelib/fs.stat@2.0.3", "@nodelib/fs.stat@^2.0.2":
1208 version "2.0.3"
1209 "#,
1210 vec![
1211 ("@nodelib/fs.stat", "2.0.3"),
1212 ("@nodelib/fs.stat", "^2.0.2"),
1213 ],
1214 );
1215
1216 assert(
1218 r#""@nodelib/fs.stat@npm:2.0.3, @nodelib/fs.stat@npm:^2.0.2":
1219 version "2.0.3"
1220 "#,
1221 vec![
1222 ("@nodelib/fs.stat", "npm:2.0.3"),
1223 ("@nodelib/fs.stat", "npm:^2.0.2"),
1224 ],
1225 );
1226
1227 assert(
1228 r#"foolib@npm:1.2.3 || ^2.0.0":
1229 version "1.2.3"
1230 "#,
1231 vec![("foolib", "npm:1.2.3 || ^2.0.0")],
1232 );
1233 }
1234
1235 #[test]
1236 fn unknown_line_works() {
1237 let res = unknown_line("foo\nbar").unwrap();
1238 assert_eq!(res, ("bar", EntryItem::Unknown("foo\n")));
1239 }
1240
1241 #[test]
1242 fn integrity_works() {
1243 fn assert(input: &str, expect: EntryItem) {
1244 let res = integrity(input).unwrap();
1245 assert_eq!(res.1, expect);
1246 }
1247
1248 assert(
1249 r#" "integrity" "sha1-jQrELxbqVd69MyyvTEA4s+P139k="
1250 "#,
1251 EntryItem::Integrity("sha1-jQrELxbqVd69MyyvTEA4s+P139k="),
1252 );
1253
1254 assert(
1255 r#" integrity sha1-jQrELxbqVd69MyyvTEA4s+P139k=
1256 "#,
1257 EntryItem::Integrity("sha1-jQrELxbqVd69MyyvTEA4s+P139k="),
1258 );
1259
1260 assert(
1261 r#" checksum: fb47e70bf0001fdeabdc0429d431863e9475e7e43ea5f94ad86503d918423c1543361cc5166d713eaa7029dd7a3d34775af04764bebff99ef413111a5af18c80
1262 "#,
1263 EntryItem::Integrity(
1264 "fb47e70bf0001fdeabdc0429d431863e9475e7e43ea5f94ad86503d918423c1543361cc5166d713eaa7029dd7a3d34775af04764bebff99ef413111a5af18c80",
1265 ),
1266 );
1267 }
1268
1269 #[test]
1270 fn parse_dependencies_work() {
1271 fn assert(input: &str, expect: EntryItem) {
1272 let res = parse_dependencies(input).unwrap();
1273 assert_eq!(res.1, expect);
1274 }
1275
1276 assert(
1277 r#" dependencies:
1278 foo "1.0"
1279 "bar" "0.3-alpha1"
1280 "#,
1281 EntryItem::Dependencies(vec![("foo", "1.0"), ("bar", "0.3-alpha1")]),
1282 );
1283
1284 assert(
1285 r#" dependencies:
1286 foo "1.0 || 2.0"
1287 "bar" "0.3-alpha1"
1288 "#,
1289 EntryItem::Dependencies(vec![("foo", "1.0 || 2.0"), ("bar", "0.3-alpha1")]),
1290 );
1291
1292 assert(
1293 r#" dependencies:
1294 foo: 1.0 || 2.0
1295 "bar": "0.3-alpha1"
1296 "#,
1297 EntryItem::Dependencies(vec![("foo", "1.0 || 2.0"), ("bar", "0.3-alpha1")]),
1298 );
1299
1300 assert(
1301 r#" peerDependencies:
1302 foo: 1.0 || 2.0
1303 "bar": "0.3-alpha1"
1304 "#,
1305 EntryItem::PeerDependencies(vec![("foo", "1.0 || 2.0"), ("bar", "0.3-alpha1")]),
1306 );
1307
1308 assert(
1309 r#" optionalDependencies:
1310 foo "1.0"
1311 "bar" "0.3-alpha1"
1312 "#,
1313 EntryItem::OptionalDependencies(vec![("foo", "1.0"), ("bar", "0.3-alpha1")]),
1314 );
1315
1316 assert(
1317 r#" optionalDependencies:
1318 foo "1.0 || 2.0"
1319 "bar" "0.3-alpha1"
1320 "#,
1321 EntryItem::OptionalDependencies(vec![("foo", "1.0 || 2.0"), ("bar", "0.3-alpha1")]),
1322 );
1323
1324 assert(
1325 r#" optionalDependencies:
1326 foo: 1.0 || 2.0
1327 "bar": "0.3-alpha1"
1328 "#,
1329 EntryItem::OptionalDependencies(vec![("foo", "1.0 || 2.0"), ("bar", "0.3-alpha1")]),
1330 );
1331 }
1332
1333 #[test]
1334 fn take_till_the_end_works() {
1335 let k = take_till_line_end("foo\r\nbar").unwrap();
1336 assert_eq!(k.0, "bar");
1337 assert_eq!(k.1, "foo\r\n");
1338 }
1339
1340 #[test]
1341 fn supports_github_version_protocol() {
1342 let content = std::fs::read_to_string("tests/github_version/yarn.lock").unwrap();
1344 let res = parse(&content);
1345 assert!(!res.is_err());
1346
1347 let content = std::fs::read_to_string("tests/github_version/yarn1.lock").unwrap();
1349 let res = parse(&content);
1350 assert!(!res.is_err());
1351
1352 let content = std::fs::read_to_string("tests/github_version/bun.lock").unwrap();
1354 let res = parse(&content);
1355 assert!(!res.is_err());
1356 }
1357
1358 #[test]
1359 fn supports_git_url_descriptor() {
1360 let content = std::fs::read_to_string("tests/v1_git_url/yarn.lock").unwrap();
1361 let res = parse_str(&content).unwrap();
1362
1363 assert_eq!(
1364 res.entries.last().unwrap(),
1365 &Entry {
1366 name: "minimatch",
1367 version: "10.0.1",
1368 resolved: "https://github.com/isaacs/minimatch.git#0569cd3373408f9d701d3aab187b3f43a24a0db7",
1369 integrity: "",
1370 dependencies: vec![("brace-expansion", "^2.0.1")],
1371 descriptors: vec![(
1372 "minimatch",
1373 "https://github.com/isaacs/minimatch.git#v10.0.1"
1374 )],
1375 ..Default::default()
1376 }
1377 );
1378 }
1379
1380 #[test]
1381 fn supports_at_in_version_descriptor() {
1382 let content = std::fs::read_to_string("tests/v1_git_ssh/yarn.lock").unwrap();
1383 let res = parse_str(&content).unwrap();
1384
1385 assert_eq!(
1386 res.entries.last().unwrap(),
1387 &Entry {
1388 name: "node-semver",
1389 version: "7.6.3",
1390 resolved: "ssh://git@github.com/npm/node-semver.git#0a12d6c7debb1dc82d8645c770e77c47bac5e1ea",
1391 integrity: "",
1392 dependencies: vec![],
1393 descriptors: vec![(
1394 "node-semver",
1395 "ssh://git@github.com/npm/node-semver.git#semver:^7.5.0"
1396 )],
1397 ..Default::default()
1398 }
1399 );
1400 }
1401
1402 #[test]
1403 fn supports_dependencies_meta() {
1404 let content = std::fs::read_to_string("tests/v2_deps_meta/yarn.lock").unwrap();
1405 let res = parse_str(&content).unwrap();
1406
1407 assert_eq!(
1408 res.entries[2],
1409 Entry {
1410 name: "jsonfile",
1411 version: "4.0.0",
1412 resolved: "jsonfile@npm:4.0.0",
1413 dependencies: vec![("graceful-fs", "^4.1.6")],
1414 dependencies_meta: vec![(
1415 "graceful-fs",
1416 DepMeta {
1417 optional: Some(true),
1418 }
1419 )],
1420 integrity: "a40b7b64da41c84b0dc7ad753737ba240bb0dc50a94be20ec0b73459707dede69a6f89eb44b4d29e6994ed93ddf8c9b6e57f6b1f09dd707567959880ad6cee7f",
1421 descriptors: vec![("jsonfile", "npm:4.0.0")],
1422 ..Default::default()
1423 }
1424 );
1425 }
1426
1427 #[test]
1428 fn supports_peer_dependencies() {
1429 let content = std::fs::read_to_string("tests/peer_dependencies/yarn.lock").unwrap();
1430 let res = parse_str(&content).unwrap();
1431
1432 assert_eq!(
1433 res.entries[4],
1434 Entry {
1435 name: "react-router",
1436 version: "7.2.0",
1437 resolved: "react-router@npm:7.2.0",
1438 descriptors: vec![("react-router", "npm:^7.2.0")],
1439 integrity: "05c79d86639f146aafc64351bb042acd785dbb69c7874ad8e0a3f5f3e70890b1b3ee07d0e18f8cebaffd62bca47e58d0645b07d1cc428a73ba449ce378cbef01",
1440 dependencies: vec![
1441 ("@types/cookie", "^0.6.0"),
1442 ("cookie", "^1.0.1"),
1443 ("set-cookie-parser", "^2.6.0"),
1444 ("turbo-stream", "2.4.0"),
1445 ],
1446 peer_dependencies: vec![("react", ">=18"), ("react-dom", ">=18"),],
1447 peer_dependencies_meta: vec![(
1448 "react-dom",
1449 DepMeta {
1450 optional: Some(true),
1451 ..Default::default()
1452 }
1453 )],
1454 ..Default::default()
1455 }
1456 );
1457 }
1458
1459 #[test]
1460 fn supports_version_url() {
1461 let content = std::fs::read_to_string("tests/bun_version_url/yarn.lock").unwrap();
1463 let res = parse_str(&content).unwrap();
1464
1465 assert_eq!(
1466 res.entries.last().unwrap(),
1467 &Entry {
1468 name: "@a/verboden(name~'!*)",
1469 version: "https://s.lnl.gay/@a/verboden(name~'!*)/-/verboden(name~'!*)-1.0.0.tgz",
1470 resolved: "https://s.lnl.gay/@a/verboden(name~'!*)/-/verboden(name~'!*)-1.0.0.tgz",
1471 integrity: "",
1472 dependencies: vec![],
1473 descriptors: vec![(
1474 "@a/verboden(name~'!*)",
1475 "https://s.lnl.gay/@a/verboden(name~'!*)/-/verboden(name~'!*)-1.0.0.tgz"
1476 ),],
1477 ..Default::default()
1478 }
1479 )
1480 }
1481
1482 #[test]
1483
1484 fn empty_lockfile() {
1485 let content = std::fs::read_to_string("tests/v1_empty/yarn.lock").unwrap();
1486 let res = parse_str(&content).unwrap();
1487 assert_eq!(res.entries, []);
1488 assert_eq!(res.generator, Generator::Yarn);
1489 assert_eq!(res.version, 1);
1490 }
1491
1492 #[test]
1493 fn parses_cache_key() {
1494 let content = std::fs::read_to_string("tests/v2/yarn.lock").unwrap();
1495 let res = parse_str(&content).unwrap();
1496 assert_eq!(res.cache_key, Some("8"));
1497
1498 let content = std::fs::read_to_string("tests/v2_cache_key/yarn.lock").unwrap();
1499 let res = parse_str(&content).unwrap();
1500 assert_eq!(res.cache_key, Some("10c0"));
1501 }
1502}