Skip to content

Commit

Permalink
Add support for RAUC file parsing
Browse files Browse the repository at this point in the history
Allow parsing of RAUC files using `PySquashFsImage`.  DB still needs migration.
  • Loading branch information
UpstreamData committed Sep 10, 2024
1 parent f7a0ce7 commit c0e60fd
Show file tree
Hide file tree
Showing 8 changed files with 125 additions and 36 deletions.
16 changes: 16 additions & 0 deletions goosebit/db/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,28 @@ class Hardware(Model):
revision = fields.CharField(max_length=255)


class SoftwareImageFormat(IntEnum):
SWU = 0
RAUC = 1

def __str__(self):
return self.name.upper()

@classmethod
def from_str(cls, name):
try:
return cls[name.upper()]
except KeyError:
return cls.SWU


class Software(Model):
id = fields.IntField(primary_key=True)
uri = fields.CharField(max_length=255)
size = fields.BigIntField()
hash = fields.CharField(max_length=255)
version = fields.CharField(max_length=255)
image_format = fields.IntEnumField(SoftwareImageFormat, default=SoftwareImageFormat.SWU)
compatibility = fields.ManyToManyField(
"models.Hardware",
related_name="softwares",
Expand Down
2 changes: 1 addition & 1 deletion goosebit/ui/templates/software.html.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
<div class="col">
<input class="form-control"
type="file"
accept=".swu"
accept=".swu,.raucb"
id="file-upload"
name="file" />
</div>
Expand Down
11 changes: 6 additions & 5 deletions goosebit/updates/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from fastapi.requests import Request
from tortoise.expressions import Q

from goosebit.db.models import Hardware, Software
from goosebit.db.models import Hardware, Software, SoftwareImageFormat
from goosebit.updater.manager import UpdateManager

from ..settings import config
Expand All @@ -20,10 +20,10 @@ async def create_software_update(uri: str, temp_file: Path | None) -> Software:

# parse swu header into update_info
if parsed_uri.scheme == "file" and temp_file is not None:
try:
update_info = await swdesc.parse_file(temp_file)
except Exception:
raise HTTPException(422, "Software swu header cannot be parsed")
# try:
update_info = await swdesc.parse_file(temp_file)
# except Exception:
# raise HTTPException(422, "Software swu header cannot be parsed")

elif parsed_uri.scheme.startswith("http"):
try:
Expand Down Expand Up @@ -57,6 +57,7 @@ async def create_software_update(uri: str, temp_file: Path | None) -> Software:
version=str(update_info["version"]),
size=update_info["size"],
hash=update_info["hash"],
image_format=SoftwareImageFormat.from_str(update_info["image_format"]),
)

# create compatibility information
Expand Down
32 changes: 32 additions & 0 deletions goosebit/updates/swdesc/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import logging

import aiofiles
import httpx
from anyio import Path, open_file
from . import rauc, swu

logger = logging.getLogger(__name__)


async def parse_remote(url: str):
async with httpx.AsyncClient() as c:
file = await c.get(url)

Check failure

Code scanning / CodeQL

Full server-side request forgery Critical

The full URL of this request depends on a
user-provided value
.
The full URL of this request depends on a
user-provided value
.
async with aiofiles.tempfile.NamedTemporaryFile("w+b") as f:
await f.write(file.content)
return await parse_file(Path(str(f.name)))


async def parse_file(file: Path):
async with await open_file(file, "r+b") as f:
magic = await f.read(4)
if magic == swu.MAGIC:
image_format = "SWU"
attributes = await swu.parse_file(file)
elif magic == rauc.MAGIC:
image_format = "RAUC"
attributes = await rauc.parse_file(file)
else:
logger.warning(f"Unknown file format, magic={magic}")
raise ValueError(f"Unknown file format, magic={magic}")
attributes["image_format"] = image_format
return attributes
19 changes: 19 additions & 0 deletions goosebit/updates/swdesc/func.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import hashlib

from anyio import AsyncFile


