diff options
Diffstat (limited to 'src/file.rs')
| -rw-r--r-- | src/file.rs | 285 |
1 files changed, 285 insertions, 0 deletions
diff --git a/src/file.rs b/src/file.rs new file mode 100644 index 00000000..a9d3c85e --- /dev/null +++ b/src/file.rs @@ -0,0 +1,285 @@ +//! File and package management. + +use std::collections::HashMap; +use std::fmt::{self, Debug, Display, Formatter}; +use std::path::{Path, PathBuf}; +use std::str::FromStr; +use std::sync::RwLock; + +use ecow::{eco_format, EcoString}; +use once_cell::sync::Lazy; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +use crate::diag::{bail, FileError, StrResult}; +use crate::syntax::is_ident; +use crate::util::PathExt; + +/// The global package-path interner. +static INTERNER: Lazy<RwLock<Interner>> = + Lazy::new(|| RwLock::new(Interner { to_id: HashMap::new(), from_id: Vec::new() })); + +/// A package-path interner. +struct Interner { + to_id: HashMap<Pair, FileId>, + from_id: Vec<Pair>, +} + +/// An interned pair of a package specification and a path. +type Pair = &'static (Option<PackageSpec>, PathBuf); + +/// Identifies a file. +/// +/// This type is interned and thus cheap to clone, compare, and hash. +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct FileId(u16); + +impl FileId { + /// Create a new interned file specification. + /// + /// Normalizes the path before interning. + pub fn new(package: Option<PackageSpec>, path: &Path) -> Self { + let pair = (package, path.normalize()); + let mut interner = INTERNER.write().unwrap(); + interner.to_id.get(&pair).copied().unwrap_or_else(|| { + let leaked = Box::leak(Box::new(pair)); + let len = interner.from_id.len(); + if len >= usize::from(u16::MAX) { + panic!("too many file specifications"); + } + let id = FileId(len as u16); + interner.to_id.insert(leaked, id); + interner.from_id.push(leaked); + id + }) + } + + /// Get an id that does not identify any real file. + pub const fn detached() -> Self { + Self(u16::MAX) + } + + /// Whether the id is the detached. + pub const fn is_detached(self) -> bool { + self.0 == Self::detached().0 + } + + /// The package the file resides in, if any. + pub fn package(&self) -> Option<&'static PackageSpec> { + if self.is_detached() { + None + } else { + self.pair().0.as_ref() + } + } + + /// The normalized path to the file (within the package if there's a + /// package). + pub fn path(&self) -> &'static Path { + if self.is_detached() { + Path::new("<detached>") + } else { + &self.pair().1 + } + } + + /// Resolve a file location relative to this file. + pub fn join(self, path: &str) -> StrResult<Self> { + if self.is_detached() { + bail!("cannot access file system from here"); + } + + let package = self.package().cloned(); + let base = self.path(); + Ok(if let Some(parent) = base.parent() { + Self::new(package, &parent.join(path)) + } else { + Self::new(package, Path::new(path)) + }) + } + + /// Construct from a raw number. + pub(crate) const fn from_u16(v: u16) -> Self { + Self(v) + } + + /// Extract the raw underlying number. + pub(crate) const fn as_u16(self) -> u16 { + self.0 + } + + /// Get the static pair. + fn pair(&self) -> Pair { + INTERNER.read().unwrap().from_id[usize::from(self.0)] + } +} + +impl Display for FileId { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let path = self.path().display(); + match self.package() { + Some(package) => write!(f, "{package}/{path}"), + None => write!(f, "{path}"), + } + } +} + +impl Debug for FileId { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + Display::fmt(self, f) + } +} + +/// Identifies a package. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct PackageSpec { + /// The namespace the package lives in. + pub namespace: EcoString, + /// The name of the package within its namespace. + pub name: EcoString, + /// The package's version. + pub version: Version, +} + +impl FromStr for PackageSpec { + type Err = EcoString; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + let mut s = unscanny::Scanner::new(s); + if !s.eat_if('@') { + bail!("package specification must start with '@'"); + } + + let namespace = s.eat_until('/'); + if namespace.is_empty() { + bail!("package specification is missing namespace"); + } else if !is_ident(namespace) { + bail!("`{namespace}` is not a valid package namespace"); + } + + s.eat_if('/'); + + let name = s.eat_until(':'); + if name.is_empty() { + bail!("package specification is missing name"); + } else if !is_ident(name) { + bail!("`{name}` is not a valid package name"); + } + + s.eat_if(':'); + + let version = s.after(); + if version.is_empty() { + bail!("package specification is missing version"); + } + + Ok(Self { + namespace: namespace.into(), + name: name.into(), + version: version.parse()?, + }) + } +} + +impl Display for PackageSpec { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "@{}/{}:{}", self.namespace, self.name, self.version) + } +} + +/// A package's version. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct Version { + /// The package's major version. + pub major: u32, + /// The package's minor version. + pub minor: u32, + /// The package's patch version. + pub patch: u32, +} + +impl FromStr for Version { + type Err = EcoString; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + let mut parts = s.split('.'); + let mut next = |kind| { + let Some(part) = parts.next().filter(|s| !s.is_empty()) else { + bail!("version number is missing {kind} version"); + }; + part.parse::<u32>() + .map_err(|_| eco_format!("`{part}` is not a valid {kind} version")) + }; + + let major = next("major")?; + let minor = next("minor")?; + let patch = next("patch")?; + if let Some(rest) = parts.next() { + bail!("version number has unexpected fourth component: `{rest}`"); + } + + Ok(Self { major, minor, patch }) + } +} + +impl Display for Version { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "{}.{}.{}", self.major, self.minor, self.patch) + } +} + +impl Serialize for Version { + fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> { + s.collect_str(self) + } +} + +impl<'de> Deserialize<'de> for Version { + fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> { + let string = EcoString::deserialize(d)?; + string.parse().map_err(serde::de::Error::custom) + } +} + +/// A parsed package manifest. +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +pub struct PackageManifest { + /// Details about the package itself. + pub package: PackageInfo, +} + +impl PackageManifest { + /// Parse the manifest from raw bytes. + pub fn parse(bytes: &[u8]) -> StrResult<Self> { + let string = std::str::from_utf8(bytes).map_err(FileError::from)?; + toml::from_str(string).map_err(|err| { + eco_format!("package manifest is malformed: {}", err.message()) + }) + } + + /// Ensure that this manifest is indeed for the specified package. + pub fn validate(&self, spec: &PackageSpec) -> StrResult<()> { + if self.package.name != spec.name { + bail!("package manifest contains mismatched name `{}`", self.package.name); + } + + if self.package.version != spec.version { + bail!( + "package manifest contains mismatched version {}", + self.package.version + ); + } + + Ok(()) + } +} + +/// The `package` key in the manifest. +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +pub struct PackageInfo { + /// The name of the package within its namespace. + pub name: EcoString, + /// The package's version. + pub version: Version, + /// The path of the entrypoint into the package. + pub entrypoint: EcoString, +} |
