resolve($tenantId, $store); $info = $service->stat($this->qualify($reference, $key)); return $info === null ? null : $this->logicalInfo($info, $reference); } public function read(string $tenantId, string $store, string $key): ?BinaryResource { [$service, $reference] = $this->resolve($tenantId, $store); return $service->read($this->qualify($reference, $key)); } public function write( string $tenantId, string $store, string $key, BinaryResource $content, array $metadata = [], ?WriteCondition $condition = null, ): BlobInfo { [$service, $reference] = $this->resolve($tenantId, $store); $info = $service->write( $this->qualify($reference, $key), $content, $metadata, $condition, ); return $this->logicalInfo($info, $reference); } public function delete( string $tenantId, string $store, string $key, ?WriteCondition $condition = null, ): bool { [$service, $reference] = $this->resolve($tenantId, $store); return $service->delete($this->qualify($reference, $key), $condition); } public function list(string $tenantId, string $store, string $prefix = ''): iterable { [$service, $reference] = $this->resolve($tenantId, $store); $physicalPrefix = $reference->namespace . '/'; if ($prefix !== '') { $physicalPrefix .= $this->normalizeKey($prefix, true); } return $this->logicalItems($service->list($physicalPrefix), $reference); } /** @return array{SystemStoreServiceInterface, StoreReference} */ private function resolve(string $tenantId, string $store): array { $tenant = $this->tenants->fetchById($tenantId); if ($tenant === null) { throw new SystemStoreException("Tenant '{$tenantId}' was not found"); } $reference = $tenant->getConfiguration()->stores()->store($store); if ($reference === null) { throw new SystemStoreException("System store '{$store}' is not configured for tenant '{$tenantId}'"); } $provider = $this->providers->resolve(ProviderInterface::TYPE_SYSTEM_STORE, $reference->provider); if (!$provider instanceof ProviderBaseInterface) { throw new SystemStoreException("System-store provider '{$reference->provider}' is unavailable or incompatible"); } $service = $provider->serviceFetch($tenantId, SystemIdentity::USER, $reference->service); if (!$service instanceof SystemStoreServiceInterface) { throw new SystemStoreException("System-store service '{$reference->service}' is unavailable or incompatible"); } return [$service, $reference]; } private function qualify(StoreReference $reference, string $key): string { return $reference->namespace . '/' . $this->normalizeKey($key); } private function normalizeKey(string $key, bool $allowTrailingSlash = false): string { if ( $key === '' || str_starts_with($key, '/') || (!$allowTrailingSlash && str_ends_with($key, '/')) || str_contains($key, '\\') || str_contains($key, "\0") ) { throw new InvalidKeyException('System-store keys must be non-empty normalized relative keys'); } $segments = explode('/', $key); if ($allowTrailingSlash && end($segments) === '') { array_pop($segments); } if (in_array('', $segments, true) || in_array('.', $segments, true) || in_array('..', $segments, true)) { throw new InvalidKeyException('System-store keys cannot contain empty or traversal segments'); } return $key; } private function logicalInfo(BlobInfo $info, StoreReference $reference): BlobInfo { $prefix = $reference->namespace . '/'; if (!str_starts_with($info->key, $prefix)) { throw new SystemStoreException('System-store provider returned a blob outside the configured namespace'); } return new BlobInfo( key: substr($info->key, strlen($prefix)), mimeType: $info->mimeType, size: $info->size, etag: $info->etag, modifiedAt: $info->modifiedAt, attributes: $info->attributes, ); } /** * @param iterable $items * @return iterable */ private function logicalItems(iterable $items, StoreReference $reference): iterable { foreach ($items as $item) { if (!$item instanceof BlobInfo) { throw new SystemStoreException('System-store provider returned invalid listing metadata'); } yield $this->logicalInfo($item, $reference); } } }