termivar_scanner/payload_strategies/
encoding.rs1use serde::{Deserialize, Serialize};
8
9use crate::payload_strategy::{
10 PayloadArtifact, PayloadStrategyError, PayloadStrategyLimits, PayloadStrategyRef,
11 PayloadVariantRole,
12};
13
14#[must_use]
16pub fn percent_encode(input: &[u8]) -> String {
17 let mut output = String::with_capacity(input.len());
18 for byte in input {
19 if byte.is_ascii_alphanumeric() || matches!(*byte, b'-' | b'_' | b'.' | b'~') {
20 output.push(char::from(*byte));
21 } else {
22 output.push_str(&format!("%{:02X}", *byte));
23 }
24 }
25 output
26}
27
28#[must_use]
30pub fn hex_encode(input: &[u8]) -> String {
31 input.iter().map(|byte| format!("{:02x}", *byte)).collect()
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(rename_all = "snake_case")]
37pub enum PayloadEncoding {
38 Percent,
39 Hex,
40}
41
42impl PayloadEncoding {
43 #[must_use]
44 pub fn apply(self, input: &[u8]) -> Vec<u8> {
45 match self {
46 Self::Percent => percent_encode(input).into_bytes(),
47 Self::Hex => hex_encode(input).into_bytes(),
48 }
49 }
50}
51
52pub fn encode_into_artifact(
54 strategy: &PayloadStrategyRef,
55 role: PayloadVariantRole,
56 input: &[u8],
57 encoding: PayloadEncoding,
58 limits: PayloadStrategyLimits,
59) -> Result<PayloadArtifact, PayloadStrategyError> {
60 PayloadArtifact::new(strategy.clone(), role, encoding.apply(input), limits)
61}
62
63#[cfg(test)]
64mod tests {
65 use super::*;
66
67 fn reference() -> PayloadStrategyRef {
68 PayloadStrategyRef::new("encoding.fixture", 1).expect("valid fixture identity")
69 }
70
71 #[test]
72 fn encoders_are_byte_based_and_deterministic() {
73 assert_eq!(percent_encode(b"a b/c"), "a%20b%2Fc");
74 assert_eq!(hex_encode(b"AB"), "4142");
75 assert_eq!(percent_encode(&[0xff]), "%FF");
76 assert_eq!(percent_encode(b"a b"), percent_encode(b"a b"));
77 }
78
79 #[test]
80 fn artifact_encoding_enforces_the_byte_bound_and_redacts_values() {
81 let tight = PayloadStrategyLimits::new(64, 4).expect("valid limits");
82 assert!(matches!(
83 encode_into_artifact(
84 &reference(),
85 PayloadVariantRole::Candidate,
86 b"<a>",
87 PayloadEncoding::Percent,
88 tight,
89 ),
90 Err(PayloadStrategyError::ArtifactTooLarge { .. })
91 ));
92
93 let artifact = encode_into_artifact(
94 &reference(),
95 PayloadVariantRole::Control,
96 b"ab",
97 PayloadEncoding::Hex,
98 PayloadStrategyLimits::default(),
99 )
100 .expect("bounded artifact");
101 assert_eq!(artifact.as_bytes(), b"6162");
102 assert!(!format!("{artifact:?}").contains("6162"));
103 assert!(!serde_json::to_string(&artifact.receipt())
104 .expect("receipt serializes")
105 .contains("6162"));
106 }
107}