From 2120f790bf61f74180548ce313a033b916c559e7 Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Tue, 31 Aug 2021 17:46:25 +0100 Subject: [PATCH] Split git and web binaries --- Cargo.lock | 27 +++++++++++++++++---------- Cargo.toml | 36 +++++------------------------------- chartered-git/Cargo.toml | 25 +++++++++++++++++++++++++ chartered-web/Cargo.toml | 12 ++++++++++++ src/main.rs | 272 -------------------------------------------------------------------------------- chartered-git/src/main.rs | 272 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ chartered-web/src/main.rs | 4 ++++ src/git/codec.rs | 102 -------------------------------------------------------------------------------- src/git/mod.rs | 73 ------------------------------------------------------------------------- src/git/packfile.rs | 322 -------------------------------------------------------------------------------- chartered-git/src/git/codec.rs | 102 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ chartered-git/src/git/mod.rs | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ chartered-git/src/git/packfile.rs | 322 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 13 files changed, 832 insertions(+), 810 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 82ecddb..ddfce59 100644 --- a/Cargo.lock +++ a/Cargo.lock @@ -183,12 +183,11 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] -name = "chartered" +name = "chartered-git" version = "0.1.0" dependencies = [ "anyhow", "async-trait", - "axum", "bytes", "chrono", "const-sha1", @@ -204,6 +203,14 @@ "thrussh-keys", "tokio", "tokio-util", +] + +[[package]] +name = "chartered-web" +version = "0.1.0" +dependencies = [ + "axum", + "tokio", "tower", "tower-http", ] @@ -640,9 +647,9 @@ [[package]] name = "lock_api" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0382880606dff6d15c9476c416d18690b72742aa7b605bb6dd6ec9030fbf07eb" +checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" dependencies = [ "scopeguard", ] @@ -769,9 +776,9 @@ [[package]] name = "parking_lot" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" dependencies = [ "instant", "lock_api", @@ -780,9 +787,9 @@ [[package]] name = "parking_lot_core" -version = "0.8.3" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018" +checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" dependencies = [ "cfg-if", "instant", @@ -1012,9 +1019,9 @@ [[package]] name = "serde_json" -version = "1.0.66" +version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "336b10da19a12ad094b59d870ebde26a45402e5b470add4b5fd03c5048a32127" +checksum = "a7f9e390c27c3c0ce8bc5d725f6e4d30a29d26659494aa4b17535f7522c5c950" dependencies = [ "itoa", "ryu", diff --git a/Cargo.toml b/Cargo.toml index c2a9079..5560843 100644 --- a/Cargo.toml +++ a/Cargo.toml @@ -1,31 +1,5 @@ -[package] -name = "chartered" -version = "0.1.0" -edition = "2018" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -axum = "0.2" -tokio = { version = "1", features = ["full"] } -tower = { version = "0.4", features = ["util", "filter"] } -tower-http = { version = "0.1", features = ["trace"] } - -async-trait = "0" -thrussh = "0.33" -futures = "0.3" -thrussh-keys = "0.21" -anyhow = "1" -env_logger = "0.9" -tokio-util = { version = "0.6", features = ["codec"] } -bytes = "1" -flate2 = "1.0" -sha-1 = "0.9" -const-sha1 = "0.2" -crc = "2" -chrono = "0.4" -itoa = "0.4" - - -format-bytes = "0.1" -hex = "0.4"+[workspace] +members = [ + "chartered-git", + "chartered-web", +] diff --git a/chartered-git/Cargo.toml b/chartered-git/Cargo.toml new file mode 100644 index 0000000..688bb7b 100644 --- /dev/null +++ a/chartered-git/Cargo.toml @@ -1,0 +1,25 @@ +[package] +name = "chartered-git" +version = "0.1.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1" +async-trait = "0" +bytes = "1" +chrono = "0.4" +const-sha1 = "0.2" +crc = "2" +env_logger = "0.9" +flate2 = "1.0" +format-bytes = "0.1" +futures = "0.3" +hex = "0.4" +itoa = "0.4" +sha-1 = "0.9" +thrussh = "0.33" +thrussh-keys = "0.21" +tokio = { version = "1", features = ["full"] } +tokio-util = { version = "0.6", features = ["codec"] } diff --git a/chartered-web/Cargo.toml b/chartered-web/Cargo.toml new file mode 100644 index 0000000..e50ae48 100644 --- /dev/null +++ a/chartered-web/Cargo.toml @@ -1,0 +1,12 @@ +[package] +name = "chartered-web" +version = "0.1.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +axum = "0.2" +tokio = { version = "1", features = ["full"] } +tower = { version = "0.4", features = ["util", "filter"] } +tower-http = { version = "0.1", features = ["trace"] } diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 92529cc..0000000 100644 --- a/src/main.rs +++ /dev/null @@ -1,272 +1,0 @@ -#![deny(clippy::pedantic)] -#[allow(clippy::missing_errors_doc)] -pub mod git; - -use crate::git::{ - codec::{Encoder, GitCodec}, - packfile::{Commit, CommitUserInfo, PackFileEntry, TreeItem, TreeItemKind}, - PktLine, -}; - -use bytes::BytesMut; -use futures::future::Future; -use std::{fmt::Write, pin::Pin, sync::Arc}; -use thrussh::{ - server::{self, Auth, Session}, - ChannelId, CryptoVec, -}; -use thrussh_keys::key; -use tokio_util::codec::{Decoder, Encoder as TokioEncoder}; - -#[tokio::main] -#[allow(clippy::semicolon_if_nothing_returned)] // broken clippy lint -async fn main() { - env_logger::init(); - - let mut config = thrussh::server::Config::default(); - config.keys.push(key::KeyPair::generate_ed25519().unwrap()); - let config = Arc::new(config); - thrussh::server::run(config, "127.0.0.1:2233", Server) - .await - .unwrap(); -} - -#[derive(Clone)] -struct Server; - -impl server::Server for Server { - type Handler = Handler; - - fn new(&mut self, _: Option) -> Self::Handler { - Handler::default() - } -} - -#[derive(Default)] -struct Handler { - codec: GitCodec, - input_bytes: BytesMut, - output_bytes: BytesMut, -} - -impl Handler { - fn write(&mut self, packet: PktLine<'_>) -> Result<(), anyhow::Error> { - Encoder {}.encode(packet, &mut self.output_bytes) - } - - fn flush(&mut self, session: &mut Session, channel: ChannelId) { - session.data( - channel, - CryptoVec::from_slice(self.output_bytes.split().as_ref()), - ); - } -} - -type AsyncHandlerFn = Pin< - Box< - dyn Future::Error>> + Send, - >, ->; - -impl server::Handler for Handler { - type Error = anyhow::Error; - type FutureAuth = futures::future::Ready>; - type FutureUnit = AsyncHandlerFn; - type FutureBool = futures::future::Ready>; - - fn finished_auth(self, auth: Auth) -> Self::FutureAuth { - eprintln!("finished auth"); - futures::future::ready(Ok((self, auth))) - } - - fn finished_bool(self, b: bool, s: Session) -> Self::FutureBool { - eprintln!("finished bool"); - futures::future::ready(Ok((self, s, b))) - } - - fn finished(self, s: Session) -> Self::FutureUnit { - eprintln!("finished"); - Box::pin(futures::future::ready(Ok((self, s)))) - } - - fn shell_request(mut self, channel: ChannelId, mut session: Session) -> Self::FutureUnit { - Box::pin(async move { - write!(&mut self.output_bytes, "Hi there! You've successfully authenticated, but chartered does not provide shell access.\r\n")?; - self.flush(&mut session, channel); - session.close(channel); - Ok((self, session)) - }) - } - - fn exec_request( - mut self, - channel: ChannelId, - data: &[u8], - mut session: Session, - ) -> Self::FutureUnit { - eprintln!("exec {:x?}", data); - - let git_upload_pack = data.starts_with(b"git-upload-pack "); - - Box::pin(async move { - if git_upload_pack { - // TODO: check GIT_PROTOCOL=version=2 set - self.write(PktLine::Data(b"version 2\n"))?; - self.write(PktLine::Data(b"agent=chartered/0.1.0\n"))?; - self.write(PktLine::Data(b"ls-refs=unborn\n"))?; - self.write(PktLine::Data(b"fetch=shallow wait-for-done\n"))?; - self.write(PktLine::Data(b"server-option\n"))?; - self.write(PktLine::Data(b"object-info\n"))?; - self.write(PktLine::Flush)?; - self.flush(&mut session, channel); - } else { - session.data( - channel, - CryptoVec::from_slice(b"Sorry, I have no clue who you are\r\n"), - ); - session.close(channel); - } - - Ok((self, session)) - }) - } - - fn subsystem_request( - self, - _channel: ChannelId, - data: &str, - session: Session, - ) -> Self::FutureUnit { - eprintln!("subsystem req: {}", data); - Box::pin(futures::future::ready(Ok((self, session)))) - } - - fn auth_publickey(self, _: &str, _: &key::PublicKey) -> Self::FutureAuth { - eprintln!("finished auth pubkey"); - self.finished_auth(server::Auth::Accept) - } - - fn data(mut self, channel: ChannelId, data: &[u8], mut session: Session) -> Self::FutureUnit { - self.input_bytes.extend_from_slice(data); - - Box::pin(async move { - let mut ls_refs = false; - let mut fetch = false; - let mut done = false; - - while let Some(frame) = self.codec.decode(&mut self.input_bytes)? { - eprintln!("data: {:x?}", frame); - - if frame.as_ref() == "command=ls-refs".as_bytes() { - ls_refs = true; - } else if frame.as_ref() == "command=fetch".as_bytes() { - fetch = true; - } else if frame.as_ref() == "done".as_bytes() { - fetch = false; - done = true; - } - } - - // echo -ne "0012command=fetch\n0001000ethin-pack\n0010include-tag\n000eofs-delta\n0032want d24d8020163b5fee57c9babfd0c595b8c90ba253\n0009done\n" - - let test_crate_file = PackFileEntry::Blob(br#"{"name":"charteredtest","vers":"1.0.0","deps":[],"cksum":"7b821735f0211fd00032a9892d1bf2323c9d05d9c59b9303eb382f5ec1898bfc","features":{},"yanked":false,"links":null}"#); - let config_file = PackFileEntry::Blob( - br#"{ - "dl": "http://127.0.0.1:8888/api/v1/crates", - "api": "http://127.0.0.1:8888" - }"#, - ); - - let ch_ar_tree = PackFileEntry::Tree(vec![ - TreeItem { - kind: TreeItemKind::File, - name: "charteredtest", - hash: test_crate_file.hash()?, - } - ]); - - let ch_tree = PackFileEntry::Tree(vec![ - TreeItem { - kind: TreeItemKind::Directory, - name: "ar", - hash: ch_ar_tree.hash()?, - } - ]); - - let root_tree = PackFileEntry::Tree(vec![ - TreeItem { - kind: TreeItemKind::Directory, - name: "ch", - hash: ch_tree.hash()?, - }, - TreeItem { - kind: TreeItemKind::File, - name: "config.json", - hash: config_file.hash()?, - }, - ]); - - let commit_user = CommitUserInfo { - name: "Jordan Doyle", - email: "jordan@doyle.la", - time: chrono::Utc::now(), - }; - - let commit = PackFileEntry::Commit(Commit { - tree: root_tree.hash()?, - author: commit_user, - committer: commit_user, - message: "cool commit", - }); - - // echo -ne "0014command=ls-refs\n0014agent=git/2.321\n00010009peel\n000csymrefs\n000bunborn\n0014ref-prefix HEAD\n0019ref-prefix refs/HEAD\n001eref-prefix refs/tags/HEAD\n001fref-prefix refs/heads/HEAD\n0021ref-prefix refs/remotes/HEAD\n0026ref-prefix refs/remotes/HEAD/HEAD\n001aref-prefix refs/tags/\n0000" - // GIT_PROTOCOL=version=2 ssh -o SendEnv=GIT_PROTOCOL git@github.com git-upload-pack '/w4/chartered.git' - // ''.join([('{:04x}'.format(len(v) + 5)), v, "\n"]) - // echo -ne "0012command=fetch\n0001000ethin-pack\n0010no-progress\n0010include-tag\n000eofs-delta\n0032want f6046cf6372e0d8ab845f6dec1602c303a66ee91\n" - // sends a 000dpackfile back - // https://shafiul.github.io/gitbook/7_the_packfile.html - if ls_refs { - let commit_hash = hex::encode(&commit.hash()?); - self.write(PktLine::Data( - format!("{} HEAD symref-target:refs/heads/master\n", commit_hash).as_bytes(), - ))?; - self.write(PktLine::Flush)?; - self.flush(&mut session, channel); - } - - if fetch { - self.write(PktLine::Data(b"acknowledgments\n"))?; - self.write(PktLine::Data(b"ready\n"))?; - self.write(PktLine::Delimiter)?; - // self.write(PktLine::Data(b"shallow-info\n"))?; - // self.write(PktLine::Data(b"unshallow\n"))?; - done = true; - } - - if done { - self.write(PktLine::Data(b"packfile\n"))?; - - self.write(PktLine::SidebandMsg(b"Hello from chartered!\n"))?; - self.flush(&mut session, channel); - - let packfile = git::packfile::PackFile::new(vec![ - commit, - test_crate_file, - ch_tree, - ch_ar_tree, - config_file, - root_tree, - ]); - self.write(PktLine::SidebandData(packfile))?; - self.write(PktLine::Flush)?; - self.flush(&mut session, channel); - - session.exit_status_request(channel, 0); - session.eof(channel); - session.close(channel); - } - - Ok((self, session)) - }) - } -} diff --git a/chartered-git/src/main.rs b/chartered-git/src/main.rs new file mode 100644 index 0000000..92529cc 100644 --- /dev/null +++ a/chartered-git/src/main.rs @@ -1,0 +1,272 @@ +#![deny(clippy::pedantic)] +#[allow(clippy::missing_errors_doc)] +pub mod git; + +use crate::git::{ + codec::{Encoder, GitCodec}, + packfile::{Commit, CommitUserInfo, PackFileEntry, TreeItem, TreeItemKind}, + PktLine, +}; + +use bytes::BytesMut; +use futures::future::Future; +use std::{fmt::Write, pin::Pin, sync::Arc}; +use thrussh::{ + server::{self, Auth, Session}, + ChannelId, CryptoVec, +}; +use thrussh_keys::key; +use tokio_util::codec::{Decoder, Encoder as TokioEncoder}; + +#[tokio::main] +#[allow(clippy::semicolon_if_nothing_returned)] // broken clippy lint +async fn main() { + env_logger::init(); + + let mut config = thrussh::server::Config::default(); + config.keys.push(key::KeyPair::generate_ed25519().unwrap()); + let config = Arc::new(config); + thrussh::server::run(config, "127.0.0.1:2233", Server) + .await + .unwrap(); +} + +#[derive(Clone)] +struct Server; + +impl server::Server for Server { + type Handler = Handler; + + fn new(&mut self, _: Option) -> Self::Handler { + Handler::default() + } +} + +#[derive(Default)] +struct Handler { + codec: GitCodec, + input_bytes: BytesMut, + output_bytes: BytesMut, +} + +impl Handler { + fn write(&mut self, packet: PktLine<'_>) -> Result<(), anyhow::Error> { + Encoder {}.encode(packet, &mut self.output_bytes) + } + + fn flush(&mut self, session: &mut Session, channel: ChannelId) { + session.data( + channel, + CryptoVec::from_slice(self.output_bytes.split().as_ref()), + ); + } +} + +type AsyncHandlerFn = Pin< + Box< + dyn Future::Error>> + Send, + >, +>; + +impl server::Handler for Handler { + type Error = anyhow::Error; + type FutureAuth = futures::future::Ready>; + type FutureUnit = AsyncHandlerFn; + type FutureBool = futures::future::Ready>; + + fn finished_auth(self, auth: Auth) -> Self::FutureAuth { + eprintln!("finished auth"); + futures::future::ready(Ok((self, auth))) + } + + fn finished_bool(self, b: bool, s: Session) -> Self::FutureBool { + eprintln!("finished bool"); + futures::future::ready(Ok((self, s, b))) + } + + fn finished(self, s: Session) -> Self::FutureUnit { + eprintln!("finished"); + Box::pin(futures::future::ready(Ok((self, s)))) + } + + fn shell_request(mut self, channel: ChannelId, mut session: Session) -> Self::FutureUnit { + Box::pin(async move { + write!(&mut self.output_bytes, "Hi there! You've successfully authenticated, but chartered does not provide shell access.\r\n")?; + self.flush(&mut session, channel); + session.close(channel); + Ok((self, session)) + }) + } + + fn exec_request( + mut self, + channel: ChannelId, + data: &[u8], + mut session: Session, + ) -> Self::FutureUnit { + eprintln!("exec {:x?}", data); + + let git_upload_pack = data.starts_with(b"git-upload-pack "); + + Box::pin(async move { + if git_upload_pack { + // TODO: check GIT_PROTOCOL=version=2 set + self.write(PktLine::Data(b"version 2\n"))?; + self.write(PktLine::Data(b"agent=chartered/0.1.0\n"))?; + self.write(PktLine::Data(b"ls-refs=unborn\n"))?; + self.write(PktLine::Data(b"fetch=shallow wait-for-done\n"))?; + self.write(PktLine::Data(b"server-option\n"))?; + self.write(PktLine::Data(b"object-info\n"))?; + self.write(PktLine::Flush)?; + self.flush(&mut session, channel); + } else { + session.data( + channel, + CryptoVec::from_slice(b"Sorry, I have no clue who you are\r\n"), + ); + session.close(channel); + } + + Ok((self, session)) + }) + } + + fn subsystem_request( + self, + _channel: ChannelId, + data: &str, + session: Session, + ) -> Self::FutureUnit { + eprintln!("subsystem req: {}", data); + Box::pin(futures::future::ready(Ok((self, session)))) + } + + fn auth_publickey(self, _: &str, _: &key::PublicKey) -> Self::FutureAuth { + eprintln!("finished auth pubkey"); + self.finished_auth(server::Auth::Accept) + } + + fn data(mut self, channel: ChannelId, data: &[u8], mut session: Session) -> Self::FutureUnit { + self.input_bytes.extend_from_slice(data); + + Box::pin(async move { + let mut ls_refs = false; + let mut fetch = false; + let mut done = false; + + while let Some(frame) = self.codec.decode(&mut self.input_bytes)? { + eprintln!("data: {:x?}", frame); + + if frame.as_ref() == "command=ls-refs".as_bytes() { + ls_refs = true; + } else if frame.as_ref() == "command=fetch".as_bytes() { + fetch = true; + } else if frame.as_ref() == "done".as_bytes() { + fetch = false; + done = true; + } + } + + // echo -ne "0012command=fetch\n0001000ethin-pack\n0010include-tag\n000eofs-delta\n0032want d24d8020163b5fee57c9babfd0c595b8c90ba253\n0009done\n" + + let test_crate_file = PackFileEntry::Blob(br#"{"name":"charteredtest","vers":"1.0.0","deps":[],"cksum":"7b821735f0211fd00032a9892d1bf2323c9d05d9c59b9303eb382f5ec1898bfc","features":{},"yanked":false,"links":null}"#); + let config_file = PackFileEntry::Blob( + br#"{ + "dl": "http://127.0.0.1:8888/api/v1/crates", + "api": "http://127.0.0.1:8888" + }"#, + ); + + let ch_ar_tree = PackFileEntry::Tree(vec![ + TreeItem { + kind: TreeItemKind::File, + name: "charteredtest", + hash: test_crate_file.hash()?, + } + ]); + + let ch_tree = PackFileEntry::Tree(vec![ + TreeItem { + kind: TreeItemKind::Directory, + name: "ar", + hash: ch_ar_tree.hash()?, + } + ]); + + let root_tree = PackFileEntry::Tree(vec![ + TreeItem { + kind: TreeItemKind::Directory, + name: "ch", + hash: ch_tree.hash()?, + }, + TreeItem { + kind: TreeItemKind::File, + name: "config.json", + hash: config_file.hash()?, + }, + ]); + + let commit_user = CommitUserInfo { + name: "Jordan Doyle", + email: "jordan@doyle.la", + time: chrono::Utc::now(), + }; + + let commit = PackFileEntry::Commit(Commit { + tree: root_tree.hash()?, + author: commit_user, + committer: commit_user, + message: "cool commit", + }); + + // echo -ne "0014command=ls-refs\n0014agent=git/2.321\n00010009peel\n000csymrefs\n000bunborn\n0014ref-prefix HEAD\n0019ref-prefix refs/HEAD\n001eref-prefix refs/tags/HEAD\n001fref-prefix refs/heads/HEAD\n0021ref-prefix refs/remotes/HEAD\n0026ref-prefix refs/remotes/HEAD/HEAD\n001aref-prefix refs/tags/\n0000" + // GIT_PROTOCOL=version=2 ssh -o SendEnv=GIT_PROTOCOL git@github.com git-upload-pack '/w4/chartered.git' + // ''.join([('{:04x}'.format(len(v) + 5)), v, "\n"]) + // echo -ne "0012command=fetch\n0001000ethin-pack\n0010no-progress\n0010include-tag\n000eofs-delta\n0032want f6046cf6372e0d8ab845f6dec1602c303a66ee91\n" + // sends a 000dpackfile back + // https://shafiul.github.io/gitbook/7_the_packfile.html + if ls_refs { + let commit_hash = hex::encode(&commit.hash()?); + self.write(PktLine::Data( + format!("{} HEAD symref-target:refs/heads/master\n", commit_hash).as_bytes(), + ))?; + self.write(PktLine::Flush)?; + self.flush(&mut session, channel); + } + + if fetch { + self.write(PktLine::Data(b"acknowledgments\n"))?; + self.write(PktLine::Data(b"ready\n"))?; + self.write(PktLine::Delimiter)?; + // self.write(PktLine::Data(b"shallow-info\n"))?; + // self.write(PktLine::Data(b"unshallow\n"))?; + done = true; + } + + if done { + self.write(PktLine::Data(b"packfile\n"))?; + + self.write(PktLine::SidebandMsg(b"Hello from chartered!\n"))?; + self.flush(&mut session, channel); + + let packfile = git::packfile::PackFile::new(vec![ + commit, + test_crate_file, + ch_tree, + ch_ar_tree, + config_file, + root_tree, + ]); + self.write(PktLine::SidebandData(packfile))?; + self.write(PktLine::Flush)?; + self.flush(&mut session, channel); + + session.exit_status_request(channel, 0); + session.eof(channel); + session.close(channel); + } + + Ok((self, session)) + }) + } +} diff --git a/chartered-web/src/main.rs b/chartered-web/src/main.rs new file mode 100644 index 0000000..7e3d561 100644 --- /dev/null +++ a/chartered-web/src/main.rs @@ -1,0 +1,4 @@ +#[tokio::main] +async fn main() { + println!("Hello, world!"); +} diff --git a/src/git/codec.rs b/src/git/codec.rs deleted file mode 100644 index 96ce563..0000000 100644 --- a/src/git/codec.rs +++ /dev/null @@ -1,102 +1,0 @@ -#![allow(clippy::module_name_repetitions)] - -use bytes::{Buf, Bytes, BytesMut}; -use tokio_util::codec; - -use super::PktLine; - -pub struct Encoder { - // buf: BytesMut, -} - -impl codec::Encoder> for Encoder { - type Error = anyhow::Error; - - fn encode(&mut self, item: PktLine<'_>, dst: &mut BytesMut) -> Result<(), Self::Error> { - item.encode_to(dst)?; - Ok(()) - } -} - -#[derive(Default)] -pub struct GitCodec; - -impl codec::Decoder for GitCodec { - type Item = Bytes; - type Error = anyhow::Error; - - fn decode(&mut self, src: &mut bytes::BytesMut) -> Result, Self::Error> { - if src.len() < 4 { - return Ok(None); - } - - let mut length_bytes = [0_u8; 4]; - length_bytes.copy_from_slice(&src[..4]); - let length = u16::from_str_radix(std::str::from_utf8(&length_bytes)?, 16)? as usize; - - if length == 0 // flush-pkt - || length == 1 // delim-pkt - || length == 2 - // response-end-pkt - { - eprintln!("pkt: {}", length); - src.advance(4); - return self.decode(src); - } - - if !(4..=65520).contains(&length) { - return Err( - std::io::Error::new(std::io::ErrorKind::InvalidData, "protocol abuse").into(), - ); - } - - if src.len() < length { - src.reserve(length - src.len()); - return Ok(None); - } - - let mut bytes = src.split_to(length); - bytes.advance(4); - - if bytes.ends_with(b"\n") { - bytes.truncate(bytes.len() - 1); - } - - Ok(Some(bytes.freeze())) - } -} - -#[cfg(test)] -mod test { - use bytes::BytesMut; - use std::fmt::Write; - use tokio_util::codec::Decoder; - - #[test] - fn decode() { - let mut codec = super::GitCodec; - - let mut bytes = BytesMut::new(); - - bytes.write_str("0015agent=git/2.32.0").unwrap(); - let res = codec.decode(&mut bytes).unwrap(); - assert_eq!(res, None); - - bytes.write_char('\n').unwrap(); - bytes.write_str("0002").unwrap(); - bytes.write_str("0004").unwrap(); - bytes.write_str("0005a").unwrap(); - - let res = codec.decode(&mut bytes).unwrap(); - assert_eq!(res.as_deref(), Some("agent=git/2.32.0".as_bytes())); - - let res = codec.decode(&mut bytes).unwrap(); - assert_eq!(res.as_deref(), Some("".as_bytes())); - - let res = codec.decode(&mut bytes).unwrap(); - assert_eq!(res.as_deref(), Some("a".as_bytes())); - - let res = codec.decode(&mut bytes).unwrap(); - assert_eq!(res.as_deref(), None); - } -} diff --git a/src/git/mod.rs b/src/git/mod.rs deleted file mode 100644 index e06d861..0000000 100644 --- a/src/git/mod.rs +++ /dev/null @@ -1,73 +1,0 @@ -pub mod codec; -pub mod packfile; - -use bytes::{BufMut, BytesMut}; -use std::fmt::Write; - -use self::packfile::PackFile; - -pub enum PktLine<'a> { - Data(&'a [u8]), - /// Similar to a data packet, but used during packfile sending to indicate this - /// packet is a block of data by appending a byte containing the u8 `1`. - SidebandData(PackFile<'a>), - /// Similar to a data packet, but used during packfile sending to indicate this - /// packet is a status message by appending a byte containing the u8 `2`. - SidebandMsg(&'a [u8]), - Flush, - Delimiter, - ResponseEnd, -} - -impl PktLine<'_> { - pub fn encode_to(&self, buf: &mut BytesMut) -> Result<(), anyhow::Error> { - match self { - Self::Data(data) => { - write!(buf, "{:04x}", data.len() + 4)?; - buf.extend_from_slice(data); - } - Self::SidebandData(packfile) => { - // split the buf off so the cost of counting the bytes to put in the - // data line prefix is just the cost of `unsplit` (an atomic decrement) - let mut data_buf = buf.split_off(buf.len()); - - data_buf.put_u8(1); // sideband, 1 = data - packfile.encode_to(&mut data_buf)?; - - // write into the buf not the data buf so it's at the start of the msg - write!(buf, "{:04x}", data_buf.len() + 4)?; - buf.unsplit(data_buf); - } - Self::SidebandMsg(msg) => { - write!(buf, "{:04x}", msg.len() + 4 + 1)?; - buf.put_u8(2); // sideband, 2 = msg - buf.extend_from_slice(msg); - } - Self::Flush => buf.extend_from_slice(b"0000"), - Self::Delimiter => buf.extend_from_slice(b"0001"), - Self::ResponseEnd => buf.extend_from_slice(b"0002"), - } - - Ok(()) - } -} - -impl<'a> From<&'a str> for PktLine<'a> { - fn from(val: &'a str) -> Self { - PktLine::Data(val.as_bytes()) - } -} - -#[cfg(test)] -mod test { - use bytes::BytesMut; - - #[test] - fn test_pkt_line() { - let mut buffer = BytesMut::new(); - super::PktLine::Data(b"agent=git/2.32.0\n") - .encode_to(&mut buffer) - .unwrap(); - assert_eq!(buffer.as_ref(), b"0015agent=git/2.32.0\n"); - } -} diff --git a/src/git/packfile.rs b/src/git/packfile.rs deleted file mode 100644 index 99fccd7..0000000 100644 --- a/src/git/packfile.rs +++ /dev/null @@ -1,322 +1,0 @@ -use bytes::{BufMut, BytesMut}; -use flate2::{write::ZlibEncoder, Compression}; -use sha1::{ - digest::{generic_array::GenericArray, FixedOutputDirty}, - Digest, Sha1, -}; -use std::{convert::TryInto, fmt::Write, io::Write as IoWrite}; - -// The packfile itself is a very simple format. There is a header, a -// series of packed objects (each with it's own header and body) and -// then a checksum trailer. The first four bytes is the string 'PACK', -// which is sort of used to make sure you're getting the start of the -// packfile correctly. This is followed by a 4-byte packfile version -// number and then a 4-byte number of entries in that file. -pub struct PackFile<'a> { - entries: Vec>, -} - -impl<'a> PackFile<'a> { - #[must_use] - pub fn new(entries: Vec>) -> Self { - Self { entries } - } - - #[must_use] - pub const fn header_size() -> usize { - "PACK".len() + std::mem::size_of::() + std::mem::size_of::() - } - - #[must_use] - pub const fn footer_size() -> usize { - 20 - } - - pub fn encode_to(&self, original_buf: &mut BytesMut) -> Result<(), anyhow::Error> { - let mut buf = original_buf.split_off(original_buf.len()); - buf.reserve(Self::header_size() + Self::footer_size()); - - // header - buf.extend_from_slice(b"PACK"); // magic header - buf.put_u32(2); // version - buf.put_u32(self.entries.len().try_into()?); // number of entries in the packfile - - // body - for entry in &self.entries { - entry.encode_to(&mut buf)?; - } - - // footer - buf.extend_from_slice(&sha1::Sha1::digest(&buf[..])); - - original_buf.unsplit(buf); - - Ok(()) - } -} - -pub struct Commit<'a> { - pub tree: GenericArray::OutputSize>, // [u8; 20], but sha-1 returns a GenericArray - // pub parent: [u8; 20], - pub author: CommitUserInfo<'a>, - pub committer: CommitUserInfo<'a>, - // pub gpgsig: &str, - pub message: &'a str, -} - -impl Commit<'_> { - fn encode_to(&self, out: &mut BytesMut) -> Result<(), anyhow::Error> { - let mut tree_hex = [0_u8; 20 * 2]; - hex::encode_to_slice(self.tree, &mut tree_hex)?; - - out.write_str("tree ")?; - out.extend_from_slice(&tree_hex); - out.write_char('\n')?; - - writeln!(out, "author {}", self.author.encode())?; - writeln!(out, "committer {}", self.committer.encode())?; - write!(out, "\n{}", self.message)?; - - Ok(()) - } - - #[must_use] - pub fn size(&self) -> usize { - let mut len = 0; - len += "tree ".len() + (self.tree.len() * 2) + "\n".len(); - len += "author ".len() + self.author.size() + "\n".len(); - len += "committer ".len() + self.committer.size() + "\n".len(); - len += "\n".len() + self.message.len(); - len - } -} - -#[derive(Copy, Clone, Debug)] -pub struct CommitUserInfo<'a> { - pub name: &'a str, - pub email: &'a str, - pub time: chrono::DateTime, -} - -impl CommitUserInfo<'_> { - fn encode(&self) -> String { - // TODO: remove `format!`, `format_args!`? - format!( - "{} <{}> {} +0000", - self.name, - self.email, - self.time.timestamp() - ) - } - - #[must_use] - pub fn size(&self) -> usize { - let timestamp_len = itoa::Buffer::new().format(self.time.timestamp()).len(); - - self.name.len() - + "< ".len() - + self.email.len() - + "> ".len() - + timestamp_len - + " +0000".len() - } -} - -pub enum TreeItemKind { - File, - Directory, -} - -impl TreeItemKind { - #[must_use] - pub const fn mode(&self) -> &'static str { - match self { - Self::File => "100644", - Self::Directory => "40000", - } - } -} - -pub struct TreeItem<'a> { - pub kind: TreeItemKind, - pub name: &'a str, - pub hash: GenericArray::OutputSize>, // [u8; 20] - but we have to deal with GenericArrays -} - -// `[mode] [name]\0[hash]` -impl TreeItem<'_> { - fn encode_to(&self, out: &mut BytesMut) -> Result<(), anyhow::Error> { - out.write_str(self.kind.mode())?; - write!(out, " {}\0", self.name)?; - out.extend_from_slice(&self.hash); - Ok(()) - } - - #[must_use] - pub fn size(&self) -> usize { - self.kind.mode().len() + " ".len() + self.name.len() + "\0".len() + self.hash.len() - } -} - -pub enum PackFileEntry<'a> { - // jordan@Jordans-MacBook-Pro-2 0d % printf "\x1f\x8b\x08\x00\x00\x00\x00\x00" | cat - f5/473259d9674ed66239766a013f96a3550374e3 | gzip -dc - // commit 1068tree 0d586b48bc42e8591773d3d8a7223551c39d453c - // parent c2a862612a14346ae95234f26efae1ee69b5b7a9 - // author Jordan Doyle 1630244577 +0100 - // committer Jordan Doyle 1630244577 +0100 - // gpgsig -----BEGIN PGP SIGNATURE----- - // - // iQIzBAABCAAdFiEEMn1zof7yzaURQBGDHqa65vZtxJoFAmErjuEACgkQHqa65vZt - // xJqhvhAAieKXnGRjT926qzozcvarC8D3TlA+Z1wVXueTAWqfusNIP0zCun/crOb2 - // tOULO+/DXVBmwu5eInAf+t/wvlnIsrzJonhVr1ZT0f0vDX6fs2vflWg4UCVEuTsZ - // tg+aTjcibwnmViIM9XVOzhU8Au2OIqMQLyQOMWSt8NhY0W2WhBCdQvhktvK1V8W6 - // omPs04SrR39xWBDQaxsXYxq/1ZKUYXDwudvEfv14EvrxG1vWumpUVJd7Ib5w4gXX - // fYa95DxYL720ZaiWPIYEG8FMBzSOpo6lUzY9g2/o/wKwSQZJNvpaMGCuouy8Fb+E - // UaqC0XPxqpKG9duXPgCldUr+P7++48CF5zc358RBGz5OCNeTREsIQQo5PUO1k+wO - // FnGOQTT8vvNOrxBgb3QgKu67RVwWDc6JnQCNpUrhUJrXMDWnYLBqo4Y+CdKGSQ4G - // hW8V/hVTOlJZNi8bbU4v53cxh4nXiMM6NKUblUKs65ar3/2dkojwunz7r7GVZ6mG - // QUpr9+ybG61XDqd1ad1A/B/i3WdWixTmJS3K/4uXjFjFX1f3RAk7O0gHc9I8HYOE - // Vd8UsHzLOWAUHeaqbsd6xx3GCXF4D5D++kh9OY9Ov7CXlqbYbHd6Atg+PQ7VnqNf - // bDqWN0Q2qcKX3k4ggtucmkkA6gP+K3+F5ANQj3AsGMQeddowC0Y= - // =fXoH - // -----END PGP SIGNATURE----- - // - // test - Commit(Commit<'a>), - // jordan@Jordans-MacBook-Pro-2 0d % printf "\x1f\x8b\x08\x00\x00\x00\x00\x00" | cat - 0d/586b48bc42e8591773d3d8a7223551c39d453c | gzip -dc - // tree 20940000 .cargo���CYy��Ve�������100644 .gitignore�K��_ow�]����4�n�ݺ100644 Cargo.lock�7�3-�?/�� - // kt��c0C�100644 Cargo.toml�6�&(��]\8@�SHA�]f40000 src0QW��ƅ���b[�!�S&N�100644 test�G2Y�gN�b9vj?��Ut� - Tree(Vec>), - // jordan@Jordans-MacBook-Pro-2 objects % printf "\x1f\x8b\x08\x00\x00\x00\x00\x00" | cat - f5/473259d9674ed66239766a013f96a3550374e3| gzip -dc - // blob 23try and find me in .git - Blob(&'a [u8]), - // Tag, - // OfsDelta, - // RefDelta, -} - -impl PackFileEntry<'_> { - fn write_header(&self, buf: &mut BytesMut) { - let mut size = self.uncompressed_size(); - - // write header - { - let mut val = 0b1000_0000_u8; - - val |= match self { - Self::Commit(_) => 0b001, - Self::Tree(_) => 0b010, - Self::Blob(_) => 0b011, - // Self::Tag => 0b100, - // Self::OfsDelta => 0b110, - // Self::RefDelta => 0b111, - } << 4; - - // pack the 4 LSBs of the size into the header - #[allow(clippy::cast_possible_truncation)] // value is masked - { - val |= (size & 0b1111) as u8; - } - size >>= 4; - - buf.put_u8(val); - } - - // write size bytes - while size != 0 { - // read 7 LSBs from the `size` and push them off for the next iteration - #[allow(clippy::cast_possible_truncation)] // value is masked - let mut val = (size & 0b111_1111) as u8; - size >>= 7; - - if size != 0 { - // MSB set to 1 implies there's more size bytes to come, otherwise - // the data starts after this byte - val |= 1 << 7; - } - - buf.put_u8(val); - } - } - - pub fn encode_to(&self, original_out: &mut BytesMut) -> Result<(), anyhow::Error> { - self.write_header(original_out); // TODO: this needs space reserving for it - - // todo is there a way to stream through the zlibencoder so we don't have to - // have this intermediate bytesmut and vec? - let mut out = BytesMut::new(); - - let size = self.uncompressed_size(); - original_out.reserve(size); - // the data ends up getting compressed but we'll need at least this many bytes - out.reserve(size); - - match self { - Self::Commit(commit) => { - commit.encode_to(&mut out)?; - } - Self::Tree(items) => { - for item in items { - item.encode_to(&mut out)?; - } - } - Self::Blob(data) => { - out.extend_from_slice(data); - } - } - - debug_assert_eq!(out.len(), size); - - let mut e = ZlibEncoder::new(Vec::new(), Compression::default()); - e.write_all(&out)?; - let compressed_data = e.finish()?; - - original_out.extend_from_slice(&compressed_data); - - Ok(()) - } - - #[must_use] - pub fn uncompressed_size(&self) -> usize { - match self { - Self::Commit(commit) => commit.size(), - Self::Tree(items) => items.iter().map(TreeItem::size).sum(), - Self::Blob(data) => data.len(), - } - } - - // wen const generics for RustCrypto? :-( - pub fn hash( - &self, - ) -> Result::OutputSize>, anyhow::Error> { - let size = self.uncompressed_size(); - - let file_prefix = match self { - Self::Commit(_) => "commit", - Self::Tree(_) => "tree", - Self::Blob(_) => "blob", - }; - - let size_len = itoa::Buffer::new().format(size).len(); - - let mut out = - BytesMut::with_capacity(file_prefix.len() + " ".len() + size_len + "\n".len() + size); - - write!(out, "{} {}\0", file_prefix, size)?; - match self { - Self::Commit(commit) => { - commit.encode_to(&mut out)?; - } - Self::Tree(items) => { - for item in items { - item.encode_to(&mut out)?; - } - } - Self::Blob(blob) => { - out.extend_from_slice(blob); - } - } - - Ok(sha1::Sha1::digest(&out)) - } -} diff --git a/chartered-git/src/git/codec.rs b/chartered-git/src/git/codec.rs new file mode 100644 index 0000000..96ce563 100644 --- /dev/null +++ a/chartered-git/src/git/codec.rs @@ -1,0 +1,102 @@ +#![allow(clippy::module_name_repetitions)] + +use bytes::{Buf, Bytes, BytesMut}; +use tokio_util::codec; + +use super::PktLine; + +pub struct Encoder { + // buf: BytesMut, +} + +impl codec::Encoder> for Encoder { + type Error = anyhow::Error; + + fn encode(&mut self, item: PktLine<'_>, dst: &mut BytesMut) -> Result<(), Self::Error> { + item.encode_to(dst)?; + Ok(()) + } +} + +#[derive(Default)] +pub struct GitCodec; + +impl codec::Decoder for GitCodec { + type Item = Bytes; + type Error = anyhow::Error; + + fn decode(&mut self, src: &mut bytes::BytesMut) -> Result, Self::Error> { + if src.len() < 4 { + return Ok(None); + } + + let mut length_bytes = [0_u8; 4]; + length_bytes.copy_from_slice(&src[..4]); + let length = u16::from_str_radix(std::str::from_utf8(&length_bytes)?, 16)? as usize; + + if length == 0 // flush-pkt + || length == 1 // delim-pkt + || length == 2 + // response-end-pkt + { + eprintln!("pkt: {}", length); + src.advance(4); + return self.decode(src); + } + + if !(4..=65520).contains(&length) { + return Err( + std::io::Error::new(std::io::ErrorKind::InvalidData, "protocol abuse").into(), + ); + } + + if src.len() < length { + src.reserve(length - src.len()); + return Ok(None); + } + + let mut bytes = src.split_to(length); + bytes.advance(4); + + if bytes.ends_with(b"\n") { + bytes.truncate(bytes.len() - 1); + } + + Ok(Some(bytes.freeze())) + } +} + +#[cfg(test)] +mod test { + use bytes::BytesMut; + use std::fmt::Write; + use tokio_util::codec::Decoder; + + #[test] + fn decode() { + let mut codec = super::GitCodec; + + let mut bytes = BytesMut::new(); + + bytes.write_str("0015agent=git/2.32.0").unwrap(); + let res = codec.decode(&mut bytes).unwrap(); + assert_eq!(res, None); + + bytes.write_char('\n').unwrap(); + bytes.write_str("0002").unwrap(); + bytes.write_str("0004").unwrap(); + bytes.write_str("0005a").unwrap(); + + let res = codec.decode(&mut bytes).unwrap(); + assert_eq!(res.as_deref(), Some("agent=git/2.32.0".as_bytes())); + + let res = codec.decode(&mut bytes).unwrap(); + assert_eq!(res.as_deref(), Some("".as_bytes())); + + let res = codec.decode(&mut bytes).unwrap(); + assert_eq!(res.as_deref(), Some("a".as_bytes())); + + let res = codec.decode(&mut bytes).unwrap(); + assert_eq!(res.as_deref(), None); + } +} diff --git a/chartered-git/src/git/mod.rs b/chartered-git/src/git/mod.rs new file mode 100644 index 0000000..e06d861 100644 --- /dev/null +++ a/chartered-git/src/git/mod.rs @@ -1,0 +1,73 @@ +pub mod codec; +pub mod packfile; + +use bytes::{BufMut, BytesMut}; +use std::fmt::Write; + +use self::packfile::PackFile; + +pub enum PktLine<'a> { + Data(&'a [u8]), + /// Similar to a data packet, but used during packfile sending to indicate this + /// packet is a block of data by appending a byte containing the u8 `1`. + SidebandData(PackFile<'a>), + /// Similar to a data packet, but used during packfile sending to indicate this + /// packet is a status message by appending a byte containing the u8 `2`. + SidebandMsg(&'a [u8]), + Flush, + Delimiter, + ResponseEnd, +} + +impl PktLine<'_> { + pub fn encode_to(&self, buf: &mut BytesMut) -> Result<(), anyhow::Error> { + match self { + Self::Data(data) => { + write!(buf, "{:04x}", data.len() + 4)?; + buf.extend_from_slice(data); + } + Self::SidebandData(packfile) => { + // split the buf off so the cost of counting the bytes to put in the + // data line prefix is just the cost of `unsplit` (an atomic decrement) + let mut data_buf = buf.split_off(buf.len()); + + data_buf.put_u8(1); // sideband, 1 = data + packfile.encode_to(&mut data_buf)?; + + // write into the buf not the data buf so it's at the start of the msg + write!(buf, "{:04x}", data_buf.len() + 4)?; + buf.unsplit(data_buf); + } + Self::SidebandMsg(msg) => { + write!(buf, "{:04x}", msg.len() + 4 + 1)?; + buf.put_u8(2); // sideband, 2 = msg + buf.extend_from_slice(msg); + } + Self::Flush => buf.extend_from_slice(b"0000"), + Self::Delimiter => buf.extend_from_slice(b"0001"), + Self::ResponseEnd => buf.extend_from_slice(b"0002"), + } + + Ok(()) + } +} + +impl<'a> From<&'a str> for PktLine<'a> { + fn from(val: &'a str) -> Self { + PktLine::Data(val.as_bytes()) + } +} + +#[cfg(test)] +mod test { + use bytes::BytesMut; + + #[test] + fn test_pkt_line() { + let mut buffer = BytesMut::new(); + super::PktLine::Data(b"agent=git/2.32.0\n") + .encode_to(&mut buffer) + .unwrap(); + assert_eq!(buffer.as_ref(), b"0015agent=git/2.32.0\n"); + } +} diff --git a/chartered-git/src/git/packfile.rs b/chartered-git/src/git/packfile.rs new file mode 100644 index 0000000..99fccd7 100644 --- /dev/null +++ a/chartered-git/src/git/packfile.rs @@ -1,0 +1,322 @@ +use bytes::{BufMut, BytesMut}; +use flate2::{write::ZlibEncoder, Compression}; +use sha1::{ + digest::{generic_array::GenericArray, FixedOutputDirty}, + Digest, Sha1, +}; +use std::{convert::TryInto, fmt::Write, io::Write as IoWrite}; + +// The packfile itself is a very simple format. There is a header, a +// series of packed objects (each with it's own header and body) and +// then a checksum trailer. The first four bytes is the string 'PACK', +// which is sort of used to make sure you're getting the start of the +// packfile correctly. This is followed by a 4-byte packfile version +// number and then a 4-byte number of entries in that file. +pub struct PackFile<'a> { + entries: Vec>, +} + +impl<'a> PackFile<'a> { + #[must_use] + pub fn new(entries: Vec>) -> Self { + Self { entries } + } + + #[must_use] + pub const fn header_size() -> usize { + "PACK".len() + std::mem::size_of::() + std::mem::size_of::() + } + + #[must_use] + pub const fn footer_size() -> usize { + 20 + } + + pub fn encode_to(&self, original_buf: &mut BytesMut) -> Result<(), anyhow::Error> { + let mut buf = original_buf.split_off(original_buf.len()); + buf.reserve(Self::header_size() + Self::footer_size()); + + // header + buf.extend_from_slice(b"PACK"); // magic header + buf.put_u32(2); // version + buf.put_u32(self.entries.len().try_into()?); // number of entries in the packfile + + // body + for entry in &self.entries { + entry.encode_to(&mut buf)?; + } + + // footer + buf.extend_from_slice(&sha1::Sha1::digest(&buf[..])); + + original_buf.unsplit(buf); + + Ok(()) + } +} + +pub struct Commit<'a> { + pub tree: GenericArray::OutputSize>, // [u8; 20], but sha-1 returns a GenericArray + // pub parent: [u8; 20], + pub author: CommitUserInfo<'a>, + pub committer: CommitUserInfo<'a>, + // pub gpgsig: &str, + pub message: &'a str, +} + +impl Commit<'_> { + fn encode_to(&self, out: &mut BytesMut) -> Result<(), anyhow::Error> { + let mut tree_hex = [0_u8; 20 * 2]; + hex::encode_to_slice(self.tree, &mut tree_hex)?; + + out.write_str("tree ")?; + out.extend_from_slice(&tree_hex); + out.write_char('\n')?; + + writeln!(out, "author {}", self.author.encode())?; + writeln!(out, "committer {}", self.committer.encode())?; + write!(out, "\n{}", self.message)?; + + Ok(()) + } + + #[must_use] + pub fn size(&self) -> usize { + let mut len = 0; + len += "tree ".len() + (self.tree.len() * 2) + "\n".len(); + len += "author ".len() + self.author.size() + "\n".len(); + len += "committer ".len() + self.committer.size() + "\n".len(); + len += "\n".len() + self.message.len(); + len + } +} + +#[derive(Copy, Clone, Debug)] +pub struct CommitUserInfo<'a> { + pub name: &'a str, + pub email: &'a str, + pub time: chrono::DateTime, +} + +impl CommitUserInfo<'_> { + fn encode(&self) -> String { + // TODO: remove `format!`, `format_args!`? + format!( + "{} <{}> {} +0000", + self.name, + self.email, + self.time.timestamp() + ) + } + + #[must_use] + pub fn size(&self) -> usize { + let timestamp_len = itoa::Buffer::new().format(self.time.timestamp()).len(); + + self.name.len() + + "< ".len() + + self.email.len() + + "> ".len() + + timestamp_len + + " +0000".len() + } +} + +pub enum TreeItemKind { + File, + Directory, +} + +impl TreeItemKind { + #[must_use] + pub const fn mode(&self) -> &'static str { + match self { + Self::File => "100644", + Self::Directory => "40000", + } + } +} + +pub struct TreeItem<'a> { + pub kind: TreeItemKind, + pub name: &'a str, + pub hash: GenericArray::OutputSize>, // [u8; 20] - but we have to deal with GenericArrays +} + +// `[mode] [name]\0[hash]` +impl TreeItem<'_> { + fn encode_to(&self, out: &mut BytesMut) -> Result<(), anyhow::Error> { + out.write_str(self.kind.mode())?; + write!(out, " {}\0", self.name)?; + out.extend_from_slice(&self.hash); + Ok(()) + } + + #[must_use] + pub fn size(&self) -> usize { + self.kind.mode().len() + " ".len() + self.name.len() + "\0".len() + self.hash.len() + } +} + +pub enum PackFileEntry<'a> { + // jordan@Jordans-MacBook-Pro-2 0d % printf "\x1f\x8b\x08\x00\x00\x00\x00\x00" | cat - f5/473259d9674ed66239766a013f96a3550374e3 | gzip -dc + // commit 1068tree 0d586b48bc42e8591773d3d8a7223551c39d453c + // parent c2a862612a14346ae95234f26efae1ee69b5b7a9 + // author Jordan Doyle 1630244577 +0100 + // committer Jordan Doyle 1630244577 +0100 + // gpgsig -----BEGIN PGP SIGNATURE----- + // + // iQIzBAABCAAdFiEEMn1zof7yzaURQBGDHqa65vZtxJoFAmErjuEACgkQHqa65vZt + // xJqhvhAAieKXnGRjT926qzozcvarC8D3TlA+Z1wVXueTAWqfusNIP0zCun/crOb2 + // tOULO+/DXVBmwu5eInAf+t/wvlnIsrzJonhVr1ZT0f0vDX6fs2vflWg4UCVEuTsZ + // tg+aTjcibwnmViIM9XVOzhU8Au2OIqMQLyQOMWSt8NhY0W2WhBCdQvhktvK1V8W6 + // omPs04SrR39xWBDQaxsXYxq/1ZKUYXDwudvEfv14EvrxG1vWumpUVJd7Ib5w4gXX + // fYa95DxYL720ZaiWPIYEG8FMBzSOpo6lUzY9g2/o/wKwSQZJNvpaMGCuouy8Fb+E + // UaqC0XPxqpKG9duXPgCldUr+P7++48CF5zc358RBGz5OCNeTREsIQQo5PUO1k+wO + // FnGOQTT8vvNOrxBgb3QgKu67RVwWDc6JnQCNpUrhUJrXMDWnYLBqo4Y+CdKGSQ4G + // hW8V/hVTOlJZNi8bbU4v53cxh4nXiMM6NKUblUKs65ar3/2dkojwunz7r7GVZ6mG + // QUpr9+ybG61XDqd1ad1A/B/i3WdWixTmJS3K/4uXjFjFX1f3RAk7O0gHc9I8HYOE + // Vd8UsHzLOWAUHeaqbsd6xx3GCXF4D5D++kh9OY9Ov7CXlqbYbHd6Atg+PQ7VnqNf + // bDqWN0Q2qcKX3k4ggtucmkkA6gP+K3+F5ANQj3AsGMQeddowC0Y= + // =fXoH + // -----END PGP SIGNATURE----- + // + // test + Commit(Commit<'a>), + // jordan@Jordans-MacBook-Pro-2 0d % printf "\x1f\x8b\x08\x00\x00\x00\x00\x00" | cat - 0d/586b48bc42e8591773d3d8a7223551c39d453c | gzip -dc + // tree 20940000 .cargo���CYy��Ve�������100644 .gitignore�K��_ow�]����4�n�ݺ100644 Cargo.lock�7�3-�?/�� + // kt��c0C�100644 Cargo.toml�6�&(��]\8@�SHA�]f40000 src0QW��ƅ���b[�!�S&N�100644 test�G2Y�gN�b9vj?��Ut� + Tree(Vec>), + // jordan@Jordans-MacBook-Pro-2 objects % printf "\x1f\x8b\x08\x00\x00\x00\x00\x00" | cat - f5/473259d9674ed66239766a013f96a3550374e3| gzip -dc + // blob 23try and find me in .git + Blob(&'a [u8]), + // Tag, + // OfsDelta, + // RefDelta, +} + +impl PackFileEntry<'_> { + fn write_header(&self, buf: &mut BytesMut) { + let mut size = self.uncompressed_size(); + + // write header + { + let mut val = 0b1000_0000_u8; + + val |= match self { + Self::Commit(_) => 0b001, + Self::Tree(_) => 0b010, + Self::Blob(_) => 0b011, + // Self::Tag => 0b100, + // Self::OfsDelta => 0b110, + // Self::RefDelta => 0b111, + } << 4; + + // pack the 4 LSBs of the size into the header + #[allow(clippy::cast_possible_truncation)] // value is masked + { + val |= (size & 0b1111) as u8; + } + size >>= 4; + + buf.put_u8(val); + } + + // write size bytes + while size != 0 { + // read 7 LSBs from the `size` and push them off for the next iteration + #[allow(clippy::cast_possible_truncation)] // value is masked + let mut val = (size & 0b111_1111) as u8; + size >>= 7; + + if size != 0 { + // MSB set to 1 implies there's more size bytes to come, otherwise + // the data starts after this byte + val |= 1 << 7; + } + + buf.put_u8(val); + } + } + + pub fn encode_to(&self, original_out: &mut BytesMut) -> Result<(), anyhow::Error> { + self.write_header(original_out); // TODO: this needs space reserving for it + + // todo is there a way to stream through the zlibencoder so we don't have to + // have this intermediate bytesmut and vec? + let mut out = BytesMut::new(); + + let size = self.uncompressed_size(); + original_out.reserve(size); + // the data ends up getting compressed but we'll need at least this many bytes + out.reserve(size); + + match self { + Self::Commit(commit) => { + commit.encode_to(&mut out)?; + } + Self::Tree(items) => { + for item in items { + item.encode_to(&mut out)?; + } + } + Self::Blob(data) => { + out.extend_from_slice(data); + } + } + + debug_assert_eq!(out.len(), size); + + let mut e = ZlibEncoder::new(Vec::new(), Compression::default()); + e.write_all(&out)?; + let compressed_data = e.finish()?; + + original_out.extend_from_slice(&compressed_data); + + Ok(()) + } + + #[must_use] + pub fn uncompressed_size(&self) -> usize { + match self { + Self::Commit(commit) => commit.size(), + Self::Tree(items) => items.iter().map(TreeItem::size).sum(), + Self::Blob(data) => data.len(), + } + } + + // wen const generics for RustCrypto? :-( + pub fn hash( + &self, + ) -> Result::OutputSize>, anyhow::Error> { + let size = self.uncompressed_size(); + + let file_prefix = match self { + Self::Commit(_) => "commit", + Self::Tree(_) => "tree", + Self::Blob(_) => "blob", + }; + + let size_len = itoa::Buffer::new().format(size).len(); + + let mut out = + BytesMut::with_capacity(file_prefix.len() + " ".len() + size_len + "\n".len() + size); + + write!(out, "{} {}\0", file_prefix, size)?; + match self { + Self::Commit(commit) => { + commit.encode_to(&mut out)?; + } + Self::Tree(items) => { + for item in items { + item.encode_to(&mut out)?; + } + } + Self::Blob(blob) => { + out.extend_from_slice(blob); + } + } + + Ok(sha1::Sha1::digest(&out)) + } +} -- rgit 0.1.3