yarn-lock-parser 0.4.1

yarn.lock parser
Documentation
use nom::{
    branch::alt,
    bytes::complete::{is_not, tag, take, take_till, take_until},
    character::{
        complete::{line_ending, not_line_ending, one_of, space0, space1},
        streaming::multispace0,
    },
    combinator::{eof, fail, opt, recognize},
    error::{context, ParseError, VerboseError},
    multi::{count, many0, many1, many_till, separated_list1},
    sequence::{delimited, tuple},
    AsChar, IResult,
};

use thiserror::Error;

type Res<T, U> = IResult<T, U, VerboseError<T>>;

/// Parser error
#[derive(Debug, Error)]
#[error("yarn.lock error")]
pub enum YarnLockError {
    #[error("Error parsing yarn.lock file")]
    Parser {
        #[from]
        source: nom::Err<VerboseError<String>>,
    },
}

/// yarn.lock entry.
/// It only shows the name of the dependency and the version.
#[derive(Debug, PartialEq, Eq, Default)]
pub struct Entry<'a> {
    pub name: &'a str,
    pub version: &'a str,
    pub integrity: &'a str,
    pub dependencies: Vec<(&'a str, &'a str)>,
    pub descriptors: Vec<(&'a str, &'a str)>,
}

/// Accepts the `yarn.lock` content and returns all the entries.
/// # Errors
/// - `YarnLockError`
pub fn parse_str(content: &str) -> Result<Vec<Entry>, YarnLockError> {
    parse(content).map(|(_, entries)| entries).map_err(|e| {
        e.map(|ve| {
            let errors = ve
                .errors
                .into_iter()
                .map(|v| (v.0.to_string(), v.1))
                .collect();
            VerboseError { errors }
        })
        .into()
    })
}

fn parse(input: &str) -> Res<&str, Vec<Entry>> {
    let (i, _) = yarn_lock_header(input)?;
    let (i, _) = opt(yarn_lock_metadata)(i)?;
    let (i, mut entries) = many0(entry)(i)?;
    let (i, final_entry) = entry_final(i)?;
    entries.push(final_entry);
    Ok((i, entries))
}

fn take_till_line_end(input: &str) -> Res<&str, &str> {
    recognize(tuple((
        alt((take_until("\n"), take_until("\r\n"))),
        take(1usize),
    )))(input)
}

fn take_till_optional_line_end(input: &str) -> Res<&str, &str> {
    recognize(tuple((
        alt((take_until("\n"), take_until("\r\n"), space0)),
        take(1usize),
    )))(input)
}

fn yarn_lock_header(input: &str) -> Res<&str, &str> {
    recognize(tuple((count(take_till_line_end, 2), multispace0)))(input)
}

fn yarn_lock_metadata(input: &str) -> Res<&str, &str> {
    context(
        "metadata",
        recognize(tuple((
            tag("__metadata:"),
            take_till_line_end,
            many_till(take_till_line_end, recognize(tuple((space0, line_ending)))),
            multispace0,
        ))),
    )(input)
}

fn entry_final(input: &str) -> Res<&str, Entry> {
    recognize(many_till(take_till_optional_line_end, eof))(input).map(|(i, capture)| {
        let (_, my_entry) = parse_entry(capture).expect("Error parsing Entry");
        (i, my_entry)
    })
}

fn entry(input: &str) -> Res<&str, Entry> {
    recognize(many_till(
        take_till_line_end,
        recognize(tuple((space0, line_ending))),
    ))(input)
    .map(|(i, capture)| {
        let (_, my_entry) = parse_entry(capture).expect("Error parsing Entry");
        (i, my_entry)
    })
}

