1use kcode_k1_rust_cache::{CheckIdentity, LockGeneration, SourceGeneration, UserRustCache};
2use kcode_k1_rust_package::{K1Dependency, LibraryFamily, LibraryId, SourcePackage};
3use kcode_k1_rust_podman::{CommandDiagnostics, RustPodman, RustPodmanError, RustPodmanPaths};
4use kcode_k1_rust_projection::{K1RustProjection, PublishOutcome, PublishStatus};
5use kcode_k1_rust_registry::{InstallOutcome, LocalRegistry};
6use kcode_k1_transaction_id::TxId;
7use std::collections::BTreeSet;
8use std::error::Error;
9use std::ffi::OsString;
10use std::fmt::{Debug, Display, Formatter};
11use std::fs::{self, OpenOptions};
12use std::io::Write;
13use std::path::{Path, PathBuf};
14use std::sync::Arc;
15
16type CodingResult<T> = Result<T, RustCodingError>;
17
18pub trait RustAuthorization {
19 fn authorize_view(&self, family: &LibraryFamily) -> Result<bool, String>;
20 fn authorize_publish(&self, family: &LibraryFamily) -> Result<bool, String>;
21}
22
23#[derive(Clone, Debug)]
24pub struct RustCodingConfigValues {
25 pub boot_id: String,
26 pub schema_id: String,
27 pub toolchain_policy: String,
28 pub image: String,
29 pub rust_toolchain: String,
30 pub check_policy: String,
31 pub target_triple: String,
32 pub command_policy: String,
33 pub podman_program: OsString,
34}
35
36#[derive(Clone, Debug)]
37pub struct RustCodingConfig {
38 values: RustCodingConfigValues,
39}
40
41impl RustCodingConfig {
42 pub fn new(values: RustCodingConfigValues) -> CodingResult<Self> {
43 let text = [
44 &values.boot_id,
45 &values.schema_id,
46 &values.toolchain_policy,
47 &values.image,
48 &values.rust_toolchain,
49 &values.check_policy,
50 &values.target_triple,
51 &values.command_policy,
52 ];
53 if text.iter().any(|value| value.is_empty()) || values.podman_program.is_empty() {
54 return Err(RustCodingError::State(
55 "Rust coding configuration values must be nonempty".into(),
56 ));
57 }
58 Ok(Self { values })
59 }
60}
61
62#[derive(Debug)]
63pub enum CheckOutcome {
64 Reused,
65 Checked {
66 fetch: CommandDiagnostics,
67 check: CommandDiagnostics,
68 },
69}
70
71#[derive(Debug)]
72pub struct PublishResult {
73 pub check: CheckOutcome,
74 pub outcome: PublishOutcome,
75}
76
77#[derive(Debug)]
78pub struct BinaryBuild {
79 pub path: PathBuf,
80 pub check: CheckOutcome,
81 pub diagnostics: CommandDiagnostics,
82}
83
84#[derive(Debug)]
85pub enum RustCodingError {
86 State(String),
87 Authorization(String),
88 DependencyUnavailable(LibraryId),
89 PublishDenied,
90 Podman(RustPodmanError),
91 AfterPublish {
92 outcome: PublishOutcome,
93 cause: Box<RustCodingError>,
94 },
95}
96
97impl Display for RustCodingError {
98 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
99 Debug::fmt(self, formatter)
100 }
101}
102
103impl Error for RustCodingError {}
104
105pub struct K1RustCoding {
106 config: RustCodingConfig,
107 projection: Arc<K1RustProjection>,
108 cache: UserRustCache,
109 registry: LocalRegistry,
110 podman: RustPodman,
111}
112
113impl K1RustCoding {
114 pub fn open(
115 cache_root: impl AsRef<Path>,
116 user: TxId,
117 config: RustCodingConfig,
118 projection: Arc<K1RustProjection>,
119 ) -> CodingResult<Self> {
120 let values = &config.values;
121 let cache = state_result(UserRustCache::open(
122 cache_root,
123 user,
124 &values.boot_id,
125 &values.schema_id,
126 &values.toolchain_policy,
127 ))?;
128 let registry = state_result(LocalRegistry::open(cache.registry_root()))?;
129 let podman = RustPodman::new(values.podman_program.clone(), values.image.clone())
130 .map_err(RustCodingError::Podman)?;
131 let value = Self {
132 config,
133 projection,
134 cache,
135 registry,
136 podman,
137 };
138 value.ensure_config()?;
139 Ok(value)
140 }
141
142 pub fn cache_epoch(&self) -> u64 {
143 self.cache.epoch()
144 }
145
146 pub fn admit_library(
147 &mut self,
148 id: &LibraryId,
149 gate: &dyn RustAuthorization,
150 ) -> CodingResult<()> {
151 self.resolve(id, gate, &mut BTreeSet::new(), &mut BTreeSet::new())
152 }
153
154 pub fn check(
155 &mut self,
156 package: &SourcePackage,
157 gate: &dyn RustAuthorization,
158 ) -> CodingResult<CheckOutcome> {
159 self.ensure_admitted(package.id(), gate)?;
160 let source = state_result(self.cache.materialize(package))?;
161 let mut visiting = BTreeSet::new();
162 let mut done = BTreeSet::new();
163 for dependency in package.dependencies() {
164 self.resolve(&dependency_id(dependency)?, gate, &mut visiting, &mut done)?;
165 }
166 self.ensure_config()?;
167 let family = package.id().family();
168 if let Some(lock) = state_result(self.cache.lock_generation(family))? {
169 let identity = self.identity(source, lock)?;
170 if state_result(self.cache.has_check(family, &identity))? {
171 return Ok(CheckOutcome::Reused);
172 }
173 }
174 let paths = self.paths(family);
175 let fetch = self.podman.fetch(&paths).map_err(RustCodingError::Podman)?;
176 let lock = state_result(
177 self.cache
178 .record_lock(family, &read_ordinary(&paths.workspace.join("Cargo.lock"))?),
179 )?;
180 let identity = self.identity(source, lock)?;
181 let check = self.podman.check(&paths).map_err(RustCodingError::Podman)?;
182 state_result(self.cache.record_check(family, &identity))?;
183 Ok(CheckOutcome::Checked { fetch, check })
184 }
185
186 pub fn publish(
187 &mut self,
188 package: &SourcePackage,
189 gate: &dyn RustAuthorization,
190 ) -> CodingResult<PublishResult> {
191 let check = self.check(package, gate)?;
192 match gate.authorize_publish(package.id().family()) {
193 Ok(true) => {}
194 Ok(false) => return Err(RustCodingError::PublishDenied),
195 Err(cause) => return Err(RustCodingError::Authorization(cause)),
196 }
197 let outcome = state_result(self.projection.publish(package))?;
198 if outcome.status() != PublishStatus::Conflict
199 && let Err(cause) = self.complete_publication(package)
200 {
201 return Err(RustCodingError::AfterPublish {
202 outcome,
203 cause: Box::new(cause),
204 });
205 }
206 Ok(PublishResult { check, outcome })
207 }
208
209 pub fn build_binary(
210 &mut self,
211 package: &SourcePackage,
212 binary: &str,
213 gate: &dyn RustAuthorization,
214 ) -> CodingResult<BinaryBuild> {
215 validate_binary(binary)?;
216 let check = self.check(package, gate)?;
217 let id = package.id();
218 let authority = id.family().authority().to_string();
219 let version = id.version().to_string();
220 let directory = ensure_directories(
221 &self.cache.binaries_root(),
222 &[&authority, id.family().logical_name(), &version],
223 )?;
224 let path = directory.join(binary);
225 let diagnostics = self
226 .podman
227 .build_binary(&self.paths(id.family()), binary, &path)
228 .map_err(RustCodingError::Podman)?;
229 Ok(BinaryBuild {
230 path,
231 check,
232 diagnostics,
233 })
234 }
235
236 pub fn reset(&mut self) -> CodingResult<()> {
237 let values = &self.config.values;
238 state_result(self.cache.reset(
239 &values.boot_id,
240 &values.schema_id,
241 &values.toolchain_policy,
242 ))?;
243 self.registry = state_result(LocalRegistry::open(self.cache.registry_root()))?;
244 self.ensure_config()
245 }
246
247 fn resolve(
248 &mut self,
249 id: &LibraryId,
250 gate: &dyn RustAuthorization,
251 visiting: &mut BTreeSet<LibraryId>,
252 done: &mut BTreeSet<LibraryId>,
253 ) -> CodingResult<()> {
254 if done.contains(id) || !visiting.insert(id.clone()) {
255 return Ok(());
256 }
257 let result = self.resolve_one(id, gate, visiting, done);
258 visiting.remove(id);
259 if result.is_ok() {
260 done.insert(id.clone());
261 }
262 result
263 }
264
265 fn resolve_one(
266 &mut self,
267 id: &LibraryId,
268 gate: &dyn RustAuthorization,
269 visiting: &mut BTreeSet<LibraryId>,
270 done: &mut BTreeSet<LibraryId>,
271 ) -> CodingResult<()> {
272 self.ensure_admitted(id, gate)?;
273 let package = state_result(self.projection.load(id))?
274 .ok_or_else(|| RustCodingError::DependencyUnavailable(id.clone()))?;
275 for dependency in package.dependencies() {
276 self.resolve(&dependency_id(dependency)?, gate, visiting, done)?;
277 }
278 self.install(&package)
279 }
280
281 fn ensure_admitted(&self, id: &LibraryId, gate: &dyn RustAuthorization) -> CodingResult<()> {
282 if state_result(self.cache.is_admitted(id.family()))? {
283 return Ok(());
284 }
285 match gate.authorize_view(id.family()) {
286 Ok(true) => state_result(self.cache.admit(id.family())).map(|_| ()),
287 Ok(false) => Err(RustCodingError::DependencyUnavailable(id.clone())),
288 Err(cause) => Err(RustCodingError::Authorization(cause)),
289 }
290 }
291
292 fn install(&mut self, package: &SourcePackage) -> CodingResult<()> {
293 if state_result(self.registry.install(package))? == InstallOutcome::Installed {
294 state_result(self.cache.advance_registry())?;
295 }
296 Ok(())
297 }
298
299 fn complete_publication(&mut self, package: &SourcePackage) -> CodingResult<()> {
300 self.install(package)?;
301 state_result(self.cache.admit(package.id().family()))?;
302 self.ensure_config()
303 }
304
305 fn identity(
306 &self,
307 source: SourceGeneration,
308 lock: LockGeneration,
309 ) -> CodingResult<CheckIdentity> {
310 let projection = state_result(self.projection.cursor())?
311 .map(|cursor| cursor.to_string().parse::<TxId>())
312 .transpose()
313 .map_err(state)?;
314 let values = &self.config.values;
315 Ok(CheckIdentity {
316 source,
317 lock,
318 registry: self.cache.registry_identity(),
319 projection,
320 image_identity: values.image.clone(),
321 rust_toolchain: values.rust_toolchain.clone(),
322 check_policy: values.check_policy.clone(),
323 target_triple: values.target_triple.clone(),
324 command_policy: values.command_policy.clone(),
325 })
326 }
327
328 fn paths(&self, family: &LibraryFamily) -> RustPodmanPaths {
329 RustPodmanPaths::new(
330 self.cache.workspace(family),
331 self.cache.cargo_home(),
332 self.cache.target_root(),
333 self.cache.registry_root(),
334 self.cache.cargo_home().join("config.toml"),
335 )
336 }
337
338 fn ensure_config(&self) -> CodingResult<()> {
339 write_config(
340 &self.cache.cargo_home().join("config.toml"),
341 self.registry.cargo_config().as_bytes(),
342 )
343 }
344}
345
346fn dependency_id(dependency: &K1Dependency) -> CodingResult<LibraryId> {
347 LibraryId::new(dependency.family().clone(), dependency.version().clone()).map_err(state)
348}
349
350fn read_ordinary(path: &Path) -> CodingResult<Vec<u8>> {
351 let metadata = fs::symlink_metadata(path).map_err(state)?;
352 if metadata.file_type().is_symlink() || !metadata.is_file() {
353 return Err(RustCodingError::State(format!(
354 "not an ordinary nonsymlink file: {}",
355 path.display()
356 )));
357 }
358 fs::read(path).map_err(state)
359}
360
361fn write_config(path: &Path, bytes: &[u8]) -> CodingResult<()> {
362 match fs::symlink_metadata(path) {
363 Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_file() => {
364 return Err(RustCodingError::State(format!(
365 "Cargo configuration is not an ordinary nonsymlink file: {}",
366 path.display()
367 )));
368 }
369 Ok(_) if fs::read(path).map_err(state)? == bytes => return Ok(()),
370 Ok(_) => {}
371 Err(cause) if cause.kind() == std::io::ErrorKind::NotFound => {}
372 Err(cause) => return Err(state(cause)),
373 }
374 let parent = path
375 .parent()
376 .ok_or_else(|| RustCodingError::State("Cargo configuration has no parent".into()))?;
377 let stage = parent.join(format!(".config.toml.stage-{}", std::process::id()));
378 match fs::symlink_metadata(&stage) {
379 Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_file() => {
380 return Err(RustCodingError::State(format!(
381 "Cargo configuration stage is not an ordinary nonsymlink file: {}",
382 stage.display()
383 )));
384 }
385 Ok(_) => fs::remove_file(&stage).map_err(state)?,
386 Err(cause) if cause.kind() == std::io::ErrorKind::NotFound => {}
387 Err(cause) => return Err(state(cause)),
388 }
389 let result = (|| {
390 let mut file = OpenOptions::new()
391 .write(true)
392 .create_new(true)
393 .open(&stage)?;
394 file.write_all(bytes)?;
395 file.sync_all()?;
396 fs::rename(&stage, path)
397 })()
398 .map_err(state);
399 if result.is_err() {
400 let _ = fs::remove_file(stage);
401 }
402 result
403}
404
405fn ensure_directories(root: &Path, names: &[&str]) -> CodingResult<PathBuf> {
406 let mut path = root.to_path_buf();
407 require_directory(&path)?;
408 for name in names {
409 path.push(name);
410 match fs::create_dir(&path) {
411 Ok(()) => {}
412 Err(cause) if cause.kind() == std::io::ErrorKind::AlreadyExists => {
413 require_directory(&path)?;
414 }
415 Err(cause) => return Err(state(cause)),
416 }
417 }
418 Ok(path)
419}
420
421fn require_directory(path: &Path) -> CodingResult<()> {
422 let metadata = fs::symlink_metadata(path).map_err(state)?;
423 if metadata.file_type().is_symlink() || !metadata.is_dir() {
424 return Err(RustCodingError::State(format!(
425 "not an ordinary nonsymlink directory: {}",
426 path.display()
427 )));
428 }
429 Ok(())
430}
431
432fn validate_binary(value: &str) -> CodingResult<()> {
433 let bytes = value.as_bytes();
434 let alphanumeric = |byte: &u8| byte.is_ascii_lowercase() || byte.is_ascii_digit();
435 let valid = (1..=36).contains(&bytes.len())
436 && alphanumeric(&bytes[0])
437 && alphanumeric(&bytes[bytes.len() - 1])
438 && bytes.iter().all(|byte| alphanumeric(byte) || *byte == b'-')
439 && !bytes.windows(2).any(|pair| pair == b"--");
440 if valid {
441 Ok(())
442 } else {
443 Err(RustCodingError::Podman(RustPodmanError::InvalidInput {
444 field: "binary",
445 reason: "must be lowercase kebab-case of length 1-36".into(),
446 }))
447 }
448}
449
450fn state(cause: impl Display) -> RustCodingError {
451 RustCodingError::State(cause.to_string())
452}
453
454fn state_result<T>(result: Result<T, String>) -> CodingResult<T> {
455 result.map_err(RustCodingError::State)
456}