Skip to main content

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}