computeFileHash($zipPath); if (! hash_equals((string) $expectedZipHash, $actualZipHash)) { $errors[] = 'zip_hash_mismatch'; } } // 5. composer.json/lock 해시 비교 (소스에 존재할 때만) $composerJsonPath = $sourceDir.DIRECTORY_SEPARATOR.'composer.json'; if (file_exists($composerJsonPath)) { $expected = $manifest['composer_json_sha256'] ?? null; if ($expected !== null) { $actual = $this->computeFileHash($composerJsonPath); if (! hash_equals((string) $expected, $actual)) { $errors[] = 'composer_json_sha_mismatch'; } } } $composerLockPath = $sourceDir.DIRECTORY_SEPARATOR.'composer.lock'; if (file_exists($composerLockPath)) { $expected = $manifest['composer_lock_sha256'] ?? null; if ($expected !== null) { $actual = $this->computeFileHash($composerLockPath); if (! hash_equals((string) $expected, $actual)) { $errors[] = 'composer_lock_sha_mismatch'; } } } if (! empty($errors)) { return IntegrityResult::invalid($errors, $manifest); } return IntegrityResult::valid($manifest, $warnings); } /** * 파일의 SHA256 해시를 계산합니다. */ public function computeFileHash(string $path): string { return hash_file('sha256', $path) ?: ''; } /** * manifest만 파싱하여 반환 (검증 없이 메타 조회용). * * @return array|null */ public function readManifest(string $sourceDir): ?array { $path = $sourceDir.DIRECTORY_SEPARATOR.self::MANIFEST_FILENAME; if (! file_exists($path)) { return null; } $json = @file_get_contents($path); if ($json === false) { return null; } try { $data = json_decode($json, true, 512, JSON_THROW_ON_ERROR); } catch (\JsonException $e) { return null; } return is_array($data) ? $data : null; } }