-
Notifications
You must be signed in to change notification settings - Fork 47
gen_udev_rules: add raw partition rule generator #156
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| # Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. | ||
| # SPDX-License-Identifier: BSD-3-Clause | ||
|
|
||
| # Skip filesystem probing for reviewed Qualcomm raw GPT partitions. | ||
| # | ||
| # Match by GPT partition name instead of kernel disk name. Disk enumeration is | ||
| # not stable across all boards, and a disk can contain both raw firmware and | ||
| # mountable filesystem partitions. | ||
| # | ||
| # 60-persistent-storage.rules creates partition metadata links after the blkid | ||
| # import. Since this rule skips that import for raw partitions, emit those | ||
| # links here from kernel-provided PARTNAME/PARTUUID properties. | ||
| ACTION=="remove", GOTO="qcom_raw_noblkid_end" | ||
| SUBSYSTEM!="block", GOTO="qcom_raw_noblkid_end" | ||
| ENV{DEVTYPE}!="partition", GOTO="qcom_raw_noblkid_end" | ||
| ENV{PARTNAME}=="", GOTO="qcom_raw_links" | ||
|
|
||
| # Generated from the selected qcom-ptool partition layouts. New labels are not | ||
| # excluded from blkid probing until their patterns have been reviewed. | ||
| @QCOM_RAW_PARTITION_RULES@ | ||
| GOTO="qcom_raw_noblkid_end" | ||
|
|
||
| LABEL="qcom_raw_noblkid" | ||
| ENV{UDEV_DISABLE_PERSISTENT_STORAGE_BLKID_FLAG}="1" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will work only on recent versions of systemd. |
||
|
|
||
| LABEL="qcom_raw_links" | ||
| # 60-persistent-storage.rules normally imports ID_PATH after this rule runs. | ||
| ENV{ID_PATH}!="?*", IMPORT{parent}="ID_PATH" | ||
| ENV{PARTUUID}=="?*", SYMLINK+="disk/by-partuuid/$env{PARTUUID}" | ||
| ENV{PARTNAME}=="?*", OPTIONS+="string_escape=replace", SYMLINK+="disk/by-partlabel/$env{PARTNAME}" | ||
| ENV{ID_PATH}=="?*", SYMLINK+="disk/by-path/$env{ID_PATH}-part/by-partnum/%n" | ||
| ENV{ID_PATH}=="?*", ENV{PARTUUID}=="?*", SYMLINK+="disk/by-path/$env{ID_PATH}-part/by-partuuid/$env{PARTUUID}" | ||
| ENV{ID_PATH}=="?*", ENV{PARTNAME}=="?*", OPTIONS+="string_escape=replace", SYMLINK+="disk/by-path/$env{ID_PATH}-part/by-partlabel/$env{PARTNAME}" | ||
|
|
||
| LABEL="qcom_raw_noblkid_end" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| # Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. | ||
| # SPDX-License-Identifier: BSD-3-Clause |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| # Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. | ||
| # SPDX-License-Identifier: BSD-3-Clause | ||
|
|
||
| # Partition-name patterns reviewed as safe to exclude from blkid probing. | ||
| # Keep one udev glob per line. A new partition label is not implicitly safe. | ||
| abl_[ab] | ||
| ALIGN_TO_128K_* | ||
| aop_[ab] | ||
| aop_config_[ab] | ||
| apdp | ||
| apdp_[ab] | ||
| catecontentfv | ||
| cdt | ||
| cmnlib64_[ab] | ||
| cmnlib_[ab] | ||
| cpucp_[ab] | ||
| ddr | ||
| ddr_[ab] | ||
| devcfg_[ab] | ||
| devinfo | ||
| diag_log | ||
| dip | ||
| dtb_[ab] | ||
| emac | ||
| featenabler_[ab] | ||
| fsc | ||
| fsg | ||
| gearvm_[ab] | ||
| gvm_log | ||
| hyp_[ab] | ||
| imagefv_[ab] | ||
| keymaster_[ab] | ||
| limits | ||
| limits-cdsp | ||
| logdump | ||
| modemst1 | ||
| modemst2 | ||
| multiimgoem | ||
| multiimgoem_[ab] | ||
| multiimgqti | ||
| multiimgqti_[ab] | ||
| pvm_log | ||
| qmcs | ||
| quantumfv | ||
| quantumsdk | ||
| questdatafv | ||
| qupfw_[ab] | ||
| qweslicstore_[ab] | ||
| recoveryinfo | ||
| secdata | ||
| shrm_[ab] | ||
| softsku | ||
| splash | ||
| spunvm | ||
| storsec | ||
| storsec_[ab] | ||
| SYSFW_VERSION | ||
| toolsfv | ||
| tz_[ab] | ||
| TZAPPS | ||
| uefi_[ab] | ||
| uefisecapp_[ab] | ||
| vbmeta_[ab] | ||
| xbl_[ab] | ||
| xbl_config_[ab] | ||
| xbl_logs | ||
| xbl_ramdump_[ab] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| # Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. | ||
| # SPDX-License-Identifier: BSD-3-Clause | ||
|
|
||
| """Generate udev rules for reviewed raw partitions in selected layouts.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import argparse | ||
| import fnmatch | ||
| import getopt | ||
| import re | ||
| import sys | ||
| from pathlib import Path | ||
|
|
||
| from qcom_ptool.loaders import load as load_spec | ||
|
|
||
| DATA_DIR = Path(__file__).with_name("data") | ||
| POLICY_FILE = DATA_DIR / "approved-raw-partition-patterns.list" | ||
| TEMPLATE_FILE = DATA_DIR / "55-qcom-raw-partitions-noblkid.rules.in" | ||
| RULES_PLACEHOLDER = "@QCOM_RAW_PARTITION_RULES@" | ||
| LABEL_RE = re.compile(r"[A-Za-z0-9_.+-]+") | ||
| PATTERN_RE = re.compile(r"[A-Za-z0-9_.+*?\[\]-]+") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems that PATTERN_RE rejects |
||
|
|
||
|
|
||
| def load_patterns() -> list[str]: | ||
| """Return non-comment entries from the packaged policy file.""" | ||
| patterns: list[str] = [] | ||
| seen: set[str] = set() | ||
| for line_number, line in enumerate( | ||
| POLICY_FILE.read_text(encoding="utf-8").splitlines(), start=1 | ||
| ): | ||
| pattern = line.partition("#")[0].strip() | ||
| if not pattern: | ||
| continue | ||
| if PATTERN_RE.fullmatch(pattern) is None: | ||
| raise ValueError(f"{POLICY_FILE}:{line_number}: invalid pattern: {pattern}") | ||
| if pattern in seen: | ||
| raise ValueError(f"{POLICY_FILE}:{line_number}: duplicate pattern: {pattern}") | ||
| patterns.append(pattern) | ||
| seen.add(pattern) | ||
|
|
||
| if not patterns: | ||
| raise ValueError(f"approved pattern list is empty: {POLICY_FILE}") | ||
| return patterns | ||
|
|
||
|
|
||
| def collect_labels(input_paths: list[Path]) -> set[str]: | ||
| """Return partition labels defined by the selected layouts.""" | ||
| labels: set[str] = set() | ||
| for path in input_paths: | ||
| spec = load_spec(str(path)) | ||
| labels.update( | ||
| entry["label"] | ||
| for entries in spec["partitions"].values() | ||
| for entry in entries | ||
| ) | ||
| return labels | ||
|
|
||
|
|
||
| def generate_rules(input_paths: list[Path]) -> str: | ||
| """Render udev rules for approved labels present in the layouts.""" | ||
| patterns = load_patterns() | ||
| approved_labels = { | ||
| label | ||
| for label in collect_labels(input_paths) | ||
| if any(fnmatch.fnmatchcase(label, pattern) for pattern in patterns) | ||
| } | ||
| invalid_labels = sorted( | ||
| label for label in approved_labels if LABEL_RE.fullmatch(label) is None | ||
| ) | ||
| if invalid_labels: | ||
| raise ValueError(f"invalid approved partition label: {invalid_labels[0]!r}") | ||
|
|
||
| labels = sorted(approved_labels) | ||
| if not labels: | ||
| return "" | ||
|
|
||
| rules = "\n".join( | ||
| f'ENV{{PARTNAME}}=="{label}", GOTO="qcom_raw_noblkid"' | ||
| for label in labels | ||
| ) | ||
| template = TEMPLATE_FILE.read_text(encoding="utf-8") | ||
| if template.count(RULES_PLACEHOLDER) != 1: | ||
| raise ValueError("rules template must contain exactly one placeholder") | ||
| return template.replace(RULES_PLACEHOLDER, rules) | ||
|
|
||
|
|
||
| def parse_args(argv: list[str] | None = None) -> argparse.Namespace: | ||
| parser = argparse.ArgumentParser(description=__doc__) | ||
| parser.add_argument( | ||
| "-i", | ||
| "--input", | ||
| action="append", | ||
| required=True, | ||
| type=Path, | ||
| dest="inputs", | ||
| help="partition layout to include; repeat for additional storage", | ||
| ) | ||
| parser.add_argument("-o", "--output", required=True, type=Path) | ||
| return parser.parse_args(argv) | ||
|
|
||
|
|
||
| def main(argv: list[str] | None = None) -> int: | ||
| args = parse_args(argv) | ||
| try: | ||
| content = generate_rules(args.inputs) | ||
| if not content: | ||
| args.output.unlink(missing_ok=True) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. a typo'd |
||
| print("skipped: selected layouts contain no approved raw partitions") | ||
| return 0 | ||
| args.output.parent.mkdir(parents=True, exist_ok=True) | ||
| args.output.write_text(content, encoding="utf-8") | ||
| except (OSError, ValueError, getopt.GetoptError) as error: | ||
| print(f"error: {error}", file=sys.stderr) | ||
| return 2 | ||
|
|
||
| print(f"generated: {args.output}") | ||
| return 0 | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| raise SystemExit(main()) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| # Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. | ||
| # SPDX-License-Identifier: BSD-3-Clause | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from pathlib import Path | ||
|
|
||
| import pytest | ||
|
|
||
| from qcom_ptool import gen_udev_rules | ||
|
|
||
|
|
||
| def write_layout(path: Path, labels: list[str]) -> None: | ||
| partitions = "".join( | ||
| f"--partition --name={label} --size=1KB " | ||
| "--type-guid=00000000-0000-0000-0000-000000000000\n" | ||
| for label in labels | ||
| ) | ||
| path.write_text( | ||
| "--disk --type=ufs --size=1048576\n" + partitions, | ||
| encoding="utf-8", | ||
| ) | ||
|
|
||
|
|
||
| def test_generate_rules_filters_and_combines_layouts(tmp_path: Path) -> None: | ||
| first = tmp_path / "first.conf" | ||
| second = tmp_path / "second.conf" | ||
| write_layout(first, ["rootfs", "xbl_a"]) | ||
| write_layout(second, ["xbl_a", "ALIGN_TO_128K_7"]) | ||
|
|
||
| rules = gen_udev_rules.generate_rules([first, second]) | ||
|
|
||
| assert 'ENV{PARTNAME}=="xbl_a"' in rules | ||
| assert 'ENV{PARTNAME}=="ALIGN_TO_128K_7"' in rules | ||
| assert rules.count('ENV{PARTNAME}=="xbl_a"') == 1 | ||
| assert "ALIGN_TO_128K_*" not in rules | ||
| assert "rootfs" not in rules | ||
|
|
||
|
|
||
| def test_generate_rules_rejects_unsafe_approved_label( | ||
| monkeypatch: pytest.MonkeyPatch, | ||
| ) -> None: | ||
| monkeypatch.setattr( | ||
| gen_udev_rules, | ||
| "collect_labels", | ||
| lambda _: {'ALIGN_TO_128K_", RUN+="/bin/true'}, | ||
| ) | ||
|
|
||
| with pytest.raises(ValueError, match="invalid approved partition label"): | ||
| gen_udev_rules.generate_rules([]) | ||
|
|
||
|
|
||
| def test_load_patterns_rejects_unsafe_pattern( | ||
| tmp_path: Path, monkeypatch: pytest.MonkeyPatch | ||
| ) -> None: | ||
| policy = tmp_path / "patterns.list" | ||
| policy.write_text('xbl_[ab]\nxbl_", RUN+="/bin/true\n', encoding="utf-8") | ||
| monkeypatch.setattr(gen_udev_rules, "POLICY_FILE", policy) | ||
|
|
||
| with pytest.raises(ValueError, match="invalid pattern") as error: | ||
| gen_udev_rules.load_patterns() | ||
|
|
||
| assert f"{policy}:2:" in str(error.value) | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| ("content", "message"), | ||
| [ | ||
| ("xbl_a\nxbl_a\n", "duplicate pattern"), | ||
| ("# comments only\n", "approved pattern list is empty"), | ||
| ], | ||
| ) | ||
| def test_load_patterns_rejects_invalid_policy( | ||
| tmp_path: Path, | ||
| monkeypatch: pytest.MonkeyPatch, | ||
| content: str, | ||
| message: str, | ||
| ) -> None: | ||
| policy = tmp_path / "patterns.list" | ||
| policy.write_text(content, encoding="utf-8") | ||
| monkeypatch.setattr(gen_udev_rules, "POLICY_FILE", policy) | ||
|
|
||
| with pytest.raises(ValueError, match=message): | ||
| gen_udev_rules.load_patterns() | ||
|
|
||
|
|
||
| def test_main_writes_rules(tmp_path: Path) -> None: | ||
| layout = tmp_path / "partitions.conf" | ||
| output = tmp_path / "rules.d" / "55-qcom.rules" | ||
| write_layout(layout, ["cdt"]) | ||
|
|
||
| assert gen_udev_rules.main(["-i", str(layout), "-o", str(output)]) == 0 | ||
| assert 'ENV{PARTNAME}=="cdt"' in output.read_text(encoding="utf-8") | ||
|
|
||
|
|
||
| def test_main_skips_output_without_approved_labels( | ||
| tmp_path: Path, capsys: pytest.CaptureFixture[str] | ||
| ) -> None: | ||
| layout = tmp_path / "partitions.conf" | ||
| output = tmp_path / "rules.d" / "55-qcom.rules" | ||
| write_layout(layout, ["rootfs"]) | ||
| output.parent.mkdir(parents=True) | ||
| output.write_text("stale", encoding="utf-8") | ||
|
|
||
| assert gen_udev_rules.main(["-i", str(layout), "-o", str(output)]) == 0 | ||
| assert not output.exists() | ||
| assert "no approved raw partitions" in capsys.readouterr().out |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why not
ENV{PARTNAME}=="", GOTO="qcom_raw_noblkid_end"?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Empty PARTNAME now falls through to a shared qcom_raw_links label instead of exiting early.、
For partitions without a name, this still creates the non-name-dependent partition metadata links:
disk/by-partuuid/...、
disk/by-path/.../by-partnum/...、
disk/by-path/.../by-partuuid/...、
The by-partlabel links remain guarded by PARTNAME=="?*", so we do not create invalid empty-label symlinks.