Skip to content
Merged
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
4 changes: 4 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ Here are a few things you can do that will increase the likelihood of your pull

- Follow the existing style where possible.
- Write tests.
- Keep the partition source formats in sync: when adding an option to
the legacy `.conf` loader, add the matching typed field to the YAML
schemas (`qcom_ptool/schema/`) and the YAML loader in the same pull
request, with a test pinning that both formats load identically.
- Keep your change as focused as possible.
If you want to make multiple independent changes, please consider submitting them as separate pull requests.
- Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
Expand Down
17 changes: 17 additions & 0 deletions qcom_ptool/loaders/yaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,23 @@ def _partition_from_node(
attribute_bits = int(str(node["attributes"]), 16)
entry["bootable"] = "true" if attribute_bits & (1 << 2) else "false"
entry["readonly"] = "true" if attribute_bits & (1 << 60) else "false"
# Named fields mirror the .conf options; applied after attributes so they win.
if "bootable" in node:
entry["bootable"] = _bool_str(node["bootable"])
if "readonly" in node:
entry["readonly"] = _bool_str(node["readonly"])
if "priority" in node:
entry["priority"] = str(int(node["priority"]) & 0x03)
if "tries-remaining" in node:
entry["triesremaining"] = str(int(node["tries-remaining"]) & 0x07)
if "active" in node:
entry["active"] = _bool_str(node["active"])
if "successful" in node:
entry["successful"] = _bool_str(node["successful"])
if "unbootable" in node:
entry["unbootable"] = _bool_str(node["unbootable"])
if "unique-guid" in node:
entry["uniqueguid"] = str(node["unique-guid"])
if "filename" in node:
entry["filename"] = str(node["filename"])
if "sparse" in node:
Expand Down
8 changes: 8 additions & 0 deletions qcom_ptool/schema/board.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@
"type-guid": {"type": "string", "pattern": "^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$"},
"filename": {"type": "string"},
"attributes": {"type": "string", "pattern": "^[0-9A-Fa-f]+$"},
"bootable": {"type": "boolean"},
"readonly": {"type": "boolean"},
"priority": {"type": "integer", "minimum": 0, "maximum": 3},
"tries-remaining": {"type": "integer", "minimum": 0, "maximum": 7},
"active": {"type": "boolean"},
"successful": {"type": "boolean"},
"unbootable": {"type": "boolean"},
"unique-guid": {"type": "string", "pattern": "^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$"},
"sparse": {"type": "boolean"},
"lun": {"type": "integer", "minimum": 0},
"phys-part": {"type": "integer", "minimum": 0}
Expand Down
8 changes: 8 additions & 0 deletions qcom_ptool/schema/include.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@
"type-guid": {"type": "string", "pattern": "^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$"},
"filename": {"type": "string"},
"attributes": {"type": "string", "pattern": "^[0-9A-Fa-f]+$"},
"bootable": {"type": "boolean"},
"readonly": {"type": "boolean"},
"priority": {"type": "integer", "minimum": 0, "maximum": 3},
"tries-remaining": {"type": "integer", "minimum": 0, "maximum": 7},
"active": {"type": "boolean"},
"successful": {"type": "boolean"},
"unbootable": {"type": "boolean"},
"unique-guid": {"type": "string", "pattern": "^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$"},
"sparse": {"type": "boolean"},
"lun": {"type": "integer", "minimum": 0},
"phys-part": {"type": "integer", "minimum": 0}
Expand Down
39 changes: 38 additions & 1 deletion qcom_ptool/schema/partitions.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,47 @@
"type": "string"
},
"attributes": {
"description": "GPT attribute bits as a hexadecimal string.",
"description": "GPT attribute bits as a hexadecimal string. The named fields below override the bits they cover.",
"type": "string",
"pattern": "^[0-9A-Fa-f]+$"
},
"bootable": {
"description": "Legacy BIOS bootable flag (GPT attribute bit 2).",
"type": "boolean"
},
"readonly": {
"description": "Read-only flag (GPT attribute bit 60).",
"type": "boolean"
},
"priority": {
"description": "A/B slot priority (GPT attribute bits 48-49).",
"type": "integer",
"minimum": 0,
"maximum": 3
},
"tries-remaining": {
"description": "A/B slot retry count (GPT attribute bits 51-53).",
"type": "integer",
"minimum": 0,
"maximum": 7
},
"active": {
"description": "A/B slot active flag (GPT attribute bit 50).",
"type": "boolean"
},
"successful": {
"description": "A/B slot boot-successful flag (GPT attribute bit 54).",
"type": "boolean"
},
"unbootable": {
"description": "A/B slot unbootable flag (GPT attribute bit 55).",
"type": "boolean"
},
"unique-guid": {
"description": "Partition unique GUID; must be a quoted string to avoid integer misparsing.",
"type": "string",
"pattern": "^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$"
},
"sparse": {
"type": "boolean"
},
Expand Down
138 changes: 138 additions & 0 deletions tests/unit/test_loaders_yaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,3 +201,141 @@ def test_schema_rejects_invalid(tmp_path, text):
def test_top_level_must_be_mapping(tmp_path):
with pytest.raises(yaml_loader.YamlParseError):
load(_write(tmp_path, "list.yaml", "- 1\n- 2\n"))


