@lamentis/naome 1.3.8 → 1.3.10
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 +2 -2
- package/README.md +5 -0
- package/bin/naome.js +1 -1
- package/crates/naome-cli/Cargo.toml +1 -1
- package/crates/naome-cli/src/architecture_commands.rs +123 -0
- package/crates/naome-cli/src/cli_args.rs +4 -0
- package/crates/naome-cli/src/dispatcher.rs +2 -0
- package/crates/naome-cli/src/install_bridge.rs +56 -8
- package/crates/naome-cli/src/main.rs +6 -0
- package/crates/naome-core/Cargo.toml +1 -1
- package/crates/naome-core/src/architecture/config/parser/scalar.rs +26 -0
- package/crates/naome-core/src/architecture/config/parser/sections.rs +137 -0
- package/crates/naome-core/src/architecture/config/parser.rs +96 -0
- package/crates/naome-core/src/architecture/config.rs +114 -0
- package/crates/naome-core/src/architecture/model.rs +80 -0
- package/crates/naome-core/src/architecture/output.rs +178 -0
- package/crates/naome-core/src/architecture/rules.rs +140 -0
- package/crates/naome-core/src/architecture/scan/graph_builder/emit.rs +56 -0
- package/crates/naome-core/src/architecture/scan/graph_builder/facts.rs +88 -0
- package/crates/naome-core/src/architecture/scan/graph_builder.rs +134 -0
- package/crates/naome-core/src/architecture/scan/path_scan.rs +92 -0
- package/crates/naome-core/src/architecture/scan.rs +75 -0
- package/crates/naome-core/src/architecture.rs +31 -0
- package/crates/naome-core/src/harness_health/integrity.rs +41 -23
- package/crates/naome-core/src/harness_health/manifest.rs +97 -0
- package/crates/naome-core/src/harness_health.rs +58 -106
- package/crates/naome-core/src/install_plan.rs +2 -0
- package/crates/naome-core/src/lib.rs +16 -8
- package/crates/naome-core/src/quality/cache.rs +122 -19
- package/crates/naome-core/src/quality/scanner/analysis.rs +4 -2
- package/crates/naome-core/src/quality/scanner/repo_paths.rs +27 -3
- package/crates/naome-core/src/quality/scanner.rs +5 -2
- package/crates/naome-core/src/workflow/integrity_support.rs +10 -3
- package/crates/naome-core/tests/architecture.rs +209 -0
- package/crates/naome-core/tests/harness_health.rs +150 -0
- package/crates/naome-core/tests/quality_performance.rs +63 -2
- package/installer/filesystem.js +38 -0
- package/installer/flows.js +6 -1
- package/installer/harness-file-ops.js +36 -8
- package/installer/harness-files.js +3 -0
- package/installer/manifest-state.js +2 -2
- package/installer/native.js +63 -18
- package/native/darwin-arm64/naome +0 -0
- package/native/linux-x64/naome +0 -0
- package/package.json +1 -1
- package/templates/naome-root/.naome/bin/check-harness-health.js +23 -19
- package/templates/naome-root/.naome/bin/check-task-state.js +33 -40
- package/templates/naome-root/.naome/bin/naome.js +2 -2
- package/templates/naome-root/.naome/manifest.json +8 -6
- package/templates/naome-root/.naome/verification.json +15 -1
- package/templates/naome-root/docs/naome/architecture-fitness.md +97 -0
- package/templates/naome-root/docs/naome/index.md +4 -3
- package/templates/naome-root/docs/naome/testing.md +6 -3
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
mod integrity;
|
|
2
|
+
mod manifest;
|
|
2
3
|
|
|
3
|
-
use std::collections::
|
|
4
|
+
use std::collections::HashMap;
|
|
4
5
|
use std::fs;
|
|
5
6
|
use std::path::{Component, Path};
|
|
6
7
|
|
|
@@ -11,6 +12,7 @@ use self::integrity::{
|
|
|
11
12
|
is_integrity_hash, native_integrity_from_naome_command, sha256_bytes,
|
|
12
13
|
NAOME_COMMAND_RELATIVE_PATH, NATIVE_BINARY_RELATIVE_PATH,
|
|
13
14
|
};
|
|
15
|
+
use self::manifest::{string_array, validate_manifest_ownership, validate_manifest_shape};
|
|
14
16
|
use crate::install_plan::{MACHINE_OWNED_PATHS, PROJECT_OWNED_PATHS};
|
|
15
17
|
use crate::models::NaomeError;
|
|
16
18
|
|
|
@@ -56,62 +58,6 @@ pub fn validate_harness_health(
|
|
|
56
58
|
Ok(errors)
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
fn validate_manifest_shape(manifest: &Value, errors: &mut Vec<String>) {
|
|
60
|
-
let Some(object) = manifest.as_object() else {
|
|
61
|
-
errors.push(".naome/manifest.json must be a JSON object.".to_string());
|
|
62
|
-
return;
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
if object.get("name").and_then(Value::as_str) != Some("naome") {
|
|
66
|
-
errors.push(".naome/manifest.json name must be naome.".to_string());
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if !object
|
|
70
|
-
.get("harnessVersion")
|
|
71
|
-
.and_then(Value::as_str)
|
|
72
|
-
.is_some_and(is_version)
|
|
73
|
-
{
|
|
74
|
-
errors.push(".naome/manifest.json harnessVersion must be semver.".to_string());
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if !string_array(object.get("machineOwned")).is_some() {
|
|
78
|
-
errors.push(".naome/manifest.json machineOwned must be a string array.".to_string());
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if !string_array(object.get("projectOwned")).is_some() {
|
|
82
|
-
errors.push(".naome/manifest.json projectOwned must be a string array.".to_string());
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if !object.get("integrity").is_some_and(Value::is_object) {
|
|
86
|
-
errors.push(".naome/manifest.json integrity must be an object.".to_string());
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
fn validate_manifest_ownership(manifest: &Value, errors: &mut Vec<String>) {
|
|
91
|
-
let Some(object) = manifest.as_object() else {
|
|
92
|
-
return;
|
|
93
|
-
};
|
|
94
|
-
let Some(machine_owned) = string_array(object.get("machineOwned")) else {
|
|
95
|
-
return;
|
|
96
|
-
};
|
|
97
|
-
let Some(project_owned) = string_array(object.get("projectOwned")) else {
|
|
98
|
-
return;
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
validate_contains_all(
|
|
102
|
-
&machine_owned,
|
|
103
|
-
MACHINE_OWNED_PATHS,
|
|
104
|
-
".naome/manifest.json machineOwned",
|
|
105
|
-
errors,
|
|
106
|
-
);
|
|
107
|
-
validate_contains_all(
|
|
108
|
-
&project_owned,
|
|
109
|
-
PROJECT_OWNED_PATHS,
|
|
110
|
-
".naome/manifest.json projectOwned",
|
|
111
|
-
errors,
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
61
|
fn validate_manifest_integrity(
|
|
116
62
|
root: &Path,
|
|
117
63
|
manifest: &Value,
|
|
@@ -186,13 +132,41 @@ fn validate_native_decision_binary(
|
|
|
186
132
|
return Ok(());
|
|
187
133
|
};
|
|
188
134
|
|
|
189
|
-
|
|
135
|
+
let wrapper_paths = [
|
|
136
|
+
NAOME_COMMAND_RELATIVE_PATH,
|
|
137
|
+
".naome/bin/check-harness-health.js",
|
|
138
|
+
".naome/bin/check-task-state.js",
|
|
139
|
+
];
|
|
140
|
+
let wrapper_integrity = wrapper_paths
|
|
141
|
+
.iter()
|
|
142
|
+
.map(|relative_path| {
|
|
143
|
+
native_integrity_from_regular_file(root, relative_path)
|
|
144
|
+
.map(|expected| (*relative_path, expected))
|
|
145
|
+
})
|
|
146
|
+
.collect::<Result<Vec<_>, _>>()?;
|
|
147
|
+
let native_is_declared = machine_owned
|
|
148
|
+
.iter()
|
|
149
|
+
.any(|entry| entry == NATIVE_BINARY_RELATIVE_PATH);
|
|
150
|
+
let native_has_integrity = integrity.contains_key(NATIVE_BINARY_RELATIVE_PATH);
|
|
151
|
+
let native_path_present = fs::symlink_metadata(root.join(NATIVE_BINARY_RELATIVE_PATH)).is_ok();
|
|
152
|
+
let wrapper_requires_native = wrapper_integrity
|
|
190
153
|
.iter()
|
|
191
|
-
.any(|
|
|
154
|
+
.any(|(_, expected)| expected.is_some());
|
|
155
|
+
|
|
156
|
+
if !native_is_declared
|
|
157
|
+
&& !native_has_integrity
|
|
158
|
+
&& !wrapper_requires_native
|
|
159
|
+
&& !native_path_present
|
|
192
160
|
{
|
|
193
161
|
return Ok(());
|
|
194
162
|
}
|
|
195
163
|
|
|
164
|
+
if !native_is_declared {
|
|
165
|
+
errors.push(format!(
|
|
166
|
+
".naome/manifest.json machineOwned must include {NATIVE_BINARY_RELATIVE_PATH}."
|
|
167
|
+
));
|
|
168
|
+
}
|
|
169
|
+
|
|
196
170
|
validate_regular_file(root, NATIVE_BINARY_RELATIVE_PATH, errors)?;
|
|
197
171
|
|
|
198
172
|
if !root.join(NATIVE_BINARY_RELATIVE_PATH).exists()
|
|
@@ -222,22 +196,36 @@ fn validate_native_decision_binary(
|
|
|
222
196
|
));
|
|
223
197
|
}
|
|
224
198
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
let command_expected = native_integrity_from_naome_command(&command_content);
|
|
232
|
-
if command_expected.as_deref() != Some(manifest_expected) {
|
|
233
|
-
errors.push(format!(
|
|
234
|
-
"{NAOME_COMMAND_RELATIVE_PATH} native binary integrity does not match .naome/manifest.json."
|
|
235
|
-
));
|
|
199
|
+
for (relative_path, expected) in wrapper_integrity {
|
|
200
|
+
if expected.as_deref() != Some(manifest_expected) {
|
|
201
|
+
errors.push(format!(
|
|
202
|
+
"{relative_path} native binary integrity does not match .naome/manifest.json."
|
|
203
|
+
));
|
|
204
|
+
}
|
|
236
205
|
}
|
|
237
206
|
|
|
238
207
|
Ok(())
|
|
239
208
|
}
|
|
240
209
|
|
|
210
|
+
fn native_integrity_from_regular_file(
|
|
211
|
+
root: &Path,
|
|
212
|
+
relative_path: &str,
|
|
213
|
+
) -> Result<Option<String>, NaomeError> {
|
|
214
|
+
let file_path = root.join(relative_path);
|
|
215
|
+
if has_symlink_in_path(root, relative_path)? {
|
|
216
|
+
return Ok(None);
|
|
217
|
+
}
|
|
218
|
+
let Ok(metadata) = fs::symlink_metadata(&file_path) else {
|
|
219
|
+
return Ok(None);
|
|
220
|
+
};
|
|
221
|
+
if !metadata.is_file() {
|
|
222
|
+
return Ok(None);
|
|
223
|
+
}
|
|
224
|
+
Ok(native_integrity_from_naome_command(&fs::read_to_string(
|
|
225
|
+
file_path,
|
|
226
|
+
)?))
|
|
227
|
+
}
|
|
228
|
+
|
|
241
229
|
fn validate_naome_ignore(root: &Path, errors: &mut Vec<String>) -> Result<(), NaomeError> {
|
|
242
230
|
let relative_path = ".naomeignore";
|
|
243
231
|
if !root.join(relative_path).exists() || has_symlink_in_path(root, relative_path)? {
|
|
@@ -342,20 +330,6 @@ fn read_json(
|
|
|
342
330
|
}
|
|
343
331
|
}
|
|
344
332
|
|
|
345
|
-
fn validate_contains_all(
|
|
346
|
-
actual_values: &[String],
|
|
347
|
-
expected_values: &[&str],
|
|
348
|
-
field_name: &str,
|
|
349
|
-
errors: &mut Vec<String>,
|
|
350
|
-
) {
|
|
351
|
-
let actual: HashSet<&str> = actual_values.iter().map(String::as_str).collect();
|
|
352
|
-
for expected in expected_values {
|
|
353
|
-
if !actual.contains(expected) {
|
|
354
|
-
errors.push(format!("{field_name} must include {expected}."));
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
333
|
fn validate_regular_file(
|
|
360
334
|
root: &Path,
|
|
361
335
|
relative_path: &str,
|
|
@@ -423,25 +397,3 @@ fn has_symlink_in_path(root: &Path, relative_path: &str) -> Result<bool, NaomeEr
|
|
|
423
397
|
|
|
424
398
|
Ok(false)
|
|
425
399
|
}
|
|
426
|
-
|
|
427
|
-
fn string_array(value: Option<&Value>) -> Option<Vec<String>> {
|
|
428
|
-
value.and_then(Value::as_array).and_then(|entries| {
|
|
429
|
-
entries
|
|
430
|
-
.iter()
|
|
431
|
-
.map(|entry| {
|
|
432
|
-
entry
|
|
433
|
-
.as_str()
|
|
434
|
-
.filter(|text| !text.trim().is_empty())
|
|
435
|
-
.map(ToString::to_string)
|
|
436
|
-
})
|
|
437
|
-
.collect()
|
|
438
|
-
})
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
fn is_version(value: &str) -> bool {
|
|
442
|
-
let parts: Vec<&str> = value.split('.').collect();
|
|
443
|
-
parts.len() == 3
|
|
444
|
-
&& parts
|
|
445
|
-
.iter()
|
|
446
|
-
.all(|part| !part.is_empty() && part.chars().all(|ch| ch.is_ascii_digit()))
|
|
447
|
-
}
|
|
@@ -12,6 +12,7 @@ pub const MACHINE_OWNED_PATHS: &[&str] = &[
|
|
|
12
12
|
"docs/naome/agent-workflow.md",
|
|
13
13
|
"docs/naome/context-economy.md",
|
|
14
14
|
"docs/naome/task-ledger.md",
|
|
15
|
+
"docs/naome/architecture-fitness.md",
|
|
15
16
|
"docs/naome/execution.md",
|
|
16
17
|
"docs/naome/upgrade.md",
|
|
17
18
|
];
|
|
@@ -48,6 +49,7 @@ pub const LOCAL_ONLY_MACHINE_OWNED_PATHS: &[&str] = &[
|
|
|
48
49
|
"docs/naome/agent-workflow.md",
|
|
49
50
|
"docs/naome/context-economy.md",
|
|
50
51
|
"docs/naome/task-ledger.md",
|
|
52
|
+
"docs/naome/architecture-fitness.md",
|
|
51
53
|
"docs/naome/execution.md",
|
|
52
54
|
"docs/naome/upgrade.md",
|
|
53
55
|
];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
mod architecture;
|
|
1
2
|
mod context;
|
|
2
3
|
mod decision;
|
|
3
4
|
mod git;
|
|
@@ -19,6 +20,14 @@ mod verification_contract;
|
|
|
19
20
|
mod verification_contract_policy;
|
|
20
21
|
mod workflow;
|
|
21
22
|
|
|
23
|
+
pub use architecture::{
|
|
24
|
+
default_architecture_config_text, format_architecture_explain, format_architecture_scan,
|
|
25
|
+
format_architecture_validation, scan_architecture, validate_architecture,
|
|
26
|
+
ArchitectureAgentFeedback, ArchitectureConfig, ArchitectureEdge, ArchitectureEdgeKind,
|
|
27
|
+
ArchitectureGraph, ArchitectureMetadata, ArchitectureNode, ArchitectureNodeKind,
|
|
28
|
+
ArchitectureScanOptions, ArchitectureScanReport, ArchitectureValidation, ArchitectureViolation,
|
|
29
|
+
ContextConfig, LayerConfig, RuleConfig, Severity, SourceRange, ViolationSummary,
|
|
30
|
+
};
|
|
22
31
|
pub use context::{
|
|
23
32
|
select_context_for_changed_paths, select_context_for_prompt, ContextBudgetLedger,
|
|
24
33
|
ContextCapsule, ContextItem, ContextSelection,
|
|
@@ -36,14 +45,13 @@ pub use journal::{append_task_journal, TaskJournalEntry};
|
|
|
36
45
|
pub use models::{Decision, NaomeError};
|
|
37
46
|
pub use quality::{
|
|
38
47
|
check_repository_quality, check_repository_quality_paths, check_semantic_legacy,
|
|
39
|
-
check_semantic_legacy_paths,
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
SemanticFinding, SemanticReport, StructurePathExplanation,
|
|
48
|
+
check_semantic_legacy_paths, clear_quality_cache, explain_repository_structure,
|
|
49
|
+
init_repository_quality, init_repository_quality_with_mode, plan_quality_cleanup,
|
|
50
|
+
quality_cache_status, reconcile_repository_quality, route_quality_cleanup,
|
|
51
|
+
semantic_route_for_finding, QualityCacheStatus, QualityCleanupPlan, QualityCleanupRoute,
|
|
52
|
+
QualityCleanupTask, QualityInitMode, QualityInitResult, QualityMode, QualityReconcileReport,
|
|
53
|
+
QualityReport, QualitySummary, QualityViolation, RepositoryQualityConfig,
|
|
54
|
+
RepositoryStructureConfig, SemanticFinding, SemanticReport, StructurePathExplanation,
|
|
47
55
|
};
|
|
48
56
|
pub use repository_model::{
|
|
49
57
|
explain_repository_model_path, refresh_repository_model, repository_model_drift,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
use std::fs;
|
|
2
|
-
use std::
|
|
1
|
+
use std::fs::{self, OpenOptions};
|
|
2
|
+
use std::io::Write;
|
|
3
|
+
use std::path::{Component, Path, PathBuf};
|
|
3
4
|
|
|
4
5
|
use serde::{Deserialize, Serialize};
|
|
5
6
|
use sha2::{Digest, Sha256};
|
|
@@ -49,7 +50,11 @@ impl QualityCache {
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
pub(crate) fn read(&self, path: &str, content_hash: &str) -> Option<FileAnalysis> {
|
|
52
|
-
let
|
|
53
|
+
let cache_path = self.safe_entry_path(path, content_hash).ok()?;
|
|
54
|
+
if !regular_file_without_symlink(&cache_path) {
|
|
55
|
+
return None;
|
|
56
|
+
}
|
|
57
|
+
let entry = fs::read_to_string(cache_path).ok()?;
|
|
53
58
|
let entry: CacheEntry = serde_json::from_str(&entry).ok()?;
|
|
54
59
|
if entry.schema == CACHE_SCHEMA
|
|
55
60
|
&& entry.naome_version == env!("CARGO_PKG_VERSION")
|
|
@@ -70,10 +75,7 @@ impl QualityCache {
|
|
|
70
75
|
content_hash: &str,
|
|
71
76
|
analysis: &FileAnalysis,
|
|
72
77
|
) -> Result<(), NaomeError> {
|
|
73
|
-
let cache_path = self.
|
|
74
|
-
if let Some(parent) = cache_path.parent() {
|
|
75
|
-
fs::create_dir_all(parent)?;
|
|
76
|
-
}
|
|
78
|
+
let cache_path = self.safe_entry_path(path, content_hash)?;
|
|
77
79
|
let entry = CacheEntry {
|
|
78
80
|
schema: CACHE_SCHEMA.to_string(),
|
|
79
81
|
naome_version: env!("CARGO_PKG_VERSION").to_string(),
|
|
@@ -84,31 +86,29 @@ impl QualityCache {
|
|
|
84
86
|
analysis: analysis.clone(),
|
|
85
87
|
};
|
|
86
88
|
let content = format!("{}\n", serde_json::to_string(&entry)?);
|
|
87
|
-
|
|
88
|
-
fs::write(cache_path, content)?;
|
|
89
|
-
}
|
|
90
|
-
Ok(())
|
|
89
|
+
write_cache_entry(&cache_path, &content)
|
|
91
90
|
}
|
|
92
91
|
|
|
93
|
-
fn
|
|
94
|
-
self.root
|
|
92
|
+
fn safe_entry_path(&self, path: &str, content_hash: &str) -> Result<PathBuf, NaomeError> {
|
|
93
|
+
let cache_root = safe_cache_root(&self.root, true)?;
|
|
94
|
+
Ok(cache_root.join(stable_key(&[
|
|
95
95
|
env!("CARGO_PKG_VERSION"),
|
|
96
96
|
&self.config_hash,
|
|
97
97
|
ADAPTER_VERSION,
|
|
98
98
|
path,
|
|
99
99
|
content_hash,
|
|
100
|
-
]))
|
|
100
|
+
])))
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
pub fn quality_cache_status(root: &Path) -> Result<QualityCacheStatus, NaomeError> {
|
|
105
|
-
let path = root
|
|
105
|
+
let path = safe_cache_root(root, false)?;
|
|
106
106
|
let mut entry_count = 0;
|
|
107
107
|
let mut bytes = 0;
|
|
108
|
-
if path
|
|
108
|
+
if regular_directory_without_symlink(&path) {
|
|
109
109
|
for entry in fs::read_dir(&path)? {
|
|
110
110
|
let entry = entry?;
|
|
111
|
-
let metadata = entry.
|
|
111
|
+
let metadata = fs::symlink_metadata(entry.path())?;
|
|
112
112
|
if metadata.is_file() {
|
|
113
113
|
entry_count += 1;
|
|
114
114
|
bytes += metadata.len();
|
|
@@ -124,13 +124,116 @@ pub fn quality_cache_status(root: &Path) -> Result<QualityCacheStatus, NaomeErro
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
pub fn clear_quality_cache(root: &Path) -> Result<QualityCacheStatus, NaomeError> {
|
|
127
|
-
let path = root
|
|
128
|
-
if path
|
|
127
|
+
let path = safe_cache_root(root, false)?;
|
|
128
|
+
if regular_directory_without_symlink(&path) {
|
|
129
129
|
fs::remove_dir_all(&path)?;
|
|
130
130
|
}
|
|
131
131
|
quality_cache_status(root)
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
fn write_cache_entry(cache_path: &Path, content: &str) -> Result<(), NaomeError> {
|
|
135
|
+
validate_cache_entry_parent(cache_path)?;
|
|
136
|
+
match fs::symlink_metadata(cache_path) {
|
|
137
|
+
Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_file() => {
|
|
138
|
+
return Err(NaomeError::new(format!(
|
|
139
|
+
"quality cache entry must be a regular file: {}",
|
|
140
|
+
cache_path.display()
|
|
141
|
+
)));
|
|
142
|
+
}
|
|
143
|
+
Ok(_) => {
|
|
144
|
+
if fs::read_to_string(cache_path).map_or(false, |current| current == content) {
|
|
145
|
+
return Ok(());
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
|
|
149
|
+
Err(error) => return Err(error.into()),
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let temp_path = cache_path.with_extension(format!(
|
|
153
|
+
"tmp.{}.{}",
|
|
154
|
+
std::process::id(),
|
|
155
|
+
stable_key(&[content]).trim_end_matches(".json")
|
|
156
|
+
));
|
|
157
|
+
validate_cache_entry_parent(&temp_path)?;
|
|
158
|
+
let mut temp = OpenOptions::new()
|
|
159
|
+
.write(true)
|
|
160
|
+
.create_new(true)
|
|
161
|
+
.open(&temp_path)?;
|
|
162
|
+
if let Err(error) = temp.write_all(content.as_bytes()) {
|
|
163
|
+
let _ = fs::remove_file(&temp_path);
|
|
164
|
+
return Err(error.into());
|
|
165
|
+
}
|
|
166
|
+
if let Err(error) = temp.sync_all() {
|
|
167
|
+
let _ = fs::remove_file(&temp_path);
|
|
168
|
+
return Err(error.into());
|
|
169
|
+
}
|
|
170
|
+
drop(temp);
|
|
171
|
+
validate_cache_entry_parent(cache_path)?;
|
|
172
|
+
if let Err(error) = fs::rename(&temp_path, cache_path) {
|
|
173
|
+
let _ = fs::remove_file(&temp_path);
|
|
174
|
+
return Err(error.into());
|
|
175
|
+
}
|
|
176
|
+
Ok(())
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
fn validate_cache_entry_parent(cache_path: &Path) -> Result<(), NaomeError> {
|
|
180
|
+
let Some(parent) = cache_path.parent() else {
|
|
181
|
+
return Err(NaomeError::new("quality cache entry path has no parent"));
|
|
182
|
+
};
|
|
183
|
+
if !regular_directory_without_symlink(parent) {
|
|
184
|
+
return Err(NaomeError::new(format!(
|
|
185
|
+
"quality cache path must be a regular directory without symlinks: {}",
|
|
186
|
+
parent.display()
|
|
187
|
+
)));
|
|
188
|
+
}
|
|
189
|
+
Ok(())
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
fn safe_cache_root(root: &Path, create: bool) -> Result<PathBuf, NaomeError> {
|
|
193
|
+
let mut current = root.to_path_buf();
|
|
194
|
+
for component in Path::new(CACHE_RELATIVE_PATH).components() {
|
|
195
|
+
let Component::Normal(component) = component else {
|
|
196
|
+
return Err(NaomeError::new(
|
|
197
|
+
"quality cache path must be repository-relative",
|
|
198
|
+
));
|
|
199
|
+
};
|
|
200
|
+
current.push(component);
|
|
201
|
+
match fs::symlink_metadata(¤t) {
|
|
202
|
+
Ok(metadata) if metadata.file_type().is_symlink() => {
|
|
203
|
+
return Err(NaomeError::new(format!(
|
|
204
|
+
"quality cache path must not contain symlinks: {}",
|
|
205
|
+
current.display()
|
|
206
|
+
)));
|
|
207
|
+
}
|
|
208
|
+
Ok(metadata) if metadata.is_dir() => {}
|
|
209
|
+
Ok(_) => {
|
|
210
|
+
return Err(NaomeError::new(format!(
|
|
211
|
+
"quality cache path component is not a directory: {}",
|
|
212
|
+
current.display()
|
|
213
|
+
)));
|
|
214
|
+
}
|
|
215
|
+
Err(error) if error.kind() == std::io::ErrorKind::NotFound && create => {
|
|
216
|
+
fs::create_dir(¤t)?;
|
|
217
|
+
}
|
|
218
|
+
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(current),
|
|
219
|
+
Err(error) => return Err(error.into()),
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
Ok(current)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
fn regular_directory_without_symlink(path: &Path) -> bool {
|
|
226
|
+
fs::symlink_metadata(path)
|
|
227
|
+
.map(|metadata| metadata.is_dir() && !metadata.file_type().is_symlink())
|
|
228
|
+
.unwrap_or(false)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
fn regular_file_without_symlink(path: &Path) -> bool {
|
|
232
|
+
fs::symlink_metadata(path)
|
|
233
|
+
.map(|metadata| metadata.is_file() && !metadata.file_type().is_symlink())
|
|
234
|
+
.unwrap_or(false)
|
|
235
|
+
}
|
|
236
|
+
|
|
134
237
|
pub(crate) fn content_hash(content: &str) -> String {
|
|
135
238
|
stable_key(&[content])
|
|
136
239
|
}
|
|
@@ -8,6 +8,8 @@ use super::{FileAnalysis, NormalizedLine, SymbolAnalysis};
|
|
|
8
8
|
use crate::quality::cache::{content_hash, QualityCache};
|
|
9
9
|
use normalize::{normalize_line, token_set};
|
|
10
10
|
|
|
11
|
+
use super::repo_paths::regular_repo_file_path;
|
|
12
|
+
|
|
11
13
|
pub(super) fn analyze_repo_file(
|
|
12
14
|
root: &Path,
|
|
13
15
|
path: &str,
|
|
@@ -16,8 +18,8 @@ pub(super) fn analyze_repo_file(
|
|
|
16
18
|
cache: &QualityCache,
|
|
17
19
|
allow_cache: bool,
|
|
18
20
|
) -> Option<(FileAnalysis, bool)> {
|
|
19
|
-
let full_path = root
|
|
20
|
-
if
|
|
21
|
+
let full_path = regular_repo_file_path(root, path)?;
|
|
22
|
+
if is_binary_extension(path) {
|
|
21
23
|
return None;
|
|
22
24
|
}
|
|
23
25
|
if allow_cache {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
use std::collections::{HashMap, HashSet};
|
|
2
2
|
use std::fs;
|
|
3
|
-
use std::path::Path;
|
|
3
|
+
use std::path::{Component, Path};
|
|
4
4
|
use std::process::Command;
|
|
5
5
|
|
|
6
6
|
use crate::models::NaomeError;
|
|
@@ -44,8 +44,10 @@ pub(super) fn added_lines_by_path(
|
|
|
44
44
|
if !target_paths.contains(&path) {
|
|
45
45
|
continue;
|
|
46
46
|
}
|
|
47
|
-
if let
|
|
48
|
-
|
|
47
|
+
if let Some(file_path) = regular_repo_file_path(root, &path) {
|
|
48
|
+
if let Ok(content) = fs::read_to_string(file_path) {
|
|
49
|
+
added.insert(path, content.lines().count());
|
|
50
|
+
}
|
|
49
51
|
}
|
|
50
52
|
}
|
|
51
53
|
Ok(added)
|
|
@@ -92,3 +94,25 @@ fn untracked_paths(root: &Path) -> Result<Vec<String>, NaomeError> {
|
|
|
92
94
|
.map(|entry| String::from_utf8_lossy(entry).replace('\\', "/"))
|
|
93
95
|
.collect())
|
|
94
96
|
}
|
|
97
|
+
|
|
98
|
+
pub(super) fn regular_repo_file_path(root: &Path, path: &str) -> Option<std::path::PathBuf> {
|
|
99
|
+
let relative = Path::new(path);
|
|
100
|
+
if relative.is_absolute() {
|
|
101
|
+
return None;
|
|
102
|
+
}
|
|
103
|
+
let mut current = root.to_path_buf();
|
|
104
|
+
for component in relative.components() {
|
|
105
|
+
let Component::Normal(component) = component else {
|
|
106
|
+
return None;
|
|
107
|
+
};
|
|
108
|
+
current.push(component);
|
|
109
|
+
let metadata = fs::symlink_metadata(¤t).ok()?;
|
|
110
|
+
if metadata.file_type().is_symlink() {
|
|
111
|
+
return None;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
fs::symlink_metadata(¤t)
|
|
115
|
+
.ok()
|
|
116
|
+
.filter(|metadata| metadata.is_file())
|
|
117
|
+
.map(|_| current)
|
|
118
|
+
}
|
|
@@ -9,7 +9,7 @@ use sha2::{Digest, Sha256};
|
|
|
9
9
|
|
|
10
10
|
use crate::{git, models::NaomeError, paths};
|
|
11
11
|
pub(crate) use repo_paths::collect_repo_paths;
|
|
12
|
-
use repo_paths::{added_lines_by_path, tracked_blob_hashes};
|
|
12
|
+
use repo_paths::{added_lines_by_path, regular_repo_file_path, tracked_blob_hashes};
|
|
13
13
|
|
|
14
14
|
use super::cache::QualityCache;
|
|
15
15
|
use super::types::{
|
|
@@ -317,7 +317,10 @@ fn max_file_bytes(mode: QualityMode) -> u64 {
|
|
|
317
317
|
}
|
|
318
318
|
|
|
319
319
|
fn file_exceeds_budget(root: &Path, path: &str, mode: QualityMode) -> bool {
|
|
320
|
-
|
|
320
|
+
let Some(full_path) = regular_repo_file_path(root, path) else {
|
|
321
|
+
return false;
|
|
322
|
+
};
|
|
323
|
+
fs::symlink_metadata(full_path).map_or(false, |metadata| {
|
|
321
324
|
metadata.is_file() && metadata.len() > max_file_bytes(mode)
|
|
322
325
|
})
|
|
323
326
|
}
|
|
@@ -23,8 +23,14 @@ pub(super) fn refresh_support_files(
|
|
|
23
23
|
changed.push(path.to_string());
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
for path in [
|
|
27
|
+
NAOME_COMMAND_PATH,
|
|
28
|
+
HEALTH_CHECKER_PATH,
|
|
29
|
+
TASK_STATE_CHECKER_PATH,
|
|
30
|
+
] {
|
|
31
|
+
if replace_native_integrity(root, path, integrity)? {
|
|
32
|
+
changed.push(path.to_string());
|
|
33
|
+
}
|
|
28
34
|
}
|
|
29
35
|
Ok(changed)
|
|
30
36
|
}
|
|
@@ -76,12 +82,13 @@ fn render_expected_integrity_block(integrity: &BTreeMap<String, String>) -> Stri
|
|
|
76
82
|
|
|
77
83
|
fn replace_native_integrity(
|
|
78
84
|
root: &Path,
|
|
85
|
+
relative_path: &str,
|
|
79
86
|
integrity: &BTreeMap<String, String>,
|
|
80
87
|
) -> Result<bool, NaomeError> {
|
|
81
88
|
let Some(native_hash) = integrity.get(NATIVE_BINARY_PATH) else {
|
|
82
89
|
return Ok(false);
|
|
83
90
|
};
|
|
84
|
-
let path = root.join(
|
|
91
|
+
let path = root.join(relative_path);
|
|
85
92
|
if !path.is_file() {
|
|
86
93
|
return Ok(false);
|
|
87
94
|
}
|