use std::str::FromStr;
use serde::{Deserialize, Deserializer, Serialize};
use typst::syntax::package::PackageSpec;
pub const MANIFEST_PATH: &str = "typst-pack.toml";
pub const FORMAT_VERSION: u32 = 1;
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub(crate) struct PackManifest {
format_version: u32,
project: ProjectManifest,
#[serde(default, skip_serializing_if = "PackagesManifest::is_empty")]
packages: PackagesManifest,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
fonts: Vec<FontManifest>,
#[serde(default, skip_serializing_if = "Option::is_none")]
metadata: Option<PackMetadata>,
}
#[derive(Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
struct Version1Manifest {
format_version: u32,
project: ProjectManifest,
#[serde(default)]
packages: Version1PackagesManifest,
#[serde(default)]
fonts: Vec<FontManifest>,
#[serde(default)]
metadata: Option<PackMetadata>,
}
impl TryFrom<Version1Manifest> for PackManifest {
type Error = PackManifestError;
fn try_from(manifest: Version1Manifest) -> Result<Self, Self::Error> {
Ok(Self {
format_version: manifest.format_version,
project: manifest.project,
packages: manifest.packages.try_into()?,
fonts: manifest.fonts,
metadata: manifest.metadata,
})
}
}
impl<'de> Deserialize<'de> for PackManifest {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = toml::Value::deserialize(deserializer)?;
parse_manifest_value(value).map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub(crate) struct ProjectManifest {
entrypoint: String,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub(crate) struct PackagesManifest {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
vendored: Vec<PackageManifest>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
unvendored: Vec<PackageManifest>,
}
#[derive(Default, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
struct Version1PackagesManifest {
#[serde(default)]
vendored: Vec<PackageManifest>,
#[serde(default)]
unvendored: Vec<PackageManifest>,
}
impl TryFrom<Version1PackagesManifest> for PackagesManifest {
type Error = PackManifestError;
fn try_from(packages: Version1PackagesManifest) -> Result<Self, Self::Error> {
Ok(Self {
vendored: packages.vendored,
unvendored: packages.unvendored,
})
}
}
impl<'de> Deserialize<'de> for PackagesManifest {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
Version1PackagesManifest::deserialize(deserializer)?
.try_into()
.map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub(crate) struct PackageManifest {
spec: String,
tree_digest: String,
tree_identity_kind: String,
tree_identity_schema: String,
tree_identity_algorithm: String,
file_count: u64,
byte_length: u64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub(crate) struct FontManifest {
path: String,
#[serde(default, skip_serializing_if = "is_zero")]
index: u32,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
families: Vec<String>,
#[serde(default, skip_serializing_if = "is_false")]
external: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
container_digest: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
container_identity_kind: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
container_identity_schema: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
container_identity_algorithm: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
container_length: Option<u64>,
}
fn is_zero(index: &u32) -> bool {
*index == 0
}
fn is_false(value: &bool) -> bool {
!*value
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct PackMetadata {
#[serde(default, skip_serializing_if = "Option::is_none")]
name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
description: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
authors: Vec<String>,
}
impl ProjectManifest {
pub fn entrypoint(&self) -> &str {
&self.entrypoint
}
}
impl PackagesManifest {
pub fn vendored(&self) -> &[PackageManifest] {
&self.vendored
}
pub fn unvendored(&self) -> &[PackageManifest] {
&self.unvendored
}
fn is_empty(&self) -> bool {
self.vendored.is_empty() && self.unvendored.is_empty()
}
}
impl PackageManifest {
#[cfg(test)]
pub(crate) fn new(
spec: PackageSpec,
tree_digest: String,
file_count: u64,
byte_length: u64,
) -> Self {
Self {
spec: spec.to_string(),
tree_digest,
tree_identity_kind: "complete-package-tree".to_owned(),
tree_identity_schema: "typst-pack-complete-package-tree-v1".to_owned(),
tree_identity_algorithm: "typst-hash128-0.15".to_owned(),
file_count,
byte_length,
}
}
pub(crate) fn spec(&self) -> Result<PackageSpec, InvalidPackageSpec> {
PackageSpec::from_str(&self.spec).map_err(|error| InvalidPackageSpec {
spec: self.spec.clone(),
message: error.to_string(),
})
}
pub fn tree_digest(&self) -> &str {
&self.tree_digest
}
pub fn tree_identity_kind(&self) -> &str {
&self.tree_identity_kind
}
pub fn tree_identity_schema(&self) -> &str {
&self.tree_identity_schema
}
pub fn tree_identity_algorithm(&self) -> &str {
&self.tree_identity_algorithm
}
pub fn file_count(&self) -> u64 {
self.file_count
}
pub fn byte_length(&self) -> u64 {
self.byte_length
}
}
impl std::fmt::Display for PackageManifest {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(&self.spec)
}
}
impl FontManifest {
#[cfg(test)]
pub(crate) fn with_identity_fields(
container_digest: Option<String>,
container_identity_kind: Option<String>,
container_identity_schema: Option<String>,
container_identity_algorithm: Option<String>,
) -> Self {
Self {
path: "fonts/test.ttf".to_owned(),
index: 0,
families: Vec::new(),
external: false,
container_digest,
container_identity_kind,
container_identity_schema,
container_identity_algorithm,
container_length: None,
}
}
pub fn path(&self) -> &str {
&self.path
}
pub fn index(&self) -> u32 {
self.index
}
#[cfg(test)]
pub(crate) fn families(&self) -> &[String] {
&self.families
}
pub fn is_external(&self) -> bool {
self.external
}
pub(crate) fn container_digest(&self) -> Option<&str> {
self.container_digest.as_deref()
}
pub(crate) fn container_length(&self) -> Option<u64> {
self.container_length
}
pub(crate) fn container_identity_kind(&self) -> Option<&str> {
self.container_identity_kind.as_deref()
}
pub(crate) fn container_identity_schema(&self) -> Option<&str> {
self.container_identity_schema.as_deref()
}
pub(crate) fn container_identity_algorithm(&self) -> Option<&str> {
self.container_identity_algorithm.as_deref()
}
}
impl PackMetadata {
pub fn new() -> Self {
Self::default()
}
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn with_author(mut self, author: impl Into<String>) -> Self {
self.authors.push(author.into());
self
}
pub fn name(&self) -> Option<&str> {
self.name.as_deref()
}
pub fn description(&self) -> Option<&str> {
self.description.as_deref()
}
pub fn authors(&self) -> &[String] {
&self.authors
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum PackManifestError {
#[error("failed to parse manifest: {0}")]
Parse(#[from] toml::de::Error),
#[error("missing or invalid `format-version`")]
InvalidFormatVersion,
#[error("unsupported pack format version {0} (this reader supports version {FORMAT_VERSION})")]
UnsupportedVersion(u32),
#[error("the {MANIFEST_PATH} manifest is not valid UTF-8: {0}")]
NotUtf8(#[source] std::str::Utf8Error),
}
#[derive(Debug)]
pub(crate) struct InvalidPackageSpec {
pub(crate) spec: String,
pub(crate) message: String,
}
impl PackManifest {
#[cfg(test)]
pub(crate) fn new(
entrypoint: String,
vendored_packages: Vec<PackageManifest>,
unvendored_packages: Vec<PackageManifest>,
fonts: Vec<FontManifest>,
metadata: Option<PackMetadata>,
) -> Self {
Self {
format_version: FORMAT_VERSION,
project: ProjectManifest { entrypoint },
packages: PackagesManifest {
vendored: vendored_packages,
unvendored: unvendored_packages,
},
fonts,
metadata,
}
}
#[cfg(test)]
pub(crate) fn format_version(&self) -> u32 {
self.format_version
}
pub fn project(&self) -> &ProjectManifest {
&self.project
}
pub fn packages(&self) -> &PackagesManifest {
&self.packages
}
pub fn fonts(&self) -> &[FontManifest] {
&self.fonts
}
pub fn metadata(&self) -> Option<&PackMetadata> {
self.metadata.as_ref()
}
pub fn from_toml(text: &str) -> Result<Self, PackManifestError> {
Self::from_toml_value(toml::from_str(text)?)
}
pub(crate) fn from_toml_value(value: toml::Value) -> Result<Self, PackManifestError> {
parse_manifest_value(value)
}
#[cfg(test)]
pub fn to_toml(&self) -> String {
toml::to_string_pretty(self).expect("manifest is always serializable")
}
fn validate(&self) -> Result<(), PackManifestError> {
if self.format_version != FORMAT_VERSION {
return Err(PackManifestError::UnsupportedVersion(self.format_version));
}
Ok(())
}
}
fn parse_manifest_value(value: toml::Value) -> Result<PackManifest, PackManifestError> {
let version = value
.get("format-version")
.and_then(toml::Value::as_integer)
.ok_or(PackManifestError::InvalidFormatVersion)?;
let version = u32::try_from(version).map_err(|_| PackManifestError::InvalidFormatVersion)?;
if version != FORMAT_VERSION {
return Err(PackManifestError::UnsupportedVersion(version));
}
let wire: Version1Manifest = value.try_into()?;
let manifest = PackManifest::try_from(wire)?;
manifest.validate()?;
Ok(manifest)
}