Skip to main content

union_square/domain/commands/
audit_buffer.rs

1//! Buffer management for audit events
2//!
3//! This module provides functionality to buffer request/response bodies
4//! from chunk events and process them when complete.
5
6use crate::proxy::types::{ChunkOffset, RequestId};
7use std::collections::HashMap;
8
9/// Buffered data for a request or response
10#[derive(Debug, Clone)]
11pub struct BufferedData {
12    chunks: Vec<(ChunkOffset, Vec<u8>)>,
13    total_size: usize,
14}
15
16impl BufferedData {
17    fn new() -> Self {
18        Self {
19            chunks: Vec::new(),
20            total_size: 0,
21        }
22    }
23
24    /// Add a chunk to the buffer
25    pub fn add_chunk(&mut self, offset: ChunkOffset, data: Vec<u8>) {
26        self.total_size = (*offset.as_ref() + data.len()).max(self.total_size);
27        self.chunks.push((offset, data));
28    }
29
30    /// Set the expected total size (for cases where we know it upfront)
31    pub fn set_total_size(&mut self, size: usize) {
32        self.total_size = size;
33    }
34
35    /// Check if the buffer is complete (has all data)
36    pub fn is_complete(&self) -> bool {
37        if self.chunks.is_empty() {
38            return false;
39        }
40
41        // If we don't have a total_size set, we can't determine completeness
42        if self.total_size == 0 {
43            return false;
44        }
45
46        // Sort chunks by offset
47        let mut sorted_chunks = self.chunks.clone();
48        sorted_chunks.sort_by_key(|(offset, _)| *offset.as_ref());
49
50        // Check for gaps
51        let mut current_pos = 0;
52        for (offset, data) in &sorted_chunks {
53            if *offset.as_ref() != current_pos {
54                return false; // Gap found
55            }
56            current_pos += data.len();
57        }
58
59        current_pos == self.total_size
60    }
61
62    /// Reconstruct the complete data from chunks
63    pub fn reconstruct(&self) -> Option<Vec<u8>> {
64        if !self.is_complete() {
65            return None;
66        }
67
68        let mut sorted_chunks = self.chunks.clone();
69        sorted_chunks.sort_by_key(|(offset, _)| *offset.as_ref());
70
71        let mut result = Vec::with_capacity(self.total_size);
72        for (_, data) in sorted_chunks {
73            result.extend_from_slice(&data);
74        }
75
76        Some(result)
77    }
78}
79
80/// Manager for buffering audit data across multiple requests
81pub struct AuditBufferManager {
82    request_buffers: HashMap<RequestId, BufferedData>,
83    response_buffers: HashMap<RequestId, BufferedData>,
84}
85
86impl AuditBufferManager {
87    pub fn new() -> Self {
88        Self {
89            request_buffers: HashMap::new(),
90            response_buffers: HashMap::new(),
91        }
92    }
93
94    /// Add a request chunk
95    pub fn add_request_chunk(&mut self, request_id: RequestId, offset: ChunkOffset, data: Vec<u8>) {
96        self.request_buffers
97            .entry(request_id)
98            .or_insert_with(BufferedData::new)
99            .add_chunk(offset, data);
100    }
101
102    /// Add a response chunk
103    pub fn add_response_chunk(
104        &mut self,
105        request_id: RequestId,
106        offset: ChunkOffset,
107        data: Vec<u8>,
108    ) {
109        self.response_buffers
110            .entry(request_id)
111            .or_insert_with(BufferedData::new)
112            .add_chunk(offset, data);
113    }
114
115    /// Check if request body is complete and return it
116    pub fn get_complete_request_body(&mut self, request_id: &RequestId) -> Option<Vec<u8>> {
117        if let Some(buffer) = self.request_buffers.get(request_id) {
118            if let Some(data) = buffer.reconstruct() {
119                // Remove the buffer once we've reconstructed it
120                self.request_buffers.remove(request_id);
121                return Some(data);
122            }
123        }
124        None
125    }
126
127    /// Check if response body is complete and return it
128    pub fn get_complete_response_body(&mut self, request_id: &RequestId) -> Option<Vec<u8>> {
129        if let Some(buffer) = self.response_buffers.get(request_id) {
130            if let Some(data) = buffer.reconstruct() {
131                // Remove the buffer once we've reconstructed it
132                self.response_buffers.remove(request_id);
133                return Some(data);
134            }
135        }
136        None
137    }
138
139    /// Clean up old buffers for a request
140    pub fn cleanup_request(&mut self, request_id: &RequestId) {
141        self.request_buffers.remove(request_id);
142        self.response_buffers.remove(request_id);
143    }
144}
145
146impl Default for AuditBufferManager {
147    fn default() -> Self {
148        Self::new()
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    #[test]
157    fn test_buffered_data_complete() {
158        let mut buffer = BufferedData::new();
159
160        // Add chunks in order
161        buffer.add_chunk(ChunkOffset::from(0), vec![1, 2, 3]);
162        buffer.add_chunk(ChunkOffset::from(3), vec![4, 5, 6]);
163
164        assert!(buffer.is_complete());
165        assert_eq!(buffer.reconstruct(), Some(vec![1, 2, 3, 4, 5, 6]));
166    }
167
168    #[test]
169    fn test_buffered_data_with_gap() {
170        let mut buffer = BufferedData::new();
171
172        // Add chunks with a gap
173        buffer.add_chunk(ChunkOffset::from(0), vec![1, 2, 3]);
174        buffer.add_chunk(ChunkOffset::from(6), vec![7, 8, 9]); // Gap at 3-5
175
176        assert!(!buffer.is_complete());
177        assert_eq!(buffer.reconstruct(), None);
178    }
179
180    #[test]
181    fn test_buffered_data_out_of_order() {
182        let mut buffer = BufferedData::new();
183
184        // Add chunks out of order
185        buffer.add_chunk(ChunkOffset::from(3), vec![4, 5, 6]);
186        buffer.add_chunk(ChunkOffset::from(0), vec![1, 2, 3]);
187
188        assert!(buffer.is_complete());
189        assert_eq!(buffer.reconstruct(), Some(vec![1, 2, 3, 4, 5, 6]));
190    }
191
192    #[test]
193    fn test_audit_buffer_manager() {
194        let mut manager = AuditBufferManager::new();
195        let request_id = RequestId::new();
196
197        // For this test, we need a different approach since we need to know total size
198        // Let's test the cleanup functionality instead
199        manager.add_request_chunk(request_id, ChunkOffset::from(0), vec![1, 2, 3]);
200        manager.add_response_chunk(request_id, ChunkOffset::from(0), vec![4, 5, 6]);
201
202        // Cleanup should remove both buffers
203        manager.cleanup_request(&request_id);
204
205        // After cleanup, trying to get data returns None
206        assert!(manager.get_complete_request_body(&request_id).is_none());
207        assert!(manager.get_complete_response_body(&request_id).is_none());
208    }
209}