use serde::{Deserialize, Serialize};
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Location {
#[serde(deserialize_with = "deserialize_location_file")]
pub file: Arc<str>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_optional_positive_u32"
)]
pub line: Option<u32>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_optional_positive_u32"
)]
pub column: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum LocationError {
EmptyFilePath,
NullByteInPath,
PathTooLong,
ZeroLineNumber,
ZeroColumnNumber,
PathTraversal,
}
impl std::fmt::Display for LocationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::EmptyFilePath => write!(f, "file path cannot be empty. Fix: provide a valid file path."),
Self::NullByteInPath => write!(f, "file path cannot contain null bytes. Fix: sanitize the file path."),
Self::PathTooLong => write!(f, "file path exceeds maximum length (16KB). Fix: use a shorter path."),
Self::ZeroLineNumber => write!(f, "line number cannot be 0 (line numbers are 1-based). Fix: use 1 or greater."),
Self::ZeroColumnNumber => write!(f, "column number cannot be 0 (column numbers are 1-based). Fix: use 1 or greater."),
Self::PathTraversal => write!(f, "file path contains directory traversal sequences (..). Fix: use a normalized path."),
}
}
}
impl std::error::Error for LocationError {}
impl Location {
pub const MAX_PATH_LEN: usize = 16_384;
pub fn new(file: impl Into<String>) -> Result<Self, LocationError> {
let file = file.into();
validate_location_file(&file)?;
Ok(Self {
file: Arc::from(file),
line: None,
column: None,
})
}
pub fn line(mut self, line: u32) -> Result<Self, LocationError> {
if line == 0 {
return Err(LocationError::ZeroLineNumber);
}
self.line = Some(line);
Ok(self)
}
pub fn column(mut self, column: u32) -> Result<Self, LocationError> {
if column == 0 {
return Err(LocationError::ZeroColumnNumber);
}
self.column = Some(column);
Ok(self)
}
}
impl std::fmt::Display for Location {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.file)?;
if let Some(line) = self.line {
write!(f, ":{line}")?;
if let Some(col) = self.column {
write!(f, ":{col}")?;
}
}
Ok(())
}
}
fn validate_location_file(file: &str) -> Result<(), LocationError> {
if file.is_empty() {
return Err(LocationError::EmptyFilePath);
}
if file.len() > Location::MAX_PATH_LEN {
return Err(LocationError::PathTooLong);
}
if file.contains('\0') {
return Err(LocationError::NullByteInPath);
}
let path = std::path::Path::new(file);
if path.is_absolute() {
return Err(LocationError::PathTraversal);
}
for component in path.components() {
use std::path::Component;
if matches!(component, Component::ParentDir) {
return Err(LocationError::PathTraversal);
}
if matches!(component, Component::Prefix(_)) {
return Err(LocationError::PathTraversal);
}
}
Ok(())
}
fn deserialize_location_file<'de, D>(deserializer: D) -> Result<Arc<str>, D::Error>
where
D: serde::Deserializer<'de>,
{
let file = String::deserialize(deserializer)?;
validate_location_file(&file).map_err(serde::de::Error::custom)?;
Ok(Arc::from(file))
}
fn deserialize_optional_positive_u32<'de, D>(deserializer: D) -> Result<Option<u32>, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = Option::<u32>::deserialize(deserializer)?;
match value {
Some(0) => Err(serde::de::Error::custom(
"line and column values must be 1 or greater. Fix: use a positive source coordinate.",
)),
_ => Ok(value),
}
}