Skip to content

Commit c8afffd

Browse files
committed
Fix case-insensitive MuPDF detail path collision
1 parent 5b0f447 commit c8afffd

5 files changed

Lines changed: 45 additions & 5 deletions

File tree

‎docs/rules-v5-contract.md‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,12 @@ Each matcher has the following **required** fields:
2424
- `priority`: nonnegative integer, lower first; import uses legacy `_id`.
2525
- `label`: original DB display label, independent of localized details.
2626
- `iconId`: stable `ic_lib_*` string or null; explicit per-matcher value.
27-
- `regexName`: original string or null, used only for compatibility paths.
27+
- `regexName`: compatibility detail alias or null; a nonempty alias makes Android
28+
request `<type-dir>/regex/<regexName>.json`, including for `exact` matchers.
29+
It does not change `name` or matching mode.
2830
- `hasDetail`: boolean; false means no details, even if library `data` exists.
29-
- `legacyPath`: original detail path or null.
31+
- `legacyPath`: compatibility detail path or null; must agree with the client's
32+
request path, including any `regexName` alias.
3033
- `examples`: `{positive: [string], negative: [string]}`; empty arrays permitted for
3134
imported patterns without trustworthy examples, never invented witnesses.
3235

@@ -93,6 +96,13 @@ legacy files are retained during migration; new source edits ship updated legacy
9396
artifacts from the same compiler invocation. Orphan descriptions remain available
9497
at old paths without becoming matches.
9598

99+
`load_source` rejects distinct legacy detail paths whose `casefold()` values are
100+
equal, so generated files can coexist on case-insensitive filesystems. Use a
101+
distinct detail alias when exact names differ only by case; keep their matching
102+
names unchanged. Publish a detail path change together with the updated rules DB.
103+
Removing the old path makes its detail unavailable to clients still using the old
104+
DB until they update their rules; a path rename is not seamless compatibility.
105+
96106
## Matching and SVG safety
97107

98108
Exact name equality is attempted first for **all rows**, including the literal

‎libraries/D100E2DE-D729-4D97-A66E-8B51A090ED5B.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@
3939
"priority": 1773,
4040
"label": "MuPDF",
4141
"iconId": null,
42-
"regexName": null,
42+
"regexName": "mupdf_uppercase",
4343
"hasDetail": true,
44-
"legacyPath": "native-libs/libMuPDF.so.json",
44+
"legacyPath": "native-libs/regex/mupdf_uppercase.json",
4545
"examples": {
4646
"positive": [
4747
"libMuPDF.so"

‎tests/test_rules.py‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,35 @@ def test_source_details_and_exact_lookup(self):
3030
self.assertEqual(row['uuid'],detail['uuid'])
3131
self.assertTrue(detail['data'])
3232

33+
def test_mupdf_case_distinct_names_and_detail_paths(self):
34+
for name,id,legacy_path,alias in [('libMuPDF.so',1773,'native-libs/regex/mupdf_uppercase.json','mupdf_uppercase'),
35+
('libmupdf.so',3090,'native-libs/libmupdf.so.json',None)]:
36+
row=next(row for row in self.rows if row['id']==id)
37+
self.assertFalse(row['isRegexRule'])
38+
self.assertEqual(alias,row['regexName'])
39+
for use_regex in (False,True):
40+
self.assertEqual(id,rules.find_rule(self.rows,name,0,use_regex))
41+
self.assertEqual(self.common[row['detailPath']],self.legacy[legacy_path])
42+
self.assertNotIn('native-libs/libMuPDF.so.json',self.legacy)
43+
44+
def test_legacy_paths_reject_case_collisions_and_allow_identical_reuse(self):
45+
with tempfile.TemporaryDirectory() as tmp:
46+
root=Path(tmp);shutil.copytree(ROOT/'icons',root/'icons');(root/'libraries').mkdir()
47+
filename='D100E2DE-D729-4D97-A66E-8B51A090ED5B.json'
48+
original=rules.read(ROOT/'libraries'/filename);path=root/'libraries'/filename
49+
for source in ('matcher','orphan'):
50+
with self.subTest(source=source):
51+
lib=copy.deepcopy(original)
52+
if source=='matcher':lib['matchers'][0]['legacyPath']='native-libs/libMuPDF.so.json'
53+
else:lib['unreferencedDescriptions']=[dict(path='native-libs/libMuPDF.so.json',payload=dict(uuid=lib['uuid'],data=lib['data']))]
54+
path.write_bytes(rules.encoded(lib))
55+
with self.assertRaisesRegex(ValueError,'Case-insensitive legacy path collision'):rules.load_source(root)
56+
lib=copy.deepcopy(original)
57+
lib['unreferencedDescriptions']=[dict(path=lib['matchers'][0]['legacyPath'],payload=dict(uuid=lib['uuid'],data=lib['data']))]
58+
path.write_bytes(rules.encoded(lib))
59+
_,_,legacy,_=rules.load_source(root)
60+
self.assertEqual(2,len(legacy))
61+
3362
def test_reproducible_build_and_versioning(self):
3463
with tempfile.TemporaryDirectory() as tmp,contextlib.redirect_stdout(io.StringIO()):
3564
a,b=Path(tmp)/'a',Path(tmp)/'b'

‎tools/rules.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,10 +149,11 @@ def load_source(root=ROOT):
149149
raw=path.read_bytes();generated=validate_vector(raw,path.stem)
150150
fail(icons.get(f'icons/{path.stem}.svg')==generated,f'SVG must be derived from Android XML: {path.stem}')
151151
vectors[f'icons/android/{path.name}']=raw
152-
libraries=[];rules=[];details={};legacy={};ids=set();names=set()
152+
libraries=[];rules=[];details={};legacy={};legacy_names={};ids=set();names=set()
153153
def add_legacy(path,payload):
154154
safe_path(path)
155155
fail(path.split('/')[0] in ('native-libs','services-libs','activities-libs','receivers-libs','providers-libs','dex-libs','static-libs','permissions-libs','metadata-libs','actions-libs') and path.endswith('.json'),'Invalid legacy detail path')
156+
fail(legacy_names.setdefault(path.casefold(),path)==path,f'Case-insensitive legacy path collision: {legacy_names[path.casefold()]} and {path}')
156157
value=encoded(payload)
157158
fail(path not in legacy or legacy[path]==value,f'Conflicting legacy details: {path}')
158159
legacy[path]=value

0 commit comments

Comments
 (0)