use crate::text::log_escape::encode_log_safe_character;
const TRUNCATED: &str = "<truncated>";
pub(crate) struct BoundedUriWriter {
output: String,
marker_boundary: usize,
max_bytes: usize,
truncated: bool,
}
impl BoundedUriWriter {
pub(crate) fn new(max_bytes: usize) -> Self {
Self {
output: String::new(),
marker_boundary: 0,
max_bytes,
truncated: false,
}
}
pub(crate) fn write_str(&mut self, value: &str) -> bool {
if self.truncated {
return false;
}
let mut remaining = value;
while !remaining.is_empty() {
if remaining.as_bytes().first() == Some(&b'%')
&& remaining.len() >= 3
&& remaining.as_bytes()[1].is_ascii_hexdigit()
&& remaining.as_bytes()[2].is_ascii_hexdigit()
{
if !self.append_piece(&remaining[..3]) {
return false;
}
remaining = &remaining[3..];
continue;
}
let character = remaining
.chars()
.next()
.expect("non-empty text has a first character");
let mut encoded = [0_u8; 12];
let Ok(piece) = encode_log_safe_character(character, &mut encoded)
else {
self.truncate();
return false;
};
if !self.append_piece(piece) {
return false;
}
remaining = &remaining[character.len_utf8()..];
}
true
}
pub(crate) fn write_percent_encoded(&mut self, byte: u8) -> bool {
let encoded = [b'%', hex_digit(byte >> 4), hex_digit(byte & 0x0f)];
let piece = std::str::from_utf8(&encoded)
.expect("percent encoding is always valid ASCII");
self.append_piece(piece)
}
#[inline]
pub(crate) const fn is_full(&self) -> bool {
self.truncated
}
pub(crate) fn finish(mut self) -> (String, bool) {
if self.truncated {
self.output.truncate(self.marker_boundary);
self.output.push_str(TRUNCATED);
}
(self.output, self.truncated)
}
fn append_piece(&mut self, piece: &str) -> bool {
if self.truncated {
return false;
}
if self.output.len().saturating_add(piece.len()) > self.max_bytes {
self.truncate();
return false;
}
self.output.push_str(piece);
let payload_limit = self.max_bytes.saturating_sub(TRUNCATED.len());
if self.output.len() <= payload_limit {
self.marker_boundary = self.output.len();
}
true
}
fn truncate(&mut self) {
self.output.truncate(self.marker_boundary);
self.truncated = true;
}
}
const fn hex_digit(value: u8) -> u8 {
match value {
0..=9 => b'0' + value,
_ => b'A' + value - 10,
}
}