Skip to content

Commit

Permalink
Use Composer's HttpDownloader natively (#77)
Browse files Browse the repository at this point in the history
  • Loading branch information
phenaproxima authored Feb 7, 2023
1 parent 57df719 commit 56e372d
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 9 deletions.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"require": {
"composer-plugin-api": "^2.2",
"php-tuf/php-tuf": "dev-main",
"guzzlehttp/guzzle": "^6.5 || ^7.2"
"guzzlehttp/psr7": "^1.7"
},
"autoload": {
"psr-4": {
Expand Down
46 changes: 46 additions & 0 deletions src/Loader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

namespace Tuf\ComposerIntegration;

use Composer\Downloader\MaxFileSizeExceededException;
use Composer\Downloader\TransportException;
use Composer\Util\HttpDownloader;
use GuzzleHttp\Psr7\Utils;
use Psr\Http\Message\StreamInterface;
use Tuf\Exception\DownloadSizeException;
use Tuf\Exception\RepoFileNotFound;
use Tuf\Loader\LoaderInterface;

/**
* Defines a data loader that wraps around Composer's HttpDownloader.
*/
class Loader implements LoaderInterface
{
public function __construct(private HttpDownloader $downloader, private string $baseUrl = '')
{
}

/**
* {@inheritDoc}
*/
public function load(string $locator, int $maxBytes): StreamInterface
{
$url = $this->baseUrl . $locator;

try {
// Add 1 to $maxBytes to work around a bug in Composer.
// @see \Tuf\ComposerIntegration\ComposerCompatibleUpdater::getLength()
$content = $this->downloader->get($url, ['max_file_size' => $maxBytes + 1])
->getBody();
return Utils::streamFor($content);
} catch (TransportException $e) {
if ($e->getStatusCode() === 404) {
throw new RepoFileNotFound("$locator not found");
} elseif ($e instanceof MaxFileSizeExceededException) {
throw new DownloadSizeException("$locator exceeded $maxBytes bytes");
} else {
throw new \RuntimeException($e->getMessage(), $e->getCode(), $e);
}
}
}
}
9 changes: 1 addition & 8 deletions src/TufValidatedComposerRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,8 @@
use Composer\Repository\ComposerRepository;
use Composer\Util\Http\Response;
use Composer\Util\HttpDownloader;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Utils;
use Tuf\Exception\NotFoundException;
use Tuf\Loader\GuzzleLoader;
use Tuf\Loader\SizeCheckingLoader;
use Tuf\Metadata\RootMetadata;
use Tuf\Metadata\StorageInterface;
Expand Down Expand Up @@ -55,13 +53,8 @@ public function __construct(array $repoConfig, IOInterface $io, Config $config,
$url = rtrim($repoConfig['url'], '/');

if (isset($repoConfig['tuf'])) {
$client = new Client([
// We need the trailing slash here in order for Guzzle to build
// the correct URLs.
'base_uri' => "$url/metadata/",
]);
$this->updater = new ComposerCompatibleUpdater(
new SizeCheckingLoader(new GuzzleLoader($client)),
new SizeCheckingLoader(new Loader($httpDownloader, "$url/metadata/")),
// @todo: Write a custom implementation of FileStorage that stores repo keys to user's global composer cache?
$this->initializeStorage($url, $config)
);
Expand Down
74 changes: 74 additions & 0 deletions tests/LoaderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

namespace Tuf\ComposerIntegration\Tests;

use Composer\Downloader\MaxFileSizeExceededException;
use Composer\Downloader\TransportException;
use Composer\Util\HttpDownloader;
use GuzzleHttp\Psr7\Response;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use Psr\Http\Message\StreamInterface;
use Tuf\ComposerIntegration\Loader;
use Tuf\Exception\DownloadSizeException;
use Tuf\Exception\RepoFileNotFound;

/**
* @covers \Tuf\ComposerIntegration\Loader
*/
class LoaderTest extends TestCase
{
use ProphecyTrait;

public function testLoader(): void
{
$downloader = $this->prophesize(HttpDownloader::class);
$loader = new Loader($downloader->reveal(), '/metadata/');

$downloader->get('/metadata/root.json', ['max_file_size' => 129])
->willReturn(new Response())
->shouldBeCalled();
$this->assertInstanceOf(StreamInterface::class, $loader->load('root.json', 128));

// Any TransportException with a 404 error could should be converted
// into a RepoFileNotFound exception.
$exception = new TransportException();
$exception->setStatusCode(404);
$downloader->get('/metadata/bogus.txt', ['max_file_size' => 11])
->willThrow($exception)
->shouldBeCalled();
try {
$loader->load('bogus.txt', 10);
$this->fail('Expected a RepoFileNotFound exception, but none was thrown.');
} catch (RepoFileNotFound $e) {
$this->assertSame('bogus.txt not found', $e->getMessage());
}

// A MaxFileSizeExceededException should be converted into a
// DownloadSizeException.
$downloader->get('/metadata/too_big.txt', ['max_file_size' => 11])
->willThrow(new MaxFileSizeExceededException())
->shouldBeCalled();
try {
$loader->load('too_big.txt', 10);
$this->fail('Expected a DownloadSizeException, but none was thrown.');
} catch (DownloadSizeException $e) {
$this->assertSame('too_big.txt exceeded 10 bytes', $e->getMessage());
}

// Any other TransportException should be wrapped in a
// \RuntimeException.
$originalException = new TransportException('Whiskey Tango Foxtrot', -32);
$downloader->get('/metadata/wtf.txt', ['max_file_size' => 11])
->willThrow($originalException)
->shouldBeCalled();
try {
$loader->load('wtf.txt', 10);
$this->fail('Expected a RuntimeException, but none was thrown.');
} catch (\RuntimeException $e) {
$this->assertSame($originalException->getMessage(), $e->getMessage());
$this->assertSame($originalException->getCode(), $e->getCode());
$this->assertSame($originalException, $e->getPrevious());
}
}
}

0 comments on commit 56e372d

Please sign in to comment.