pesde/src/manifest/mod.rs
daimond113 7043f3a499
refactor: impose additional limits on aliases
Aliases didn't have a maximum length before, and
they could've been made with reserved Windows file
names which could've caused issues. This commit
imposes a limit of 24 characters on aliases and
forbids the reserved file names.
2025-04-30 22:05:28 +02:00

339 lines
9.1 KiB
Rust

use crate::{
engine::{runtime::RuntimeKind, EngineKind},
manifest::{
overrides::{OverrideKey, OverrideSpecifier},
target::Target,
},
names::PackageName,
ser_display_deser_fromstr,
source::specifiers::DependencySpecifiers,
};
use relative_path::RelativePathBuf;
use semver::{Version, VersionReq};
use serde::{Deserialize, Serialize};
use std::{
collections::{BTreeMap, HashMap},
fmt::Display,
hash::Hash,
str::FromStr,
};
use tracing::instrument;
/// Overrides
pub mod overrides;
/// Targets
pub mod target;
/// A package manifest
#[derive(Serialize, Deserialize, Debug, Clone)]
#[cfg_attr(test, derive(schemars::JsonSchema))]
pub struct Manifest {
/// The name of the package
pub name: PackageName,
/// The version of the package
pub version: Version,
/// The description of the package
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
/// The license of the package
#[serde(default, skip_serializing_if = "Option::is_none")]
pub license: Option<String>,
/// The authors of the package
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub authors: Vec<String>,
/// The repository of the package
#[serde(default, skip_serializing_if = "Option::is_none")]
pub repository: Option<url::Url>,
/// The target of the package
pub target: Target,
/// Whether the package is private
#[serde(default)]
pub private: bool,
/// The scripts of the package
#[serde(default, skip_serializing)]
pub scripts: BTreeMap<String, Script>,
/// The indices to use for the package
#[serde(
default,
skip_serializing,
deserialize_with = "crate::util::deserialize_gix_url_map"
)]
#[cfg_attr(test, schemars(with = "BTreeMap<String, url::Url>"))]
pub indices: BTreeMap<String, gix::Url>,
/// The indices to use for the package's wally dependencies
#[cfg(feature = "wally-compat")]
#[serde(
default,
skip_serializing,
deserialize_with = "crate::util::deserialize_gix_url_map"
)]
#[cfg_attr(test, schemars(with = "BTreeMap<String, url::Url>"))]
pub wally_indices: BTreeMap<String, gix::Url>,
/// The overrides this package has
#[serde(default, skip_serializing)]
pub overrides: BTreeMap<OverrideKey, OverrideSpecifier>,
/// The files to include in the package
#[serde(default)]
pub includes: Vec<String>,
/// The patches to apply to packages
#[cfg(feature = "patches")]
#[serde(default, skip_serializing)]
#[cfg_attr(
test,
schemars(
with = "BTreeMap<crate::names::PackageNames, BTreeMap<crate::source::ids::VersionId, std::path::PathBuf>>"
)
)]
pub patches: BTreeMap<
crate::names::PackageNames,
BTreeMap<crate::source::ids::VersionId, RelativePathBuf>,
>,
/// A list of globs pointing to workspace members' directories
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub workspace_members: Vec<String>,
/// The Roblox place of this project
#[serde(default, skip_serializing)]
pub place: BTreeMap<target::RobloxPlaceKind, String>,
/// The engines this package supports
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
#[cfg_attr(test, schemars(with = "BTreeMap<EngineKind, String>"))]
pub engines: BTreeMap<EngineKind, VersionReq>,
/// The standard dependencies of the package
#[serde(
default,
skip_serializing_if = "BTreeMap::is_empty",
deserialize_with = "crate::util::deserialize_no_dup_keys"
)]
pub dependencies: BTreeMap<Alias, DependencySpecifiers>,
/// The peer dependencies of the package
#[serde(
default,
skip_serializing_if = "BTreeMap::is_empty",
deserialize_with = "crate::util::deserialize_no_dup_keys"
)]
pub peer_dependencies: BTreeMap<Alias, DependencySpecifiers>,
/// The dev dependencies of the package
#[serde(
default,
skip_serializing_if = "BTreeMap::is_empty",
deserialize_with = "crate::util::deserialize_no_dup_keys"
)]
pub dev_dependencies: BTreeMap<Alias, DependencySpecifiers>,
/// The user-defined fields of the package
#[cfg_attr(test, schemars(skip))]
#[serde(flatten)]
pub user_defined_fields: HashMap<String, toml::Value>,
}
/// An alias of a dependency
/// Equality checks (Ord, PartialOrd, PartialEq, Eq, Hash) are case-insensitive
#[derive(Debug, Clone)]
pub struct Alias(String);
ser_display_deser_fromstr!(Alias);
impl Ord for Alias {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.0.to_lowercase().cmp(&other.0.to_lowercase())
}
}
impl PartialOrd for Alias {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for Alias {
fn eq(&self, other: &Self) -> bool {
self.0.to_lowercase() == other.0.to_lowercase()
}
}
impl Eq for Alias {}
impl Hash for Alias {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.0.to_lowercase().hash(state);
}
}
impl Display for Alias {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.pad(&self.0)
}
}
impl FromStr for Alias {
type Err = errors::AliasFromStr;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.is_empty() {
return Err(errors::AliasFromStr::Empty);
}
if s.len() > 24 {
return Err(errors::AliasFromStr::TooLong(s.to_string()));
}
if [
"con", "prn", "aux", "nul", "com1", "com2", "com3", "com4", "com5", "com6", "com7",
"com8", "com9", "com¹", "com²", "com³", "lpt1", "lpt2", "lpt3", "lpt4", "lpt5", "lpt6",
"lpt7", "lpt8", "lpt9", "lpt¹", "lpt²", "lpt³",
]
.contains(&s.to_ascii_lowercase().as_str())
{
return Err(errors::AliasFromStr::Reserved(s.to_string()));
}
if !s
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
{
return Err(errors::AliasFromStr::InvalidCharacters(s.to_string()));
}
if EngineKind::from_str(s).is_ok() {
return Err(errors::AliasFromStr::EngineName(s.to_string()));
}
Ok(Self(s.to_string()))
}
}
#[cfg(test)]
impl schemars::JsonSchema for Alias {
fn schema_name() -> std::borrow::Cow<'static, str> {
"Alias".into()
}
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({
"type": "string",
"pattern": r"^[a-zA-Z0-9_-]+$",
})
}
}
impl Alias {
/// Get the alias as a string
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
/// A script
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(untagged)]
#[cfg_attr(test, derive(schemars::JsonSchema))]
pub enum Script {
/// A path only script
#[cfg_attr(test, schemars(with = "std::path::PathBuf"))]
Path(RelativePathBuf),
/// A script which specifies both its path and its runtime
RuntimePath {
/// The runtime to execute this script with
runtime: RuntimeKind,
/// The path of the script to run
#[cfg_attr(test, schemars(with = "std::path::PathBuf"))]
path: RelativePathBuf,
},
}
/// A dependency type
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[serde(rename_all = "snake_case")]
pub enum DependencyType {
/// A standard dependency
Standard,
/// A peer dependency
Peer,
/// A dev dependency
Dev,
}
impl DependencyType {
/// All possible dependency types
pub const VARIANTS: &'static [DependencyType] = &[
DependencyType::Standard,
DependencyType::Peer,
DependencyType::Dev,
];
}
impl Manifest {
/// Get all dependencies from the manifest
#[instrument(skip(self), ret(level = "trace"), level = "debug")]
pub fn all_dependencies(
&self,
) -> Result<BTreeMap<Alias, (DependencySpecifiers, DependencyType)>, errors::AllDependenciesError>
{
let mut all_deps = BTreeMap::new();
for (deps, ty) in [
(&self.dependencies, DependencyType::Standard),
(&self.peer_dependencies, DependencyType::Peer),
(&self.dev_dependencies, DependencyType::Dev),
] {
for (alias, spec) in deps {
if all_deps.insert(alias.clone(), (spec.clone(), ty)).is_some() {
return Err(errors::AllDependenciesError::AliasConflict(alias.clone()));
}
}
}
Ok(all_deps)
}
}
/// Errors that can occur when interacting with manifests
pub mod errors {
use crate::manifest::Alias;
use thiserror::Error;
/// Errors that can occur when parsing an alias from a string
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum AliasFromStr {
/// The alias is empty
#[error("the alias is empty")]
Empty,
/// The alias has more than 24 characters
#[error("alias `{0}` has more than 24 characters")]
TooLong(String),
/// The alias is a reserved file name
#[error("alias `{0}` is a reserved file name")]
Reserved(String),
/// The alias contains characters outside a-z, A-Z, 0-9, -, and _
#[error("alias `{0}` contains characters outside a-z, A-Z, 0-9, -, and _")]
InvalidCharacters(String),
/// The alias is an engine name
#[error("alias `{0}` is an engine name")]
EngineName(String),
}
/// Errors that can occur when trying to get all dependencies from a manifest
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum AllDependenciesError {
/// Another specifier is already using the alias
#[error("another specifier is already using the alias {0}")]
AliasConflict(Alias),
}
}
#[cfg(test)]
mod tests {
#[test]
pub fn generate_schema() {
let schema = schemars::schema_for!(super::Manifest);
let schema = serde_json::to_string_pretty(&schema).unwrap();
std::fs::write("manifest.schema.json", schema).unwrap();
}
}