$lines Lines. */ public static function render( array $lines ): string { $clean = array(); foreach ( $lines as $line ) { $line = trim( (string) $line ); if ( '' !== $line ) { $clean[] = $line; } } sort( $clean ); return implode( "\n", $clean ) . ( $clean ? "\n" : '' ); } /** * Hash of the checksums payload list (sorted body). * * @param list $lines Lines. */ public static function payload_list_hash( array $lines ): string { return hash( 'sha256', self::render( $lines ) ); } /** * Verify checksum lines against files currently in a zip (excluding checksums/manifest). * * @param string $zip_path Zip path. * @param list $lines Expected lines. * @return array{ok:bool,error?:string,checked?:int} */ public static function verify_zip_members( string $zip_path, array $lines ): array { if ( ! class_exists( ZipArchive::class ) ) { return array( 'ok' => false, 'error' => __( 'ZipArchive missing for checksum verify.', '007aj-backup-restore-verifier' ), ); } $expected = array(); foreach ( $lines as $line ) { $line = trim( (string) $line ); if ( '' === $line ) { continue; } if ( ! preg_match( '/^([a-f0-9]{64})\s+(.+)$/', $line, $m ) ) { return array( 'ok' => false, 'error' => __( 'Malformed checksum line.', '007aj-backup-restore-verifier' ), ); } $expected[ $m[2] ] = $m[1]; } $zip = new ZipArchive(); if ( true !== $zip->open( $zip_path ) ) { return array( 'ok' => false, 'error' => __( 'Unable to open zip for checksum verify.', '007aj-backup-restore-verifier' ), ); } $checked = 0; foreach ( $expected as $name => $hash ) { if ( 'checksums.sha256' === $name || 'manifest.json' === $name ) { continue; } $index = $zip->locateName( $name, ZipArchive::FL_NOCASE ); if ( false === $index ) { $zip->close(); return array( 'ok' => false, 'error' => sprintf( /* translators: %s: archive member path */ __( 'Missing archive member for checksum: %s', '007aj-backup-restore-verifier' ), $name ), ); } $contents = $zip->getFromIndex( $index ); if ( false === $contents ) { $zip->close(); return array( 'ok' => false, 'error' => sprintf( /* translators: %s: archive member path */ __( 'Unable to read archive member: %s', '007aj-backup-restore-verifier' ), $name ), ); } $actual = hash( 'sha256', $contents ); if ( ! hash_equals( $hash, $actual ) ) { $zip->close(); return array( 'ok' => false, 'error' => sprintf( /* translators: %s: archive member path */ __( 'Checksum mismatch: %s', '007aj-backup-restore-verifier' ), $name ), ); } ++$checked; } $zip->close(); return array( 'ok' => true, 'checked' => $checked, ); } }