Skip to content

Commit

Permalink
Use field metadata instead of class variable dictionaries
Browse files Browse the repository at this point in the history
  • Loading branch information
Zedeldi committed Dec 3, 2024
1 parent 7d90bd7 commit 5c2c2d1
Show file tree
Hide file tree
Showing 9 changed files with 299 additions and 289 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,10 @@ Most of the higher-level models are taken directly from `igelsdk.h`, with added
`BaseBytesModel` provides an abstract base class, with concrete methods for handling bytes, shared across various models.

`BaseDataModel` is the parent class for all higher-level models.
For these models to be instantiated directly from bytes, they must define `MODEL_ATTRIBUTE_SIZES`
as a mapping of dataclass field names to the length of bytes to read.
For these models to be instantiated directly from bytes, they must define fields
with metadata containing the size of bytes to read.
To set default values when instantiating models from nothing with `new`,
add the value to the `DEFAULT_VALUES` dictionary of the model.
add the `default` value to the metadata of the field.

#### Section

Expand Down
62 changes: 51 additions & 11 deletions igelfs/models/base.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,45 @@
"""Concrete base classes for various data models."""

import io
from collections.abc import Mapping
from dataclasses import Field, dataclass
from typing import Any, ClassVar, get_args, get_origin
from typing import Any, Iterator, get_args, get_origin

from igelfs.models.abc import BaseBytesModel
from igelfs.models.collections import DataModelCollection
from igelfs.models.mixins import DataclassMixin
from igelfs.utils import replace_bytes


@dataclass
class DataModelMetadata(Mapping, DataclassMixin):
"""
Dataclass to provide metadata for data models.
The metadata for fields must be a mapping. This dataclass is used
to provide a specification for attribute names instead of a dictionary.
"""

size: int
default: Any = None

def __getitem__(self, key: str) -> Any:
"""Implement get item method for metadata."""
return self.to_dict(shallow=True)[key]

def __iter__(self) -> Iterator[str]:
"""Implement iterating through metadata."""
yield from self.to_dict(shallow=True)

def __len__(self) -> int:
"""Implement getting length of metadata."""
return len(self.to_dict(shallow=True))


@dataclass
class BaseDataModel(BaseBytesModel, DataclassMixin):
"""Concrete base class for data model."""

MODEL_ATTRIBUTE_SIZES: ClassVar[dict[str, int]]
DEFAULT_VALUES: ClassVar[dict[str, Any]]

def __len__(self) -> int:
"""Implement __len__ data model method."""
return self.get_actual_size()
Expand All @@ -37,24 +60,40 @@ def to_bytes(self) -> bytes:
fd.seek(0)
return fd.read()

@classmethod
def _get_attribute_metadata(
cls: type["BaseDataModel"],
) -> dict[str, Mapping[str, Any]]:
"""Return dictionary of attribute metadata."""
return {field.name: field.metadata for field in cls.get_fields(init_only=True)}

@classmethod
def _get_attribute_metadata_by_name(
cls: type["BaseDataModel"], name: str
) -> Mapping[str, Any]:
"""Return metadata for specified attribute."""
return cls._get_attribute_metadata()[name]

@classmethod
def get_model_size(cls: type["BaseDataModel"]) -> int:
"""Return expected total size of data for model."""
return sum(cls.MODEL_ATTRIBUTE_SIZES.values())
return sum(
metadata["size"] for metadata in cls._get_attribute_metadata().values()
)

@classmethod
def get_attribute_size(cls: type["BaseDataModel"], name: str) -> int:
"""Return size of data for attribute."""
return cls.MODEL_ATTRIBUTE_SIZES[name]
return cls._get_attribute_metadata_by_name(name)["size"]

@classmethod
def get_attribute_offset(cls: type["BaseDataModel"], name: str) -> int:
"""Return offset of bytes for attribute."""
offset = 0
for attribute, size in cls.MODEL_ATTRIBUTE_SIZES.items():
for attribute, metadata in cls._get_attribute_metadata().items():
if attribute == name:
return offset
offset += size
offset += metadata["size"]
else:
raise KeyError(f"Attribute '{name}' not found")

