use std::collections::BTreeMap;
use std::fs::{create_dir, read_dir, remove_dir, rename, File, OpenOptions};
use std::io::{self, Read, Write};
use std::os::unix::fs::{symlink, OpenOptionsExt, PermissionsExt};
use std::path::{Path, PathBuf};
use base32::{self, Alphabet};
use rand::rngs::OsRng;
use rand::RngCore;
use sha2::{Digest, Sha384};
use crate::Manifest;
const B32_ALPHABET: Alphabet = Alphabet::RFC4648 { padding: false };
pub fn b32enc(bin: &[u8]) -> String {
base32::encode(B32_ALPHABET, bin)
}
pub fn b32dec(txt: &str) -> Option<Vec<u8>> {
base32::decode(B32_ALPHABET, txt)
}
fn block_relpath(sig: &[u8; 64]) -> PathBuf {
PathBuf::from("block").join(b32enc(sig))
}
fn object_relpath(key: &[u8; 48]) -> PathBuf {
PathBuf::from("object").join(b32enc(key))
}
fn tail_to_block(sig: &[u8; 64]) -> PathBuf {
PathBuf::from("../..").join(block_relpath(sig))
}
pub fn random_id() -> String {
let mut key = [0u8; 15];
OsRng.fill_bytes(&mut key);
b32enc(&key)
}
fn create_dir_if_needed<P: AsRef<Path>>(path: P) -> io::Result<()> {
if path.as_ref().is_dir() {
return Ok(());
}
create_dir(path.as_ref())
}
fn to_canonical<P: AsRef<Path>, Q: AsRef<Path>>(src: P, dst: Q) -> io::Result<()> {
let parent = dst.as_ref().parent().unwrap();
create_dir_if_needed(parent)?;
rename(src.as_ref(), dst.as_ref())
}
pub struct Store {
basedir: PathBuf,
}
impl Store {
pub fn new<P: AsRef<Path>>(basedir: P) -> Store {
Store {
basedir: PathBuf::from(basedir.as_ref()),
}
}
pub fn remove_tmp_dir(&self) -> io::Result<()> {
let tmp = self.basedir.join("tmp");
remove_dir(tmp)
}
pub fn temp_path(&self) -> PathBuf {
self.basedir.join("tmp").join(random_id())
}
pub fn object_path(&self, key: &[u8; 48]) -> PathBuf {
self.basedir.join(object_relpath(key))
}
pub fn block_path(&self, sig: &[u8; 64]) -> PathBuf {
self.basedir.join(block_relpath(sig))
}
fn _write_content(&self, content: &[u8]) -> io::Result<PathBuf> {
let tmp = self.temp_path();
create_dir_if_needed(tmp.parent().unwrap())?;
{
let mut opt = OpenOptions::new();
let opt = opt.create_new(true).write(true).mode(0o400);
let mut file = opt.open(tmp.as_path())?;
file.write_all(content)?;
file.sync_all()?;
}
Ok(tmp)
}
pub fn import_object<P: AsRef<Path>>(&self, src: P) -> io::Result<[u8; 48]> {
let key = {
let mut file = File::open(src.as_ref())?;
{
let mut perm = file.metadata().unwrap().permissions();
perm.set_mode(0o400);
file.set_permissions(perm)?;
file.sync_all()?;
}
let mut hasher = Sha384::default();
let mut buf = [0u8; 4096];
loop {
let len = file.read(&mut buf)?;
if len == 0 {
break;
}
hasher.update(&buf[..len]);
}
let mut key = [0u8; 48];
key.copy_from_slice(hasher.finalize().as_slice());
key
};
let dst = self.object_path(&key);
to_canonical(src, dst)?;
Ok(key)
}
pub fn import_artifacts(&self, time: u64) -> io::Result<Manifest> {
let artifacts = self.basedir.join("artifacts");
let mut files = BTreeMap::new();
let entries = read_dir(artifacts.as_path())?;
for entry in entries {
let entry = entry?;
let name = entry
.file_name()
.into_string()
.map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, format!("{:?}", err)))?;
let key = self.import_object(entry.path())?;
files.insert(name, b32enc(&key[..]));
let target = PathBuf::from("..").join(object_relpath(&key));
let link = entry.path();
symlink(target.as_path(), link.as_path())?;
}
Ok(Manifest { time, files })
}
pub fn write_object(&self, object: &[u8]) -> io::Result<[u8; 48]> {
let key = {
let mut key = [0u8; 48];
let digest = Sha384::digest(object);
key.copy_from_slice(digest.as_slice());
key
};
let tmp = self._write_content(object)?;
let dst = self.object_path(&key);
to_canonical(tmp, dst)?;
Ok(key)
}
pub fn write_manifest(&self, object: &[u8]) -> io::Result<[u8; 48]> {
let key = self.write_object(object)?;
let link = self.basedir.join("manifest.json");
let target = object_relpath(&key);
symlink(target.as_path(), link.as_path())?;
Ok(key)
}
pub fn open_object(&self, key: &[u8; 48]) -> io::Result<File> {
File::open(self.object_path(key))
}
pub fn write_block(&self, block: &[u8; 400]) -> io::Result<[u8; 64]> {
let sig = {
let mut sig = [0u8; 64];
sig.copy_from_slice(&block[0..64]);
sig
};
let tmp = self._write_content(block)?;
let dst = self.block_path(&sig);
to_canonical(tmp, dst)?;
Ok(sig)
}
pub fn write_tail(
&self,
project: &str,
branch: &str,
block: &[u8; 400],
) -> io::Result<[u8; 64]> {
let sig = self.write_block(block)?;
let mut pb = self.basedir.join("tail");
create_dir_if_needed(&pb)?;
pb.push(project);
create_dir_if_needed(&pb)?;
pb.push(branch);
let target = tail_to_block(&sig);
symlink(target.as_path(), pb.as_path())?;
Ok(sig)
}
pub fn open_block(&self, sig: &[u8; 64]) -> io::Result<File> {
File::open(self.block_path(sig))
}
}
#[cfg(test)]
mod tests {
use std::fs::{create_dir, File};
use std::io::{Read, Write};
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use rand::{rngs::OsRng, RngCore};
use tempfile::TempDir;
use super::{tail_to_block, Store};
#[test]
fn test_new() {
let s = Store::new(Path::new("/nope"));
assert_eq!(s.basedir.as_path(), Path::new("/nope/"));
}
#[test]
fn test_temp_path() {
let s = Store::new(Path::new("/nope"));
let p1 = s.temp_path();
assert_eq!(&(p1.to_str().unwrap()[0..10]), "/nope/tmp/");
assert_eq!(p1.to_str().unwrap().len(), 34);
let p2 = s.temp_path();
assert_eq!(&(p2.to_str().unwrap()[0..10]), "/nope/tmp/");
assert_eq!(p2.to_str().unwrap().len(), 34);
assert_ne!(p1, p2);
assert_ne!(p1.to_str().unwrap(), p2.to_str().unwrap());
assert_eq!(p1.to_str().unwrap()[..10], p2.to_str().unwrap()[..10]);
assert_ne!(p1.to_str().unwrap()[10..], p2.to_str().unwrap()[10..]);
}
#[test]
fn test_object_path() {
let s = Store::new(Path::new("/p"));
assert_eq!(
s.object_path(&[0u8; 48]).as_path(),
Path::new("/p/object/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
);
assert_eq!(
s.object_path(&[255u8; 48]).as_path(),
Path::new("/p/object/77777777777777777777777777777777777777777777777777777777777777777777777777776")
);
}
#[test]
fn test_block_path() {
let s = Store::new(Path::new("/p"));
let sig1 = [0u8; 64];
let sig2 = [255u8; 64];
assert_eq!(
s.block_path(&sig1).as_path(),
Path::new("/p/block/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
);
assert_eq!(
s.block_path(&sig2).as_path(),
Path::new("/p/block/777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777Y")
);
}
#[test]
fn test_import_object() {
let temp_dir = TempDir::with_prefix("buildchain-test.").unwrap();
let store = Store::new(&temp_dir);
let content = {
let mut content = [0u8; 34969];
OsRng.fill_bytes(&mut content);
content
};
let example = {
let artifacts = PathBuf::from(temp_dir.path()).join("artifacts");
create_dir(artifacts.as_path()).unwrap();
artifacts.join("example")
};
{
let mut file = File::create(example.as_path()).unwrap();
file.write_all(&content).unwrap();
}
let key: [u8; 48] = store.import_object(example.as_path()).unwrap();
{
let mut file = store.open_object(&key).unwrap();
let perm = file.metadata().unwrap().permissions();
assert_eq!(perm.mode() & 511, 0o400);
let mut buf = [0u8; 34969];
assert_eq!(file.read(&mut buf).unwrap(), buf.len());
assert_eq!(content.to_vec(), buf.to_vec());
}
{
let tmp = temp_dir.path().join("tmp");
assert!(!tmp.exists());
}
temp_dir.close().unwrap();
}
#[test]
fn test_write_object() {
let temp_dir = TempDir::with_prefix("buildchain-test.").unwrap();
let store = Store::new(&temp_dir);
let content = {
let mut content = [0u8; 1776];
OsRng.fill_bytes(&mut content);
content
};
let key: [u8; 48] = store.write_object(&content).unwrap();
{
let mut file = store.open_object(&key).unwrap();
let perm = file.metadata().unwrap().permissions();
assert_eq!(perm.mode() & 511, 0o400);
let mut buf = [0u8; 1776];
assert_eq!(file.read(&mut buf).unwrap(), buf.len());
assert_eq!(content.to_vec(), buf.to_vec());
}
{
let tmp = temp_dir.path().join("tmp");
assert!(tmp.is_dir());
store.remove_tmp_dir().unwrap();
assert!(!tmp.exists());
}
temp_dir.close().unwrap();
}
#[test]
fn test_write_block() {
let temp_dir = TempDir::with_prefix("buildchain-test.").unwrap();
let store = Store::new(&temp_dir);
let block = {
let mut block = [0u8; 400];
OsRng.fill_bytes(&mut block);
block
};
let sig: [u8; 64] = store.write_block(&block).unwrap();
assert_eq!(sig.to_vec(), block[0..64].to_vec());
{
let mut file = store.open_block(&sig).unwrap();
let perm = file.metadata().unwrap().permissions();
assert_eq!(perm.mode() & 511, 0o400);
let mut buf = [0u8; 400];
assert_eq!(file.read(&mut buf).unwrap(), buf.len());
assert_eq!(block.to_vec(), buf.to_vec());
}
{
let tmp = temp_dir.path().join("tmp");
assert!(tmp.is_dir());
store.remove_tmp_dir().unwrap();
assert!(!tmp.exists());
}
temp_dir.close().unwrap();
}
#[test]
fn test_write_tail() {
let temp_dir = TempDir::with_prefix("buildchain-test.").unwrap();
let store = Store::new(&temp_dir);
let block = {
let mut block = [0u8; 400];
OsRng.fill_bytes(&mut block);
block
};
let sig = store.write_tail("stuff", "junk", &block).unwrap();
assert_eq!(sig.to_vec(), block[0..64].to_vec());
{
let mut file = store.open_block(&sig).unwrap();
let perm = file.metadata().unwrap().permissions();
assert_eq!(perm.mode() & 511, 0o400);
let mut buf = [0u8; 400];
assert_eq!(file.read(&mut buf).unwrap(), buf.len());
assert_eq!(block.to_vec(), buf.to_vec());
}
{
let mut pb = temp_dir.path().join("tail");
assert!(pb.is_dir());
pb.push("stuff");
assert!(pb.is_dir());
pb.push("junk");
assert!(pb.is_file());
assert_eq!(pb.read_link().unwrap(), tail_to_block(&sig));
}
{
let tmp = temp_dir.path().join("tmp");
assert!(tmp.is_dir());
}
temp_dir.close().unwrap();
}
}