#[derive(PartialEq, Debug)]
enum EntryItem<'a> {
    Version(&'a str),
    Dependencies(Vec<(&'a str, &'a str)>),
    Integrity(&'a str),
    Unknown(&'a str),
}

fn unknown_line(input: &str) -> Res<&str, EntryItem> {
    take_till_line_end(input).map(|(i, res)| (i, EntryItem::Unknown(res)))
}

fn integrity(input: &str) -> Res<&str, EntryItem> {
    context(
        "integrity",
        tuple((
            space1,
            opt(tag("\"")),
            alt((tag("checksum"), tag("integrity"))),
            opt(tag("\"")),
            opt(tag(":")),
            space1,
            opt(tag("\"")),
            take_till(|c| c == '"' || c == '\n' || c == '\r'),
        )),
    )(input)
    .map(|(i, (_, _, _, _, _, _, _, integrity))| (i, EntryItem::Integrity(integrity)))
}

fn entry_item(input: &str) -> Res<&str, EntryItem> {
    alt((entry_version, parse_dependencies, integrity, unknown_line))(input)
}

fn parse_entry(input: &str) -> Res<&str, Entry> {
    context("entry", tuple((entry_descriptors, many1(entry_item))))(input).map(
        |(next_input, res)| {
            let (descriptors, entry_items) = res;

            // descriptors is guaranteed to be of length >= 1
            let first_descriptor = descriptors.first().expect("Somehow descriptors is empty");

            let name = first_descriptor.0;

            let mut version = "";
            let mut dependencies = Vec::new();
            let mut integrity = "";

            for ei in entry_items {
                match ei {
                    EntryItem::Version(v) => version = v,
                    EntryItem::Dependencies(d) => dependencies = d,
                    EntryItem::Integrity(c) => integrity = c,
                    EntryItem::Unknown(_) => (),
                }
            }

            assert_ne!(version, "", "Version is empty");

            (
                next_input,
                Entry {
                    name,
                    version,
                    integrity,
                    dependencies,
                    descriptors,
                },
            )
        },
    )
}

fn dependency_version(input: &str) -> Res<&str, &str> {
    alt((double_quoted_text, not_line_ending))(input).map(|(i, version)| {
        (
            i,
            // e.g. 1.2.3
            // e.g. "1.2.3"
            // e.g. "npm:foo@1.0.0 || 1.0.1" # it happens with aliased deps
            version.rsplit_once('@').unwrap_or(("", version)).1,
        )
    })
}

fn parse_dependencies(input: &str) -> Res<&str, EntryItem> {
    let (input, (indent, _, _)) = tuple((space1, tag("dependencies:"), line_ending))(input)?;

    let dependencies_parser = many1(move |i| {
        tuple((
            tag(indent),  // indented as much as the parent...
            space1,       // ... plus extra indentation
            is_not(": "), // package name
            one_of(": "),
            space0,
            dependency_version,         // version
            alt((line_ending, space0)), // newline or space
        ))(i)
        .map(|(i, (_, _, p, _, _, v, _))| (i, (p.trim_matches('"'), v)))
    });
    context("dependencies", dependencies_parser)(input)
        .map(|(i, res)| (i, EntryItem::Dependencies(res)))
}

/**
 * Simple version, it doesn't consider escaped quotes since in our scenarios
 * it can't happen.
 */
fn double_quoted_text(input: &str) -> Res<&str, &str> {
    delimited(tag("\""), take_until("\""), tag("\""))(input)
}

fn entry_single_descriptor<'a>(input: &'a str) -> Res<&str, (&str, &str)> {
    let (i, (_, desc)) = tuple((opt(tag("\"")), is_not(",\"\n")))(input)?;
    let i = i.strip_prefix('"').unwrap_or(i);

    let (_, (name, version)) = context("entry single descriptor", |i: &'a str| {
        let version_start_idx = i.rfind('@').map(|idx| {
            let idx = idx + 1;
            // does it also contains a colon? e.g. foo@workspace:.
            i[idx..].rfind(':').map_or(idx, |new_idx| idx + new_idx + 1)
        });

        #[allow(clippy::manual_strip)]
        let name_end_idx = if i.starts_with('@') {
            i[1..].find('@').map(|idx| idx + 1)
        } else {
            i.find('@')
        };

        if name_end_idx.is_none() || version_start_idx.is_none() {
            return Err(nom::Err::Failure(
                nom::error::VerboseError::from_error_kind(
                    "version format error: @ not found",
                    nom::error::ErrorKind::Fail,
                ),
            ));
        };

        let (name, version) = (
            &i[..name_end_idx.unwrap()],
            &i[version_start_idx.unwrap()..],
        );

        Ok((i, (name, version)))
    })(desc)?;

    Ok((i, (name, version)))
}

