26 lines
2.3 KiB
PHP
26 lines
2.3 KiB
PHP
<?php
|
|
namespace Tempo\Finance\Composition;
|
|
/** Verify against a caller-pinned manifest BEFORE loading executable package files. */
|
|
final class Bundle
|
|
{
|
|
public static function verify(string $root,string $expected):array
|
|
{
|
|
$root=realpath($root)?:throw new \LogicException('FINANCE_BUNDLE_MISSING');$manifest=$root.'/manifest.json';
|
|
if(!preg_match('/^[a-f0-9]{64}$/D',$expected)||!is_file($manifest)||is_link($manifest)||!hash_equals($expected,hash_file('sha256',$manifest)))throw new \LogicException('FINANCE_BUNDLE_MANIFEST_MISMATCH');
|
|
$m=json_decode(file_get_contents($manifest),true,64,JSON_THROW_ON_ERROR);
|
|
if(($m['contract']??null)!=='COMMON-FINANCE-BUNDLE-v1'||!is_array($m['files']??null)||count($m['files'])<3)throw new \LogicException('FINANCE_BUNDLE_CONTRACT');
|
|
$expectedFiles=['manifest.json'];$seen=[];
|
|
foreach($m['files'] as $entry){
|
|
$name=$entry['file']??'';
|
|
if(!is_string($name)||!preg_match('~^(Bundle\.php|(?:payables|general-ledger)/(?:bootstrap\.php|README\.md|(?:src|ui)/[A-Za-z0-9_-]+\.php))$~D',$name)||isset($seen[$name]))throw new \LogicException('FINANCE_BUNDLE_PATH');
|
|
$seen[$name]=true;$file=$root.'/'.$name;$actual=realpath($file);
|
|
if(!$actual||!str_starts_with($actual,$root.DIRECTORY_SEPARATOR)||is_link($file)||!is_file($actual)||!hash_equals($entry['sha256']??'',hash_file('sha256',$actual)))throw new \LogicException('FINANCE_BUNDLE_FILE_MISMATCH');
|
|
$expectedFiles[]=$name;
|
|
}
|
|
$actualFiles=[];foreach(new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($root,\FilesystemIterator::SKIP_DOTS)) as $file){if($file->isLink())throw new \LogicException('FINANCE_BUNDLE_SYMLINK');if($file->isFile())$actualFiles[]=str_replace('\\','/',substr($file->getPathname(),strlen($root)+1));}
|
|
sort($actualFiles);sort($expectedFiles);if($actualFiles!==$expectedFiles)throw new \LogicException('FINANCE_BUNDLE_EXTRA_OR_MISSING_FILE');
|
|
foreach(['Bundle.php','payables/bootstrap.php','general-ledger/bootstrap.php'] as $required)if(!isset($seen[$required]))throw new \LogicException('FINANCE_BUNDLE_INCOMPLETE');
|
|
return ['payables'=>$root.'/payables','ledger'=>$root.'/general-ledger','files'=>count($seen),'manifest_sha256'=>$expected];
|
|
}
|
|
}
|