use std::marker::PhantomData;
use super::key::redact_declaration_key;
use super::url::url_for_tracing;
mod private {
pub trait Sealed {}
}
pub trait RedactionKind: private::Sealed {
fn redact(raw: &str) -> String;
}
pub enum UrlRedaction {}
impl private::Sealed for UrlRedaction {}
impl RedactionKind for UrlRedaction {
fn redact(raw: &str) -> String {
url_for_tracing(raw)
}
}
pub enum NameRedaction {}
impl private::Sealed for NameRedaction {}
impl RedactionKind for NameRedaction {
fn redact(raw: &str) -> String {
redact_declaration_key(raw).into_owned()
}
}
pub struct RedactedText<K: RedactionKind>(String, PhantomData<fn() -> K>);
impl<K: RedactionKind> RedactedText<K> {
#[must_use]
pub fn new(raw: &str) -> Self {
Self(K::redact(raw), PhantomData)
}
#[must_use]
pub fn into_inner(self) -> String {
self.0
}
}
impl<K: RedactionKind> From<&str> for RedactedText<K> {
fn from(raw: &str) -> Self {
Self::new(raw)
}
}
impl<K: RedactionKind> From<String> for RedactedText<K> {
fn from(raw: String) -> Self {
Self::new(&raw)
}
}
impl<K: RedactionKind> Clone for RedactedText<K> {
fn clone(&self) -> Self {
Self(self.0.clone(), PhantomData)
}
}
impl<K: RedactionKind> PartialEq for RedactedText<K> {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
impl<K: RedactionKind> Eq for RedactedText<K> {}
impl<K: RedactionKind> std::hash::Hash for RedactedText<K> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.0.hash(state);
}
}
impl<K: RedactionKind> std::fmt::Display for RedactedText<K> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl<K: RedactionKind> std::fmt::Debug for RedactedText<K> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Debug::fmt(&self.0, f)
}
}
impl<K: RedactionKind> AsRef<str> for RedactedText<K> {
fn as_ref(&self) -> &str {
&self.0
}
}
impl<K: RedactionKind> PartialEq<str> for RedactedText<K> {
fn eq(&self, other: &str) -> bool {
self.0 == other
}
}
impl<K: RedactionKind> PartialEq<&str> for RedactedText<K> {
fn eq(&self, other: &&str) -> bool {
self.0 == *other
}
}
pub type RedactedUrl = RedactedText<UrlRedaction>;
pub type RedactedName = RedactedText<NameRedaction>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_redacted_url_matches_url_for_tracing_byte_for_byte() {
let inputs = [
"https://npm.internal/pkg?token=super-secret-value",
"https://user:hunter2@registry.example/simple?token=x",
"https://registry.example/simple",
"not-a-url-at-all",
"@types/node",
"c:/user:hunter2@evil",
"c:///user:hunter2@evil",
"https://npm-proxy.fury.io/AbCdEf1234567890xyz/acme/",
];
for raw in inputs {
assert_eq!(RedactedUrl::new(raw).to_string(), url_for_tracing(raw));
}
}
#[test]
fn test_redacted_name_matches_redact_declaration_key_byte_for_byte() {
let inputs = [
"com.google.guava:guava",
"com.google.guava:deploy:TOKEN@git.internal.corp",
"source:Blocked",
"scope:@myorg",
"top-level",
];
for raw in inputs {
assert_eq!(
RedactedName::new(raw).to_string(),
redact_declaration_key(raw)
);
}
}
#[test]
fn test_redacted_url_display_and_as_ref_never_expose_raw_credential() {
let raw = "https://user:hunter2@registry.example/simple?token=super-secret-value";
let redacted = RedactedUrl::new(raw);
assert!(!redacted.to_string().contains("hunter2"));
assert!(!redacted.to_string().contains("super-secret-value"));
assert!(!redacted.as_ref().contains("hunter2"));
assert!(!redacted.as_ref().contains("super-secret-value"));
assert_eq!(redacted.to_string(), "https://***@registry.example/simple");
let owned = redacted.into_inner();
assert!(!owned.contains("hunter2"));
assert!(!owned.contains("super-secret-value"));
assert_eq!(owned, "https://***@registry.example/simple");
}
#[test]
fn test_into_inner_matches_display_byte_for_byte() {
for raw in [
"https://npm.internal/pkg?token=super-secret-value",
"https://user:hunter2@registry.example/simple?token=x",
"https://registry.example/simple",
"@types/node",
] {
assert_eq!(RedactedUrl::new(raw).into_inner(), url_for_tracing(raw));
}
for raw in [
"com.google.guava:guava",
"com.google.guava:deploy:TOKEN@git.internal.corp",
"source:Blocked",
] {
assert_eq!(
RedactedName::new(raw).into_inner(),
redact_declaration_key(raw)
);
}
}
#[test]
fn test_redacted_url_debug_forwards_to_inner_string_and_redacts() {
let redacted = RedactedUrl::new("https://user:hunter2@registry.example/simple");
assert_eq!(
format!("{redacted:?}"),
"\"https://***@registry.example/simple\""
);
}
#[test]
fn test_redacted_url_noop_for_scoped_package_name() {
assert_eq!(RedactedUrl::new("@types/node").to_string(), "@types/node");
}
#[test]
fn test_redacted_url_partial_eq_str_compares_redacted_text() {
let redacted = RedactedUrl::new("https://user:hunter2@registry.example/simple");
let expected: &str = "https://***@registry.example/simple";
assert_eq!(redacted, expected);
assert_eq!(redacted, *expected);
assert_ne!(redacted, "https://user:hunter2@registry.example/simple");
}
#[test]
fn test_redacted_name_partial_eq_str_compares_redacted_text() {
let redacted = RedactedName::new("com.google.guava:guava");
let expected: &str = "com.google.guava:guava";
assert_eq!(redacted, expected);
assert_eq!(redacted, *expected);
}
}