@@ -894,6 +894,235 @@ suite("Sandboxing", () => {
894894 } ) ,
895895 ) ;
896896} ) ;
897+ suite ( "FS openAt descriptor flags" , ( ) => {
898+ test ( "preopen descriptor has read, write, and mutateDirectory flags" , async ( ) => {
899+ const { filesystem } = await import ( "@bytecodealliance/preview2-shim" ) ;
900+ const [ [ rootDescriptor ] ] = filesystem . preopens . getDirectories ( ) ;
901+ const flags = rootDescriptor . getFlags ( ) ;
902+ assert . strictEqual ( flags . read , true ) ;
903+ assert . strictEqual ( flags . write , true ) ;
904+ assert . strictEqual ( flags . mutateDirectory , true ) ;
905+ } ) ;
906+
907+ test (
908+ "openAt with mutateDirectory descriptor flag succeeds on preopen" ,
909+ testWithGCWrap ( async ( ) => {
910+ const { mkdtemp, mkdir } = await import ( "node:fs/promises" ) ;
911+ const { tmpdir } = await import ( "node:os" ) ;
912+ const { sep, normalize } = await import ( "node:path" ) ;
913+ const { WASIShim } = await import ( "@bytecodealliance/preview2-shim/instantiation" ) ;
914+
915+ const tmpDir = await mkdtemp ( normalize ( tmpdir ( ) + sep ) ) ;
916+ await mkdir ( `${ tmpDir } /subdir` ) ;
917+
918+ const shim = new WASIShim ( {
919+ sandbox : { preopens : { "/work" : tmpDir } } ,
920+ } ) ;
921+ const importObj = shim . getImportObject ( ) ;
922+ const [ [ rootDescriptor ] ] = importObj [ "wasi:filesystem/preopens" ] . getDirectories ( ) ;
923+
924+ const dirDescriptor = rootDescriptor . openAt (
925+ { } ,
926+ "subdir" ,
927+ { directory : true } ,
928+ { read : true , mutateDirectory : true } ,
929+ ) ;
930+ dirDescriptor [ symbolDispose ] ( ) ;
931+ } ) ,
932+ ) ;
933+
934+ test (
935+ "openAt with create and write flags succeeds on preopen" ,
936+ testWithGCWrap ( async ( ) => {
937+ const { mkdtemp } = await import ( "node:fs/promises" ) ;
938+ const { tmpdir } = await import ( "node:os" ) ;
939+ const { sep, normalize } = await import ( "node:path" ) ;
940+ const { WASIShim } = await import ( "@bytecodealliance/preview2-shim/instantiation" ) ;
941+
942+ const tmpDir = await mkdtemp ( normalize ( tmpdir ( ) + sep ) ) ;
943+
944+ const shim = new WASIShim ( {
945+ sandbox : { preopens : { "/work" : tmpDir } } ,
946+ } ) ;
947+ const importObj = shim . getImportObject ( ) ;
948+ const [ [ rootDescriptor ] ] = importObj [ "wasi:filesystem/preopens" ] . getDirectories ( ) ;
949+
950+ const fd = rootDescriptor . openAt (
951+ { } ,
952+ "test-file.txt" ,
953+ { create : true } ,
954+ { write : true } ,
955+ ) ;
956+ fd [ symbolDispose ] ( ) ;
957+ } ) ,
958+ ) ;
959+
960+ test (
961+ "openAt read-only on read-only base descriptor succeeds" ,
962+ testWithGCWrap ( async ( ) => {
963+ const { mkdtemp, mkdir, writeFile } = await import ( "node:fs/promises" ) ;
964+ const { tmpdir } = await import ( "node:os" ) ;
965+ const { sep, normalize } = await import ( "node:path" ) ;
966+ const { WASIShim } = await import ( "@bytecodealliance/preview2-shim/instantiation" ) ;
967+
968+ const tmpDir = await mkdtemp ( normalize ( tmpdir ( ) + sep ) ) ;
969+ await mkdir ( `${ tmpDir } /subdir` ) ;
970+ await writeFile ( `${ tmpDir } /subdir/existing.txt` , "hello" ) ;
971+
972+ const shim = new WASIShim ( {
973+ sandbox : { preopens : { "/work" : tmpDir } } ,
974+ } ) ;
975+ const importObj = shim . getImportObject ( ) ;
976+ const [ [ rootDescriptor ] ] = importObj [ "wasi:filesystem/preopens" ] . getDirectories ( ) ;
977+
978+ const readOnlyDir = rootDescriptor . openAt (
979+ { } ,
980+ "subdir" ,
981+ { directory : true } ,
982+ { read : true } ,
983+ ) ;
984+
985+ const fd = readOnlyDir . openAt ( { } , "existing.txt" , { } , { read : true } ) ;
986+ fd [ symbolDispose ] ( ) ;
987+ readOnlyDir [ symbolDispose ] ( ) ;
988+ } ) ,
989+ ) ;
990+
991+ test (
992+ "openAt throws read-only when base lacks mutateDirectory and write requested" ,
993+ testWithGCWrap ( async ( ) => {
994+ const { mkdtemp, mkdir, writeFile } = await import ( "node:fs/promises" ) ;
995+ const { tmpdir } = await import ( "node:os" ) ;
996+ const { sep, normalize } = await import ( "node:path" ) ;
997+ const { WASIShim } = await import ( "@bytecodealliance/preview2-shim/instantiation" ) ;
998+
999+ const tmpDir = await mkdtemp ( normalize ( tmpdir ( ) + sep ) ) ;
1000+ await mkdir ( `${ tmpDir } /subdir` ) ;
1001+ await writeFile ( `${ tmpDir } /subdir/existing.txt` , "hello" ) ;
1002+
1003+ const shim = new WASIShim ( {
1004+ sandbox : { preopens : { "/work" : tmpDir } } ,
1005+ } ) ;
1006+ const importObj = shim . getImportObject ( ) ;
1007+ const [ [ rootDescriptor ] ] = importObj [ "wasi:filesystem/preopens" ] . getDirectories ( ) ;
1008+
1009+ const readOnlyDir = rootDescriptor . openAt (
1010+ { } ,
1011+ "subdir" ,
1012+ { directory : true } ,
1013+ { read : true } ,
1014+ ) ;
1015+
1016+ throws (
1017+ ( ) => readOnlyDir . openAt ( { } , "existing.txt" , { } , { write : true } ) ,
1018+ ( err ) => err === "read-only" ,
1019+ ) ;
1020+
1021+ readOnlyDir [ symbolDispose ] ( ) ;
1022+ } ) ,
1023+ ) ;
1024+
1025+ test (
1026+ "openAt throws read-only when base lacks mutateDirectory and create requested" ,
1027+ testWithGCWrap ( async ( ) => {
1028+ const { mkdtemp, mkdir } = await import ( "node:fs/promises" ) ;
1029+ const { tmpdir } = await import ( "node:os" ) ;
1030+ const { sep, normalize } = await import ( "node:path" ) ;
1031+ const { WASIShim } = await import ( "@bytecodealliance/preview2-shim/instantiation" ) ;
1032+
1033+ const tmpDir = await mkdtemp ( normalize ( tmpdir ( ) + sep ) ) ;
1034+ await mkdir ( `${ tmpDir } /subdir` ) ;
1035+
1036+ const shim = new WASIShim ( {
1037+ sandbox : { preopens : { "/work" : tmpDir } } ,
1038+ } ) ;
1039+ const importObj = shim . getImportObject ( ) ;
1040+ const [ [ rootDescriptor ] ] = importObj [ "wasi:filesystem/preopens" ] . getDirectories ( ) ;
1041+
1042+ const readOnlyDir = rootDescriptor . openAt (
1043+ { } ,
1044+ "subdir" ,
1045+ { directory : true } ,
1046+ { read : true } ,
1047+ ) ;
1048+
1049+ throws (
1050+ ( ) => readOnlyDir . openAt ( { } , "new-file.txt" , { create : true } , { read : true } ) ,
1051+ ( err ) => err === "read-only" ,
1052+ ) ;
1053+
1054+ readOnlyDir [ symbolDispose ] ( ) ;
1055+ } ) ,
1056+ ) ;
1057+
1058+ test (
1059+ "openAt throws read-only when base lacks mutateDirectory and truncate requested" ,
1060+ testWithGCWrap ( async ( ) => {
1061+ const { mkdtemp, mkdir, writeFile } = await import ( "node:fs/promises" ) ;
1062+ const { tmpdir } = await import ( "node:os" ) ;
1063+ const { sep, normalize } = await import ( "node:path" ) ;
1064+ const { WASIShim } = await import ( "@bytecodealliance/preview2-shim/instantiation" ) ;
1065+
1066+ const tmpDir = await mkdtemp ( normalize ( tmpdir ( ) + sep ) ) ;
1067+ await mkdir ( `${ tmpDir } /subdir` ) ;
1068+ await writeFile ( `${ tmpDir } /subdir/existing.txt` , "hello" ) ;
1069+
1070+ const shim = new WASIShim ( {
1071+ sandbox : { preopens : { "/work" : tmpDir } } ,
1072+ } ) ;
1073+ const importObj = shim . getImportObject ( ) ;
1074+ const [ [ rootDescriptor ] ] = importObj [ "wasi:filesystem/preopens" ] . getDirectories ( ) ;
1075+
1076+ const readOnlyDir = rootDescriptor . openAt (
1077+ { } ,
1078+ "subdir" ,
1079+ { directory : true } ,
1080+ { read : true } ,
1081+ ) ;
1082+
1083+ throws (
1084+ ( ) => readOnlyDir . openAt ( { } , "existing.txt" , { truncate : true } , { write : true } ) ,
1085+ ( err ) => err === "read-only" ,
1086+ ) ;
1087+
1088+ readOnlyDir [ symbolDispose ] ( ) ;
1089+ } ) ,
1090+ ) ;
1091+
1092+ test (
1093+ "openAt throws read-only when base lacks mutateDirectory and mutateDirectory requested" ,
1094+ testWithGCWrap ( async ( ) => {
1095+ const { mkdtemp, mkdir } = await import ( "node:fs/promises" ) ;
1096+ const { tmpdir } = await import ( "node:os" ) ;
1097+ const { sep, normalize } = await import ( "node:path" ) ;
1098+ const { WASIShim } = await import ( "@bytecodealliance/preview2-shim/instantiation" ) ;
1099+
1100+ const tmpDir = await mkdtemp ( normalize ( tmpdir ( ) + sep ) ) ;
1101+ await mkdir ( `${ tmpDir } /subdir` ) ;
1102+
1103+ const shim = new WASIShim ( {
1104+ sandbox : { preopens : { "/work" : tmpDir } } ,
1105+ } ) ;
1106+ const importObj = shim . getImportObject ( ) ;
1107+ const [ [ rootDescriptor ] ] = importObj [ "wasi:filesystem/preopens" ] . getDirectories ( ) ;
1108+
1109+ const readOnlyDir = rootDescriptor . openAt (
1110+ { } ,
1111+ "subdir" ,
1112+ { directory : true } ,
1113+ { read : true } ,
1114+ ) ;
1115+
1116+ throws (
1117+ ( ) => readOnlyDir . openAt ( { } , "subdir" , { directory : true } , { read : true , mutateDirectory : true } ) ,
1118+ ( err ) => err === "read-only" ,
1119+ ) ;
1120+
1121+ readOnlyDir [ symbolDispose ] ( ) ;
1122+ } ) ,
1123+ ) ;
1124+ } ) ;
1125+
8971126suite ( "Browser shim guards" , ( ) => {
8981127 test ( "pollList throws on empty list" , async ( ) => {
8991128 const { poll } = await import ( "../lib/browser/io.js" ) ;
0 commit comments