Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Once installed, the tool is invoked as:
```sh
qcom-ptool gen_partition -i platforms/<soc>/<variant>/partitions.conf -o partitions.xml
qcom-ptool gen_contents -p partitions.xml -t contents.xml.in -o contents.xml
qcom-ptool gen_udev_rules -i platforms/<soc>/<variant>/partitions.conf -o 55-qcom-raw-partitions-noblkid.rules
qcom-ptool ptool -x partitions.xml
qcom-ptool msp -r rawprogram0.xml -d /dev/sdX -p patch0.xml
```
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ include = ["qcom_ptool*"]

[tool.setuptools.package-data]
"qcom_ptool.schema" = ["*.json"]
"qcom_ptool.data" = ["*.in", "*.list"]

[tool.ruff]
target-version = "py38"
Expand Down
1 change: 1 addition & 0 deletions qcom_ptool/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
SUBCOMMANDS = {
"gen_partition": "qcom_ptool.gen_partition",
"gen_contents": "qcom_ptool.gen_contents",
"gen_udev_rules": "qcom_ptool.gen_udev_rules",
"ptool": "qcom_ptool.ptool",
"msp": "qcom_ptool.msp",
}
Expand Down
35 changes: 35 additions & 0 deletions qcom_ptool/data/55-qcom-raw-partitions-noblkid.rules.in
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"

Copy link
Copy Markdown
Contributor

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" ?

Copy link
Copy Markdown
Author

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.


# 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"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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"
2 changes: 2 additions & 0 deletions qcom_ptool/data/__init__.py
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
67 changes: 67 additions & 0 deletions qcom_ptool/data/approved-raw-partition-patterns.list
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]
122 changes: 122 additions & 0 deletions qcom_ptool/gen_udev_rules.py
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_.+*?\[\]-]+")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems that PATTERN_RE rejects [!...] negation globs, which udev and fnmatch both support



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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a typo'd -o pointing at an existing unrelated file (say, another rules file in /etc/udev/rules.d/) gets deleted, with exit 0 and only a "skipped" message. Writing nothing, or refusing to touch an existing file it didn't create, would be safer.

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())
107 changes: 107 additions & 0 deletions tests/unit/test_gen_udev_rules.py
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
Loading