Skip to content

Commit ce9617d

Browse files
committed
fix(tools): harden rss file mutation parity
Address Task 0D quality findings: leaf symlink exact-oracle coverage, linear bounded replacement with mid-replace cancellation, and a real post-publish publication_indeterminate seam.
1 parent 1186c2e commit ce9617d

3 files changed

Lines changed: 421 additions & 78 deletions

File tree

‎rss/tools/patch.rss‎

Lines changed: 183 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -574,7 +574,7 @@ fn count_matches(source: string, needle: string) -> map {
574574
let mut index: int = 0;
575575
let mut scanned: int = 0;
576576
let mut result: map = { ok: true, count: 0, code: "", message: "" };
577-
while map_bool(result, "ok", false) && index + needle.length <= source.length {
577+
while map_bool(result, "ok", false) && can_scan(index, needle.length, source.length) {
578578
if source[index:(index + needle.length)] == needle {
579579
count = count + 1;
580580
index = index + needle.length;
@@ -601,27 +601,162 @@ fn count_matches(source: string, needle: string) -> map {
601601
result
602602
}
603603

604-
fn replace_text(source: string, old: string, new: string, limit: int) -> string {
605-
let mut out: string = "";
606-
let mut start: int = 0;
607-
let mut index: int = 0;
608-
let mut replaced: int = 0;
609-
while index + old.length <= source.length {
610-
if replaced == limit {
611-
index = source.length;
612-
} else {
613-
if source[index:(index + old.length)] == old {
614-
out = out + source[start:index] + new;
615-
index = index + old.length;
616-
start = index;
617-
replaced = replaced + 1;
604+
fn can_scan(index: int, needle_len: int, source_len: int) -> bool {
605+
let mut result: bool = false;
606+
if index >= 0 {
607+
if needle_len >= 0 {
608+
if source_len >= 0 {
609+
if needle_len <= source_len {
610+
if index <= source_len - needle_len {
611+
result = true;
612+
}
613+
}
614+
}
615+
}
616+
}
617+
result
618+
}
619+
620+
fn chunk_bound(limit: int) -> map {
621+
let mut result: map = { ok: false, value: 0 };
622+
if limit >= 0 {
623+
if limit <= 2147483646 {
624+
result = { ok: true, value: limit * 2 + 1 };
625+
}
626+
}
627+
result
628+
}
629+
630+
fn join_string_chunks(chunks: array) -> string {
631+
let mut current: array = chunks.copy();
632+
while current.length > 1 {
633+
let mut next: array = [];
634+
let mut i: int = 0;
635+
let current_len: int = current.length;
636+
while i < current_len {
637+
if i + 1 < current_len {
638+
let left: string = current[i].copy();
639+
let right: string = current[i + 1].copy();
640+
let joined: string = left + right;
641+
next[next.length] = joined;
642+
i = i + 2;
618643
} else {
619-
index = index + 1;
644+
let leftover: string = current[i].copy();
645+
next[next.length] = leftover;
646+
i = i + 1;
647+
}
648+
}
649+
current = next.copy();
650+
}
651+
let mut text: string = "";
652+
if current.length == 1 {
653+
text = current[0].copy();
654+
}
655+
text
656+
}
657+
658+
fn replace_text(source: string, old: string, new: string, limit: int) -> map {
659+
let mut result: map = { ok: true, text: "", code: "", message: "" };
660+
let bound: map = chunk_bound(limit);
661+
if map_bool(bound, "ok", false) == false {
662+
result = {
663+
ok: false,
664+
text: "",
665+
code: "internal_error",
666+
message: "replacement chunk bound overflow"
667+
};
668+
} else {
669+
let max_chunks: int = map_int(bound, "value", 0);
670+
let mut chunks: array = [];
671+
let mut start: int = 0;
672+
let mut index: int = 0;
673+
let mut replaced: int = 0;
674+
let mut scanned: int = 0;
675+
while map_bool(result, "ok", false) && can_scan(index, old.length, source.length) {
676+
if replaced == limit {
677+
index = source.length;
678+
} else {
679+
if source[index:(index + old.length)] == old {
680+
let prefix: string = source[start:index];
681+
if prefix.length > 0 {
682+
if chunks.length >= max_chunks {
683+
result = {
684+
ok: false,
685+
text: "",
686+
code: "internal_error",
687+
message: "replacement chunk bound exceeded"
688+
};
689+
} else {
690+
chunks[chunks.length] = prefix;
691+
}
692+
}
693+
if map_bool(result, "ok", false) {
694+
if new.length > 0 {
695+
if chunks.length >= max_chunks {
696+
result = {
697+
ok: false,
698+
text: "",
699+
code: "internal_error",
700+
message: "replacement chunk bound exceeded"
701+
};
702+
} else {
703+
chunks[chunks.length] = new.copy();
704+
}
705+
}
706+
}
707+
if map_bool(result, "ok", false) {
708+
index = index + old.length;
709+
start = index;
710+
replaced = replaced + 1;
711+
}
712+
} else {
713+
index = index + 1;
714+
}
715+
}
716+
scanned = scanned + 1;
717+
if scanned == 64 {
718+
scanned = 0;
719+
let checked: map = control_failure();
720+
if map_bool(checked, "ok", false) == false {
721+
result = {
722+
ok: false,
723+
text: "",
724+
code: types::map_string(checked, "code", "cancelled"),
725+
message: types::map_string(checked, "message", "")
726+
};
727+
}
728+
}
729+
}
730+
if map_bool(result, "ok", false) {
731+
let tail: string = source[start:source.length];
732+
if tail.length > 0 {
733+
if chunks.length >= max_chunks {
734+
result = {
735+
ok: false,
736+
text: "",
737+
code: "internal_error",
738+
message: "replacement chunk bound exceeded"
739+
};
740+
} else {
741+
chunks[chunks.length] = tail;
742+
}
743+
}
744+
}
745+
if map_bool(result, "ok", false) {
746+
let finished: map = control_failure();
747+
if map_bool(finished, "ok", false) == false {
748+
result = {
749+
ok: false,
750+
text: "",
751+
code: types::map_string(finished, "code", "cancelled"),
752+
message: types::map_string(finished, "message", "")
753+
};
754+
} else {
755+
result.text = join_string_chunks(chunks);
620756
}
621757
}
622758
}
623-
out = out + source[start:source.length];
624-
out
759+
result
625760
}
626761

627762
pub fn descriptor() -> map {
@@ -703,7 +838,7 @@ pub fn execute(context: map, arguments: map) -> map {
703838
result = fail_host(meta);
704839
} else {
705840
let file_type: string = types::map_string(meta, "file_type", "");
706-
if file_type != "file" {
841+
if file_type == "directory" || file_type == "other" {
707842
result = fail("path_denied", "read-only open requires a regular file", unpublished());
708843
} else {
709844
let file_len: int = map_int(meta, "len", 0);
@@ -771,33 +906,38 @@ pub fn execute(context: map, arguments: map) -> map {
771906
if replace_all {
772907
limit = matches;
773908
}
774-
let updated: string = replace_text(source, old_string, new_string, limit);
775-
if utf8_len(updated) > max_patch_bytes {
776-
result = fail("patch_too_large", "result exceeds the configured patch budget", unpublished());
909+
let replaced_text: map = replace_text(source, old_string, new_string, limit);
910+
if map_bool(replaced_text, "ok", false) == false {
911+
result = fail(types::map_string(replaced_text, "code", "cancelled"), types::map_string(replaced_text, "message", ""), unpublished());
777912
} else {
778-
let before_publish: map = control_failure();
779-
if map_bool(before_publish, "ok", false) == false {
780-
result = fail(types::map_string(before_publish, "code", "cancelled"), types::map_string(before_publish, "message", ""), unpublished());
913+
let updated: string = types::map_string(replaced_text, "text", "");
914+
if utf8_len(updated) > max_patch_bytes {
915+
result = fail("patch_too_large", "result exceeds the configured patch budget", unpublished());
781916
} else {
782-
let payload: bytes = bytes::from_utf8(updated);
783-
let written: map = cap::fs_write_atomic(token, path, expected_hash, payload);
784-
if map_bool(written, "ok", false) == false {
785-
result = fail_host(written);
917+
let before_publish: map = control_failure();
918+
if map_bool(before_publish, "ok", false) == false {
919+
result = fail(types::map_string(before_publish, "code", "cancelled"), types::map_string(before_publish, "message", ""), unpublished());
786920
} else {
787-
let preview: string = bounded_diff(path, source, updated, max_patch_preview_bytes);
788-
let mut published: map = succeed(
789-
preview,
790-
{
791-
publication: "published",
792-
durable: map_bool(written, "durable", true),
793-
staging_cleaned: map_bool(written, "staging_cleaned", true),
794-
bytes: map_int(written, "len", utf8_len(updated)),
795-
replacements: replacements
796-
},
797-
false,
798-
[]
799-
);
800-
result = shrink_result(published, max_output_bytes, token);
921+
let payload: bytes = bytes::from_utf8(updated);
922+
let written: map = cap::fs_write_atomic(token, path, expected_hash, payload);
923+
if map_bool(written, "ok", false) == false {
924+
result = fail_host(written);
925+
} else {
926+
let preview: string = bounded_diff(path, source, updated, max_patch_preview_bytes);
927+
let mut published: map = succeed(
928+
preview,
929+
{
930+
publication: "published",
931+
durable: map_bool(written, "durable", true),
932+
staging_cleaned: map_bool(written, "staging_cleaned", true),
933+
bytes: map_int(written, "len", utf8_len(updated)),
934+
replacements: replacements
935+
},
936+
false,
937+
[]
938+
);
939+
result = shrink_result(published, max_output_bytes, token);
940+
}
801941
}
802942
}
803943
}

‎src/capabilities/filesystem.rs‎

Lines changed: 55 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ pub struct FsWrite {
8282
}
8383

8484
type BeforeWriteHook = Arc<dyn Fn(&str, &[u8]) -> Result<(), CapabilityError> + Send + Sync>;
85+
type AfterPublishHook = Arc<dyn Fn(&str, &[u8]) -> bool + Send + Sync>;
8586

8687
/// Confined filesystem capability bound to one lifecycle owner.
8788
#[derive(Clone)]
@@ -93,6 +94,7 @@ pub struct FilesystemCapability {
9394
frozen: Arc<FrozenDir>,
9495
cas_locks: Arc<Mutex<HashMap<String, Arc<Mutex<()>>>>>,
9596
before_write: Arc<Mutex<Option<BeforeWriteHook>>>,
97+
after_publish: Arc<Mutex<Option<AfterPublishHook>>>,
9698
}
9799

98100
impl FilesystemCapability {
@@ -131,6 +133,7 @@ impl FilesystemCapability {
131133
frozen: Arc::new(frozen),
132134
cas_locks: Arc::new(Mutex::new(HashMap::new())),
133135
before_write: Arc::new(Mutex::new(None)),
136+
after_publish: Arc::new(Mutex::new(None)),
134137
})
135138
}
136139

@@ -145,6 +148,18 @@ impl FilesystemCapability {
145148
.unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(hook);
146149
}
147150

151+
/// Installs a production-neutral post-publish hook for tests.
152+
///
153+
/// The hook runs after confined rename/publish returns a published
154+
/// outcome. Returning true forces the same `publication_indeterminate`
155+
/// capability error produced by `ConfinedPublicationState::Indeterminate`.
156+
pub fn inject_after_publish(&self, hook: AfterPublishHook) {
157+
*self
158+
.after_publish
159+
.lock()
160+
.unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(hook);
161+
}
162+
148163
/// Stats a workspace-relative path without following a leaf symlink.
149164
pub fn metadata(&self, token: &str, path: &str) -> Result<FsMetadata, CapabilityError> {
150165
let _claims = self.authorize(token, CapabilityRisk::Read)?;
@@ -279,26 +294,35 @@ impl FilesystemCapability {
279294
self.validate_expected_hash(path, expected_hash)?;
280295
}
281296
match self.root.write_file(path, bytes) {
282-
Ok(publication) => Ok(FsWrite {
283-
hash: content_hash(bytes),
284-
len: bytes.len(),
285-
durable: publication.is_durable(),
286-
staging_cleaned: publication.staging_cleaned(),
287-
}),
288-
Err(error) => match error.publication_state() {
289-
ConfinedPublicationState::Published {
290-
durable,
291-
staging_cleaned,
292-
} => Ok(FsWrite {
297+
Ok(publication) => {
298+
if self.after_publish_forces_indeterminate(path, bytes) {
299+
return Err(publication_indeterminate_error());
300+
}
301+
Ok(FsWrite {
293302
hash: content_hash(bytes),
294303
len: bytes.len(),
304+
durable: publication.is_durable(),
305+
staging_cleaned: publication.staging_cleaned(),
306+
})
307+
}
308+
Err(error) => match error.publication_state() {
309+
ConfinedPublicationState::Published {
295310
durable,
296311
staging_cleaned,
297-
}),
298-
ConfinedPublicationState::Indeterminate { .. } => Err(CapabilityError::new(
299-
"publication_indeterminate",
300-
"write publication could not be classified",
301-
)),
312+
} => {
313+
if self.after_publish_forces_indeterminate(path, bytes) {
314+
return Err(publication_indeterminate_error());
315+
}
316+
Ok(FsWrite {
317+
hash: content_hash(bytes),
318+
len: bytes.len(),
319+
durable,
320+
staging_cleaned,
321+
})
322+
}
323+
ConfinedPublicationState::Indeterminate { .. } => {
324+
Err(publication_indeterminate_error())
325+
}
302326
ConfinedPublicationState::NotPublished => Err(map_fs_error(error)),
303327
},
304328
}
@@ -366,6 +390,21 @@ impl FilesystemCapability {
366390
.authorize(&self.owner, token, risk)
367391
.map_err(CapabilityError::from)
368392
}
393+
394+
fn after_publish_forces_indeterminate(&self, path: &str, bytes: &[u8]) -> bool {
395+
self.after_publish
396+
.lock()
397+
.unwrap_or_else(|poisoned| poisoned.into_inner())
398+
.as_ref()
399+
.is_some_and(|hook| hook(path, bytes))
400+
}
401+
}
402+
403+
fn publication_indeterminate_error() -> CapabilityError {
404+
CapabilityError::new(
405+
"publication_indeterminate",
406+
"write publication could not be classified",
407+
)
369408
}
370409

371410
fn bounded_identity(offset: u64, window_len: usize, file_len: u64) -> String {

0 commit comments

Comments
 (0)