tower_http_cache/codec/legacy.rs
1//! Reader for cache entries written by tower-http-cache 0.5.x.
2//!
3//! **Deprecated: this module and the `legacy-bincode1-read` feature that gates
4//! it are removed in 0.7.0.** Cache entries are self-expiring, so once every
5//! 0.5.x-written entry has aged past its TTL plus its stale window the feature
6//! can be turned off. Disabling it is safe at any time; the only cost is a cold
7//! cache.
8//!
9//! # Why this is hand-written
10//!
11//! Calling `bincode` here would keep `bincode 1.3.3` in the dependency graph
12//! and keep the permanently-ignored RUSTSEC-2025-0141 suppression in
13//! `deny.toml`, which is the thing 0.6.0 exists to clear. The bincode 1 default
14//! configuration is a small, fixed encoding, so the three struct shapes 0.5.x
15//! wrote are decoded directly:
16//!
17//! * fixed-width integers, little-endian;
18//! * every sequence, string and byte array prefixed with its length as a
19//! little-endian `u64`;
20//! * `Option` as a single `u8` tag, `0` for `None` and `1` for `Some`.
21//!
22//! # The shape
23//!
24//! ```text
25//! bincode1(RedisRecord { payload: bincode1(StoredEntry), expires_at_ms, stale_until_ms })
26//! ```
27//!
28//! 0.5.x's Redis payload is a private `StoredEntry` with **no `tags` field**,
29//! so [`decode_legacy_redis`] sets `tags: None` unconditionally. Tags did not
30//! cross the 0.5.x Redis wire at all; asserting `None` is what pins the scope
31//! of that fix.
32//!
33//! There was a second decoder here, for 0.5.x's memcached record. It was
34//! removed along with the memcached backend itself in 0.6.0 -- nothing can
35//! produce those bytes any more. See the CHANGELOG for why that backend never
36//! functioned; the decoder and its golden fixtures remain in git history at
37//! `eb026cc` should they ever be needed.
38//!
39//! # Safety against hostile input
40//!
41//! Every length read from the buffer is checked against the bytes remaining
42//! before it is used, so a corrupt `u64` length cannot drive a large
43//! allocation, and no path can panic or read out of bounds. Both decoders also
44//! require the buffer to be consumed exactly.
45
46use bytes::Bytes;
47use http::StatusCode;
48
49use super::envelope::unix_ms_to_system_time;
50use super::version_from_u8;
51use crate::backend::{CacheEntry, CacheRead};
52use crate::error::CacheError;
53
54fn err(msg: impl Into<String>) -> CacheError {
55 CacheError::Backend(format!("legacy bincode1 decode: {}", msg.into()))
56}
57
58/// Bounds-checked forward cursor over a byte slice.
59struct Cursor<'a> {
60 bytes: &'a [u8],
61 pos: usize,
62}
63
64impl<'a> Cursor<'a> {
65 fn new(bytes: &'a [u8]) -> Self {
66 Self { bytes, pos: 0 }
67 }
68
69 fn remaining(&self) -> usize {
70 self.bytes.len() - self.pos
71 }
72
73 fn take(&mut self, n: usize) -> Result<&'a [u8], CacheError> {
74 let end = self
75 .pos
76 .checked_add(n)
77 .ok_or_else(|| err("length overflow"))?;
78 if end > self.bytes.len() {
79 return Err(err(format!(
80 "unexpected end of input: need {} bytes, {} remain",
81 n,
82 self.remaining()
83 )));
84 }
85 let slice = &self.bytes[self.pos..end];
86 self.pos = end;
87 Ok(slice)
88 }
89
90 fn u8(&mut self) -> Result<u8, CacheError> {
91 Ok(self.take(1)?[0])
92 }
93
94 fn u16(&mut self) -> Result<u16, CacheError> {
95 Ok(u16::from_le_bytes(self.take(2)?.try_into().unwrap()))
96 }
97
98 fn u64(&mut self) -> Result<u64, CacheError> {
99 Ok(u64::from_le_bytes(self.take(8)?.try_into().unwrap()))
100 }
101
102 /// Reads a bincode-1 collection length: a little-endian `u64`, rejected
103 /// immediately if it exceeds the bytes left in the buffer.
104 ///
105 /// This is the bound that keeps a corrupt length from reaching
106 /// `Vec::with_capacity`.
107 fn len(&mut self) -> Result<usize, CacheError> {
108 let n = self.u64()?;
109 if n > self.remaining() as u64 {
110 return Err(err(format!(
111 "declared length {} exceeds the {} bytes remaining",
112 n,
113 self.remaining()
114 )));
115 }
116 Ok(n as usize)
117 }
118
119 fn byte_string(&mut self) -> Result<Vec<u8>, CacheError> {
120 let n = self.len()?;
121 Ok(self.take(n)?.to_vec())
122 }
123
124 fn string(&mut self) -> Result<String, CacheError> {
125 let n = self.len()?;
126 String::from_utf8(self.take(n)?.to_vec()).map_err(|e| err(e.to_string()))
127 }
128
129 fn headers(&mut self) -> Result<Vec<(String, Vec<u8>)>, CacheError> {
130 // A header pair costs at least 16 bytes (two u64 length prefixes), so
131 // the count is bounded by the remaining buffer before any allocation.
132 let n = self.len()?;
133 let mut out = Vec::with_capacity(n.min(self.remaining() / 16 + 1));
134 for _ in 0..n {
135 let name = self.string()?;
136 let value = self.byte_string()?;
137 out.push((name, value));
138 }
139 Ok(out)
140 }
141
142 /// Rejects trailing bytes. Both 0.5.x records framed their value exactly,
143 /// so leftovers mean this is not the shape we think it is.
144 fn finish(self) -> Result<(), CacheError> {
145 if self.pos != self.bytes.len() {
146 return Err(err(format!(
147 "{} trailing bytes after the record",
148 self.remaining()
149 )));
150 }
151 Ok(())
152 }
153}
154
155fn status_from_u16(value: u16) -> Result<StatusCode, CacheError> {
156 StatusCode::from_u16(value).map_err(|e| err(e.to_string()))
157}
158
159/// Decodes a 0.5.x Redis value: `bincode1(RedisRecord)`.
160///
161/// The inner payload is 0.5.x's private `StoredEntry`, which carried no tags,
162/// so the returned entry always has `tags: None`.
163pub fn decode_legacy_redis(bytes: &[u8]) -> Result<CacheRead, CacheError> {
164 let mut cursor = Cursor::new(bytes);
165 let payload = cursor.byte_string()?;
166 let expires_at_ms = cursor.u64()?;
167 let stale_until_ms = cursor.u64()?;
168 cursor.finish()?;
169
170 let mut inner = Cursor::new(&payload);
171 let status = inner.u16()?;
172 let version = inner.u8()?;
173 let headers = inner.headers()?;
174 let body = inner.byte_string()?;
175 inner.finish()?;
176
177 Ok(CacheRead {
178 entry: CacheEntry {
179 status: status_from_u16(status)?,
180 // 0.5.x's `StoredEntry` decoder rejected unknown version bytes.
181 version: version_from_u8(version)?,
182 headers,
183 body: Bytes::from(body),
184 tags: None,
185 },
186 expires_at: Some(unix_ms_to_system_time(expires_at_ms)),
187 stale_until: Some(unix_ms_to_system_time(stale_until_ms)),
188 })
189}