[PATCH pve-storage v1 24/54] tree-wide: replace usages of VZTMPL_EXT_RE_1 with parsing functions

Max R. Carrara
2026-04-22T11:18:34Z

Add support for the 'vztmpl' volume type in
`PVE::Storage::Common::Parse`.

Also add corresponding test cases.

Use the module's parsing functions to replace usages of the
`PVE::Storage::VZTMPL_EXT_RE_1` regex across the repository.

As done with the `ISO_EXT_RE_0` regex, keep the `VZTMPL_EXT_RE_1`
regex around for now, since it's public and therefore also part of the
storage API. On an APIVER + APIAGE bump, this regex should be marked
for removal.

Signed-off-by: Max R. Carrara <m.carrara@proxmox.com>
---
src/PVE/API2/Storage/Status.pm | 6 +-
src/PVE/Storage.pm | 4 +-
src/PVE/Storage/Common/Parse.pm | 23 +++++
src/PVE/Storage/Common/test/parser_tests.pl | 109 ++++++++++++++++++++
src/PVE/Storage/Plugin.pm | 16 +--
5 files changed, 147 insertions(+), 11 deletions(-)

diff --git a/src/PVE/API2/Storage/Status.pm b/src/PVE/API2/Storage/Status.pm
index cf60c148..03929d26 100644
--- a/src/PVE/API2/Storage/Status.pm
+++ b/src/PVE/API2/Storage/Status.pm
@@ -85,11 +85,13 @@ my sub parse_transferred_file_path_extension : prototype($$) {
}

