Skip to main content

weir_core/
payload.rs

1//! The opaque payload byte buffer carried end-to-end through weir.
2
3use std::ops::Deref;
4
5use bytes::Bytes;
6
7/// Opaque payload bytes — a newtype over ref-counted [`bytes::Bytes`] so clones
8/// through the drain / sink path are O(1) instead of heap copies.
9///
10/// `Payload` *wraps* `Bytes` rather than aliasing it, so the common path of
11/// weir's public API (slicing, `&[u8]` / `Vec<u8>` / `&str` interop) never names
12/// the `bytes` crate and is insulated from its semver. The zero-copy `Bytes`
13/// conversions (`From<Bytes>`, `From<Payload> for Bytes`, `into_bytes`) do
14/// expose `Bytes` deliberately, so a future `bytes 2.0` is a *contained* breaking
15/// surface there — not across the whole API. It derefs to `[u8]`, so
16/// slicing, indexing, iteration, `len()`, and `&payload` → `&[u8]` coercion all
17/// work transparently. `Debug` prints only the length — never the bytes — so a
18/// stray `debug!(?payload)` cannot leak (possibly sensitive) record contents.
19#[derive(Clone, Default, PartialEq, Eq, Hash)]
20pub struct Payload(Bytes);
21
22impl Payload {
23    /// An empty payload.
24    #[must_use]
25    pub fn new() -> Self {
26        Payload(Bytes::new())
27    }
28
29    /// Wraps a `'static` byte slice without copying (O(1)).
30    #[must_use]
31    pub fn from_static(bytes: &'static [u8]) -> Self {
32        Payload(Bytes::from_static(bytes))
33    }
34
35    /// Copies a borrowed slice into a new ref-counted buffer.
36    #[must_use]
37    pub fn copy_from_slice(data: &[u8]) -> Self {
38        Payload(Bytes::copy_from_slice(data))
39    }
40
41    /// Borrows the payload as a byte slice.
42    #[must_use]
43    pub fn as_slice(&self) -> &[u8] {
44        &self.0
45    }
46
47    /// Consumes the payload, returning the inner [`Bytes`] (the escape hatch for
48    /// code that genuinely needs the `bytes` type).
49    #[must_use]
50    pub fn into_bytes(self) -> Bytes {
51        self.0
52    }
53}
54
55impl Deref for Payload {
56    type Target = [u8];
57
58    fn deref(&self) -> &[u8] {
59        &self.0
60    }
61}
62
63impl AsRef<[u8]> for Payload {
64    fn as_ref(&self) -> &[u8] {
65        &self.0
66    }
67}
68
69impl std::borrow::Borrow<[u8]> for Payload {
70    fn borrow(&self) -> &[u8] {
71        &self.0
72    }
73}
74
75impl std::fmt::Debug for Payload {
76    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77        // Length only — the bytes may be sensitive record data and must never
78        // land in a log line. This is the F2 footgun fix.
79        write!(f, "Payload({} bytes)", self.0.len())
80    }
81}
82
83impl From<Bytes> for Payload {
84    fn from(b: Bytes) -> Self {
85        Payload(b)
86    }
87}
88
89impl From<Payload> for Bytes {
90    fn from(p: Payload) -> Self {
91        p.0
92    }
93}
94
95impl From<Vec<u8>> for Payload {
96    fn from(v: Vec<u8>) -> Self {
97        Payload(Bytes::from(v))
98    }
99}
100
101impl From<&[u8]> for Payload {
102    fn from(s: &[u8]) -> Self {
103        Payload(Bytes::copy_from_slice(s))
104    }
105}
106
107impl From<&str> for Payload {
108    /// Copies the string's UTF-8 bytes into a new payload. Convenience for the
109    /// common case of pushing text records (`Payload::from("hello")`).
110    fn from(s: &str) -> Self {
111        Payload(Bytes::copy_from_slice(s.as_bytes()))
112    }
113}
114
115impl From<String> for Payload {
116    /// Takes ownership of the string's buffer with no copy (`String` → `Vec<u8>`
117    /// → `Bytes` is a move).
118    fn from(s: String) -> Self {
119        Payload(Bytes::from(s.into_bytes()))
120    }
121}
122
123impl PartialEq<[u8]> for Payload {
124    fn eq(&self, other: &[u8]) -> bool {
125        self.0.as_ref() == other
126    }
127}
128
129impl PartialEq<Vec<u8>> for Payload {
130    fn eq(&self, other: &Vec<u8>) -> bool {
131        self.0.as_ref() == other.as_slice()
132    }
133}
134
135impl PartialEq<&[u8]> for Payload {
136    fn eq(&self, other: &&[u8]) -> bool {
137        self.0.as_ref() == *other
138    }
139}
140
141// Reverse impls so equality reads both directions (`slice == payload`, not just
142// `payload == slice`). PartialEq is not symmetric across types unless both
143// orderings are implemented (G09).
144impl PartialEq<Payload> for [u8] {
145    fn eq(&self, other: &Payload) -> bool {
146        self == other.0.as_ref()
147    }
148}
149
150impl PartialEq<Payload> for Vec<u8> {
151    fn eq(&self, other: &Payload) -> bool {
152        self.as_slice() == other.0.as_ref()
153    }
154}
155
156impl PartialEq<Payload> for &[u8] {
157    fn eq(&self, other: &Payload) -> bool {
158        *self == other.0.as_ref()
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    #[test]
167    fn from_static_is_byte_identical() {
168        let p = Payload::from_static(b"hello");
169        assert_eq!(&p[..], b"hello");
170        assert_eq!(p.len(), 5);
171        assert!(!p.is_empty());
172    }
173
174    #[test]
175    fn from_str_and_string_carry_utf8_bytes() {
176        assert_eq!(&Payload::from("héllo")[..], "héllo".as_bytes());
177        assert_eq!(&Payload::from(String::from("world"))[..], b"world");
178    }
179
180    #[test]
181    fn from_vec_and_slice_round_trip() {
182        assert_eq!(&Payload::from(vec![1u8, 2, 3])[..], &[1, 2, 3]);
183        assert_eq!(&Payload::from(&[4u8, 5][..])[..], &[4, 5]);
184    }
185
186    #[test]
187    fn new_and_default_are_empty() {
188        assert!(Payload::new().is_empty());
189        assert!(Payload::default().is_empty());
190    }
191
192    #[test]
193    fn clone_shares_buffer_and_compares_equal() {
194        let a = Payload::from(vec![9u8; 32]);
195        let b = a.clone();
196        assert_eq!(a, b);
197    }
198
199    #[test]
200    fn debug_prints_length_not_contents() {
201        let p = Payload::from_static(b"super-secret-token");
202        let dbg = format!("{p:?}");
203        assert!(!dbg.contains("secret"), "Debug leaked payload bytes: {dbg}");
204        assert_eq!(dbg, "Payload(18 bytes)");
205    }
206
207    #[test]
208    fn bytes_round_trip_is_zero_copy_identity() {
209        let b = Bytes::from_static(b"abc");
210        let p = Payload::from(b.clone());
211        assert_eq!(Bytes::from(p), b);
212    }
213
214    // ── F51: PartialEq impls + Borrow/Hash contract ───────────────────────────
215
216    #[test]
217    fn partial_eq_against_slice_vec_and_ref_slice() {
218        let p = Payload::from_static(b"abc");
219        // PartialEq<[u8]>
220        assert_eq!(p, *b"abc".as_slice());
221        assert_ne!(p, *b"abd".as_slice());
222        // PartialEq<Vec<u8>>
223        assert_eq!(p, b"abc".to_vec());
224        assert_ne!(p, b"ab".to_vec());
225        // PartialEq<&[u8]>
226        let s: &[u8] = b"abc";
227        assert_eq!(p, s);
228        let s2: &[u8] = b"abcd";
229        assert_ne!(p, s2);
230    }
231
232    /// G09: equality reads both directions — the reverse impls let `slice ==
233    /// payload` compile and agree with `payload == slice`.
234    #[test]
235    fn partial_eq_is_symmetric_across_types() {
236        let p = Payload::from_static(b"abc");
237        // [u8] == Payload
238        assert_eq!(*b"abc".as_slice(), p);
239        assert_ne!(*b"abd".as_slice(), p);
240        // Vec<u8> == Payload
241        assert_eq!(b"abc".to_vec(), p);
242        assert_ne!(b"ab".to_vec(), p);
243        // &[u8] == Payload
244        let s: &[u8] = b"abc";
245        assert_eq!(s, p);
246        let s2: &[u8] = b"abcd";
247        assert_ne!(s2, p);
248    }
249
250    /// The `Borrow<[u8]>` + derived `Hash` (over the inner `Bytes`) must agree
251    /// with hashing the borrowed `&[u8]`, or `Payload` would be unusable as a
252    /// `HashMap`/`HashSet` key looked up by slice. This pins the Borrow/Hash
253    /// contract that the derive + Borrow impl quietly rely on.
254    #[test]
255    fn borrow_and_hash_agree_with_slice() {
256        use std::borrow::Borrow;
257        use std::collections::hash_map::DefaultHasher;
258        use std::hash::{Hash, Hasher};
259
260        fn hash_of<T: Hash + ?Sized>(t: &T) -> u64 {
261            let mut h = DefaultHasher::new();
262            t.hash(&mut h);
263            h.finish()
264        }
265
266        let p = Payload::from_static(b"key-bytes");
267        let slice: &[u8] = b"key-bytes";
268
269        // Borrow exposes the same bytes…
270        let borrowed: &[u8] = p.borrow();
271        assert_eq!(borrowed, slice);
272        // …and Hash(Payload) must equal Hash(&[u8]) for the Borrow contract.
273        assert_eq!(
274            hash_of(&p),
275            hash_of(slice),
276            "Hash(Payload) must match Hash(&[u8]) for Borrow<[u8]> lookups"
277        );
278    }
279
280    /// End-to-end Borrow demonstration: a HashSet<Payload> can be queried by a
281    /// plain &[u8] (the whole point of the Borrow<[u8]> impl).
282    #[test]
283    fn hashset_keyed_by_payload_is_queryable_by_slice() {
284        use std::collections::HashSet;
285        let mut set: HashSet<Payload> = HashSet::new();
286        set.insert(Payload::from_static(b"present"));
287        assert!(set.contains(b"present".as_slice()));
288        assert!(!set.contains(b"absent".as_slice()));
289    }
290}