use std::collections::BTreeSet;
use super::WITHHELD;
use super::names::{
AUDIT_FIELDS, Action, Class, Pair, SCHEMA_KEYWORDS, SCHEMA_VALUE_KEYWORDS, SUBSCHEMA_KEYWORDS,
action, classify, header_action, header_class, header_item_action, is_secret_location,
member_class, normalised,
};
use super::values::{
has_private_key, header_credential_spans, key_value_credential_spans, looks_like_json_text,
scheme_credential_spans, shows_credential, urls_in,
};
pub use super::view::is_schema;
use super::view::{Json, Kind, is_empty, shape};
pub trait Document: Json {
fn document(text: &str) -> Option<Self>;
}
pub fn is_container<J: Json>(value: &J) -> bool {
matches!(value.kind(), Kind::Object | Kind::Array)
}
pub fn pair<J: Json>(value: &J) -> Option<Pair> {
Pair::of(
value
.members()
.into_iter()
.map(|(name, child)| (name, child.text())),
)
}
pub fn located<J: Json>(value: &J) -> bool {
is_secret_location(value.members().into_iter().map(|(name, _)| name))
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Step {
Member(String),
Item(usize),
Text,
}
pub type Path = Vec<Step>;
pub fn withheld<J: Document>(doc: &J) -> bool {
is_container(doc) && violations(doc).is_empty()
}
pub fn violations<J: Document>(doc: &J) -> BTreeSet<Path> {
let mut checker = Checker::default();
checker.walk(doc);
checker.found
}
#[derive(Default)]
struct Checker {
at: Path,
found: BTreeSet<Path>,
}
impl Checker {
fn fail(&mut self) {
self.found.insert(self.at.clone());
}
fn under(&mut self, step: Step, check: impl FnOnce(&mut Self)) {
self.at.push(step);
check(self);
self.at.pop();
}
fn hidden<J: Document>(&mut self, value: &J) {
if value.text() != Some(WITHHELD) {
self.fail();
}
}
fn take<J: Document>(&mut self, action: Action, value: &J) {
match action {
Action::Read => self.walk(value),
Action::Headers => self.header_map(value),
Action::Keep => {}
Action::Withhold(_) => self.hidden(value),
}
}
fn member<J: Document>(&mut self, name: &str, located: bool, child: &J) {
if AUDIT_FIELDS.contains(&normalised(name).as_str()) {
if child.kind() != Kind::Null {
self.hidden(child);
}
return;
}
self.take(action(member_class(name, located), shape(child)), child);
}
fn header_map<J: Document>(&mut self, value: &J) {
match value.kind() {
Kind::Object => {
let pair = pair(value);
for (name, child) in value.members() {
self.under(Step::Member(name.to_owned()), |checker| match &pair {
Some(pair) if pair.names_member(name, child.text()) => checker.walk(child),
Some(pair) if name == super::names::PAIR_VALUE => {
checker.take(header_action(pair.class(), shape(child)), child);
}
_ => checker.take(header_action(header_class(name), shape(child)), child),
});
}
}
Kind::Array => {
for (at, item) in value.items().into_iter().enumerate() {
self.under(Step::Item(at), |checker| {
if item.kind() == Kind::Object {
checker.header_map(item);
} else {
checker.take(header_item_action(item.text(), shape(item)), item);
}
});
}
}
Kind::String => match value.text().and_then(J::document) {
Some(inner) => self.under(Step::Text, |checker| checker.header_map(&inner)),
None => self.hidden(value),
},
_ => {}
}
}
fn property_map<J: Document>(&mut self, map: &J) {
for (name, child) in map.members() {
self.under(Step::Member(name.to_owned()), |checker| {
if is_schema(child) {
checker.schema(name, child);
} else {
checker.member(name, false, child);
}
});
}
}
fn instance<J: Document>(&mut self, name: &str, own: &str, child: &J) {
if classify(own) >= classify(name) {
self.member(own, false, child);
} else {
self.member(name, false, child);
}
}
fn describes_no_credential<J: Document>(&mut self, value: &J) {
match value.kind() {
Kind::String => {
if value.text().is_some_and(shows_credential) {
self.fail();
}
}
Kind::Array => {
for (at, item) in value.items().into_iter().enumerate() {
self.under(Step::Item(at), |checker| {
checker.describes_no_credential(item)
});
}
}
Kind::Object => {
for (name, child) in value.members() {
self.under(Step::Member(name.to_owned()), |checker| {
checker.describes_no_credential(child);
});
}
}
_ => {}
}
}
fn schema<J: Document>(&mut self, name: &str, value: &J) {
let credential = classify(name) > Class::Plain;
for (keyword, child) in value.members() {
self.under(Step::Member(keyword.to_owned()), |checker| {
if keyword == "properties" && child.kind() == Kind::Object {
checker.property_map(child);
} else if SUBSCHEMA_KEYWORDS.contains(&keyword) {
match child.kind() {
Kind::Object => checker.schema(name, child),
Kind::Array => {
for (at, item) in child.items().into_iter().enumerate() {
checker.under(Step::Item(at), |checker| {
if item.kind() == Kind::Object {
checker.schema(name, item);
} else {
checker.instance(name, keyword, item);
}
});
}
}
_ => checker.instance(name, keyword, child),
}
} else if SCHEMA_KEYWORDS.contains(&keyword) {
checker.walk(child);
if credential {
checker.describes_no_credential(child);
}
} else if SCHEMA_VALUE_KEYWORDS.contains(&keyword) && is_empty(child) {
} else {
checker.instance(name, keyword, child);
}
});
}
}
fn walk<J: Document>(&mut self, value: &J) {
match value.kind() {
Kind::Object => {
let pair = pair(value);
let located = located(value);
for (name, child) in value.members() {
self.under(Step::Member(name.to_owned()), |checker| match &pair {
Some(pair) if pair.names_member(name, child.text()) => checker.walk(child),
Some(pair) if name == super::names::PAIR_VALUE => {
checker.take(action(pair.class_outside_headers(), shape(child)), child);
}
_ if name == "properties" && child.kind() == Kind::Object => {
checker.property_map(child);
}
_ => checker.member(name, located, child),
});
}
}
Kind::Array => {
for (at, item) in value.items().into_iter().enumerate() {
self.under(Step::Item(at), |checker| checker.walk(item));
}
}
Kind::String => {
let text = value.text().unwrap_or_default();
match J::document(text) {
Some(inner) => self.under(Step::Text, |checker| checker.walk(&inner)),
None => {
if looks_like_json_text(text)
|| has_private_key(text)
|| !urls_withheld(text)
|| !schemes_withheld(text)
{
self.fail();
}
}
}
}
_ => {}
}
}
}
pub fn schemes_withheld(text: &str) -> bool {
scheme_credential_spans(text).is_empty()
&& header_credential_spans(text).is_empty()
&& key_value_credential_spans(text).is_empty()
}
pub fn urls_withheld(text: &str) -> bool {
urls_in(text).into_iter().all(|url| {
[url.userinfo, url.query, url.fragment]
.into_iter()
.flatten()
.all(|range| range.is_empty() || &text[range] == WITHHELD)
})
}
impl Json for serde_json::Value {
fn kind(&self) -> Kind {
match self {
Self::Null => Kind::Null,
Self::Bool(_) => Kind::Bool,
Self::Number(_) => Kind::Number,
Self::String(_) => Kind::String,
Self::Array(_) => Kind::Array,
Self::Object(_) => Kind::Object,
}
}
fn text(&self) -> Option<&str> {
self.as_str()
}
fn items(&self) -> Vec<&Self> {
self.as_array()
.map(|items| items.iter().collect())
.unwrap_or_default()
}
fn members(&self) -> Vec<(&str, &Self)> {
self.as_object()
.map(|map| {
map.iter()
.map(|(name, child)| (name.as_str(), child))
.collect()
})
.unwrap_or_default()
}
}
impl Document for serde_json::Value {
fn document(text: &str) -> Option<Self> {
if !text.trim_start().starts_with(['{', '[']) {
return None;
}
serde_json::from_str::<Self>(text)
.ok()
.filter(|inner| inner.is_object() || inner.is_array())
}
}