1use std::error::Error;
9use std::fmt;
10use std::path::{Path, PathBuf};
11
12use crate::daemon_registration as backend;
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub enum CacheRootKind {
20 CacheData,
22 CacheIndex,
24 CacheLogs,
26 CacheLocks,
28 CacheTmp,
30 Unknown(i32),
32}
33
34impl CacheRootKind {
35 fn from_backend(value: i32) -> Self {
36 match backend::protocol::CacheRootKind::try_from(value) {
37 Ok(backend::protocol::CacheRootKind::CacheData) => Self::CacheData,
38 Ok(backend::protocol::CacheRootKind::CacheIndex) => Self::CacheIndex,
39 Ok(backend::protocol::CacheRootKind::CacheLogs) => Self::CacheLogs,
40 Ok(backend::protocol::CacheRootKind::CacheLocks) => Self::CacheLocks,
41 Ok(backend::protocol::CacheRootKind::CacheTmp) => Self::CacheTmp,
42 Ok(_) | Err(_) => Self::Unknown(value),
43 }
44 }
45
46 fn into_backend(self) -> i32 {
47 match self {
48 Self::CacheData => backend::protocol::CacheRootKind::CacheData as i32,
49 Self::CacheIndex => backend::protocol::CacheRootKind::CacheIndex as i32,
50 Self::CacheLogs => backend::protocol::CacheRootKind::CacheLogs as i32,
51 Self::CacheLocks => backend::protocol::CacheRootKind::CacheLocks as i32,
52 Self::CacheTmp => backend::protocol::CacheRootKind::CacheTmp as i32,
53 Self::Unknown(value) => value,
54 }
55 }
56}
57
58#[derive(Clone, Copy, Debug, PartialEq, Eq)]
60pub struct CacheRoot<'a> {
61 kind: CacheRootKind,
62 path: &'a str,
63}
64
65impl<'a> CacheRoot<'a> {
66 #[must_use]
68 pub fn kind(self) -> CacheRootKind {
69 self.kind
70 }
71
72 #[must_use]
74 pub fn path(self) -> &'a str {
75 self.path
76 }
77}
78
79#[derive(Debug)]
81pub enum DaemonRegistrationError {
82 Io(std::io::Error),
84 MalformedRecord,
86 ManifestIntegrityFailure,
88 UnsupportedManifestSchema {
90 got: u32,
92 supported: u32,
94 },
95 InvalidNameOrVersion {
97 detail: String,
99 },
100 MissingParent {
102 path: PathBuf,
104 },
105 InsecureDirectory {
107 path: PathBuf,
109 },
110 ServiceNameMismatch {
112 requested: String,
114 actual: String,
116 },
117 InvalidServicePath {
119 field: &'static str,
121 path: String,
123 reason: &'static str,
125 },
126 InvalidServiceIsolation {
128 reason: &'static str,
130 },
131 Serialization,
133}
134
135impl fmt::Display for DaemonRegistrationError {
136 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
137 match self {
138 Self::Io(error) => write!(formatter, "daemon registration I/O failed: {error}"),
139 Self::MalformedRecord => formatter.write_str("malformed frozen v1 registration record"),
140 Self::ManifestIntegrityFailure => {
141 formatter.write_str("frozen v1 manifest SHA-256 seal did not verify")
142 }
143 Self::UnsupportedManifestSchema { got, supported } => {
144 write!(
145 formatter,
146 "unsupported manifest schema {got}; supported through {supported}"
147 )
148 }
149 Self::InvalidNameOrVersion { detail } => {
150 write!(
151 formatter,
152 "invalid frozen v1 service name or version: {detail}"
153 )
154 }
155 Self::MissingParent { path } => {
156 write!(
157 formatter,
158 "registration path has no parent: {}",
159 path.display()
160 )
161 }
162 Self::InsecureDirectory { path } => {
163 write!(
164 formatter,
165 "registration directory is not owner-private: {}",
166 path.display()
167 )
168 }
169 Self::ServiceNameMismatch { requested, actual } => {
170 write!(
171 formatter,
172 "requested service {requested:?}, found {actual:?}"
173 )
174 }
175 Self::InvalidServicePath {
176 field,
177 path,
178 reason,
179 } => write!(
180 formatter,
181 "invalid service-definition {field} {path:?}: {reason}"
182 ),
183 Self::InvalidServiceIsolation { reason } => {
184 write!(formatter, "invalid service-definition isolation: {reason}")
185 }
186 Self::Serialization => {
187 formatter.write_str("frozen v1 registration serialization failed")
188 }
189 }
190 }
191}
192
193impl Error for DaemonRegistrationError {
194 fn source(&self) -> Option<&(dyn Error + 'static)> {
195 match self {
196 Self::Io(error) => Some(error),
197 _ => None,
198 }
199 }
200}
201
202fn manifest_error(error: backend::manifest::ManifestError) -> DaemonRegistrationError {
203 match error {
204 backend::manifest::ManifestError::Io(error) => DaemonRegistrationError::Io(error),
205 backend::manifest::ManifestError::Decode(_) => DaemonRegistrationError::MalformedRecord,
206 backend::manifest::ManifestError::Encode(_) => DaemonRegistrationError::Serialization,
207 backend::manifest::ManifestError::Corruption => {
208 DaemonRegistrationError::ManifestIntegrityFailure
209 }
210 backend::manifest::ManifestError::SchemaTooNew { got, supported } => {
211 DaemonRegistrationError::UnsupportedManifestSchema { got, supported }
212 }
213 backend::manifest::ManifestError::InvalidName(error) => {
214 DaemonRegistrationError::InvalidNameOrVersion {
215 detail: error.to_string(),
216 }
217 }
218 backend::manifest::ManifestError::MissingParent(path) => {
219 DaemonRegistrationError::MissingParent { path }
220 }
221 backend::manifest::ManifestError::InsecureRegistry(path) => {
222 DaemonRegistrationError::InsecureDirectory { path }
223 }
224 }
225}
226
227fn service_error(
228 error: backend::service_def_loader::ServiceDefinitionError,
229) -> DaemonRegistrationError {
230 match error {
231 backend::service_def_loader::ServiceDefinitionError::Io(error) => {
232 DaemonRegistrationError::Io(error)
233 }
234 backend::service_def_loader::ServiceDefinitionError::Decode(_) => {
235 DaemonRegistrationError::MalformedRecord
236 }
237 backend::service_def_loader::ServiceDefinitionError::InvalidName(error) => {
238 DaemonRegistrationError::InvalidNameOrVersion {
239 detail: error.to_string(),
240 }
241 }
242 backend::service_def_loader::ServiceDefinitionError::InsecureDirectory(path) => {
243 DaemonRegistrationError::InsecureDirectory { path }
244 }
245 backend::service_def_loader::ServiceDefinitionError::ServiceNameMismatch {
246 requested,
247 actual,
248 } => DaemonRegistrationError::ServiceNameMismatch { requested, actual },
249 backend::service_def_loader::ServiceDefinitionError::InvalidPath {
250 field,
251 path,
252 reason,
253 } => DaemonRegistrationError::InvalidServicePath {
254 field,
255 path,
256 reason,
257 },
258 backend::service_def_loader::ServiceDefinitionError::InvalidIsolation { reason } => {
259 DaemonRegistrationError::InvalidServiceIsolation { reason }
260 }
261 }
262}
263
264#[derive(Clone, Debug)]
266pub struct CacheManifestBuilder {
267 inner: backend::builders::CacheManifestBuilder,
268 roots: Vec<(i32, String)>,
269}
270
271impl CacheManifestBuilder {
272 #[must_use]
274 pub fn new(service_name: impl Into<String>, service_version: impl Into<String>) -> Self {
275 Self {
276 inner: backend::builders::CacheManifestBuilder::new(service_name, service_version),
277 roots: Vec::new(),
278 }
279 }
280
281 #[must_use]
283 pub fn broker_instance(mut self, instance: impl Into<String>) -> Self {
284 self.inner = self.inner.broker_instance(instance);
285 self
286 }
287
288 #[must_use]
290 pub fn root(mut self, kind: CacheRootKind, path: impl Into<String>) -> Self {
291 self.roots.push((kind.into_backend(), path.into()));
292 self
293 }
294
295 pub fn build(self) -> Result<CacheManifest, DaemonRegistrationError> {
297 let mut manifest = self.inner.build().map_err(manifest_error)?;
298 manifest.roots = self
299 .roots
300 .into_iter()
301 .map(|(kind, path)| backend::protocol::CacheRoot {
302 kind,
303 path,
304 ..Default::default()
305 })
306 .collect();
307 backend::manifest::manifest_with_self_sha256(&manifest)
308 .map(CacheManifest::from_backend)
309 .map_err(manifest_error)
310 }
311
312 pub fn publish(self) -> Result<PathBuf, DaemonRegistrationError> {
314 let manifest = self.build()?;
315 backend::manifest::write_to_central(
316 manifest.service_name(),
317 manifest.service_version(),
318 &manifest.inner,
319 )
320 .map_err(manifest_error)
321 }
322
323 pub fn publish_in(
325 self,
326 registry_dir: impl AsRef<Path>,
327 ) -> Result<PathBuf, DaemonRegistrationError> {
328 let manifest = self.build()?;
329 backend::manifest::write_to_central_in_dir(
330 registry_dir.as_ref(),
331 manifest.service_name(),
332 manifest.service_version(),
333 &manifest.inner,
334 )
335 .map_err(manifest_error)
336 }
337}
338
339#[derive(Clone, Debug, PartialEq)]
341pub struct CacheManifest {
342 inner: backend::protocol::CacheManifest,
343}
344
345impl CacheManifest {
346 fn from_backend(inner: backend::protocol::CacheManifest) -> Self {
347 Self { inner }
348 }
349
350 pub fn read(path: impl AsRef<Path>) -> Result<Self, DaemonRegistrationError> {
352 backend::manifest::read_manifest(path.as_ref())
353 .map(Self::from_backend)
354 .map_err(manifest_error)
355 }
356
357 #[must_use]
359 pub fn service_name(&self) -> &str {
360 &self.inner.service_name
361 }
362
363 #[must_use]
365 pub fn service_version(&self) -> &str {
366 &self.inner.service_version
367 }
368
369 #[must_use]
371 pub fn broker_envelope_version(&self) -> &str {
372 &self.inner.broker_envelope_version
373 }
374
375 #[must_use]
377 pub fn broker_instance(&self) -> &str {
378 &self.inner.broker_instance
379 }
380
381 #[must_use]
383 pub fn schema_version(&self) -> u32 {
384 self.inner.manifest_schema_version
385 }
386
387 #[must_use]
389 pub fn media_type(&self) -> &str {
390 &self.inner.media_type
391 }
392
393 #[must_use]
395 pub fn created_at_unix_ms(&self) -> u64 {
396 self.inner.created_at_unix_ms
397 }
398
399 #[must_use]
401 pub fn last_active_unix_ms(&self) -> u64 {
402 self.inner.last_active_unix_ms
403 }
404
405 #[must_use]
407 pub fn has_host_identity(&self) -> bool {
408 self.inner.host.is_some()
409 }
410
411 #[must_use]
413 pub fn has_sha256_seal(&self) -> bool {
414 self.inner.self_sha256.len() == 32
415 }
416
417 pub fn roots(&self) -> impl ExactSizeIterator<Item = CacheRoot<'_>> {
419 self.inner.roots.iter().map(|root| CacheRoot {
420 kind: CacheRootKind::from_backend(root.kind),
421 path: &root.path,
422 })
423 }
424}
425
426#[derive(Clone, Debug)]
428pub struct ServiceDefinitionBuilder {
429 inner: backend::builders::ServiceDefinitionBuilder,
430}
431
432impl ServiceDefinitionBuilder {
433 #[must_use]
435 pub fn shared_broker(service_name: impl Into<String>, binary_path: impl Into<String>) -> Self {
436 Self {
437 inner: backend::builders::ServiceDefinitionBuilder::shared_broker(
438 service_name,
439 binary_path,
440 ),
441 }
442 }
443
444 #[must_use]
446 pub fn per_version_binary_dir(mut self, directory: impl Into<String>) -> Self {
447 self.inner = self.inner.per_version_binary_dir(directory);
448 self
449 }
450
451 #[must_use]
453 pub fn min_version(mut self, version: impl Into<String>) -> Self {
454 self.inner = self.inner.min_version(version);
455 self
456 }
457
458 #[must_use]
460 pub fn allow_version(mut self, version: impl Into<String>) -> Self {
461 self.inner = self.inner.allow_version(version);
462 self
463 }
464
465 #[must_use]
467 pub fn label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
468 self.inner = self.inner.label(key, value);
469 self
470 }
471
472 pub fn build(self) -> Result<ServiceDefinition, DaemonRegistrationError> {
474 self.inner
475 .build()
476 .map(ServiceDefinition::from_backend)
477 .map_err(service_error)
478 }
479
480 pub fn install(self) -> Result<PathBuf, DaemonRegistrationError> {
482 self.inner.install().map_err(service_error)
483 }
484
485 pub fn install_in(self, root: impl AsRef<Path>) -> Result<PathBuf, DaemonRegistrationError> {
487 self.inner.install_in(root.as_ref()).map_err(service_error)
488 }
489}
490
491#[derive(Clone, Debug, PartialEq)]
493pub struct ServiceDefinition {
494 inner: backend::protocol::ServiceDefinition,
495}
496
497impl ServiceDefinition {
498 fn from_backend(inner: backend::protocol::ServiceDefinition) -> Self {
499 Self { inner }
500 }
501
502 pub fn read(
504 root: impl AsRef<Path>,
505 service_name: impl AsRef<str>,
506 ) -> Result<Self, DaemonRegistrationError> {
507 backend::service_def_loader::ServiceDefinitionLoader::new(root.as_ref())
508 .load(service_name.as_ref())
509 .map(Self::from_backend)
510 .map_err(service_error)
511 }
512
513 #[must_use]
515 pub fn service_name(&self) -> &str {
516 &self.inner.service_name
517 }
518
519 #[must_use]
521 pub fn binary_path(&self) -> &str {
522 &self.inner.binary_path
523 }
524
525 #[must_use]
527 pub fn is_shared_broker(&self) -> bool {
528 self.inner.isolation == backend::protocol::BrokerIsolation::SharedBroker as i32
529 }
530
531 #[must_use]
533 pub fn per_version_binary_dir(&self) -> &str {
534 &self.inner.per_version_binary_dir
535 }
536
537 #[must_use]
539 pub fn min_version(&self) -> &str {
540 &self.inner.min_version
541 }
542
543 pub fn allowed_versions(&self) -> impl ExactSizeIterator<Item = &str> {
545 self.inner.version_allow_list.iter().map(String::as_str)
546 }
547
548 #[must_use]
550 pub fn label(&self, key: &str) -> Option<&str> {
551 self.inner.labels.get(key).map(String::as_str)
552 }
553}
554
555#[must_use]
557pub fn manifest_directory() -> PathBuf {
558 backend::manifest::central_registry_dir()
559}
560
561#[must_use]
563pub fn service_definition_directory() -> PathBuf {
564 backend::service_def_loader::service_definition_dir()
565}
566
567#[cfg(test)]
568mod compatibility_tests {
569 use super::*;
570
571 const FROZEN_MANIFEST_V1: &[u8] = &[
572 0x0a, 0x07, b'z', b'c', b'c', b'a', b'c', b'h', b'e', 0x12, 0x05, b'1', b'.', b'2', b'.',
573 b'3', 0x1a, 0x02, b'v', b'1', 0x20, 0x01, 0x28, 0x02, 0xc2, 0x02, 0x06, b's', b'h', b'a',
574 b'r', b'e', b'd', 0xb2, 0x04, 0x06, b'b', b'u', b'n', b'd', b'l', b'e', 0xa0, 0x06, 0x01,
575 0xaa, 0x06, 0x31, b'a', b'p', b'p', b'l', b'i', b'c', b'a', b't', b'i', b'o', b'n', b'/',
576 b'v', b'n', b'd', b'.', b'r', b'u', b'n', b'n', b'i', b'n', b'g', b'-', b'p', b'r', b'o',
577 b'c', b'e', b's', b's', b'.', b'c', b'a', b'c', b'h', b'e', b'-', b'm', b'a', b'n', b'i',
578 b'f', b'e', b's', b't', b'.', b'v', b'1', 0xb2, 0x06, 0x20, 0x01, 0x12, 0x0d, 0x59, 0xff,
579 0xa9, 0x45, 0xe3, 0xff, 0xa4, 0x6a, 0xaa, 0xaf, 0xee, 0xc8, 0x6f, 0xef, 0xfe, 0x55, 0xc2,
580 0x5f, 0x0a, 0x04, 0x0c, 0x9d, 0xe3, 0xdb, 0x67, 0x4b, 0xe3, 0xa0, 0x51,
581 ];
582
583 #[test]
584 fn reads_the_frozen_v1_manifest_golden_without_protocol_types() {
585 let directory = tempfile::tempdir().expect("manifest tempdir");
586 let path = directory.path().join("zccache-1.2.3.pb");
587 std::fs::write(&path, FROZEN_MANIFEST_V1).expect("write frozen manifest");
588
589 let manifest = CacheManifest::read(&path).expect("read frozen manifest");
590 assert_eq!(manifest.service_name(), "zccache");
591 assert_eq!(manifest.service_version(), "1.2.3");
592 assert_eq!(manifest.broker_envelope_version(), "v1");
593 assert_eq!(manifest.broker_instance(), "shared");
594 assert_eq!(manifest.schema_version(), 1);
595 assert_eq!(
596 manifest.media_type(),
597 "application/vnd.running-process.cache-manifest.v1"
598 );
599 assert_eq!(manifest.created_at_unix_ms(), 1);
600 assert_eq!(manifest.last_active_unix_ms(), 2);
601 assert!(manifest.has_sha256_seal());
602 }
603
604 #[test]
605 fn frozen_manifest_rejects_payload_changes_under_the_original_seal() {
606 let directory = tempfile::tempdir().expect("manifest tempdir");
607 let path = directory.path().join("tampered.pb");
608 let mut bytes = FROZEN_MANIFEST_V1.to_vec();
609 assert_eq!(bytes[2], b'z');
612 bytes[2] = b'x';
613 std::fs::write(&path, bytes).expect("write tampered manifest");
614 assert!(matches!(
615 CacheManifest::read(&path),
616 Err(DaemonRegistrationError::ManifestIntegrityFailure)
617 ));
618 }
619
620 #[test]
621 fn unknown_cache_root_discriminants_and_borrowed_paths_survive() {
622 for raw in [-7, 713, i32::MAX] {
623 assert_eq!(
624 CacheRootKind::from_backend(raw),
625 CacheRootKind::Unknown(raw)
626 );
627 assert_eq!(CacheRootKind::from_backend(raw).into_backend(), raw);
628 }
629 let path = String::from("/cache/retained");
630 let root = CacheRoot {
631 kind: CacheRootKind::Unknown(713),
632 path: &path,
633 };
634 assert_eq!(root.kind(), CacheRootKind::Unknown(713));
635 assert_eq!(root.path().as_ptr(), path.as_ptr());
636 }
637
638 #[test]
639 fn integrity_schema_and_io_failures_keep_distinct_classifications() {
640 assert!(matches!(
641 manifest_error(backend::manifest::ManifestError::Corruption),
642 DaemonRegistrationError::ManifestIntegrityFailure
643 ));
644 assert!(matches!(
645 manifest_error(backend::manifest::ManifestError::SchemaTooNew {
646 got: 99,
647 supported: 1,
648 }),
649 DaemonRegistrationError::UnsupportedManifestSchema {
650 got: 99,
651 supported: 1
652 }
653 ));
654 let error = manifest_error(backend::manifest::ManifestError::Io(
655 std::io::Error::from_raw_os_error(13),
656 ));
657 assert!(error.source().is_some());
658 let DaemonRegistrationError::Io(source) = error else {
659 panic!("I/O error expected")
660 };
661 assert_eq!(source.raw_os_error(), Some(13));
662 }
663}