@@ -60,6 +60,12 @@ interface HostMountFileMaterializationResponse {
6060 skipped ?: number
6161}
6262
63+ interface HostMountFileVerificationResponse {
64+ schema ?: string
65+ repaired ?: number
66+ skipped ?: number
67+ }
68+
6369const HOST_MOUNT_FILE_BATCH_SIZE = 100
6470const HOST_MOUNT_DIRECTORY_BATCH_SIZE = 500
6571
@@ -200,6 +206,7 @@ async function materializeHostMountToVfs(server: PlaygroundCliServer, mount: Mou
200206 let skipped = 0
201207 const directoryBatch : string [ ] = [ ]
202208 const fileBatch : HostMountFilePayload [ ] = [ ]
209+ const verificationBatch : HostMountFilePayload [ ] = [ ]
203210
204211 const flushDirectories = async ( ) => {
205212 if ( directoryBatch . length === 0 ) {
@@ -224,6 +231,13 @@ async function materializeHostMountToVfs(server: PlaygroundCliServer, mount: Mou
224231 created += result . created
225232 skipped += result . skipped
226233 }
234+ const flushVerificationBatch = async ( ) => {
235+ if ( verificationBatch . length === 0 ) {
236+ return
237+ }
238+ const result = await verifyHostMountFilesWithPhp ( server , verificationBatch . splice ( 0 , verificationBatch . length ) )
239+ skipped += result . skipped
240+ }
227241 const writeFilePayload = async ( payload : HostMountFilePayload ) => {
228242 if ( ! server . playground . writeFile ) {
229243 fileBatch . push ( payload )
@@ -249,6 +263,10 @@ async function materializeHostMountToVfs(server: PlaygroundCliServer, mount: Mou
249263 try {
250264 await server . playground . writeFile ( target , text )
251265 materialized ++
266+ verificationBatch . push ( payload )
267+ if ( verificationBatch . length >= HOST_MOUNT_FILE_BATCH_SIZE ) {
268+ await flushVerificationBatch ( )
269+ }
252270 } catch {
253271 const fallback = await materializeHostMountFilesWithPhp ( server , [ payload ] , [ ] )
254272 materialized += fallback . materialized
@@ -271,6 +289,7 @@ async function materializeHostMountToVfs(server: PlaygroundCliServer, mount: Mou
271289 }
272290 await flushDirectories ( )
273291 await flushFileBatch ( )
292+ await flushVerificationBatch ( )
274293 return { materialized, created, skipped }
275294}
276295
@@ -366,6 +385,19 @@ async function materializeHostMountFilesWithPhp(server: PlaygroundCliServer, fil
366385 }
367386}
368387
388+ async function verifyHostMountFilesWithPhp ( server : PlaygroundCliServer , files : HostMountFilePayload [ ] ) : Promise < { repaired : number ; skipped : number } > {
389+ const response = await server . playground . run ( { code : hostMountVerifyPhp ( files ) } )
390+ assertPlaygroundResponseOk ( "playground-staged-input-verify" , response )
391+ const parsed = parseMaterializationJson < HostMountFileVerificationResponse > ( response . text , "wp-codebox/host-mount-verification/v1" , "playground-staged-input-verify" )
392+ if ( ( parsed . skipped ?? 0 ) > 0 ) {
393+ throw new Error ( `playground-staged-input-verify could not preserve ${ parsed . skipped } staged input file(s)` )
394+ }
395+ return {
396+ repaired : parsed . repaired ?? 0 ,
397+ skipped : parsed . skipped ?? 0 ,
398+ }
399+ }
400+
369401function parseMaterializationJson < T extends { schema ?: string } > ( text : string , schema : string , command : string ) : T {
370402 let parsed : unknown
371403 try {
@@ -493,6 +525,7 @@ foreach (($payload['directories'] ?? array()) as $directory) {
493525 }
494526 $skipped++;
495527}
528+
496529foreach (($payload['files'] ?? array()) as $file) {
497530 $target = (string) ($file['target'] ?? '');
498531 $contents = (string) ($file['contentsBase64'] ?? '');
@@ -516,6 +549,39 @@ echo json_encode(array('schema' => 'wp-codebox/host-mount-materialization/v1', '
516549`
517550}
518551
552+ function hostMountVerifyPhp ( files : HostMountFilePayload [ ] ) : string {
553+ const payload = JSON . stringify ( JSON . stringify ( { files } ) )
554+ return `<?php
555+ $payload = json_decode(${ payload } , true);
556+ $repaired = 0;
557+ $skipped = 0;
558+ foreach (($payload['files'] ?? array()) as $file) {
559+ $target = (string) ($file['target'] ?? '');
560+ $contents = base64_decode((string) ($file['contentsBase64'] ?? ''), true);
561+ if ('' === $target || str_contains($target, "\0") || false === $contents) {
562+ $skipped++;
563+ continue;
564+ }
565+ $target_hash = is_file($target) ? hash_file('sha256', $target) : false;
566+ if (is_string($target_hash) && hash_equals(hash('sha256', $contents), $target_hash)) {
567+ continue;
568+ }
569+ $directory = dirname($target);
570+ if ((!is_dir($directory) && !mkdir($directory, 0777, true) && !is_dir($directory)) || false === file_put_contents($target, $contents)) {
571+ $skipped++;
572+ continue;
573+ }
574+ $repaired_hash = hash_file('sha256', $target);
575+ if (!is_string($repaired_hash) || !hash_equals(hash('sha256', $contents), $repaired_hash)) {
576+ $skipped++;
577+ continue;
578+ }
579+ $repaired++;
580+ }
581+ echo json_encode(array('schema' => 'wp-codebox/host-mount-verification/v1', 'repaired' => $repaired, 'skipped' => $skipped), JSON_UNESCAPED_SLASHES);
582+ `
583+ }
584+
519585function hostMountMkdirPhp ( directories : string [ ] ) : string {
520586 const payload = JSON . stringify ( JSON . stringify ( { directories } ) )
521587 return `<?php
0 commit comments