Split git and web binaries
Diff
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(-)
@@ -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",
@@ -1,31 +1,5 @@
[package]
name = "chartered"
version = "0.1.0"
edition = "2018"
[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",
]
@@ -1,0 +1,25 @@
[package]
name = "chartered-git"
version = "0.1.0"
edition = "2018"
[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"] }
@@ -1,0 +1,12 @@
[package]
name = "chartered-web"
version = "0.1.0"
edition = "2018"
[dependencies]
axum = "0.2"
tokio = { version = "1", features = ["full"] }
tower = { version = "0.4", features = ["util", "filter"] }
tower-http = { version = "0.1", features = ["trace"] }
@@ -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)]
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<std::net::SocketAddr>) -> 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<Output = Result<(Handler, Session), <Handler as server::Handler>::Error>> + Send,
>,
>;
impl server::Handler for Handler {
type Error = anyhow::Error;
type FutureAuth = futures::future::Ready<Result<(Self, server::Auth), anyhow::Error>>;
type FutureUnit = AsyncHandlerFn;
type FutureBool = futures::future::Ready<Result<(Self, Session, bool), anyhow::Error>>;
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 {
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;
}
}
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",
});
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)?;
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))
})
}
}
@@ -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)]
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<std::net::SocketAddr>) -> 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<Output = Result<(Handler, Session), <Handler as server::Handler>::Error>> + Send,
>,
>;
impl server::Handler for Handler {
type Error = anyhow::Error;
type FutureAuth = futures::future::Ready<Result<(Self, server::Auth), anyhow::Error>>;
type FutureUnit = AsyncHandlerFn;
type FutureBool = futures::future::Ready<Result<(Self, Session, bool), anyhow::Error>>;
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 {
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;
}
}
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",
});
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)?;
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))
})
}
}
@@ -1,0 +1,4 @@
#[tokio::main]
async fn main() {
println!("Hello, world!");
}
@@ -1,102 +1,0 @@
#![allow(clippy::module_name_repetitions)]
use bytes::{Buf, Bytes, BytesMut};
use tokio_util::codec;
use super::PktLine;
pub struct Encoder {
}
impl codec::Encoder<PktLine<'_>> 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<Option<Self::Item>, 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
|| length == 1
|| length == 2
{
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);
}
}
@@ -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]),
SidebandData(PackFile<'a>),
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) => {
let mut data_buf = buf.split_off(buf.len());
data_buf.put_u8(1);
packfile.encode_to(&mut data_buf)?;
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);
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");
}
}
@@ -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};
pub struct PackFile<'a> {
entries: Vec<PackFileEntry<'a>>,
}
impl<'a> PackFile<'a> {
#[must_use]
pub fn new(entries: Vec<PackFileEntry<'a>>) -> Self {
Self { entries }
}
#[must_use]
pub const fn header_size() -> usize {
"PACK".len() + std::mem::size_of::<u32>() + std::mem::size_of::<u32>()
}
#[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());
buf.extend_from_slice(b"PACK");
buf.put_u32(2);
buf.put_u32(self.entries.len().try_into()?);
for entry in &self.entries {
entry.encode_to(&mut buf)?;
}
buf.extend_from_slice(&sha1::Sha1::digest(&buf[..]));
original_buf.unsplit(buf);
Ok(())
}
}
pub struct Commit<'a> {
pub tree: GenericArray<u8, <Sha1 as FixedOutputDirty>::OutputSize>,
pub author: CommitUserInfo<'a>,
pub committer: CommitUserInfo<'a>,
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<chrono::Utc>,
}
impl CommitUserInfo<'_> {
fn encode(&self) -> String {
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<u8, <Sha1 as FixedOutputDirty>::OutputSize>,
}
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> {
Commit(Commit<'a>),
Tree(Vec<TreeItem<'a>>),
Blob(&'a [u8]),
}
impl PackFileEntry<'_> {
fn write_header(&self, buf: &mut BytesMut) {
let mut size = self.uncompressed_size();
{
let mut val = 0b1000_0000_u8;
val |= match self {
Self::Commit(_) => 0b001,
Self::Tree(_) => 0b010,
Self::Blob(_) => 0b011,
} << 4;
#[allow(clippy::cast_possible_truncation)]
{
val |= (size & 0b1111) as u8;
}
size >>= 4;
buf.put_u8(val);
}
while size != 0 {
#[allow(clippy::cast_possible_truncation)]
let mut val = (size & 0b111_1111) as u8;
size >>= 7;
if size != 0 {
val |= 1 << 7;
}
buf.put_u8(val);
}
}
pub fn encode_to(&self, original_out: &mut BytesMut) -> Result<(), anyhow::Error> {
self.write_header(original_out);
let mut out = BytesMut::new();
let size = self.uncompressed_size();
original_out.reserve(size);
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(),
}
}
pub fn hash(
&self,
) -> Result<GenericArray<u8, <Sha1 as FixedOutputDirty>::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))
}
}
@@ -1,0 +1,102 @@
#![allow(clippy::module_name_repetitions)]
use bytes::{Buf, Bytes, BytesMut};
use tokio_util::codec;
use super::PktLine;
pub struct Encoder {
}
impl codec::Encoder<PktLine<'_>> 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<Option<Self::Item>, 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
|| length == 1
|| length == 2
{
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);
}
}
@@ -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]),
SidebandData(PackFile<'a>),
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) => {
let mut data_buf = buf.split_off(buf.len());
data_buf.put_u8(1);
packfile.encode_to(&mut data_buf)?;
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);
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");
}
}
@@ -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};
pub struct PackFile<'a> {
entries: Vec<PackFileEntry<'a>>,
}
impl<'a> PackFile<'a> {
#[must_use]
pub fn new(entries: Vec<PackFileEntry<'a>>) -> Self {
Self { entries }
}
#[must_use]
pub const fn header_size() -> usize {
"PACK".len() + std::mem::size_of::<u32>() + std::mem::size_of::<u32>()
}
#[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());
buf.extend_from_slice(b"PACK");
buf.put_u32(2);
buf.put_u32(self.entries.len().try_into()?);
for entry in &self.entries {
entry.encode_to(&mut buf)?;
}
buf.extend_from_slice(&sha1::Sha1::digest(&buf[..]));
original_buf.unsplit(buf);
Ok(())
}
}
pub struct Commit<'a> {
pub tree: GenericArray<u8, <Sha1 as FixedOutputDirty>::OutputSize>,
pub author: CommitUserInfo<'a>,
pub committer: CommitUserInfo<'a>,
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<chrono::Utc>,
}
impl CommitUserInfo<'_> {
fn encode(&self) -> String {
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<u8, <Sha1 as FixedOutputDirty>::OutputSize>,
}
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> {
Commit(Commit<'a>),
Tree(Vec<TreeItem<'a>>),
Blob(&'a [u8]),
}
impl PackFileEntry<'_> {
fn write_header(&self, buf: &mut BytesMut) {
let mut size = self.uncompressed_size();
{
let mut val = 0b1000_0000_u8;
val |= match self {
Self::Commit(_) => 0b001,
Self::Tree(_) => 0b010,
Self::Blob(_) => 0b011,
} << 4;
#[allow(clippy::cast_possible_truncation)]
{
val |= (size & 0b1111) as u8;
}
size >>= 4;
buf.put_u8(val);
}
while size != 0 {
#[allow(clippy::cast_possible_truncation)]
let mut val = (size & 0b111_1111) as u8;
size >>= 7;
if size != 0 {
val |= 1 << 7;
}
buf.put_u8(val);
}
}
pub fn encode_to(&self, original_out: &mut BytesMut) -> Result<(), anyhow::Error> {
self.write_header(original_out);
let mut out = BytesMut::new();
let size = self.uncompressed_size();
original_out.reserve(size);
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(),
}
}
pub fn hash(
&self,
) -> Result<GenericArray<u8, <Sha1 as FixedOutputDirty>::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))
}
}