Skip to content

Commit bafd33c

Browse files
authored
Add bbolt implementation (#63)
1 parent 62f6301 commit bafd33c

8 files changed

Lines changed: 511 additions & 0 deletions

File tree

‎dissect/database/bbolt/__init__.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from __future__ import annotations
2+
3+
from dissect.database.bbolt.bbolt import Bbolt
4+
5+
BoltDB = Bbolt
6+
"""Alias Bbolt to BoltDB."""
7+
8+
__all__ = ["Bbolt", "BoltDB"]

‎dissect/database/bbolt/bbolt.py‎

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
from __future__ import annotations
2+
3+
from pathlib import Path
4+
from typing import TYPE_CHECKING, BinaryIO, Literal
5+
6+
from dissect.database.bbolt.c_bbolt import BucketLeafFlag, PageFlag, c_bbolt
7+
8+
if TYPE_CHECKING:
9+
from collections.abc import Iterable
10+
11+
12+
class Bbolt:
13+
"""bbolt database implementation.
14+
15+
References:
16+
- https://github.com/etcd-io/bbolt
17+
- https://www.qtmuniao.com/en/2020/11/29/bolt-data-organised/
18+
- https://www.qtmuniao.com/en/2020/12/14/bolt-index-design/
19+
"""
20+
21+
def __init__(self, fh: BinaryIO | Path) -> None:
22+
if isinstance(fh, Path):
23+
self.path = fh
24+
self.fh = fh.open("rb")
25+
elif hasattr(fh, "read"):
26+
self.path = None
27+
self.fh = fh
28+
else:
29+
raise TypeError("Argument fh must be Path or BytesIO-like object")
30+
31+
self.meta = c_bbolt.Meta(self.fh)
32+
self.version = self.meta.version
33+
self.page_size = self.meta.pageSize
34+
35+
if self.meta.magic != c_bbolt.Magic:
36+
raise ValueError(f"Unexpected bbolt meta magic value {self.meta.magic:x}")
37+
38+
self.root = Page(self, self.meta.root.root)
39+
40+
def __repr__(self) -> str:
41+
return f"<Bbolt version={self.version} page_size={self.page_size} path={self.path or self.fh}>"
42+
43+
def _iter(self, type: Literal["page", "value"], path: str | None, sep: str) -> Page | bytes | None:
44+
"""Iterate over :class:`Page`` and :class:`Inode` until the requested page or key value is found."""
45+
# Return the root page if no path was given
46+
if not path or path == sep:
47+
return self.root
48+
49+
page = self.root
50+
parts = path.split(sep)
51+
for i, part in enumerate(parts):
52+
# Search for the current part in each inode of the current page
53+
for inode in page.inodes():
54+
if inode.key == part:
55+
if inode.flags == BucketLeafFlag:
56+
# Read the page inside the inode
57+
if inode.value.startswith(16 * b"\x00"):
58+
page = Page(self, 0, inode._value_offset + 0x10)
59+
60+
# Read the page this inode points to
61+
else:
62+
pgid = c_bbolt.InBucket(inode.value).root if not inode.pgid else inode.pgid
63+
page = Page(self, pgid)
64+
65+
# If the new page has an inode 0 with BucketLeafFlag, read that bucket instead
66+
if (
67+
page.count
68+
and (inode_nested := page.inode(0)).flags == BucketLeafFlag
69+
and inode_nested.key == part
70+
):
71+
page = Page(self, 0, inode_nested._value_offset + 0x10)
72+
73+
# Spec dictates we should ignore pages with no inodes
74+
if not page.count:
75+
continue
76+
77+
if i + 1 == len(parts):
78+
if type == "page":
79+
return page
80+
if type == "value" and inode.value:
81+
return inode.value
82+
83+
# We can stop iterating over more inodes of this page, continue to the next path part
84+
break
85+
else:
86+
# Return early if we did not find the current part in any of the inodes in this page
87+
return None
88+
89+
return None
90+
91+
def keys(self, path: str | None = None, *, sep: str = " ") -> list[str] | None:
92+
"""Get a list of key names from the given path."""
93+
if not (page := self._iter("page", path, sep)):
94+
return None
95+
return [child.key for child in page.inodes()]
96+
97+
def get(self, path: str | None, *, sep: str = " ", decode: bool = True) -> str | bytes | None:
98+
"""Get a key value from the given path."""
99+
if not (value := self._iter("value", path, sep)):
100+
return None
101+
return value.decode() if decode else value
102+
103+
104+
class Page:
105+
"""Represents a bbolt page (meta, freelist, branch or leaf)."""
106+
107+
def __init__(self, db: Bbolt, pgid: int, offset: int | None = None) -> None:
108+
self.db = db
109+
self.pgid = pgid
110+
111+
if not offset:
112+
self.offset = db.fh.seek(pgid * db.page_size)
113+
else:
114+
self.offset = db.fh.seek(offset)
115+
116+
self.page = c_bbolt.Page(db.fh)
117+
118+
self.flags = self.page.flags
119+
self.count = self.page.count
120+
self.overflow = self.page.overflow
121+
122+
if self.flags == PageFlag.Leaf and self.count:
123+
self.key = self.inode(0).key
124+
else:
125+
self.key = None
126+
127+
def __repr__(self) -> str:
128+
return f"<Page offset=0x{self.offset:x} pgid={self.pgid} flags={self.flags} count={self.count} overflow={self.overflow} key={self.key}>" # noqa: E501
129+
130+
def inode(self, i: int) -> Inode:
131+
"""Return the :class:`Inode` corresponding to the given index number."""
132+
if i >= self.count or i < 0:
133+
raise ValueError(f"invalid inode number: got {i}, should be 0-{self.count - 1}")
134+
135+
offset = self.offset + self.page.size + (i * 16)
136+
137+
if self.flags == PageFlag.Leaf:
138+
return InodeLeaf(self, c_bbolt.leafPageElement, offset)
139+
140+
if self.flags == PageFlag.Branch:
141+
return InodeBranch(self, c_bbolt.branchPageElement, offset)
142+
143+
raise NotImplementedError(self.pgid, self.flags)
144+
145+
def inodes(self) -> Iterable[InodeLeaf]:
146+
"""Yield all :class:`Inode` present in this :class:`Page`.
147+
148+
References:
149+
- internal/common/inode.go @ ReadInodeFromPage
150+
"""
151+
offset = self.offset + self.page.size
152+
for i in range(self.count):
153+
element_offset = offset + (i * 16)
154+
155+
if self.flags == PageFlag.Leaf:
156+
yield InodeLeaf(self, c_bbolt.leafPageElement, element_offset)
157+
elif self.flags == PageFlag.Branch:
158+
# For easier enumeration we iterate all inodes from the page this branch inode points to,
159+
# and do not yield the branch inode.
160+
branch = InodeBranch(self, c_bbolt.branchPageElement, element_offset)
161+
page = Page(self.db, branch.pgid)
162+
yield from page.inodes()
163+
else:
164+
raise NotImplementedError(self.flags)
165+
166+
167+
class Inode:
168+
"""Represents a bbolt Inode branch or leaf element.
169+
170+
Only for internal use, see :class:`InodeLeaf` and :class:`InodeBranch`.
171+
"""
172+
173+
def __init__(self, page: Page, structure: c_bbolt.leafPageElement | c_bbolt.branchPageElement, offset: int) -> None:
174+
self.page = page
175+
self.offset = offset
176+
177+
page.db.fh.seek(offset)
178+
element = structure(page.db.fh)
179+
self.element = element
180+
181+
self.flags = None
182+
self.value = None
183+
self.pgid = None
184+
185+
page.db.fh.seek(offset)
186+
page.db.fh.read(element.pos)
187+
188+
self.key = page.db.fh.read(element.ksize).decode()
189+
190+
191+
class InodeLeaf(Inode):
192+
"""Represents a bbolt Inode leaf element."""
193+
194+
def __init__(self, page: Page, structure: c_bbolt.leafPageElement, offset: int) -> None:
195+
super().__init__(page, structure, offset)
196+
197+
self.flags = self.element.flags
198+
self._value_offset = page.db.fh.tell()
199+
self.value = page.db.fh.read(self.element.vsize)
200+
201+
def __repr__(self) -> str:
202+
return f"<Inode type=leaf flags={self.flags} key={self.key} value={self.value}>"
203+
204+
205+
class InodeBranch(Inode):
206+
"""Represents a bbolt Inode branch element."""
207+
208+
def __init__(self, page: Page, structure: c_bbolt.branchPageElement, offset: int) -> None:
209+
super().__init__(page, structure, offset)
210+
211+
self.pgid = self.element.pgid
212+
213+
def __repr__(self) -> str:
214+
return f"<Inode type=branch pgid={self.pgid} (first)key={self.key}>"

‎dissect/database/bbolt/c_bbolt.py‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
from __future__ import annotations
2+
3+
from dissect.cstruct import cstruct
4+
5+
# References:
6+
# - https://github.com/etcd-io/bbolt/blob/main/internal/common/types.go
7+
# - https://github.com/etcd-io/bbolt/blob/main/internal/common/meta.go
8+
# - https://github.com/etcd-io/bbolt/blob/main/internal/common/bucket.go
9+
# - https://github.com/etcd-io/bbolt/blob/main/internal/common/page.go
10+
bbolt_def = """
11+
typedef uint64 Pgid; // page identifier
12+
typedef uint64 Txid; // transaction identifier
13+
14+
#define Magic 0xED0CDAED
15+
#define BucketLeafFlag 0x01
16+
17+
enum PageFlag: uint16 {
18+
Branch = 0x01,
19+
Leaf = 0x02,
20+
Meta = 0x04,
21+
Freelist = 0x10,
22+
};
23+
24+
struct InBucket {
25+
Pgid root;
26+
uint64 sequence;
27+
};
28+
29+
struct Page {
30+
Pgid id;
31+
PageFlag flags; // type of the page: branch, leaf, meta or freelist
32+
uint16 count; // number of key/value pairs in this page
33+
uint32 overflow; // number of overflow pages
34+
};
35+
36+
struct Meta {
37+
Page page;
38+
uint32 magic;
39+
uint32 version;
40+
uint32 pageSize;
41+
uint32 flags;
42+
InBucket root;
43+
Pgid freelist;
44+
Pgid pgid;
45+
Txid txid;
46+
uint64 checksum;
47+
};
48+
49+
struct branchPageElement {
50+
uint32 pos; // relative
51+
uint32 ksize;
52+
Pgid pgid;
53+
};
54+
55+
struct leafPageElement {
56+
uint32 flags; // 0x01 = BucketLeafFlag
57+
uint32 pos; // relative
58+
uint32 ksize;
59+
uint32 vsize;
60+
};
61+
62+
struct Inode {
63+
uint32 flags;
64+
Pgid pgid;
65+
};
66+
"""
67+
68+
c_bbolt = cstruct().load(bbolt_def)
69+
70+
PageFlag = c_bbolt.PageFlag
71+
BucketLeafFlag = c_bbolt.BucketLeafFlag

0 commit comments

Comments
 (0)