Skip to main content

kcode_k1_web_package/
source.rs

1use crate::{
2    AuthorityId, DependencySelector, PackageError, WebId, fail, validate_logical_name,
3    validate_source_path, validate_stable,
4};
5use kcode_k1_transaction_id::TxId;
6use semver::Version;
7use serde::Deserialize;
8use std::str::FromStr;
9
10#[derive(Deserialize)]
11#[serde(deny_unknown_fields)]
12struct Manifest {
13    name: String,
14    version: String,
15    entry: String,
16    tests: String,
17    dependencies: Vec<ManifestDependency>,
18}
19
20#[derive(Deserialize)]
21#[serde(deny_unknown_fields)]
22struct ManifestDependency {
23    authority: String,
24    name: String,
25    selector: String,
26}
27
28#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
29pub struct SourceFile(String, Vec<u8>);
30
31impl SourceFile {
32    pub fn new(path: impl Into<String>, bytes: Vec<u8>) -> Self {
33        Self(path.into(), bytes)
34    }
35
36    pub fn path(&self) -> &str {
37        &self.0
38    }
39
40    pub fn bytes(&self) -> &[u8] {
41        &self.1
42    }
43}
44
45#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
46pub struct WebDependency(AuthorityId, String, DependencySelector);
47
48impl WebDependency {
49    pub const fn authority(&self) -> AuthorityId {
50        self.0
51    }
52
53    pub fn name(&self) -> &str {
54        &self.1
55    }
56
57    pub fn selector(&self) -> &DependencySelector {
58        &self.2
59    }
60}
61
62#[derive(Debug, Eq, PartialEq)]
63pub struct SourcePackage {
64    id: WebId,
65    files: Vec<SourceFile>,
66    entry: String,
67    tests: String,
68    dependencies: Vec<WebDependency>,
69}
70
71impl SourcePackage {
72    pub fn new(id: WebId, mut files: Vec<SourceFile>) -> Result<Self, PackageError> {
73        files
74            .iter()
75            .try_for_each(|file| validate_source_path(file.path()))?;
76        files.sort();
77        if files
78            .windows(2)
79            .any(|pair| pair[0].path() == pair[1].path())
80        {
81            return fail("duplicate source path");
82        }
83        if files
84            .iter()
85            .any(|file| has_file_ancestor(&files, file.path()))
86        {
87            return fail("source path collides with a file ancestor");
88        }
89
90        let manifest_source = required_utf8(&files, "k1-web.json")?;
91        required_utf8(&files, "Documentation.md")?;
92        let manifest: Manifest = serde_json::from_str(manifest_source)
93            .map_err(|cause| PackageError(format!("invalid k1-web.json: {cause}")))?;
94        validate_identity(&id, &manifest)?;
95        required_javascript(&files, &manifest.entry, "entry")?;
96        required_javascript(&files, &manifest.tests, "tests")?;
97        let dependencies = parse_dependencies(manifest.dependencies)?;
98
99        Ok(Self {
100            id,
101            files,
102            entry: manifest.entry,
103            tests: manifest.tests,
104            dependencies,
105        })
106    }
107
108    pub fn id(&self) -> &WebId {
109        &self.id
110    }
111
112    pub fn files(&self) -> &[SourceFile] {
113        &self.files
114    }
115
116    pub fn entry(&self) -> &str {
117        &self.entry
118    }
119
120    pub fn tests(&self) -> &str {
121        &self.tests
122    }
123
124    pub fn dependencies(&self) -> &[WebDependency] {
125        &self.dependencies
126    }
127}
128
129pub(crate) fn validate_manifest_for_projection(source: &str) -> Result<(), PackageError> {
130    let manifest: Manifest = serde_json::from_str(source)
131        .map_err(|cause| PackageError(format!("invalid k1-web.json: {cause}")))?;
132    validate_logical_name(&manifest.name)?;
133    canonical_manifest_version(&manifest.version)?;
134    validate_javascript_path(&manifest.entry, "entry")?;
135    validate_javascript_path(&manifest.tests, "tests")?;
136    parse_dependencies(manifest.dependencies)?;
137    Ok(())
138}
139
140fn has_file_ancestor(files: &[SourceFile], path: &str) -> bool {
141    path.match_indices('/').any(|(index, _)| {
142        files
143            .binary_search_by(|file| file.path().cmp(&path[..index]))
144            .is_ok()
145    })
146}
147
148fn required_utf8<'a>(files: &'a [SourceFile], path: &str) -> Result<&'a str, PackageError> {
149    let file = files
150        .iter()
151        .find(|file| file.path() == path)
152        .ok_or_else(|| PackageError(format!("missing {path}")))?;
153    std::str::from_utf8(file.bytes()).map_err(|_| PackageError(format!("{path} must be UTF-8")))
154}
155
156fn required_javascript(files: &[SourceFile], path: &str, field: &str) -> Result<(), PackageError> {
157    validate_javascript_path(path, field)?;
158    required_utf8(files, path)?;
159    Ok(())
160}
161
162fn validate_javascript_path(path: &str, field: &str) -> Result<(), PackageError> {
163    validate_source_path(path)?;
164    if !(path.ends_with(".js") || path.ends_with(".mjs")) {
165        return fail(format!("manifest {field} must name a JavaScript file"));
166    }
167    Ok(())
168}
169
170fn validate_identity(id: &WebId, manifest: &Manifest) -> Result<(), PackageError> {
171    if manifest.name != id.family().logical_name() {
172        return fail("manifest name does not match package identity");
173    }
174    let version = canonical_manifest_version(&manifest.version)?;
175    if &version != id.version() {
176        return fail("manifest version does not match package identity canonically");
177    }
178    Ok(())
179}
180
181fn canonical_manifest_version(text: &str) -> Result<Version, PackageError> {
182    let version = Version::parse(text)
183        .map_err(|_| PackageError("manifest version is not valid SemVer".into()))?;
184    validate_stable(&version)?;
185    if version.to_string() != text {
186        return fail("manifest version does not use canonical spelling");
187    }
188    Ok(version)
189}
190
191fn parse_dependencies(
192    declarations: Vec<ManifestDependency>,
193) -> Result<Vec<WebDependency>, PackageError> {
194    let mut dependencies = Vec::with_capacity(declarations.len());
195    for declaration in declarations {
196        validate_logical_name(&declaration.name)?;
197        dependencies.push(WebDependency(
198            parse_authority(&declaration.authority)?,
199            declaration.name,
200            DependencySelector::parse(&declaration.selector)?,
201        ));
202    }
203    dependencies.sort();
204    if dependencies.windows(2).any(|pair| pair[0] == pair[1]) {
205        return fail("duplicate dependency declaration");
206    }
207    Ok(dependencies)
208}
209
210fn parse_authority(text: &str) -> Result<AuthorityId, PackageError> {
211    let transaction_id =
212        TxId::from_str(text).map_err(|_| PackageError("invalid dependency authority".into()))?;
213    if transaction_id.to_string() != text {
214        return fail("dependency authority must use canonical spelling");
215    }
216    Ok(AuthorityId::new(transaction_id))
217}