Skip to main content

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