use std::borrow::Cow;
use std::fmt::{self, Write as _};
use std::str::FromStr;
use rama_net::Protocol;
use rama_net::address::Domain;
use crate::Error;
use super::host_source::HostSource;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum HashAlgorithm {
Sha256,
Sha384,
Sha512,
}
impl HashAlgorithm {
pub const fn as_str(&self) -> &'static str {
match self {
Self::Sha256 => "sha256",
Self::Sha384 => "sha384",
Self::Sha512 => "sha512",
}
}
}
impl fmt::Display for HashAlgorithm {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum SourceExpression {
SelfOrigin,
None,
UnsafeInline,
UnsafeEval,
StrictDynamic,
UnsafeHashes,
WasmUnsafeEval,
ReportSample,
InlineSpeculationRules,
Wildcard,
Scheme(Protocol),
Host(HostSource),
Nonce(Cow<'static, str>),
Hash {
algorithm: HashAlgorithm,
value: Cow<'static, str>,
},
}
impl SourceExpression {
pub fn scheme(scheme: impl Into<Protocol>) -> Self {
Self::Scheme(scheme.into())
}
pub fn host(host: impl Into<HostSource>) -> Self {
Self::Host(host.into())
}
pub fn nonce(nonce: impl Into<Cow<'static, str>>) -> Self {
Self::Nonce(nonce.into())
}
pub fn hash(algorithm: HashAlgorithm, value: impl Into<Cow<'static, str>>) -> Self {
Self::Hash {
algorithm,
value: value.into(),
}
}
}
impl fmt::Display for SourceExpression {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::SelfOrigin => f.write_str("'self'"),
Self::None => f.write_str("'none'"),
Self::UnsafeInline => f.write_str("'unsafe-inline'"),
Self::UnsafeEval => f.write_str("'unsafe-eval'"),
Self::StrictDynamic => f.write_str("'strict-dynamic'"),
Self::UnsafeHashes => f.write_str("'unsafe-hashes'"),
Self::WasmUnsafeEval => f.write_str("'wasm-unsafe-eval'"),
Self::ReportSample => f.write_str("'report-sample'"),
Self::InlineSpeculationRules => f.write_str("'inline-speculation-rules'"),
Self::Wildcard => f.write_char('*'),
Self::Scheme(p) => write!(f, "{}:", p.as_str()),
Self::Host(h) => write!(f, "{h}"),
Self::Nonce(n) => write!(f, "'nonce-{n}'"),
Self::Hash { algorithm, value } => write!(f, "'{algorithm}-{value}'"),
}
}
}
impl From<Protocol> for SourceExpression {
fn from(p: Protocol) -> Self {
Self::Scheme(p)
}
}
impl From<Domain> for SourceExpression {
fn from(d: Domain) -> Self {
Self::Host(HostSource::new(d))
}
}
impl From<HostSource> for SourceExpression {
fn from(h: HostSource) -> Self {
Self::Host(h)
}
}
impl FromStr for SourceExpression {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"*" => return Ok(Self::Wildcard),
"'self'" => return Ok(Self::SelfOrigin),
"'none'" => return Ok(Self::None),
"'unsafe-inline'" => return Ok(Self::UnsafeInline),
"'unsafe-eval'" => return Ok(Self::UnsafeEval),
"'strict-dynamic'" => return Ok(Self::StrictDynamic),
"'unsafe-hashes'" => return Ok(Self::UnsafeHashes),
"'wasm-unsafe-eval'" => return Ok(Self::WasmUnsafeEval),
"'report-sample'" => return Ok(Self::ReportSample),
"'inline-speculation-rules'" => return Ok(Self::InlineSpeculationRules),
_ => {}
}
if let Some(inner) = s.strip_prefix('\'').and_then(|t| t.strip_suffix('\'')) {
if let Some(nonce) = inner.strip_prefix("nonce-") {
return Ok(Self::Nonce(Cow::Owned(nonce.to_owned())));
}
for alg in [
HashAlgorithm::Sha256,
HashAlgorithm::Sha384,
HashAlgorithm::Sha512,
] {
if let Some(hash) = inner.strip_prefix(&format!("{}-", alg.as_str())) {
return Ok(Self::Hash {
algorithm: alg,
value: Cow::Owned(hash.to_owned()),
});
}
}
return HostSource::try_parse(s)
.map(Self::Host)
.map_err(|_err| Error::invalid());
}
if let Some(scheme) = s.strip_suffix(':')
&& !scheme.is_empty()
&& !s.contains('/')
{
let proto = Protocol::try_from(scheme).map_err(|_err| Error::invalid())?;
return Ok(Self::Scheme(proto));
}
HostSource::try_parse(s).map(Self::Host)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn keywords_round_trip() {
for (s, want) in [
("'self'", SourceExpression::SelfOrigin),
("'none'", SourceExpression::None),
("'unsafe-inline'", SourceExpression::UnsafeInline),
("'unsafe-eval'", SourceExpression::UnsafeEval),
("'strict-dynamic'", SourceExpression::StrictDynamic),
("*", SourceExpression::Wildcard),
] {
assert_eq!(SourceExpression::from_str(s).unwrap(), want);
assert_eq!(want.to_string(), s);
}
}
#[test]
fn scheme_uses_typed_protocol() {
let parsed = SourceExpression::from_str("data:").unwrap();
assert!(matches!(parsed, SourceExpression::Scheme(ref p) if p.as_str() == "data"));
assert_eq!(parsed.to_string(), "data:");
let https = SourceExpression::scheme(Protocol::HTTPS);
assert_eq!(https.to_string(), "https:");
}
#[test]
fn host_uses_typed_host_source() {
let parsed = SourceExpression::from_str("https://raw.githubusercontent.com").unwrap();
match parsed {
SourceExpression::Host(h) => {
assert_eq!(h.scheme().unwrap().as_str(), "https");
assert_eq!(h.host().as_ref(), "raw.githubusercontent.com");
}
other => panic!("expected Host, got {other:?}"),
}
}
#[test]
fn wildcard_subdomain_host_round_trips() {
let parsed = SourceExpression::from_str("*.example.com").unwrap();
assert_eq!(parsed.to_string(), "*.example.com");
}
#[test]
fn nonce_and_hash_round_trip() {
assert_eq!(
SourceExpression::from_str("'nonce-abc'")
.unwrap()
.to_string(),
"'nonce-abc'"
);
let h = SourceExpression::from_str("'sha384-xyz'").unwrap();
assert!(matches!(
h,
SourceExpression::Hash {
algorithm: HashAlgorithm::Sha384,
..
}
));
assert_eq!(h.to_string(), "'sha384-xyz'");
}
#[test]
fn from_impls_construct_typed_sources() {
let from_proto: SourceExpression = Protocol::HTTPS.into();
assert_eq!(from_proto.to_string(), "https:");
let from_domain: SourceExpression = Domain::from_static("example.com").into();
assert_eq!(from_domain.to_string(), "example.com");
}
}