use super::SourceRef;
use super::{filename::FileName, fragment::Fragment, immutable_string::ImmutableString};
use std::io;
use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
use std::{fs::File, sync::Arc};
#[cfg(feature = "file_memmap")]
use std::{sync::mpsc, thread, time::Duration};
#[cfg(feature = "file_memmap")]
use fs4::fs_std::FileExt;
#[cfg(feature = "file_memmap")]
use memmap2::Mmap;
#[cfg(feature = "file_memmap")]
use crate::reporting::Diagnostic;
#[cfg(feature = "file_memmap")]
pub const FILE_LOCK_WARNING_TIME: Duration = Duration::from_secs(5);
static SOURCE_ID_GENERATOR: AtomicU64 = AtomicU64::new(1);
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SourceId(u64);
#[derive(Debug)]
pub struct Source {
pub id: SourceId,
name: FileName,
source: ImmutableString,
line_starts: Vec<usize>,
}
impl Source {
fn new(name: FileName, source: ImmutableString) -> Self {
Source {
id: SourceId(SOURCE_ID_GENERATOR.fetch_add(1, Ordering::Relaxed)),
name,
line_starts: source.line_starts().collect(),
source,
}
}
pub fn new_from_string(name: FileName, source: String) -> Self {
Source::new(name, ImmutableString::new_owned(source.into_boxed_str()))
}
pub fn new_from_static_str(name: FileName, source: &'static str) -> Self {
Source::new(name, ImmutableString::new_static(source))
}
#[cfg(feature = "file_memmap")]
pub fn new_mapped_from_disk(path: PathBuf) -> io::Result<Self> {
use crate::source_tracking::SourceMap;
enum ChannelMessage {
FileLocked(File),
LockingError(io::Error),
FiveSecondWarning,
}
let file: File = File::open(&path)?;
let (tx, rx) = mpsc::sync_channel::<ChannelMessage>(0);
let timeout_tx = tx.clone();
thread::spawn(move || match file.lock_exclusive() {
Ok(_) => tx.send(ChannelMessage::FileLocked(file)),
Err(err) => tx.send(ChannelMessage::LockingError(err)),
});
thread::spawn(move || {
thread::sleep(FILE_LOCK_WARNING_TIME);
timeout_tx.send(ChannelMessage::FiveSecondWarning)
});
loop {
match rx.recv() {
Ok(ChannelMessage::FiveSecondWarning) => {
let message = format!(
"Getting a file lock on {} has taken more than {} seconds.",
path.display(),
FILE_LOCK_WARNING_TIME.as_secs()
);
Diagnostic::warning()
.with_message(message)
.with_notes(["This may be caused by another process holding or failing to release a lock on this file."])
.print(&SourceMap::new())
.expect("codespan-reporting error");
}
Ok(ChannelMessage::LockingError(io_err)) => Err(io_err)?,
Ok(ChannelMessage::FileLocked(file)) => {
let mem_map: Mmap = unsafe {
Mmap::map(&file)
.inspect_err(|_| {
FileExt::unlock(&file)
.map_err(|err| eprintln!("Error unlocking file: {:?}", err))
.ok();
})
}?;
let raw_data: &[u8] = mem_map.as_ref();
if let Err(utf8_error) = std::str::from_utf8(raw_data) {
FileExt::unlock(&file)
.map_err(|err| eprintln!("Error unlocking file: {:?}", err))
.ok();
Err(io::Error::new(io::ErrorKind::InvalidData, utf8_error))?;
}
return Ok(Source::new(
FileName::Real(path),
ImmutableString::new_locked_file(file, mem_map),
));
}
Err(_) => unreachable!(
"The reciever should never reach a state where both senders are closed."
),
}
}
}
pub fn new_read_from_disk(path: PathBuf) -> io::Result<Self> {
let file: File = File::open(&path)?;
let content: String = io::read_to_string(&file)?;
Ok(Self::new(
FileName::Real(path),
ImmutableString::new_owned(content.into_boxed_str()),
))
}
pub fn new_mapped_or_read(path: PathBuf) -> io::Result<Self> {
#[cfg(feature = "file_memmap")]
match Self::new_mapped_from_disk(path.clone()) {
ok @ Ok(_) => return ok,
Err(e) => {
eprintln!(
"warn: attempted to map file at {} and got {e}, falling back to read",
path.display()
);
}
};
Self::new_read_from_disk(path)
}
pub fn line_starts(&self) -> &[usize] {
self.line_starts.as_slice()
}
pub fn count_lines(&self) -> usize {
self.line_starts.len()
}
pub fn line_index(&self, byte_index: usize) -> usize {
let line_starts: &[usize] = self.line_starts();
line_starts
.binary_search(&byte_index)
.unwrap_or_else(|not_found_index| not_found_index.saturating_sub(1))
}
pub fn get_line(self: Arc<Source>, line_index: usize) -> Fragment {
if line_index >= self.count_lines() {
panic!("{} is greater than the number of lines in {}", line_index, self.name);
}
let start_byte_index: usize = self.line_starts[line_index];
let end_byte_index: usize = if line_index + 1 == self.count_lines() {
self.source.len()
} else {
self.line_starts[line_index + 1]
};
let frag = Fragment {
source: Arc::clone(&self),
range: start_byte_index..end_byte_index,
};
debug_assert!(frag.is_valid());
frag
}
pub fn lines(self: SourceRef) -> impl Iterator<Item = Fragment> {
(0..self.count_lines()).map(move |line_index| self.clone().get_line(line_index))
}
pub const fn source(&self) -> &ImmutableString {
&self.source
}
pub const fn name(&self) -> &FileName {
&self.name
}
pub fn as_fragment(self: SourceRef) -> Fragment {
let len = self.source.len();
Fragment {
source: self,
range: 0..len,
}
}
}
#[cfg(test)]
mod tests {
use std::{sync::mpsc, thread};
use crate::source_tracking::filename::FileName;
use super::Source;
#[test]
fn dozen_threads_dont_share_gids() {
let (tx, rx) = mpsc::channel();
for i in 0..12 {
let tx = tx.clone();
thread::spawn(move || {
let source = Source::new_from_string(FileName::None, format!("{i}"));
tx.send(source.id).unwrap();
});
}
let mut gids = (0..12).map(|_| rx.recv().unwrap()).collect::<Vec<_>>();
let original_len = gids.len();
println!("{gids:?}");
gids.sort();
gids.dedup();
let dedup_len = gids.len();
assert_eq!(original_len, dedup_len, "global ids are not duplicated");
}
}