CLI: Build file URLs with as_uri instead of by hand

populate_config prefixed "file://" onto the result of pathname2url, which
already returns a leading "///" on Windows. Three slashes plus two gives
file://///C:/..., an empty authority that urlopen reads as a UNC path, so
every URIRequirement failed there before the file was ever opened. That
covers --single-location, --yara-file, --yara-compiled-file, --strings-file,
--isf and volshell's --script.

pathlib's as_uri produces the same string as the current code on POSIX for
every supported Python version, and the correct one on Windows, including
for UNC paths. It also sidesteps the Python 3.14 rewrite of pathname2url,
which gives POSIX the same leading "///" that Windows always returned.

The two other places in the codebase that build file URLs,
URIRequirement.location_from_file and volshell's run_script, were already
correct; this was the only one assembling the scheme by hand.
This commit is contained in:
Hmkz0x00
2026-08-03 21:32:18 +05:30
parent fffd844b8e
commit 7b36e6b39e
2 changed files with 100 additions and 2 deletions
+94
View File
@@ -0,0 +1,94 @@
# volatility3 command line tests
#
# These require no memory image, but the conftest --volatility option must
# still be supplied for collection to succeed.
#
# IMPORTS
#
import argparse
from urllib.request import urlopen
import pytest
from volatility3.cli import CommandLine
from volatility3.framework import contexts, interfaces
from volatility3.framework.configuration import requirements
#
# HELPER CLASSES AND FUNCTIONS
#
class URIConfigurable(interfaces.configuration.ConfigurableInterface):
"""A configurable offering nothing but a single URIRequirement."""
@classmethod
def get_requirements(cls):
return [
requirements.URIRequirement(
name="testfile", description="A file to be located"
)
]
def populate_uri_requirement(value: str):
"""Run the given value through the command line's config population.
Args:
value: The value as it would arrive from the command line
Returns:
The value as it was stored in the context's configuration
"""
context = contexts.Context()
CommandLine().populate_config(
context,
{"testplugin": URIConfigurable},
argparse.Namespace(testfile=value),
"plugins.TestPlugin",
)
return context.config["plugins.TestPlugin.testfile"]
#
# TESTS
#
def test_uri_requirement_path_becomes_an_openable_url(tmp_path):
"""A filesystem path must become a URL the framework can actually open.
The URL used to be assembled by hand, which left an empty authority
section in place on platforms where pathname2url already returns a
leading "///".
"""
testfile = tmp_path / "memory dump.raw"
testfile.write_bytes(b"volatility")
location = populate_uri_requirement(str(testfile))
assert location == testfile.as_uri()
with urlopen(location) as fp:
assert fp.read() == b"volatility"
def test_uri_requirement_leaves_a_url_alone(tmp_path):
"""A value that already carries a scheme must be passed through as is."""
testfile = tmp_path / "memory.raw"
testfile.write_bytes(b"volatility")
url = testfile.as_uri()
assert populate_uri_requirement(url) == url
def test_uri_requirement_rejects_a_missing_file(tmp_path):
"""A path that does not exist must be reported rather than converted."""
with pytest.raises(FileNotFoundError):
populate_uri_requirement(str(tmp_path / "absent.raw"))
+6 -2
View File
@@ -17,11 +17,12 @@ import io
import json import json
import logging import logging
import os import os
import pathlib
import sys import sys
import tempfile import tempfile
import traceback import traceback
from typing import Any, Dict, List, Optional, Tuple, Type, Union from typing import Any, Dict, List, Optional, Tuple, Type, Union
from urllib import parse, request from urllib import parse
try: try:
import argcomplete import argcomplete
@@ -743,7 +744,10 @@ class CommandLine:
raise FileNotFoundError( raise FileNotFoundError(
f"Non-existent file {value} passed to URIRequirement" f"Non-existent file {value} passed to URIRequirement"
) )
value = f"file://{request.pathname2url(os.path.abspath(value))}" # as_uri builds a correctly formed file URL on
# every platform, whereas prefixing the scheme
# by hand leaves too many slashes on Windows
value = pathlib.Path(os.path.abspath(value)).as_uri()
if isinstance(requirement, requirements.ListRequirement): if isinstance(requirement, requirements.ListRequirement):
if not isinstance(value, list): if not isinstance(value, list):
raise TypeError( raise TypeError(