if ($vtype eq 'vztmpl') {
- if ($path !~ m![^/]+$PVE::Storage::VZTMPL_EXT_RE_1$!) {
+ my $parts = parse_path_as_volname_parts($path, $vtype);
+
+ if (!defined($parts)) {
raise_param_exc({ filename => "wrong file extension" });
}

- my $ext = $1;
+ my $ext = $parts->{ext};
return $ext;
}

diff --git a/src/PVE/Storage.pm b/src/PVE/Storage.pm
index d7d683ad..d5b0b637 100755
--- a/src/PVE/Storage.pm
+++ b/src/PVE/Storage.pm
@@ -761,9 +761,7 @@ sub path_to_volume_id {
}

if ($vtype eq 'vztmpl') {
- return if $filename !~ m!/([^/]+$VZTMPL_EXT_RE_1)$!;
- my $name = $1;
- return "$sid:vztmpl/$name";
+ return parse_path_as_volid($sid, $scfg, $path, $vtype);
}

if ($vtype eq 'backup') {
diff --git a/src/PVE/Storage/Common/Parse.pm b/src/PVE/Storage/Common/Parse.pm
index bcc6f9fc..b18ef576 100644
--- a/src/PVE/Storage/Common/Parse.pm
+++ b/src/PVE/Storage/Common/Parse.pm
@@ -33,6 +33,14 @@ primarily related to.

=cut

+my @VZTMPL_COMPRESSION_EXTENSIONS = ('gz', 'xz', 'zst', 'bz2');
+
+my sub join_to_re_alternations(@list) {
+ return join('|', map { quotemeta } @list);
+}
+
+my $RE_VZTMPL_COMPRESSION_EXTENSIONS = join_to_re_alternations(@VZTMPL_COMPRESSION_EXTENSIONS);
+
my $RE_PARENT_DIR = quotemeta('..');
my $RE_CONTAINS_PARENT_DIR = qr!
( ^$RE_PARENT_DIR/ ) # ../ --> Beginning of path
@@ -48,12 +56,27 @@ my $RE_ISO_FILE_PATH = qr!
)
!xn;

+my $RE_VZTMPL_FILE_PATH = qr!
+ (?<path>
+ (?<file>
+ [^/]+
+ \.
+ (?<ext>
+ (?<ext_archive> (?i: tar) )
+ ( \. (?<ext_compression> (?i: $RE_VZTMPL_COMPRESSION_EXTENSIONS)) )?
+ )
+ )
+ )
+!xn;
+
my $RE_FILE_PATH_FOR_VTYPE = {
iso => qr/^$RE_ISO_FILE_PATH$/,
+ vztmpl => qr/^$RE_VZTMPL_FILE_PATH$/,
};

my $RE_VOLNAME_FOR_VTYPE = {
iso => qr/^$RE_ISO_FILE_PATH$/,
+ vztmpl => qr/^$RE_VZTMPL_FILE_PATH$/,
};

my sub contains_parent_dir($path) {
diff --git a/src/PVE/Storage/Common/test/parser_tests.pl b/src/PVE/Storage/Common/test/parser_tests.pl
index 0167ca74..31575b66 100755
--- a/src/PVE/Storage/Common/test/parser_tests.pl
+++ b/src/PVE/Storage/Common/test/parser_tests.pl
@@ -86,12 +86,121 @@ my $volname_cases_iso_invalid = [
},
];

+my $volname_cases_vztmpl_valid = [
+ # plain paths
+ {
+ path => 'archlinux-base_20190924-1_amd64.tar',
+ expected => {
+ file => 'archlinux-base_20190924-1_amd64.tar',
+ ext => 'tar',
+ 'ext-archive' => 'tar',
+ 'disk-path' => 'archlinux-base_20190924-1_amd64.tar',
+ path => 'archlinux-base_20190924-1_amd64.tar',
+ vtype => 'vztmpl',
+ volname => 'vztmpl/archlinux-base_20190924-1_amd64.tar',
+ },
+ },
+ {
+ path => 'archlinux-base_20190924-1_amd64.tar.gz',
+ expected => {
+ file => 'archlinux-base_20190924-1_amd64.tar.gz',
+ ext => 'tar.gz',
+ 'ext-archive' => 'tar',
+ 'ext-compression' => 'gz',
+ 'disk-path' => 'archlinux-base_20190924-1_amd64.tar.gz',
+ path => 'archlinux-base_20190924-1_amd64.tar.gz',
+ vtype => 'vztmpl',
+ volname => 'vztmpl/archlinux-base_20190924-1_amd64.tar.gz',
+ },
+ },
+ {
+ path => 'alpine-3.10-default_20190626_amd64.tar.xz',
+ expected => {
+ file => 'alpine-3.10-default_20190626_amd64.tar.xz',
+ ext => 'tar.xz',
+ 'ext-archive' => 'tar',
+ 'ext-compression' => 'xz',
+ 'disk-path' => 'alpine-3.10-default_20190626_amd64.tar.xz',
+ path => 'alpine-3.10-default_20190626_amd64.tar.xz',
+ vtype => 'vztmpl',
+ volname => 'vztmpl/alpine-3.10-default_20190626_amd64.tar.xz',
+ },
+ },
+ {
+ path => 'debian-10.0-standard_10.0-1_amd64.tar.zst',
+ expected => {
+ file => 'debian-10.0-standard_10.0-1_amd64.tar.zst',
+ ext => 'tar.zst',
+ 'ext-archive' => 'tar',
+ 'ext-compression' => 'zst',
+ 'disk-path' => 'debian-10.0-standard_10.0-1_amd64.tar.zst',
+ path => 'debian-10.0-standard_10.0-1_amd64.tar.zst',
+ vtype => 'vztmpl',
+ volname => 'vztmpl/debian-10.0-standard_10.0-1_amd64.tar.zst',
+ },
+ },
+ {
+ path => 'debian-11.0-standard_11.0-1_amd64.tar.bz2',
+ expected => {
+ file => 'debian-11.0-standard_11.0-1_amd64.tar.bz2',
+ ext => 'tar.bz2',
+ 'ext-archive' => 'tar',
+ 'ext-compression' => 'bz2',
+ 'disk-path' => 'debian-11.0-standard_11.0-1_amd64.tar.bz2',
+ path => 'debian-11.0-standard_11.0-1_amd64.tar.bz2',
+ vtype => 'vztmpl',
+ volname => 'vztmpl/debian-11.0-standard_11.0-1_amd64.tar.bz2',
+ },
+ },
+
+ # case-insensitive file extensions
+ {
+ path => 'ARCHLINUX-BASE 20190924-1 AMD64.TAR.GZ',
+ expected => {
+ file => 'ARCHLINUX-BASE 20190924-1 AMD64.TAR.GZ',
+ ext => 'TAR.GZ',
+ 'ext-archive' => 'TAR',
+ 'ext-compression' => 'GZ',
+ 'disk-path' => 'ARCHLINUX-BASE 20190924-1 AMD64.TAR.GZ',
+ path => 'ARCHLINUX-BASE 20190924-1 AMD64.TAR.GZ',
+ vtype => 'vztmpl',
+ volname => 'vztmpl/ARCHLINUX-BASE 20190924-1 AMD64.TAR.GZ',
+ },
+ },
+ {
+ path => 'Alpine 3.10 default_20190626_amd64.Tar.xZ',
+ expected => {
+ file => 'Alpine 3.10 default_20190626_amd64.Tar.xZ',
+ ext => 'Tar.xZ',
+ 'ext-archive' => 'Tar',
+ 'ext-compression' => 'xZ',
+ 'disk-path' => 'Alpine 3.10 default_20190626_amd64.Tar.xZ',
+ path => 'Alpine 3.10 default_20190626_amd64.Tar.xZ',
+ vtype => 'vztmpl',
+ volname => 'vztmpl/Alpine 3.10 default_20190626_amd64.Tar.xZ',
+ },
+ },
+];
+
+my $volname_cases_vztmpl_invalid = [
+ {
+ description => "Invalid file extension (vztmpl)",
+ args => {
+ path => 'archlinux-base_20190924-1_amd64.zip',
+ vtype => 'vztmpl',
+ },
+ expected => undef,
+ },
+];
+
my $cases_valid_all = [
$volname_cases_iso_valid,
+ $volname_cases_vztmpl_valid,
];

my $cases_invalid_all = [
$volname_cases_iso_invalid,
+ $volname_cases_vztmpl_invalid,
];

{
diff --git a/src/PVE/Storage/Plugin.pm b/src/PVE/Storage/Plugin.pm
index 590ba3e0..6bfa5c11 100644
--- a/src/PVE/Storage/Plugin.pm
+++ b/src/PVE/Storage/Plugin.pm
@@ -826,10 +826,10 @@ sub parse_volname {
if ($vtype eq 'iso') {
return ($vtype, $volume_path, undef, undef, undef, undef, 'raw');
}
- }

- if ($volname =~ m!^vztmpl/([^/]+$PVE::Storage::VZTMPL_EXT_RE_1)$!) {
- return ('vztmpl', $1, undef, undef, undef, undef, 'raw');
+ if ($vtype eq 'vztmpl') {
+ return ($vtype, $volume_path, undef, undef, undef, undef, 'raw');
+ }
}

if ($volname =~ m!^backup/([^/]+$PVE::Storage::BACKUP_EXT_RE_2)$!) {
@@ -1708,11 +1708,15 @@ my sub get_subdir_files {
}

if ($vtype eq 'vztmpl') {
- return if $filename !~ m!/([^/]+$PVE::Storage::VZTMPL_EXT_RE_1)$!;
+ my $parts = parse_path_as_volid_parts($storeid, $scfg, $path, $vtype);
+ return if !defined($parts);
+
+ my ($ext, $ext_compression) = $parts->@{qw(ext ext-compression)};
+ my $format = $ext eq 'tar' ? $ext : ('t' . $ext_compression);

return {
- volid => "$storeid:vztmpl/$1",
- format => $2 eq 'tar' ? $2 : "t$2",
+ volid => $parts->{volid},
+ format => $format,
};
}

--
2.47.3