word_tally/input/
mapped.rs

1//! Mapped source for direct data access (mmap or bytes).
2
3use std::{
4    fmt::{self, Display, Formatter},
5    ops::Deref,
6    path::{Path, PathBuf},
7};
8
9use memmap2::Mmap;
10
11use super::{FileType, Metadata, open_file_with_error_context};
12use crate::WordTallyError;
13
14/// Mapped source for direct data access (mmap or bytes).
15#[derive(Debug)]
16pub enum Mapped {
17    /// Memory-mapped file with metadata.
18    File {
19        path: PathBuf,
20        mmap: Mmap,
21        file_type: FileType,
22    },
23    /// In-memory byte data.
24    Memory { bytes: Box<[u8]> },
25}
26
27impl AsRef<[u8]> for Mapped {
28    /// Returns the underlying byte slice.
29    fn as_ref(&self) -> &[u8] {
30        match self {
31            Self::File { mmap, .. } => mmap,
32            Self::Memory { bytes } => bytes,
33        }
34    }
35}
36
37impl Deref for Mapped {
38    type Target = [u8];
39
40    /// Provides direct access to the underlying byte data.
41    fn deref(&self) -> &Self::Target {
42        match self {
43            Self::File { mmap, .. } => mmap,
44            Self::Memory { bytes } => bytes,
45        }
46    }
47}
48
49impl Display for Mapped {
50    /// Formats the view for display.
51    /// Shows file path for file or "<bytes>" for memory data.
52    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
53        match self {
54            Self::File { path, .. } => write!(f, "{}", path.display()),
55            Self::Memory { .. } => write!(f, "<bytes>"),
56        }
57    }
58}
59
60impl From<Box<[u8]>> for Mapped {
61    /// Creates a view from boxed bytes.
62    fn from(bytes: Box<[u8]>) -> Self {
63        Self::Memory { bytes }
64    }
65}
66
67impl From<&[u8]> for Mapped {
68    /// Creates a view from a byte slice.
69    fn from(bytes: &[u8]) -> Self {
70        Self::Memory {
71            bytes: Box::from(bytes),
72        }
73    }
74}
75
76impl From<Vec<u8>> for Mapped {
77    /// Creates a view from a byte vector.
78    fn from(bytes: Vec<u8>) -> Self {
79        Self::Memory {
80            bytes: bytes.into_boxed_slice(),
81        }
82    }
83}
84
85impl<const N: usize> From<&[u8; N]> for Mapped {
86    /// Creates a view from a fixed-size byte array.
87    fn from(bytes: &[u8; N]) -> Self {
88        Self::Memory {
89            bytes: Box::from(bytes.as_slice()),
90        }
91    }
92}
93
94impl TryFrom<&Path> for Mapped {
95    type Error = WordTallyError;
96
97    /// Creates a memory-mapped view from a file path.
98    ///
99    /// # Errors
100    ///
101    /// Returns an error if:
102    /// - `WordTallyError::Io` if file metadata cannot be read
103    /// - `WordTallyError::NotMappable` if file type does not support memory mapping
104    /// - `WordTallyError::Io` if file cannot be opened or memory mapping fails
105    fn try_from(path: &Path) -> Result<Self, Self::Error> {
106        // Get metadata to check if file is mappable
107        let metadata = std::fs::metadata(path).map_err(|source| {
108            let path_str = path.display().to_string();
109            let message = match source.kind() {
110                std::io::ErrorKind::NotFound => "no such file".to_string(),
111                std::io::ErrorKind::PermissionDenied => "permission denied".to_string(),
112                _ => "failed to read metadata".to_string(),
113            };
114            WordTallyError::Io {
115                path: path_str,
116                message,
117                file_type: None, // We don't have metadata yet
118                source,
119            }
120        })?;
121
122        // Get the file type from metadata
123        let file_type = FileType::from(&metadata);
124
125        // Check if file type is mappable (regular file or block device)
126        #[cfg(unix)]
127        let is_mappable = {
128            use std::os::unix::fs::FileTypeExt;
129            let ft = metadata.file_type();
130            ft.is_file() || ft.is_block_device()
131        };
132        #[cfg(not(unix))]
133        let is_mappable = metadata.is_file();
134
135        if !is_mappable {
136            return Err(WordTallyError::NotMappable {
137                file_type,
138                path: path.display().to_string(),
139            });
140        }
141
142        // Now safe to open the file
143        let file = open_file_with_error_context(path)?;
144
145        // Safety: Memory mapping requires `unsafe` per memmap2 crate
146        #[allow(unsafe_code)]
147        let mmap = unsafe { Mmap::map(&file) }.map_err(|e| WordTallyError::Io {
148            path: path.display().to_string(),
149            message: "failed to create memory map".into(),
150            file_type: Some(file_type),
151            source: e,
152        })?;
153
154        Ok(Self::File {
155            path: path.to_path_buf(),
156            mmap,
157            file_type,
158        })
159    }
160}
161
162impl TryFrom<PathBuf> for Mapped {
163    type Error = WordTallyError;
164
165    /// Creates a memory-mapped view from a path buffer.
166    ///
167    /// # Errors
168    ///
169    /// Returns an error if:
170    /// - `WordTallyError::StdinInvalid` if path is "-"
171    /// - `WordTallyError::Io` if file cannot be opened or memory mapping fails
172    fn try_from(path: PathBuf) -> Result<Self, Self::Error> {
173        Self::try_from(path.as_path())
174    }
175}
176
177impl TryFrom<&str> for Mapped {
178    type Error = WordTallyError;
179
180    /// Creates a memory-mapped view from a string path.
181    ///
182    /// # Errors
183    ///
184    /// Returns an error if:
185    /// - `WordTallyError::StdinNotMappable` if path is "-" (stdin cannot be memory-mapped)
186    /// - `WordTallyError::NotMappable` if file type does not support memory mapping
187    /// - `WordTallyError::Io` if file cannot be opened or memory mapping fails
188    fn try_from(path: &str) -> Result<Self, Self::Error> {
189        if path == "-" {
190            return Err(WordTallyError::StdinNotMappable);
191        }
192        Self::try_from(Path::new(path))
193    }
194}
195
196impl Metadata for Mapped {
197    /// Returns the file path for file views, `None` for memory.
198    fn path(&self) -> Option<&Path> {
199        match self {
200            Self::File { path, .. } => Some(path.as_path()),
201            Self::Memory { .. } => None,
202        }
203    }
204
205    /// Returns the size in bytes (always available).
206    fn size(&self) -> Option<u64> {
207        Some(self.len() as u64)
208    }
209
210    /// Returns the file type for file views, `None` for memory.
211    fn file_type(&self) -> Option<FileType> {
212        match self {
213            Self::File { file_type, .. } => Some(*file_type),
214            Self::Memory { .. } => None,
215        }
216    }
217}