fn entry_descriptors<'a>(input: &'a str) -> Res<&str, Vec<(&str, &str)>> {
    // foo@1:
    // "foo@npm:1.2":
    // "foo@1.2", "foo@npm:3.4":
    // "foo@npm:1.2, foo@npm:3.4":
    // "foo@npm:0.3.x, foo@npm:>= 0.3.2 < 0.4.0":

    context(
        "descriptors",
        |input: &'a str| -> Res<&str, Vec<(&str, &str)>> {
            let (input, line) = take_till_line_end(input)?;

            let line = line
                .strip_suffix(":\r\n")
                .or_else(|| line.strip_suffix(":\n"));

            if line.is_none() {
                fail::<_, &str, _>("descriptor does not end with : followed by newline")?;
            }
            let line = line.unwrap();

            let (_, res) =
                separated_list1(tuple((opt(tag("\"")), tag(", "))), entry_single_descriptor)(line)?;

            Ok((input, res))
        },
    )(input)
}

fn entry_version(input: &str) -> Res<&str, EntryItem> {
    // "version \"7.12.13\"\r\n"
    context(
        "version",
        tuple((
            space1,
            opt(tag("\"")),
            tag("version"),
            opt(tag("\"")),
            opt(tag(":")),
            space1,
            opt(tag("\"")),
            is_version,
            opt(tag("\"")),
            line_ending,
        )),
    )(input)
    .map(|(i, (_, _, _, _, _, _, _, version, _, _))| (i, EntryItem::Version(version)))
}

fn is_version<T, E: nom::error::ParseError<T>>(input: T) -> IResult<T, T, E>
where
    T: nom::InputTakeAtPosition,
    <T as nom::InputTakeAtPosition>::Item: AsChar,
{
    input.split_at_position1_complete(
        |item| {
            let c: char = item.as_char();
            !(c == '.' || c == '-' || c.is_alphanum())
        },
        nom::error::ErrorKind::AlphaNumeric,
    )
}

#[cfg(test)]
mod tests {
    use super::*;

    fn assert_v1(res: (&str, Vec<Entry>)) {
        assert_eq!(res.0, "");
        assert_eq!(
            res.1.first().unwrap(),
            &Entry {
                name: "@babel/code-frame",
                version: "7.12.13",
                descriptors: vec![("@babel/code-frame", "^7.0.0")],
                dependencies: vec![("@babel/highlight", "^7.12.13")],
                integrity: "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g=="
            }
        );

        assert_eq!(
            res.1.last().unwrap(),
            &Entry {
                name: "yargs",
                version: "9.0.1",
                descriptors: vec![("yargs", "^9.0.0")],
                dependencies: vec![
                    ("camelcase", "^4.1.0"),
                    ("cliui", "^3.2.0"),
                    ("decamelize", "^1.1.1"),
                    ("get-caller-file", "^1.0.1"),
                    ("os-locale", "^2.0.0"),
                    ("read-pkg-up", "^2.0.0"),
                    ("require-directory", "^2.1.1"),
                    ("require-main-filename", "^1.0.1"),
                    ("set-blocking", "^2.0.0"),
                    ("string-width", "^2.0.0"),
                    ("which-module", "^2.0.0"),
                    ("y18n", "^3.2.1"),
                    ("yargs-parser", "^7.0.0"),
                ],
                integrity: "sha1-UqzCP+7Kw0BCB47njAwAf1CF20w="
            }
        );
    }

    #[test]
    fn parse_windows_server_from_memory_works() {
        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";
        let res = parse(&content).unwrap();
        assert_v1(res);
    }

    #[test]
    fn parse_v1_doc_from_file_works() {
        let content = std::fs::read_to_string("tests/v1/yarn.lock").unwrap();
        let res = parse(&content).unwrap();
        assert_v1(res);
    }

    #[test]
    fn parse_v1_doc_from_file_without_endline_works() {
        let content = std::fs::read_to_string("tests/v1_without_endline/yarn.lock").unwrap();
        let res = parse(&content).unwrap();
        assert_v1(res)
    }

    #[test]
    fn parse_v1_doc_from_file_with_npm_bug_works() {
        // SEE: https://github.com/robertohuertasm/yarn-lock-parser/issues/3
        let content = std::fs::read_to_string("tests/v1_with_npm_bug/yarn.lock").unwrap();
        let res = parse(&content).unwrap();
        // using v6 as we generated the lock file with v6 information.
        // the npm bug convert it back to v1.
        assert_v6(res, true);
    }

