Skip to main content

kcode_k1_rust_coding/
lib.rs

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}