open_sesame/input/
buffer.rs

1//! Input buffer for collecting keyboard input
2//!
3//! Provides a clean abstraction over user input accumulation.
4
5use std::fmt;
6
7/// Maximum input buffer length to prevent unbounded memory growth
8///
9/// 64 characters is more than enough for any reasonable hint sequence
10/// (typical hints are 1-3 characters). This prevents memory exhaustion
11/// from malicious or buggy input sources.
12const MAX_INPUT_LENGTH: usize = 64;
13
14/// Buffer for collecting keyboard input
15///
16/// **Invariant:** All characters are stored in lowercase ASCII for case-insensitive
17/// matching. Both `push()` and `From<&str>` enforce this by converting input.
18#[derive(Debug, Clone, Default)]
19pub struct InputBuffer {
20    /// Characters entered so far (always lowercase)
21    chars: Vec<char>,
22}
23
24impl InputBuffer {
25    /// Create a new empty buffer
26    pub fn new() -> Self {
27        Self { chars: Vec::new() }
28    }
29
30    /// Pushes a character to the buffer.
31    ///
32    /// Returns `true` if the character was added, `false` if the buffer is full.
33    pub fn push(&mut self, c: char) -> bool {
34        if self.chars.len() >= MAX_INPUT_LENGTH {
35            tracing::debug!(
36                "Input buffer full ({} chars), ignoring input",
37                MAX_INPUT_LENGTH
38            );
39            return false;
40        }
41        // Maintains invariant: stores lowercase for case-insensitive matching
42        self.chars.push(c.to_ascii_lowercase());
43        true
44    }
45
46    /// Removes and returns the last character.
47    pub fn pop(&mut self) -> Option<char> {
48        self.chars.pop()
49    }
50
51    /// Clears the buffer.
52    pub fn clear(&mut self) {
53        self.chars.clear();
54    }
55
56    /// Returns true if the buffer is empty.
57    pub fn is_empty(&self) -> bool {
58        self.chars.is_empty()
59    }
60
61    /// Returns the number of characters in the buffer.
62    pub fn len(&self) -> usize {
63        self.chars.len()
64    }
65
66    /// Returns the first character for determining launch key.
67    pub fn first_char(&self) -> Option<char> {
68        self.chars.first().copied()
69    }
70
71    /// Returns the buffer contents as a string.
72    pub fn as_str(&self) -> String {
73        self.chars.iter().collect()
74    }
75
76    /// Returns the characters as a slice.
77    pub fn chars(&self) -> &[char] {
78        &self.chars
79    }
80}
81
82impl fmt::Display for InputBuffer {
83    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84        write!(f, "{}", self.as_str())
85    }
86}
87
88impl From<&str> for InputBuffer {
89    fn from(s: &str) -> Self {
90        Self {
91            chars: s.to_lowercase().chars().take(MAX_INPUT_LENGTH).collect(),
92        }
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn test_empty_buffer() {
102        let buf = InputBuffer::new();
103        assert!(buf.is_empty());
104        assert_eq!(buf.len(), 0);
105        assert_eq!(buf.as_str(), "");
106    }
107
108    #[test]
109    fn test_push_pop() {
110        let mut buf = InputBuffer::new();
111        buf.push('g');
112        buf.push('G'); // Should be lowercased
113        assert_eq!(buf.as_str(), "gg");
114        assert_eq!(buf.len(), 2);
115
116        assert_eq!(buf.pop(), Some('g'));
117        assert_eq!(buf.as_str(), "g");
118    }
119
120    #[test]
121    fn test_first_char() {
122        let mut buf = InputBuffer::new();
123        assert_eq!(buf.first_char(), None);
124
125        buf.push('f');
126        buf.push('f');
127        assert_eq!(buf.first_char(), Some('f'));
128    }
129
130    #[test]
131    fn test_from_str() {
132        let buf = InputBuffer::from("GGG");
133        assert_eq!(buf.as_str(), "ggg");
134    }
135
136    #[test]
137    fn test_display() {
138        let buf = InputBuffer::from("test");
139        assert_eq!(format!("{}", buf), "test");
140    }
141
142    #[test]
143    fn test_max_length_push() {
144        let mut buf = InputBuffer::new();
145        // Fill the buffer to max
146        for _ in 0..MAX_INPUT_LENGTH {
147            assert!(buf.push('a'));
148        }
149        assert_eq!(buf.len(), MAX_INPUT_LENGTH);
150
151        // Attempt to push more should fail
152        assert!(!buf.push('b'));
153        assert_eq!(buf.len(), MAX_INPUT_LENGTH);
154    }
155
156    #[test]
157    fn test_max_length_from_str() {
158        let long_string = "a".repeat(MAX_INPUT_LENGTH * 2);
159        let buf = InputBuffer::from(long_string.as_str());
160        assert_eq!(buf.len(), MAX_INPUT_LENGTH);
161    }
162}