    #[test]
    fn parse_v1_doc_from_memory_works_v1() {
        fn assert(input: &str, expect: &[Entry]) {
            let res = parse(input).unwrap();
            assert_eq!(res.1, expect);
        }

        assert(
            r#"# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1


"@babel/code-frame@^7.0.0":
    version "7.12.13"
    resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658"
    integrity sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==
        dependencies:
            "@babel/highlight" "^7.12.13"

"@babel/helper-validator-identifier@^7.12.11":
    version "7.12.11"
    resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed"
    integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==
"#,
            &[
                Entry {
                    name: "@babel/code-frame",
                    version: "7.12.13",
                    descriptors: vec![("@babel/code-frame", "^7.0.0")],
                    dependencies: vec![("@babel/highlight", "^7.12.13")],
                    integrity: "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g=="
                },
                Entry {
                    name: "@babel/helper-validator-identifier",
                    version: "7.12.11",
                    descriptors: vec![("@babel/helper-validator-identifier", "^7.12.11")],
                    integrity: "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==",
                    ..Default::default()
                },
            ],
        );
    }

    fn assert_v6(res: (&str, Vec<Entry>), with_bug: bool) {
        assert_eq!(res.0, "");
        assert_eq!(
            res.1.first().unwrap(),
            &Entry {
                name: "@babel/code-frame",
                version: "7.18.6",
                descriptors: vec![("@babel/code-frame", "^7.18.6")],
                dependencies: vec![("@babel/highlight", "^7.18.6")],
                integrity: if with_bug {
                    "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q=="
                } else {
                    "195e2be3172d7684bf95cff69ae3b7a15a9841ea9d27d3c843662d50cdd7d6470fd9c8e64be84d031117e4a4083486effba39f9aef6bbb2c89f7f21bcfba33ba"
                }
            }
        );

        assert_eq!(
            res.1.last().unwrap(),
            &Entry {
                name: "yargs",
                version: "17.5.1",
                descriptors: vec![("yargs", "^17.5.1")],
                dependencies: vec![
                    ("cliui", "^7.0.2"),
                    ("escalade", "^3.1.1"),
                    ("get-caller-file", "^2.0.5"),
                    ("require-directory", "^2.1.1"),
                    ("string-width", "^4.2.3"),
                    ("y18n", "^5.0.5"),
                    ("yargs-parser", "^21.0.0"),
                ],
                integrity: if with_bug {
                    "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA=="
                } else {
                    "00d58a2c052937fa044834313f07910fd0a115dec5ee35919e857eeee3736b21a4eafa8264535800ba8bac312991ce785ecb8a51f4d2cc8c4676d865af1cfbde"
                }
            }
        );
    }

    #[test]
    fn parse_v6_doc_from_file_works() {
        let content = std::fs::read_to_string("tests/v2/yarn.lock").unwrap();
        let res = parse(&content).unwrap();
        assert_v6(res, false)
    }

    #[test]
    fn parse_v6_doc_from_file_without_endline_works() {
        let content = std::fs::read_to_string("tests/v2_without_endline/yarn.lock").unwrap();
        let res = parse(&content).unwrap();
        assert_v6(res, false)
    }

