Diff
Cargo.lock | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
chartered-git/Cargo.toml | 2 ++
chartered-git/src/main.rs | 39 +++++++++++++++------------------------
chartered-git/src/tree.rs | 26 +++++++++++++-------------
chartered-git/src/command_handlers/fetch.rs | 16 +++++++---------
chartered-git/src/command_handlers/ls_refs.rs | 6 ++----
chartered-git/src/git/codec.rs | 138 --------------------------------------------------------------------------------
chartered-git/src/git/mod.rs | 74 --------------------------------------------------------------------------
chartered-git/src/git/packfile/high_level.rs | 183 --------------------------------------------------------------------------------
chartered-git/src/git/packfile/low_level.rs | 327 --------------------------------------------------------------------------------
chartered-git/src/git/packfile/mod.rs | 2 --
chartered-web/src/endpoints/web_api/organisations/info.rs | 2 +-
12 files changed, 119 insertions(+), 786 deletions(-)
@@ -32,6 +32,17 @@
]
[[package]]
name = "ahash"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
dependencies = [
"getrandom",
"once_cell",
"version_check",
]
[[package]]
name = "aho-corasick"
version = "0.7.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -118,7 +129,7 @@
"http",
"hyper",
"ring",
"time 0.3.14",
"time 0.3.15",
"tokio",
"tower",
"tracing",
@@ -251,7 +262,7 @@
"percent-encoding",
"regex",
"ring",
"time 0.3.14",
"time 0.3.15",
"tracing",
]
@@ -378,7 +389,7 @@
"itoa",
"num-integer",
"ryu",
"time 0.3.14",
"time 0.3.15",
]
[[package]]
@@ -704,6 +715,7 @@
"indexmap",
"indoc",
"itoa",
"packfile",
"serde",
"serde_json",
"sha-1",
@@ -716,6 +728,7 @@
"tracing",
"tracing-subscriber",
"url",
"ustr",
]
[[package]]
@@ -973,7 +986,7 @@
"hashbrown",
"lock_api",
"once_cell",
"parking_lot_core",
"parking_lot_core 0.9.3",
]
[[package]]
@@ -1301,7 +1314,7 @@
"futures-timer",
"no-std-compat",
"nonzero_ext",
"parking_lot",
"parking_lot 0.12.1",
"quanta",
"rand",
"smallvec",
@@ -2021,6 +2034,35 @@
version = "6.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff"
[[package]]
name = "packfile"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eeb3adcc3e5e1d0bf59cd1a1bc0a663497c7f89a4dc1632d8568c15da66dbef"
dependencies = [
"bytes",
"flate2",
"hex",
"indexmap",
"itoa",
"sha1",
"thiserror",
"time 0.3.15",
"tokio-util",
"tracing",
]
[[package]]
name = "parking_lot"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
dependencies = [
"instant",
"lock_api",
"parking_lot_core 0.8.5",
]
[[package]]
name = "parking_lot"
@@ -2029,7 +2071,21 @@
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
dependencies = [
"lock_api",
"parking_lot_core",
"parking_lot_core 0.9.3",
]
[[package]]
name = "parking_lot_core"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216"
dependencies = [
"cfg-if",
"instant",
"libc",
"redox_syscall",
"smallvec",
"winapi",
]
[[package]]
@@ -2204,7 +2260,7 @@
checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93"
dependencies = [
"log",
"parking_lot",
"parking_lot 0.12.1",
"scheduled-thread-pool",
]
@@ -2436,7 +2492,7 @@
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "977a7519bff143a44f842fd07e80ad1329295bd71686457f18e496736f4bf9bf"
dependencies = [
"parking_lot",
"parking_lot 0.12.1",
]
[[package]]
@@ -2815,9 +2871,9 @@
[[package]]
name = "time"
version = "0.3.14"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c3f9a28b618c3a6b9251b6908e9c99e04b9e5c02e6581ccbb67d59c34ef7f9b"
checksum = "d634a985c4d4238ec39cacaed2e7ae552fbd3c476b552c1deac3021b7d7eaf0c"
dependencies = [
"libc",
"num_threads",
@@ -2851,7 +2907,7 @@
"mio",
"num_cpus",
"once_cell",
"parking_lot",
"parking_lot 0.12.1",
"pin-project-lite",
"signal-hook-registry",
"socket2",
@@ -3116,6 +3172,18 @@
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68b90931029ab9b034b300b797048cf23723400aa757e8a2bfb9d748102f9821"
[[package]]
name = "ustr"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "371436099f2980de56dc385b615696d3eabbdac9649a72b85f9d75f68474fa9c"
dependencies = [
"ahash",
"byteorder",
"lazy_static",
"parking_lot 0.11.2",
]
[[package]]
name = "uuid"
@@ -24,6 +24,7 @@
indexmap = "1"
indoc = "1.0"
itoa = "1"
packfile = "0.1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
shlex = "1"
@@ -36,6 +37,7 @@
tracing = "0.1"
tracing-subscriber = "0.3"
url = "2"
ustr = "0.9"
[features]
sqlite = ["chartered-db/sqlite"]
@@ -5,24 +5,17 @@
mod generators;
mod tree;
#[allow(clippy::missing_errors_doc)]
pub mod git;
use crate::{
generators::CargoConfig,
git::{
codec::{Encoder, GitCodec},
packfile::high_level::GitRepository,
PktLine,
},
tree::Tree,
};
use crate::{generators::CargoConfig, tree::Tree};
use arrayvec::ArrayVec;
use bytes::BytesMut;
use chartered_db::server_private_key::ServerPrivateKey;
use clap::Parser;
use futures::future::Future;
use packfile::{
codec::{Encoder, GitCodec},
high_level::GitRepository,
PktLine,
};
use std::{fmt::Write, path::PathBuf, pin::Pin, sync::Arc};
use thrussh::{
server::{self, Auth, Session},
@@ -85,7 +78,7 @@
let server = Server {
db,
config: Arc::new(config),
config: Box::leak(Box::new(config)),
};
info!("SSH server listening on {}", bind_address);
@@ -98,7 +91,7 @@
#[derive(Clone)]
struct Server {
db: chartered_db::ConnectionPool,
config: Arc<config::Config>,
config: &'static config::Config,
}
impl server::Server for Server {
@@ -113,7 +106,7 @@
Handler {
ip,
span,
config: self.config.clone(),
config: self.config,
codec: GitCodec::default(),
input_bytes: BytesMut::default(),
output_bytes: BytesMut::default(),
@@ -129,7 +122,7 @@
ip: Option<std::net::SocketAddr>,
span: tracing::Span,
codec: GitCodec,
config: Arc<config::Config>,
config: &'static config::Config,
input_bytes: BytesMut,
output_bytes: BytesMut,
db: chartered_db::ConnectionPool,
@@ -145,7 +138,7 @@
impl Handler {
fn write(&mut self, packet: PktLine<'_>) -> Result<(), anyhow::Error> {
Encoder {}.encode(packet, &mut self.output_bytes)
Ok(Encoder.encode(packet, &mut self.output_bytes)?)
}
fn flush(&mut self, session: &mut Session, channel: ChannelId) {
@@ -275,7 +268,7 @@
let config =
CargoConfig::new(&self.config.web_base_uri, &authed.auth_key, org_name);
let config = serde_json::to_vec(&config)?;
packfile.insert(ArrayVec::<_, 0>::new(), "config.json", &config)?;
packfile.insert(&[], "config.json", config.into())?;
@@ -284,15 +277,13 @@
let tree =
Tree::build(self.db.clone(), authed.user.id, org_name.to_string()).await;
tree.write_to_packfile(&mut packfile)?;
let config = self.config.clone();
let (commit_hash, packfile_entries) = packfile.commit(
&config.committer.name,
&config.committer.email,
&config.committer.message,
&self.config.committer.name,
&self.config.committer.email,
&self.config.committer.message,
)?;
match frame.command.as_ref() {
@@ -1,11 +1,14 @@
use crate::git::packfile::high_level::GitRepository;
use std::{collections::BTreeMap, sync::Arc};
use arrayvec::ArrayVec;
use bytes::Bytes;
use chartered_db::crates::Crate;
use std::collections::BTreeMap;
use packfile::high_level::GitRepository;
use ustr::ustr;
#[derive(serde::Serialize)]
pub struct CrateFileEntry<'a> {
@@ -16,7 +19,7 @@
}
pub struct Tree {
crates: BTreeMap<String, String>,
crates: BTreeMap<Arc<str>, Bytes>,
}
impl Tree {
@@ -50,20 +53,17 @@
}
crates.insert(crate_def.name, file);
crates.insert(crate_def.name.into(), file.into());
}
Self { crates }
}
pub fn write_to_packfile<'a>(
&'a self,
repo: &mut GitRepository<'a>,
) -> Result<(), anyhow::Error> {
pub fn write_to_packfile(&self, repo: &mut GitRepository) -> Result<(), anyhow::Error> {
for (name, content) in &self.crates {
let crate_folder = get_crate_folder(name);
repo.insert(crate_folder, name, content.as_bytes())?;
repo.insert(&crate_folder, name.clone(), content.clone())?;
}
Ok(())
@@ -74,7 +74,7 @@
fn get_crate_folder(crate_name: &str) -> ArrayVec<&str, 2> {
fn get_crate_folder(crate_name: &str) -> ArrayVec<&'static str, 2> {
let mut folders = ArrayVec::new();
match crate_name.len() {
@@ -83,11 +83,11 @@
2 => folders.push("2"),
3 => {
folders.push("3");
folders.push(&crate_name[..1]);
folders.push(ustr(&crate_name[..1]).as_str());
}
_ => {
folders.push(&crate_name[..2]);
folders.push(&crate_name[2..4]);
folders.push(ustr(&crate_name[..2]).as_str());
folders.push(ustr(&crate_name[2..4]).as_str());
}
}
@@ -1,20 +1,18 @@
use bytes::Bytes;
use packfile::{
low_level::{PackFile, PackFileEntry},
PktLine,
};
use thrussh::{server::Session, ChannelId};
use crate::{
git::{
packfile::low_level::{PackFile, PackFileEntry},
PktLine,
},
Handler,
};
use crate::Handler;
pub(crate) fn handle(
handle: &mut Handler,
session: &mut Session,
channel: ChannelId,
metadata: Vec<Bytes>,
packfile_entries: Vec<PackFileEntry<'_>>,
packfile_entries: Vec<PackFileEntry>,
) -> Result<(), anyhow::Error> {
@@ -37,7 +35,7 @@
handle.flush(session, channel);
let packfile = PackFile::new(packfile_entries);
let packfile = PackFile::new(&packfile_entries);
handle.write(PktLine::SidebandData(packfile))?;
handle.write(PktLine::Flush)?;
handle.flush(session, channel);
@@ -5,12 +5,10 @@
use bytes::Bytes;
use packfile::{low_level::HashOutput, PktLine};
use thrussh::{server::Session, ChannelId};
use crate::{
git::{packfile::low_level::HashOutput, PktLine},
Handler,
};
use crate::Handler;
pub(crate) fn handle(
handle: &mut Handler,
@@ -1,138 +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(Debug, Default, PartialEq, Eq)]
pub struct GitCommand {
pub command: Bytes,
pub metadata: Vec<Bytes>,
}
#[derive(Default)]
pub struct GitCodec {
command: GitCommand,
}
impl codec::Decoder for GitCodec {
type Item = GitCommand;
type Error = anyhow::Error;
fn decode(&mut self, src: &mut bytes::BytesMut) -> Result<Option<Self::Item>, Self::Error> {
loop {
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 {
src.advance(4);
return Ok(Some(std::mem::take(&mut self.command)));
} else if length == 1 || length == 2 {
src.advance(4);
continue;
} else 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 data = src.split_to(length).freeze();
data.advance(4);
if data.ends_with(b"\n") {
data.truncate(data.len() - 1);
}
if self.command.command.is_empty() {
self.command.command = data;
} else {
self.command.metadata.push(data);
}
}
}
}
#[cfg(test)]
mod test {
use bytes::{Bytes, BytesMut};
use std::fmt::Write;
use tokio_util::codec::Decoder;
#[test]
fn decode() {
let mut codec = super::GitCodec::default();
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();
let res = codec.decode(&mut bytes).unwrap();
assert_eq!(res, None);
bytes.write_str("0000").unwrap();
let res = codec.decode(&mut bytes).unwrap();
assert_eq!(
res,
Some(super::GitCommand {
command: Bytes::from_static(b"agent=git/2.32.0"),
metadata: vec![],
})
);
bytes.write_str("0000").unwrap();
let res = codec.decode(&mut bytes).unwrap();
assert_eq!(
res,
Some(super::GitCommand {
command: Bytes::new(),
metadata: vec![],
})
);
bytes.write_str("0002").unwrap();
bytes.write_str("0005a").unwrap();
bytes.write_str("0001").unwrap();
bytes.write_str("0005b").unwrap();
bytes.write_str("0000").unwrap();
let res = codec.decode(&mut bytes).unwrap();
assert_eq!(
res,
Some(super::GitCommand {
command: Bytes::from_static(b"a"),
metadata: vec![Bytes::from_static(b"b")],
})
);
}
}
@@ -1,74 +1,0 @@
pub mod codec;
pub mod packfile;
use bytes::{BufMut, BytesMut};
use std::fmt::Write;
use self::packfile::low_level::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,183 +1,0 @@
use std::borrow::Cow;
use arrayvec::ArrayVec;
use indexmap::IndexMap;
use super::low_level::{
Commit, CommitUserInfo, HashOutput, PackFileEntry, TreeItem as LowLevelTreeItem, TreeItemKind,
};
#[derive(Debug)]
pub struct GitRepository<'a> {
packfile_entries: IndexMap<HashOutput, PackFileEntry<'a>>,
tree: Tree<'a>,
}
impl Default for GitRepository<'_> {
fn default() -> Self {
Self {
packfile_entries: IndexMap::new(),
tree: Tree::default(),
}
}
}
impl<'a> GitRepository<'a> {
pub fn insert<const N: usize>(
&mut self,
path: ArrayVec<&'a str, N>,
file: &'a str,
content: &'a [u8],
) -> Result<(), anyhow::Error> {
let mut directory = &mut self.tree;
for part in path {
let tree_item = directory
.0
.entry(part)
.or_insert_with(|| Box::new(TreeItem::Tree(Tree::default())));
if let TreeItem::Tree(d) = tree_item.as_mut() {
directory = d;
} else {
anyhow::bail!("attempted to use a file as a directory");
}
}
let entry = PackFileEntry::Blob(content);
let file_hash = entry.hash()?;
directory
.0
.insert(file, Box::new(TreeItem::Blob(file_hash)));
self.packfile_entries.insert(file_hash, entry);
Ok(())
}
pub fn commit(
&'a mut self,
name: &'a str,
email: &'a str,
message: &'a str,
) -> Result<(HashOutput, Vec<PackFileEntry<'a>>), anyhow::Error> {
let tree_hash = self.tree.to_packfile_entries(&mut self.packfile_entries)?;
let commit_user = CommitUserInfo {
name,
email,
time: chrono::Utc::now(),
};
let commit = PackFileEntry::Commit(Commit {
tree: tree_hash,
author: commit_user,
committer: commit_user,
message,
});
let commit_hash = commit.hash()?;
self.packfile_entries.insert(commit_hash, commit);
Ok((
commit_hash,
self.packfile_entries.values().cloned().collect(),
))
}
}
#[derive(Default, Debug)]
struct Tree<'a>(IndexMap<&'a str, Box<TreeItem<'a>>>);
impl<'a> Tree<'a> {
fn to_packfile_entries(
&self,
pack_file: &mut IndexMap<HashOutput, PackFileEntry<'a>>,
) -> Result<HashOutput, anyhow::Error> {
let mut tree = Vec::with_capacity(self.0.len());
for (name, item) in &self.0 {
tree.push(match item.as_ref() {
TreeItem::Blob(hash) => LowLevelTreeItem {
kind: TreeItemKind::File,
name,
sort_name: Cow::Borrowed(name),
hash: *hash,
},
TreeItem::Tree(tree) => LowLevelTreeItem {
kind: TreeItemKind::Directory,
name,
sort_name: Cow::Owned(format!("{}/", name)),
hash: tree.to_packfile_entries(pack_file)?,
},
});
}
tree.sort_unstable_by(|a, b| a.sort_name.cmp(&b.sort_name));
let tree = PackFileEntry::Tree(tree);
let hash = tree.hash()?;
pack_file.insert(hash, tree);
Ok(hash)
}
}
#[derive(Debug)]
enum TreeItem<'a> {
Blob(HashOutput),
Tree(Tree<'a>),
}
@@ -1,327 +1,0 @@
use bytes::{BufMut, BytesMut};
use flate2::{write::ZlibEncoder, Compression};
use sha1::{
digest::{generic_array::GenericArray, OutputSizeUser},
Digest, Sha1,
};
use std::{borrow::Cow, convert::TryInto, fmt::Write, io::Write as IoWrite};
pub type HashOutput = GenericArray<u8, <Sha1 as OutputSizeUser>::OutputSize>;
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(())
}
}
#[derive(Debug, Clone, Copy)]
pub struct Commit<'a> {
pub tree: HashOutput,
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()
}
}
#[derive(Debug, Copy, Clone)]
pub enum TreeItemKind {
File,
Directory,
}
impl TreeItemKind {
#[must_use]
pub const fn mode(&self) -> &'static str {
match self {
Self::File => "100644",
Self::Directory => "40000",
}
}
}
#[derive(Debug, Clone)]
pub struct TreeItem<'a> {
pub kind: TreeItemKind,
pub name: &'a str,
pub sort_name: Cow<'a, str>,
pub hash: HashOutput,
}
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()
}
}
#[derive(Debug, Clone)]
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<HashOutput, 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,2 +1,0 @@
pub mod high_level;
pub mod low_level;
@@ -48,7 +48,7 @@
uuid: user.uuid.0,
display_name: user.display_name().to_string(),
picture_url: user.picture_url,
permissions: can_manage_users.then(|| perms),
permissions: can_manage_users.then_some(perms),
})
.collect(),
public: organisation.organisation().public,