From bf42afb4fa2173187c12c0e0c7cd3cfd3311470b Mon Sep 17 00:00:00 2001 From: JSCU-CNI <121175071+JSCU-CNI@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:58:18 +0200 Subject: [PATCH 1/4] Add bbolt implementation --- dissect/database/bbolt/__init__.py | 8 ++ dissect/database/bbolt/bbolt.py | 210 +++++++++++++++++++++++++++++ dissect/database/bbolt/c_bbolt.py | 67 +++++++++ dissect/database/bbolt/c_bbolt.pyi | 107 +++++++++++++++ tests/_data/bbolt/meta.db | 3 + tests/_data/bbolt/metadata.db | 3 + tests/bbolt/__init__.py | 0 tests/bbolt/test_bbolt.py | 101 ++++++++++++++ 8 files changed, 499 insertions(+) create mode 100644 dissect/database/bbolt/__init__.py create mode 100755 dissect/database/bbolt/bbolt.py create mode 100755 dissect/database/bbolt/c_bbolt.py create mode 100644 dissect/database/bbolt/c_bbolt.pyi create mode 100644 tests/_data/bbolt/meta.db create mode 100644 tests/_data/bbolt/metadata.db create mode 100644 tests/bbolt/__init__.py create mode 100755 tests/bbolt/test_bbolt.py diff --git a/dissect/database/bbolt/__init__.py b/dissect/database/bbolt/__init__.py new file mode 100644 index 0000000..f0ae5af --- /dev/null +++ b/dissect/database/bbolt/__init__.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +from dissect.database.bbolt.bbolt import Bbolt + +BoltDB = Bbolt +"""Alias Bbolt to BoltDB.""" + +__all__ = ["Bbolt", "BoltDB"] diff --git a/dissect/database/bbolt/bbolt.py b/dissect/database/bbolt/bbolt.py new file mode 100755 index 0000000..c7cbe2a --- /dev/null +++ b/dissect/database/bbolt/bbolt.py @@ -0,0 +1,210 @@ +from __future__ import annotations + +from pathlib import Path +from typing import TYPE_CHECKING, BinaryIO, Literal + +from dissect.database.bbolt.c_bbolt import c_bbolt + +if TYPE_CHECKING: + from collections.abc import Iterable + + +class Bbolt: + """bbolt database implementation. + + References: + - https://github.com/etcd-io/bbolt + - https://www.qtmuniao.com/en/2020/11/29/bolt-data-organised/ + - https://www.qtmuniao.com/en/2020/12/14/bolt-index-design/ + """ + + def __init__(self, fh: BinaryIO | Path) -> None: + if isinstance(fh, Path): + self.path = fh + self.fh = fh.open("rb") + elif hasattr(fh, "read"): + self.path = None + self.fh = fh + else: + raise TypeError("Argument fh must be Path or BytesIO-like object") + + self.meta = c_bbolt.Meta(self.fh) + self.version = self.meta.version + self.page_size = self.meta.pageSize + + if self.meta.magic != c_bbolt.Magic: + raise ValueError(f"Unexpected bbolt meta magic value {self.meta.magic:x}") + + self.root = Page(self, self.meta.root.root) + + def __repr__(self) -> str: + return f"" + + def _iter(self, type: Literal["page", "value"], path: str | None, sep: str) -> Page | bytes | None: + """Iterate over :class:`Page`` and :class:`Inode` until the requested page or key value is found.""" + # Return the root page if no path was given + if not path or path == sep: + return self.root + + page = self.root + parts = path.split(sep) + for i, part in enumerate(parts): + found = False + + for inode in page.inodes(): + if inode.key == part: + if inode.flags == 1: + if inode.value.startswith(16 * b"\x00"): + page = Page(self, 0, inode._value_offset + 0x10) + else: + pgid = c_bbolt.InBucket(inode.value).root if not inode.pgid else inode.pgid + page = Page(self, pgid) + + # If the new page has an inode 0 with flags=BucketLeafFlag, read that bucket instead + if page.count and (inode_nested := page.inode(0)).flags == 1 and inode_nested.key == part: + page = Page(self, 0, inode_nested._value_offset + 0x10) + + # Spec dictates we should ignore pages with no inodes + if not page.count: + continue + + if i + 1 == len(parts): + if type == "page": + return page + if type == "value" and inode.value: + return inode.value + + # We can stop iterating over more inodes of this page, continue to the next path part + found = True + break + + # Return early if we did not find the current part in any of the inodes in this page + if not found: + return None + + return None + + def keys(self, path: str | None = None, *, sep: str = " ") -> list[str] | None: + """Get a list of key names from the given path.""" + if not (page := self._iter("page", path, sep)): + return None + return [child.key for child in page.inodes()] + + def get(self, path: str | None, *, sep: str = " ", decode: bool = True) -> str | bytes | None: + """Get a key value from the given path.""" + if not (value := self._iter("value", path, sep)): + return None + return value.decode() if decode else value + + +class Page: + """Represents a bbolt page (meta, freelist, branch or leaf).""" + + def __init__(self, db: Bbolt, pgid: int, offset: int | None = None) -> None: + self.db = db + self.pgid = pgid + + if not offset: + self.offset = db.fh.seek(pgid * db.page_size) + else: + self.offset = db.fh.seek(offset) + + self.page = c_bbolt.Page(db.fh) + + self.flags = self.page.flags + self.count = self.page.count + self.overflow = self.page.overflow + + if self.flags == c_bbolt.PageFlag.Leaf and self.count: + self.key = self.inode(0).key + else: + self.key = None + + def __repr__(self) -> str: + return f"" # noqa: E501 + + def inode(self, i: int) -> Inode: + """Return the :class:`Inode` corresponding to the given index number.""" + if i >= self.count or i < 0: + raise ValueError(f"invalid inode number: got {i}, should be 0-{self.count - 1}") + + offset = self.offset + self.page.size + (i * 16) + + if self.flags == c_bbolt.PageFlag.Leaf: + return InodeLeaf(self, c_bbolt.leafPageElement, offset) + + if self.flags == c_bbolt.PageFlag.Branch: + return InodeBranch(self, c_bbolt.branchPageElement, offset) + + raise NotImplementedError(self.pgid, self.flags) + + def inodes(self) -> Iterable[InodeLeaf]: + """Yield all :class:`Inode` present in this :class:`Page`. + + References: + - internal/common/inode.go @ ReadInodeFromPage + """ + offset = self.offset + self.page.size + for i in range(self.count): + element_offset = offset + (i * 16) + + if self.flags == c_bbolt.PageFlag.Leaf: + yield InodeLeaf(self, c_bbolt.leafPageElement, element_offset) + elif self.flags == c_bbolt.PageFlag.Branch: + # For easier enumeration we iterate all inodes from the page this branch inode points to, + # and do not yield the branch inode. + branch = InodeBranch(self, c_bbolt.branchPageElement, element_offset) + page = Page(self.db, branch.pgid) + yield from page.inodes() + else: + raise NotImplementedError(self.flags) + + +class Inode: + """Represents a bbolt Inode branch or leaf element. + + Only for internal use, see :class:`InodeLeaf` and :class:`InodeBranch`. + """ + + def __init__(self, page: Page, structure: c_bbolt.leafPageElement | c_bbolt.branchPageElement, offset: int) -> None: + self.page = page + self.offset = offset + + page.db.fh.seek(offset) + element = structure(page.db.fh) + self.element = element + + self.flags = None + self.value = None + self.pgid = None + + page.db.fh.seek(offset) + page.db.fh.read(element.pos) + + self.key = page.db.fh.read(element.ksize).decode() + + +class InodeLeaf(Inode): + """Represents a bbolt Inode leaf element.""" + + def __init__(self, page: Page, structure: c_bbolt.leafPageElement, offset: int) -> None: + super().__init__(page, structure, offset) + + self.flags = self.element.flags + self._value_offset = page.db.fh.tell() + self.value = page.db.fh.read(self.element.vsize) + + def __repr__(self) -> str: + return f"" + + +class InodeBranch(Inode): + """Represents a bbolt Inode branch element.""" + + def __init__(self, page: Page, structure: c_bbolt.branchPageElement, offset: int) -> None: + super().__init__(page, structure, offset) + + self.pgid = self.element.pgid + + def __repr__(self) -> str: + return f"" diff --git a/dissect/database/bbolt/c_bbolt.py b/dissect/database/bbolt/c_bbolt.py new file mode 100755 index 0000000..0fce165 --- /dev/null +++ b/dissect/database/bbolt/c_bbolt.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +from dissect.cstruct import cstruct + +# References: +# - https://github.com/etcd-io/bbolt/blob/main/internal/common/types.go +# - https://github.com/etcd-io/bbolt/blob/main/internal/common/meta.go +# - https://github.com/etcd-io/bbolt/blob/main/internal/common/bucket.go +# - https://github.com/etcd-io/bbolt/blob/main/internal/common/page.go +bbolt_def = """ +typedef uint64 Pgid; // page identifier +typedef uint64 Txid; // transaction identifier + +#define Magic 0xED0CDAED + +enum PageFlag: uint16 { + Branch = 0x01, + Leaf = 0x02, + Meta = 0x04, + Freelist = 0x10, +}; + +struct InBucket { + Pgid root; + uint64 sequence; +}; + +struct Page { + Pgid id; + PageFlag flags; // type of the page: branch, leaf, meta or freelist + uint16 count; // number of key/value pairs in this page + uint32 overflow; // number of overflow pages +}; + +struct Meta { + Page page; + uint32 magic; + uint32 version; + uint32 pageSize; + uint32 flags; + InBucket root; + Pgid freelist; + Pgid pgid; + Txid txid; + uint64 checksum; +}; + +struct branchPageElement { + uint32 pos; // relative + uint32 ksize; + Pgid pgid; +}; + +struct leafPageElement { + uint32 flags; // 0x01 = BucketLeafFlag + uint32 pos; // relative + uint32 ksize; + uint32 vsize; +}; + +struct Inode { + uint32 flags; + Pgid pgid; +}; +""" + +c_bbolt = cstruct().load(bbolt_def) diff --git a/dissect/database/bbolt/c_bbolt.pyi b/dissect/database/bbolt/c_bbolt.pyi new file mode 100644 index 0000000..008421f --- /dev/null +++ b/dissect/database/bbolt/c_bbolt.pyi @@ -0,0 +1,107 @@ +# Generated by cstruct-stubgen +from typing import BinaryIO, Literal, TypeAlias, overload + +import dissect.cstruct as __cs__ + +class _c_bbolt(__cs__.cstruct): + Magic: Literal[3977042669] = ... + Pgid: TypeAlias = _c_bbolt.uint64 + Txid: TypeAlias = _c_bbolt.uint64 + class PageFlag(__cs__.Enum): + Branch = ... + Leaf = ... + Meta = ... + Freelist = ... + + class InBucket(__cs__.Structure): + root: _c_bbolt.uint64 + sequence: _c_bbolt.uint64 + @overload + def __init__(self, root: _c_bbolt.uint64 | None = ..., sequence: _c_bbolt.uint64 | None = ...): ... + @overload + def __init__(self, fh: bytes | memoryview | bytearray | BinaryIO, /): ... + + class Page(__cs__.Structure): + id: _c_bbolt.uint64 + flags: _c_bbolt.PageFlag + count: _c_bbolt.uint16 + overflow: _c_bbolt.uint32 + @overload + def __init__( + self, + id: _c_bbolt.uint64 | None = ..., + flags: _c_bbolt.PageFlag | None = ..., + count: _c_bbolt.uint16 | None = ..., + overflow: _c_bbolt.uint32 | None = ..., + ): ... + @overload + def __init__(self, fh: bytes | memoryview | bytearray | BinaryIO, /): ... + + class Meta(__cs__.Structure): + page: _c_bbolt.Page + magic: _c_bbolt.uint32 + version: _c_bbolt.uint32 + pageSize: _c_bbolt.uint32 + flags: _c_bbolt.uint32 + root: _c_bbolt.InBucket + freelist: _c_bbolt.uint64 + pgid: _c_bbolt.uint64 + txid: _c_bbolt.uint64 + checksum: _c_bbolt.uint64 + @overload + def __init__( + self, + page: _c_bbolt.Page | None = ..., + magic: _c_bbolt.uint32 | None = ..., + version: _c_bbolt.uint32 | None = ..., + pageSize: _c_bbolt.uint32 | None = ..., + flags: _c_bbolt.uint32 | None = ..., + root: _c_bbolt.InBucket | None = ..., + freelist: _c_bbolt.uint64 | None = ..., + pgid: _c_bbolt.uint64 | None = ..., + txid: _c_bbolt.uint64 | None = ..., + checksum: _c_bbolt.uint64 | None = ..., + ): ... + @overload + def __init__(self, fh: bytes | memoryview | bytearray | BinaryIO, /): ... + + class branchPageElement(__cs__.Structure): + pos: _c_bbolt.uint32 + ksize: _c_bbolt.uint32 + pgid: _c_bbolt.uint64 + @overload + def __init__( + self, + pos: _c_bbolt.uint32 | None = ..., + ksize: _c_bbolt.uint32 | None = ..., + pgid: _c_bbolt.uint64 | None = ..., + ): ... + @overload + def __init__(self, fh: bytes | memoryview | bytearray | BinaryIO, /): ... + + class leafPageElement(__cs__.Structure): + flags: _c_bbolt.uint32 + pos: _c_bbolt.uint32 + ksize: _c_bbolt.uint32 + vsize: _c_bbolt.uint32 + @overload + def __init__( + self, + flags: _c_bbolt.uint32 | None = ..., + pos: _c_bbolt.uint32 | None = ..., + ksize: _c_bbolt.uint32 | None = ..., + vsize: _c_bbolt.uint32 | None = ..., + ): ... + @overload + def __init__(self, fh: bytes | memoryview | bytearray | BinaryIO, /): ... + + class Inode(__cs__.Structure): + flags: _c_bbolt.uint32 + pgid: _c_bbolt.uint64 + @overload + def __init__(self, flags: _c_bbolt.uint32 | None = ..., pgid: _c_bbolt.uint64 | None = ...): ... + @overload + def __init__(self, fh: bytes | memoryview | bytearray | BinaryIO, /): ... + +# Technically `c_bbolt` is an instance of `_c_bbolt`, but then we can't use it in type hints +c_bbolt: TypeAlias = _c_bbolt diff --git a/tests/_data/bbolt/meta.db b/tests/_data/bbolt/meta.db new file mode 100644 index 0000000..b372c16 --- /dev/null +++ b/tests/_data/bbolt/meta.db @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:71ca2cb553eff30e6ac654b8fc49025b31fda3cacf5901499275ad078a4ff493 +size 262144 diff --git a/tests/_data/bbolt/metadata.db b/tests/_data/bbolt/metadata.db new file mode 100644 index 0000000..c343897 --- /dev/null +++ b/tests/_data/bbolt/metadata.db @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c6180b05a88417fcecda4b9decaf906314c7e9ceba9102ebc59c6d833bbea15b +size 65536 diff --git a/tests/bbolt/__init__.py b/tests/bbolt/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/bbolt/test_bbolt.py b/tests/bbolt/test_bbolt.py new file mode 100755 index 0000000..18bd840 --- /dev/null +++ b/tests/bbolt/test_bbolt.py @@ -0,0 +1,101 @@ +from __future__ import annotations + +from dissect.database.bbolt import Bbolt, BoltDB +from tests._util import absolute_path + +container_id = "5fc9c48c9ee7a72c4e733a19c0388e6d7b26413fd0949f855067bfb8dd2d2181" + + +def test_bbolt_meta_db() -> None: + """Test if we can parse a containerd meta.db file. Created on amd64 Debian 13.6.0 with Docker 29.7.2.""" + path = absolute_path("_data/bbolt/meta.db") + db = Bbolt(path) + + assert db.version == 2 + assert db.page_size == 4096 + + assert db.keys() == ["v1"] + assert db.keys("v1") == ["moby", "version"] + assert db.get("v1 version") == "\x08" + assert db.keys("v1 moby") == [ + "containers", + "content", + "images", + "leases", + "snapshots", + ] + + assert db.keys("v1\\moby", sep="\\") == [ + "containers", + "content", + "images", + "leases", + "snapshots", + ] + + assert db.keys(f"v1 moby snapshots overlayfs {container_id}") == [ + "createdat", + "name", + "parent", + "updatedat", + ] + + name = db.get(f"v1 moby snapshots overlayfs {container_id} name") + assert name == f"moby/18/{container_id}" + + assert db.keys("v1 moby containers") == [container_id] + + assert db.keys(f"v1 moby containers {container_id}") == [ + "createdat", + "image", + "labels", + "runtime", + "sandboxid", + "snapshotKey", + "snapshotter", + "spec", + "updatedat", + ] + assert db.keys(f"v1 moby containers {container_id} labels") == ["com.docker/engine.bundle.path"] + assert ( + db.get(f"v1 moby containers {container_id} labels com.docker/engine.bundle.path") + == f"/var/run/docker/containerd/{container_id}" + ) + + +def test_bbolt_metadata_db() -> None: + """Test if we can parse a containerd metadata.db file. Created on amd64 Debian 13.6.0 with Docker 29.7.2.""" + path = absolute_path("_data/bbolt/metadata.db") + db = BoltDB(path) + + assert db.keys() == ["v1"] + assert db.keys("v1") == ["parents", "snapshots"] + assert db.keys("v1 snapshots") == [ + "moby/10/36cd9d816e2c23639eee0d4cb933d76ddc482761dd07cddb64a3f9f83c7fbb3f-init", + "moby/11/36cd9d816e2c23639eee0d4cb933d76ddc482761dd07cddb64a3f9f83c7fbb3f", + "moby/13/sha256:b577adbc0b589bb7c1d3be98cc4b703e8228ab4413156754445e8b20a001c5d3", + "moby/15/sha256:82198e809c0011c54f98a18f72fcb1db9d563c8835cff6d37126dbb030a8302c", + f"moby/17/{container_id}-init", + f"moby/18/{container_id}", + "moby/2/sha256:897b3f2a7c1bc2f3d02432f7892fe31c6272c521ad4d70257df624504a3238b4", + "moby/4/13553351c913076aa74e1ef76519aa82127cf85121413eb5fe5002731d97556c-init", + "moby/5/13553351c913076aa74e1ef76519aa82127cf85121413eb5fe5002731d97556c", + "moby/7/abf09ccc5e9d25015bd042a3139f9ecd387c711f81ff5fde20093515b9825a5b-init", + "moby/8/abf09ccc5e9d25015bd042a3139f9ecd387c711f81ff5fde20093515b9825a5b", + ] + + assert db.keys(f"v1 snapshots moby/18/{container_id}") == [ + "createdat", + "id", + "kind", + "parent", + "updatedat", + ] + + id = db.get(f"v1 snapshots moby/18/{container_id} id", decode=False) + assert id == b"\x0b" + + assert db.get( + f"v1 snapshots moby/18/{container_id} createdat", + decode=False, + ) == bytes.fromhex("010000000ee2221c201f08a2f6ffff") From 6aed632d49d048af76820e420061f32016d15f00 Mon Sep 17 00:00:00 2001 From: JSCU-CNI <121175071+JSCU-CNI@users.noreply.github.com> Date: Thu, 27 Aug 2026 17:13:43 +0200 Subject: [PATCH 2/4] Use flags directly --- dissect/database/bbolt/bbolt.py | 22 +++++++++++++--------- dissect/database/bbolt/c_bbolt.py | 4 ++++ dissect/database/bbolt/c_bbolt.pyi | 4 ++++ 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/dissect/database/bbolt/bbolt.py b/dissect/database/bbolt/bbolt.py index c7cbe2a..b9ab772 100755 --- a/dissect/database/bbolt/bbolt.py +++ b/dissect/database/bbolt/bbolt.py @@ -3,7 +3,7 @@ from pathlib import Path from typing import TYPE_CHECKING, BinaryIO, Literal -from dissect.database.bbolt.c_bbolt import c_bbolt +from dissect.database.bbolt.c_bbolt import BucketLeafFlag, PageFlag, c_bbolt if TYPE_CHECKING: from collections.abc import Iterable @@ -53,15 +53,19 @@ def _iter(self, type: Literal["page", "value"], path: str | None, sep: str) -> P for inode in page.inodes(): if inode.key == part: - if inode.flags == 1: + if inode.flags == BucketLeafFlag: if inode.value.startswith(16 * b"\x00"): page = Page(self, 0, inode._value_offset + 0x10) else: pgid = c_bbolt.InBucket(inode.value).root if not inode.pgid else inode.pgid page = Page(self, pgid) - # If the new page has an inode 0 with flags=BucketLeafFlag, read that bucket instead - if page.count and (inode_nested := page.inode(0)).flags == 1 and inode_nested.key == part: + # If the new page has an inode 0 with BucketLeafFlag, read that bucket instead + if ( + page.count + and (inode_nested := page.inode(0)).flags == BucketLeafFlag + and inode_nested.key == part + ): page = Page(self, 0, inode_nested._value_offset + 0x10) # Spec dictates we should ignore pages with no inodes @@ -115,7 +119,7 @@ def __init__(self, db: Bbolt, pgid: int, offset: int | None = None) -> None: self.count = self.page.count self.overflow = self.page.overflow - if self.flags == c_bbolt.PageFlag.Leaf and self.count: + if self.flags == PageFlag.Leaf and self.count: self.key = self.inode(0).key else: self.key = None @@ -130,10 +134,10 @@ def inode(self, i: int) -> Inode: offset = self.offset + self.page.size + (i * 16) - if self.flags == c_bbolt.PageFlag.Leaf: + if self.flags == PageFlag.Leaf: return InodeLeaf(self, c_bbolt.leafPageElement, offset) - if self.flags == c_bbolt.PageFlag.Branch: + if self.flags == PageFlag.Branch: return InodeBranch(self, c_bbolt.branchPageElement, offset) raise NotImplementedError(self.pgid, self.flags) @@ -148,9 +152,9 @@ def inodes(self) -> Iterable[InodeLeaf]: for i in range(self.count): element_offset = offset + (i * 16) - if self.flags == c_bbolt.PageFlag.Leaf: + if self.flags == PageFlag.Leaf: yield InodeLeaf(self, c_bbolt.leafPageElement, element_offset) - elif self.flags == c_bbolt.PageFlag.Branch: + elif self.flags == PageFlag.Branch: # For easier enumeration we iterate all inodes from the page this branch inode points to, # and do not yield the branch inode. branch = InodeBranch(self, c_bbolt.branchPageElement, element_offset) diff --git a/dissect/database/bbolt/c_bbolt.py b/dissect/database/bbolt/c_bbolt.py index 0fce165..f8afee3 100755 --- a/dissect/database/bbolt/c_bbolt.py +++ b/dissect/database/bbolt/c_bbolt.py @@ -12,6 +12,7 @@ typedef uint64 Txid; // transaction identifier #define Magic 0xED0CDAED +#define BucketLeafFlag 0x01 enum PageFlag: uint16 { Branch = 0x01, @@ -65,3 +66,6 @@ """ c_bbolt = cstruct().load(bbolt_def) + +PageFlag = c_bbolt.PageFlag +BucketLeafFlag = c_bbolt.BucketLeafFlag diff --git a/dissect/database/bbolt/c_bbolt.pyi b/dissect/database/bbolt/c_bbolt.pyi index 008421f..07fe2f1 100644 --- a/dissect/database/bbolt/c_bbolt.pyi +++ b/dissect/database/bbolt/c_bbolt.pyi @@ -5,6 +5,7 @@ import dissect.cstruct as __cs__ class _c_bbolt(__cs__.cstruct): Magic: Literal[3977042669] = ... + BucketLeafFlag: Literal[1] = ... Pgid: TypeAlias = _c_bbolt.uint64 Txid: TypeAlias = _c_bbolt.uint64 class PageFlag(__cs__.Enum): @@ -105,3 +106,6 @@ class _c_bbolt(__cs__.cstruct): # Technically `c_bbolt` is an instance of `_c_bbolt`, but then we can't use it in type hints c_bbolt: TypeAlias = _c_bbolt + +PageFlag: TypeAlias = c_bbolt.PageFlag +BucketLeafFlag: Literal[1] From 628504596464f24de8c1f5db66e8e322cedd3030 Mon Sep 17 00:00:00 2001 From: Computer Network Investigation <121175071+JSCU-CNI@users.noreply.github.com> Date: Thu, 27 Aug 2026 17:14:47 +0200 Subject: [PATCH 3/4] Apply suggestion from @Schamper Co-authored-by: Erik Schamper <1254028+Schamper@users.noreply.github.com> --- dissect/database/bbolt/bbolt.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/dissect/database/bbolt/bbolt.py b/dissect/database/bbolt/bbolt.py index b9ab772..f7cfad1 100755 --- a/dissect/database/bbolt/bbolt.py +++ b/dissect/database/bbolt/bbolt.py @@ -79,11 +79,9 @@ def _iter(self, type: Literal["page", "value"], path: str | None, sep: str) -> P return inode.value # We can stop iterating over more inodes of this page, continue to the next path part - found = True break - - # Return early if we did not find the current part in any of the inodes in this page - if not found: + else: + # Return early if we did not find the current part in any of the inodes in this page return None return None From 4693c01864ce85e49d1332b8134928887f39e795 Mon Sep 17 00:00:00 2001 From: JSCU-CNI <121175071+JSCU-CNI@users.noreply.github.com> Date: Thu, 27 Aug 2026 17:20:25 +0200 Subject: [PATCH 4/4] Small cleanup --- dissect/database/bbolt/bbolt.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dissect/database/bbolt/bbolt.py b/dissect/database/bbolt/bbolt.py index f7cfad1..7c83cdd 100755 --- a/dissect/database/bbolt/bbolt.py +++ b/dissect/database/bbolt/bbolt.py @@ -49,13 +49,15 @@ def _iter(self, type: Literal["page", "value"], path: str | None, sep: str) -> P page = self.root parts = path.split(sep) for i, part in enumerate(parts): - found = False - + # Search for the current part in each inode of the current page for inode in page.inodes(): if inode.key == part: if inode.flags == BucketLeafFlag: + # Read the page inside the inode if inode.value.startswith(16 * b"\x00"): page = Page(self, 0, inode._value_offset + 0x10) + + # Read the page this inode points to else: pgid = c_bbolt.InBucket(inode.value).root if not inode.pgid else inode.pgid page = Page(self, pgid)