1use serde_json::{Map, Value};
15use std::path::PathBuf;
16
17pub const PROTOCOL_VERSION: u32 = 4;
19
20pub const MAX_LINE_BYTES: usize = 1 << 20;
22
23pub const MAX_CHANGES: usize = 4096;
25
26pub const ALLOWED_MODES: [u32; 3] = [0o100_644, 0o100_755, 0o120_000];
28
29#[derive(Clone, Debug, Eq, PartialEq)]
40pub struct Change {
41 pub path: String,
43 pub mode: u32,
45 pub blob: Option<String>,
47}
48
49#[derive(Clone, Debug, Eq, PartialEq)]
59pub struct CommitRequest {
60 pub id: String,
62 pub repository: PathBuf,
64 pub common_dir: PathBuf,
66 pub reference: String,
68 pub base: String,
70 pub changes: Vec<Change>,
72 pub message: String,
74 pub author: Option<String>,
76 pub committer: String,
78 pub amend: bool,
81 pub tree: Option<String>,
84 pub commit: Option<String>,
88}
89
90impl CommitRequest {
91 pub fn lands_alone(&self) -> bool {
93 return self.amend || self.commit.is_some();
94 }
95}
96
97#[derive(Clone, Copy, Debug, Eq, PartialEq)]
99pub enum FailureCode {
100 RepositoryFull,
102 RepositoryReadOnly,
104 IndexInUse,
106 Rejected,
108 BranchKeepsChanging,
110 BranchMoved,
112 Unavailable,
114}
115
116impl FailureCode {
117 pub fn word(self) -> &'static str {
119 return match self {
120 Self::RepositoryFull => "repository-full",
121 Self::RepositoryReadOnly => "repository-read-only",
122 Self::IndexInUse => "index-in-use",
123 Self::Rejected => "rejected",
124 Self::BranchKeepsChanging => "branch-keeps-changing",
125 Self::BranchMoved => "branch-moved",
126 Self::Unavailable => "unavailable",
127 };
128 }
129
130 pub const ALL: [Self; 7] = [
132 Self::RepositoryFull,
133 Self::RepositoryReadOnly,
134 Self::IndexInUse,
135 Self::Rejected,
136 Self::BranchKeepsChanging,
137 Self::BranchMoved,
138 Self::Unavailable,
139 ];
140
141 pub fn from_word(word: &str) -> Option<Self> {
143 return Self::ALL.into_iter().find(|code| { return code.word() == word; });
144 }
145}
146
147#[derive(Clone, Debug, Eq, PartialEq)]
149pub enum ClientMessage {
150 Commit(Box<CommitRequest>),
152 Retire,
154 Work {
156 common_dir: PathBuf,
158 },
159}
160
161#[derive(Clone, Debug, Eq, PartialEq)]
163pub enum ServiceMessage {
164 Welcome { version: u32 },
166 Position { number: usize, of: usize },
168 Landed { id: String, commit: String, already: bool },
170 RetryPrepare { id: String, base: String },
172 Failed { id: String, code: FailureCode },
174}
175
176fn field<'a>(value: &'a Value, name: &str) -> Result<&'a str, String> {
177 return value.get(name).and_then(Value::as_str).ok_or_else(|| { return format!("field {name} is missing or not text"); });
178}
179
180pub fn is_object_name(text: &str) -> bool {
182 return (text.len() == 40 || text.len() == 64) && text.bytes().all(|byte| { return byte.is_ascii_hexdigit(); });
183}
184
185pub fn is_plain_path(path: &str) -> bool {
187 return !path.is_empty()
188 && !path.starts_with('/')
189 && !path.contains(['\0', '\n'])
190 && path.split('/').all(|part| { return !part.is_empty() && part != "." && part != ".." && part != ".git"; });
191}
192
193fn object(entries: Vec<(&str, Value)>) -> String {
195 let mut map: Map<String, Value> = Map::new();
196 for (key, value) in entries {
197 map.insert(key.to_string(), value);
198 }
199 return Value::Object(map).to_string();
200}
201
202pub fn encode_client(message: &ClientMessage) -> String {
212 return match message {
213 ClientMessage::Retire => object(vec![("type", Value::from("retire")), ("version", Value::from(PROTOCOL_VERSION))]),
214 ClientMessage::Work { common_dir } => object(vec![
215 ("type", Value::from("work")),
216 ("version", Value::from(PROTOCOL_VERSION)),
217 ("commonDir", Value::from(common_dir.to_string_lossy().as_ref())),
218 ]),
219 ClientMessage::Commit(request) => object(vec![
220 ("type", Value::from("commit")),
221 ("version", Value::from(PROTOCOL_VERSION)),
222 ("id", Value::from(request.id.as_str())),
223 ("repository", Value::from(request.repository.to_string_lossy().as_ref())),
224 ("commonDir", Value::from(request.common_dir.to_string_lossy().as_ref())),
225 ("reference", Value::from(request.reference.as_str())),
226 ("base", Value::from(request.base.as_str())),
227 (
228 "changes",
229 Value::Array(
230 request
231 .changes
232 .iter()
233 .map(|change| {
234 return Value::Object(Map::from_iter([
235 (String::from("path"), Value::from(change.path.as_str())),
236 (String::from("mode"), Value::from(change.mode)),
237 (String::from("blob"), change.blob.as_deref().map_or(Value::Null, Value::from)),
238 ]));
239 })
240 .collect(),
241 ),
242 ),
243 ("message", Value::from(request.message.as_str())),
244 ("author", request.author.as_deref().map_or(Value::Null, Value::from)),
245 ("committer", Value::from(request.committer.as_str())),
246 ("amend", Value::from(request.amend)),
247 ("tree", request.tree.as_deref().map_or(Value::Null, Value::from)),
248 ("commit", request.commit.as_deref().map_or(Value::Null, Value::from)),
249 ]),
250 };
251}
252
253pub fn decode_client(line: &str) -> Result<ClientMessage, String> {
263 if line.len() > MAX_LINE_BYTES {
264 return Err(String::from("line is too long"));
265 }
266 let value: Value = serde_json::from_str(line).map_err(|error| { return error.to_string(); })?;
267 if value.get("version").and_then(Value::as_u64) != Some(u64::from(PROTOCOL_VERSION)) {
268 return Err(String::from("unsupported version"));
269 }
270 match field(&value, "type")? {
271 "retire" => return Ok(ClientMessage::Retire),
272 "work" => {
273 let common_dir: PathBuf = PathBuf::from(field(&value, "commonDir")?);
274 if !common_dir.is_absolute() || common_dir.to_string_lossy().contains('\0') {
275 return Err(String::from("work request is not acceptable"));
276 }
277 return Ok(ClientMessage::Work { common_dir });
278 }
279 "commit" => {}
280 other => return Err(format!("unknown message type {other}")),
281 }
282 let mut changes: Vec<Change> = Vec::new();
283 for entry in value.get("changes").and_then(Value::as_array).ok_or("changes is missing")? {
284 let path: &str = field(entry, "path")?;
285 let mode: u32 = u32::try_from(entry.get("mode").and_then(Value::as_u64).ok_or("mode is missing")?)
286 .map_err(|_| { return String::from("mode is out of range"); })?;
287 let blob: Option<String> = entry.get("blob").and_then(Value::as_str).map(str::to_string);
288 if !is_plain_path(path) || !ALLOWED_MODES.contains(&mode) || blob.as_deref().is_some_and(|name| { return !is_object_name(name); }) {
289 return Err(format!("change for {path:?} is not acceptable"));
290 }
291 changes.push(Change { path: path.to_string(), mode, blob });
292 }
293 if changes.len() > MAX_CHANGES {
294 return Err(String::from("number of changes is not acceptable"));
295 }
296 let request = CommitRequest {
297 id: field(&value, "id")?.to_string(),
298 repository: PathBuf::from(field(&value, "repository")?),
299 common_dir: PathBuf::from(field(&value, "commonDir")?),
300 reference: field(&value, "reference")?.to_string(),
301 base: field(&value, "base")?.to_string(),
302 changes,
303 message: field(&value, "message")?.to_string(),
304 author: value.get("author").and_then(Value::as_str).map(str::to_string),
305 committer: field(&value, "committer")?.to_string(),
306 amend: value.get("amend").and_then(Value::as_bool).unwrap_or(false),
307 tree: value.get("tree").and_then(Value::as_str).map(str::to_string),
308 commit: value.get("commit").and_then(Value::as_str).map(str::to_string),
309 };
310 let well_formed: bool = !request.id.is_empty()
311 && request.id.bytes().all(|byte| { return byte.is_ascii_alphanumeric() || byte == b'-'; })
312 && request.reference.starts_with("refs/")
313 && !request.reference.contains(['\0', '\n', ' '])
314 && is_object_name(&request.base)
315 && request.repository.is_absolute()
316 && request.common_dir.is_absolute()
317 && !request.committer.contains(['\0', '\n'])
318 && request.author.as_deref().is_none_or(|author| { return !author.contains(['\0', '\n']); })
319 && (request.amend && request.commit.is_none()) == request.tree.is_some()
322 && request.tree.as_deref().is_none_or(is_object_name)
323 && request.commit.as_deref().is_none_or(is_object_name);
324 if !well_formed {
325 return Err(String::from("request is not acceptable"));
326 }
327 return Ok(ClientMessage::Commit(Box::new(request)));
328}
329
330pub fn encode_service(message: &ServiceMessage) -> String {
340 return match message {
341 ServiceMessage::Welcome { version } => object(vec![("type", Value::from("welcome")), ("version", Value::from(*version))]),
342 ServiceMessage::Position { number, of } => {
343 object(vec![("type", Value::from("position")), ("number", Value::from(*number)), ("of", Value::from(*of))])
344 }
345 ServiceMessage::Landed { id, commit, already } => object(vec![
346 ("type", Value::from("landed")),
347 ("id", Value::from(id.as_str())),
348 ("commit", Value::from(commit.as_str())),
349 ("already", Value::from(*already)),
350 ]),
351 ServiceMessage::RetryPrepare { id, base } => {
352 object(vec![("type", Value::from("retry-prepare")), ("id", Value::from(id.as_str())), ("base", Value::from(base.as_str()))])
353 }
354 ServiceMessage::Failed { id, code } => {
355 object(vec![("type", Value::from("failed")), ("id", Value::from(id.as_str())), ("code", Value::from(code.word()))])
356 }
357 };
358}
359
360pub fn decode_service(line: &str) -> Result<ServiceMessage, String> {
370 if line.len() > MAX_LINE_BYTES {
371 return Err(String::from("line is too long"));
372 }
373 let value: Value = serde_json::from_str(line).map_err(|error| { return error.to_string(); })?;
374 let number = |name: &str| -> Result<usize, String> {
375 return value.get(name).and_then(Value::as_u64).and_then(|n| { return usize::try_from(n).ok(); })
376 .ok_or_else(|| { return format!("field {name} is missing"); });
377 };
378 return match field(&value, "type")? {
379 "welcome" => Ok(ServiceMessage::Welcome {
380 version: u32::try_from(number("version")?).map_err(|_| { return String::from("version is out of range"); })?,
381 }),
382 "position" => Ok(ServiceMessage::Position { number: number("number")?, of: number("of")? }),
383 "landed" => Ok(ServiceMessage::Landed {
384 id: field(&value, "id")?.to_string(),
385 commit: field(&value, "commit")?.to_string(),
386 already: value.get("already").and_then(Value::as_bool).unwrap_or(false),
387 }),
388 "retry-prepare" => Ok(ServiceMessage::RetryPrepare { id: field(&value, "id")?.to_string(), base: field(&value, "base")?.to_string() }),
389 "failed" => Ok(ServiceMessage::Failed {
390 id: field(&value, "id")?.to_string(),
391 code: FailureCode::from_word(field(&value, "code")?).ok_or("unknown failure code")?,
392 }),
393 other => Err(format!("unknown message type {other}")),
394 };
395}
396
397#[cfg(test)]
399#[path = "service_protocol_tests.rs"]
400mod tests;