yarn_lock_parser/
lib.rs

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/// Parser error
20#[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/// A parsed yarn.lock file.
31#[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/// yarn.lock entry.
48/// It only shows the name of the dependency and the version.
49#[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
64/// Accepts the `yarn.lock` content and returns all the entries.
65/// # Errors
66/// - `YarnLockError`
67pub 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        // This shouldn't happen.
95        (true, Some(_)) | (false, None) => unreachable!(),
96    };
97
98    // allow one extra line at the end as per #13
99    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    // 2 lines for Yarn
149    // 3 lines for Bun
150    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            // descriptors is guaranteed to be of length >= 1
254            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        // yarn v1 only
314        Optional,
315        // yarn berry only
316        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),  // indented as much as the parent...
332            space1,       // ... plus extra indentation
333            is_not(": "), // package name
334            one_of(": "),
335            space0,
336            dependency_version,         // version
337            alt((line_ending, space0)), // newline or space
338        )
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
437/**
438 * Simple version, it doesn't consider escaped quotes since in our scenarios
439 * it can't happen.
440 */
441fn 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    // foo@1:
475    // "foo@npm:1.2":
476    // "foo@1.2", "foo@npm:3.4":
477    // "foo@npm:1.2, foo@npm:3.4":
478    // "foo@npm:0.3.x, foo@npm:>= 0.3.2 < 0.4.0":
479
480    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    // "  resolved \"https://registry.yarnpkg.com/yargs/-/yargs-9.0.1.tgz#52acc23feecac34042078ee78c0c007f5085db4c\"\r\n"
508    // "  resolution: \"@babel/code-frame@npm:7.18.6\"\r\n"
509
510    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    // "version \"7.12.13\"\r\n"
533    // "version \"workspace:foobar\"\r\n"
534    // "version \"https://s.lnl.gay/@a/verboden(name~'!*)/-/verboden(name~'!*)-1.0.0.tgz\"\r\n"
535
536    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            // Regular semver
560            b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z'
561            | b'.' | b'-' | b'+'
562            // URL chars, which might appear due to Bun bugs in yarn output.
563            | b'@' | b':' | b'/' | b'#' | b'%'
564            // Chars exempt from ECMA-262 encodeURIComponent.
565            | 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        // SEE: https://github.com/robertohuertasm/yarn-lock-parser/issues/3
681        let content = std::fs::read_to_string("tests/v1_with_npm_bug/yarn.lock").unwrap();
682        let res = parse(&content).unwrap();
683        // using v6 as we generated the lock file with v6 information.
684        // the npm bug convert it back to v1.
685        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        // with final spaces
1006        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        // without final spaces
1024        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        // escaped lines
1049        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        // escaped lines windows
1063        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        // normal
1077        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        // bun workspaces
1145        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        // github:
1155        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        // npm:
1168        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        // yarn >= 2.0 format
1217        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        // yarn > 1
1343        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        // yarn 1
1348        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        // bun
1353        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        // https://github.com/oven-sh/bun/issues/17091
1462        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}