use std::fmt::Display;
use std::fs;
use std::path::Path;
use std::sync::Arc;
use crate::errors::{ErrorDetails, ErrorKind, Severity, SourceFileError};
use crate::io::SourceStream;
use encoding_rs::{mem::utf8_latin1_up_to, CoderResult, WINDOWS_1252};
#[derive(Debug, Clone)]
pub struct SourceContext {
inner: Arc<SourceContextInner>,
}
#[derive(Debug, Clone)]
struct SourceContextInner {
file_name: Box<str>,
file_content: Box<str>,
}
impl SourceContext {
pub fn new(file_name: impl Into<String>, content: impl Into<String>) -> Self {
Self {
inner: Arc::new(SourceContextInner {
file_name: file_name.into().into_boxed_str(),
file_content: content.into().into_boxed_str(),
}),
}
}
#[must_use]
pub fn file_name(&self) -> &str {
&self.inner.file_name
}
#[must_use]
pub fn content(&self) -> &str {
&self.inner.file_content
}
#[must_use]
pub fn slice(&self, start: u32, end: u32) -> &str {
let start = start as usize;
let end = end as usize;
&self.inner.file_content[start..end]
}
}
#[derive(Debug, Clone)]
pub struct SourceFile {
context: SourceContext,
}
impl AsRef<str> for SourceFile {
fn as_ref(&self) -> &str {
self.context.content()
}
}
impl Display for SourceFile {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"SourceFile {{ file name: '{}', content len: {} }}",
self.context.file_name(),
self.context.content().len()
)
}
}
impl SourceFile {
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, ErrorDetails<'static>> {
let path = path.as_ref();
let bytes = fs::read(path).map_err(|io_err| ErrorDetails {
kind: Box::new(ErrorKind::SourceFile(SourceFileError::Malformed {
message: format!("Failed to read file: {io_err}"),
})),
error_offset: 0,
source_content: "",
source_name: path.display().to_string().into_boxed_str(),
line_start: 0,
line_end: 0,
severity: Severity::Error,
labels: vec![],
notes: vec![],
})?;
let file_name = path
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("unknown")
.to_string();
Self::decode_with_replacement(file_name, &bytes).map_err(|err| ErrorDetails {
kind: err.kind,
error_offset: err.error_offset,
source_content: "",
source_name: err.source_name,
line_start: err.line_start,
line_end: err.line_end,
severity: err.severity,
labels: err.labels,
notes: err.notes,
})
}
#[must_use]
pub fn file_name(&self) -> &str {
self.context.file_name()
}
#[must_use]
pub fn from_string(file_name: impl Into<String>, source_code: impl Into<String>) -> Self {
SourceFile {
context: SourceContext::new(file_name.into(), source_code.into()),
}
}
pub fn decode_with_replacement(
file_name: impl Into<String>,
source_code: &[u8],
) -> Result<Self, ErrorDetails<'_>> {
Self::decode_internal(file_name, source_code, true)
}
fn decode_internal(
file_name: impl Into<String>,
source_code: &[u8],
allow_replacement: bool,
) -> Result<Self, ErrorDetails<'_>> {
let mut decoder = WINDOWS_1252.new_decoder();
let file_name = file_name.into();
let Some(max_len) = decoder.max_utf8_buffer_length(source_code.len()) else {
return Err(ErrorDetails {
kind: Box::new(ErrorKind::SourceFile(SourceFileError::Malformed {
message: "Failed to decode the source code. '{file_name}' was empty.".into(),
})),
error_offset: 0,
source_content: "",
source_name: file_name.into_boxed_str(),
line_start: 0,
line_end: 0,
severity: Severity::Error,
labels: vec![],
notes: vec![],
});
};
let mut decoded_content = String::with_capacity(max_len);
let last = true;
let (coder_result, attempted_decode_len, all_processed) =
decoder.decode_to_string(source_code, &mut decoded_content, last);
if decoded_content.len() == source_code.len() {
let source_file = SourceFile {
context: SourceContext::new(file_name, decoded_content),
};
return Ok(source_file);
}
if (!all_processed && !allow_replacement) || coder_result == CoderResult::OutputFull {
let mut decoded_len = utf8_latin1_up_to(source_code);
let mut error_offset = decoded_len - 1;
if attempted_decode_len == decoded_len {
let source_file = SourceFile {
context: SourceContext::new(file_name, decoded_content),
};
return Ok(source_file);
}
let text_up_to_error = if let Ok(v) = str::from_utf8(&source_code[0..decoded_len]) {
v.to_owned()
} else {
error_offset = 0;
decoded_len = 0;
String::new()
};
let details = ErrorDetails {
kind: Box::new(ErrorKind::SourceFile(SourceFileError::Malformed {
message: format!(
r"Failed to decode the source file. '{file_name}' may not use latin-1 (Windows-1252) code page.
Currently, only latin-1 source code is supported."
),
})),
source_content: Box::leak(text_up_to_error.into_boxed_str()),
source_name: file_name.into_boxed_str(),
error_offset: u32::try_from(error_offset).unwrap_or(0),
line_start: 0,
line_end: u32::try_from(decoded_len).unwrap_or(0),
severity: Severity::Error,
labels: vec![],
notes: vec![],
};
return Err(details);
}
let source_file = SourceFile {
context: SourceContext::new(file_name, decoded_content),
};
Ok(source_file)
}
pub fn decode(
file_name: impl Into<String>,
source_code: &[u8],
) -> Result<Self, ErrorDetails<'_>> {
Self::decode_internal(file_name, source_code, false)
}
#[must_use]
pub fn source_stream(&'_ self) -> SourceStream<'_> {
SourceStream::new(self.context.file_name(), self.context.content())
}
}