berry 0.7.2

A simple yarn berry lockfile parser
Documentation
//! # Berry
//!
//! A fast parser for Yarn Berry (v2+) lockfiles.
//!
//! ```
//! let src = concat!(
//!   "# This file is generated by running \"yarn install\" inside your project.\n",
//!   "# Manual changes might be lost - proceed with caution!\n\n",
//!   "__metadata:\n  version: 8\n  cacheKey: 10\n\n",
//!   "\"left-pad@npm:^1.3.0\":\n  version: 1.3.0\n  resolution: \"left-pad@npm:1.3.0\"\n",
//!   "  dependencies:\n    \"@scope/dep\": \"npm:^2.0.0\"\n  languageName: node\n  linkType: hard\n",
//! );
//! let lockfile = berry::parse(src)?;
//! assert_eq!(lockfile.metadata().version, "8");
//! assert!(lockfile.remaining().is_empty());
//! for entry in &lockfile {
//!   assert_eq!(entry.descriptors().first().unwrap().to_string(), "left-pad@npm:^1.3.0");
//!   assert_eq!(entry.version(), Some("1.3.0"));
//!   let dep = entry.dependencies().get("@scope/dep").unwrap();
//!   assert_eq!(dep.ident().scope(), Some("@scope"));
//!   assert_eq!(dep.range(), "npm:^2.0.0");
//! }
//! # Ok::<(), berry::Error>(())
//! ```
//!
//! The parse produces span tables (`u32` offsets into the text) instead of
//! owned strings or maps: a constant number of allocations, independent of the
//! number of entries, and none when [`Buffers`] are reused. Every accessor is
//! a borrowed, `Copy` view.
//!
//! Inputs: [`parse`] (`&str`), [`parse_bytes`] (`&[u8]`, validated as UTF-8)
//! and [`parse_file`] (a path; memory-mapped or read, see its docs for the
//! concurrent-modification contract of mapped files: Yarn rewrites
//! `yarn.lock` in place), each with a `_with`
//! variant that reuses [`Buffers`].
//!
//! Inputs of 2.5 MB and more are parsed in parallel on the current rayon
//! pool; the result is always identical to a sequential parse.
//! `RAYON_NUM_THREADS=1` (or a one-thread `ThreadPool::install`) makes every
//! parse sequential. A parse called from inside a rayon pool whose workers are
//! all busy with long tasks waits for them; latency-sensitive callers should
//! call it from outside the pool.
//!
//! ----
//! This project is not affiliated with Yarn or the Yarn team.
#![deny(clippy::all)]

mod error;
mod file;
pub mod ident;
mod lockfile;
mod pipeline;
mod scan;
mod table;
mod utf8;
mod view;

pub use error::{Error, ErrorKind};
pub use file::FileError;
pub use ident::{Descriptor, Ident, Locator, Protocol, Range};
pub use lockfile::{Buffers, Entries, Lockfile, Metadata};
pub use view::{
  BinIter, Bins, Dependencies, DependenciesMeta, DependencyIter, DependencyLookup, DependencyMeta,
  DependencyMetaIter, DescriptorIter, Descriptors, EntryView, LastWins, LinkType,
  PeerDependenciesMeta, PeerDependencyMeta, PeerDependencyMetaIter,
};

use lockfile::Source;
use pipeline::Plan;

/// Parse a lockfile.
///
/// Errors only for a missing/invalid header or `__metadata` block, an unknown
/// package property, an invalid `linkType`, or an input of 4 GiB or more;
/// any other text that is not an entry ends the parse (see
/// [`Lockfile::remaining`]).
pub fn parse(src: &str) -> Result<Lockfile<'_>, Error> {
  parse_with(src, &mut Buffers::new())
}

/// [`parse`] reusing the storage in `buffers` (taken out of it; get it back
/// with [`Lockfile::into_buffers`]). On error the storage stays in `buffers`.
pub fn parse_with<'a>(src: &'a str, buffers: &mut Buffers) -> Result<Lockfile<'a>, Error> {
  Lockfile::parse(Source::Borrowed(src), buffers, Plan::Auto)
}