async def sha1_hash_file(fileobj: AsyncFile):
last = await fileobj.tell()
await fileobj.seek(0)
sha1_hash = hashlib.sha1()
buf = bytearray(2**18)
view = memoryview(buf)
while True:
size = await fileobj.readinto(buf)
if size == 0:
break
sha1_hash.update(view[:size])

await fileobj.seek(last)
return sha1_hash.hexdigest()
42 changes: 42 additions & 0 deletions goosebit/updates/swdesc/rauc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import configparser
import logging

import semver
from PySquashfsImage import SquashFsImage
from anyio import Path, open_file

from .func import sha1_hash_file

MAGIC = b"hsqs"

logger = logging.getLogger(__name__)


async def parse_file(file: Path):
async with await open_file(file, "r+b") as f:
image_data = await f.read()

image = SquashFsImage.from_bytes(image_data)
manifest = image.select("manifest.raucm")
manifest_str = manifest.read_bytes().decode("utf-8")
config = configparser.ConfigParser()
config.read_string(manifest_str)
swdesc_attrs = parse_descriptor(config)

stat = await file.stat()
swdesc_attrs["size"] = stat.st_size
swdesc_attrs["hash"] = await sha1_hash_file(f)
return swdesc_attrs


def parse_descriptor(manifest: configparser.ConfigParser):
swdesc_attrs = {}
try:
# specified as optional in the RAUC docs
swdesc_attrs["version"] = semver.Version.parse(manifest["update"].get("version") + ".0")
swdesc_attrs["compatibility"] = [{"hw_model": "default", "hw_revision": manifest["update"]["compatible"]}]
except KeyError as e:
logger.warning(f"Parsing RAUC descriptor failed, error={e}")
raise ValueError("Parsing RAUC descriptor failed", e)

return swdesc_attrs
38 changes: 8 additions & 30 deletions goosebit/updates/swdesc.py → goosebit/updates/swdesc/swu.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import hashlib
import logging
from typing import Any

import aiofiles
import httpx
import libconf
import semver
from anyio import AsyncFile, Path, open_file
from anyio import Path, open_file
from .func import sha1_hash_file


logger = logging.getLogger(__name__)

MAGIC = b"0707"


def _append_compatibility(boardname, value, compatibility):
if "hardware-compatibility" in value:
Expand All @@ -34,7 +35,7 @@ def parse_descriptor(swdesc: libconf.AttrDict[Any, Any | None]):

swdesc_attrs["compatibility"] = compatibility
except KeyError as e:
logging.warning(f"Parsing swu descriptor failed, error={e}")
logger.warning(f"Parsing swu descriptor failed, error={e}")
raise ValueError("Parsing swu descriptor failed", e)

return swdesc_attrs
Expand All @@ -61,29 +62,6 @@ async def parse_file(file: Path):
swdesc_attrs = parse_descriptor(swdesc)
stat = await file.stat()
swdesc_attrs["size"] = stat.st_size
swdesc_attrs["hash"] = await _sha1_hash_file(f)
return swdesc_attrs
swdesc_attrs["hash"] = await sha1_hash_file(f)


async def parse_remote(url: str):
async with httpx.AsyncClient() as c:
file = await c.get(url)
async with aiofiles.tempfile.NamedTemporaryFile("w+b") as f:
await f.write(file.content)
return await parse_file(Path(str(f.name)))


async def _sha1_hash_file(fileobj: AsyncFile):
last = await fileobj.tell()
await fileobj.seek(0)
sha1_hash = hashlib.sha1()
buf = bytearray(2**18)
view = memoryview(buf)
while True:
size = await fileobj.readinto(buf)
if size == 0:
break
sha1_hash.update(view[:size])

await fileobj.seek(last)
return sha1_hash.hexdigest()
return swdesc_attrs
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ httpx = "^0.27.0"
pydantic-settings = "^2.4.0"

asyncpg = { version = "^0.29.0", optional = true }
pysquashfsimage = "^0.9.0"

[tool.poetry.extras]
postgresql = ["asyncpg"]
Expand Down

0 comments on commit c0e60fd

Please sign in to comment.