Skip to content

Commit e7fea04

Browse files
Boshenclaude
andcommitted
refactor(file_system): deduplicate read methods and use Vec<u8>
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 1dfdc55 commit e7fea04

File tree

7 files changed

+46
-79
lines changed

7 files changed

+46
-79
lines changed

benches/memory_fs.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -220,11 +220,10 @@ impl FileSystem for BenchMemoryFS {
220220
Self::default()
221221
}
222222

223-
fn read_to_string(&self, path: &Path) -> io::Result<String> {
223+
fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
224224
// Try direct lookup first
225225
if let Some(bytes) = self.files.get(path) {
226-
return String::from_utf8(bytes.clone())
227-
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e));
226+
return Ok(bytes.clone());
228227
}
229228

230229
// Try following symlinks
@@ -243,14 +242,18 @@ impl FileSystem for BenchMemoryFS {
243242
};
244243

245244
if let Some(bytes) = self.files.get(&current) {
246-
return String::from_utf8(bytes.clone())
247-
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e));
245+
return Ok(bytes.clone());
248246
}
249247
}
250248

251249
Err(io::Error::new(io::ErrorKind::NotFound, format!("File not found: {}", path.display())))
252250
}
253251

252+
fn read_to_string(&self, path: &Path) -> io::Result<String> {
253+
let bytes = self.read(path)?;
254+
String::from_utf8(bytes).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
255+
}
256+
254257
fn metadata(&self, path: &Path) -> io::Result<FileMetadata> {
255258
// Check if it's a file (direct)
256259
if self.files.contains_key(path) {

benches/resolver.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,7 @@ fn bench_package_json_deserialization(c: &mut Criterion) {
371371
for (name, json) in data {
372372
group.bench_function(name, |b| {
373373
b.iter_with_setup_wrapper(|runner| {
374-
let json = json.clone();
374+
let json = json.clone().into_bytes();
375375
runner.run(|| {
376376
PackageJson::parse(&fs, test_path.clone(), test_realpath.clone(), json)
377377
.expect("Failed to parse JSON");

src/cache/cache_impl.rs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,7 @@ impl<Fs: FileSystem> Cache<Fs> {
109109
.package_json
110110
.get_or_try_init(|| {
111111
let package_json_path = path.path.join("package.json");
112-
let Ok(package_json_string) =
113-
self.fs.read_to_string_bypass_system_cache(&package_json_path)
114-
else {
112+
let Ok(package_json_bytes) = self.fs.read(&package_json_path) else {
115113
return Ok(None);
116114
};
117115

@@ -120,7 +118,7 @@ impl<Fs: FileSystem> Cache<Fs> {
120118
} else {
121119
package_json_path.clone()
122120
};
123-
PackageJson::parse(&self.fs, package_json_path, real_path, package_json_string)
121+
PackageJson::parse(&self.fs, package_json_path, real_path, package_json_bytes)
124122
.map(|package_json| Some(Arc::new(package_json)))
125123
.map_err(ResolveError::Json)
126124
})
@@ -167,7 +165,7 @@ impl<Fs: FileSystem> Cache<Fs> {
167165
};
168166
let mut tsconfig_string = self
169167
.fs
170-
.read_to_string_bypass_system_cache(&tsconfig_path)
168+
.read_to_string(&tsconfig_path)
171169
.map_err(|_| ResolveError::TsconfigNotFound(path.to_path_buf()))?;
172170
let mut tsconfig =
173171
TsConfig::parse(root, &tsconfig_path, &mut tsconfig_string).map_err(|error| {

src/file_system.rs

Lines changed: 13 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -17,29 +17,19 @@ pub trait FileSystem: Send + Sync {
1717
#[cfg(not(feature = "yarn_pnp"))]
1818
fn new() -> Self;
1919

20-
/// See [std::fs::read_to_string]
20+
/// See [std::fs::read]
2121
///
2222
/// # Errors
2323
///
24-
/// * See [std::fs::read_to_string]
25-
/// ## Warning
26-
/// Use `&Path` instead of a generic `P: AsRef<Path>` here,
27-
/// because object safety requirements, it is especially useful, when
28-
/// you want to store multiple `dyn FileSystem` in a `Vec` or use a `ResolverGeneric<Fs>` in
29-
/// napi env.
30-
fn read_to_string(&self, path: &Path) -> io::Result<String>;
24+
/// * See [std::fs::read]
25+
fn read(&self, path: &Path) -> io::Result<Vec<u8>>;
3126

32-
/// Reads a file while bypassing the system cache.
33-
///
34-
/// This is useful in scenarios where the file content is already cached in memory
35-
/// and you want to avoid the overhead of using the system cache.
27+
/// See [std::fs::read_to_string]
3628
///
3729
/// # Errors
3830
///
3931
/// * See [std::fs::read_to_string]
40-
fn read_to_string_bypass_system_cache(&self, path: &Path) -> io::Result<String> {
41-
self.read_to_string(path)
42-
}
32+
fn read_to_string(&self, path: &Path) -> io::Result<String>;
4333

4434
/// See [std::fs::metadata]
4535
///
@@ -236,51 +226,26 @@ impl FileSystem for FileSystemOs {
236226
Self
237227
}
238228

239-
fn read_to_string(&self, path: &Path) -> io::Result<String> {
229+
fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
240230
cfg_if! {
241231
if #[cfg(feature = "yarn_pnp")] {
242232
if self.yarn_pnp {
243233
return match VPath::from(path)? {
244234
VPath::Zip(info) => {
245-
self.pnp_lru.read_to_string(info.physical_base_path(), info.zip_path)
235+
self.pnp_lru.read(info.physical_base_path(), info.zip_path)
246236
}
247-
VPath::Virtual(info) => Self::read_to_string(&info.physical_base_path()),
248-
VPath::Native(path) => Self::read_to_string(&path),
237+
VPath::Virtual(info) => fs::read(info.physical_base_path()),
238+
VPath::Native(path) => fs::read(path),
249239
}
250240
}
251241
}
252242
}
253-
Self::read_to_string(path)
243+
fs::read(path)
254244
}
255245

256-
#[allow(clippy::items_after_statements)]
257-
fn read_to_string_bypass_system_cache(&self, path: &Path) -> io::Result<String> {
258-
#[cfg(feature = "yarn_pnp")]
259-
if self.yarn_pnp {
260-
return match VPath::from(path)? {
261-
VPath::Zip(info) => {
262-
self.pnp_lru.read_to_string(info.physical_base_path(), info.zip_path)
263-
}
264-
VPath::Virtual(info) => Self::read_to_string(&info.physical_base_path()),
265-
VPath::Native(path) => Self::read_to_string(&path),
266-
};
267-
}
268-
269-
cfg_if! {
270-
if #[cfg(target_os = "macos")] {
271-
use std::io::Read;
272-
let mut fd = fs::OpenOptions::new().read(true).open(path)?;
273-
// Apply F_NOCACHE to bypass filesystem cache
274-
rustix::fs::fcntl_nocache(&fd, true)?;
275-
let meta = fd.metadata()?;
276-
#[allow(clippy::cast_possible_truncation)]
277-
let mut buffer = Vec::with_capacity(meta.len() as usize);
278-
fd.read_to_end(&mut buffer)?;
279-
Self::validate_string(buffer)
280-
} else {
281-
Self::read_to_string(path)
282-
}
283-
}
246+
fn read_to_string(&self, path: &Path) -> io::Result<String> {
247+
let bytes = self.read(path)?;
248+
Self::validate_string(bytes)
284249
}
285250

286251
fn metadata(&self, path: &Path) -> io::Result<FileMetadata> {

src/package_json/serde.rs

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -217,27 +217,23 @@ impl PackageJson {
217217
Ok(None)
218218
}
219219

220-
/// Parse a package.json file from JSON string
220+
/// Parse a package.json file from JSON bytes
221221
///
222222
/// # Errors
223223
pub fn parse<Fs: FileSystem>(
224224
_fs: &Fs,
225225
path: PathBuf,
226226
realpath: PathBuf,
227-
json: String,
227+
json: Vec<u8>,
228228
) -> Result<Self, JSONError> {
229-
// Strip BOM
230-
let json_string = if json.starts_with('\u{FEFF}') {
231-
json.trim_start_matches('\u{FEFF}')
232-
} else {
233-
json.as_str()
234-
};
229+
// Strip BOM - UTF-8 BOM is 3 bytes: 0xEF, 0xBB, 0xBF
230+
let json_bytes = if json.starts_with(b"\xEF\xBB\xBF") { &json[3..] } else { &json[..] };
235231

236232
// Check if empty after BOM stripping
237-
super::check_if_empty(json_string.as_bytes(), path.clone())?;
233+
super::check_if_empty(json_bytes, path.clone())?;
238234

239-
// Parse JSON
240-
let value = serde_json::from_str::<Value>(json_string).map_err(|error| JSONError {
235+
// Parse JSON directly from bytes
236+
let value = serde_json::from_slice::<Value>(json_bytes).map_err(|error| JSONError {
241237
path: path.clone(),
242238
message: error.to_string(),
243239
line: error.line(),

src/package_json/simd.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -250,18 +250,18 @@ impl PackageJson {
250250
Ok(None)
251251
}
252252

253-
/// Parse a package.json file from JSON string
253+
/// Parse a package.json file from JSON bytes
254254
///
255255
/// # Panics
256256
/// # Errors
257257
pub fn parse<Fs: FileSystem>(
258258
fs: &Fs,
259259
path: PathBuf,
260260
realpath: PathBuf,
261-
json: String,
261+
json: Vec<u8>,
262262
) -> Result<Self, JSONError> {
263263
// Strip BOM in place by replacing with spaces (no reallocation)
264-
let mut json_bytes = json.into_bytes();
264+
let mut json_bytes = json;
265265
if json_bytes.starts_with(b"\xEF\xBB\xBF") {
266266
json_bytes[0] = b' ';
267267
json_bytes[1] = b' ';
@@ -281,15 +281,15 @@ impl PackageJson {
281281
// We re-read because simd_json may have mutated the buffer during its failed parse attempt
282282
// simd_json doesn't provide line/column info, so we use serde_json for better error messages
283283
let fallback_result = fs
284-
.read_to_string(&realpath)
284+
.read(&realpath)
285285
.map_err(|io_error| JSONError {
286286
path: path.clone(),
287287
message: format!("Failed to re-read file for error reporting: {io_error}"),
288288
line: 0,
289289
column: 0,
290290
})
291-
.and_then(|content| {
292-
serde_json::from_str::<serde_json::Value>(&content).map_err(|serde_error| {
291+
.and_then(|bytes| {
292+
serde_json::from_slice::<serde_json::Value>(&bytes).map_err(|serde_error| {
293293
JSONError {
294294
path: path.clone(),
295295
message: serde_error.to_string(),

src/tests/memory_fs.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,17 +52,22 @@ impl FileSystem for MemoryFS {
5252
Self::default()
5353
}
5454

55-
fn read_to_string(&self, path: &Path) -> io::Result<String> {
55+
fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
5656
use vfs::FileSystem;
5757
let mut file = self
5858
.fs
5959
.open_file(path.to_string_lossy().as_ref())
6060
.map_err(|err| io::Error::new(io::ErrorKind::NotFound, err))?;
61-
let mut buffer = String::new();
62-
file.read_to_string(&mut buffer).unwrap();
61+
let mut buffer = Vec::new();
62+
file.read_to_end(&mut buffer).unwrap();
6363
Ok(buffer)
6464
}
6565

66+
fn read_to_string(&self, path: &Path) -> io::Result<String> {
67+
let bytes = self.read(path)?;
68+
crate::FileSystemOs::validate_string(bytes)
69+
}
70+
6671
fn metadata(&self, path: &Path) -> io::Result<FileMetadata> {
6772
use vfs::FileSystem;
6873
let metadata = self

0 commit comments

Comments
 (0)