1313use CodedMonkey \Dirigent \Message \ResolveDistribution ;
1414use Symfony \Component \DependencyInjection \Attribute \Autowire ;
1515use Symfony \Component \Filesystem \Filesystem ;
16+ use Symfony \Component \Filesystem \Path ;
1617use Symfony \Component \Lock \LockFactory ;
1718use Symfony \Component \Lock \SharedLockInterface ;
1819use Symfony \Component \Messenger \MessageBusInterface ;
@@ -34,7 +35,7 @@ public function __construct(
3435 string $ storagePath ,
3536 ) {
3637 $ this ->filesystem = new Filesystem ();
37- $ this ->storagePath = "$ storagePath/distribution " ;
38+ $ this ->storagePath = Path:: canonicalize ( "$ storagePath/distribution " ) ;
3839 }
3940
4041 public function exists (Metadata $ metadata , string $ type ): bool
@@ -44,12 +45,20 @@ public function exists(Metadata $metadata, string $type): bool
4445
4546 public function path (Metadata $ metadata , string $ type ): string
4647 {
47- $ packageName = $ metadata ->getPackage ()->getName ();
48- $ versionName = $ metadata ->getNormalizedVersionName ();
48+ $ packageName = explode ('/ ' , $ metadata ->getPackage ()->getName (), 2 )
49+ |> (fn ($ x ) => array_map ($ this ->encodePathComponent (...), $ x ))
50+ |> (fn ($ x ) => implode ('/ ' , $ x ));
51+ $ versionName = $ this ->encodePathComponent ($ metadata ->getNormalizedVersionName ());
4952 $ revision = $ metadata ->getRevision ();
50- $ reference = $ metadata ->getReference ();
53+ $ reference = $ this ->encodePathComponent ($ metadata ->getReference ());
54+ $ type = $ this ->encodePathComponent ($ type );
5155
52- return "{$ this ->storagePath }/ {$ packageName }/ {$ versionName }-r {$ revision }- {$ reference }. {$ type }" ;
56+ $ path = Path::canonicalize ("{$ this ->storagePath }/ {$ packageName }/ {$ versionName }-r {$ revision }- {$ reference }. {$ type }" );
57+ if (!Path::isBasePath ($ this ->storagePath , $ path ) || $ this ->storagePath === $ path ) {
58+ throw new \RuntimeException ('Distribution path is outside the configured storage directory. ' );
59+ }
60+
61+ return $ path ;
5362 }
5463
5564 public function remove (Distribution $ distribution ): void
@@ -60,16 +69,11 @@ public function remove(Distribution $distribution): void
6069 try {
6170 $ this ->filesystem ->remove ($ path );
6271
63- // Remove the package directory if it's empty
64- $ packageDirectory = dirname ($ path );
65- if (is_dir ($ packageDirectory ) && !new \FilesystemIterator ($ packageDirectory )->valid ()) {
66- $ this ->filesystem ->remove ($ packageDirectory );
67- }
68-
69- // Remove the vendor directory if it's empty
70- $ vendorDirectory = dirname ($ packageDirectory );
71- if (is_dir ($ vendorDirectory ) && !new \FilesystemIterator ($ vendorDirectory )->valid ()) {
72- $ this ->filesystem ->remove ($ vendorDirectory );
72+ // Remove parent directories that aren't empty
73+ $ directory = dirname ($ path );
74+ while ($ this ->storagePath !== $ directory && Path::isBasePath ($ this ->storagePath , $ directory ) && is_dir ($ directory ) && !new \FilesystemIterator ($ directory )->valid ()) {
75+ $ this ->filesystem ->remove ($ directory );
76+ $ directory = dirname ($ directory );
7377 }
7478 } finally {
7579 $ lock ->release ();
@@ -165,4 +169,16 @@ private function fileExists(string $path): bool
165169 {
166170 return $ this ->filesystem ->exists ($ path );
167171 }
172+
173+ private function encodePathComponent (string $ component ): string
174+ {
175+ $ encodedComponent = rawurlencode ($ component );
176+
177+ return match ($ encodedComponent ) {
178+ '' => '%00 ' ,
179+ '. ' => '%2E ' ,
180+ '.. ' => '%2E%2E ' ,
181+ default => $ encodedComponent ,
182+ };
183+ }
168184}
0 commit comments