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