kcode_k1_web_package/
source.rs1use 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}