use crate::{Identifier, ProtectedString, Sourced};
use std::{error::Error, fmt};
pub const METADATA_MAX_DEPTH: usize = 64;
pub const METADATA_MAX_BYTES: usize = 1024 * 1024;
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum MetadataScope {
Application,
Service(Identifier),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MetadataEntry {
key: Sourced<ProtectedString>,
value: Sourced<MetadataNode>,
}
impl MetadataEntry {
#[must_use]
pub const fn new(key: Sourced<ProtectedString>, value: Sourced<MetadataNode>) -> Self {
Self { key, value }
}
#[must_use]
pub const fn key(&self) -> &Sourced<ProtectedString> {
&self.key
}
#[must_use]
pub const fn value(&self) -> &Sourced<MetadataNode> {
&self.value
}
}
#[derive(Clone, Eq, PartialEq)]
#[non_exhaustive]
pub enum MetadataValue {
Null,
Boolean(bool),
Number(ProtectedString),
String(ProtectedString),
Sequence(Vec<Sourced<MetadataNode>>),
Mapping(Vec<MetadataEntry>),
}
impl fmt::Debug for MetadataValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Null => f.write_str("Null"),
Self::Boolean(_) => f.write_str("Boolean([REDACTED])"),
Self::Number(value) => f.debug_tuple("Number").field(value).finish(),
Self::String(value) => f.debug_tuple("String").field(value).finish(),
Self::Sequence(values) => f.debug_tuple("Sequence").field(values).finish(),
Self::Mapping(entries) => f.debug_tuple("Mapping").field(entries).finish(),
}
}
}
#[derive(Clone, Eq, PartialEq)]
pub struct MetadataNode {
value: MetadataValue,
sensitive: bool,
folded: bool,
}
impl MetadataNode {
pub fn new(value: MetadataValue, sensitive: bool, folded: bool) -> Result<Self, MetadataError> {
let mut node = Self {
value,
sensitive,
folded,
};
node.measure(1)?;
if node.is_sensitive() {
node.protect_contents();
}
Ok(node)
}
#[must_use]
pub const fn value(&self) -> &MetadataValue {
&self.value
}
#[must_use]
pub fn is_sensitive(&self) -> bool {
self.sensitive
|| matches!(&self.value, MetadataValue::String(value) | MetadataValue::Number(value) if value.is_sensitive())
}
#[must_use]
pub const fn is_folded(&self) -> bool {
self.folded
}
#[must_use]
pub fn contains_sensitive(&self) -> bool {
self.is_sensitive()
|| match &self.value {
MetadataValue::Sequence(values) => values.iter().any(|value| value.value().contains_sensitive()),
MetadataValue::Mapping(entries) => entries
.iter()
.any(|entry| entry.key.value().is_sensitive() || entry.value.value().contains_sensitive()),
_ => false,
}
}
fn protect_contents(&mut self) {
self.sensitive = true;
match &mut self.value {
MetadataValue::String(value) | MetadataValue::Number(value) => {
*value = ProtectedString::sensitive(value.expose());
}
MetadataValue::Sequence(values) => {
for value in values {
value.value_mut().protect_contents();
}
}
MetadataValue::Mapping(entries) => {
for entry in entries {
*entry.key.value_mut() = ProtectedString::sensitive(entry.key.value().expose());
entry.value.value_mut().protect_contents();
}
}
MetadataValue::Null | MetadataValue::Boolean(_) => {}
}
}
pub(crate) fn measure(&self, depth: usize) -> Result<usize, MetadataError> {
if depth > METADATA_MAX_DEPTH || (self.folded && !matches!(self.value, MetadataValue::String(_))) {
return Err(MetadataError::InvalidValue);
}
let mut bytes = 32_usize;
match &self.value {
MetadataValue::Number(value) => {
if !valid_number(value.expose()) {
return Err(MetadataError::InvalidValue);
}
bytes += value.expose().len();
}
MetadataValue::String(value) => bytes += value.expose().len(),
MetadataValue::Sequence(values) => {
for value in values {
bytes = bytes
.checked_add(value.value().measure(depth + 1)?)
.ok_or(MetadataError::Limit)?;
}
}
MetadataValue::Mapping(entries) => {
let mut keys = std::collections::BTreeSet::new();
for entry in entries {
if !keys.insert(entry.key.value().expose()) {
return Err(MetadataError::Duplicate);
}
bytes = bytes
.checked_add(entry.key.value().expose().len())
.and_then(|bytes| bytes.checked_add(32))
.ok_or(MetadataError::Limit)?;
bytes = bytes
.checked_add(entry.value.value().measure(depth + 1)?)
.ok_or(MetadataError::Limit)?;
}
}
_ => {}
}
if bytes > METADATA_MAX_BYTES {
return Err(MetadataError::Limit);
}
Ok(bytes)
}
}
impl fmt::Debug for MetadataNode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.is_sensitive() {
f.write_str("MetadataNode([REDACTED])")
} else {
f.debug_struct("MetadataNode")
.field("value", &self.value)
.field("sensitive", &self.sensitive)
.field("folded", &self.folded)
.finish()
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct VendorMetadata {
scope: MetadataScope,
namespace: Sourced<ProtectedString>,
payload: Sourced<MetadataNode>,
}
impl VendorMetadata {
pub fn new(
scope: MetadataScope,
namespace: Sourced<ProtectedString>,
payload: Sourced<MetadataNode>,
) -> Result<Self, MetadataError> {
if !namespace.value().expose().starts_with("x-") || namespace.value().expose().len() <= 2 {
return Err(MetadataError::InvalidValue);
}
payload.value().measure(1)?;
Ok(Self {
scope,
namespace,
payload,
})
}
#[must_use]
pub const fn scope(&self) -> &MetadataScope {
&self.scope
}
#[must_use]
pub const fn namespace(&self) -> &Sourced<ProtectedString> {
&self.namespace
}
#[must_use]
pub const fn payload(&self) -> &Sourced<MetadataNode> {
&self.payload
}
#[must_use]
pub fn contains_sensitive(&self) -> bool {
self.namespace.value().is_sensitive() || self.payload.value().contains_sensitive()
}
pub(crate) fn measure(&self) -> Result<usize, MetadataError> {
self.payload
.value()
.measure(1)?
.checked_add(self.namespace.value().expose().len())
.ok_or(MetadataError::Limit)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum MetadataError {
InvalidValue,
Duplicate,
UnknownOwner,
Limit,
}
impl fmt::Display for MetadataError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::InvalidValue => "invalid vendor metadata value",
Self::Duplicate => "duplicate vendor metadata identity or key",
Self::UnknownOwner => "vendor metadata references an absent service",
Self::Limit => "vendor metadata exceeds supported bounds",
})
}
}
impl Error for MetadataError {}
fn valid_number(text: &str) -> bool {
let bytes = text.as_bytes();
let mut i = usize::from(bytes.first() == Some(&b'-'));
match bytes.get(i) {
Some(b'0') => i += 1,
Some(b'1'..=b'9') => {
i += 1;
while bytes.get(i).is_some_and(u8::is_ascii_digit) {
i += 1;
}
}
_ => return false,
}
if bytes.get(i) == Some(&b'.') {
i += 1;
let start = i;
while bytes.get(i).is_some_and(u8::is_ascii_digit) {
i += 1;
}
if start == i {
return false;
}
}
if matches!(bytes.get(i), Some(b'e' | b'E')) {
i += 1;
if matches!(bytes.get(i), Some(b'+' | b'-')) {
i += 1;
}
let start = i;
while bytes.get(i).is_some_and(u8::is_ascii_digit) {
i += 1;
}
if start == i {
return false;
}
}
i == bytes.len()
}