    #[test]
    fn parse_v6_doc_from_memory_works() {
        fn assert(input: &str, expect: &[Entry]) {
            let res = parse(input).unwrap();
            assert_eq!(res.1, expect);
        }

        assert(
            r#"# This file is generated by running "yarn install" inside your project.
# Manual changes might be lost - proceed with caution!

__metadata:
  version: 6
  cacheKey: 8

"@babel/helper-plugin-utils@npm:^7.16.7":
  version: 7.16.7
  resolution: "@babel/helper-plugin-utils@npm:7.16.7"
  checksum: d08dd86554a186c2538547cd537552e4029f704994a9201d41d82015c10ed7f58f9036e8d1527c3760f042409163269d308b0b3706589039c5f1884619c6d4ce
  languageName: node
  linkType: hard

"@babel/plugin-transform-for-of@npm:^7.12.1":
  version: 7.16.7
  resolution: "@babel/plugin-transform-for-of@npm:7.16.7"
  dependencies:
    "@babel/helper-plugin-utils": ^7.16.7
  peerDependencies:
    "@babel/core": ^7.0.0-0
  checksum: 35c9264ee4bef814818123d70afe8b2f0a85753a0a9dc7b73f93a71cadc5d7de852f1a3e300a7c69a491705805704611de1e2ccceb5686f7828d6bca2e5a7306
  languageName: node
  linkType: hard

"@babel/runtime@npm:^7.12.5":
  version: 7.17.9
  resolution: "@babel/runtime@npm:7.17.9"
  dependencies:
    regenerator-runtime: ^0.13.4
  checksum: 4d56bdb82890f386d5a57c40ef985a0ed7f0a78f789377a2d0c3e8826819e0f7f16ba0fe906d9b2241c5f7ca56630ef0653f5bb99f03771f7b87ff8af4bf5fe3
  languageName: node
  linkType: hard
"#,
            &[
                Entry {
                    name: "@babel/helper-plugin-utils",
                    version: "7.16.7",
                    descriptors: vec![("@babel/helper-plugin-utils", "^7.16.7")],
                    integrity: "d08dd86554a186c2538547cd537552e4029f704994a9201d41d82015c10ed7f58f9036e8d1527c3760f042409163269d308b0b3706589039c5f1884619c6d4ce",
                    ..Default::default()
                },
                Entry {
                    name: "@babel/plugin-transform-for-of",
                    version: "7.16.7",
                    descriptors: vec![("@babel/plugin-transform-for-of", "^7.12.1")],
                    dependencies: vec![("@babel/helper-plugin-utils", "^7.16.7")],
                    integrity: "35c9264ee4bef814818123d70afe8b2f0a85753a0a9dc7b73f93a71cadc5d7de852f1a3e300a7c69a491705805704611de1e2ccceb5686f7828d6bca2e5a7306",
                },
                Entry {
                    name: "@babel/runtime",
                    version: "7.17.9",
                    descriptors: vec![("@babel/runtime", "^7.12.5")],
                    dependencies: vec![("regenerator-runtime", "^0.13.4")],
                    integrity: "4d56bdb82890f386d5a57c40ef985a0ed7f0a78f789377a2d0c3e8826819e0f7f16ba0fe906d9b2241c5f7ca56630ef0653f5bb99f03771f7b87ff8af4bf5fe3"
                },
            ],
        );
    }

    #[test]
    fn parse_v6_doc_from_memory_with_npm_in_dependencies_works() {
        fn assert(input: &str, expect: &[Entry]) {
            let res = parse(input).unwrap();
            assert_eq!(res.1, expect);
        }

        assert(
            r#"# This file is generated by running "yarn install" inside your project.
# Manual changes might be lost - proceed with caution!

__metadata:
  version: 6
  cacheKey: 8

"foo@workspace:.":
  version: 0.0.0-use.local
  resolution: "foo@workspace:."
  dependencies:
    valib-aliased: "npm:valib@1.0.0 || 1.0.1"
  languageName: unknown
  linkType: soft

"valib-aliased@npm:valib@1.0.0 || 1.0.1":
  version: 1.0.0
  resolution: "valib@npm:1.0.0"
  checksum: ad4f5a0b5dde5ab5e3cc87050fad4d7096c32797454d8e37c7dadf3455a43a7221a3caaa0ad9e72b8cd96668168e5a25d5f0072e21990f7f80a64b1a4e34e921
  languageName: node
  linkType: hard
"#,
            &[
                Entry {
                    name: "foo",
                    version: "0.0.0-use.local",
                    integrity: "",
                    descriptors: vec![("foo", ".")],
                    dependencies: vec![("valib-aliased", "1.0.0 || 1.0.1")],
                },
                Entry {
                    name: "valib-aliased",
                    version: "1.0.0",
                    integrity: "ad4f5a0b5dde5ab5e3cc87050fad4d7096c32797454d8e37c7dadf3455a43a7221a3caaa0ad9e72b8cd96668168e5a25d5f0072e21990f7f80a64b1a4e34e921",
                    descriptors: vec![("valib-aliased", "1.0.0 || 1.0.1")],
                    dependencies: vec![],
                },
            ],
        );
    }

