Skip to main content

phoxal_bundle/
error.rs

1//! Bundle document, selection, and I/O failures.
2
3use std::path::PathBuf;
4
5use phoxal_model::component::capability::MotorCommand;
6use phoxal_model::identity::{CapabilityRef, ComponentInstanceId};
7use phoxal_model::{AssetId, Clock};
8use phoxal_runtime_contract::identity::{ParticipantArtifactId, ParticipantId};
9use phoxal_runtime_contract::metadata::ParticipantKind;
10use phoxal_runtime_contract::metadata::ParticipantRequirement;
11use phoxal_runtime_contract::version::RobotApiVersion;
12
13use crate::{BundlePath, BundlePathError, DigestError, ParticipantClock, Sha256Digest};
14
15/// Why a persisted runtime document is not a valid execution plan.
16#[derive(Clone, Debug, thiserror::Error)]
17pub enum DocumentError {
18    #[error(
19        "artifact '{artifact}' speaks robot API {actual}, but this runtime already selected {expected}"
20    )]
21    MixedRobotApi {
22        artifact: ParticipantArtifactId,
23        expected: RobotApiVersion,
24        actual: RobotApiVersion,
25    },
26    #[error("duplicate participant id '{id}'")]
27    DuplicateParticipant { id: ParticipantId },
28    #[error("duplicate participant binary path '{path}'")]
29    DuplicateBinary { path: BundlePath },
30    #[error("participant '{participant}' references unknown artifact '{artifact}'")]
31    UnknownArtifact {
32        participant: ParticipantId,
33        artifact: ParticipantArtifactId,
34    },
35    #[error("artifact '{artifact}' contract names '{contract}'")]
36    ArtifactContractMismatch {
37        artifact: ParticipantArtifactId,
38        contract: ParticipantArtifactId,
39    },
40    #[error("artifact '{artifact}' path is outside bin/: {path}")]
41    ArtifactOutsideBin {
42        artifact: ParticipantArtifactId,
43        path: BundlePath,
44    },
45    #[error("brain artifact id is '{actual}', expected 'brain'")]
46    BrainArtifactId { actual: ParticipantArtifactId },
47    #[error("brain artifact path is '{actual}', expected 'bin/brain'")]
48    BrainArtifactPath { actual: BundlePath },
49    #[error("artifact '{artifact}' is not selected by any runtime participant")]
50    UnusedArtifact { artifact: ParticipantArtifactId },
51    #[error("participant '{participant}' names unknown component instance '{component_instance}'")]
52    UnknownComponent {
53        participant: ParticipantId,
54        component_instance: ComponentInstanceId,
55    },
56    #[error("driver participant '{participant}' has no component binding")]
57    MissingDriverComponent { participant: ParticipantId },
58    #[error(
59        "{kind:?} participant '{participant}' cannot bind component instance '{component_instance}'"
60    )]
61    UnexpectedComponent {
62        participant: ParticipantId,
63        kind: ParticipantKind,
64        component_instance: ComponentInstanceId,
65    },
66    #[error("runtime graph has no mandatory 'brain' participant")]
67    MissingBrain,
68    #[error("brain artifact is mounted as participant '{actual}', expected 'brain'")]
69    BrainIdMismatch { actual: ParticipantId },
70    #[error("runtime graph contains more than one brain participant")]
71    DuplicateBrain,
72    #[error("simulated runtime graph has no simulator world authority")]
73    MissingSimulator,
74    #[error("simulated runtime graph contains more than one simulator world authority")]
75    DuplicateSimulator,
76    #[error(
77        "participant '{participant}' clock {participant_clock:?} conflicts with robot clock {robot:?}"
78    )]
79    ClockMismatch {
80        participant: ParticipantId,
81        robot: Clock,
82        participant_clock: ParticipantClock,
83    },
84    #[error(
85        "{kind:?} participant '{participant}' clock {participant_clock:?} is incompatible with robot clock {robot:?}"
86    )]
87    ExecutionModeMismatch {
88        participant: ParticipantId,
89        kind: ParticipantKind,
90        robot: Clock,
91        participant_clock: ParticipantClock,
92    },
93    #[error(
94        "artifact '{artifact}' requires {requirement:?}, but the robot uses {actual} kinematics"
95    )]
96    RequirementKinematicsMismatch {
97        artifact: ParticipantArtifactId,
98        requirement: ParticipantRequirement,
99        actual: phoxal_model::robot::KinematicKind,
100    },
101    #[error("artifact '{artifact}' requires at least one {side} actuator")]
102    RequirementActuatorListEmpty {
103        artifact: ParticipantArtifactId,
104        side: &'static str,
105    },
106    #[error("artifact '{artifact}' requirement could not resolve actuator '{actuator}': {error}")]
107    RequirementActuatorInvalid {
108        artifact: ParticipantArtifactId,
109        actuator: CapabilityRef,
110        error: String,
111    },
112    #[error(
113        "artifact '{artifact}' actuator '{actuator}' is configured for {actual:?}, but the binary requires {expected:?}"
114    )]
115    RequirementMotorModeMismatch {
116        artifact: ParticipantArtifactId,
117        actuator: CapabilityRef,
118        expected: MotorCommand,
119        actual: MotorCommand,
120    },
121    #[error("artifact '{artifact}' has an invalid config schema: {error}")]
122    InvalidConfigSchema {
123        artifact: ParticipantArtifactId,
124        error: String,
125    },
126    #[error("participant '{participant}' config does not match its binary schema: {error}")]
127    InvalidConfig {
128        participant: ParticipantId,
129        error: String,
130    },
131    #[error("asset id '{id}' is persisted at the wrong path '{path}'", id = id.as_str())]
132    AssetPathMismatch { id: AssetId, path: BundlePath },
133    #[error("asset path is outside assets/: {path}")]
134    AssetOutsideAssets { path: BundlePath },
135    #[error("duplicate asset id '{id}'", id = id.as_str())]
136    DuplicateAssetId { id: AssetId },
137    #[error("duplicate asset path '{path}'")]
138    DuplicateAssetPath { path: BundlePath },
139    #[error("router config is outside assets/: {path}")]
140    RouterOutsideAssets { path: BundlePath },
141    #[error("router config is not present in the asset index: {path}")]
142    RouterMissingAsset { path: BundlePath },
143    #[error("supplied asset bytes do not match the persisted asset index")]
144    AssetIndexMismatch,
145    #[error("supplied executable sources do not match the persisted artifact set")]
146    BinaryIndexMismatch,
147    #[error("bundle path is not valid: {0}")]
148    Path(#[from] BundlePathError),
149}
150
151/// Why an exact participant selection failed.
152#[derive(Clone, Debug, thiserror::Error)]
153pub enum SelectionError {
154    #[error("runtime bundle has no participant '{requested}'")]
155    Unknown { requested: ParticipantId },
156    #[error("participant '{participant}' references missing artifact '{artifact}'")]
157    MissingArtifact {
158        participant: ParticipantId,
159        artifact: ParticipantArtifactId,
160    },
161}
162
163/// Why reading or validating a bundle failed.
164#[derive(Debug, thiserror::Error)]
165pub enum BundleError {
166    #[error("failed to resolve bundle root {}: {source}", path.display())]
167    Root {
168        path: PathBuf,
169        #[source]
170        source: std::io::Error,
171    },
172    #[error("bundle root is not a directory: {0}")]
173    NotDirectory(PathBuf),
174    #[error("bundle target already exists: {0}")]
175    TargetExists(PathBuf),
176    #[error("secure no-follow opening is unsupported for bundle file {path}")]
177    UnsupportedSecureOpen { path: PathBuf },
178    #[error("atomic no-replace publication is unsupported for bundle target {path}")]
179    UnsupportedAtomicPublish { path: PathBuf },
180    #[error("failed to read runtime document {}: {source}", path.display())]
181    ReadDocument {
182        path: PathBuf,
183        #[source]
184        source: std::io::Error,
185    },
186    #[error("runtime document is not valid JSON: {0}")]
187    DocumentJson(#[from] serde_json::Error),
188    #[error(transparent)]
189    Document(#[from] DocumentError),
190    #[error("bundle contains forbidden symlink {path}")]
191    ForbiddenSymlink { path: PathBuf },
192    #[error("bundle contains unsupported filesystem entry {path}")]
193    UnsupportedEntry { path: PathBuf },
194    #[error("bundle executable is not executable: {path}")]
195    NotExecutable { path: PathBuf },
196    #[error(
197        "bundle executable {path} has mode {actual:#05o}, expected canonical mode {expected:#05o}"
198    )]
199    ExecutableMode {
200        path: PathBuf,
201        expected: u32,
202        actual: u32,
203    },
204    #[error(
205        "bundle data file {path} has mode {actual:#05o}, expected canonical mode {expected:#05o}"
206    )]
207    DataFileMode {
208        path: PathBuf,
209        expected: u32,
210        actual: u32,
211    },
212    #[error(
213        "bundle directory {path} has mode {actual:#05o}, expected canonical mode {expected:#05o}"
214    )]
215    DirectoryMode {
216        path: PathBuf,
217        expected: u32,
218        actual: u32,
219    },
220    #[error("bundle contains unexpected file {path}")]
221    UnexpectedFile { path: PathBuf },
222    #[error("bundle contains unindexed directory {path}")]
223    UnindexedDirectory { path: PathBuf },
224    #[error("bundle is missing required file {path}")]
225    MissingFile { path: PathBuf },
226    #[error("failed to read bundle file {path}: {source}", path = path.display())]
227    ReadFile {
228        path: PathBuf,
229        #[source]
230        source: std::io::Error,
231    },
232    #[error("bundle file {path} changed after indexing: expected {expected}, found {actual}")]
233    Integrity {
234        path: PathBuf,
235        expected: Sha256Digest,
236        actual: Sha256Digest,
237    },
238    #[error("bundle file {path} has size {actual}, expected {expected}")]
239    Size {
240        path: PathBuf,
241        expected: u64,
242        actual: u64,
243    },
244    #[error("asset '{id}' is not declared by runtime.json", id = id.as_str())]
245    UndeclaredAsset { id: AssetId },
246    #[error(transparent)]
247    Path(#[from] BundlePathError),
248    #[error(transparent)]
249    Digest(#[from] DigestError),
250    #[error(transparent)]
251    Selection(#[from] SelectionError),
252}