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