use core::fmt;
use crate::error::{EncodingError, FormError, Location};
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FileNameSource {
Plain,
Extended,
}
#[derive(Clone, Copy)]
pub struct FileName<'a> {
raw: &'a [u8],
source: FileNameSource,
}
impl<'a> FileName<'a> {
pub(crate) const fn new(raw: &'a [u8], source: FileNameSource) -> Self {
Self { raw, source }
}
#[must_use]
pub const fn raw(&self) -> &'a [u8] {
self.raw
}
pub fn as_str(&self) -> Result<&'a str, FormError> {
core::str::from_utf8(self.raw)
.map_err(|_| FormError::encoding(EncodingError::InvalidUtf8, Location::at(0)))
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.raw.is_empty()
}
#[must_use]
pub const fn source(&self) -> FileNameSource {
self.source
}
#[must_use]
pub fn basename(&self) -> &'a [u8] {
match self.raw.iter().rposition(|&b| b == b'/' || b == b'\\') {
Some(i) => self.raw.get(i.wrapping_add(1)..).unwrap_or(&[]),
None => self.raw,
}
}
pub fn html_unescape_into<'b>(&self, out: &'b mut [u8]) -> Result<&'b str, FormError> {
if out.len() < self.raw.len() {
return Err(FormError::BufferTooSmall {
needed: self.raw.len(),
});
}
let n = html_unescape(self.raw, out);
core::str::from_utf8(out.get(..n).unwrap_or(&[]))
.map_err(|_| FormError::encoding(EncodingError::InvalidUtf8, Location::at(0)))
}
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
pub fn html_unescaped(&self) -> Result<alloc::borrow::Cow<'a, str>, FormError> {
if !has_html_escape(self.raw) {
return self.as_str().map(alloc::borrow::Cow::Borrowed);
}
let mut out = alloc::vec![0u8; self.raw.len()];
let n = html_unescape(self.raw, &mut out);
out.truncate(n);
alloc::string::String::from_utf8(out)
.map(alloc::borrow::Cow::Owned)
.map_err(|_| FormError::encoding(EncodingError::InvalidUtf8, Location::at(0)))
}
}
impl fmt::Debug for FileName<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "FileName({} bytes)", self.raw.len())
}
}
#[inline]
fn html_escape_at(s: &[u8]) -> Option<u8> {
match s.get(..3)? {
b"%0A" => Some(b'\n'),
b"%0D" => Some(b'\r'),
b"%22" => Some(b'"'),
_ => None,
}
}
pub(crate) fn has_html_escape(s: &[u8]) -> bool {
let mut i = 0usize;
loop {
i = crate::scan::find_byte(s, i, b'%');
if i >= s.len() {
return false;
}
if html_escape_at(s.get(i..).unwrap_or(&[])).is_some() {
return true;
}
i = i.wrapping_add(1);
}
}
pub(crate) fn html_unescape(src: &[u8], out: &mut [u8]) -> usize {
let mut i = 0usize;
let mut w = 0usize;
while i < src.len() {
let rest = src.get(i..).unwrap_or(&[]);
let (byte, step) = match html_escape_at(rest) {
Some(b) => (b, 3usize),
None => (*rest.first().unwrap_or(&0), 1usize),
};
if let Some(slot) = out.get_mut(w) {
*slot = byte;
}
w = w.wrapping_add(1);
i = i.wrapping_add(step);
}
w
}
#[cfg(test)]
mod tests {
use super::*;
fn name(raw: &[u8]) -> FileName<'_> {
FileName::new(raw, FileNameSource::Plain)
}
#[test]
fn test_basename_cases() {
assert_eq!(name(b"a/b\\c.txt").basename(), b"c.txt");
assert_eq!(name(b"c.txt").basename(), b"c.txt");
assert_eq!(name(b"/").basename(), b"");
assert_eq!(name(b"").basename(), b"");
assert_eq!(name(b"\\\\").basename(), b"");
}
#[test]
fn test_html_unescape_only_three_escapes() {
let mut out = [0u8; 32];
assert_eq!(
name(b"%0A%0D%22").html_unescape_into(&mut out).unwrap(),
"\n\r\""
);
assert_eq!(
name(b"%25%0a%41").html_unescape_into(&mut out).unwrap(),
"%25%0a%41"
);
assert_eq!(name(b"%2").html_unescape_into(&mut out).unwrap(), "%2");
assert!(name(b"\xff").html_unescape_into(&mut out).is_err());
let mut small = [0u8; 2];
assert_eq!(
name(b"abc").html_unescape_into(&mut small),
Err(FormError::BufferTooSmall { needed: 3 })
);
}
#[cfg(feature = "alloc")]
#[test]
fn test_html_unescaped_borrows_when_plain() {
assert!(matches!(
name(b"plain").html_unescaped().unwrap(),
alloc::borrow::Cow::Borrowed("plain")
));
assert_eq!(name(b"a%22").html_unescaped().unwrap(), "a\"");
}
#[test]
fn test_debug_hides_the_name() {
assert_eq!(
alloc::format!("{:?}", name(b"secret.pdf")),
"FileName(10 bytes)"
);
}
}