Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions tests/test_updater_fetch_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,51 @@ def test_fetch_target(self, target: TestTarget) -> None:
self.assertEqual(path, updater.find_cached_target(info))
self.assertEqual(path, updater.find_cached_target(info, path))

@utils.run_sub_tests_with_dataset(targets)
def test_fetch_target_bytes(self, target: TestTarget) -> None:
path = os.path.join(self.targets_dir, target.encoded_path)

# Add target to repository
self.sim.targets.version += 1
self.sim.add_target("targets", target.content, target.path)
self.sim.update_snapshot()

updater = self._init_updater()
info = updater.get_targetinfo(target.path)
assert info is not None

# download_target_bytes returns the verified content
self.assertEqual(target.content, updater.download_target_bytes(info))

# ...and does not touch the local cache
self.assertFalse(os.path.exists(path))
self.assertIsNone(updater.find_cached_target(info))

def test_invalid_target_download_bytes(self) -> None:
target = TestTarget("targetpath", b"content", "targetpath")

# Add target to repository
self.sim.targets.version += 1
self.sim.add_target("targets", target.content, target.path)
self.sim.update_snapshot()

updater = self._init_updater()
info = updater.get_targetinfo(target.path)
assert info is not None

# Corrupt the file content to not match the hash
self.sim.target_files[target.path].data = b"conten@"
with self.assertRaises(RepositoryError):
updater.download_target_bytes(info)

# Corrupt the file content to not match the length
self.sim.target_files[target.path].data = b"cont"
with self.assertRaises(RepositoryError):
updater.download_target_bytes(info)

# Verify nothing is persisted in cache
self.assertIsNone(updater.find_cached_target(info))

def test_download_targets_with_succinct_roles(self) -> None:
self.sim.add_succinct_roles("targets", 8, "bin")
self.sim.update_snapshot()
Expand Down
75 changes: 62 additions & 13 deletions tuf/ngclient/updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,67 @@ def download_target(
filepath = self._generate_target_file_path(targetinfo)
Path(filepath).parent.mkdir(exist_ok=True, parents=True)

full_url = self._target_file_url(targetinfo, target_base_url)

with self._fetcher.download_file(
full_url, targetinfo.length
) as target_file:
targetinfo.verify_length_and_hashes(target_file)

target_file.seek(0)
with open(filepath, "wb") as destination_file:
shutil.copyfileobj(target_file, destination_file)

logger.debug("Downloaded target %s", targetinfo.path)
return filepath

def download_target_bytes(
self,
targetinfo: TargetFile,
target_base_url: str | None = None,
) -> bytes:
"""Download the target file specified by ``targetinfo`` as bytes.

This is like ``download_target()`` but returns the verified target
content instead of writing it into the local cache. The local target
cache is neither read nor written, so ``find_cached_target()`` is not
consulted: use ``download_target()`` if caching is wanted.

The whole target is buffered in memory (the content cannot be verified
until it has been downloaded in full), so this is not suitable for very
large targets.

Args:
targetinfo: ``TargetFile`` from ``get_targetinfo()``.
target_base_url: Base URL used to form the final target
download URL. Default is the value provided in ``Updater()``

Raises:
ValueError: Invalid arguments
DownloadError: Download of the target file failed in some way
RepositoryError: Downloaded target failed to be verified in some way

Returns:
Verified target file content
"""

full_url = self._target_file_url(targetinfo, target_base_url)

with self._fetcher.download_file(
full_url, targetinfo.length
) as target_file:
targetinfo.verify_length_and_hashes(target_file)

target_file.seek(0)
data: bytes = target_file.read()

logger.debug("Downloaded target %s", targetinfo.path)
return data

def _target_file_url(
self, targetinfo: TargetFile, target_base_url: str | None
) -> str:
"""Build the remote download URL for a target file."""
if target_base_url is None:
if self._target_base_url is None:
raise ValueError(
Expand All @@ -292,19 +353,7 @@ def download_target(
hashes = list(targetinfo.hashes.values())
dirname, sep, basename = target_filepath.rpartition("/")
target_filepath = f"{dirname}{sep}{hashes[0]}.{basename}"
full_url = f"{target_base_url}{target_filepath}"

with self._fetcher.download_file(
full_url, targetinfo.length
) as target_file:
targetinfo.verify_length_and_hashes(target_file)

target_file.seek(0)
with open(filepath, "wb") as destination_file:
shutil.copyfileobj(target_file, destination_file)

logger.debug("Downloaded target %s", targetinfo.path)
return filepath
return f"{target_base_url}{target_filepath}"

def _download_metadata(
self, rolename: str, length: int, version: int | None = None
Expand Down