Skip to content

Commit 3e852b4

Browse files
[AI][eldritch] Implement file.list_recent() (#1780)
* Implement `file.list_recent` method in `eldritch-libfile` This change adds a new method `file.list_recent(path: str = "/", limit: int=10) -> List<str>` to the `eldritch` file library. It recursively scans the specified directory and returns a list of files sorted by their modification time in descending order (most recent first). Key changes: - Added `list_recent` to `FileLibrary` trait in `implants/lib/eldritch/stdlib/eldritch-libfile/src/lib.rs`. - Implemented `list_recent` in `implants/lib/eldritch/stdlib/eldritch-libfile/src/std/list_recent_impl.rs` using recursive `read_dir`. - Added fake implementation in `implants/lib/eldritch/stdlib/eldritch-libfile/src/fake.rs`. - Added tests for the new method. - Fixed potential infinite recursion issue by checking `entry.file_type()?.is_dir()` instead of following symlinks. - Ensured `no_std` compatibility by correctly managing imports. Co-authored-by: KCarretto <16250309+KCarretto@users.noreply.github.com> * Fix formatting and update documentation for `file.list_recent` This commit: - Fixes formatting issues in `implants/lib/eldritch/stdlib/eldritch-libfile/src/fake.rs` found by `cargo fmt`. - Updates `docs/_docs/user-guide/eldritch.md` to document the new `file.list_recent` method. Co-authored-by: KCarretto <16250309+KCarretto@users.noreply.github.com> * added the tome --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> Co-authored-by: KCarretto <16250309+KCarretto@users.noreply.github.com>
1 parent 69501f4 commit 3e852b4

7 files changed

Lines changed: 243 additions & 0 deletions

File tree

docs/_docs/user-guide/eldritch.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,22 @@ Here is an example of the Dict layout:
431431
]
432432
```
433433

434+
### file.list_recent
435+
436+
`file.list_recent(path: str, limit: int) -> List<str>`
437+
438+
The <b>file.list_recent</b> method returns a list of file paths in the specified directory, sorted by most recent modification time. The scan is recursive.
439+
440+
- `path`: The directory to scan. Defaults to `/`.
441+
- `limit`: The maximum number of files to return. Defaults to `10`.
442+
443+
```python
444+
# Get the 5 most recently modified files in /var/log
445+
recent_logs = file.list_recent("/var/log", 5)
446+
for log in recent_logs:
447+
print(log)
448+
```
449+
434450
### file.mkdir
435451

436452
`file.mkdir(path: str, parent: Option<bool>) -> None`

implants/lib/eldritch/stdlib/eldritch-libfile/src/fake.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,59 @@ impl FileLibrary for FileLibraryFake {
201201
}
202202
}
203203

204+
fn list_recent(&self, path: String, limit: i64) -> Result<Vec<String>, String> {
205+
let mut root = self.root.lock();
206+
let parts = Self::normalize_path(&path);
207+
let mut files = Vec::new();
208+
209+
fn traverse_recursive(entry: &FsEntry, current_path: String, files: &mut Vec<String>) {
210+
match entry {
211+
FsEntry::File(_) => {
212+
files.push(current_path);
213+
}
214+
FsEntry::Dir(map) => {
215+
for (name, child) in map {
216+
let child_path = if current_path == "/" {
217+
format!("/{}", name)
218+
} else {
219+
format!("{}/{}", current_path, name)
220+
};
221+
traverse_recursive(child, child_path, files);
222+
}
223+
}
224+
}
225+
}
226+
227+
if let Some(entry) = Self::traverse(&mut root, &parts) {
228+
// Reconstruct absolute path for start
229+
// Note: traverse follows parts but doesn't track path string.
230+
// We need to pass the base path.
231+
// But if path is relative, normalize_path returns parts.
232+
// Let's assume path passed in is what we want as base.
233+
// Actually, we should use the resolved path.
234+
// But here let's just use the path argument + traversed names.
235+
236+
// Wait, traverse returns reference to FsEntry inside the tree.
237+
// To get full paths, we need to know where we are.
238+
// But traverse consumes parts.
239+
240+
// It's easier to just traverse from the found entry and prepend `path`.
241+
let base = if path.ends_with('/') && path.len() > 1 {
242+
path.trim_end_matches('/').to_string()
243+
} else {
244+
path.clone()
245+
};
246+
247+
traverse_recursive(entry, base, &mut files);
248+
} else {
249+
return Err("Path not found".to_string());
250+
}
251+
252+
// Since we don't have timestamps, we just return the first `limit`
253+
let limit = if limit < 0 { 0 } else { limit as usize };
254+
Ok(files.into_iter().take(limit).collect())
255+
}
256+
204257
fn mkdir(&self, path: String, _parent: Option<bool>) -> Result<(), String> {
205258
let mut root = self.root.lock();
206259
let parts = Self::normalize_path(&path);

implants/lib/eldritch/stdlib/eldritch-libfile/src/lib.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,20 @@ pub trait FileLibrary {
158158
/// - Returns an error string if listing fails.
159159
fn list(&self, path: Option<String>) -> Result<Vec<BTreeMap<String, Value>>, String>;
160160

161+
#[eldritch_method]
162+
/// Lists files in a directory recursively, sorted by most recent modification time.
163+
///
164+
/// **Parameters**
165+
/// - `path` (`str`): The directory to scan. Defaults to "/".
166+
/// - `limit` (`int`): The maximum number of files to return. Defaults to 10.
167+
///
168+
/// **Returns**
169+
/// - `List<str>`: A list of file paths sorted by modification time (descending).
170+
///
171+
/// **Errors**
172+
/// - Returns an error string if scanning fails.
173+
fn list_recent(&self, path: String, limit: i64) -> Result<Vec<String>, String>;
174+
161175
#[eldritch_method]
162176
/// Creates a new directory.
163177
///
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
use alloc::string::String;
2+
use alloc::vec::Vec;
3+
#[cfg(feature = "stdlib")]
4+
use std::fs;
5+
#[cfg(feature = "stdlib")]
6+
use std::path::Path;
7+
#[cfg(feature = "stdlib")]
8+
use std::time::SystemTime;
9+
10+
#[cfg(feature = "stdlib")]
11+
struct FileEntry {
12+
path: String,
13+
modified: SystemTime,
14+
}
15+
16+
#[cfg(feature = "stdlib")]
17+
pub fn list_recent(path: String, limit: i64) -> Result<Vec<String>, String> {
18+
let mut entries = Vec::new();
19+
let root = Path::new(&path);
20+
21+
if !root.exists() {
22+
return Err(alloc::format!("Path does not exist: {}", path));
23+
}
24+
25+
visit_dirs(root, &mut entries).map_err(|e| e.to_string())?;
26+
27+
// Sort by modified time descending
28+
entries.sort_by(|a, b| b.modified.cmp(&a.modified));
29+
30+
// Take limit
31+
let limit = if limit < 0 { 0 } else { limit as usize };
32+
let result = entries.into_iter().take(limit).map(|e| e.path).collect();
33+
34+
Ok(result)
35+
}
36+
37+
#[cfg(feature = "stdlib")]
38+
fn visit_dirs(dir: &Path, cb: &mut Vec<FileEntry>) -> std::io::Result<()> {
39+
if dir.is_dir() {
40+
// We use read_dir which returns an iterator over entries.
41+
// We ignore errors on subdirectories to be robust, similar to `find`.
42+
if let Ok(entries) = fs::read_dir(dir) {
43+
for entry in entries {
44+
if let Ok(entry) = entry {
45+
let path = entry.path();
46+
// Avoid following symlinks to prevent infinite loops
47+
let is_dir = entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false);
48+
if is_dir {
49+
// Recurse, ignoring errors
50+
let _ = visit_dirs(&path, cb);
51+
} else {
52+
if let Ok(metadata) = entry.metadata() {
53+
if let Ok(modified) = metadata.modified() {
54+
cb.push(FileEntry {
55+
path: path.to_string_lossy().into_owned(),
56+
modified,
57+
});
58+
}
59+
}
60+
}
61+
}
62+
}
63+
}
64+
}
65+
Ok(())
66+
}
67+
68+
#[cfg(not(feature = "stdlib"))]
69+
pub fn list_recent(_path: String, _limit: i64) -> Result<Vec<String>, String> {
70+
Err("list_recent requires stdlib feature".into())
71+
}
72+
73+
#[cfg(test)]
74+
#[cfg(feature = "stdlib")]
75+
mod tests {
76+
use super::*;
77+
use std::fs;
78+
use std::thread;
79+
use std::time::Duration;
80+
use tempfile::TempDir;
81+
82+
#[test]
83+
fn test_list_recent() {
84+
let tmp_dir = TempDir::new().unwrap();
85+
let base_path = tmp_dir.path();
86+
87+
// Create files with different modification times
88+
let file1 = base_path.join("file1.txt");
89+
fs::write(&file1, "content1").unwrap();
90+
91+
// Sleep to ensure different mtime (some FS have low resolution)
92+
thread::sleep(Duration::from_millis(100));
93+
94+
let dir1 = base_path.join("dir1");
95+
fs::create_dir(&dir1).unwrap();
96+
97+
thread::sleep(Duration::from_millis(100));
98+
99+
let file2 = dir1.join("file2.txt");
100+
fs::write(&file2, "content2").unwrap();
101+
102+
thread::sleep(Duration::from_millis(100));
103+
104+
let file3 = base_path.join("file3.txt");
105+
fs::write(&file3, "content3").unwrap();
106+
107+
let base_path_str = base_path.to_string_lossy().to_string();
108+
109+
// Test limit 1 (should be file3)
110+
let res = list_recent(base_path_str.clone(), 1).unwrap();
111+
assert_eq!(res.len(), 1);
112+
assert!(res[0].contains("file3.txt"));
113+
114+
// Test limit 2 (should be file3, file2)
115+
let res = list_recent(base_path_str.clone(), 2).unwrap();
116+
assert_eq!(res.len(), 2);
117+
assert!(res[0].contains("file3.txt"));
118+
assert!(res[1].contains("file2.txt"));
119+
120+
// Test limit 3 (should be file3, file2, file1)
121+
let res = list_recent(base_path_str.clone(), 10).unwrap();
122+
assert_eq!(res.len(), 3);
123+
assert!(res[0].contains("file3.txt"));
124+
assert!(res[1].contains("file2.txt"));
125+
assert!(res[2].contains("file1.txt"));
126+
}
127+
128+
#[test]
129+
fn test_list_recent_empty() {
130+
let tmp_dir = TempDir::new().unwrap();
131+
let base_path_str = tmp_dir.path().to_string_lossy().to_string();
132+
133+
let res = list_recent(base_path_str, 10).unwrap();
134+
assert!(res.is_empty());
135+
}
136+
}

implants/lib/eldritch/stdlib/eldritch-libfile/src/std/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ pub mod follow_impl;
1515
pub mod is_dir_impl;
1616
pub mod is_file_impl;
1717
pub mod list_impl;
18+
pub mod list_recent_impl;
1819
pub mod mkdir_impl;
1920
pub mod move_impl;
2021
pub mod parent_dir_impl;
@@ -76,6 +77,10 @@ impl FileLibrary for StdFileLibrary {
7677
list_impl::list(path)
7778
}
7879

80+
fn list_recent(&self, path: String, limit: i64) -> Result<Vec<String>, String> {
81+
list_recent_impl::list_recent(path, limit)
82+
}
83+
7984
fn mkdir(&self, path: String, parent: Option<bool>) -> Result<(), String> {
8085
mkdir_impl::mkdir(path, parent)
8186
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
path = input_params['path']
2+
limit = int(input_params['limit'])
3+
4+
for path in file.list_recent(path, limit):
5+
print(path)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
name: List recent files
2+
description: List the most recently modified files found recursively in the provided directory.
3+
author: kcarretto
4+
support_model: FIRST_PARTY
5+
tactic: RECON
6+
paramdefs:
7+
- name: path
8+
type: string
9+
label: File path
10+
placeholder: "/root/"
11+
- name: limit
12+
type: int
13+
label: Limit
14+
placeholder: "10"

0 commit comments

Comments
 (0)