fabrica

hanna/fabrica

4498 bytes
Raw
1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Parsing the SSH `exec` command line.
6//!
7//! Only the three git pack commands are accepted, each with exactly one quoted
8//! path argument. Anything else — including an interactive shell — is refused with
9//! a friendly banner by the caller.
10
11use git::pack::Service;
12
13/// A parsed, accepted git command: the service and its (raw, un-normalized) path.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct GitCommand {
16 /// The git service requested.
17 pub service: Service,
18 /// The repository path argument, as given (still needs normalizing).
19 pub path: String,
20}
21
22/// Parse an exec command line into an accepted [`GitCommand`], or `None` if it is
23/// not one of the three git pack commands with a single path argument.
24#[must_use]
25pub fn parse(command: &str) -> Option<GitCommand> {
26 let words = shell_split(command)?;
27 let (name, rest) = words.split_first()?;
28 if rest.len() != 1 {
29 return None;
30 }
31 let service = match name.as_str() {
32 "git-upload-pack" => Service::UploadPack,
33 "git-receive-pack" => Service::ReceivePack,
34 "git-upload-archive" => Service::UploadArchive,
35 _ => return None,
36 };
37 Some(GitCommand {
38 service,
39 path: rest[0].clone(),
40 })
41}
42
43/// A parsed `git-lfs-authenticate <path> <operation>` command: the git-lfs client
44/// runs this over SSH to obtain an HTTP endpoint and bearer token for LFS transfer.
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct LfsAuth {
47 /// The repository path argument (still needs normalizing).
48 pub path: String,
49 /// The operation: `upload` or `download`.
50 pub operation: String,
51}
52
53/// Parse a `git-lfs-authenticate <path> <operation>` command line, or `None`.
54#[must_use]
55pub fn parse_lfs(command: &str) -> Option<LfsAuth> {
56 let words = shell_split(command)?;
57 let (name, rest) = words.split_first()?;
58 if name != "git-lfs-authenticate" || rest.len() != 2 {
59 return None;
60 }
61 if rest[1] != "upload" && rest[1] != "download" {
62 return None;
63 }
64 Some(LfsAuth {
65 path: rest[0].clone(),
66 operation: rest[1].clone(),
67 })
68}
69
70/// Split a command line into words, honouring single and double quotes (git sends
71/// the path single-quoted). Returns `None` on an unterminated quote.
72fn shell_split(input: &str) -> Option<Vec<String>> {
73 let mut words = Vec::new();
74 let mut current = String::new();
75 let mut has_word = false;
76 let mut chars = input.chars().peekable();
77
78 while let Some(c) = chars.next() {
79 match c {
80 ' ' | '\t' => {
81 if has_word {
82 words.push(std::mem::take(&mut current));
83 has_word = false;
84 }
85 }
86 '\'' | '"' => {
87 has_word = true;
88 let quote = c;
89 loop {
90 match chars.next() {
91 Some(q) if q == quote => break,
92 Some(other) => current.push(other),
93 None => return None, // unterminated quote
94 }
95 }
96 }
97 other => {
98 has_word = true;
99 current.push(other);
100 }
101 }
102 }
103 if has_word {
104 words.push(current);
105 }
106 Some(words)
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112
113 #[test]
114 fn parses_the_three_git_commands() {
115 assert_eq!(
116 parse("git-upload-pack 'owner/repo.git'"),
117 Some(GitCommand {
118 service: Service::UploadPack,
119 path: "owner/repo.git".to_string()
120 })
121 );
122 assert_eq!(
123 parse("git-receive-pack '/owner/repo'").map(|c| c.service),
124 Some(Service::ReceivePack)
125 );
126 assert_eq!(
127 parse("git-upload-archive 'x'").map(|c| c.service),
128 Some(Service::UploadArchive)
129 );
130 }
131
132 #[test]
133 fn rejects_shells_and_extra_args() {
134 assert!(parse("bash").is_none());
135 assert!(parse("").is_none());
136 assert!(parse("git-upload-pack").is_none());
137 assert!(parse("git-upload-pack a b").is_none());
138 assert!(parse("scp -t /tmp").is_none());
139 assert!(parse("git-upload-pack 'unterminated").is_none());
140 }
141}