server/lib/src/metered.rs
//! **The paid state — compiled ONLY by the metered build.**
//!
//! The community holger does not decline to enforce a plan; it does not contain
//! one. This module, the `holger.metered.v1` proto package and the `monetize-embed`
//! dependency all hang off the `metered` feature, so an open binary has no plan
//! vocabulary in its string table and no slot on `HolgerGrpc` to put one in.
//!
//! ## What arrives, and what is checked before anything moves
//!
//! monetize pushes a signed [`EntitlementFact`] per repository. `Set` is ADMIN, and
//! in this order:
//!
//! 1. **shape** — a fact missing its repository, plan, state or source, or carrying
//! a signature that is not 64 bytes, is `INVALID_ARGUMENT`. Cheap, and it tells a
//! malformed push apart from a forged one.
//! 2. **the caps are ones holger can police** — `caps` is an open map, and a cap on
//! a meter this server only measures SERVER-WIDE (`bytes_served`, `requests`,
//! `proxy_upstream_hits`, `repos`) is a promise about a number that is not the
//! tenant's. Refused at the sale rather than stored, logged and then ignored for
//! the life of the plan — gunnar learned that one the expensive way.
//! 3. **the signature** — Ed25519 by monetize's key over the canonical JSON of the
//! RUST `monetize_product::EntitlementFact` minus `signature`.
//! `monetize_embed::signing::{fact_message, verify_fact}` is the ONE writer and
//! the ONE verifier of that form; holger writes no second canonical form. A fact
//! that does not verify is `PERMISSION_DENIED` and nothing moves.
//!
//! ⚠ **The proto shape is NOT the signed shape**, and that is the trap in this file.
//! monetize signs the Rust struct (`tenant`, `state` as the variant NAME `"Paid"`,
//! `caps` an object with sorted keys); the wire carries a prost message
//! (`repository`, `state` as an `i32`, `caps` a `HashMap`). [`to_signed`] is the one
//! conversion, and `the_conversion_is_what_monetize_signed` pins it — get it wrong
//! and every signature fails for a reason no message would explain.
//!
//! ## Where a fact lives
//!
//! `<data_root>/holger-entitlements/<repository>.json`, one file per repository,
//! closed to the repository roster exactly as the SBOM and property stores are: a
//! repository name arrives off the wire and is authorized as a raw string, so
//! without a roster a caller writes plan files for repositories that do not exist.
//! holger has no separate attestation log — the append-only audit log IS the record
//! that a plan changed, and `Set` writes an [`AuditAction::Entitlement`] line
//! carrying the fact's `source` (`operator` or `payment:<vendor>:<reference>`).
use crate::lock::{LockOrRecover as _, RwLockOrRecover as _};
use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use monetize_embed::VerifyingKey;
use monetize_product::{EntitlementFact as SignedFact, State, TenantId};
use tonic::{Request, Response, Status};
use crate::audit::{AuditAction, AuditEvent};
use crate::grpc::holger_metered_proto::entitlement_service_server::EntitlementService;
use crate::grpc::holger_metered_proto::{
EntitlementFact, EntitlementState, GetEntitlementRequest, GetEntitlementResponse,
ListEntitlementsRequest, ListEntitlementsResponse, PurgeRepositoryRequest,
PurgeRepositoryResponse, SetEntitlementRequest, SetEntitlementResponse,
};
use crate::grpc::{coded, internal_err, HolgerGrpc};
use crate::metering::PER_REPOSITORY_METERS;
use holger_errcode as ec;
/// One file per repository, under the data root beside the other metadata stores.
pub const STORE_DIR: &str = "holger-entitlements";
/// The paid state on disk, closed to the repositories this server actually serves.
pub struct EntitlementStore {
root: PathBuf,
roster: Arc<BTreeSet<String>>,
/// monetize's Ed25519 verifying key. Without one this store REFUSES every
/// write: a metered build that cannot check a signature must not keep a fact it
/// cannot vouch for. `None` while the key is AWAITED (an estate box whose front
/// offers it after the start, A7 row 8): [`EntitlementStore::set_key`] applies it
/// in place, no restart.
key: std::sync::RwLock<Option<VerifyingKey>>,
}
impl EntitlementStore {
/// `public_key` is monetize's 32-byte Ed25519 verifying key.
pub fn new(
root: impl Into<PathBuf>,
roster: Arc<BTreeSet<String>>,
public_key: &[u8; 32],
) -> Result<Self, monetize_embed::KeyError> {
Ok(Self {
root: root.into(),
roster,
key: std::sync::RwLock::new(Some(
VerifyingKey::from_bytes(public_key).map_err(|_| monetize_embed::KeyError)?,
)),
})
}
/// **A store whose key is awaited** (A7 row 8): it keeps nothing and polices nothing
/// until [`EntitlementStore::set_key`] gives it monetize's key.
pub fn awaiting(root: impl Into<PathBuf>, roster: Arc<BTreeSet<String>>) -> Self {
Self {
root: root.into(),
roster,
key: std::sync::RwLock::new(None),
}
}
/// **monetize's key, applied in place** when the installer feeds it after the start.
pub fn set_key(&self, public_key: &[u8; 32]) -> Result<(), monetize_embed::KeyError> {
let key = VerifyingKey::from_bytes(public_key).map_err(|_| monetize_embed::KeyError)?;
*self.key.write_or_recover() = Some(key);
Ok(())
}
/// True once this store has monetize's key (it verifies, keeps and polices).
pub fn keyed(&self) -> bool {
self.key.read_or_recover().is_some()
}
fn path_of(&self, repository: &str) -> PathBuf {
self.root.join(format!("{}.json", sanitize(repository)))
}
/// The store's directory — also what names this server to the purge gate
/// ([`crate::purge::close`]), since every door opens the same one.
pub fn root(&self) -> &Path {
&self.root
}
/// True when this server serves `repository` — the roster gate every metadata
/// store here applies.
pub fn serves(&self, repository: &str) -> bool {
self.roster.contains(repository)
}
pub fn get(&self, repository: &str) -> std::io::Result<Option<SignedFact>> {
match std::fs::read_to_string(self.path_of(repository)) {
Ok(text) => Ok(serde_json::from_str(&text).ok()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(e),
}
}
/// Verify, then write. The order is the point: a fact that does not verify never
/// reaches the disk.
pub fn put(&self, fact: &SignedFact) -> Result<(), PutRefused> {
let key = (*self.key.read_or_recover()).ok_or_else(|| {
PutRefused::Signature("no monetize key has reached this box yet".into())
})?;
monetize_embed::signing::verify_fact(fact, &key)
.map_err(|e| PutRefused::Signature(e.to_string()))?;
std::fs::create_dir_all(&self.root).map_err(|e| PutRefused::Io(e.to_string()))?;
let text = serde_json::to_string_pretty(fact)
.map_err(|e| PutRefused::Io(format!("serialize the fact: {e}")))?;
std::fs::write(self.path_of(&fact.tenant.0), text)
.map_err(|e| PutRefused::Io(e.to_string()))
}
pub fn list(&self) -> Vec<SignedFact> {
let mut out = Vec::new();
let Ok(dir) = std::fs::read_dir(&self.root) else {
return out;
};
for entry in dir.flatten() {
if let Ok(text) = std::fs::read_to_string(entry.path()) {
if let Ok(f) = serde_json::from_str::<SignedFact>(&text) {
out.push(f);
}
}
}
out.sort_by(|a, b| a.tenant.0.cmp(&b.tenant.0));
out
}
}
/// Why a write was refused, kept apart so the handler can answer
/// `PERMISSION_DENIED` for a signature and `INTERNAL` for a disk.
#[derive(Debug)]
pub enum PutRefused {
Signature(String),
Io(String),
}
/// A repository name is a file name here, so it may not escape the store.
fn sanitize(repository: &str) -> String {
repository.replace(['/', '\\'], "_").replace("..", "_")
}
/// **The one conversion from the wire shape to the shape monetize SIGNED.**
///
/// Field for field, and the two that differ are the whole reason this function
/// exists: `repository` is the signed `tenant`, and the numeric `state` is the
/// variant NAME in the canonical JSON.
pub fn to_signed(fact: &EntitlementFact) -> Result<SignedFact, Status> {
let state = match EntitlementState::try_from(fact.state) {
Ok(EntitlementState::Free) => State::Free,
Ok(EntitlementState::Paid) => State::Paid,
Ok(EntitlementState::Grace) => State::Grace,
Ok(EntitlementState::Suspended) => State::Suspended,
Ok(EntitlementState::Retention) => State::Retention,
Ok(EntitlementState::Unspecified) | Err(_) => {
return Err(coded(
&ec::METERED_FACT_MALFORMED,
tonic::Code::InvalidArgument,
"fact.state is unspecified; a fact names one of Free, Paid, Grace, \
Suspended, Retention",
))
}
};
Ok(SignedFact {
tenant: TenantId(fact.repository.clone()),
plan: fact.plan.clone(),
state,
paid_until_unix_ms: fact.paid_until_unix_ms,
caps: fact
.caps
.iter()
.map(|(k, v)| (k.clone(), *v))
.collect::<BTreeMap<_, _>>(),
source: fact.source.clone(),
signature: fact.signature.clone(),
// Carried through, never invented: dropping the stamp would turn a
// replay-guarded fact into the legal unstamped case and lose the guard
// with the signature still verifying.
issued_unix_ms: fact.issued_unix_ms,
issued_signature: fact.issued_signature.clone(),
})
}
/// The wire shape of a stored fact, for `Get`/`List` — the inverse of [`to_signed`].
///
/// `pub` so the round trip can be PINNED from a test: the two functions carry the
/// issue stamp (`issued_unix_ms`, `issued_signature`) through in both directions,
/// and a fixture that is not stamped cannot tell whether they do.
pub fn to_wire(fact: &SignedFact) -> EntitlementFact {
let state = match fact.state {
State::Free => EntitlementState::Free,
State::Paid => EntitlementState::Paid,
State::Grace => EntitlementState::Grace,
State::Suspended => EntitlementState::Suspended,
State::Retention => EntitlementState::Retention,
};
EntitlementFact {
repository: fact.tenant.0.clone(),
plan: fact.plan.clone(),
state: state as i32,
paid_until_unix_ms: fact.paid_until_unix_ms,
caps: fact.caps.iter().map(|(k, v)| (k.clone(), *v)).collect(),
source: fact.source.clone(),
signature: fact.signature.clone(),
issued_unix_ms: fact.issued_unix_ms,
issued_signature: fact.issued_signature.clone(),
}
}
/// **The caps this server cannot police**, named so the refusal can list them.
///
/// A cap is a promise. holger measures `artifacts` and `archive_bytes` per
/// repository and everything else server-wide, so a cap on a server-wide meter
/// would be a wall the tenant can neither hit nor be told about.
pub fn caps_holger_cannot_honour<'a>(keys: impl Iterator<Item = &'a String>) -> Vec<String> {
keys.filter(|k| !PER_REPOSITORY_METERS.contains(&k.as_str()))
.cloned()
.collect()
}
/// The shape checks, before the signature is looked at. `pub` so a test can drive
/// the door itself rather than a copy of it.
pub fn well_formed(fact: &EntitlementFact) -> Result<(), Status> {
if fact.repository.is_empty() {
return Err(coded(
&ec::METERED_FACT_MALFORMED,
tonic::Code::InvalidArgument,
"fact.repository is required",
));
}
if fact.plan.is_empty() {
return Err(coded(
&ec::METERED_FACT_MALFORMED,
tonic::Code::InvalidArgument,
"fact.plan is required",
));
}
if fact.source.is_empty() {
return Err(coded(
&ec::METERED_FACT_MALFORMED,
tonic::Code::InvalidArgument,
"fact.source is required: `operator` or `payment:<vendor>:<reference>`",
));
}
if fact.signature.len() != 64 {
return Err(coded(
&ec::METERED_FACT_MALFORMED,
tonic::Code::InvalidArgument,
format!(
"fact.signature is {} byte(s); an Ed25519 signature is 64",
fact.signature.len()
),
));
}
let unknown = caps_holger_cannot_honour(fact.caps.keys());
if !unknown.is_empty() {
return Err(coded(
&ec::METERED_CAP_UNPOLICEABLE,
tonic::Code::InvalidArgument,
format!(
"caps name meter(s) holger cannot police per repository: {}. holger measures \
{} per repository; everything else is server-wide, and a cap on a server-wide \
number is a promise about something that is not this tenant's.",
unknown.join(", "),
PER_REPOSITORY_METERS.join(", ")
),
));
}
Ok(())
}
impl HolgerGrpc {
/// The ceilings in force for one repository, echoed onto `ReadUsage` so a
/// tenant can see its own rather than discovering it at a refusal.
///
/// A ceiling of `0` is carried through unchanged and means NO CEILING. It has
/// never meant "may hold nothing", and a reader that treats it that way would
/// refuse every write to a tenant whose order simply did not name one.
///
/// An unconfigured or unreadable store yields NO caps, which is the honest
/// answer: this box is not policing anything.
pub(crate) fn caps_for_repository(
&self,
repository: &str,
) -> Vec<crate::grpc::holger_proto::MeterCap> {
let Some(store) = self.entitlements.as_ref() else {
return Vec::new();
};
let Ok(Some(fact)) = store.get(repository) else {
return Vec::new();
};
let mut caps: Vec<crate::grpc::holger_proto::MeterCap> = fact
.caps
.iter()
.map(|(meter, limit)| crate::grpc::holger_proto::MeterCap {
meter: meter.clone(),
limit: *limit,
})
.collect();
caps.sort_by(|a, b| a.meter.cmp(&b.meter));
caps
}
/// The store, or the refusal that says this build was never configured with
/// monetize's key.
fn entitlement_store(&self) -> Result<&Arc<EntitlementStore>, Status> {
self.entitlements
.as_ref()
.filter(|s| s.keyed())
.ok_or_else(|| {
coded(
&ec::METERED_NOT_CONFIGURED,
tonic::Code::FailedPrecondition,
"this server has no monetize public key configured, so it cannot verify a \
signed entitlement — no plan is enforced",
)
})
}
}
#[tonic::async_trait]
impl EntitlementService for Arc<HolgerGrpc> {
async fn get(
&self,
request: Request<GetEntitlementRequest>,
) -> Result<Response<GetEntitlementResponse>, Status> {
let repo = request.get_ref().repository.clone();
// A plan is the provider's business with the tenant: ADMIN, like gunnar's.
self.authorize_admin(&request, &repo).await?;
let store = self.entitlement_store()?;
let fact = store
.get(&repo)
.map_err(|e| internal_err(format!("read the fact: {e}")))?;
Ok(Response::new(GetEntitlementResponse {
fact: fact.as_ref().map(to_wire),
}))
}
async fn set(
&self,
request: Request<SetEntitlementRequest>,
) -> Result<Response<SetEntitlementResponse>, Status> {
let source_ip = self.audit_peer(&request);
let fact = request.get_ref().fact.clone().ok_or_else(|| {
coded(
&ec::METERED_FACT_MALFORMED,
tonic::Code::InvalidArgument,
"request carries no fact",
)
})?;
let repo = fact.repository.clone();
let identity = self.authorize_admin(&request, &repo).await?;
let ident = identity
.as_ref()
.map(|id| id.subject.clone())
.unwrap_or_else(|| "anonymous".to_string());
well_formed(&fact)?;
let store = self.entitlement_store()?;
if !store.serves(&repo) {
return Err(coded(
&ec::METERED_UNKNOWN_REPOSITORY,
tonic::Code::NotFound,
format!("this server serves no repository '{repo}'"),
));
}
let signed = to_signed(&fact)?;
match store.put(&signed) {
Ok(()) => {
// The audit log IS holger's attestation trail: who presented the
// fact, for which repository, and what it came from.
self.record_audit(
AuditEvent::new(
&ident,
AuditAction::Entitlement,
&repo,
"",
&source_ip,
200,
0,
)
.with_detail(format!(
"state={:?} plan={} source={}",
signed.state, signed.plan, signed.source
)),
);
crate::grpc::functional_status(
"holger-grpc/entitlement_set",
"entitlement_stored",
true,
&repo,
);
Ok(Response::new(SetEntitlementResponse { state: fact.state }))
}
Err(PutRefused::Signature(why)) => {
self.record_audit(
AuditEvent::new(
&ident,
AuditAction::Entitlement,
&repo,
"",
&source_ip,
403,
0,
)
.with_detail(format!("signature refused: {why}")),
);
crate::grpc::functional_status(
"holger-grpc/entitlement_set",
"entitlement_stored",
false,
"signature did not verify",
);
Err(coded(
&ec::METERED_SIGNATURE_INVALID,
tonic::Code::PermissionDenied,
"the fact's signature was not made by this server's configured monetize key",
))
}
Err(PutRefused::Io(why)) => Err(internal_err(format!("store the fact: {why}"))),
}
}
async fn list(
&self,
request: Request<ListEntitlementsRequest>,
) -> Result<Response<ListEntitlementsResponse>, Status> {
// Server-wide read: every plan on the box. ADMIN, scoped to no repository.
self.authorize_admin(&request, "").await?;
let store = self.entitlement_store()?;
Ok(Response::new(ListEntitlementsResponse {
facts: store.list().iter().map(to_wire).collect(),
}))
}
/// **Delete every artifact of one repository** — see `Purge` in
/// `proto/holger-metered.proto` for the contract, [`crate::purge`] for the walk.
async fn purge(
&self,
request: Request<PurgeRepositoryRequest>,
) -> Result<Response<PurgeRepositoryResponse>, Status> {
let source_ip = self.audit_peer(&request);
let req = request.get_ref().clone();
let repo = req.repository.clone();
// The same door as `Set`: ADMIN, and a twin refuses it like every mutation.
let identity = self.authorize_admin(&request, &repo).await?;
let ident = identity
.as_ref()
.map(|id| id.subject.clone())
.unwrap_or_else(|| "anonymous".to_string());
if repo.is_empty() {
return Err(coded(
&ec::METERED_FACT_MALFORMED,
tonic::Code::InvalidArgument,
"request.repository is required",
));
}
let store = Arc::clone(self.entitlement_store()?);
if !store.serves(&repo) {
return Err(coded(
&ec::METERED_UNKNOWN_REPOSITORY,
tonic::Code::NotFound,
format!("this server serves no repository '{repo}'"),
));
}
// holger's OWN guard, independent of whoever asks: only a repository whose
// stored fact says it is not paid for may lose a byte.
let fact = store
.get(&repo)
.map_err(|e| internal_err(format!("read the fact: {e}")))?;
if let Err(why) = purgeable(&repo, fact.as_ref(), crate::clock::now_unix_ms()) {
self.record_audit(
AuditEvent::new(&ident, AuditAction::Purge, &repo, "", &source_ip, 412, 0)
.with_detail(format!("refused: {why}; reason given: {}", req.reason)),
);
return Err(coded(
&ec::METERED_PURGE_REFUSED,
tonic::Code::FailedPrecondition,
why,
));
}
let backend = self.get_repo(&repo)?;
let (walk_store, walk_repo, reason) =
(Arc::clone(&store), repo.clone(), req.reason.clone());
// The walk runs off the reactor and OWNS the gate: a caller that gives up
// (a deadline) does not stop it, and the gate opens only when it ends.
let run = tokio::task::spawn_blocking(move || {
purge_walk(&walk_store, backend.as_ref(), &walk_repo, &reason)
})
.await
.map_err(|e| internal_err(format!("the purge walk failed: {e}")))?;
let PurgeRun::Done {
walked,
failures,
record,
} = run
else {
return Ok(Response::new(PurgeRepositoryResponse {
repository: repo.clone(),
gate_released: false,
state: fact.as_ref().map(|f| to_wire(f).state).unwrap_or_default(),
detail: format!(
"a purge of `{repo}` is already walking; ask again when it has ended"
),
..Default::default()
}));
};
let after = store.get(&repo).ok().flatten();
let detail = format!(
"{} artifact(s), {} byte(s) deleted from `{repo}`{}",
walked.deleted,
walked.bytes,
if failures.is_empty() {
String::new()
} else {
format!("; {} could not be", failures.len())
}
);
self.record_audit(
AuditEvent::new(
&ident,
AuditAction::Purge,
&repo,
"",
&source_ip,
if failures.is_empty() { 200 } else { 500 },
walked.bytes,
)
.with_detail(format!("{detail}; reason: {}", req.reason)),
);
crate::grpc::functional_status(
"holger-grpc/entitlement_purge",
"repository_purged",
failures.is_empty(),
&detail,
);
Ok(Response::new(PurgeRepositoryResponse {
repository: repo,
artifacts_deleted: walked.deleted,
bytes_reclaimed: walked.bytes,
failures,
gate_released: true,
state: after.as_ref().map(|f| to_wire(f).state).unwrap_or_default(),
record,
detail,
}))
}
}
/// What one purge walk did ([`purge_walk`]).
pub enum PurgeRun {
/// A walk of the repository is already running (its gate is closed).
Busy,
Done {
walked: crate::purge::Walked,
failures: Vec<String>,
record: String,
},
}
/// **Empty one repository**, blocking: close its gate (or answer [`PurgeRun::Busy`]),
/// delete every artifact, and write the record its twin applies. The ONE walk both the
/// admin door (`EntitlementService.Purge`) and a pulled `purge_tenant` order run. The
/// caller has checked the stored fact says Suspended or Retention.
pub fn purge_walk(
store: &EntitlementStore,
backend: &dyn traits::RepositoryBackendTrait,
repo: &str,
reason: &str,
) -> PurgeRun {
let scope = store.root().to_path_buf();
let Some(gate) = crate::purge::close(&scope, repo) else {
return PurgeRun::Busy;
};
let _gate = gate;
let data_root = scope
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| scope.clone());
let w = crate::purge::walk(backend, &|_| crate::purge::Verdict::Delete);
let mut failures = w.failures.clone();
let mut record = String::new();
if failures.is_empty() {
let r = crate::purge::Record {
repository: repo.to_string(),
at_unix_ms: crate::clock::now_unix_ms().max(0) as u64,
reason: reason.to_string(),
artifacts: w.deleted,
bytes: w.bytes,
};
match crate::purge::write_record(&data_root, &r) {
Ok(p) => {
record = format!(
"{}/{}",
crate::purge::RECORDS_DIR,
p.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_default()
)
}
// Deleted here but the twin would never learn: not complete.
Err(e) => failures.push(format!(
"the purge record for the twin could not be written: {e}"
)),
}
}
PurgeRun::Done {
walked: w,
failures,
record,
}
}
/// The store path under a data root — one place, so the server and a test agree.
pub fn store_dir(data_root: &Path) -> PathBuf {
data_root.join(STORE_DIR)
}
// ── ENFORCEMENT: the paid `disk_bytes` limit on the write doors ──────────────
//
// One check, [`Quota`], asked by the HTTP door and the gRPC door alike before a
// body is read, so the two cannot disagree about whether a repository is full.
// No fact for a repository ⇒ nothing is policed (the open behaviour). A fact in
// `Suspended` or `Retention` refuses every upload. A fact with a positive
// `disk_bytes` cap refuses an upload that would take what the repository
// OCCUPIES (`metering::disk_bytes`, the reading monetize bills) past it — and
// the refusal carries the numbers and WHERE the bytes are, so the customer can
// see what to delete or what to buy. Deletes are never refused here: they are
// how a full repository gets back under its limit.
//
// Cost: an upload that FITS is answered from the last reading plus what was
// admitted since ([`Quota`]'s cache, re-walked every [`REWALK`]); only a
// refusal, or a stale reading, walks the repository's storage places.
/// **How long after `paid_until` holger still takes writes on its own: none.**
/// The owner's non-payment ruling (2026-09-29): a customer who stops paying is
/// READ-ONLY AT ONCE — monetize mails and deletes after the grace the ONE box decides
/// (`[nonpay] delete_after_days`, see [`purgeable`]), and no grace is granted in between. Past `paid_until` by holger's own
/// clock, a `Paid` or `Grace` fact nobody replaced is LAPSED. (It was 14 days,
/// the grace monetize's `order::Policy` once granted before suspending.)
pub const LAPSE_GRACE_MS: u64 = 0;
/// `YYYY-MM-DD` (UTC) of a unix-ms instant — the civil-from-days arithmetic,
/// so a refusal names the day and not a number.
pub fn civil_date(ms: u64) -> String {
let z = (ms / 86_400_000) as i64 + 719_468;
let era = z.div_euclid(146_097);
let doe = z - era * 146_097;
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = yoe + era * 400 + i64::from(m <= 2);
format!("{y:04}-{m:02}-{d:02}")
}
/// Pure: **a lapsed paid period** (g-holger-expiry, audit 2026-09-28; read-only at
/// once since the non-payment ruling of 2026-09-29). A `Paid` or `Grace` fact whose
/// `paid_until_unix_ms` (plus [`LAPSE_GRACE_MS`], which is zero) lies before
/// `now_ms` (holger's OWN clock, [`crate::clock`], which follows the estate's
/// signed time) was never renewed and never moved on by the front — so the
/// repository is READ-ONLY by holger's own reading: uploads refused, reads and
/// deletes go on. A front that stops pushing no longer keeps writes open.
pub fn lapsed(fact: &SignedFact, now_ms: i64) -> Option<String> {
if !matches!(fact.state, State::Paid | State::Grace) {
return None;
}
let until = fact.paid_until_unix_ms.filter(|t| *t > 0)?;
let closes = until.saturating_add(LAPSE_GRACE_MS);
if now_ms <= 0 || (now_ms as u64) <= closes {
return None;
}
Some(format!(
"repository `{}` was paid until {} (order {}) and no renewal has reached this server since, \
so it is read-only by its own clock: uploads are refused until it is paid again, reads go on",
fact.tenant.0,
civil_date(until),
fact.plan,
))
}
/// Pure: **may this repository be purged now?** — holger's own guard on a purge the ONE box
/// ordered. The ONE box decides WHEN (its nonpay grace, `[nonpay] delete_after_days`, 10 days
/// on the ONE box): holger keeps no day count of its own. It refuses only a repository that is
/// paid for by its stored fact: `Free` (or no fact), or a `Paid`/`Grace` fact whose period has
/// not ended by holger's own clock. A `Suspended` or `Retention` fact, or a `Paid`/`Grace` one
/// past its `paid_until` ([`lapsed`]: monetize stopped renewing it), is purged.
pub fn purgeable(repo: &str, fact: Option<&SignedFact>, now_ms: i64) -> Result<(), String> {
let Some(f) = fact else {
return Err(format!("repository `{repo}` is Free (no fact stored): a repository nobody sold is never purged, and nothing was deleted"));
};
if matches!(f.state, State::Suspended | State::Retention) || lapsed(f, now_ms).is_some() {
return Ok(());
}
Err(format!("repository `{repo}` is {:?} (order {}) and paid for by its own clock: only a lapsed, Suspended or Retention repository is purged, and nothing was deleted", f.state, f.plan))
}
/// Pure: the verdict for one upload. `None` ⇒ allowed; `Some(why)` ⇒ refused.
pub fn over_cap(fact: &SignedFact, current: u64, incoming: u64, where_: &str) -> Option<String> {
if matches!(fact.state, State::Suspended | State::Retention) {
return Some(format!(
"repository `{}` is {:?} (order {}): uploads are refused until it is paid again",
fact.tenant.0, fact.state, fact.plan
));
}
let limit = *fact.caps.get(crate::metering::METER_DISK_BYTES)?;
if limit == 0 {
return None;
}
let after = current.saturating_add(incoming);
if current >= limit || after > limit {
// The customer never sees a cap (the owner's rule, A10 row 18): the refusal says the
// room is full and where more is bought; the numbers are the operator's
// ([`operator_line`], logged by the write door).
let _ = where_;
return Some(format!(
"repository `{}` is full: delete something, or buy more room for it",
fact.tenant.0
));
}
None
}
/// **What the operator is told about a refused upload**: the numbers the customer is never
/// shown — what the repository would occupy, its cap, and where its bytes are.
pub fn operator_line(fact: &SignedFact, current: u64, incoming: u64, where_: &str) -> String {
let limit = fact
.caps
.get(crate::metering::METER_DISK_BYTES)
.copied()
.unwrap_or(0);
format!(
"repository `{}` would occupy {} bytes on disk (now {current} + {incoming} incoming); its paid disk_bytes cap is {limit} — where the bytes are: {where_}",
fact.tenant.0,
current.saturating_add(incoming)
)
}
/// The write-door check: the paid state plus where each repository keeps its bytes.
pub struct Quota {
store: Arc<EntitlementStore>,
/// Swapped (never edited in place) when a repository is made at run time (lane M4b).
places: std::sync::RwLock<Arc<crate::metering::StoragePlaces>>,
/// The last reading per repository, plus every upload admitted since.
readings: std::sync::Mutex<std::collections::HashMap<String, Reading>>,
/// The estate's pay page a paid refusal names (lane M4; `{tenant}` the repository).
/// Replaced live when a later feed names another ([`Quota::set_pay_page`]).
pay_page: std::sync::RwLock<Option<String>>,
}
/// What a repository occupied at `at`, plus what was admitted since — an
/// OVER-estimate as long as every write is admitted here (a failed or
/// deleted write only makes it larger than the truth, never smaller).
struct Reading {
bytes: u64,
at: std::time::Instant,
}
/// How long a reading may answer before the disk is walked again — the bound on
/// drift from bytes no admission saw (index sidecars, archive overhead) and on
/// how long a delete takes to be credited when nothing else forces a walk.
pub const REWALK: std::time::Duration = std::time::Duration::from_secs(30);
impl Quota {
pub fn new(store: Arc<EntitlementStore>, places: Arc<crate::metering::StoragePlaces>) -> Self {
Self {
store,
places: std::sync::RwLock::new(places),
readings: Default::default(),
pay_page: std::sync::RwLock::new(None),
}
}
/// Where a repository made at run time keeps its bytes (lane M4b), so its paid limit
/// measures it from its first upload.
pub fn add_places(&self, repository: &str, places: Vec<crate::repository::StoragePlace>) {
let mut cell = self.places.write_or_recover();
let mut next = (**cell).clone();
next.insert(repository.to_string(), places);
*cell = Arc::new(next);
}
/// The quota whose paid refusals name `pay_page` (an empty one: none).
pub fn with_pay_page(self, pay_page: Option<String>) -> Self {
self.set_pay_page(pay_page);
self
}
/// **The pay page, applied in place** when a later feed names it (A7 row 8).
pub fn set_pay_page(&self, pay_page: Option<String>) {
*self.pay_page.write_or_recover() = pay_page
.map(|p| p.trim().to_string())
.filter(|p| !p.is_empty());
}
/// The paid-state store this limit reads.
pub fn store(&self) -> &Arc<EntitlementStore> {
&self.store
}
/// A paid refusal, with where it is paid.
fn paid(&self, repository: &str, why: String) -> String {
match &*self.pay_page.read_or_recover() {
Some(p) => format!("{why}; it is paid at {}", p.replace("{tenant}", repository)),
None => why,
}
}
/// `Ok(())` ⇒ take the upload; `Err(line)` ⇒ refuse it with
/// [`holger_errcode::METERED_OVER_CAP`]. A store or disk that cannot be read
/// refuses too (fail closed): a paid limit nobody can check is not a limit.
pub fn admit(&self, repository: &str, incoming: u64) -> Result<(), String> {
// A repository being PURGED takes nothing, whatever its fact says now — a
// payment landing mid-walk must not open writes the walk would delete.
if crate::purge::is_closed(self.store.root(), repository) {
return Err(crate::purge::gated_refusal(repository));
}
// No monetize key has reached this box yet: nothing is paid for, nothing is policed.
if !self.store.keyed() {
return Ok(());
}
let fact = match self.store.get(repository) {
Ok(Some(f)) => f,
Ok(None) => return Ok(()),
Err(e) => {
return Err(format!(
"repository `{repository}`: the paid state cannot be read: {e}"
))
}
};
// A paid period nobody renewed closes writes by holger's own clock.
if let Some(why) = lapsed(&fact, crate::clock::now_unix_ms()) {
return Err(self.paid(repository, why));
}
if fact
.caps
.get(crate::metering::METER_DISK_BYTES)
.copied()
.unwrap_or(0)
== 0
&& !matches!(fact.state, State::Suspended | State::Retention)
{
return Ok(());
}
// A suspended plan needs no measurement to refuse.
if matches!(fact.state, State::Suspended | State::Retention) {
return Err(self.paid(
repository,
over_cap(&fact, 0, incoming, "").unwrap_or_default(),
));
}
let limit = fact
.caps
.get(crate::metering::METER_DISK_BYTES)
.copied()
.unwrap_or(0);
// FAST ACCEPT: a fresh reading that says it fits takes the upload with no
// walk, and counts it. A REFUSAL is never decided from the cache — only
// from the disk, walked now, so a delete is credited before anybody is
// told no.
{
let mut readings = self.readings.lock_or_recover();
if let Some(r) = readings.get_mut(repository) {
if r.at.elapsed() < REWALK
&& r.bytes < limit
&& r.bytes.saturating_add(incoming) <= limit
{
r.bytes += incoming;
return Ok(());
}
}
}
let all = Arc::clone(&self.places.read_or_recover());
let places = all.get(repository).map(Vec::as_slice).unwrap_or(&[]);
let (current, where_) = crate::metering::disk_bytes(places).map_err(|e| {
format!("repository `{repository}`: what it occupies cannot be measured: {e}")
})?;
let verdict = over_cap(&fact, current, incoming, &where_);
if verdict.is_some() {
log::info!(
"metered: upload refused, {}",
operator_line(&fact, current, incoming, &where_)
);
}
let counted = if verdict.is_none() {
current.saturating_add(incoming)
} else {
current
};
self.readings.lock_or_recover().insert(
repository.to_string(),
Reading {
bytes: counted,
at: std::time::Instant::now(),
},
);
match verdict {
None => Ok(()),
Some(why) => Err(self.paid(repository, why)),
}
}
}
/// **The paid limit on EVERY programmatic write** — a repository's backend,
/// wrapped once at wiring (metered build only), so the slow paths that write
/// with `put` and never pass a door are policed by the same [`Quota`]: a mirror
/// pull, a replication copy, a job's finalized upload, a proxy's cache fill (the
/// proxy's primary IS this wrapped backend) and a gRPC `PutArtifact`. What each
/// caller does with the refusal is its own, already-written failure path: the
/// mirror logs the put as failed, replication reports the row `Failed`, the job
/// fails, and a proxy serves the bytes it fetched WITHOUT caching them.
///
/// **A twin is never policed.** It holds the primary's bytes because the primary
/// took them under the primary's limit, and it carries the primary's facts
/// (the estate copies `holger-entitlements/`); refusing its follow would make a
/// twin that silently stops being a copy. A box configured as a twin is never
/// wrapped; one ELECTED later is recognised on every write by its role record
/// (`twin::recorded_role`), so the exemption holds without a restart.
///
/// A [`traits::Decorator`]: every trait method reaches the inner backend by
/// construction; only the two writes that carry bytes are overridden.
pub struct QuotaGuard {
inner: std::sync::Arc<dyn traits::RepositoryBackendTrait>,
repository: String,
quota: std::sync::Arc<Quota>,
data_root: PathBuf,
}
impl QuotaGuard {
pub fn wrap(
inner: std::sync::Arc<dyn traits::RepositoryBackendTrait>,
repository: impl Into<String>,
quota: std::sync::Arc<Quota>,
data_root: PathBuf,
) -> std::sync::Arc<dyn traits::RepositoryBackendTrait> {
std::sync::Arc::new(Self {
inner,
repository: repository.into(),
quota,
data_root,
})
}
fn admit(&self, incoming: u64) -> anyhow::Result<()> {
if unpoliced(&self.data_root).is_some() {
return Ok(());
}
self.quota
.admit(&self.repository, incoming)
.map_err(|why| anyhow::anyhow!(holger_errcode::METERED_OVER_CAP.tag(why)))
}
}
/// **The writes the paid limit never polices on this box, and why** — `None`: every
/// write is policed.
///
/// * a TWIN: it holds the primary's bytes, taken under the primary's limit;
/// * a box being RECHARGED from its twin (`twin_follow::RECHARGING`): it serves
/// nothing until the recharge is whole (`holger-twin-15`), so every write it takes
/// is the recharge's own — the twin's copy of bytes the dead primary took under its
/// limit. MEASURED by the private-holger-ops estate-box gate (2026-10-04, R1): a
/// lapsed repository (Suspended: read-only, its data still the customer's) lost
/// every artifact to a recharge — each restore put was refused `[holger-metered-6]`
/// and the new main served 404 for what the old one held.
pub fn unpoliced(data_root: &std::path::Path) -> Option<&'static str> {
if matches!(crate::twin::recorded_role(data_root), Ok(Some(ref r)) if r.role.is_twin()) {
return Some("a twin holds the primary's bytes");
}
if data_root.join(crate::twin_follow::RECHARGING).exists() {
return Some("a recharge restores what the primary held");
}
None
}
impl traits::Decorator for QuotaGuard {
fn name(&self) -> &str {
self.inner.name()
}
fn format(&self) -> traits::ArtifactFormat {
self.inner.format()
}
fn is_writable(&self) -> bool {
self.inner.is_writable()
}
fn caps(&self) -> traits::Caps {
self.inner.caps()
}
fn around<T>(
&self,
_: traits::Access,
call: impl FnOnce(&dyn traits::RepositoryBackendTrait) -> anyhow::Result<T>,
) -> anyhow::Result<T> {
call(self.inner.as_ref())
}
fn put(&self, id: &traits::ArtifactId, data: &[u8]) -> anyhow::Result<()> {
self.admit(data.len() as u64)?;
self.inner.put(id, data)
}
fn put_from_reader(
&self,
id: &traits::ArtifactId,
body: &mut dyn std::io::Read,
size_hint: Option<u64>,
) -> anyhow::Result<()> {
if self.inner.caps().streaming_uploads {
self.admit(size_hint.unwrap_or(0))?;
}
self.inner.put_from_reader(id, body, size_hint)
}
/// Reads are not policed: a read in pieces is the inner backend's (lane HOL-RSS).
fn stream_get(
&self,
suburl: &str,
piece_bytes: usize,
) -> anyhow::Result<Option<traits::StreamedGet>> {
self.inner.stream_get(suburl, piece_bytes)
}
// A delete is never refused (it is how a full repository gets back under),
// and the two HTTP entries are not checked here: the HTTP door asked the
// same Quota before it read the body (`GatewaySettings::admit_upload`).
}
#[cfg(test)]
mod quota_tests {
use super::*;
/// ★ **A recharge restores a lapsed repository whole** (the estate-box gate's R1,
/// 2026-10-04): while the box is being recharged (`holger-recharging`), no write is
/// policed; before and after it, every write is.
#[test]
fn a_box_being_recharged_is_not_policed_and_one_serving_is() {
let d = tempfile::tempdir().unwrap();
assert_eq!(unpoliced(d.path()), None, "a serving primary is policed");
std::fs::write(
d.path().join(crate::twin_follow::RECHARGING),
"recharging from 10.6.0.13:50052\n",
)
.unwrap();
assert!(unpoliced(d.path()).is_some_and(|w| w.contains("recharge")));
std::fs::remove_file(d.path().join(crate::twin_follow::RECHARGING)).unwrap();
assert_eq!(
unpoliced(d.path()),
None,
"the recharge whole, the limit is back"
);
}
fn fact(state: State, cap: Option<u64>) -> SignedFact {
let mut caps = BTreeMap::new();
if let Some(c) = cap {
caps.insert(crate::metering::METER_DISK_BYTES.to_string(), c);
}
SignedFact {
tenant: TenantId("crates".into()),
plan: "holger/crates/2026-09-27+disk_bytes".into(),
state,
paid_until_unix_ms: None,
caps,
source: "operator".into(),
signature: vec![0; 64],
issued_unix_ms: None,
issued_signature: Vec::new(),
}
}
/// ★ **The customer never sees a cap** (A10 row 18, the owner's rule): over its room an
/// upload is refused with words that say the room is full and that more is bought, and no
/// number or disk path; the numbers and where the bytes are go to the operator's log line.
#[test]
fn under_the_limit_is_taken_and_over_it_is_refused_without_the_cap() {
let f = fact(State::Paid, Some(1000));
assert_eq!(
over_cap(&f, 400, 600, "archive /d/crates.znippy (400 B)"),
None,
"exactly the limit fits"
);
let why = over_cap(&f, 400, 601, "archive /d/crates.znippy (400 B)").unwrap();
assert!(
why.contains("`crates`") && why.contains("full") && why.contains("more room"),
"{why}"
);
for secret in [
"1000",
"1001",
"601",
"400",
"/d/crates.znippy",
"limit",
"bytes",
] {
assert!(
!why.contains(secret),
"the refusal names no cap ({secret}): {why}"
);
}
let said = operator_line(&f, 400, 601, "archive /d/crates.znippy (400 B)");
assert!(
said.contains("1001") && said.contains("1000") && said.contains("/d/crates.znippy"),
"the operator is told: {said}"
);
assert!(
over_cap(&f, 1000, 0, "x").is_some(),
"a full repository takes no upload of unknown size"
);
}
#[test]
fn an_upload_that_fits_needs_no_walk_and_a_refusal_always_reads_the_disk() {
use ed25519_dalek::Signer;
let d = tempfile::tempdir().unwrap();
let files = d.path().join("crates");
std::fs::create_dir_all(&files).unwrap();
std::fs::write(files.join("a.tar"), vec![1u8; 1000]).unwrap();
let key = ed25519_dalek::SigningKey::from_bytes(&[5u8; 32]);
let roster = Arc::new(BTreeSet::from(["crates".to_string()]));
let store = Arc::new(
EntitlementStore::new(d.path().join("e"), roster, &key.verifying_key().to_bytes())
.unwrap(),
);
let mut f = fact(State::Paid, Some(1 << 20));
f.signature = key
.sign(&monetize_embed::signing::fact_message(&f))
.to_bytes()
.to_vec();
store.put(&f).unwrap();
let places: crate::metering::StoragePlaces = [(
"crates".to_string(),
vec![crate::repository::StoragePlace {
kind: "files",
path: files.clone(),
}],
)]
.into_iter()
.collect();
let q = Quota::new(store, Arc::new(places));
let on_disk = crate::metering::disk_bytes(&q.places.read().unwrap()["crates"])
.unwrap()
.0;
q.admit("crates", 1000).unwrap();
// Something grows the disk behind the cache's back: the next FITTING upload
// is still answered from the reading (no walk) …
std::fs::write(files.join("b.tar"), vec![1u8; 900 << 10]).unwrap();
q.admit("crates", 1000).unwrap();
assert_eq!(
q.readings.lock().unwrap()["crates"].bytes,
on_disk + 2000,
"counted, not walked"
);
// … but one the reading says would NOT fit is decided by the disk, walked
// now — and a delete is credited before anybody is told no.
std::fs::remove_file(files.join("b.tar")).unwrap();
// The reading (on_disk + 2000) says this is 500 over; the disk says it fits.
q.admit("crates", (1 << 20) - on_disk - 1500)
.expect("the delete was credited by a fresh walk");
assert_eq!(
q.readings.lock().unwrap()["crates"].bytes,
(1 << 20) - 1500,
"re-read from the disk"
);
let why = q.admit("crates", 1 << 20).unwrap_err();
assert!(
why.contains("is full")
&& !why.contains("1048576")
&& !why.contains(&files.display().to_string()),
"refused, and no cap or path shown to the customer (A10 row 18): {why}"
);
}
/// ★ g-holger-expiry (audit 2026-09-28): a Paid fact whose period ended long
/// ago, and that nobody replaced (the front stopped pushing), must NOT keep
/// write access. holger locks writes by its OWN clock once the paid period
/// is over — READ-ONLY: uploads refused, reads go on.
#[test]
fn a_lapsed_paid_fact_closes_writes_by_holgers_own_clock() {
use ed25519_dalek::Signer;
let d = tempfile::tempdir().unwrap();
let key = ed25519_dalek::SigningKey::from_bytes(&[7u8; 32]);
let roster = Arc::new(BTreeSet::from(["crates".to_string()]));
let store = Arc::new(
EntitlementStore::new(d.path().join("e"), roster, &key.verifying_key().to_bytes())
.unwrap(),
);
let q = Quota::new(
store.clone(),
Arc::new(crate::metering::StoragePlaces::default()),
);
let now = crate::clock::now_unix_ms().max(0) as u64;
// Paid until 60 days ago, never renewed: past any grace.
let mut f = fact(State::Paid, None);
f.paid_until_unix_ms = Some(now - 60 * 86_400_000);
f.signature = key
.sign(&monetize_embed::signing::fact_message(&f))
.to_bytes()
.to_vec();
store.put(&f).unwrap();
let why = q
.admit("crates", 1)
.expect_err("a paid period that ended 60 days ago still took an upload");
assert!(
why.contains("read-only") && why.contains("paid until"),
"{why}"
);
// Paid until yesterday: no grace since the non-payment ruling — refused.
f.paid_until_unix_ms = Some(now - 86_400_000);
f.signature = key
.sign(&monetize_embed::signing::fact_message(&f))
.to_bytes()
.to_vec();
store.put(&f).unwrap();
q.admit("crates", 1)
.expect_err("a period that ended yesterday is read-only at once");
// Paid into the future: taken.
f.paid_until_unix_ms = Some(now + 30 * 86_400_000);
f.signature = key
.sign(&monetize_embed::signing::fact_message(&f))
.to_bytes()
.to_vec();
store.put(&f).unwrap();
q.admit("crates", 1)
.expect("a running period takes an upload");
}
/// ★ The owner's non-payment ruling (2026-09-29): a customer who stops paying
/// is READ-ONLY AT ONCE — one millisecond past `paid_until` by holger's own
/// clock, not fourteen days later. monetize mails, and deletes after the ONE box's
/// grace; holger's own reading must not hand out two free weeks of writes.
#[test]
fn a_paid_fact_one_ms_past_paid_until_is_lapsed() {
let until = 1_790_553_600_000u64;
let mut f = fact(State::Paid, None);
f.paid_until_unix_ms = Some(until);
assert_eq!(
lapsed(&f, until as i64),
None,
"AT paid_until the period still runs"
);
let why = lapsed(&f, until as i64 + 1).expect("one ms past paid_until is read-only");
assert!(
why.contains("read-only") && why.contains("2026-09-28"),
"{why}"
);
assert!(
!why.contains("grace"),
"no grace is granted, so none is named: {why}"
);
}
#[test]
fn lapsed_is_paid_or_grace_past_the_period_only() {
let day = 86_400_000i64;
let mut f = fact(State::Paid, None);
assert_eq!(
lapsed(&f, 100 * day),
None,
"no paid_until: nothing to lapse"
);
f.paid_until_unix_ms = Some((50 * day) as u64);
assert_eq!(lapsed(&f, 49 * day), None, "before the end: running");
assert!(
lapsed(&f, 60 * day).unwrap().contains("read-only"),
"10 days past: lapsed, no grace"
);
assert!(
lapsed(&f, 65 * day).unwrap().contains("read-only"),
"15 days past: lapsed"
);
f.state = State::Grace;
assert!(lapsed(&f, 65 * day).is_some());
f.state = State::Free;
assert_eq!(lapsed(&f, 65 * day), None, "a free plan has no period");
f.state = State::Suspended;
assert_eq!(
lapsed(&f, 65 * day),
None,
"Suspended is refused by over_cap already, with its own sentence"
);
f.state = State::Paid;
assert_eq!(
lapsed(&f, 0),
None,
"no clock reading: never lock on a guess"
);
assert_eq!(civil_date(1_790_553_600_000), "2026-09-28");
assert_eq!(civil_date(0), "1970-01-01");
}
/// ★ While a purge walks a repository, EVERY upload is refused by name — even
/// one to a repository whose fact now says Paid (a payment landing mid-walk)
/// or that has no fact at all — and the gate opens when the walk ends.
#[test]
fn a_repository_being_purged_takes_no_upload_whatever_its_fact_says() {
use ed25519_dalek::Signer;
let d = tempfile::tempdir().unwrap();
let key = ed25519_dalek::SigningKey::from_bytes(&[8u8; 32]);
let roster = Arc::new(BTreeSet::from(["crates".to_string(), "free".to_string()]));
let store = Arc::new(
EntitlementStore::new(d.path().join("e"), roster, &key.verifying_key().to_bytes())
.unwrap(),
);
let mut f = fact(State::Paid, None);
f.signature = key
.sign(&monetize_embed::signing::fact_message(&f))
.to_bytes()
.to_vec();
store.put(&f).unwrap();
let q = Quota::new(
store.clone(),
Arc::new(crate::metering::StoragePlaces::default()),
);
q.admit("crates", 1).expect("paid, not purged: taken");
let gate = crate::purge::close(store.root(), "crates").unwrap();
let why = q
.admit("crates", 1)
.expect_err("a repository being purged took an upload");
assert!(
why.contains("being purged") && why.contains("crates"),
"{why}"
);
let other = crate::purge::close(store.root(), "free").unwrap();
assert!(
q.admit("free", 1).is_err(),
"no fact is no exemption from the gate"
);
drop((gate, other));
q.admit("crates", 1)
.expect("the gate opens when the walk ends");
q.admit("free", 1).expect("and for the factless one");
}
#[test]
fn no_cap_is_not_policed_and_a_suspended_plan_takes_nothing() {
assert_eq!(
over_cap(&fact(State::Paid, None), u64::MAX / 2, 1 << 40, "x"),
None
);
assert_eq!(over_cap(&fact(State::Paid, Some(0)), 5, 5, "x"), None);
assert!(over_cap(&fact(State::Suspended, None), 0, 1, "x")
.unwrap()
.contains("Suspended"));
assert!(over_cap(&fact(State::Retention, Some(1 << 40)), 0, 1, "x").is_some());
}
}
#[cfg(test)]
mod guard_tests {
use super::*;
/// Every trait method reaches the wrapped backend, by construction: around
/// a backend with every cap the guard answers every method, and around a
/// core-only one it claims and answers none.
#[test]
fn every_method_reaches_the_wrapped_backend() {
let d = tempfile::tempdir().unwrap();
// A recharging box polices nothing, so the probes meet the backend.
std::fs::write(d.path().join(crate::twin_follow::RECHARGING), "probe\n").unwrap();
let key = ed25519_dalek::SigningKey::from_bytes(&[8u8; 32]);
let store = Arc::new(
EntitlementStore::new(
d.path().join("e"),
Arc::new(BTreeSet::new()),
&key.verifying_key().to_bytes(),
)
.unwrap(),
);
let quota = Arc::new(Quota::new(
store,
Arc::new(crate::metering::StoragePlaces::default()),
));
let all: Arc<dyn traits::RepositoryBackendTrait> =
Arc::new(traits::backend::conformance::Everything::new("all"));
let core: Arc<dyn traits::RepositoryBackendTrait> =
Arc::new(traits::backend::conformance::Everything::core_only("core"));
for inner in [all, core] {
let g = QuotaGuard::wrap(inner, "r", Arc::clone("a), d.path().to_path_buf());
traits::backend::conformance::check(g.as_ref()).unwrap();
}
}
}