cobolx 1.0.3 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/package.json +1 -1
- package/src/agent/client.rs +107 -8
- package/src/agent/db_agent.rs +71 -23
- package/src/agent/explain_agent.rs +53 -22
- package/src/agent/fs_agent.rs +211 -83
- package/src/agent/skills.rs +336 -0
- package/src/agent/types.rs +7 -0
- package/src/agent.rs +1 -0
- package/src/cobol/indexer.rs +375 -5
- package/src/cobol/model.rs +78 -0
- package/src/cobol/scanner.rs +2 -0
- package/src/cobol/source_parser.rs +341 -2
- package/src/lib.rs +1 -0
- package/src/main.rs +1 -0
- package/src/memory/memories.rs +208 -0
- package/src/memory/runs.rs +161 -0
- package/src/memory/store.rs +120 -0
- package/src/memory.rs +8 -2
- package/src/path_safety.rs +280 -0
- package/src/ui/draw.rs +1 -0
- package/src/ui/tui.rs +239 -0
- package/tests/indexer_tests.rs +261 -0
- package/tests/project_files_tests.rs +23 -51
- package/src/memory/files.rs +0 -155
|
@@ -1,72 +1,44 @@
|
|
|
1
|
-
use rdo::
|
|
1
|
+
use rdo::path_safety::write_sandbox_file;
|
|
2
2
|
use tempfile::tempdir;
|
|
3
3
|
|
|
4
4
|
#[test]
|
|
5
|
-
fn
|
|
5
|
+
fn markdown_files_are_created_under_docs_dir() {
|
|
6
6
|
let dir = tempdir().unwrap();
|
|
7
|
-
let
|
|
8
|
-
|
|
9
|
-
let path =
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
assert!(path.starts_with(dir.path().join("docs")));
|
|
7
|
+
let sandbox = dir.path();
|
|
8
|
+
|
|
9
|
+
let path = write_sandbox_file(sandbox, "docs/analysis/init.md", "# Init\n").unwrap();
|
|
10
|
+
assert!(path.starts_with(sandbox.join("docs")));
|
|
11
|
+
write_sandbox_file(
|
|
12
|
+
sandbox,
|
|
13
|
+
"docs/analysis/init.md",
|
|
14
|
+
"# Init\n\nCOBOL inventory ready.\n",
|
|
15
|
+
)
|
|
16
|
+
.unwrap();
|
|
18
17
|
assert_eq!(
|
|
19
|
-
|
|
18
|
+
std::fs::read_to_string(sandbox.join("docs/analysis/init.md")).unwrap(),
|
|
20
19
|
"# Init\n\nCOBOL inventory ready.\n"
|
|
21
20
|
);
|
|
22
21
|
}
|
|
23
22
|
|
|
24
23
|
#[test]
|
|
25
|
-
fn
|
|
26
|
-
let dir = tempdir().unwrap();
|
|
27
|
-
let store = MemoryStore::open_or_create(dir.path()).unwrap();
|
|
28
|
-
|
|
29
|
-
let path = store
|
|
30
|
-
.write_skill_file("cobol-migration/SKILL.md", "# COBOL Migration\n")
|
|
31
|
-
.unwrap();
|
|
32
|
-
|
|
33
|
-
assert!(path.starts_with(store.skills_dir()));
|
|
34
|
-
assert_eq!(
|
|
35
|
-
store.read_skill_file("cobol-migration/SKILL.md").unwrap(),
|
|
36
|
-
"# COBOL Migration\n"
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
#[test]
|
|
41
|
-
fn windows_style_relative_paths_are_normalized() {
|
|
24
|
+
fn windows_style_relative_paths_are_normalized_for_docs() {
|
|
42
25
|
let dir = tempdir().unwrap();
|
|
43
|
-
let
|
|
44
|
-
|
|
45
|
-
let path = store
|
|
46
|
-
.write_markdown("analysis\\windows.md", "# Windows\n")
|
|
47
|
-
.unwrap();
|
|
26
|
+
let sandbox = dir.path();
|
|
48
27
|
|
|
49
|
-
|
|
28
|
+
write_sandbox_file(sandbox, "docs\\analysis\\windows.md", "# Windows\n").unwrap();
|
|
50
29
|
assert_eq!(
|
|
51
|
-
|
|
30
|
+
std::fs::read_to_string(sandbox.join("docs/analysis/windows.md")).unwrap(),
|
|
52
31
|
"# Windows\n"
|
|
53
32
|
);
|
|
54
33
|
}
|
|
55
34
|
|
|
56
35
|
#[test]
|
|
57
|
-
fn
|
|
36
|
+
fn sandbox_writer_rejects_unsafe_or_wrong_paths() {
|
|
58
37
|
let dir = tempdir().unwrap();
|
|
59
|
-
let
|
|
38
|
+
let sandbox = dir.path();
|
|
60
39
|
|
|
61
|
-
assert!(
|
|
62
|
-
assert!(
|
|
63
|
-
assert!(
|
|
64
|
-
assert!(
|
|
65
|
-
assert!(store.write_markdown("CON.md", "bad").is_err());
|
|
66
|
-
assert!(
|
|
67
|
-
store
|
|
68
|
-
.write_markdown("notes/not-markdown.txt", "bad")
|
|
69
|
-
.is_err()
|
|
70
|
-
);
|
|
71
|
-
assert!(store.write_skill_file("../SKILL.md", "bad").is_err());
|
|
40
|
+
assert!(write_sandbox_file(sandbox, "../escape.md", "bad").is_err());
|
|
41
|
+
assert!(write_sandbox_file(sandbox, "..\\escape.md", "bad").is_err());
|
|
42
|
+
assert!(write_sandbox_file(sandbox, "docs/CON.md", "bad").is_err());
|
|
43
|
+
assert!(write_sandbox_file(sandbox, "docs/notes/not-markdown.txt", "bad").is_err());
|
|
72
44
|
}
|
package/src/memory/files.rs
DELETED
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
use crate::memory::MemoryStore;
|
|
2
|
-
use std::error::Error;
|
|
3
|
-
use std::path::{Path, PathBuf};
|
|
4
|
-
|
|
5
|
-
type FileResult<T> = Result<T, Box<dyn Error + Send + Sync>>;
|
|
6
|
-
|
|
7
|
-
#[allow(dead_code)]
|
|
8
|
-
impl MemoryStore {
|
|
9
|
-
pub fn write_markdown(
|
|
10
|
-
&self,
|
|
11
|
-
relative_path: impl AsRef<Path>,
|
|
12
|
-
content: &str,
|
|
13
|
-
) -> FileResult<PathBuf> {
|
|
14
|
-
let path = resolve_markdown(self.docs_dir(), relative_path.as_ref())?;
|
|
15
|
-
write_file(&path, content)?;
|
|
16
|
-
Ok(path)
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
pub fn append_markdown(
|
|
20
|
-
&self,
|
|
21
|
-
relative_path: impl AsRef<Path>,
|
|
22
|
-
content: &str,
|
|
23
|
-
) -> FileResult<PathBuf> {
|
|
24
|
-
let path = resolve_markdown(self.docs_dir(), relative_path.as_ref())?;
|
|
25
|
-
append_file(&path, content)?;
|
|
26
|
-
Ok(path)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
pub fn read_markdown(&self, relative_path: impl AsRef<Path>) -> FileResult<String> {
|
|
30
|
-
let path = resolve_markdown(self.docs_dir(), relative_path.as_ref())?;
|
|
31
|
-
Ok(std::fs::read_to_string(path)?)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
pub fn write_skill_file(
|
|
35
|
-
&self,
|
|
36
|
-
relative_path: impl AsRef<Path>,
|
|
37
|
-
content: &str,
|
|
38
|
-
) -> FileResult<PathBuf> {
|
|
39
|
-
let path = resolve_under(self.skills_dir(), relative_path.as_ref())?;
|
|
40
|
-
write_file(&path, content)?;
|
|
41
|
-
Ok(path)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
pub fn read_skill_file(&self, relative_path: impl AsRef<Path>) -> FileResult<String> {
|
|
45
|
-
let path = resolve_under(self.skills_dir(), relative_path.as_ref())?;
|
|
46
|
-
Ok(std::fs::read_to_string(path)?)
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
fn resolve_markdown(base: &Path, relative_path: &Path) -> FileResult<PathBuf> {
|
|
51
|
-
let raw = relative_path.as_os_str().to_string_lossy();
|
|
52
|
-
if !raw.to_ascii_lowercase().ends_with(".md") {
|
|
53
|
-
return Err("markdown file path must end with .md".into());
|
|
54
|
-
}
|
|
55
|
-
resolve_under(base, relative_path)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
fn resolve_under(base: &Path, relative_path: &Path) -> FileResult<PathBuf> {
|
|
59
|
-
let raw = relative_path.as_os_str().to_string_lossy();
|
|
60
|
-
let normalized = normalize_relative_path(&raw)?;
|
|
61
|
-
let mut sanitized = PathBuf::new();
|
|
62
|
-
for part in normalized.split('/') {
|
|
63
|
-
sanitized.push(part);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if sanitized.as_os_str().is_empty() {
|
|
67
|
-
return Err("file path must not be empty".into());
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
Ok(base.join(sanitized))
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
fn normalize_relative_path(raw: &str) -> FileResult<String> {
|
|
74
|
-
if raw.is_empty() {
|
|
75
|
-
return Err("file path must not be empty".into());
|
|
76
|
-
}
|
|
77
|
-
if raw.as_bytes().contains(&0) {
|
|
78
|
-
return Err("file path must not contain NUL".into());
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
let normalized = raw.replace('\\', "/");
|
|
82
|
-
if normalized.starts_with('/') {
|
|
83
|
-
return Err("file path must be relative".into());
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
let mut parts = Vec::new();
|
|
87
|
-
for part in normalized.split('/') {
|
|
88
|
-
if part.is_empty() || part == "." {
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
if part == ".." {
|
|
92
|
-
return Err("file path must not contain parent components".into());
|
|
93
|
-
}
|
|
94
|
-
if has_windows_forbidden_chars(part) {
|
|
95
|
-
return Err("file path contains characters invalid on Windows".into());
|
|
96
|
-
}
|
|
97
|
-
if is_windows_reserved_name(part) {
|
|
98
|
-
return Err("file path contains a Windows reserved name".into());
|
|
99
|
-
}
|
|
100
|
-
parts.push(part);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if parts.is_empty() {
|
|
104
|
-
return Err("file path must not be empty".into());
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
Ok(parts.join("/"))
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
fn has_windows_forbidden_chars(part: &str) -> bool {
|
|
111
|
-
part.contains(':')
|
|
112
|
-
|| part
|
|
113
|
-
.chars()
|
|
114
|
-
.any(|c| matches!(c, '<' | '>' | '"' | '|' | '?' | '*'))
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
fn is_windows_reserved_name(part: &str) -> bool {
|
|
118
|
-
let stem = part
|
|
119
|
-
.split('.')
|
|
120
|
-
.next()
|
|
121
|
-
.unwrap_or(part)
|
|
122
|
-
.trim_end_matches(' ')
|
|
123
|
-
.to_ascii_uppercase();
|
|
124
|
-
|
|
125
|
-
matches!(stem.as_str(), "CON" | "PRN" | "AUX" | "NUL")
|
|
126
|
-
|| stem
|
|
127
|
-
.strip_prefix("COM")
|
|
128
|
-
.and_then(|n| n.parse::<u8>().ok())
|
|
129
|
-
.is_some_and(|n| (1..=9).contains(&n))
|
|
130
|
-
|| stem
|
|
131
|
-
.strip_prefix("LPT")
|
|
132
|
-
.and_then(|n| n.parse::<u8>().ok())
|
|
133
|
-
.is_some_and(|n| (1..=9).contains(&n))
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
fn write_file(path: &Path, content: &str) -> FileResult<()> {
|
|
137
|
-
if let Some(parent) = path.parent() {
|
|
138
|
-
std::fs::create_dir_all(parent)?;
|
|
139
|
-
}
|
|
140
|
-
std::fs::write(path, content)?;
|
|
141
|
-
Ok(())
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
fn append_file(path: &Path, content: &str) -> FileResult<()> {
|
|
145
|
-
if let Some(parent) = path.parent() {
|
|
146
|
-
std::fs::create_dir_all(parent)?;
|
|
147
|
-
}
|
|
148
|
-
use std::io::Write;
|
|
149
|
-
let mut file = std::fs::OpenOptions::new()
|
|
150
|
-
.create(true)
|
|
151
|
-
.append(true)
|
|
152
|
-
.open(path)?;
|
|
153
|
-
file.write_all(content.as_bytes())?;
|
|
154
|
-
Ok(())
|
|
155
|
-
}
|