Deploy HickLab3D Dashboard / Deploy to production (push) Failing after 23s
Replace the old touchmark/PNG identity with the supplied lockup and monogram, segmented role-aware navigation, Live/Draft listing filters with generation-based cache invalidation, and admin Projects covering immutable file revisions, listing live pointers, and exact-decimal filament/packaging/shipping costing. Additive schema only; no Etsy publication. Isolated clone tests: 301 tests, 1162 assertions.
593 lines
23 KiB
PHP
593 lines
23 KiB
PHP
<?php
|
|
|
|
namespace Tests\Unit;
|
|
|
|
use App\Services\ManufacturingFileValidator;
|
|
use Illuminate\Http\UploadedFile;
|
|
use RuntimeException;
|
|
use Tests\TestCase;
|
|
use ZipArchive;
|
|
|
|
class ManufacturingFileValidatorTest extends TestCase
|
|
{
|
|
private ManufacturingFileValidator $validator;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
$this->validator = new ManufacturingFileValidator;
|
|
}
|
|
|
|
private function uploadedFile(string $path, string $clientName): UploadedFile
|
|
{
|
|
return new UploadedFile($path, $clientName, null, null, true);
|
|
}
|
|
|
|
public function test_accepts_valid_ascii_stl(): void
|
|
{
|
|
$path = tempnam(sys_get_temp_dir(), 'stl');
|
|
file_put_contents($path, "solid cube\nfacet normal 0 0 0\nouter loop\nvertex 0 0 0\nvertex 1 0 0\nvertex 0 1 0\nendloop\nendfacet\nendsolid cube\n");
|
|
|
|
$result = $this->validator->validate($this->uploadedFile($path, 'cube.stl'));
|
|
|
|
$this->assertSame('stl', $result['extension']);
|
|
unlink($path);
|
|
}
|
|
|
|
public function test_accepts_valid_binary_stl(): void
|
|
{
|
|
$path = $this->makeBinaryStl(triangles: 2);
|
|
|
|
$result = $this->validator->validate($this->uploadedFile($path, 'part.stl'));
|
|
|
|
$this->assertSame('stl', $result['extension']);
|
|
unlink($path);
|
|
}
|
|
|
|
public function test_rejects_binary_stl_with_triangle_count_mismatch(): void
|
|
{
|
|
$path = $this->makeBinaryStl(triangles: 2, declareExtraTriangle: true);
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
try {
|
|
$this->validator->validate($this->uploadedFile($path, 'part.stl'));
|
|
} finally {
|
|
unlink($path);
|
|
}
|
|
}
|
|
|
|
public function test_rejects_ascii_stl_with_zero_facets(): void
|
|
{
|
|
// The exact review example: a single line with "solid" and "endsolid"
|
|
// tokens but no actual facet/vertex data must not be accepted.
|
|
$path = tempnam(sys_get_temp_dir(), 'stl');
|
|
file_put_contents($path, "solid garbage endsolid\n");
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
try {
|
|
$this->validator->validate($this->uploadedFile($path, 'garbage.stl'));
|
|
} finally {
|
|
unlink($path);
|
|
}
|
|
}
|
|
|
|
public function test_accepts_ascii_stl_with_multiple_facets(): void
|
|
{
|
|
$facet = "facet normal 0 0 1\nouter loop\nvertex 0 0 0\nvertex 1 0 0\nvertex 0 1 0\nendloop\nendfacet\n";
|
|
$path = tempnam(sys_get_temp_dir(), 'stl');
|
|
file_put_contents($path, "solid cube\n".$facet.$facet."endsolid cube\n");
|
|
|
|
$result = $this->validator->validate($this->uploadedFile($path, 'cube.stl'));
|
|
|
|
$this->assertSame('stl', $result['extension']);
|
|
unlink($path);
|
|
}
|
|
|
|
public function test_rejects_ascii_stl_missing_outer_loop(): void
|
|
{
|
|
$path = tempnam(sys_get_temp_dir(), 'stl');
|
|
file_put_contents($path, "solid cube\nfacet normal 0 0 1\nvertex 0 0 0\nvertex 1 0 0\nvertex 0 1 0\nendloop\nendfacet\nendsolid cube\n");
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
try {
|
|
$this->validator->validate($this->uploadedFile($path, 'cube.stl'));
|
|
} finally {
|
|
unlink($path);
|
|
}
|
|
}
|
|
|
|
public function test_rejects_ascii_stl_with_non_finite_vertex_value(): void
|
|
{
|
|
$path = tempnam(sys_get_temp_dir(), 'stl');
|
|
file_put_contents($path, "solid cube\nfacet normal 0 0 1\nouter loop\nvertex nan 0 0\nvertex 1 0 0\nvertex 0 1 0\nendloop\nendfacet\nendsolid cube\n");
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
try {
|
|
$this->validator->validate($this->uploadedFile($path, 'cube.stl'));
|
|
} finally {
|
|
unlink($path);
|
|
}
|
|
}
|
|
|
|
public function test_rejects_ascii_stl_missing_a_vertex(): void
|
|
{
|
|
$path = tempnam(sys_get_temp_dir(), 'stl');
|
|
file_put_contents($path, "solid cube\nfacet normal 0 0 1\nouter loop\nvertex 0 0 0\nvertex 1 0 0\nendloop\nendfacet\nendsolid cube\n");
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
try {
|
|
$this->validator->validate($this->uploadedFile($path, 'cube.stl'));
|
|
} finally {
|
|
unlink($path);
|
|
}
|
|
}
|
|
|
|
public function test_rejects_binary_stl_with_zero_triangles(): void
|
|
{
|
|
$path = tempnam(sys_get_temp_dir(), 'stl');
|
|
file_put_contents($path, str_repeat('A', 80).pack('V', 0));
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
try {
|
|
$this->validator->validate($this->uploadedFile($path, 'empty.stl'));
|
|
} finally {
|
|
unlink($path);
|
|
}
|
|
}
|
|
|
|
public function test_rejects_binary_stl_with_non_finite_values(): void
|
|
{
|
|
$path = tempnam(sys_get_temp_dir(), 'stl');
|
|
$handle = fopen($path, 'wb');
|
|
fwrite($handle, str_repeat('A', 80));
|
|
fwrite($handle, pack('V', 1));
|
|
fwrite($handle, str_repeat(pack('f', NAN), 12));
|
|
fwrite($handle, pack('v', 0));
|
|
fclose($handle);
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
try {
|
|
$this->validator->validate($this->uploadedFile($path, 'nan.stl'));
|
|
} finally {
|
|
unlink($path);
|
|
}
|
|
}
|
|
|
|
public function test_rejects_binary_stl_with_absurd_triangle_count(): void
|
|
{
|
|
$path = tempnam(sys_get_temp_dir(), 'stl');
|
|
$header = str_repeat('A', 80);
|
|
file_put_contents($path, $header.pack('V', 100_000_000));
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
try {
|
|
$this->validator->validate($this->uploadedFile($path, 'huge.stl'));
|
|
} finally {
|
|
unlink($path);
|
|
}
|
|
}
|
|
|
|
public function test_rejects_file_with_mismatched_extension_and_content(): void
|
|
{
|
|
$path = tempnam(sys_get_temp_dir(), 'fake');
|
|
file_put_contents($path, "G1 X10 Y10\nG1 X20 Y20\n");
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
try {
|
|
$this->validator->validate($this->uploadedFile($path, 'fake.stl'));
|
|
} finally {
|
|
unlink($path);
|
|
}
|
|
}
|
|
|
|
public function test_rejects_unsupported_extension(): void
|
|
{
|
|
$path = tempnam(sys_get_temp_dir(), 'exe');
|
|
file_put_contents($path, 'MZ'.str_repeat("\x00", 100));
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
try {
|
|
$this->validator->validate($this->uploadedFile($path, 'payload.exe'));
|
|
} finally {
|
|
unlink($path);
|
|
}
|
|
}
|
|
|
|
public function test_accepts_plain_text_gcode(): void
|
|
{
|
|
$path = tempnam(sys_get_temp_dir(), 'gcode');
|
|
file_put_contents($path, "; generated by slicer\nG28\nG1 X0 Y0 Z0.2 F3000\nM104 S200\n");
|
|
|
|
$result = $this->validator->validate($this->uploadedFile($path, 'part.gcode'));
|
|
|
|
$this->assertSame('gcode', $result['extension']);
|
|
unlink($path);
|
|
}
|
|
|
|
public function test_rejects_gcode_containing_binary_bytes(): void
|
|
{
|
|
$path = tempnam(sys_get_temp_dir(), 'gcode');
|
|
file_put_contents($path, "G1 X0\0\x01\x02 binary disguised as text");
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
try {
|
|
$this->validator->validate($this->uploadedFile($path, 'part.gcode'));
|
|
} finally {
|
|
unlink($path);
|
|
}
|
|
}
|
|
|
|
public function test_accepts_valid_3mf_archive(): void
|
|
{
|
|
$path = $this->make3mf([
|
|
'[Content_Types].xml' => '<?xml version="1.0"?><Types xmlns="x"/>',
|
|
'3D/3dmodel.model' => '<?xml version="1.0"?><model xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02"><resources></resources><build></build></model>',
|
|
'_rels/.rels' => '<?xml version="1.0"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Target="/3D/3dmodel.model" Id="rel0" Type="y"/></Relationships>',
|
|
]);
|
|
|
|
$result = $this->validator->validate($this->uploadedFile($path, 'part.3mf'));
|
|
|
|
$this->assertSame('3mf', $result['extension']);
|
|
unlink($path);
|
|
}
|
|
|
|
public function test_rejects_3mf_model_missing_required_resources_or_build_elements(): void
|
|
{
|
|
$path = $this->make3mf([
|
|
'[Content_Types].xml' => '<?xml version="1.0"?><Types xmlns="x"/>',
|
|
'3D/3dmodel.model' => '<?xml version="1.0"?><model xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02"></model>',
|
|
'_rels/.rels' => '<?xml version="1.0"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Target="/3D/3dmodel.model" Id="rel0" Type="y"/></Relationships>',
|
|
]);
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
try {
|
|
$this->validator->validate($this->uploadedFile($path, 'incomplete.3mf'));
|
|
} finally {
|
|
unlink($path);
|
|
}
|
|
}
|
|
|
|
public function test_rejects_3mf_model_with_malformed_not_well_formed_xml(): void
|
|
{
|
|
$path = $this->make3mf([
|
|
'[Content_Types].xml' => '<?xml version="1.0"?><Types xmlns="x"/>',
|
|
'3D/3dmodel.model' => '<?xml version="1.0"?><model xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02"><resources></resources><build></model>',
|
|
'_rels/.rels' => '<?xml version="1.0"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Target="/3D/3dmodel.model" Id="rel0" Type="y"/></Relationships>',
|
|
]);
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
try {
|
|
$this->validator->validate($this->uploadedFile($path, 'malformed.3mf'));
|
|
} finally {
|
|
unlink($path);
|
|
}
|
|
}
|
|
|
|
public function test_rejects_3mf_relationship_with_single_quoted_external_target(): void
|
|
{
|
|
// The old validator only regex-matched double-quoted Target="..." attributes,
|
|
// so a single-quoted external target slipped straight through unparsed.
|
|
$path = $this->make3mf([
|
|
'[Content_Types].xml' => '<?xml version="1.0"?><Types xmlns="x"/>',
|
|
'3D/3dmodel.model' => '<?xml version="1.0"?><model xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02"><resources></resources><build></build></model>',
|
|
'_rels/.rels' => "<?xml version='1.0'?><Relationships xmlns='http://schemas.openxmlformats.org/package/2006/relationships'><Relationship Target='https://evil.example.test/payload' Id='rel0' Type='y'/></Relationships>",
|
|
]);
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
try {
|
|
$this->validator->validate($this->uploadedFile($path, 'evil.3mf'));
|
|
} finally {
|
|
unlink($path);
|
|
}
|
|
}
|
|
|
|
public function test_rejects_3mf_relationship_with_external_target_mode_regardless_of_target_value(): void
|
|
{
|
|
$path = $this->make3mf([
|
|
'[Content_Types].xml' => '<?xml version="1.0"?><Types xmlns="x"/>',
|
|
'3D/3dmodel.model' => '<?xml version="1.0"?><model xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02"><resources></resources><build></build></model>',
|
|
'_rels/.rels' => '<?xml version="1.0"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Target="safe/internal/part.xml" TargetMode="External" Id="rel0" Type="y"/></Relationships>',
|
|
]);
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
try {
|
|
$this->validator->validate($this->uploadedFile($path, 'evil.3mf'));
|
|
} finally {
|
|
unlink($path);
|
|
}
|
|
}
|
|
|
|
public function test_rejects_3mf_nested_rels_part_with_external_target(): void
|
|
{
|
|
// Per-part relationship files (not just the root _rels/.rels) must be
|
|
// inspected too; a nested one pointing outside the package must still
|
|
// be rejected.
|
|
$path = $this->make3mf([
|
|
'[Content_Types].xml' => '<?xml version="1.0"?><Types xmlns="x"/>',
|
|
'3D/3dmodel.model' => '<?xml version="1.0"?><model xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02"><resources></resources><build></build></model>',
|
|
'_rels/.rels' => '<?xml version="1.0"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Target="/3D/3dmodel.model" Id="rel0" Type="y"/></Relationships>',
|
|
'3D/_rels/3dmodel.model.rels' => '<?xml version="1.0"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Target="http://evil.example.test/texture.png" Id="rel1" Type="y"/></Relationships>',
|
|
]);
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
try {
|
|
$this->validator->validate($this->uploadedFile($path, 'evil.3mf'));
|
|
} finally {
|
|
unlink($path);
|
|
}
|
|
}
|
|
|
|
public function test_rejects_3mf_with_an_encrypted_entry(): void
|
|
{
|
|
$zip = new ZipArchive;
|
|
$path = tempnam(sys_get_temp_dir(), 'tmf').'.3mf';
|
|
$zip->open($path, ZipArchive::CREATE);
|
|
$zip->addFromString('[Content_Types].xml', '<?xml version="1.0"?><Types xmlns="x"/>');
|
|
$zip->addFromString('3D/3dmodel.model', '<?xml version="1.0"?><model xmlns="x"><resources></resources><build></build></model>');
|
|
$zip->addFromString('secret.bin', 'payload');
|
|
$zip->setEncryptionName('secret.bin', ZipArchive::EM_AES_256, 'password');
|
|
$zip->close();
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
try {
|
|
$this->validator->validate($this->uploadedFile($path, 'evil.3mf'));
|
|
} finally {
|
|
unlink($path);
|
|
}
|
|
}
|
|
|
|
public function test_rejects_3mf_entry_exceeding_the_per_entry_xml_size_cap(): void
|
|
{
|
|
// Low-compressibility padding keeps this under the ratio-bomb threshold
|
|
// while still being an individually oversized XML-ish part.
|
|
$zip = new ZipArchive;
|
|
$path = tempnam(sys_get_temp_dir(), 'tmf').'.3mf';
|
|
$zip->open($path, ZipArchive::CREATE);
|
|
$zip->addFromString('[Content_Types].xml', '<?xml version="1.0"?><Types xmlns="x"/>');
|
|
$zip->addFromString('3D/3dmodel.model', '<?xml version="1.0"?><model xmlns="x"><resources></resources><build></build></model>');
|
|
$zip->addFromString('_rels/.rels', '<?xml version="1.0"?><Relationships xmlns="x"><Relationship Target="/3D/3dmodel.model" Id="rel0" Type="y"/></Relationships>');
|
|
$zip->addFromString('huge.xml', '<x>'.base64_encode(random_bytes(18 * 1024 * 1024)).'</x>');
|
|
$zip->setCompressionName('huge.xml', ZipArchive::CM_STORE);
|
|
$zip->close();
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
try {
|
|
$this->validator->validate($this->uploadedFile($path, 'huge.3mf'));
|
|
} finally {
|
|
unlink($path);
|
|
}
|
|
}
|
|
|
|
public function test_rejects_3mf_with_a_duplicate_entry_path(): void
|
|
{
|
|
// ZipArchive's own API dedupes entries by name, so a genuine duplicate
|
|
// (two local file headers + two central directory records sharing one
|
|
// name — a known zip-parser confusion vector) has to be hand-built.
|
|
$path = tempnam(sys_get_temp_dir(), 'tmf').'.3mf';
|
|
file_put_contents($path, $this->rawZipWithDuplicateEntryName());
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
try {
|
|
$this->validator->validate($this->uploadedFile($path, 'dup.3mf'));
|
|
} finally {
|
|
unlink($path);
|
|
}
|
|
}
|
|
|
|
private function rawZipWithDuplicateEntryName(): string
|
|
{
|
|
// An otherwise entirely valid, acceptable 3MF archive, plus a duplicate
|
|
// pair of an unrelated entry name — so this only fails if duplicate
|
|
// detection itself exists, not for any other unrelated reason.
|
|
$entries = [
|
|
['[Content_Types].xml', '<?xml version="1.0"?><Types xmlns="x"/>'],
|
|
['3D/3dmodel.model', '<?xml version="1.0"?><model xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02"><resources></resources><build></build></model>'],
|
|
['_rels/.rels', '<?xml version="1.0"?><Relationships xmlns="x"><Relationship Target="/3D/3dmodel.model" Id="rel0" Type="y"/></Relationships>'],
|
|
['same.txt', 'first'],
|
|
['same.txt', 'second'],
|
|
];
|
|
|
|
$local = '';
|
|
$central = '';
|
|
$offset = 0;
|
|
foreach ($entries as [$name, $content]) {
|
|
$crc = crc32($content);
|
|
$len = strlen($content);
|
|
$nameLen = strlen($name);
|
|
$lfh = pack('VvvvvvVVVvv', 0x04034B50, 20, 0, 0, 0, 0, $crc, $len, $len, $nameLen, 0).$name.$content;
|
|
$cdfh = pack('VvvvvvvVVVvvvvvVV', 0x02014B50, 20, 20, 0, 0, 0, 0, $crc, $len, $len, $nameLen, 0, 0, 0, 0, 0, $offset).$name;
|
|
$local .= $lfh;
|
|
$central .= $cdfh;
|
|
$offset += strlen($lfh);
|
|
}
|
|
|
|
$eocd = pack('VvvvvVVv', 0x06054B50, 0, 0, count($entries), count($entries), strlen($central), $offset, 0);
|
|
|
|
return $local.$central.$eocd;
|
|
}
|
|
|
|
public function test_rejects_3mf_missing_required_model_part(): void
|
|
{
|
|
$path = $this->make3mf([
|
|
'[Content_Types].xml' => '<?xml version="1.0"?><Types xmlns="x"/>',
|
|
]);
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
try {
|
|
$this->validator->validate($this->uploadedFile($path, 'broken.3mf'));
|
|
} finally {
|
|
unlink($path);
|
|
}
|
|
}
|
|
|
|
public function test_rejects_3mf_with_path_traversal_entry(): void
|
|
{
|
|
$path = $this->make3mf([
|
|
'[Content_Types].xml' => '<?xml version="1.0"?><Types xmlns="x"/>',
|
|
'3D/3dmodel.model' => '<?xml version="1.0"?><model xmlns="x"></model>',
|
|
'../../../etc/evil.txt' => 'pwned',
|
|
]);
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
try {
|
|
$this->validator->validate($this->uploadedFile($path, 'evil.3mf'));
|
|
} finally {
|
|
unlink($path);
|
|
}
|
|
}
|
|
|
|
public function test_rejects_3mf_with_absolute_path_entry(): void
|
|
{
|
|
$zip = new ZipArchive;
|
|
$path = tempnam(sys_get_temp_dir(), 'tmf').'.3mf';
|
|
$zip->open($path, ZipArchive::CREATE);
|
|
$zip->addFromString('[Content_Types].xml', '<?xml version="1.0"?><Types xmlns="x"/>');
|
|
$zip->addFromString('3D/3dmodel.model', '<?xml version="1.0"?><model xmlns="x"></model>');
|
|
// ZipArchive normalises leading slashes away, so write the entry via a raw local file header hack:
|
|
// fall back to asserting the validator rejects entries containing backslashes / drive letters instead.
|
|
$zip->addFromString('C:\\Windows\\evil.dll', 'pwned');
|
|
$zip->close();
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
try {
|
|
$this->validator->validate($this->uploadedFile($path, 'evil.3mf'));
|
|
} finally {
|
|
unlink($path);
|
|
}
|
|
}
|
|
|
|
public function test_rejects_3mf_model_part_with_doctype_declaration(): void
|
|
{
|
|
$path = $this->make3mf([
|
|
'[Content_Types].xml' => '<?xml version="1.0"?><Types xmlns="x"/>',
|
|
'3D/3dmodel.model' => '<?xml version="1.0"?><!DOCTYPE model [<!ENTITY xxe SYSTEM "file:///etc/passwd">]><model xmlns="x">&xxe;</model>',
|
|
]);
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
try {
|
|
$this->validator->validate($this->uploadedFile($path, 'xxe.3mf'));
|
|
} finally {
|
|
unlink($path);
|
|
}
|
|
}
|
|
|
|
public function test_rejects_3mf_relationship_with_external_target(): void
|
|
{
|
|
$path = $this->make3mf([
|
|
'[Content_Types].xml' => '<?xml version="1.0"?><Types xmlns="x"/>',
|
|
'3D/3dmodel.model' => '<?xml version="1.0"?><model xmlns="x"></model>',
|
|
'_rels/.rels' => '<?xml version="1.0"?><Relationships xmlns="x"><Relationship Target="https://evil.example.test/payload" Id="rel0" Type="y"/></Relationships>',
|
|
]);
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
try {
|
|
$this->validator->validate($this->uploadedFile($path, 'evil.3mf'));
|
|
} finally {
|
|
unlink($path);
|
|
}
|
|
}
|
|
|
|
public function test_rejects_3mf_with_symlink_entry(): void
|
|
{
|
|
$zip = new ZipArchive;
|
|
$path = tempnam(sys_get_temp_dir(), 'tmf').'.3mf';
|
|
$zip->open($path, ZipArchive::CREATE);
|
|
$zip->addFromString('[Content_Types].xml', '<?xml version="1.0"?><Types xmlns="x"/>');
|
|
$zip->addFromString('3D/3dmodel.model', '<?xml version="1.0"?><model xmlns="x"></model>');
|
|
$zip->addFromString('evil-link', '/etc/passwd');
|
|
$zip->setExternalAttributesName('evil-link', ZipArchive::OPSYS_UNIX, 0120777 << 16);
|
|
$zip->close();
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
try {
|
|
$this->validator->validate($this->uploadedFile($path, 'evil.3mf'));
|
|
} finally {
|
|
unlink($path);
|
|
}
|
|
}
|
|
|
|
public function test_rejects_3mf_with_too_many_entries(): void
|
|
{
|
|
$entries = ['[Content_Types].xml' => '<?xml version="1.0"?><Types xmlns="x"/>', '3D/3dmodel.model' => '<?xml version="1.0"?><model xmlns="x"></model>'];
|
|
for ($i = 0; $i < 5000; $i++) {
|
|
$entries["junk/{$i}.txt"] = 'x';
|
|
}
|
|
$path = $this->make3mf($entries);
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
try {
|
|
$this->validator->validate($this->uploadedFile($path, 'bomb.3mf'));
|
|
} finally {
|
|
unlink($path);
|
|
}
|
|
}
|
|
|
|
public function test_rejects_3mf_entry_with_extreme_compression_ratio(): void
|
|
{
|
|
$zip = new ZipArchive;
|
|
$path = tempnam(sys_get_temp_dir(), 'tmf').'.3mf';
|
|
$zip->open($path, ZipArchive::CREATE);
|
|
$zip->addFromString('[Content_Types].xml', '<?xml version="1.0"?><Types xmlns="x"/>');
|
|
$zip->addFromString('3D/3dmodel.model', '<?xml version="1.0"?><model xmlns="x"></model>');
|
|
$zip->addFromString('bomb.bin', str_repeat("\0", 20 * 1024 * 1024));
|
|
$zip->setCompressionName('bomb.bin', ZipArchive::CM_DEFLATE, 9);
|
|
$zip->close();
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
try {
|
|
$this->validator->validate($this->uploadedFile($path, 'bomb.3mf'));
|
|
} finally {
|
|
unlink($path);
|
|
}
|
|
}
|
|
|
|
public function test_rejects_file_exceeding_maximum_size(): void
|
|
{
|
|
$path = tempnam(sys_get_temp_dir(), 'stl');
|
|
$handle = fopen($path, 'wb');
|
|
fwrite($handle, str_repeat('A', 80));
|
|
fwrite($handle, pack('V', 1));
|
|
fseek($handle, (100 * 1024 * 1024) + 200);
|
|
fwrite($handle, 'x');
|
|
fclose($handle);
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
try {
|
|
$this->validator->validate($this->uploadedFile($path, 'huge.stl'));
|
|
} finally {
|
|
unlink($path);
|
|
}
|
|
}
|
|
|
|
private function makeBinaryStl(int $triangles, bool $declareExtraTriangle = false): string
|
|
{
|
|
$path = tempnam(sys_get_temp_dir(), 'stl');
|
|
$handle = fopen($path, 'wb');
|
|
fwrite($handle, str_repeat('A', 80));
|
|
fwrite($handle, pack('V', $declareExtraTriangle ? $triangles + 1 : $triangles));
|
|
for ($i = 0; $i < $triangles; $i++) {
|
|
fwrite($handle, str_repeat(pack('f', 0.0), 12));
|
|
fwrite($handle, pack('v', 0));
|
|
}
|
|
fclose($handle);
|
|
|
|
return $path;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, string> $entries
|
|
*/
|
|
private function make3mf(array $entries): string
|
|
{
|
|
$zip = new ZipArchive;
|
|
$path = tempnam(sys_get_temp_dir(), 'tmf').'.3mf';
|
|
$zip->open($path, ZipArchive::CREATE);
|
|
foreach ($entries as $name => $contents) {
|
|
$zip->addFromString($name, $contents);
|
|
}
|
|
$zip->close();
|
|
|
|
return $path;
|
|
}
|
|
}
|