# ---------------------------------------------------------------------------
# Named GPT attribute fields (conf parity)
# ---------------------------------------------------------------------------

_GUID = "DEA0BA2C-CBDD-4805-B4F9-F428251C3E98"
_UGUID = "11111111-2222-3333-4444-555555555555"


def _conf_with(tmp_path, extra: str) -> str:
return _write(
tmp_path,
"p.conf",
"--disk --type=nvme --size=68719476736\n"
"--partition --name=boot_a --size=3584KB --type-guid=%s %s\n"
% (_GUID, extra),
)


def _yaml_with(tmp_path, extra: str) -> str:
return _write(
tmp_path,
"p.yaml",
_VALID_DISK
+ "partitions:\n"
+ " - name: boot_a\n"
+ ' size: "3584KB"\n'
+ ' type-guid: "%s"\n' % _GUID
+ " %s\n" % extra,
)


@pytest.mark.parametrize(
("yaml_field", "conf_opt", "entry_key", "expected"),
[
("bootable: true", "--bootable=yes", "bootable", "true"),
("readonly: false", "--readonly=no", "readonly", "false"),
("priority: 2", "--priority=2", "priority", "2"),
("tries-remaining: 6", "--tries-remaining=6", "triesremaining", "6"),
("active: true", "--active=1", "active", "true"),
("successful: false", "--successful=0", "successful", "false"),
("unbootable: true", "--unbootable=true", "unbootable", "true"),
('unique-guid: "%s"' % _UGUID, "--uniqueguid=%s" % _UGUID, "uniqueguid", _UGUID),
("sparse: true", "--sparse=yes", "sparse", "true"),
],
)
def test_named_field_matches_conf(tmp_path, yaml_field, conf_opt, entry_key, expected):
"""Each named field loads to the exact entry the .conf option produces."""
conf_spec = load(_conf_with(tmp_path, conf_opt))
yaml_spec = load(_yaml_with(tmp_path, yaml_field))
assert yaml_spec == conf_spec
assert yaml_spec["partitions"]["0"][0][entry_key] == expected


def test_named_fields_override_attributes(tmp_path):
"""A named field wins over the raw attributes hex, as in the .conf loader."""
# attributes sets bootable (bit 2) and readonly (bit 60); named fields
# invert both.
conf_spec = load(
_conf_with(
tmp_path, "--attributes=1000000000000004 --bootable=no --readonly=no"
)
)
yaml_spec = load(
_write(
tmp_path,
"o.yaml",
_VALID_DISK
+ "partitions:\n"
+ " - name: boot_a\n"
+ ' size: "3584KB"\n'
+ ' type-guid: "%s"\n' % _GUID
+ ' attributes: "1000000000000004"\n'
+ " bootable: false\n"
+ " readonly: false\n",
)
)
assert yaml_spec == conf_spec
entry = yaml_spec["partitions"]["0"][0]
assert entry["bootable"] == "false"
assert entry["readonly"] == "false"


def test_named_fields_emit_byte_identical_xml(tmp_path):
"""All named fields together still emit byte-identical XML per format."""
extra_conf = (
"--bootable=yes --readonly=no --priority=2 --tries-remaining=6 "
"--active=yes --successful=no --unbootable=no "
"--uniqueguid=%s --sparse=true" % _UGUID
)
extra_yaml = (
" bootable: true\n"
" readonly: false\n"
" priority: 2\n"
" tries-remaining: 6\n"
" active: true\n"
" successful: false\n"
" unbootable: false\n"
' unique-guid: "%s"\n'
" sparse: true\n" % _UGUID
)
conf_spec = load(_conf_with(tmp_path, extra_conf))
yaml_spec = load(
_write(
tmp_path,
"full.yaml",
_VALID_DISK
+ "partitions:\n"
+ " - name: boot_a\n"
+ ' size: "3584KB"\n'
+ ' type-guid: "%s"\n' % _GUID
+ extra_yaml,
)
)
conf_xml = tmp_path / "from_conf.xml"
yaml_xml = tmp_path / "from_yaml.xml"
generate_partition_xml(conf_spec["disk"], conf_spec["partitions"], str(conf_xml))
generate_partition_xml(yaml_spec["disk"], yaml_spec["partitions"], str(yaml_xml))
assert yaml_xml.read_bytes() == conf_xml.read_bytes()


@pytest.mark.parametrize(
"text",
[
# priority beyond the 2-bit field.
_partition(priority="4"),
# tries-remaining beyond the 3-bit field.
_partition(**{"tries-remaining": "8"}),
# named flags must be booleans, not strings.
_partition(active='"yes"'),
# malformed unique GUID.
_partition(**{"unique-guid": '"not-a-guid"'}),
],
)
def test_schema_rejects_invalid_named_fields(tmp_path, text):
with pytest.raises(yaml_loader.YamlParseError):
load(_write(tmp_path, "bad.yaml", text))
Loading