Skip to main content

wire/
lib.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Shared protocol/auth transport types.
3
4mod auth_context;
5#[cfg(test)]
6mod auth_tests;
7mod auth_token;
8mod capabilities;
9mod message_auth;
10mod message_delta;
11mod message_hosted;
12mod message_objects;
13mod message_pushpull;
14mod message_refs;
15mod message_status;
16mod native_pack;
17mod object_availability;
18mod object_graph;
19mod object_transfer;
20mod scope_match;
21
22pub use auth_context::AuthContext;
23pub use auth_token::{AuthToken, TokenScope};
24pub use capabilities::{
25    CAPABILITY_CHUNKED_TRANSFER, CAPABILITY_PACK_TRANSFER, CAPABILITY_PARTIAL_FETCH,
26    CAPABILITY_RESUMABLE_TRANSFER, Capabilities, CapabilitySet,
27};
28pub use message_auth::{AuthMethod, Permission};
29pub use message_delta::{DeltaData, RequestDelta};
30pub use message_hosted::{
31    CreateHostedGrant, CreateHostedRepository, CreateNamespace, DeleteHostedGrant,
32    DeleteHostedRepository, DeleteNamespace, HarnessIdentity, HostedGrantCreated,
33    HostedGrantDeleted, HostedGrantInfo, HostedGrantUpdated, HostedGrantsList, HostedNamespaceInfo,
34    HostedRepositoryInfo, ListHostedGrants, ListHostedNamespaces, ListHostedRepositories,
35    NamespaceCreated, NamespaceDeleted, NamespaceUpdated, NamespacesList, ProgressCheckpoint,
36    RepositoriesList, RepositoryCreated, RepositoryDeleted, RepositoryUpdated, SessionDiffSummary,
37    SessionReportEnvelope, TranscriptAttachmentRef, UpdateHostedGrant, UpdateHostedRepository,
38    UpdateNamespace, UsageTotals, WorktreeChangeBaseline,
39};
40pub use message_objects::{HaveObjects, ObjectData, ObjectRequest, SendObjects, WantObjects};
41pub use message_pushpull::{PullComplete, PushComplete};
42pub use message_refs::{HeadInfo, ListRefs, RefEntry, RefFilter, RefUpdated, RefsList, UpdateRef};
43pub use message_status::{Error, ErrorCode, Status, StatusCode};
44pub use native_pack::{
45    GitPackChunkState, GrowingPackChunkReader, MAX_RECEIVED_GIT_PACK_SIZE, NativePackBundle,
46    NativePackFileBundle, NativePackStreamingWriter, PackChunkSpool, PackChunkState,
47    PackFileChunkReader, build_native_pack, install_received_pack, is_native_packable_object_type,
48    native_pack_excluded_object_types, next_pack_chunk, receive_pack_chunk,
49};
50pub use object_availability::{ObjectAvailabilityPlan, has_object, plan_object_availability};
51pub use object_graph::{
52    ObjectId, ObjectInfo, ObjectType, PlannedObject, StateClosureOptions, enumerate_state_closure,
53    enumerate_state_closure_plan, enumerate_state_closure_plan_with_options,
54    enumerate_state_closure_with_options, is_ancestor,
55};
56pub use object_transfer::{
57    MAX_PULL_DECODE_MESSAGE_SIZE, MAX_RECEIVED_REDACTIONS_BLOB_SIZE,
58    MAX_RECEIVED_STATE_VISIBILITY_BLOB_SIZE, check_received_transfer_blob_size, chunk_bounds,
59    chunk_count, chunk_offset, load_object_data, load_requested_object, store_received_object,
60};
61pub use scope_match::scope_contains;
62
63/// Default port for Heddle protocol.
64pub const DEFAULT_PORT: u16 = 8421;
65
66/// Protocol version.
67pub const PROTOCOL_VERSION: u32 = 1;
68
69/// Maximum message size (64 MB).
70pub const MAX_MESSAGE_SIZE: usize = 64 * 1024 * 1024;
71
72/// Error type for protocol operations.
73#[derive(Debug, thiserror::Error)]
74pub enum ProtocolError {
75    #[error("io error: {0}")]
76    Io(#[from] std::io::Error),
77
78    #[error("serialization error: {0}")]
79    Serialization(String),
80
81    #[error("message too large: {size} bytes (max {max})")]
82    MessageTooLarge { size: usize, max: usize },
83
84    #[error("invalid message type: {0}")]
85    InvalidMessageType(u8),
86
87    #[error("protocol version mismatch: server={server}, client={client}")]
88    VersionMismatch { server: u32, client: u32 },
89
90    #[error("capability not supported: {0}")]
91    CapabilityNotSupported(String),
92
93    #[error("authentication failed: {0}")]
94    AuthenticationFailed(String),
95
96    #[error("authorization failed: {0}")]
97    AuthorizationFailed(String),
98
99    #[error("object not found: {0}")]
100    ObjectNotFound(String),
101
102    #[error("already exists: {0}")]
103    AlreadyExists(String),
104
105    #[error("invalid state: {0}")]
106    InvalidState(String),
107
108    #[error("remote error: {0}")]
109    Remote(String),
110
111    #[error("remote failure ({code:?}): {message}")]
112    RemoteFailure {
113        code: ErrorCode,
114        message: String,
115        details: Option<String>,
116    },
117
118    #[error("lock error: {0}")]
119    LockError(String),
120}
121
122impl From<rmp_serde::encode::Error> for ProtocolError {
123    fn from(e: rmp_serde::encode::Error) -> Self {
124        ProtocolError::Serialization(e.to_string())
125    }
126}
127
128impl From<rmp_serde::decode::Error> for ProtocolError {
129    fn from(e: rmp_serde::decode::Error) -> Self {
130        ProtocolError::Serialization(e.to_string())
131    }
132}
133
134impl From<objects::error::HeddleError> for ProtocolError {
135    fn from(e: objects::error::HeddleError) -> Self {
136        ProtocolError::Remote(e.to_string())
137    }
138}
139
140impl ProtocolError {
141    pub fn client_message(&self) -> String {
142        match self {
143            ProtocolError::Io(_) => "network error".to_string(),
144            ProtocolError::Serialization(_) => "protocol error".to_string(),
145            ProtocolError::MessageTooLarge { .. } => "message too large".to_string(),
146            ProtocolError::InvalidMessageType(_) => "protocol error".to_string(),
147            ProtocolError::VersionMismatch { .. } => "protocol version mismatch".to_string(),
148            ProtocolError::CapabilityNotSupported(_) => "capability not supported".to_string(),
149            ProtocolError::AuthenticationFailed(_) => "permission denied".to_string(),
150            ProtocolError::AuthorizationFailed(_) => "permission denied".to_string(),
151            ProtocolError::ObjectNotFound(_) => "object not found".to_string(),
152            ProtocolError::AlreadyExists(_) => "resource already exists".to_string(),
153            ProtocolError::InvalidState(_) => "invalid request state".to_string(),
154            ProtocolError::Remote(_) => "internal server error".to_string(),
155            ProtocolError::RemoteFailure { message, .. } => message.clone(),
156            ProtocolError::LockError(_) => "internal server error".to_string(),
157        }
158    }
159
160    pub fn error_code(&self) -> ErrorCode {
161        match self {
162            ProtocolError::Io(_) => ErrorCode::Network,
163            ProtocolError::Serialization(_) => ErrorCode::Protocol,
164            ProtocolError::MessageTooLarge { .. } => ErrorCode::Protocol,
165            ProtocolError::InvalidMessageType(_) => ErrorCode::Protocol,
166            ProtocolError::VersionMismatch { .. } => ErrorCode::Protocol,
167            ProtocolError::CapabilityNotSupported(_) => ErrorCode::Protocol,
168            ProtocolError::AuthenticationFailed(_) => ErrorCode::PermissionDenied,
169            ProtocolError::AuthorizationFailed(_) => ErrorCode::PermissionDenied,
170            ProtocolError::ObjectNotFound(_) => ErrorCode::NotFound,
171            ProtocolError::AlreadyExists(_) => ErrorCode::InvalidArgument,
172            ProtocolError::InvalidState(_) => ErrorCode::InvalidArgument,
173            ProtocolError::Remote(_) => ErrorCode::Server,
174            ProtocolError::RemoteFailure { code, .. } => *code,
175            ProtocolError::LockError(_) => ErrorCode::Server,
176        }
177    }
178
179    pub fn to_wire_error(&self, details: Option<String>) -> Error {
180        Error {
181            code: self.error_code(),
182            message: self.client_message(),
183            details,
184        }
185    }
186}
187
188pub type Result<T> = std::result::Result<T, ProtocolError>;
189
190#[cfg(test)]
191mod tests {
192    use std::io;
193
194    use super::{ErrorCode, ProtocolError};
195
196    #[test]
197    fn protocol_error_public_mapping_is_stable() {
198        let cases = vec![
199            (
200                ProtocolError::Io(io::Error::new(io::ErrorKind::TimedOut, "timeout")),
201                "network error",
202                ErrorCode::Network,
203            ),
204            (
205                ProtocolError::Serialization("bad msgpack".to_string()),
206                "protocol error",
207                ErrorCode::Protocol,
208            ),
209            (
210                ProtocolError::MessageTooLarge { size: 65, max: 64 },
211                "message too large",
212                ErrorCode::Protocol,
213            ),
214            (
215                ProtocolError::InvalidMessageType(42),
216                "protocol error",
217                ErrorCode::Protocol,
218            ),
219            (
220                ProtocolError::VersionMismatch {
221                    server: 2,
222                    client: 1,
223                },
224                "protocol version mismatch",
225                ErrorCode::Protocol,
226            ),
227            (
228                ProtocolError::CapabilityNotSupported("pack-v2".to_string()),
229                "capability not supported",
230                ErrorCode::Protocol,
231            ),
232            (
233                ProtocolError::AuthenticationFailed("bad token".to_string()),
234                "permission denied",
235                ErrorCode::PermissionDenied,
236            ),
237            (
238                ProtocolError::AuthorizationFailed("missing grant".to_string()),
239                "permission denied",
240                ErrorCode::PermissionDenied,
241            ),
242            (
243                ProtocolError::ObjectNotFound("abc123".to_string()),
244                "object not found",
245                ErrorCode::NotFound,
246            ),
247            (
248                ProtocolError::AlreadyExists("__users/luke/repo".to_string()),
249                "resource already exists",
250                ErrorCode::InvalidArgument,
251            ),
252            (
253                ProtocolError::InvalidState("bad resume".to_string()),
254                "invalid request state",
255                ErrorCode::InvalidArgument,
256            ),
257            (
258                ProtocolError::Remote("database unavailable".to_string()),
259                "internal server error",
260                ErrorCode::Server,
261            ),
262            (
263                ProtocolError::RemoteFailure {
264                    code: ErrorCode::InvalidArgument,
265                    message: "server supplied message".to_string(),
266                    details: Some("remote details".to_string()),
267                },
268                "server supplied message",
269                ErrorCode::InvalidArgument,
270            ),
271            (
272                ProtocolError::LockError("ref locked".to_string()),
273                "internal server error",
274                ErrorCode::Server,
275            ),
276        ];
277
278        for (error, expected_message, expected_code) in cases {
279            assert_eq!(error.client_message(), expected_message);
280            assert_eq!(error.error_code(), expected_code);
281
282            let wire_error = error.to_wire_error(Some("trace id".to_string()));
283            assert_eq!(wire_error.code, expected_code);
284            assert_eq!(wire_error.message, expected_message);
285            assert_eq!(wire_error.details.as_deref(), Some("trace id"));
286        }
287    }
288}