use std::collections::BTreeSet;
use std::path::Path;
use schemars::JsonSchema;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::error::{Error, Result};
use crate::providers::Providers;
mod derive;
pub(crate) mod diagnostics;
pub(crate) mod evidence;
mod manifests;
mod matcher;
pub(crate) mod scope;
pub(crate) mod screen;
pub(crate) mod visibility;
mod words;
pub const PRIVATE_TERMS_FILE: &str = "private-terms.toml";
pub const PRIVATE_TERMS_VERSION: u32 = 1;
pub const BOUNDARY_SCHEMA_VERSION: u32 = 1;
#[derive(
Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
)]
#[serde(rename_all = "lowercase")]
pub enum Visibility {
Public,
Private,
#[default]
Unknown,
}
impl Visibility {
pub fn effective(self) -> Visibility {
match self {
Visibility::Public => Visibility::Public,
Visibility::Private | Visibility::Unknown => Visibility::Private,
}
}
pub fn is_unknown(&self) -> bool {
*self == Visibility::Unknown
}
}
#[derive(
Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
)]
#[serde(rename_all = "lowercase")]
pub enum Observation {
Host,
Override,
#[default]
Unknown,
}
impl Observation {
pub fn is_unknown(&self) -> bool {
*self == Observation::Unknown
}
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
)]
#[serde(rename_all = "kebab-case")]
pub enum TermMode {
Substring,
WholeWord,
OwnerNameOnly,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct TermRule {
pub term: String,
pub mode: TermMode,
#[serde(default)]
pub case_sensitive: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub enum TermScope {
#[default]
Registry,
Identities(Vec<String>),
}
impl TermScope {
pub fn is_registry(&self) -> bool {
*self == TermScope::Registry
}
}
impl Serialize for TermScope {
fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
match self {
TermScope::Registry => serializer.serialize_none(),
TermScope::Identities(identities) => identities.serialize(serializer),
}
}
}
impl<'de> Deserialize<'de> for TermScope {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
Ok(match Option::<Vec<String>>::deserialize(deserializer)? {
None => TermScope::Registry,
Some(identities) => TermScope::Identities(identities),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct BoundaryInput {
pub destination: Visibility,
#[serde(default)]
pub text: Vec<String>,
#[serde(default)]
pub paths: Vec<String>,
#[serde(default)]
pub metadata: Vec<String>,
#[serde(default, skip_serializing_if = "TermScope::is_registry")]
#[schemars(with = "Option<Vec<String>>")]
pub scope: TermScope,
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
)]
#[serde(rename_all = "kebab-case")]
pub enum Surface {
Text,
Path,
Metadata,
Content,
Removal,
CommitMessage,
Branch,
Title,
Body,
}
impl Surface {
pub fn describe(self) -> &'static str {
match self {
Surface::Text => "its text",
Surface::Path => "a path",
Surface::Metadata => "its metadata",
Surface::Content => "an added line",
Surface::Removal => "a removal its public destination does not already carry",
Surface::CommitMessage => "a commit message",
Surface::Branch => "its branch name",
Surface::Title => "its title",
Surface::Body => "its body",
}
}
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
)]
#[serde(rename_all = "kebab-case")]
pub enum Unavailability {
Registry,
Rules,
Unregistered,
Declarations,
Matcher,
History,
}
impl Unavailability {
pub fn describe(self) -> &'static str {
match self {
Unavailability::Registry => "the registry could not be read",
Unavailability::Rules => "the rules file could not be read",
Unavailability::Unregistered => "a repository in its term scope is not registered",
Unavailability::Declarations => {
"a private repository's committed manifests or term declaration could not be read"
}
Unavailability::Matcher => "the terms could not be compiled",
Unavailability::History => "what is being published could not be read",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "verdict", rename_all = "kebab-case")]
pub enum BoundaryVerdict {
Pass,
Refuse {
surface: Surface,
},
Unavailable {
reason: Unavailability,
},
}
pub(crate) const REFUSED_SENTENCE: &str = "public output carries a term of a private repository";
pub(crate) const UNAVAILABLE_SENTENCE: &str = "the public boundary check is unavailable";
impl BoundaryVerdict {
pub fn reason(&self) -> Option<String> {
match self {
BoundaryVerdict::Pass => None,
BoundaryVerdict::Refuse { surface } => {
Some(format!("{REFUSED_SENTENCE} in {}", surface.describe()))
}
BoundaryVerdict::Unavailable { reason } => {
Some(format!("{UNAVAILABLE_SENTENCE}: {}", reason.describe()))
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "kebab-case")]
pub enum Evidence {
Term {
surface: Surface,
at: String,
rule: TermRule,
identity: String,
},
Unavailable {
reason: Unavailability,
#[serde(default, skip_serializing_if = "Option::is_none")]
identity: Option<String>,
detail: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct RepositoryBoundary {
pub visibility: Visibility,
pub terms: Vec<TermRule>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PrivateTerms {
pub schema_version: u32,
#[serde(default)]
pub terms: Vec<String>,
#[serde(default)]
pub exceptions: Vec<TermException>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TermException {
pub term: String,
pub action: ExceptionAction,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ExceptionAction {
Drop,
WholeWord,
CaseSensitive,
OwnerNameOnly,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TermSource {
pub identity: String,
pub owner: Option<String>,
pub name: String,
pub packages: BTreeSet<String>,
pub declaration: Option<PrivateTerms>,
}
impl TermSource {
pub fn from_committed(checkout: &Path, identity: &str) -> Result<TermSource> {
manifests::committed_source(checkout, identity).map_err(|detail| Error::Invalid {
reason: format!("cannot read the committed term declarations: {detail}"),
})
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PublicNames {
pub repositories: BTreeSet<String>,
pub owners: BTreeSet<String>,
}
pub fn derive_terms(sources: &[TermSource], public: &PublicNames) -> Result<Vec<TermRule>> {
let mut rules = Vec::new();
for source in sources {
let derived = derive::derive(source, public).map_err(|detail| Error::Invalid {
reason: format!("a term declaration is refused: {detail}"),
})?;
rules.extend(derived);
}
rules.sort();
rules.dedup();
Ok(rules)
}
pub struct TermMatcher(matcher::Matcher);
impl TermMatcher {
pub fn new(rules: Vec<TermRule>) -> Result<TermMatcher> {
matcher::Matcher::new(rules)
.map(TermMatcher)
.map_err(|unbuildable| Error::Invalid {
reason: format!("the terms could not be compiled: {}", unbuildable.0),
})
}
pub fn rules(&self) -> &[TermRule] {
self.0.rules()
}
pub fn find(&self, text: &str) -> Vec<usize> {
self.0.find(text)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct InspectRequest {
pub repository: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct InspectAnswer {
pub visibility: Visibility,
}
pub fn boundary_schema() -> serde_json::Value {
serde_json::json!({
"schema_version": BOUNDARY_SCHEMA_VERSION,
"inspect": {
"input": schemars::schema_for!(InspectRequest),
"output": schemars::schema_for!(InspectAnswer),
},
"check": {
"input": schemars::schema_for!(BoundaryInput),
"output": schemars::schema_for!(BoundaryVerdict),
},
})
}
pub fn repository_boundary(repository: &str) -> Result<RepositoryBoundary> {
repository_boundary_with(&Providers::real(), repository)
}
pub(crate) fn repository_boundary_with(
providers: &Providers<'_>,
repository: &str,
) -> Result<RepositoryBoundary> {
let visibility = visibility::refresh_named(providers.hosting, repository)?;
if visibility.effective() == Visibility::Public {
return Ok(RepositoryBoundary {
visibility,
terms: Vec::new(),
});
}
let registry = crate::store::load()?;
let resolution = crate::store::resolve(®istry, repository)?;
let derived = scope::derive(&TermScope::Identities(vec![resolution.key.clone()]));
match derived {
Ok(derived) => Ok(RepositoryBoundary {
visibility,
terms: derived.rules,
}),
Err(failed) => Err(Error::Invalid {
reason: format!(
"the terms of {repository:?} could not be derived: {}",
failed.reason.describe()
),
}),
}
}
pub fn inspect_repository(request: &InspectRequest) -> Result<InspectAnswer> {
inspect_repository_with(&Providers::real(), request)
}
pub(crate) fn inspect_repository_with(
providers: &Providers<'_>,
request: &InspectRequest,
) -> Result<InspectAnswer> {
Ok(InspectAnswer {
visibility: visibility::refresh_named(providers.hosting, &request.repository)?,
})
}
pub fn check_public_output(input: BoundaryInput) -> Result<BoundaryVerdict> {
check_public_output_with_evidence(input, &mut Vec::new())
}
pub fn check_public_output_with_evidence(
input: BoundaryInput,
evidence: &mut Vec<Evidence>,
) -> Result<BoundaryVerdict> {
if input.destination.effective() != Visibility::Public {
return Ok(BoundaryVerdict::Pass);
}
let derived = match scope::derive(&input.scope) {
Ok(derived) => derived,
Err(failed) => return Ok(failed.verdict(evidence)),
};
let matcher = match derived.matcher() {
Ok(matcher) => matcher,
Err(failed) => return Ok(failed.verdict(evidence)),
};
let mut verdict = BoundaryVerdict::Pass;
for (surface, entries) in [
(Surface::Text, &input.text),
(Surface::Path, &input.paths),
(Surface::Metadata, &input.metadata),
] {
for (index, entry) in entries.iter().enumerate() {
for rule in matcher.find(entry) {
if verdict == BoundaryVerdict::Pass {
verdict = BoundaryVerdict::Refuse { surface };
}
evidence.push(derived.evidence(surface, index.to_string(), rule));
}
}
}
Ok(verdict)
}