/// [`parse`] for input given as bytes, e.g. a file read with `std::fs::read`.
///
/// The bytes are validated as UTF-8 first (in parallel for large inputs,
/// like the parse), then parsed. Invalid UTF-8 is
/// [`ErrorKind::InvalidUtf8`] at the length of the longest valid prefix.
pub fn parse_bytes(src: &[u8]) -> Result<Lockfile<'_>, Error> {
  parse_bytes_with(src, &mut Buffers::new())
}

/// [`parse_bytes`] reusing the storage in `buffers` (see [`parse_with`]).
pub fn parse_bytes_with<'a>(src: &'a [u8], buffers: &mut Buffers) -> Result<Lockfile<'a>, Error> {
  if src.len() > scan::MAX_INPUT_LEN {
    return Err(Error::new(ErrorKind::InputTooLarge, 0));
  }
  parse_with(utf8::validate(src)?, buffers)
}

/// Read (or memory-map) and parse a lockfile file.
///
/// The returned [`Lockfile`] owns the text, so it is `'static`: it can
/// outlive `path` and be sent to another thread. Regular files of 256 KiB
/// or more are memory-mapped read-only and validated as UTF-8 in parallel;
/// smaller files and anything that is not a regular file (pipes, devices) are
/// read with `read(2)`. Errors carry the path and, for parse errors, the
/// line and column (`path:line:column: message`).
///
/// # Concurrent modification
///
/// A mapped file that is rewritten or truncated while the parse runs or while
/// the `Lockfile` is alive changes under the parsed views: they may then see
/// bytes that are not valid UTF-8 (undefined behavior), and reads past a new
/// end of file raise `SIGBUS`. Yarn itself writes `yarn.lock` in place
/// (`fs.writeFile`: `O_TRUNC` + write on the same inode), so this is what a
/// `yarn install` or `yarn add` that changes the lockfile does. Callers must
/// not run Yarn (or anything else that writes the file) on the project from
/// the start of `parse_file` until the `Lockfile` is dropped; drop it before
/// running Yarn. If that cannot be guaranteed, read the file into memory
/// yourself (`std::fs::read`) and parse that private copy with [`parse_bytes`].
pub fn parse_file(path: impl AsRef<std::path::Path>) -> Result<Lockfile<'static>, FileError> {
  parse_file_with(path, &mut Buffers::new())
}

/// [`parse_file`] reusing the storage in `buffers`: the span tables and, for
/// files that are read rather than mapped, the read buffer.
pub fn parse_file_with(
  path: impl AsRef<std::path::Path>,
  buffers: &mut Buffers,
) -> Result<Lockfile<'static>, FileError> {
  file::parse_file_with(path.as_ref(), buffers)
}

/// Test hook (not part of the API): validate `src` as UTF-8 in pieces of
/// about `piece` bytes (0: one piece). Must agree with `std::str::from_utf8`.
#[doc(hidden)]
pub fn __validate_utf8(src: &[u8], piece: usize) -> Result<&str, Error> {
  utf8::validate_in(src, piece)
}

/// Test hook (not part of the API): parse with the body cut into chunks of
/// about `chunk_target` bytes, snapped to entry boundaries. The result must
/// be identical to [`parse`].
#[doc(hidden)]
pub fn __parse_chunked(src: &str, chunk_target: usize) -> Result<Lockfile<'_>, Error> {
  Lockfile::parse(
    Source::Borrowed(src),
    &mut Buffers::new(),
    Plan::Target(chunk_target),
  )
}

#[doc(hidden)]
pub use pipeline::PARALLEL_MIN as __PARALLEL_MIN;
/// Test hook (not part of the API): parse with chunks starting at exactly
/// these byte offsets (moved forward to a char boundary; offsets inside the
/// preamble, out of order or past the end are ignored). The result must be
/// identical to [`parse`].
#[doc(hidden)]
pub fn __parse_split<'a>(
  src: &'a str,
  starts: &[usize],
  buffers: &mut Buffers,
) -> Result<Lockfile<'a>, Error> {
  Lockfile::parse(Source::Borrowed(src), buffers, Plan::Starts(starts))
}