1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// Copyright by contributors to this project.
// SPDX-License-Identifier: (Apache-2.0 OR MIT)
//! Error types for openjd-sessions.
use std::path::PathBuf;
use crate::session::SessionState;
/// Errors that can occur during session operations.
///
/// ```
/// use openjd_sessions::SessionError;
///
/// let err = SessionError::Runtime("something went wrong".into());
/// assert_eq!(err.to_string(), "something went wrong");
/// ```
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum SessionError {
/// Session is not in the expected state for the requested operation.
#[error("Session must be in {} state, current: {current}", format_expected(.expected))]
InvalidState {
expected: Vec<SessionState>,
current: SessionState,
},
/// An environment's onEnter or onExit script failed.
#[error("Environment '{name}' {action} failed: {reason}")]
EnvironmentScriptFailed {
name: String,
action: String,
reason: String,
},
/// Failed to resolve a format string expression.
#[error("Failed to resolve {context}: {reason}")]
FormatString { context: String, reason: String },
/// Failed to write an embedded file.
#[error("Failed to write embedded file '{name}': {source}")]
EmbeddedFile {
name: String,
#[source]
source: std::io::Error,
},
/// An embedded file's filename is not a safe single path component.
///
/// Raised when a `filename` field's value contains path separators,
/// parent-directory components, null bytes, or is otherwise unsafe.
/// This is a defense-in-depth check; `openjd-model` also rejects
/// path separators in filenames at template validation time per the
/// 2023-09 spec (§6.1.1 `<Filename>`).
#[error("Embedded file '{name}' has unsafe filename '{filename}': {reason}")]
EmbeddedFilePath {
name: String,
filename: String,
reason: String,
},
/// Failed to create or access the working directory.
#[error("Failed to create working directory {path}: {source}")]
WorkingDirectory {
path: PathBuf,
#[source]
source: std::io::Error,
},
/// Subprocess failed to start.
#[error("Failed to start subprocess '{command}': {source}")]
SubprocessStart {
command: String,
#[source]
source: std::io::Error,
},
/// Failed to create or manage a temp directory.
#[error("Failed to create temp directory in {path}: {source}")]
TempDir {
path: PathBuf,
#[source]
source: std::io::Error,
},
/// A generic runtime error.
#[error("{0}")]
Runtime(String),
/// Attempted to enter an environment that is already active in this session.
#[error("Environment {id} has already been entered in this Session.")]
DuplicateEnvironment { id: String },
/// Referenced an environment identifier that does not exist in this session.
#[error("Unknown environment identifier: {identifier}")]
UnknownEnvironment { identifier: String },
/// Failed to set file ownership or permissions (chown/chmod).
#[error("Failed to set permissions on '{path}': {reason}")]
PathPermissions { path: String, reason: String },
/// Cross-user helper IPC communication failure.
#[error("Cross-user helper error: {0}")]
HelperCommunication(String),
/// Attempted to exit an environment out of LIFO order.
#[error(
"Must exit the most recently entered environment first. Expected {expected}, got {got}"
)]
LifoViolation { expected: String, got: String },
/// RFC 0008 single-layer rule: a session may contain at most one
/// environment that defines wrap hooks. Raised when entering an
/// environment with wrap hooks while another wrap-defining environment
/// is already active. The message mirrors the Python runtime's wording.
#[error(
"RFC 0008: a session may have at most one Environment defining wrap hooks \
(onWrapEnvEnter / onWrapTaskRun / onWrapEnvExit). Environment '{existing}' \
already defines wrap hooks; cannot also enter '{entering}'."
)]
MultipleWrapEnvironments { existing: String, entering: String },
}
fn format_expected(states: &[SessionState]) -> String {
match states {
[single] => single.to_string(),
_ => states
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>()
.join(" or "),
}
}