Expand Down Expand Up @@ -133,9 +172,10 @@ def from_bytes_with_remaining(
def _get_default_bytes(cls: type["BaseDataModel"]) -> bytes:
"""Return default bytes for new data model."""
data = bytes(cls.get_model_size())
if not hasattr(cls, "DEFAULT_VALUES"):
return data
for name, value in cls.DEFAULT_VALUES.items():
for name, metadata in cls._get_attribute_metadata().items():
value = metadata.get("default")
if not value:
continue
if callable(value):
value = value()
offset = cls.get_attribute_offset(name)
Expand Down
94 changes: 41 additions & 53 deletions igelfs/models/boot_registry.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
"""Data models for the boot registry of a filesystem image."""

from abc import abstractmethod
from dataclasses import dataclass
from dataclasses import dataclass, field
from datetime import datetime
from functools import partial
from typing import ClassVar

from igelfs.constants import BOOTREG_IDENT, BOOTREG_MAGIC, IGEL_BOOTREG_SIZE
from igelfs.models.base import BaseDataModel
from igelfs.models.base import BaseDataModel, DataModelMetadata
from igelfs.models.collections import DataModelCollection


Expand All @@ -21,10 +20,10 @@ def generate_boot_id() -> str:
class BootRegistryEntry(BaseDataModel):
"""Dataclass to describe each entry of boot registry."""

MODEL_ATTRIBUTE_SIZES: ClassVar[dict[str, int]] = {"flag": 2, "data": 62}

flag: int # first 9 bits next, 1 bit next present, 6 bit len key
data: bytes
flag: int = field( # first 9 bits next, 1 bit next present, 6 bit len key
metadata=DataModelMetadata(size=2)
)
data: bytes = field(metadata=DataModelMetadata(size=62))

@property
def _flag_bits(self) -> tuple[str, str, str]:
Expand All @@ -38,7 +37,7 @@ def _flag_bits(self) -> tuple[str, str, str]:
bits = (
bin(
int(
self.flag.to_bytes(self.MODEL_ATTRIBUTE_SIZES["flag"], "big").hex(),
self.flag.to_bytes(self.get_attribute_size("flag"), "big").hex(),
base=16,
)
)
Expand Down Expand Up @@ -87,8 +86,6 @@ def value(self) -> str:
class BaseBootRegistryHeader(BaseDataModel):
"""Base class for boot registry header."""

MODEL_ATTRIBUTE_SIZES: ClassVar[dict[str, int]] = {"_": IGEL_BOOTREG_SIZE}

def __post_init__(self) -> None:
"""Verify identity string on initialisation."""
if self.ident_legacy != BOOTREG_IDENT:
Expand All @@ -110,39 +107,34 @@ class BootRegistryHeader(BaseBootRegistryHeader):
The boot registry resides in section #0 of the image.
"""

MODEL_ATTRIBUTE_SIZES: ClassVar[dict[str, int]] = {
"ident_legacy": 17,
"magic": 4,
"hdr_version": 1,
"boot_id": 21,
"enc_alg": 1,
"flags": 2,
"empty": 82,
"free": 64,
"used": 64,
"dir": 252,
"reserve": 4,
"entry": 504 * BootRegistryEntry.get_model_size(),
}
DEFAULT_VALUES = {
"ident_legacy": BOOTREG_IDENT,
"magic": BOOTREG_MAGIC,
"hdr_version": 1,
"boot_id": generate_boot_id,
}

ident_legacy: str # "IGEL BOOTREGISTRY"
magic: str # BOOTREG_MAGIC
hdr_version: int # 0x01 for the first
boot_id: str # boot_id
enc_alg: int # encryption algorithm
flags: int # flags
empty: bytes # placeholder
free: bytes # bitmap with free 64 byte blocks
used: bytes # bitmap with used 64 byte blocks
dir: bytes # directory bitmap (4 bits for each block -> key len)
reserve: bytes # placeholder
entry: DataModelCollection[BootRegistryEntry] # real data
ident_legacy: str = field( # "IGEL BOOTREGISTRY"
metadata=DataModelMetadata(size=17, default=BOOTREG_IDENT)
)
magic: str = field( # BOOTREG_MAGIC
metadata=DataModelMetadata(size=4, default=BOOTREG_MAGIC)
)
hdr_version: int = field( # 0x01 for the first
metadata=DataModelMetadata(size=1, default=1)
)
boot_id: str = field( # boot_id
metadata=DataModelMetadata(size=21, default=generate_boot_id)
)
enc_alg: int = field(metadata=DataModelMetadata(size=1)) # encryption algorithm
flags: int = field(metadata=DataModelMetadata(size=2)) # flags
empty: bytes = field(metadata=DataModelMetadata(size=82)) # placeholder
free: bytes = field( # bitmap with free 64 byte blocks
metadata=DataModelMetadata(size=64)
)
used: bytes = field( # bitmap with used 64 byte blocks
metadata=DataModelMetadata(size=64)
)
dir: bytes = field( # directory bitmap (4 bits for each block -> key len)
metadata=DataModelMetadata(size=252)
)
reserve: bytes = field(metadata=DataModelMetadata(size=4)) # placeholder
entry: DataModelCollection[BootRegistryEntry] = field( # real data
metadata=DataModelMetadata(size=504 * BootRegistryEntry.get_model_size())
)

def __post_init__(self) -> None:
"""Verify magic string on initialisation."""
Expand Down Expand Up @@ -178,14 +170,10 @@ class BootRegistryHeaderLegacy(BaseBootRegistryHeader):
The boot registry resides in section #0 of the image.
"""

MODEL_ATTRIBUTE_SIZES: ClassVar[dict[str, int]] = {
"ident_legacy": 17,
"entry": IGEL_BOOTREG_SIZE - 17,
}
DEFAULT_VALUES = {"ident_legacy": BOOTREG_IDENT}

ident_legacy: str
entry: bytes
ident_legacy: str = field(
metadata=DataModelMetadata(size=17, default=BOOTREG_IDENT)
)
entry: bytes = field(metadata=DataModelMetadata(size=IGEL_BOOTREG_SIZE - 17))

def get_entries(self) -> dict[str, str]:
"""Return dictionary of all boot registry entries."""
Expand All @@ -209,8 +197,8 @@ class BootRegistryHeaderFactory:
@staticmethod
def is_legacy_boot_registry(data: bytes) -> bool:
"""Return whether bytes represent a legacy boot registry header."""
ident_legacy = BootRegistryHeader.MODEL_ATTRIBUTE_SIZES["ident_legacy"]
magic = BootRegistryHeader.MODEL_ATTRIBUTE_SIZES["magic"]
ident_legacy = BootRegistryHeader.get_attribute_size("ident_legacy")
magic = BootRegistryHeader.get_attribute_size("magic")
if data[ident_legacy : ident_legacy + magic].decode() == BOOTREG_MAGIC:
return False
return True
Expand Down
26 changes: 9 additions & 17 deletions igelfs/models/bootsplash.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,23 @@
"""Data models for bootsplash structures."""

import io
from dataclasses import dataclass
from typing import ClassVar
from dataclasses import dataclass, field

from PIL import Image

from igelfs.constants import BOOTSPLASH_MAGIC
from igelfs.models.base import BaseDataGroup, BaseDataModel
from igelfs.models.base import BaseDataGroup, BaseDataModel, DataModelMetadata
from igelfs.models.collections import DataModelCollection


@dataclass
class BootsplashHeader(BaseDataModel):
"""Dataclass to handle bootsplash header data."""

MODEL_ATTRIBUTE_SIZES: ClassVar[dict[str, int]] = {"magic": 14, "num_splashs": 1}
DEFAULT_VALUES = {"magic": BOOTSPLASH_MAGIC}

magic: str # BOOTSPLASH_MAGIC
num_splashs: int
magic: str = field( # BOOTSPLASH_MAGIC
metadata=DataModelMetadata(size=14, default=BOOTSPLASH_MAGIC)
)
num_splashs: int = field(metadata=DataModelMetadata(size=1))

def __post_init__(self) -> None:
"""Verify magic string on initialisation."""
Expand All @@ -31,15 +29,9 @@ def __post_init__(self) -> None:
class Bootsplash(BaseDataModel):
"""Dataclass to handle bootsplash data."""

MODEL_ATTRIBUTE_SIZES: ClassVar[dict[str, int]] = {
"offset": 8,
"length": 8,
"ident": 8,
}

offset: int
length: int
ident: bytes
offset: int = field(metadata=DataModelMetadata(size=8))
length: int = field(metadata=DataModelMetadata(size=8))
ident: bytes = field(metadata=DataModelMetadata(size=8))


@dataclass
Expand Down
Loading

0 comments on commit 5c2c2d1

Please sign in to comment.