    #[test]
    fn entry_works() {
        fn assert(input: &str, expect: Entry) {
            let res = entry(input).unwrap();
            assert_eq!(res.1, expect);
        }

        assert(
            "\"@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",
            Entry {
                name: "@babel/code-frame",
                version: "7.12.13",
                descriptors: vec![("@babel/code-frame", "^7.0.0")],
                dependencies: vec![("@babel/highlight", "^7.12.13")],
                integrity: "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==",
                ..Default::default()
            },
        );

        assert(
                    "\"@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",
                    Entry {
                        name: "@babel/code-frame",
                        version: "7.12.13",
                        descriptors: vec![("@babel/code-frame", "^7.0.0")],
                        dependencies: vec![("@babel/highlight", "^7.12.13")],
                        integrity: "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==",
                        ..Default::default()
                    },
                );

        assert(
                    r#""@babel/code-frame@^7.0.0":
            version "7.12.13"
            resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658"
            integrity sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==
            dependencies:
                "@babel/highlight" "^7.12.13"

         "#,
                    Entry {
                        name: "@babel/code-frame",
                        version: "7.12.13",
                        descriptors: vec![("@babel/code-frame", "^7.0.0")],
                        dependencies: vec![("@babel/highlight", "^7.12.13")],
                        integrity: "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g=="
                    },
                );

        // with final spaces
        assert(
                    r#""@babel/helper-validator-identifier@^7.12.11":
            version "7.12.11"
            resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed"
            integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==

         "#,
                    Entry {
                        name: "@babel/helper-validator-identifier",
                        version: "7.12.11",
                        descriptors: vec![("@babel/helper-validator-identifier", "^7.12.11")],
                        integrity: "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==",
                        ..Default::default()
                    },
                );

        // without final spaces
        assert(
                    r#""@babel/helper-validator-identifier@^7.12.11":
            version "7.12.11"
            resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed"
            integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==

        "#,
                    Entry {
                        name: "@babel/helper-validator-identifier",
                        version: "7.12.11",
                        descriptors: vec![("@babel/helper-validator-identifier", "^7.12.11")],
                        integrity: "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==",
                        ..Default::default()
                    },
                );
    }

    #[test]
    fn parse_entry_works() {
        fn assert(input: &str, expect: Entry) {
            let res = parse_entry(input).unwrap();
            assert_eq!(res.1, expect);
        }
        // escaped lines
        assert(
            "\"@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",
            Entry {
                name: "@babel/code-frame",
                version: "7.12.13",
                descriptors: vec![("@babel/code-frame", "^7.0.0")],
                dependencies: vec![("@babel/highlight", "^7.12.13")],
                integrity: "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==",
                ..Default::default()
            },
        );

        // escaped lines windows
        assert(
            "\"@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",
            Entry {
                name: "@babel/code-frame",
                version: "7.12.13",
                descriptors: vec![("@babel/code-frame", "^7.0.0")],
                dependencies: vec![("@babel/highlight", "^7.12.13")],
                integrity: "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==",
                ..Default::default()
            },
        );

        // normal
        assert(
                    r#""@babel/code-frame@^7.0.0":
            version "7.12.13"
            resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658"
            integrity sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==
            dependencies:
                "@babel/highlight" "^7.12.13"

        "#,
                    Entry {
                        name: "@babel/code-frame",
                        version: "7.12.13",
                        descriptors: vec![("@babel/code-frame", "^7.0.0")],
                        dependencies: vec![("@babel/highlight", "^7.12.13")],
                        integrity: "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==",
                        ..Default::default()
                    },
                );
    }

    #[test]
    fn parse_entry_without_endline_works() {
        fn assert(input: &str, expect: Entry) {
            let res = parse_entry(input).unwrap();
            assert_eq!(res.1, expect);
        }

        assert(
            r#""@babel/code-frame@^7.0.0":
    version "7.12.13"
    resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658"
    integrity sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==
    dependencies:
        "@babel/highlight" "^7.12.13""#,
            Entry {
                name: "@babel/code-frame",
                version: "7.12.13",
                descriptors: vec![("@babel/code-frame", "^7.0.0")],
                dependencies: vec![("@babel/highlight", "^7.12.13")],
                integrity: "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==",
                ..Default::default()
            },
        );
    }

    #[test]
    fn entry_version_works() {
        assert_eq!(
            entry_version(" version \"1.2.3\"\r\n"),
            Ok(("", EntryItem::Version("1.2.3")))
        );
        assert_eq!(
            entry_version("  version \"1.2.3\"\n"),
            Ok(("", EntryItem::Version("1.2.3")))
        );
        assert_eq!(
            entry_version("  version \"1.2.3-beta1\"\n"),
            Ok(("", EntryItem::Version("1.2.3-beta1")))
        );
        assert_eq!(
            entry_version("  version: 1.2.3\n"),
            Ok(("", EntryItem::Version("1.2.3")))
        );
        assert!(entry_version("    node-version: 1.0.0\n").is_err());
    }

