Skip to content

Commit c1556d4

Browse files
authored
Merge pull request #1479 from socketpair/magic
Fix: #1478: Use regex for file(1) magic matching; add .zip and .zstd support
2 parents 4af079d + 653b19f commit c1556d4

8 files changed

Lines changed: 108 additions & 11 deletions

File tree

rpmlint/checks/SourceCheck.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,16 @@ class SourceCheck(AbstractCheck):
88
Validate files in a source package.
99
"""
1010
source_regex = re.compile(r'\.(tar|tgz)$')
11+
12+
# Regex patterns. Applied to a string from file(1) tool.
1113
compressed_fileext_magic = {
12-
'xz': 'XZ compressed',
13-
'gz': 'gzip compressed',
14-
'tgz': 'gzip compressed',
15-
'bz2': 'bzip2 compressed',
16-
'zst': 'ZSTD compressed',
14+
'xz': r'XZ compressed',
15+
'gz': r'gzip compressed',
16+
'tgz': r'gzip compressed',
17+
'bz2': r'bzip2 compressed',
18+
'zst': r'(ZSTD|Zstandard) compressed',
19+
'zstd': r'(ZSTD|Zstandard) compressed',
20+
'zip': r'Zip archive data',
1721
}
1822

1923
def __init__(self, config, output):
@@ -45,12 +49,16 @@ def _check_file_ext(self, fname, pkgfile, pkg):
4549
"""
4650
Check if the filename extension is the same as what file(1) says.
4751
"""
52+
if not pkgfile.magic:
53+
return
4854
file_ext = fname.rpartition('.')[2]
49-
50-
if (file_ext in self.compressed_fileext_magic and
51-
pkgfile.magic and
52-
self.compressed_fileext_magic[file_ext] not in pkgfile.magic):
53-
self.output.add_info('W', pkg, 'inconsistent-file-extension', fname)
55+
pattern = self.compressed_fileext_magic.get(file_ext)
56+
if pattern is None:
57+
return # file(1) pattern is unknown for given file extension
58+
if re.match(pattern, pkgfile.magic, re.IGNORECASE):
59+
return
60+
self.output.add_info('W', pkg, 'inconsistent-file-extension',
61+
f'file {fname!r} magic {pkgfile.magic!r} does not match {pattern!r}')
5462

5563
def _check_permissions(self, fname, pkgfile, pkg):
5664
"""

test/files/magic/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
!*

test/files/magic/byte.bz2

37 Bytes
Binary file not shown.

test/files/magic/byte.gz

21 Bytes
Binary file not shown.

test/files/magic/byte.xz

68 Bytes
Binary file not shown.

test/files/magic/byte.zip

153 Bytes
Binary file not shown.

test/files/magic/byte.zst

14 Bytes
Binary file not shown.

test/test_sources.py

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from rpmlint.checks.SourceCheck import SourceCheck
33
from rpmlint.filter import Filter
44

5-
from Testing import CONFIG, get_tested_package
5+
from Testing import CONFIG, get_tested_mock_package, get_tested_package
66

77

88
@pytest.fixture(scope='function', autouse=True)
@@ -39,3 +39,91 @@ def test_compression_and_multispec(tmp_path, package, sourcescheck):
3939

4040
assert 'multiple-specfiles' in out
4141
assert 'package contains multiple spec files' in out
42+
43+
44+
# ── Tests for _check_file_ext (compressed_fileext_magic matching) ──
45+
#
46+
# Test data: test/files/magic/ contains small compressed files, each holding
47+
# a single byte 'x'. Created with standard CLI tools:
48+
#
49+
# echo -n x | gzip -n > byte.gz # -n omits filename/timestamp
50+
# echo -n x | bzip2 > byte.bz2
51+
# echo -n x | xz > byte.xz
52+
# echo -n x | zstd -q > byte.zst
53+
# echo -n x > 0 # zip needs a file on disk
54+
# touch -t 200001010000.00 0 # fixed timestamp for reproducibility
55+
# TZ=UTC zip -0 -j byte.zip 0 # -0 store, -j junk paths
56+
# rm 0
57+
#
58+
# In Python (bz2 is byte-identical to CLI; others differ in internal
59+
# flags but are functionally equivalent):
60+
# gzip.compress(b'x', mtime=0)
61+
# bz2.compress(b'x')
62+
# lzma.compress(b'x', format=lzma.FORMAT_XZ)
63+
# zstandard.ZstdCompressor().compress(b'x')
64+
65+
66+
@pytest.mark.parametrize('ext, content_file', [
67+
('gz', 'files/magic/byte.gz'),
68+
('tgz', 'files/magic/byte.gz'), # tgz is gzip too
69+
('bz2', 'files/magic/byte.bz2'),
70+
('xz', 'files/magic/byte.xz'),
71+
('zst', 'files/magic/byte.zst'),
72+
('zstd', 'files/magic/byte.zst'), # .zstd is the same format
73+
('zip', 'files/magic/byte.zip'),
74+
])
75+
def test_file_ext_consistent(sourcescheck, ext, content_file):
76+
"""Extension matches actual compression format → no warning."""
77+
output, test = sourcescheck
78+
pkg = get_tested_mock_package(
79+
files={f'archive.{ext}': {'content-path': content_file}},
80+
)
81+
test.check_source(pkg)
82+
out = output.print_results(output.results)
83+
assert 'inconsistent-file-extension' not in out
84+
85+
86+
@pytest.mark.parametrize('ext, content_file', [
87+
('gz', 'files/magic/byte.bz2'),
88+
('bz2', 'files/magic/byte.gz'),
89+
('xz', 'files/magic/byte.zst'),
90+
('zst', 'files/magic/byte.gz'),
91+
('zstd', 'files/magic/byte.xz'),
92+
('zip', 'files/magic/byte.gz'),
93+
])
94+
def test_file_ext_inconsistent(sourcescheck, ext, content_file):
95+
"""Extension does NOT match actual compression format → warning."""
96+
output, test = sourcescheck
97+
pkg = get_tested_mock_package(
98+
files={f'archive.{ext}': {'content-path': content_file}},
99+
)
100+
test.check_source(pkg)
101+
out = output.print_results(output.results)
102+
assert 'inconsistent-file-extension' in out
103+
104+
105+
@pytest.mark.parametrize('fname', [
106+
'archive.gz',
107+
'archive.tar',
108+
'archive.txt',
109+
])
110+
def test_file_ext_no_magic(sourcescheck, fname):
111+
"""File with no magic string (empty content) → no crash, no warning."""
112+
output, test = sourcescheck
113+
pkg = get_tested_mock_package(
114+
files={fname: {'metadata': {'magic': None}}},
115+
)
116+
test.check_source(pkg)
117+
out = output.print_results(output.results)
118+
assert 'inconsistent-file-extension' not in out
119+
120+
121+
def test_file_ext_unknown_extension(sourcescheck):
122+
"""Extension not in compressed_fileext_magic → no warning."""
123+
output, test = sourcescheck
124+
pkg = get_tested_mock_package(
125+
files={'archive.lz4': {'content-path': 'files/magic/byte.gz'}},
126+
)
127+
test.check_source(pkg)
128+
out = output.print_results(output.results)
129+
assert 'inconsistent-file-extension' not in out

0 commit comments

Comments
 (0)