use std::sync::Arc;
use turso::Row;
use url::Url;
use crate::artifacts::content::ContentKey;
use crate::policy::{Digest, Ecosystem, HashAlgorithm, PublicationTime};
use crate::store::StoreError;
use crate::upstream::UpstreamValidators;
pub use crate::artifacts::reference::{ArtifactReference, InvalidReferenceId, ReferenceId};
#[derive(Clone, Debug)]
pub struct BlocklistRow {
pub revision: u64,
pub generated_at_micros: i64,
pub expires_at_micros: i64,
pub snapshot: Arc<[u8]>,
}
impl BlocklistRow {
pub(crate) fn from_row(row: &Row) -> Result<BlocklistRow, StoreError> {
let revision = integer(row, 0, "blocklist.revision")?;
let revision = u64::try_from(revision).map_err(|_| {
StoreError::Corrupt(format!("blocklist.revision is negative: {revision}"))
})?;
Ok(BlocklistRow {
revision,
generated_at_micros: integer(row, 1, "blocklist.generated_at_micros")?,
expires_at_micros: integer(row, 2, "blocklist.expires_at_micros")?,
snapshot: blob(row, 3, "blocklist.snapshot")?,
})
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Default)]
pub struct Generation(pub u64);
#[derive(Clone, Debug)]
pub struct ProjectRow {
pub ecosystem: Ecosystem,
pub name: String,
pub payload: Arc<[u8]>,
pub validators: UpstreamValidators,
pub validated_at_micros: i64,
pub fetched_at_micros: i64,
pub generation: Generation,
pub digest_generation: u64,
}
impl ProjectRow {
pub(crate) fn from_row(row: &Row) -> Result<ProjectRow, StoreError> {
let ecosystem = text(row, 0, "projects.ecosystem")?;
let ecosystem = Ecosystem::from_tag(&ecosystem)
.ok_or_else(|| StoreError::Corrupt(format!("unknown ecosystem `{ecosystem}`")))?;
Ok(ProjectRow {
ecosystem,
name: text(row, 1, "projects.name")?,
payload: blob(row, 2, "projects.payload")?,
validators: UpstreamValidators {
etag: optional_text(row, 3),
last_modified: optional_text(row, 4),
},
validated_at_micros: integer(row, 5, "projects.validated_at_micros")?,
fetched_at_micros: integer(row, 6, "projects.fetched_at_micros")?,
generation: Generation(unsigned(row, 7, "projects.generation")?),
digest_generation: unsigned(row, 8, "projects.digest_generation")?,
})
}
}
#[derive(Clone, Debug)]
pub struct ReferenceRow {
pub id: ReferenceId,
pub reference: ArtifactReference,
pub publication_micros: Option<i64>,
pub first_seen_micros: Option<i64>,
pub pinned_sha256: Option<[u8; 32]>,
pub pinned_sha512: Option<[u8; 64]>,
pub pinned_size: Option<u64>,
pub content_key: Option<ContentKey>,
}
impl ReferenceRow {
pub fn pinned_digests(&self) -> Vec<Digest> {
let mut digests = Vec::with_capacity(2);
if let Some(sha256) = self.pinned_sha256 {
digests.push(Digest {
algorithm: HashAlgorithm::Sha256,
bytes: Box::from(sha256.as_slice()),
});
}
if let Some(sha512) = self.pinned_sha512 {
digests.push(Digest {
algorithm: HashAlgorithm::Sha512,
bytes: Box::from(sha512.as_slice()),
});
}
digests
}
pub fn publication_time(&self) -> PublicationTime {
match (self.publication_micros, self.first_seen_micros) {
(Some(micros), _) => PublicationTime::Upstream(micros),
(None, Some(micros)) => PublicationTime::FirstSeen(micros),
(None, None) => PublicationTime::Unknown,
}
}
pub(crate) fn from_row(row: &Row) -> Result<ReferenceRow, StoreError> {
let id = blob(row, 0, "artifact_references.id")?;
let id: [u8; 32] = id.as_ref().try_into().map_err(|_| {
StoreError::Corrupt("artifact_references.id is not 32 bytes".to_owned())
})?;
let ecosystem = text(row, 1, "artifact_references.ecosystem")?;
let ecosystem = Ecosystem::from_tag(&ecosystem)
.ok_or_else(|| StoreError::Corrupt(format!("unknown ecosystem `{ecosystem}`")))?;
let upstream_url = text(row, 5, "artifact_references.upstream_url")?;
let upstream_url = Url::parse(&upstream_url).map_err(|err| {
StoreError::Corrupt(format!("artifact_references.upstream_url: {err}"))
})?;
Ok(ReferenceRow {
id: ReferenceId::from_bytes(id),
reference: ArtifactReference {
ecosystem,
name: text(row, 2, "artifact_references.name")?,
version: text(row, 3, "artifact_references.version")?,
filename: text(row, 4, "artifact_references.filename")?,
upstream_url,
expected: decode_digests(&text(row, 6, "artifact_references.expected")?)?,
},
publication_micros: optional_integer(row, 7),
first_seen_micros: optional_integer(row, 8),
pinned_sha256: optional_digest(row, 9, "artifact_references.pinned_sha256")?,
pinned_sha512: optional_digest(row, 10, "artifact_references.pinned_sha512")?,
pinned_size: optional_integer(row, 11)
.map(|size| {
u64::try_from(size).map_err(|_| {
StoreError::Corrupt(format!(
"artifact_references.pinned_size is negative: {size}"
))
})
})
.transpose()?,
content_key: optional_digest::<32>(row, 12, "artifact_references.content_key")?
.map(ContentKey::from_sha256),
})
}
}
fn optional_digest<const N: usize>(
row: &Row,
index: usize,
column: &'static str,
) -> Result<Option<[u8; N]>, StoreError> {
let Some(bytes) = row
.get_value(index)
.ok()
.and_then(|value| value.as_blob().cloned())
else {
return Ok(None);
};
<[u8; N]>::try_from(bytes.as_slice())
.map(Some)
.map_err(|_| StoreError::Corrupt(format!("{column} is not {N} bytes")))
}
#[derive(Clone, Debug)]
pub struct ProjectRefresh {
pub ecosystem: Ecosystem,
pub name: String,
pub payload: Arc<[u8]>,
pub validators: UpstreamValidators,
pub validated_at_micros: i64,
pub fetched_at_micros: i64,
pub references: Vec<ReferenceUpsert>,
}
#[derive(Clone, Debug)]
pub struct ReferenceUpsert {
pub id: ReferenceId,
pub reference: ArtifactReference,
pub publication_micros: Option<i64>,
pub first_seen_micros: Option<i64>,
}
pub(crate) fn encode_digests(digests: &[Digest]) -> String {
digests
.iter()
.map(|digest| digest.to_string())
.collect::<Vec<_>>()
.join(" ")
}
pub(crate) fn decode_digests(text: &str) -> Result<Vec<Digest>, StoreError> {
text.split_whitespace()
.map(|entry| {
let (name, hex_text) = entry.split_once('-').ok_or_else(|| {
StoreError::Corrupt(format!("`{entry}` is not an `<algorithm>-<hex>` digest"))
})?;
let algorithm = HashAlgorithm::from_name(name).ok_or_else(|| {
StoreError::Corrupt(format!("unknown stored hash algorithm `{name}`"))
})?;
Digest::parse_hex(algorithm, hex_text)
.map_err(|err| StoreError::Corrupt(format!("stored digest: {err}")))
})
.collect()
}
fn integer(row: &Row, index: usize, column: &'static str) -> Result<i64, StoreError> {
optional_integer(row, index)
.ok_or_else(|| StoreError::Corrupt(format!("{column} is not an integer")))
}
fn optional_integer(row: &Row, index: usize) -> Option<i64> {
row.get_value(index)
.ok()
.and_then(|value| value.as_integer().copied())
}
fn unsigned(row: &Row, index: usize, column: &'static str) -> Result<u64, StoreError> {
let value = integer(row, index, column)?;
u64::try_from(value).map_err(|_| StoreError::Corrupt(format!("{column} is negative: {value}")))
}
fn text(row: &Row, index: usize, column: &'static str) -> Result<String, StoreError> {
optional_text(row, index).ok_or_else(|| StoreError::Corrupt(format!("{column} is not text")))
}
fn optional_text(row: &Row, index: usize) -> Option<String> {
row.get_value(index)
.ok()
.and_then(|value| value.as_text().cloned())
}
fn blob(row: &Row, index: usize, column: &'static str) -> Result<Arc<[u8]>, StoreError> {
row.get_value(index)
.ok()
.and_then(|value| value.as_blob().map(|bytes| Arc::from(bytes.as_slice())))
.ok_or_else(|| StoreError::Corrupt(format!("{column} is not a blob")))
}
#[cfg(test)]
mod tests {
use super::*;
fn reference() -> ArtifactReference {
ArtifactReference {
ecosystem: Ecosystem::Npm,
name: "left-pad".to_owned(),
version: "1.3.0".to_owned(),
filename: "left-pad-1.3.0.tgz".to_owned(),
upstream_url: Url::parse("https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz")
.expect("a test URL"),
expected: vec![
Digest::parse_hex(HashAlgorithm::Sha512, &"ab".repeat(64)).expect("a digest"),
],
}
}
#[test]
fn digests_round_trip_through_their_stored_spelling() {
let digests = reference().expected;
assert_eq!(
decode_digests(&encode_digests(&digests)).expect("stored digests parse"),
digests
);
}
#[test]
fn an_upstream_timestamp_supersedes_a_committed_first_seen() {
let mut row = ReferenceRow {
id: ReferenceId::compute(&reference()),
reference: reference(),
publication_micros: None,
first_seen_micros: Some(500),
pinned_sha256: None,
pinned_sha512: None,
pinned_size: None,
content_key: None,
};
assert_eq!(row.publication_time(), PublicationTime::FirstSeen(500));
row.publication_micros = Some(100);
assert_eq!(row.publication_time(), PublicationTime::Upstream(100));
row.first_seen_micros = None;
row.publication_micros = None;
assert_eq!(row.publication_time(), PublicationTime::Unknown);
}
}