feat: catalog shared contracts (Tasks 1-4)
Task 1: Establish catalog identity and atomic file ownership - CatalogService, CatalogServiceInterface, ModelMetadata DTO - PdoCatalogRepository with atomic operations, savepoint support - FOR UPDATE file locks in ascending order, FOR NO KEY UPDATE for metadata - FK-compatible lock composition documented (spec §6) Task 2: M3 import & indexing engine delivery layer - ImportService, ImportWorker with durable collision handling - SpaceChecker, StreamingHasher, VerifiedCopier, StlValidator - FileEnumerator, LocalFilesystem, PathSafety - Console command wiring via ContainerWiringTest Task 3: Importer integration and repair - LegacyGuessedImportGroupingAdapter isolates first-directory guessing - ImportService uses adapter; ImportWorker uses CatalogServiceInterface - Null owners remain null; DomainException mapped to CATALOG_CONFLICT - SQL failure after registration/path update with caller sentinel rollback Task 4: PostgreSQL contention evidence and bounded acceptance - 6 new PostgresCatalogContractsTest cases with independent sessions - Generated schemas, 250ms lock timeouts, 5s statement timeouts - Ascending file locks, FOR SHARE contention, stale-owner rejection - Rollback/retry, nested transaction survival, root creation contention - Negative controls: replay FOR UPDATE removal (11 failures), metadata FOR UPDATE (4 FK KEY SHARE timeouts) - Fresh Docker PostgreSQL 17 / PHP 8.4.26: 473 tests, 2718 assertions, 10 skips, exit 0 All local: 474 tests, 2347 assertions, 54 skips, foundation/lint clean.
This commit is contained in:
1 parent
6b9fb54b4f
commit
a829d6e8c6
21 files changed
+1903
-61
No files matched your search
@@ -0,0 +1,433 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MeshVault\Tests\Integration;
|
||||
|
||||
use DomainException;
|
||||
use InvalidArgumentException;
|
||||
use MeshVault\Catalog\CatalogService;
|
||||
use MeshVault\Catalog\CatalogServiceInterface;
|
||||
use MeshVault\Repository\PdoCatalogRepository;
|
||||
use PDO;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use RuntimeException;
|
||||
|
||||
final class CatalogContractsTest extends TestCase
|
||||
{
|
||||
private PDO $pdo;
|
||||
private PdoCatalogRepository $repository;
|
||||
private CatalogService $service;
|
||||
private string $database;
|
||||
private string $physicalRoot;
|
||||
|
||||
private function physicalFiles(?int $owner): array
|
||||
{
|
||||
$this->physicalRoot = RepositoryDatabase::directory('contract-files');
|
||||
$ids = [];
|
||||
foreach (['parts/one.stl', 'parts/two.stl', 'images/photo.png', 'docs/manual.pdf'] as $path) {
|
||||
if (!is_dir(dirname($this->physicalRoot . '/' . $path))) {
|
||||
mkdir(dirname($this->physicalRoot . '/' . $path));
|
||||
}
|
||||
$bytes = "fixture\0" . $path;
|
||||
file_put_contents($this->physicalRoot . '/' . $path, $bytes);
|
||||
touch($this->physicalRoot . '/' . $path, 1000000000);
|
||||
$ids[] = $this->service->registerFile($owner, $path, hash('sha256', $bytes), strlen($bytes));
|
||||
}
|
||||
return $ids;
|
||||
}
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->database = tempnam(sys_get_temp_dir(), 'meshvault-contracts-');
|
||||
$this->pdo = RepositoryDatabase::migrate($this->database);
|
||||
$this->repository = new PdoCatalogRepository($this->pdo);
|
||||
$this->service = new CatalogService($this->repository);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
unset($this->service, $this->repository, $this->pdo);
|
||||
gc_collect_cycles();
|
||||
RepositoryDatabase::cleanup($this->database);
|
||||
if (isset($this->physicalRoot)) {
|
||||
RepositoryDatabase::remove($this->physicalRoot);
|
||||
}
|
||||
}
|
||||
|
||||
public function testServiceBoundaryAndNormalizedReadOnlyLookups(): void
|
||||
{
|
||||
self::assertInstanceOf(CatalogServiceInterface::class, $this->service);
|
||||
foreach ([$this->repository, $this->service] as $catalog) {
|
||||
self::assertNull($catalog->file(999));
|
||||
self::assertNull($catalog->fileByManagedPath('absent.stl'));
|
||||
}
|
||||
self::assertSame(0, (int) $this->pdo->query('SELECT COUNT(*) FROM folders')->fetchColumn());
|
||||
$id = $this->service->registerFile(null, 'physical/nested/a.stl', 'hash', 0);
|
||||
$file = $this->service->file($id);
|
||||
self::assertSame($file, $this->repository->fileByManagedPath('physical/nested/a.stl'));
|
||||
self::assertSame($id, $file['id']);
|
||||
self::assertNull($file['model_id']);
|
||||
self::assertIsInt($file['folder_id']);
|
||||
self::assertSame(0, $file['size']);
|
||||
$this->pdo->exec('UPDATE files SET hash = NULL, size = NULL');
|
||||
self::assertNull($this->service->file($id)['hash']);
|
||||
self::assertNull($this->service->file($id)['size']);
|
||||
}
|
||||
|
||||
public function testRegistrationDerivesFoldersAndReplayPreservesEveryColumn(): void
|
||||
{
|
||||
$owner = $this->service->createModel('/home', 'Model');
|
||||
$id = $this->service->registerFile($owner, 'physical/nested/a.stl', 'hash', 1, 'model/stl', 'Missing');
|
||||
$before = $this->service->file($id);
|
||||
self::assertSame($owner, $before['model_id']);
|
||||
self::assertSame('/physical/nested', $this->pdo->query(
|
||||
'SELECT path FROM folders WHERE id = ' . $before['folder_id']
|
||||
)->fetchColumn());
|
||||
self::assertSame($id, $this->service->registerFile(
|
||||
$owner,
|
||||
'physical/nested/a.stl',
|
||||
'hash',
|
||||
1,
|
||||
'other',
|
||||
'Present'
|
||||
));
|
||||
self::assertSame($before, $this->service->file($id));
|
||||
self::assertNotSame($id, $this->service->registerFile(null, 'b.stl', 'hash', 1));
|
||||
$this->service->detachFiles($owner, [$id]);
|
||||
$this->expectException(DomainException::class);
|
||||
$this->expectExceptionMessage('Managed path already records a different file.');
|
||||
$this->service->registerFile($owner, 'physical/nested/a.stl', 'hash', 1);
|
||||
}
|
||||
|
||||
#[DataProvider('invalidRegistration')]
|
||||
public function testDirectRegistrationValidation(array $arguments): void
|
||||
{
|
||||
try {
|
||||
$this->repository->recordFile(...$arguments);
|
||||
self::fail('Malformed registration must fail');
|
||||
} catch (InvalidArgumentException) {
|
||||
self::assertSame(0, (int) $this->pdo->query('SELECT COUNT(*) FROM folders')->fetchColumn());
|
||||
}
|
||||
}
|
||||
|
||||
public function testMissingOwnerIsDomainFailureWithoutFolderWrites(): void
|
||||
{
|
||||
try {
|
||||
$this->repository->recordFile(999, 'new/nested/a.stl', 'hash', 0);
|
||||
self::fail('Missing owner must fail');
|
||||
} catch (DomainException $error) {
|
||||
self::assertSame('Unknown model.', $error->getMessage());
|
||||
self::assertSame(0, (int) $this->pdo->query('SELECT COUNT(*) FROM folders')->fetchColumn());
|
||||
}
|
||||
}
|
||||
|
||||
public function testNullStoredSizeDoesNotReplayAsZero(): void
|
||||
{
|
||||
$this->repository->recordFile(null, 'a.stl', 'hash', 0);
|
||||
$this->pdo->exec('UPDATE files SET size = NULL');
|
||||
$this->expectException(DomainException::class);
|
||||
$this->repository->recordFile(null, 'a.stl', 'hash', 0);
|
||||
}
|
||||
|
||||
public function testAtomicReturnRollbackAndFailurePreservesCallerWrites(): void
|
||||
{
|
||||
self::assertSame('result', $this->service->atomic(static fn (): string => 'result'));
|
||||
$this->pdo->beginTransaction();
|
||||
$owner = $this->service->createModel('/home', 'Earlier');
|
||||
$error = new RuntimeException('later failure');
|
||||
try {
|
||||
$this->service->atomic(function () use ($owner, $error): void {
|
||||
$this->service->registerFile($owner, 'new/nested/a.stl', 'hash', 1);
|
||||
throw $error;
|
||||
});
|
||||
self::fail('Failure must escape');
|
||||
} catch (RuntimeException $caught) {
|
||||
self::assertSame($error, $caught);
|
||||
}
|
||||
self::assertTrue($this->pdo->inTransaction());
|
||||
self::assertNotNull($this->service->aggregate($owner));
|
||||
self::assertNull($this->service->fileByManagedPath('new/nested/a.stl'));
|
||||
self::assertSame(2, (int) $this->pdo->query('SELECT COUNT(*) FROM folders')->fetchColumn());
|
||||
$id = $this->service->registerFile(null, 'a.stl', 'hash', 1);
|
||||
self::assertTrue($this->pdo->inTransaction());
|
||||
$this->pdo->rollBack();
|
||||
self::assertNull($this->service->file($id));
|
||||
self::assertNull($this->service->aggregate($owner));
|
||||
}
|
||||
|
||||
public function testLookupValidationIsEnforcedByRepositoryAndService(): void
|
||||
{
|
||||
foreach ([$this->repository, $this->service] as $catalog) {
|
||||
foreach (
|
||||
[['file', 0], ['file', -1], ['fileByManagedPath', '../a'],
|
||||
['fileByManagedPath', "\xff.stl"], ['fileByManagedPath', 'a//b']] as [$method, $value]
|
||||
) {
|
||||
try {
|
||||
$catalog->$method($value);
|
||||
self::fail('Invalid lookup must fail');
|
||||
} catch (InvalidArgumentException) {
|
||||
self::assertSame(0, (int) $this->pdo->query('SELECT COUNT(*) FROM folders')->fetchColumn());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function testMetadataAndStatusBoundaryExists(): void
|
||||
{
|
||||
foreach ([$this->repository, $this->service] as $catalog) {
|
||||
foreach (['modelMetadata', 'updateModelMetadata', 'changeFileStatus'] as $method) {
|
||||
self::assertTrue(method_exists($catalog, $method), $method);
|
||||
}
|
||||
self::assertFalse(method_exists($catalog, 'setFileStatus'));
|
||||
}
|
||||
}
|
||||
|
||||
public function testMetadataRawNamedTupleNoopLegacyAndAba(): void
|
||||
{
|
||||
$id = $this->service->createModel('/home', 'T');
|
||||
$file = $this->service->registerFile($id, 'a.stl', 'hash', 1);
|
||||
$this->pdo->exec("UPDATE models SET notes = '', source_url = 'bad URL', updated_at = '2000-01-01'");
|
||||
$before = $this->service->aggregate($id);
|
||||
$raw = ['title' => 'T', 'notes' => '', 'sourceURL' => 'bad URL'];
|
||||
self::assertSame($raw, $this->repository->modelMetadata($id));
|
||||
self::assertSame($raw, $this->service->updateModelMetadata($id, array_reverse($raw, true), $raw));
|
||||
self::assertSame($before, $this->service->aggregate($id));
|
||||
$edited = array_replace($raw, ['title' => 'New']);
|
||||
self::assertSame($edited, $this->repository->updateModelMetadata($id, $raw, $edited));
|
||||
$after = $this->service->aggregate($id);
|
||||
foreach (['id', 'folder_id', 'added_at', 'notes', 'source_url'] as $key) {
|
||||
self::assertSame($before['model'][$key], $after['model'][$key]);
|
||||
}
|
||||
self::assertSame($before['files'], $after['files']);
|
||||
self::assertNotSame($before['model']['updated_at'], $after['model']['updated_at']);
|
||||
$this->service->updateModelMetadata($id, $edited, $raw);
|
||||
self::assertSame($edited, $this->service->updateModelMetadata($id, $raw, $edited));
|
||||
self::assertSame($file, $after['files'][0]['id']);
|
||||
$cleared = array_replace($edited, ['sourceURL' => null]);
|
||||
self::assertSame($cleared, $this->service->updateModelMetadata($id, $edited, $cleared));
|
||||
}
|
||||
|
||||
public function testMetadataStaleBeforeValidationOrNoopAndIndependentFieldsConflict(): void
|
||||
{
|
||||
$id = $this->service->createModel('/', 'T');
|
||||
$raw = $this->service->modelMetadata($id);
|
||||
$current = array_replace($raw, ['notes' => 'changed']);
|
||||
$this->service->updateModelMetadata($id, $raw, $current);
|
||||
foreach ([$current, array_replace($raw, ['title' => "bad\0"]), $raw] as $replacement) {
|
||||
try {
|
||||
$this->repository->updateModelMetadata($id, $raw, $replacement);
|
||||
self::fail('Stale tuple must fail');
|
||||
} catch (DomainException $error) {
|
||||
self::assertSame('Model metadata has changed.', $error->getMessage());
|
||||
}
|
||||
}
|
||||
self::assertSame($current, $this->service->modelMetadata($id));
|
||||
}
|
||||
|
||||
public function testEveryStatusPairPreservesOtherColumnsAndNoopTimestamp(): void
|
||||
{
|
||||
$id = $this->service->registerFile(null, 'a.stl', 'hash', 5, 'model/stl');
|
||||
foreach (['Present', 'Missing', 'Updated', 'Duplicate'] as $from) {
|
||||
foreach (['Present', 'Missing', 'Updated', 'Duplicate'] as $to) {
|
||||
$this->pdo->exec("UPDATE files SET status = '$from', updated_at = '2000-01-01'");
|
||||
$before = $this->service->file($id);
|
||||
$after = $this->service->changeFileStatus($id, $from, $to);
|
||||
self::assertSame($to, $after['status']);
|
||||
self::assertSame($after, $this->repository->file($id));
|
||||
if ($from === $to) {
|
||||
self::assertSame($before, $after);
|
||||
} else {
|
||||
self::assertNotSame($before['updated_at'], $after['updated_at']);
|
||||
}
|
||||
unset($before['status'], $before['updated_at'], $after['status'], $after['updated_at']);
|
||||
self::assertSame($before, $after);
|
||||
}
|
||||
}
|
||||
$this->expectException(DomainException::class);
|
||||
$this->expectExceptionMessage('File status has changed.');
|
||||
$this->repository->changeFileStatus($id, 'Present', 'Duplicate');
|
||||
}
|
||||
|
||||
public function testCreationRejectsNulThroughBothPaths(): void
|
||||
{
|
||||
foreach (['createModel', 'ensureModel'] as $method) {
|
||||
try {
|
||||
$this->repository->$method('/', "bad\0title");
|
||||
self::fail('NUL must fail');
|
||||
} catch (InvalidArgumentException) {
|
||||
self::assertSame(0, (int) $this->pdo->query('SELECT COUNT(*) FROM models')->fetchColumn());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function testMutationValidationAndMissingIdentities(): void
|
||||
{
|
||||
$tuple = ['title' => 'T', 'notes' => null, 'sourceURL' => null];
|
||||
foreach ([$this->repository, $this->service] as $catalog) {
|
||||
self::assertNull($catalog->modelMetadata(999));
|
||||
$invalid = [
|
||||
['modelMetadata', [0]], ['modelMetadata', [-1]],
|
||||
['updateModelMetadata', [0, $tuple, $tuple]],
|
||||
['updateModelMetadata', [999, [], $tuple]],
|
||||
['updateModelMetadata', [999, $tuple, []]],
|
||||
['changeFileStatus', [0, 'Present', 'Present']],
|
||||
['changeFileStatus', [999, 'invalid', 'Present']],
|
||||
['changeFileStatus', [999, 'Present', 'invalid']],
|
||||
];
|
||||
foreach ($invalid as [$method, $args]) {
|
||||
try {
|
||||
$catalog->$method(...$args);
|
||||
self::fail('Invalid input must fail');
|
||||
} catch (InvalidArgumentException) {
|
||||
self::assertNull($catalog->modelMetadata(999));
|
||||
}
|
||||
}
|
||||
foreach (
|
||||
[['updateModelMetadata', [999, $tuple, $tuple], 'Unknown model.'],
|
||||
['changeFileStatus', [999, 'Present', 'Present'], 'Unknown file.']] as [$method, $args, $message]
|
||||
) {
|
||||
try {
|
||||
$catalog->$method(...$args);
|
||||
self::fail('Missing identity must fail');
|
||||
} catch (DomainException $error) {
|
||||
self::assertSame($message, $error->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function testMetadataEffectiveNoopAndRawNullDistinction(): void
|
||||
{
|
||||
$id = $this->service->createModel('/', 'T');
|
||||
$this->pdo->exec("UPDATE models SET updated_at = '2000-01-01'");
|
||||
$raw = $this->service->modelMetadata($id);
|
||||
$before = $this->service->aggregate($id);
|
||||
self::assertSame($raw, $this->service->updateModelMetadata(
|
||||
$id,
|
||||
$raw,
|
||||
array_replace($raw, ['notes' => '', 'sourceURL' => ''])
|
||||
));
|
||||
self::assertSame($before, $this->service->aggregate($id));
|
||||
$this->pdo->exec("UPDATE models SET source_url = ''");
|
||||
try {
|
||||
$this->service->updateModelMetadata($id, $raw, $raw);
|
||||
self::fail('Raw null must not match empty string');
|
||||
} catch (DomainException $error) {
|
||||
self::assertSame('Model metadata has changed.', $error->getMessage());
|
||||
}
|
||||
$raw = $this->service->modelMetadata($id);
|
||||
self::assertSame('', $this->service->updateModelMetadata($id, $raw, $raw)['sourceURL']);
|
||||
self::assertNull($this->service->updateModelMetadata(
|
||||
$id,
|
||||
$raw,
|
||||
array_replace($raw, ['sourceURL' => null])
|
||||
)['sourceURL']);
|
||||
}
|
||||
|
||||
public function testPhysicalSnapshotsSurviveMutationsNoopsAndRejectedChanges(): void
|
||||
{
|
||||
$owner = $this->service->createModel('/', 'Multipart');
|
||||
$other = $this->service->createModel('/other', 'Other');
|
||||
$ids = $this->physicalFiles($owner);
|
||||
$snapshot = PhysicalFileSnapshot::capture($this->physicalRoot);
|
||||
$raw = $this->service->modelMetadata($owner);
|
||||
$edited = array_replace($raw, ['title' => 'Edited', 'notes' => 'Notes']);
|
||||
$operations = [
|
||||
fn () => $this->service->updateModelMetadata($owner, $raw, $edited),
|
||||
fn () => $this->service->updateModelMetadata($owner, $edited, $edited),
|
||||
fn () => $this->service->changeFileStatus($ids[0], 'Present', 'Missing'),
|
||||
fn () => $this->service->changeFileStatus($ids[0], 'Missing', 'Missing'),
|
||||
fn () => $this->service->reassignFiles($owner, $other, $ids),
|
||||
fn () => $this->repository->changeFileOwner($ids, $other, $other),
|
||||
fn () => $this->service->detachFiles($other, $ids),
|
||||
fn () => $this->repository->changeFileOwner($ids, null, null),
|
||||
fn () => $this->service->assignFiles($owner, $ids),
|
||||
];
|
||||
foreach ($operations as $index => $operation) {
|
||||
$operation();
|
||||
self::assertSame($snapshot, PhysicalFileSnapshot::capture($this->physicalRoot), "Mutation $index");
|
||||
}
|
||||
$before = $this->service->aggregate($owner);
|
||||
$rejections = [
|
||||
fn () => $this->service->updateModelMetadata($owner, $raw, $edited),
|
||||
fn () => $this->service->updateModelMetadata($owner, $edited, array_replace($edited, ['title' => ''])),
|
||||
fn () => $this->service->changeFileStatus($ids[0], 'Present', 'Missing'),
|
||||
fn () => $this->service->changeFileStatus($ids[0], 'Missing', 'invalid'),
|
||||
fn () => $this->service->reassignFiles($other, $owner, $ids),
|
||||
];
|
||||
foreach ($rejections as $index => $operation) {
|
||||
try {
|
||||
$operation();
|
||||
self::fail('Expected rejected mutation');
|
||||
} catch (DomainException | InvalidArgumentException) {
|
||||
self::assertSame($before, $this->service->aggregate($owner));
|
||||
self::assertSame($snapshot, PhysicalFileSnapshot::capture($this->physicalRoot), "Rejection $index");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function testMutationsRollbackWithCallerAndLaterSqlFailure(): void
|
||||
{
|
||||
$id = $this->service->createModel('/', 'T');
|
||||
$files = $this->physicalFiles($id);
|
||||
$file = $files[0];
|
||||
$snapshot = PhysicalFileSnapshot::capture($this->physicalRoot);
|
||||
$raw = $this->service->modelMetadata($id);
|
||||
$new = array_replace($raw, ['title' => 'New']);
|
||||
$before = $this->service->file($file);
|
||||
$this->pdo->beginTransaction();
|
||||
$earlier = $this->service->createModel('/', 'Earlier');
|
||||
$postgres = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME) === 'pgsql';
|
||||
if ($postgres) {
|
||||
$this->pdo->exec('ALTER TABLE files ADD CONSTRAINT "forced later write" CHECK (status <> \'Missing\')');
|
||||
} else {
|
||||
$this->pdo->exec("CREATE TRIGGER fail_status BEFORE UPDATE OF status ON files
|
||||
BEGIN SELECT RAISE(ABORT, 'forced later write'); END");
|
||||
}
|
||||
try {
|
||||
$this->service->atomic(function () use ($id, $file, $files, $raw, $new): void {
|
||||
$this->service->detachFiles($id, $files);
|
||||
$this->service->updateModelMetadata($id, $raw, $new);
|
||||
$this->repository->changeFileStatus($file, 'Present', 'Missing');
|
||||
});
|
||||
self::fail('SQL failure must escape');
|
||||
} catch (\PDOException $error) {
|
||||
self::assertStringContainsString('forced later write', $error->getMessage());
|
||||
}
|
||||
self::assertTrue($this->pdo->inTransaction());
|
||||
self::assertNotNull($this->service->aggregate($earlier));
|
||||
self::assertSame($raw, $this->service->modelMetadata($id));
|
||||
self::assertSame($before, $this->service->file($file));
|
||||
self::assertSame($files, array_column($this->service->aggregate($id)['files'], 'id'));
|
||||
self::assertSame($snapshot, PhysicalFileSnapshot::capture($this->physicalRoot));
|
||||
$this->pdo->exec($postgres
|
||||
? 'ALTER TABLE files DROP CONSTRAINT "forced later write"'
|
||||
: 'DROP TRIGGER fail_status');
|
||||
$this->service->updateModelMetadata($id, $raw, $new);
|
||||
$this->service->changeFileStatus($file, 'Present', 'Missing');
|
||||
$this->service->detachFiles($id, $files);
|
||||
self::assertSame($snapshot, PhysicalFileSnapshot::capture($this->physicalRoot));
|
||||
self::assertTrue($this->pdo->inTransaction());
|
||||
$this->pdo->rollBack();
|
||||
self::assertSame($raw, $this->service->modelMetadata($id));
|
||||
self::assertSame($before, $this->service->file($file));
|
||||
self::assertNull($this->service->aggregate($earlier));
|
||||
self::assertSame($files, array_column($this->service->aggregate($id)['files'], 'id'));
|
||||
self::assertSame($snapshot, PhysicalFileSnapshot::capture($this->physicalRoot));
|
||||
}
|
||||
|
||||
public static function invalidRegistration(): array
|
||||
{
|
||||
return array_map(static fn (array $args): array => [$args], [
|
||||
[null, 'a.stl', '', 0], [null, 'a.stl', str_repeat('a', 65), 0],
|
||||
[null, 'a.stl', "bad\0hash", 0], [null, 'a.stl', "\xff", 0],
|
||||
[null, 'a.stl', 'hash', -1], [0, 'a.stl', 'hash', 0],
|
||||
[null, 'a.stl', 'hash', 0, str_repeat('a', 129)],
|
||||
[null, 'a.stl', 'hash', 0, "bad\0mime"], [null, 'a.stl', 'hash', 0, "\xff"],
|
||||
[null, "\xff.stl", 'hash', 0], [null, '../a.stl', 'hash', 0],
|
||||
[null, 'a.stl', 'hash', 0, null, 'invalid'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user