@@ -766,215 +766,6 @@ impl FsSandbox {
766766 Ok ( out)
767767 }
768768
769- /// Register all FS tool handlers on `registry`:
770- ///
771- /// - `fs_read` / `fs_write` — UTF-8 text read/write (whole-file).
772- /// - `fs_read_bytes` / `fs_write_bytes` — binary read/write with
773- /// optional offset/length/append, base64-encoded payloads.
774- /// - `fs_list` — directory enumeration as `{name, is_dir, is_file, is_symlink}`.
775- /// - `fs_stat` — size + file/dir metadata.
776- /// - `fs_mkdir` / `fs_unlink` — create/remove directory or file.
777- /// - `fs_truncate` — set file length.
778- ///
779- /// Every handler resolves its `path` argument under [`root`](Self::root)
780- /// via `FsSandbox::resolve`, which rejects `..` escapes, absolute
781- /// paths that climb outside the root, and symlinks pointing outside.
782- ///
783- /// The handlers call through `std::fs`, which behaves differently on
784- /// Linux and Windows — `normalize_fs_error` smooths out the error
785- /// wording before responses go back to the guest, so the Unikraft
786- /// guest's substring-matching classifier works on both hosts.
787- pub fn register ( self , registry : & mut ToolRegistry ) {
788- use serde_json:: json;
789-
790- let s = self . clone ( ) ;
791- registry. register ( "fs_read" , move |args| {
792- let path = args[ "path" ]
793- . as_str ( )
794- . ok_or_else ( || anyhow ! ( "fs_read: missing 'path'" ) ) ?;
795- let target = s. resolve ( path) ?;
796- let text = std:: fs:: read_to_string ( & target)
797- . map_err ( |e| anyhow ! ( "fs_read {:?}: {}" , path, e) ) ?;
798- Ok ( json ! ( { "text" : text } ) )
799- } ) ;
800-
801- let s = self . clone ( ) ;
802- registry. register ( "fs_write" , move |args| {
803- let path = args[ "path" ]
804- . as_str ( )
805- . ok_or_else ( || anyhow ! ( "fs_write: missing 'path'" ) ) ?;
806- let text = args[ "text" ]
807- . as_str ( )
808- . ok_or_else ( || anyhow ! ( "fs_write: missing 'text'" ) ) ?;
809- let append = args[ "append" ] . as_bool ( ) . unwrap_or ( false ) ;
810- let target = s. resolve ( path) ?;
811- // Create parent dirs? No — guest must fs_mkdir explicitly.
812- use std:: io:: Write ;
813- let mut f = std:: fs:: OpenOptions :: new ( )
814- . write ( true )
815- . create ( true )
816- . truncate ( !append)
817- . append ( append)
818- . open ( & target)
819- . map_err ( |e| anyhow ! ( "fs_write {:?}: {}" , path, e) ) ?;
820- f. write_all ( text. as_bytes ( ) )
821- . map_err ( |e| anyhow ! ( "fs_write {:?}: {}" , path, e) ) ?;
822- Ok ( json ! ( { "bytes_written" : text. len( ) } ) )
823- } ) ;
824-
825- let s = self . clone ( ) ;
826- registry. register ( "fs_list" , move |args| {
827- let path = args[ "path" ] . as_str ( ) . unwrap_or ( "" ) ;
828- let target = s. resolve ( path) ?;
829- let mut entries = Vec :: new ( ) ;
830- for entry in
831- std:: fs:: read_dir ( & target) . map_err ( |e| anyhow ! ( "fs_list {:?}: {}" , path, e) ) ?
832- {
833- let entry = entry?;
834- let name = entry. file_name ( ) . to_string_lossy ( ) . into_owned ( ) ;
835- let ft = entry. file_type ( ) ?;
836- entries. push ( json ! ( {
837- "name" : name,
838- "is_dir" : ft. is_dir( ) ,
839- "is_file" : ft. is_file( ) ,
840- "is_symlink" : ft. is_symlink( ) ,
841- } ) ) ;
842- }
843- Ok ( json ! ( { "entries" : entries } ) )
844- } ) ;
845-
846- let s = self . clone ( ) ;
847- registry. register ( "fs_stat" , move |args| {
848- let path = args[ "path" ]
849- . as_str ( )
850- . ok_or_else ( || anyhow ! ( "fs_stat: missing 'path'" ) ) ?;
851- let target = s. resolve ( path) ?;
852- let md =
853- std:: fs:: metadata ( & target) . map_err ( |e| anyhow ! ( "fs_stat {:?}: {}" , path, e) ) ?;
854- Ok ( json ! ( {
855- "size" : md. len( ) ,
856- "is_dir" : md. is_dir( ) ,
857- "is_file" : md. is_file( ) ,
858- } ) )
859- } ) ;
860-
861- let s = self . clone ( ) ;
862- registry. register ( "fs_mkdir" , move |args| {
863- let path = args[ "path" ]
864- . as_str ( )
865- . ok_or_else ( || anyhow ! ( "fs_mkdir: missing 'path'" ) ) ?;
866- let parents = args[ "parents" ] . as_bool ( ) . unwrap_or ( false ) ;
867- let target = s. resolve ( path) ?;
868- if parents {
869- std:: fs:: create_dir_all ( & target)
870- } else {
871- std:: fs:: create_dir ( & target)
872- }
873- . map_err ( |e| anyhow ! ( "fs_mkdir {:?}: {}" , path, e) ) ?;
874- Ok ( json ! ( { } ) )
875- } ) ;
876-
877- // fs_read_bytes / fs_write_bytes — binary variants for the Phase B
878- // transparent POSIX shim. Bytes are base64-encoded in the JSON
879- // payload so arbitrary binary content round-trips intact.
880- //
881- // fs_read_bytes args: { path, offset?, len? } → { data: "<base64>", eof: bool }
882- // fs_write_bytes args: { path, data: "<base64>", offset?, append? } → { bytes_written }
883- let s = self . clone ( ) ;
884- registry. register ( "fs_read_bytes" , move |args| {
885- use base64:: Engine ;
886- use std:: io:: { Read , Seek , SeekFrom } ;
887- let path = args[ "path" ]
888- . as_str ( )
889- . ok_or_else ( || anyhow ! ( "fs_read_bytes: missing 'path'" ) ) ?;
890- let offset = args[ "offset" ] . as_u64 ( ) . unwrap_or ( 0 ) ;
891- let want = args[ "len" ] . as_u64 ( ) . unwrap_or ( 65536 ) ;
892- let target = s. resolve ( path) ?;
893- let mut f = std:: fs:: File :: open ( & target)
894- . map_err ( |e| anyhow ! ( "fs_read_bytes {:?}: {}" , path, e) ) ?;
895- if offset > 0 {
896- f. seek ( SeekFrom :: Start ( offset) )
897- . map_err ( |e| anyhow ! ( "fs_read_bytes seek {:?}: {}" , path, e) ) ?;
898- }
899- let mut buf = vec ! [ 0u8 ; want as usize ] ;
900- let n = f
901- . read ( & mut buf)
902- . map_err ( |e| anyhow ! ( "fs_read_bytes {:?}: {}" , path, e) ) ?;
903- buf. truncate ( n) ;
904- let eof = n < want as usize ;
905- let encoded = base64:: engine:: general_purpose:: STANDARD . encode ( & buf) ;
906- Ok ( json ! ( { "data" : encoded, "eof" : eof, "bytes_read" : n } ) )
907- } ) ;
908-
909- let s = self . clone ( ) ;
910- registry. register ( "fs_write_bytes" , move |args| {
911- use base64:: Engine ;
912- use std:: io:: { Seek , SeekFrom , Write } ;
913- let path = args[ "path" ]
914- . as_str ( )
915- . ok_or_else ( || anyhow ! ( "fs_write_bytes: missing 'path'" ) ) ?;
916- let data_b64 = args[ "data" ]
917- . as_str ( )
918- . ok_or_else ( || anyhow ! ( "fs_write_bytes: missing 'data'" ) ) ?;
919- let data = base64:: engine:: general_purpose:: STANDARD
920- . decode ( data_b64)
921- . map_err ( |e| anyhow ! ( "fs_write_bytes: bad base64: {}" , e) ) ?;
922- let offset = args[ "offset" ] . as_u64 ( ) ;
923- let append = args[ "append" ] . as_bool ( ) . unwrap_or ( false ) ;
924- let target = s. resolve ( path) ?;
925- let mut f = std:: fs:: OpenOptions :: new ( )
926- . write ( true )
927- . create ( true )
928- . truncate ( offset. is_none ( ) && !append)
929- . append ( append)
930- . open ( & target)
931- . map_err ( |e| anyhow ! ( "fs_write_bytes {:?}: {}" , path, e) ) ?;
932- if let Some ( off) = offset {
933- if !append {
934- f. seek ( SeekFrom :: Start ( off) )
935- . map_err ( |e| anyhow ! ( "fs_write_bytes seek {:?}: {}" , path, e) ) ?;
936- }
937- }
938- f. write_all ( & data)
939- . map_err ( |e| anyhow ! ( "fs_write_bytes {:?}: {}" , path, e) ) ?;
940- Ok ( json ! ( { "bytes_written" : data. len( ) } ) )
941- } ) ;
942-
943- let s = self . clone ( ) ;
944- registry. register ( "fs_truncate" , move |args| {
945- let path = args[ "path" ]
946- . as_str ( )
947- . ok_or_else ( || anyhow ! ( "fs_truncate: missing 'path'" ) ) ?;
948- let length = args[ "length" ]
949- . as_u64 ( )
950- . ok_or_else ( || anyhow ! ( "fs_truncate: missing 'length'" ) ) ?;
951- let target = s. resolve ( path) ?;
952- let f = std:: fs:: OpenOptions :: new ( )
953- . write ( true )
954- . open ( & target)
955- . map_err ( |e| anyhow ! ( "fs_truncate {:?}: {}" , path, e) ) ?;
956- f. set_len ( length)
957- . map_err ( |e| anyhow ! ( "fs_truncate {:?}: {}" , path, e) ) ?;
958- Ok ( json ! ( { } ) )
959- } ) ;
960-
961- let s = self . clone ( ) ;
962- registry. register ( "fs_unlink" , move |args| {
963- let path = args[ "path" ]
964- . as_str ( )
965- . ok_or_else ( || anyhow ! ( "fs_unlink: missing 'path'" ) ) ?;
966- let target = s. resolve ( path) ?;
967- let md =
968- std:: fs:: metadata ( & target) . map_err ( |e| anyhow ! ( "fs_unlink {:?}: {}" , path, e) ) ?;
969- if md. is_dir ( ) {
970- std:: fs:: remove_dir ( & target)
971- } else {
972- std:: fs:: remove_file ( & target)
973- }
974- . map_err ( |e| anyhow ! ( "fs_unlink {:?}: {}" , path, e) ) ?;
975- Ok ( json ! ( { } ) )
976- } ) ;
977- }
978769}
979770
980771/// Internal helper: assemble the final tool registry from caller-supplied
@@ -2496,10 +2287,11 @@ mod tests {
24962287 // End-to-end through the tool registry: the error surface the
24972288 // guest actually sees.
24982289 let root = tmpdir ( "dispatch" ) ;
2290+ let preopens = vec ! [ Preopen :: new( & root, "/host" ) . unwrap( ) ] ;
24992291 let mut reg = ToolRegistry :: new ( ) ;
2500- FsSandbox :: new ( & root ) . unwrap ( ) . register ( & mut reg) ;
2292+ FsRouter :: new ( & preopens ) . unwrap ( ) . register ( & mut reg) ;
25012293
2502- let req = br#"{"name":"fs_read","args":{"path":"../outside.txt"}}"# ;
2294+ let req = br#"{"name":"fs_read","args":{"path":"/host/ ../outside.txt"}}"# ;
25032295 let resp = reg. dispatch ( req) ;
25042296 let s = std:: str:: from_utf8 ( & resp) . unwrap ( ) ;
25052297 assert ! ( s. contains( "\" error\" " ) , "{s}" ) ;
@@ -2579,15 +2371,16 @@ mod tests {
25792371 #[ test]
25802372 fn fs_write_then_read_roundtrip ( ) {
25812373 let root = tmpdir ( "roundtrip" ) ;
2374+ let preopens = vec ! [ Preopen :: new( & root, "/host" ) . unwrap( ) ] ;
25822375 let mut reg = ToolRegistry :: new ( ) ;
2583- FsSandbox :: new ( & root ) . unwrap ( ) . register ( & mut reg) ;
2376+ FsRouter :: new ( & preopens ) . unwrap ( ) . register ( & mut reg) ;
25842377
2585- let w = br#"{"name":"fs_write","args":{"path":"hello.txt","text":"hi"}}"# ;
2378+ let w = br#"{"name":"fs_write","args":{"path":"/host/ hello.txt","text":"hi"}}"# ;
25862379 let resp = reg. dispatch ( w) ;
25872380 let s = std:: str:: from_utf8 ( & resp) . unwrap ( ) ;
25882381 assert ! ( s. contains( "\" bytes_written\" :2" ) , "{s}" ) ;
25892382
2590- let r = br#"{"name":"fs_read","args":{"path":"hello.txt"}}"# ;
2383+ let r = br#"{"name":"fs_read","args":{"path":"/host/ hello.txt"}}"# ;
25912384 let resp = reg. dispatch ( r) ;
25922385 let s = std:: str:: from_utf8 ( & resp) . unwrap ( ) ;
25932386 assert ! ( s. contains( "\" text\" :\" hi\" " ) , "{s}" ) ;
0 commit comments