Skip to content

Commit a2b4985

Browse files
authored
Merge pull request #26 from xp-forge/feature/symlinks
Resolve paths in symlinks before adding to ZIP file
2 parents ba8c5ca + 736bd1f commit a2b4985

File tree

3 files changed

+244
-13
lines changed

3 files changed

+244
-13
lines changed

src/main/php/xp/lambda/PackageLambda.class.php

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
use io\streams\StreamTransfer;
66
use util\cmd\Console;
77

8+
/** @test com.amazon.aws.lambda.unittest.PackagingTest */
89
class PackageLambda {
9-
const COMPRESSION_THRESHOLD = 24;
10+
const COMPRESSION_THRESHOLD= 24;
1011

1112
private $target, $sources, $exclude, $compression;
1213

@@ -26,11 +27,31 @@ public function __construct(Path $target, Sources $sources, string $exclude= '#(
2627
}
2728

2829
/** Adds ZIP file entries */
29-
private function add(ZipArchiveWriter $zip, Path $path) {
30+
private function add(ZipArchiveWriter $zip, Path $path, Path $base, $prefix= '') {
3031
if (preg_match($this->exclude, $path->toString('/'))) return;
3132

32-
$relative= $path->relativeTo($this->sources->base);
33-
if ($path->isFile()) {
33+
// Check if the given path exists, suppressing any warnings
34+
if (false === ($stat= lstat($path))) {
35+
\xp::gc(__FILE__, __LINE__ - 1);
36+
return;
37+
}
38+
39+
// Handle the following file types:
40+
// - Links: Resolve, then handle link targets
41+
// - Files: Add to ZIP
42+
// - Folders: Recursively add all subfolders and files therein
43+
$relative= $prefix.$path->relativeTo($base);
44+
if (Sources::IS_LINK === ($stat['mode'] & Sources::IS_LINK)) {
45+
$target= new Path(readlink($path));
46+
$resolved= Path::real($target->isAbsolute() ? $target : [$path->parent(), $target], $base);
47+
if ($resolved->isFile()) {
48+
$base= new Path(dirname($resolved));
49+
$relative= dirname($relative);
50+
} else {
51+
$base= $resolved;
52+
}
53+
yield from $this->add($zip, $resolved, $base, $relative.DIRECTORY_SEPARATOR);
54+
} else if (Sources::IS_FILE === ($stat['mode'] & Sources::IS_FILE)) {
3455
$file= $zip->add(new ZipFileEntry($relative));
3556

3657
// See https://stackoverflow.com/questions/46716095/minimum-file-size-for-compression-algorithms
@@ -39,10 +60,10 @@ private function add(ZipArchiveWriter $zip, Path $path) {
3960
}
4061
(new StreamTransfer($path->asFile()->in(), $file->out()))->transferAll();
4162
yield $file;
42-
} else {
63+
} else if (Sources::IS_FOLDER === ($stat['mode'] & Sources::IS_FOLDER)) {
4364
yield $zip->add(new ZipDirEntry($relative));
4465
foreach ($path->asFolder()->entries() as $entry) {
45-
yield from $this->add($zip, $entry);
66+
yield from $this->add($zip, $entry, $base, $prefix);
4667
}
4768
}
4869
}
@@ -52,21 +73,27 @@ public function run(): int {
5273
$z= ZipFile::create($this->target->asFile()->out());
5374

5475
$sources= iterator_to_array($this->sources);
55-
$total= sizeof($sources) + 1;
76+
$total= sizeof($sources);
77+
78+
// Add class path file to root if existant
79+
$p= new Path($this->sources->base, 'class.pth');
80+
if ($p->exists()) {
81+
$file= $z->add(new ZipFileEntry('class.pth'));
82+
$file->out()->write(preg_replace($this->exclude.'m', '?$1', file_get_contents($p)));
83+
Console::writeLinef("\e[34m => [1/%d] class.pth\e[0m", ++$total);
84+
}
85+
86+
// Add all other sources
5687
foreach ($sources as $i => $source) {
5788
Console::writef("\e[34m => [%d/%d] ", $i + 1, $total);
5889
$entries= 0;
59-
foreach ($this->add($z, new Path($source)) as $entry) {
90+
foreach ($this->add($z, new Path($source), $this->sources->base) as $entry) {
6091
$entries++;
6192
Console::writef('%-60s %4d%s', substr($entry->getName(), -60), $entries, str_repeat("\x08", 65));
6293
}
6394
Console::writeLine("\e[0m");
6495
}
6596

66-
$file= $z->add(new ZipFileEntry('class.pth'));
67-
$file->out()->write(preg_replace($this->exclude.'m', '?$1', file_get_contents('class.pth')));
68-
Console::writeLinef("\e[34m => [%1\$d/%1\$d] class.pth\e[0m", $total);
69-
7097
$z->close();
7198
Console::writeLine();
7299
Console::writeLine('Wrote ', number_format(filesize($this->target)), ' bytes');

src/main/php/xp/lambda/Sources.class.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55

66
/** Returns a unique list of sources */
77
class Sources implements IteratorAggregate {
8+
9+
// See https://www.php.net/manual/en/function.fileperms.php
10+
const IS_LINK= 0120000;
11+
const IS_FILE= 0100000;
12+
const IS_FOLDER= 0040000;
13+
814
public $base;
915
private $sources;
1016

@@ -16,7 +22,7 @@ public function __construct(Path $base, array $sources) {
1622
public function getIterator(): Traversable {
1723
$seen= [];
1824
foreach ($this->sources as $source) {
19-
$path= ($source instanceof Path ? $source : new Path($source))->asRealpath();
25+
$path= ($source instanceof Path ? $source : new Path($source))->asRealpath($this->base);
2026

2127
$key= $path->hashCode();
2228
if (isset($seen[$key])) continue;
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
<?php namespace com\amazon\aws\lambda\unittest;
2+
3+
use io\archive\zip\{ZipFile, ZipIterator};
4+
use io\streams\MemoryOutputStream;
5+
use io\{File, Files, Folder, Path};
6+
use lang\Environment;
7+
use test\verify\Runtime;
8+
use test\{After, Assert, Test, Values};
9+
use util\cmd\Console;
10+
use xp\lambda\{PackageLambda, Sources};
11+
12+
class PackagingTest {
13+
private $archives= [], $cleanup= [];
14+
15+
/** Creates a new temporary folder */
16+
private function tempDir(): Folder {
17+
$this->cleanup[]= $f= new Folder(Environment::tempDir(), uniqid());
18+
$f->create();
19+
return $f;
20+
}
21+
22+
/** @return void */
23+
private function removeDir(Folder $folder) {
24+
foreach ($folder->entries() as $entry) {
25+
switch ($m= lstat($entry)['mode'] & 0170000) {
26+
case Sources::IS_LINK: unlink($entry); break;
27+
case Sources::IS_FILE: $entry->asFile()->unlink(); break;
28+
case Sources::IS_FOLDER: $this->removeDir($entry->asFolder()); break;
29+
}
30+
}
31+
}
32+
33+
/** Creates files and directory from given definitions */
34+
private function create(array $definitions, Folder $folder= null): Path {
35+
$folder ?? $folder= $this->tempDir();
36+
37+
// Create sources from definitions
38+
foreach ($definitions as $name => $definition) {
39+
switch ($definition[0]) {
40+
case Sources::IS_FILE:
41+
Files::write(new File($folder, $name), $definition[1]);
42+
break;
43+
44+
case Sources::IS_FOLDER:
45+
(new Folder($folder, $name))->create($definition[1]);
46+
break;
47+
48+
case Sources::IS_LINK:
49+
symlink($definition[1], new Path($folder, $name));
50+
break;
51+
}
52+
}
53+
54+
return new Path($folder);
55+
}
56+
57+
/** Creates package from given sources */
58+
private function package(Sources $sources): ZipIterator {
59+
60+
// Run packaging command
61+
$target= new Path($this->tempDir(), 'test.zip');
62+
$out= Console::$out->stream();
63+
Console::$out->redirect(new MemoryOutputStream());
64+
try {
65+
$cmd= new PackageLambda($target, $sources);
66+
$cmd->run();
67+
} finally {
68+
Console::$out->redirect($out);
69+
}
70+
71+
// Remember to close the archive
72+
$this->archives[]= $zip= ZipFile::open($target);
73+
return $zip->iterator();
74+
}
75+
76+
#[After]
77+
private function cleanup() {
78+
foreach ($this->files as $file) {
79+
$file->close();
80+
}
81+
foreach ($this->cleanup as $folder) {
82+
$this->removeDir($folder);
83+
}
84+
}
85+
86+
#[Test]
87+
public function single_file() {
88+
$zip= $this->package(new Sources($this->create(['file.txt' => [Sources::IS_FILE, 'Test']]), ['file.txt']));
89+
90+
$file= $zip->next();
91+
Assert::equals('file.txt', $file->getName());
92+
Assert::equals(4, $file->getSize());
93+
Assert::false($zip->hasNext());
94+
}
95+
96+
#[Test]
97+
public function single_directory() {
98+
$zip= $this->package(new Sources($this->create(['src' => [Sources::IS_FOLDER, 0755]]), ['src']));
99+
100+
$dir= $zip->next();
101+
Assert::equals('src/', $dir->getName());
102+
Assert::true($dir->isDirectory());
103+
Assert::false($zip->hasNext());
104+
}
105+
106+
#[Test]
107+
public function file_inside_directory() {
108+
$path= $this->create([
109+
'src' => [Sources::IS_FOLDER, 0755],
110+
'src/file.txt' => [Sources::IS_FILE, 'Test']
111+
]);
112+
$zip= $this->package(new Sources($path, ['src']));
113+
114+
$dir= $zip->next();
115+
Assert::equals('src/', $dir->getName());
116+
Assert::true($dir->isDirectory());
117+
118+
$file= $zip->next();
119+
Assert::equals('src/file.txt', $file->getName());
120+
Assert::equals(4, $file->getSize());
121+
122+
Assert::false($zip->hasNext());
123+
}
124+
125+
#[Test, Runtime(os: 'Linux'), Values(['../../core', '%s/core'])]
126+
public function link_inside_directory($target) {
127+
$tempDir= $this->tempDir();
128+
129+
$link= sprintf($target, rtrim($tempDir->getURI(), DIRECTORY_SEPARATOR));
130+
$path= $this->create([
131+
'core/' => [Sources::IS_FOLDER, 0755],
132+
'core/composer.json' => [Sources::IS_FILE, '{"require":{"php":">=7.0"}}'],
133+
'project' => [Sources::IS_FOLDER, 0755],
134+
'project/src' => [Sources::IS_FOLDER, 0755],
135+
'project/src/file.txt' => [Sources::IS_FILE, 'Test'],
136+
'project/lib' => [Sources::IS_FOLDER, 0755],
137+
'project/lib/core' => [Sources::IS_LINK, $link],
138+
], $tempDir);
139+
$zip= $this->package(new Sources(new Path($path, 'project'), ['src', 'lib']));
140+
141+
$dir= $zip->next();
142+
Assert::equals('src/', $dir->getName());
143+
Assert::true($dir->isDirectory());
144+
145+
$file= $zip->next();
146+
Assert::equals('src/file.txt', $file->getName());
147+
Assert::equals(4, $file->getSize());
148+
149+
$lib= $zip->next();
150+
Assert::equals('lib/', $lib->getName());
151+
Assert::true($lib->isDirectory());
152+
153+
$core= $zip->next();
154+
Assert::equals('lib/core/', $core->getName());
155+
Assert::true($core->isDirectory());
156+
157+
$composer= $zip->next();
158+
Assert::equals('lib/core/composer.json', $composer->getName());
159+
Assert::equals(27, $composer->getSize());
160+
161+
Assert::false($zip->hasNext());
162+
}
163+
164+
#[Test, Runtime(os: 'Linux'), Values(['../../libs/inc.pth', '%s/libs/inc.pth'])]
165+
public function link_to_file($target) {
166+
$tempDir= $this->tempDir();
167+
168+
$link= sprintf($target, rtrim($tempDir->getURI(), DIRECTORY_SEPARATOR));
169+
$path= $this->create([
170+
'libs/' => [Sources::IS_FOLDER, 0755],
171+
'libs/inc.pth' => [Sources::IS_FILE, 'src/main/php'],
172+
'project' => [Sources::IS_FOLDER, 0755],
173+
'project/src' => [Sources::IS_FOLDER, 0755],
174+
'project/src/file.txt' => [Sources::IS_FILE, 'Test'],
175+
'project/lib' => [Sources::IS_FOLDER, 0755],
176+
'project/lib/inc.pth' => [Sources::IS_LINK, $link],
177+
], $tempDir);
178+
$zip= $this->package(new Sources(new Path($path, 'project'), ['src', 'lib']));
179+
180+
$dir= $zip->next();
181+
Assert::equals('src/', $dir->getName());
182+
Assert::true($dir->isDirectory());
183+
184+
$file= $zip->next();
185+
Assert::equals('src/file.txt', $file->getName());
186+
Assert::equals(4, $file->getSize());
187+
188+
$lib= $zip->next();
189+
Assert::equals('lib/', $lib->getName());
190+
Assert::true($lib->isDirectory());
191+
192+
$path= $zip->next();
193+
Assert::equals('lib/inc.pth', $path->getName());
194+
Assert::equals(12, $path->getSize());
195+
196+
Assert::false($zip->hasNext());
197+
}
198+
}

0 commit comments

Comments
 (0)