use std::fs::OpenOptions;
use std::io::Write;
use std::path::PathBuf;
use serde_json::{Map, Value};
use time::macros::format_description;
use time::OffsetDateTime;
use onevcs::{ArtifactId, Envelope, Error, EventKind, Labels, Result, Source};
const ENVELOPE_VERSION: u32 = 1;
pub(crate) struct Emission {
pub stream: String,
pub identity: Option<String>,
pub kind: EventKind,
pub payload: Map<String, Value>,
}
pub(crate) fn state_root() -> Result<PathBuf> {
match std::env::var_os("ONEVCS_HOME") {
Some(value) if value.is_empty() => Err(Error::Invalid {
reason: "ONEVCS_HOME is set but empty; unset it or give it a directory".to_owned(),
}),
Some(value) => Ok(PathBuf::from(value)),
None => home_directory()
.map(|home| home.join(".onevcs"))
.ok_or_else(|| Error::Invalid {
reason: "cannot find a home directory; set ONEVCS_HOME to a directory".to_owned(),
}),
}
}
#[cfg(unix)]
fn home_directory() -> Option<PathBuf> {
std::env::var_os("HOME")
.filter(|value| !value.is_empty())
.map(PathBuf::from)
}
#[cfg(windows)]
fn home_directory() -> Option<PathBuf> {
std::env::var_os("USERPROFILE")
.filter(|value| !value.is_empty())
.map(PathBuf::from)
}
pub(crate) fn emit(emission: &Emission) {
if let Err(error) = append(emission) {
eprintln!(
"onevcs-testing: warning: cannot record a {:?} event for {}: {error}",
emission.kind, emission.stream
);
}
}
fn append(emission: &Emission) -> Result<()> {
if !is_safe_name(&emission.stream) {
return Err(Error::Invalid {
reason: format!("{:?} is not a session token", emission.stream),
});
}
let directory = state_root()?.join("streams");
std::fs::create_dir_all(&directory).map_err(|e| Error::Invalid {
reason: format!("cannot create {}: {e}", directory.display()),
})?;
let path = directory.join(format!("{}.ndjson", emission.stream));
let seq = std::fs::read_to_string(&path)
.map(|raw| raw.lines().filter(|line| !line.trim().is_empty()).count() as u64)
.unwrap_or(0)
+ 1;
let mut labels = Labels::default();
labels
.extra
.insert("session".to_owned(), Value::String(emission.stream.clone()));
if let Some(identity) = &emission.identity {
labels
.extra
.insert("identity".to_owned(), Value::String(identity.clone()));
}
let envelope = Envelope {
v: ENVELOPE_VERSION,
ts: timestamp(),
stream: emission.stream.clone(),
seq,
source: Source::Vcs,
kind: emission.kind,
labels,
payload: emission.payload.clone(),
artifacts: Vec::new(),
};
let line = serde_json::to_string(&envelope).map_err(|e| Error::Invalid {
reason: format!("cannot serialize an event: {e}"),
})?;
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(&path)
.map_err(|e| Error::Invalid {
reason: format!("cannot open {}: {e}", path.display()),
})?;
writeln!(file, "{line}").map_err(|e| Error::Invalid {
reason: format!("cannot write {}: {e}", path.display()),
})
}
pub(crate) fn is_safe_name(value: &str) -> bool {
!value.is_empty()
&& value != "."
&& value != ".."
&& value
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.'))
}
pub(crate) fn store_artifact(id: &str, contents: &str) -> Result<ArtifactId> {
if !is_safe_name(id) {
return Err(Error::Invalid {
reason: format!("{id:?} is not an artifact id"),
});
}
let directory = state_root()?.join("artifacts");
std::fs::create_dir_all(&directory).map_err(|e| Error::Invalid {
reason: format!("cannot create {}: {e}", directory.display()),
})?;
let path = directory.join(id);
std::fs::write(&path, contents).map_err(|e| Error::Invalid {
reason: format!("cannot store the artifact at {}: {e}", path.display()),
})?;
Ok(ArtifactId(id.to_owned()))
}
fn timestamp() -> String {
let description =
format_description!("[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:3]Z");
OffsetDateTime::now_utc()
.format(description)
.unwrap_or_else(|_| "1970-01-01T00:00:00.000Z".to_owned())
}
pub(crate) fn stable_sha(parts: &[&str]) -> String {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
for part in parts {
hasher.update(part.as_bytes());
hasher.update([0]);
}
hasher
.finalize()
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>()[..40]
.to_owned()
}