Skip to content

Commit abbbd64

Browse files
authored
file-entry-cache - feat: adding in current working directory (#1372)
* file-entry-cache - feat: adding in current working directory * moving to the name cwd * adding jsDoc to functions * adding in jsDoc for types * Update README.md * Create file-rename.test.ts * Update README.md * path sanitization testing * fixing strict paths * strict paths true by default * enable strictPaths
1 parent 1fe44f1 commit abbbd64

5 files changed

Lines changed: 1301 additions & 57 deletions

File tree

packages/file-entry-cache/README.md

Lines changed: 218 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,25 @@
1515
- Ideal for processes that work on a specific set of files
1616
- Persists cache to Disk via `reconcile()` or `persistInterval` on `cache` options.
1717
- Uses `checksum` to determine if a file has changed
18-
- Supports `relative` and `absolute` paths - paths are stored exactly as provided
18+
- Supports `relative` and `absolute` paths with configurable current working directory
1919
- Portable cache files when using relative paths
2020
- ESM and CommonJS support with Typescript
2121

2222
# Table of Contents
2323

2424
- [Installation](#installation)
2525
- [Getting Started](#getting-started)
26+
- [Changes from v10 to v11](#changes-from-v10-to-v11)
2627
- [Changes from v9 to v10](#changes-from-v9-to-v10)
2728
- [Global Default Functions](#global-default-functions)
2829
- [FileEntryCache Options (FileEntryCacheOptions)](#fileentrycache-options-fileentrycacheoptions)
2930
- [API](#api)
3031
- [Get File Descriptor](#get-file-descriptor)
32+
- [Path Handling and Current Working Directory](#path-handling-and-current-working-directory)
33+
- [Cache Portability](#cache-portability)
34+
- [Maximum Portability with Checksums](#maximum-portability-with-checksums)
35+
- [Handling Project Relocations](#handling-project-relocations)
36+
- [Path Security and Traversal Prevention](#path-security-and-traversal-prevention)
3137
- [Using Checksums to Determine if a File has Changed (useCheckSum)](#using-checksums-to-determine-if-a-file-has-changed-usechecksum)
3238
- [Setting Additional Meta Data](#setting-additional-meta-data)
3339
- [How to Contribute](#how-to-contribute)
@@ -78,41 +84,16 @@ let fileDescriptor = cache.getFileDescriptor('./src/file.txt');
7884
console.log(fileDescriptor.changed); // false as it has not changed from the saved cache.
7985
```
8086

81-
## Migration Guide from v10 to v11
82-
83-
The main breaking change is the removal of `currentWorkingDirectory`. Here's how to update your code:
84-
85-
**Before (v10):**
86-
```javascript
87-
const cache = new FileEntryCache({
88-
currentWorkingDirectory: '/project/root'
89-
});
90-
// This would store the key as 'src/file.js'
91-
cache.getFileDescriptor('/project/root/src/file.js');
92-
```
93-
94-
**After (v11):**
95-
```javascript
96-
const cache = new FileEntryCache();
97-
// Now stores the key exactly as provided
98-
cache.getFileDescriptor('./src/file.js'); // Key: './src/file.js'
99-
cache.getFileDescriptor('/project/root/src/file.js'); // Key: '/project/root/src/file.js'
100-
```
101-
102-
If you were using absolute paths with `currentWorkingDirectory`, you'll need to update your code to use relative paths if you want portable cache files.
10387

10488
# Changes from v10 to v11
10589

10690
**BREAKING CHANGES:**
107-
- **Removed `currentWorkingDirectory`** - This option has been completely removed from the API. Paths are now stored exactly as provided (relative or absolute).
108-
- **Path handling changes** - The cache now stores paths exactly as they are provided:
109-
- Relative paths remain relative in the cache
110-
- Absolute paths remain absolute in the cache
111-
- The same file accessed with different path formats will create separate cache entries
112-
- **Renamed method** - `renameAbsolutePathKeys()` is now `renameCacheKeys()` to reflect that it works with any path format
113-
- **Simplified API** - Removed `currentWorkingDirectory` parameter from all methods including `getFileDescriptor()`, `removeEntry()`, and factory functions
91+
- **`strictPaths` now defaults to `true`** - Path traversal protection is enabled by default for security. To restore v10 behavior, explicitly set `strictPaths: false`
11492

115-
These changes make cache files portable when using relative paths, and simplify the API by removing path manipulation logic.
93+
**NEW FEATURES:**
94+
- **Added `cwd` option** - You can now specify a custom current working directory for resolving relative paths
95+
- **Added `strictPaths` option** - Provides protection against path traversal attacks (enabled by default)
96+
- **Improved cache portability** - When using relative paths with the same `cwd`, cache files are portable across different environments
11697

11798
# Changes from v9 to v10
11899

@@ -127,13 +108,15 @@ There have been many features added and changes made to the `file-entry-cache` c
127108
- On `FileEntryDescriptor.meta` if using typescript you need to use the `meta.data` to set additional information. This is to allow for better type checking and to avoid conflicts with the `meta` object which was `any`.
128109

129110
# Global Default Functions
130-
- `create(cacheId: string, cacheDirectory?: string, useCheckSum?: boolean)` - Creates a new instance of the `FileEntryCache` class
131-
- `createFromFile(cachePath: string, useCheckSum?: boolean)` - Creates a new instance of the `FileEntryCache` class and loads the cache from a file.
111+
- `create(cacheId: string, cacheDirectory?: string, useCheckSum?: boolean, cwd?: string)` - Creates a new instance of the `FileEntryCache` class
112+
- `createFromFile(cachePath: string, useCheckSum?: boolean, cwd?: string)` - Creates a new instance of the `FileEntryCache` class and loads the cache from a file.
132113

133114
# FileEntryCache Options (FileEntryCacheOptions)
134115
- `useModifiedTime?` - If `true` it will use the modified time to determine if the file has changed. Default is `true`
135116
- `useCheckSum?` - If `true` it will use a checksum to determine if the file has changed. Default is `false`
136117
- `hashAlgorithm?` - The algorithm to use for the checksum. Default is `md5` but can be any algorithm supported by `crypto.createHash`
118+
- `cwd?` - The current working directory for resolving relative paths. Default is `process.cwd()`
119+
- `strictPaths?` - If `true` restricts file access to within `cwd` boundaries, preventing path traversal attacks. Default is `true`
137120
- `cache.ttl?` - The time to live for the cache in milliseconds. Default is `0` which means no expiration
138121
- `cache.lruSize?` - The number of items to keep in the cache. Default is `0` which means no limit
139122
- `cache.useClone?` - If `true` it will clone the data before returning it. Default is `false`
@@ -150,17 +133,21 @@ There have been many features added and changes made to the `file-entry-cache` c
150133
- `useCheckSum: boolean` - If `true` it will use a checksum to determine if the file has changed. Default is `false`
151134
- `hashAlgorithm: string` - The algorithm to use for the checksum. Default is `md5` but can be any algorithm supported by `crypto.createHash`
152135
- `getHash(buffer: Buffer): string` - Gets the hash of a buffer used for checksums
136+
- `cwd: string` - The current working directory for resolving relative paths. Default is `process.cwd()`
137+
- `strictPaths: boolean` - If `true` restricts file access to within `cwd` boundaries. Default is `true`
153138
- `createFileKey(filePath: string): string` - Returns the cache key for the file path (returns the path exactly as provided).
154139
- `deleteCacheFile(): boolean` - Deletes the cache file from disk
155140
- `destroy(): void` - Destroys the cache. This will clear the cache in memory. If using cache persistence it will stop the interval.
156-
- `removeEntry(filePath: string): void` - Removes an entry from the cache using the exact path provided.
141+
- `removeEntry(filePath: string): void` - Removes an entry from the cache.
157142
- `reconcile(): void` - Saves the cache to disk and removes any files that are no longer found.
158143
- `hasFileChanged(filePath: string): boolean` - Checks if the file has changed. This will return `true` if the file has changed.
159144
- `getFileDescriptor(filePath: string, options?: { useModifiedTime?: boolean, useCheckSum?: boolean }): FileEntryDescriptor` - Gets the file descriptor for the file. Please refer to the entire section on `Get File Descriptor` for more information.
160-
- `normalizeEntries(entries: FileEntryDescriptor[]): FileEntryDescriptor[]` - Normalizes the entries to have the correct paths. This is used when loading the cache from disk.
145+
- `normalizeEntries(files?: string[]): FileDescriptor[]` - Normalizes the entries. If no files are provided, it will return all cached entries.
161146
- `analyzeFiles(files: string[])` will return `AnalyzedFiles` object with `changedFiles`, `notFoundFiles`, and `notChangedFiles` as FileDescriptor arrays.
162147
- `getUpdatedFiles(files: string[])` will return an array of `FileEntryDescriptor` objects that have changed.
163148
- `getFileDescriptorsByPath(filePath: string): FileEntryDescriptor[]` will return an array of `FileEntryDescriptor` objects that starts with the path prefix specified.
149+
- `getAbsolutePath(filePath: string): string` - Resolves a relative path to absolute using the configured `cwd`. Returns absolute paths unchanged. When `strictPaths` is enabled, throws an error if the path resolves outside `cwd`.
150+
- `getAbsolutePathWithCwd(filePath: string, cwd: string): string` - Resolves a relative path to absolute using a custom working directory. When `strictPaths` is enabled, throws an error if the path resolves outside the provided `cwd`.
164151

165152
# Get File Descriptor
166153

@@ -172,32 +159,94 @@ The `getFileDescriptor(filePath: string, options?: { useCheckSum?: boolean, useM
172159
- `meta: FileEntryMeta` - The meta data for the file. This has the following properties: `size`, `mtime`, `hash`, `data`. Note that `data` is an object that can be used to store additional information.
173160
- `err` - If there was an error analyzing the file.
174161

175-
## Path Handling
162+
## Path Handling and Current Working Directory
163+
164+
The cache stores paths exactly as they are provided (relative or absolute). When checking if files have changed, relative paths are resolved using the configured `cwd` (current working directory):
165+
166+
```javascript
167+
// Default: uses process.cwd()
168+
const cache1 = fileEntryCache.create('cache1');
169+
170+
// Custom working directory
171+
const cache2 = fileEntryCache.create('cache2', './cache', false, '/project/root');
172+
// Or with options object
173+
const cache3 = new FileEntryCache({ cwd: '/project/root' });
174+
175+
// The cache key is always the provided path
176+
const descriptor = cache2.getFileDescriptor('./src/file.txt');
177+
console.log(descriptor.key); // './src/file.txt'
178+
// But file operations resolve from: '/project/root/src/file.txt'
179+
```
176180

177-
The cache stores paths exactly as they are provided:
181+
### Cache Portability
178182

179-
- **Relative paths** remain relative in the cache
180-
- **Absolute paths** remain absolute in the cache
181-
- The same file accessed with different path formats creates **separate cache entries**
183+
Using relative paths with a consistent `cwd` (defaults to `process.cwd()`) makes cache files portable across different machines and environments. This is especially useful for CI/CD pipelines and team development.
182184

183185
```javascript
184-
const fileEntryCache = new FileEntryCache();
186+
// On machine A (project at /home/user/project)
187+
const cacheA = fileEntryCache.create('build-cache', './cache', false, '/home/user/project');
188+
cacheA.getFileDescriptor('./src/index.js'); // Resolves to /home/user/project/src/index.js
189+
cacheA.reconcile();
190+
191+
// On machine B (project at /workspace/project)
192+
const cacheB = fileEntryCache.create('build-cache', './cache', false, '/workspace/project');
193+
cacheB.getFileDescriptor('./src/index.js'); // Resolves to /workspace/project/src/index.js
194+
// Cache hit! File hasn't changed since machine A
195+
```
185196

186-
// Using a relative path
187-
const relativeDescriptor = fileEntryCache.getFileDescriptor('./file.txt');
188-
console.log(relativeDescriptor.key); // './file.txt'
197+
### Maximum Portability with Checksums
189198

190-
// Using an absolute path
191-
const absolutePath = path.resolve('./file.txt');
192-
const absoluteDescriptor = fileEntryCache.getFileDescriptor(absolutePath);
193-
console.log(absoluteDescriptor.key); // '/full/path/to/file.txt'
199+
For maximum cache portability across different environments, use checksums (`useCheckSum: true`) along with relative paths and `cwd` which defaults to `process.cwd()`. This ensures that cache validity is determined by file content rather than modification times, which can vary across systems:
194200

195-
// These create two separate cache entries even though they refer to the same file
201+
```javascript
202+
// Development machine
203+
const devCache = fileEntryCache.create(
204+
'.buildcache',
205+
'./cache', // cache directory
206+
true // Use checksums for content-based comparison
207+
);
208+
209+
// Process files using relative paths
210+
const descriptor = devCache.getFileDescriptor('./src/index.js');
211+
if (descriptor.changed) {
212+
console.log('Building ./src/index.js...');
213+
// Build process here
214+
}
215+
devCache.reconcile(); // Save cache
216+
217+
// CI/CD Pipeline or another developer's machine
218+
const ciCache = fileEntryCache.create(
219+
'.buildcache',
220+
'./node_modules/.cache',
221+
true, // Same checksum setting
222+
process.cwd() // Different absolute path, same relative structure
223+
);
224+
225+
// Same relative path works across environments
226+
const descriptor2 = ciCache.getFileDescriptor('./src/index.js');
227+
if (!descriptor2.changed) {
228+
console.log('Using cached result for ./src/index.js');
229+
// Skip rebuild - file content unchanged
230+
}
196231
```
197232

198-
This behavior makes cache files portable when using relative paths, as they will work correctly when the project is moved to a different location.
233+
### Handling Project Relocations
234+
235+
Cache remains valid even when projects are moved or renamed:
236+
237+
```javascript
238+
// Original location: /projects/my-app
239+
const cache1 = fileEntryCache.create('.cache', './cache', true, '/projects/my-app');
240+
cache1.getFileDescriptor('./src/app.js');
241+
cache1.reconcile();
242+
243+
// After moving project to: /archived/2024/my-app
244+
const cache2 = fileEntryCache.create('.cache', './cache', true, '/archived/2024/my-app');
245+
cache2.getFileDescriptor('./src/app.js'); // Still finds cached entry!
246+
// Cache valid as long as relative structure unchanged
247+
```
199248

200-
If there is an error when trying to get the file descriptor it will return an ``notFound` and `err` property with the error.
249+
If there is an error when trying to get the file descriptor it will return a `notFound` and `err` property with the error.
201250

202251
```javascript
203252
const fileEntryCache = new FileEntryCache();
@@ -211,6 +260,124 @@ if (fileDescriptor.notFound) {
211260
}
212261
```
213262

263+
# Path Security and Traversal Prevention
264+
265+
The `strictPaths` option provides security against path traversal attacks by restricting file access to within the configured `cwd` boundaries. **This is enabled by default (since v11)** to ensure secure defaults when processing untrusted input or when running in security-sensitive environments.
266+
267+
## Basic Usage
268+
269+
```javascript
270+
// strictPaths is enabled by default for security
271+
const cache = new FileEntryCache({
272+
cwd: '/project/root'
273+
});
274+
275+
// This will work - file is within cwd
276+
const descriptor = cache.getFileDescriptor('./src/index.js');
277+
278+
// This will throw an error - attempts to access parent directory
279+
try {
280+
cache.getFileDescriptor('../../../etc/passwd');
281+
} catch (error) {
282+
console.error(error); // Path traversal attempt blocked
283+
}
284+
285+
// To allow parent directory access (not recommended for untrusted input)
286+
const unsafeCache = new FileEntryCache({
287+
cwd: '/project/root',
288+
strictPaths: false // Explicitly disable protection
289+
});
290+
```
291+
292+
## Security Features
293+
294+
When `strictPaths` is enabled:
295+
- **Path Traversal Prevention**: Blocks attempts to access files outside the working directory using `../` sequences
296+
- **Null Byte Protection**: Automatically removes null bytes from paths to prevent injection attacks
297+
- **Path Normalization**: Cleans and normalizes paths to prevent bypass attempts
298+
299+
## Use Cases
300+
301+
### Build Tools with Untrusted Input
302+
```javascript
303+
// Secure build tool configuration
304+
const cache = fileEntryCache.create(
305+
'.buildcache',
306+
'./cache',
307+
true, // useCheckSum
308+
process.cwd()
309+
);
310+
311+
// Enable strict path checking for security
312+
cache.strictPaths = true;
313+
314+
// Process user-provided file paths safely
315+
function processUserFile(userProvidedPath) {
316+
try {
317+
const descriptor = cache.getFileDescriptor(userProvidedPath);
318+
// Safe to process - file is within boundaries
319+
return descriptor;
320+
} catch (error) {
321+
if (error.message.includes('Path traversal attempt blocked')) {
322+
console.warn('Security: Blocked access to:', userProvidedPath);
323+
return null;
324+
}
325+
throw error;
326+
}
327+
}
328+
```
329+
330+
### CI/CD Environments
331+
```javascript
332+
// Strict security for CI/CD pipelines
333+
const cache = new FileEntryCache({
334+
cwd: process.env.GITHUB_WORKSPACE || process.cwd(),
335+
strictPaths: true, // Prevent access outside workspace
336+
useCheckSum: true // Content-based validation
337+
});
338+
339+
// All file operations are now restricted to the workspace
340+
cache.getFileDescriptor('./src/app.js'); // ✓ Allowed
341+
cache.getFileDescriptor('/etc/passwd'); // ✗ Blocked (absolute path outside cwd)
342+
cache.getFileDescriptor('../../../root'); // ✗ Blocked (path traversal)
343+
```
344+
345+
### Dynamic Security Control
346+
```javascript
347+
const cache = new FileEntryCache({ cwd: '/safe/directory' });
348+
349+
// Start with relaxed mode for trusted operations
350+
cache.strictPaths = false;
351+
processInternalFiles();
352+
353+
// Enable strict mode for untrusted input
354+
cache.strictPaths = true;
355+
processUserUploadedPaths();
356+
357+
// Return to relaxed mode if needed
358+
cache.strictPaths = false;
359+
```
360+
361+
## Default Behavior
362+
363+
**As of v11, `strictPaths` is enabled by default** to provide secure defaults. This means:
364+
- Path traversal attempts using `../` are blocked
365+
- File access is restricted to within the configured `cwd`
366+
- Null bytes in paths are automatically sanitized
367+
368+
### Migrating from v10 or Earlier
369+
370+
If you're upgrading from v10 or earlier and need to maintain the previous behavior (for example, if your code legitimately accesses parent directories), you can explicitly disable strict paths:
371+
372+
```javascript
373+
const cache = new FileEntryCache({
374+
cwd: process.cwd(),
375+
strictPaths: false // Restore v10 behavior
376+
});
377+
```
378+
379+
However, we strongly recommend keeping `strictPaths: true` and adjusting your code to work within the security boundaries, especially when processing any untrusted input.
380+
214381
# Using Checksums to Determine if a File has Changed (useCheckSum)
215382

216383
By default the `useCheckSum` is `false`. This means that the `FileEntryCache` will use the `mtime` and `ctime` to determine if the file has changed. If you set `useCheckSum` to `true` it will use a checksum to determine if the file has changed. This is useful when you want to make sure that the file has not changed at all.

0 commit comments

Comments
 (0)