1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
//! A minimal owned C `FILE *`.
//!
//! A couple of llama.cpp entry points read from an already-open handle at its
//! current position rather than from a path — that is the only way to load a
//! GGUF that is *embedded* in a larger file. Reaching them needs a `FILE *`,
//! which Rust's `std::fs` cannot produce.
//!
//! Opening goes through ggml's own `ggml_fopen`, which is already linked into
//! every build of this crate and does the UTF-8 → UTF-16 conversion that a bare
//! `fopen` gets wrong for a non-ASCII path on Windows. Only closing and seeking
//! have to be declared here.
// Every `unsafe` block below is justified in a `SAFETY:` comment; keep it that
// way. See the note on the lint in the workspace `Cargo.toml`.
#![deny(clippy::undocumented_unsafe_blocks)]
use std::ffi::{c_char, c_int, CString};
use std::io;
use std::path::Path;
use std::ptr::NonNull;
#[cfg(not(windows))]
use std::ffi::c_long;
use llama_cpp_sys_4::FILE;
/// `SEEK_SET` — offsets are from the start of the file.
const SEEK_SET: c_int = 0;
extern "C" {
fn fclose(file: *mut FILE) -> c_int;
// `fseek` takes a `long`, which is 32-bit on Windows, so use the CRT's
// 64-bit variant there. On a 32-bit Unix target `long` is 32-bit too and
// offsets are capped at 2 GiB — `seek_to` reports that rather than
// truncating. `fseeko` would lift the cap but takes `off_t`, whose width
// depends on `_FILE_OFFSET_BITS` in the C build, and guessing it wrong is
// an ABI mismatch.
#[cfg(not(windows))]
fn fseek(file: *mut FILE, offset: c_long, whence: c_int) -> c_int;
#[cfg(windows)]
fn _fseeki64(file: *mut FILE, offset: i64, whence: c_int) -> c_int;
}
/// An owned read-only C `FILE *`, closed on drop.
#[derive(Debug)]
pub(crate) struct CFile {
raw: NonNull<FILE>,
}
impl CFile {
/// Open `path` for reading in binary mode.
///
/// # Errors
///
/// Returns the OS error if the path contains an interior NUL or cannot be
/// opened.
pub(crate) fn open_read(path: &Path) -> io::Result<Self> {
// ggml_fopen takes UTF-8 and widens it itself on Windows, so a lossy
// conversion here would corrupt a non-UTF-8 path rather than fail. Ask
// for UTF-8 and report anything else as invalid.
let path_str = path.to_str().ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!("path {} is not valid UTF-8", path.display()),
)
})?;
let c_path = CString::new(path_str).map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("path {} contains an interior NUL: {e}", path.display()),
)
})?;
// SAFETY: both arguments are NUL-terminated C strings that outlive the
// call — `c_path` is a live `CString` and the mode is a literal.
// `ggml_fopen` only reads them, and returns either null or a handle this
// type then owns.
let raw = unsafe {
llama_cpp_sys_4::ggml_fopen(c_path.as_ptr(), c"rb".as_ptr().cast::<c_char>())
};
NonNull::new(raw)
.map(|raw| Self { raw })
.ok_or_else(io::Error::last_os_error)
}
/// Position the handle `offset` bytes from the start of the file.
///
/// # Errors
///
/// Returns the OS error if the seek fails, or
/// [`io::ErrorKind::InvalidInput`] if `offset` does not fit the platform's
/// seek offset — only reachable on a 32-bit target.
pub(crate) fn seek_to(&mut self, offset: u64) -> io::Result<()> {
#[cfg(windows)]
let rc = {
let offset = i64::try_from(offset).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!("offset {offset} does not fit this target's file offset type"),
)
})?;
// SAFETY: `self.raw` is a live handle for as long as `self` is —
// only `Drop` closes it — and `SEEK_SET` is a valid `whence`.
unsafe { _fseeki64(self.raw.as_ptr(), offset, SEEK_SET) }
};
#[cfg(not(windows))]
let rc = {
let offset = c_long::try_from(offset).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!("offset {offset} does not fit this target's file offset type"),
)
})?;
// SAFETY: as above. `offset` was narrowed to `c_long` by the
// `try_from` that precedes it, which is what makes the declared
// `c_long` parameter type the right ABI on every target here.
unsafe { fseek(self.raw.as_ptr(), offset, SEEK_SET) }
};
if rc == 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
}
pub(crate) fn as_ptr(&self) -> *mut FILE {
self.raw.as_ptr()
}
}
impl Drop for CFile {
fn drop(&mut self) {
// SAFETY: `self.raw` came from `ggml_fopen` in `open_read` and is
// closed exactly once, here. `CFile` is neither `Copy` nor `Clone`, so
// there is no second owner, and nothing hands the raw handle out except
// `as_ptr`, which borrows.
unsafe { fclose(self.raw.as_ptr()) };
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn open_read_reports_a_missing_file() {
let err = CFile::open_read(Path::new("definitely/not/a/real/path.gguf")).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::NotFound);
}
#[test]
fn open_read_rejects_an_interior_nul() {
// `Path` happily holds one; `ggml_fopen` would see a truncated name.
let err = CFile::open_read(Path::new("bad\0path.gguf")).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
}
#[test]
fn open_and_seek_an_existing_file() {
// Cargo runs tests with the package directory as CWD.
let mut file = CFile::open_read(Path::new("Cargo.toml")).expect("open Cargo.toml");
file.seek_to(4).expect("seek within the file");
// Seeking past the end is legal for a read handle; the next read fails.
file.seek_to(u64::from(u32::MAX))
.expect("seek past the end");
}
}