    #[test]
    fn entry_descriptors_works() {
        fn assert(input: &str, expect: Vec<(&str, &str)>) {
            let res = entry_descriptors(input).unwrap();
            assert_eq!(res.1, expect);
        }

        assert(
            r#"abab@^1.0.3:
            version "1.0.4"
        "#,
            vec![("abab", "^1.0.3")],
        );

        assert(
            r#""@nodelib/fs.stat@2.0.3":
            version "2.0.3"
        "#,
            vec![("@nodelib/fs.stat", "2.0.3")],
        );

        assert(
            r#"abab@^1.0.3, abab@^1.0.4:
            version "1.0.4"
        "#,
            vec![("abab", "^1.0.3"), ("abab", "^1.0.4")],
        );

        assert(
            r#""@nodelib/fs.stat@2.0.3", "@nodelib/fs.stat@^2.0.2":
            version "2.0.3"
        "#,
            vec![
                ("@nodelib/fs.stat", "2.0.3"),
                ("@nodelib/fs.stat", "^2.0.2"),
            ],
        );

        // yarn >= 2.0 format
        assert(
            r#""@nodelib/fs.stat@npm:2.0.3, @nodelib/fs.stat@npm:^2.0.2":
            version "2.0.3"
        "#,
            vec![
                ("@nodelib/fs.stat", "2.0.3"),
                ("@nodelib/fs.stat", "^2.0.2"),
            ],
        );

        assert(
            r#"foolib@npm:1.2.3 || ^2.0.0":
            version "1.2.3"
        "#,
            vec![("foolib", "1.2.3 || ^2.0.0")],
        );
    }

    #[test]
    fn unknown_line_works() {
        let res = unknown_line("foo\nbar").unwrap();
        assert_eq!(res, ("bar", EntryItem::Unknown("foo\n")));
    }

    #[test]
    fn integrity_works() {
        fn assert(input: &str, expect: EntryItem) {
            let res = integrity(input).unwrap();
            assert_eq!(res.1, expect);
        }

        assert(
            r#" "integrity" "sha1-jQrELxbqVd69MyyvTEA4s+P139k="
        "#,
            EntryItem::Integrity("sha1-jQrELxbqVd69MyyvTEA4s+P139k="),
        );

        assert(
            r#" integrity sha1-jQrELxbqVd69MyyvTEA4s+P139k=
        "#,
            EntryItem::Integrity("sha1-jQrELxbqVd69MyyvTEA4s+P139k="),
        );

        assert(
            r#" checksum: fb47e70bf0001fdeabdc0429d431863e9475e7e43ea5f94ad86503d918423c1543361cc5166d713eaa7029dd7a3d34775af04764bebff99ef413111a5af18c80
        "#,
            EntryItem::Integrity("fb47e70bf0001fdeabdc0429d431863e9475e7e43ea5f94ad86503d918423c1543361cc5166d713eaa7029dd7a3d34775af04764bebff99ef413111a5af18c80"),
        );
    }

    #[test]
    fn parse_dependencies_work() {
        fn assert(input: &str, expect: EntryItem) {
            let res = parse_dependencies(input).unwrap();
            assert_eq!(res.1, expect);
        }

        assert(
            r#"            dependencies:
                foo "1.0"
                "bar" "0.3-alpha1"
        "#,
            EntryItem::Dependencies(vec![("foo", "1.0"), ("bar", "0.3-alpha1")]),
        );

        assert(
            r#"            dependencies:
                foo "1.0 || 2.0"
                "bar" "0.3-alpha1"
        "#,
            EntryItem::Dependencies(vec![("foo", "1.0 || 2.0"), ("bar", "0.3-alpha1")]),
        );

        assert(
            r#"            dependencies:
                foo: 1.0 || 2.0
                "bar": "0.3-alpha1"
        "#,
            EntryItem::Dependencies(vec![("foo", "1.0 || 2.0"), ("bar", "0.3-alpha1")]),
        );
    }

    #[test]
    fn take_till_the_end_works() {
        let k = take_till_line_end("foo\r\nbar").unwrap();
        assert_eq!(k.0, "bar");
        assert_eq!(k.1, "foo\r\n");
    }
}