python-gdb 0.2.1__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_gdb-0.2.1 → python_gdb-0.3.0}/PKG-INFO +1 -1
- {python_gdb-0.2.1 → python_gdb-0.3.0}/pygdb/__init__.py +6 -0
- {python_gdb-0.2.1 → python_gdb-0.3.0}/pygdb/gdb.py +130 -296
- {python_gdb-0.2.1 → python_gdb-0.3.0}/pygdb/gdb_reader.py +278 -39
- {python_gdb-0.2.1 → python_gdb-0.3.0}/rust/Cargo.lock +1 -1
- {python_gdb-0.2.1 → python_gdb-0.3.0}/rust/Cargo.toml +1 -1
- {python_gdb-0.2.1 → python_gdb-0.3.0}/LICENSE +0 -0
- {python_gdb-0.2.1 → python_gdb-0.3.0}/README.md +0 -0
- {python_gdb-0.2.1 → python_gdb-0.3.0}/pygdb/grd_reader.py +0 -0
- {python_gdb-0.2.1 → python_gdb-0.3.0}/pygdb/lzrw1.py +0 -0
- {python_gdb-0.2.1 → python_gdb-0.3.0}/pygdb/registry.py +0 -0
- {python_gdb-0.2.1 → python_gdb-0.3.0}/pyproject.toml +0 -0
- {python_gdb-0.2.1 → python_gdb-0.3.0}/rust/src/lib.rs +0 -0
|
@@ -16,16 +16,19 @@ Reading only: writing/mutating `.gdb` or `.grd` files is out of scope.
|
|
|
16
16
|
|
|
17
17
|
from .gdb import GDB, CompressionInfo
|
|
18
18
|
from .gdb_reader import (
|
|
19
|
+
BlobDirectory,
|
|
19
20
|
BlobHeader,
|
|
20
21
|
ChannelRecord,
|
|
21
22
|
GDBParseWarning,
|
|
22
23
|
LineRecord,
|
|
23
24
|
check_magic,
|
|
25
|
+
exact_line_table_start,
|
|
24
26
|
find_blob,
|
|
25
27
|
find_channel_table,
|
|
26
28
|
find_line_table,
|
|
27
29
|
header_fields,
|
|
28
30
|
iter_blobs,
|
|
31
|
+
read_blob_directory,
|
|
29
32
|
read_blob_values,
|
|
30
33
|
read_channels,
|
|
31
34
|
read_lines,
|
|
@@ -36,6 +39,7 @@ from .registry import find_coordinate_systems
|
|
|
36
39
|
|
|
37
40
|
__all__ = [
|
|
38
41
|
"GDB",
|
|
42
|
+
"BlobDirectory",
|
|
39
43
|
"BlobHeader",
|
|
40
44
|
"ChannelRecord",
|
|
41
45
|
"CompressionInfo",
|
|
@@ -45,12 +49,14 @@ __all__ = [
|
|
|
45
49
|
"LZRW1DecodeError",
|
|
46
50
|
"LineRecord",
|
|
47
51
|
"check_magic",
|
|
52
|
+
"exact_line_table_start",
|
|
48
53
|
"find_blob",
|
|
49
54
|
"find_channel_table",
|
|
50
55
|
"find_coordinate_systems",
|
|
51
56
|
"find_line_table",
|
|
52
57
|
"header_fields",
|
|
53
58
|
"iter_blobs",
|
|
59
|
+
"read_blob_directory",
|
|
54
60
|
"read_blob_values",
|
|
55
61
|
"read_channels",
|
|
56
62
|
"read_grd",
|
|
@@ -30,9 +30,13 @@ from .gdb_reader import (
|
|
|
30
30
|
check_magic,
|
|
31
31
|
header_fields,
|
|
32
32
|
iter_blobs,
|
|
33
|
+
read_blob_directory,
|
|
33
34
|
read_blob_values,
|
|
34
35
|
read_channels,
|
|
35
|
-
|
|
36
|
+
_read_lines,
|
|
37
|
+
DIRECTORY_ABSENT,
|
|
38
|
+
DIRECTORY_INVALID,
|
|
39
|
+
DIRECTORY_LIVE,
|
|
36
40
|
)
|
|
37
41
|
from .registry import find_channel_roles, find_coordinate_systems
|
|
38
42
|
|
|
@@ -60,87 +64,6 @@ LineRef = Union[str, Tuple[str, int], "LineRecord"]
|
|
|
60
64
|
ChannelRef = Union[str, Tuple[str, int], "ChannelRecord"]
|
|
61
65
|
|
|
62
66
|
|
|
63
|
-
def _finite_values(values: np.ndarray) -> np.ndarray:
|
|
64
|
-
v = np.asarray(values, dtype=float).ravel()
|
|
65
|
-
return v[np.isfinite(v) & (np.abs(v) < 1e30)]
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def _roughness(values: np.ndarray) -> Optional[float]:
|
|
69
|
-
"""
|
|
70
|
-
Median row-to-row step divided by the 5-95% spread of the values.
|
|
71
|
-
|
|
72
|
-
Returns
|
|
73
|
-
-------
|
|
74
|
-
float or None
|
|
75
|
-
Small for data that varies smoothly along its rows, large for
|
|
76
|
-
the same values in a scrambled order. None if there are too few
|
|
77
|
-
finite values or they are constant.
|
|
78
|
-
"""
|
|
79
|
-
v = _finite_values(values)
|
|
80
|
-
if len(v) < 50:
|
|
81
|
-
return None
|
|
82
|
-
spread = float(np.percentile(v, 95) - np.percentile(v, 5))
|
|
83
|
-
if spread == 0.0:
|
|
84
|
-
return None
|
|
85
|
-
return float(np.median(np.abs(np.diff(v))) / spread)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def _row_order_pick(first: np.ndarray, last: np.ndarray, factor: float = 3.0) -> Optional[int]:
|
|
89
|
-
"""
|
|
90
|
-
Decide which of two copies of one channel is in acquisition order.
|
|
91
|
-
|
|
92
|
-
Parameters
|
|
93
|
-
----------
|
|
94
|
-
first, last : numpy.ndarray
|
|
95
|
-
The earlier and later copy in the blob chain.
|
|
96
|
-
factor : float, optional
|
|
97
|
-
How many times rougher one copy must be than the other.
|
|
98
|
-
|
|
99
|
-
Returns
|
|
100
|
-
-------
|
|
101
|
-
int or None
|
|
102
|
-
0 if the *first* copy is the smooth one and the last is a
|
|
103
|
-
markedly rougher reordering of it; 1 if the last is the smooth
|
|
104
|
-
one; None if the copies are not a pure reordering of each other
|
|
105
|
-
(different values or length) or neither is clearly smoother.
|
|
106
|
-
|
|
107
|
-
Notes
|
|
108
|
-
-----
|
|
109
|
-
A copy that is *itself* perfectly monotone is never judged: a sorted
|
|
110
|
-
ramp is smoother than any real signal, and a re-sort by a channel's
|
|
111
|
-
own value (a coordinate re-sorted by itself) leaves exactly that. It
|
|
112
|
-
could equally be a genuine ID or time channel, and nothing here can
|
|
113
|
-
tell the two apart, so such a pair is undecided.
|
|
114
|
-
"""
|
|
115
|
-
if len(first) != len(last):
|
|
116
|
-
return None
|
|
117
|
-
a, b = np.asarray(first).ravel(), np.asarray(last).ravel()
|
|
118
|
-
try:
|
|
119
|
-
if not np.array_equal(np.sort(a), np.sort(b), equal_nan=True):
|
|
120
|
-
return None
|
|
121
|
-
except TypeError: # dtype without a NaN notion (integers)
|
|
122
|
-
if not np.array_equal(np.sort(a), np.sort(b)):
|
|
123
|
-
return None
|
|
124
|
-
if _is_monotone_reference(a) or _is_monotone_reference(b):
|
|
125
|
-
return None
|
|
126
|
-
ra, rb = _roughness(a), _roughness(b)
|
|
127
|
-
if ra is None or rb is None:
|
|
128
|
-
return None
|
|
129
|
-
if rb > 0 and rb >= factor * ra:
|
|
130
|
-
return 0
|
|
131
|
-
if ra > 0 and ra >= factor * rb:
|
|
132
|
-
return 1
|
|
133
|
-
return None
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
def _is_monotone_reference(values: np.ndarray) -> bool:
|
|
137
|
-
"""A non-constant channel stored in non-decreasing order (an ID, date or time)."""
|
|
138
|
-
v = _finite_values(values)
|
|
139
|
-
if len(v) < 50 or v[0] == v[-1]:
|
|
140
|
-
return False
|
|
141
|
-
return bool(np.mean(np.diff(v) >= 0) >= 0.999)
|
|
142
|
-
|
|
143
|
-
|
|
144
67
|
@dataclass
|
|
145
68
|
class CompressionInfo:
|
|
146
69
|
"""
|
|
@@ -181,20 +104,15 @@ class GDB:
|
|
|
181
104
|
----------
|
|
182
105
|
path : str
|
|
183
106
|
Path to the `.gdb` file.
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
blob
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
line's ID/time channels are stored in -- and keeps the last
|
|
194
|
-
copy in every other case. Either way one `GDBParseWarning`
|
|
195
|
-
names the affected pairs and, for `"row_order"`, which ones it
|
|
196
|
-
overrode. Choosing needs both copies decoded, so it is done
|
|
197
|
-
once, when the blob index is first built.
|
|
107
|
+
include_unlisted_blobs : bool, optional
|
|
108
|
+
A file that carries a blob directory (see Notes) lists exactly
|
|
109
|
+
one live blob per (line, channel). By default a blob the
|
|
110
|
+
directory does not list -- an all-zero slot -- is **skipped**, as
|
|
111
|
+
if it were absent, and one `GDBParseWarning` says how many
|
|
112
|
+
(line, channel) blobs in which channels were skipped. Pass
|
|
113
|
+
`True` to read them anyway (the last copy in chain order). A
|
|
114
|
+
file with no directory, or an all-zero one, is unaffected: every
|
|
115
|
+
blob in the chain is served.
|
|
198
116
|
|
|
199
117
|
Raises
|
|
200
118
|
------
|
|
@@ -203,8 +121,7 @@ class GDB:
|
|
|
203
121
|
expected `.gdb` magic -- unlike the module-level functions in
|
|
204
122
|
`gdb_reader`/`registry` (which warn and return empty results),
|
|
205
123
|
since a `GDB` object that isn't backed by a real `.gdb` file
|
|
206
|
-
can't usefully do anything at all.
|
|
207
|
-
not one of the values above.
|
|
124
|
+
can't usefully do anything at all.
|
|
208
125
|
|
|
209
126
|
Examples
|
|
210
127
|
--------
|
|
@@ -234,31 +151,22 @@ class GDB:
|
|
|
234
151
|
context manager) when done with it, or just let it get
|
|
235
152
|
garbage-collected -- `__del__` closes it too, as a safety net.
|
|
236
153
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
acquisition order varies smoothly and a re-sort scrambles that. This
|
|
249
|
-
was validated against independent spreadsheet exports of the one
|
|
250
|
-
real file known to have such copies (it never contradicted them),
|
|
251
|
-
but it cannot decide a channel that is smooth in both orders, and no
|
|
252
|
-
on-disk marker has been found that would make it unnecessary.
|
|
154
|
+
**Which copy of a blob is read.** The blob chain is append-only, so
|
|
155
|
+
a (line, channel) can have several blobs, an older stale one and
|
|
156
|
+
the current one, in either order (issue #2). The file's persisted
|
|
157
|
+
*blob directory* (docs/spec.md section 2.2) says which is current,
|
|
158
|
+
and it is the only thing consulted. A directory entry is used only
|
|
159
|
+
if its start page lands on a walked blob header whose `blob_index`
|
|
160
|
+
equals the slot and whose page count matches; if a non-zero entry
|
|
161
|
+
fails that check, the last copy in chain order is used and a
|
|
162
|
+
`GDBParseWarning` says so. Without a usable directory the last copy
|
|
163
|
+
in chain order is used, with a warning if a pair is duplicated: that
|
|
164
|
+
is a guess, and not always the current copy.
|
|
253
165
|
"""
|
|
254
166
|
|
|
255
|
-
def __init__(self, path: str,
|
|
256
|
-
if duplicate_blobs not in ("last", "row_order"):
|
|
257
|
-
raise ValueError(
|
|
258
|
-
f"duplicate_blobs must be 'last' or 'row_order', got {duplicate_blobs!r}"
|
|
259
|
-
)
|
|
167
|
+
def __init__(self, path: str, include_unlisted_blobs: bool = False):
|
|
260
168
|
self.path = path
|
|
261
|
-
self.
|
|
169
|
+
self.include_unlisted_blobs = include_unlisted_blobs
|
|
262
170
|
self._file = open(path, "rb")
|
|
263
171
|
header = self._file.read(4096)
|
|
264
172
|
if not check_magic(header):
|
|
@@ -270,6 +178,7 @@ class GDB:
|
|
|
270
178
|
self._fields = header_fields(header)
|
|
271
179
|
self._channels: Optional[List[ChannelRecord]] = None
|
|
272
180
|
self._lines: Optional[List[LineRecord]] = None
|
|
181
|
+
self._lines_exact = False
|
|
273
182
|
self._channels_by_name: Optional[Dict[str, List[ChannelRecord]]] = None
|
|
274
183
|
self._lines_by_name: Optional[Dict[str, List[LineRecord]]] = None
|
|
275
184
|
self._blob_index: Optional[Dict[Tuple[int, int], BlobHeader]] = None
|
|
@@ -385,7 +294,7 @@ class GDB:
|
|
|
385
294
|
def lines(self) -> List[LineRecord]:
|
|
386
295
|
"""list of LineRecord: This file's line table, read once and cached."""
|
|
387
296
|
if self._lines is None:
|
|
388
|
-
self._lines =
|
|
297
|
+
self._lines, self._lines_exact = _read_lines(self.path)
|
|
389
298
|
self._lines_by_name = {}
|
|
390
299
|
for l in self._lines:
|
|
391
300
|
self._lines_by_name.setdefault(l.name, []).append(l)
|
|
@@ -547,219 +456,144 @@ class GDB:
|
|
|
547
456
|
Returns
|
|
548
457
|
-------
|
|
549
458
|
dict of {(int, int) : BlobHeader}
|
|
550
|
-
Maps `(line_slot, channel_slot)` to `BlobHeader
|
|
551
|
-
with one blob-chain walk and cached from
|
|
552
|
-
`iter_blobs`/`find_blob` themselves recommend this
|
|
553
|
-
anything beyond an occasional one-off lookup -- this
|
|
554
|
-
always wants line/channel listings and random-access
|
|
459
|
+
Maps `(line_slot, channel_slot)` to the `BlobHeader` of its
|
|
460
|
+
live blob, built with one blob-chain walk and cached from
|
|
461
|
+
then on. `iter_blobs`/`find_blob` themselves recommend this
|
|
462
|
+
for anything beyond an occasional one-off lookup -- this
|
|
463
|
+
class always wants line/channel listings and random-access
|
|
555
464
|
reads, so it always builds the index.
|
|
556
465
|
|
|
557
466
|
Warns
|
|
558
467
|
-----
|
|
559
468
|
GDBParseWarning
|
|
560
|
-
|
|
561
|
-
blob
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
can sit either before or after the current one, and nothing
|
|
565
|
-
decoded so far says which is which. With
|
|
566
|
-
`duplicate_blobs="row_order"` the warning also says which
|
|
567
|
-
pairs were switched to an earlier copy. Duplicates in the
|
|
568
|
-
administrative slots past the last real line (the REG/IPJ
|
|
569
|
-
registry, whose stale copies are expected and handled by
|
|
570
|
-
`pygdb.registry`) are not reported.
|
|
469
|
+
See `_select_live_blobs`: when blobs are skipped because the
|
|
470
|
+
file's blob directory does not list them, when a directory
|
|
471
|
+
entry fails validation, or when a pair is duplicated and
|
|
472
|
+
the file has no directory to say which copy is current.
|
|
571
473
|
"""
|
|
572
474
|
if self._blob_index is None:
|
|
573
475
|
chans_max = self.chans_max
|
|
574
476
|
copies: Dict[Tuple[int, int], List[BlobHeader]] = {}
|
|
477
|
+
by_offset: Dict[int, BlobHeader] = {}
|
|
575
478
|
for blob in iter_blobs(self.path):
|
|
576
479
|
copies.setdefault(blob.line_channel(chans_max), []).append(blob)
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
self.
|
|
580
|
-
|
|
581
|
-
if
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
overridden = self._prefer_row_order_copies(duplicated, copies, index)
|
|
585
|
-
self._warn_duplicate_blobs(duplicated, overridden)
|
|
480
|
+
by_offset[blob.offset] = blob
|
|
481
|
+
self._blob_index = {k: v[-1] for k, v in copies.items()}
|
|
482
|
+
if not self.lines:
|
|
483
|
+
return self._blob_index
|
|
484
|
+
if not self._lines_exact:
|
|
485
|
+
self._calibrate_line_indices()
|
|
486
|
+
self._select_live_blobs(copies, by_offset)
|
|
586
487
|
return self._blob_index
|
|
587
488
|
|
|
588
|
-
def
|
|
589
|
-
return read_blob_values(
|
|
590
|
-
self.path, blob, channel,
|
|
591
|
-
comp_level=self.comp_level or 0, page_size=self.page_size, file=self._file,
|
|
592
|
-
)
|
|
593
|
-
|
|
594
|
-
def _prefer_row_order_copies(
|
|
489
|
+
def _select_live_blobs(
|
|
595
490
|
self,
|
|
596
|
-
duplicated: List[Tuple[int, int]],
|
|
597
491
|
copies: Dict[Tuple[int, int], List[BlobHeader]],
|
|
598
|
-
|
|
599
|
-
) ->
|
|
492
|
+
by_offset: Dict[int, BlobHeader],
|
|
493
|
+
) -> None:
|
|
600
494
|
"""
|
|
601
|
-
|
|
495
|
+
Choose each real (line, channel)'s live blob using the blob directory.
|
|
602
496
|
|
|
603
497
|
Parameters
|
|
604
498
|
----------
|
|
605
|
-
duplicated : list of (int, int)
|
|
606
|
-
`(line_slot, channel_slot)` keys with more than one blob.
|
|
607
499
|
copies : dict of {(int, int) : list of BlobHeader}
|
|
608
500
|
Every blob for each key, in chain order.
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
earlier copy wherever one is preferred.
|
|
501
|
+
by_offset : dict of {int : BlobHeader}
|
|
502
|
+
Every blob of the chain, keyed by absolute offset.
|
|
612
503
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
504
|
+
Warns
|
|
505
|
+
-----
|
|
506
|
+
GDBParseWarning
|
|
507
|
+
Once per situation, naming counts and examples: (1) pairs
|
|
508
|
+
skipped because the directory has no live entry for them;
|
|
509
|
+
(2) pairs whose non-zero directory entry failed validation
|
|
510
|
+
(the last copy in chain order is used); (3) with no usable
|
|
511
|
+
directory at all, pairs that have more than one blob.
|
|
512
|
+
Administrative slots past the last real line (the REG/IPJ
|
|
513
|
+
registry, whose stale copies `pygdb.registry` handles
|
|
514
|
+
itself) are never judged or reported.
|
|
617
515
|
|
|
618
516
|
Notes
|
|
619
517
|
-----
|
|
620
|
-
See the class docstring for
|
|
621
|
-
|
|
622
|
-
different values, no order-defining channel on the line, or no
|
|
623
|
-
clear winner) simply keeps the last copy.
|
|
518
|
+
Updates `self._blob_index` in place. See the class docstring for
|
|
519
|
+
the rules.
|
|
624
520
|
"""
|
|
521
|
+
index = self._blob_index
|
|
625
522
|
real_lines = {line.index for line in self.lines}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
or channel.is_string or channel.is_array
|
|
637
|
-
):
|
|
638
|
-
continue
|
|
639
|
-
first, last = self._read_copy(blobs[0], channel), self._read_copy(blobs[1], channel)
|
|
640
|
-
if len(first) != len(last):
|
|
641
|
-
continue
|
|
642
|
-
ref_key = (line_slot, len(first))
|
|
643
|
-
if ref_key not in reference_cache:
|
|
644
|
-
reference_cache[ref_key] = self._line_has_order_reference(
|
|
645
|
-
line_slot, len(first), duplicated_keys, channels
|
|
523
|
+
directory = read_blob_directory(self.path)
|
|
524
|
+
if directory is None:
|
|
525
|
+
duplicated = [k for k, v in copies.items() if len(v) > 1 and k[0] in real_lines]
|
|
526
|
+
if duplicated:
|
|
527
|
+
self._warn_pairs(
|
|
528
|
+
f"{len(duplicated)} (line, channel) pair(s) have more than one blob in the "
|
|
529
|
+
f"blob chain ({self._describe_pairs(duplicated)}) and the file has no blob "
|
|
530
|
+
f"directory to say which is current -- using the last one in chain order, "
|
|
531
|
+
f"which is not always the current copy. If a channel's rows look scrambled "
|
|
532
|
+
f"against the line's other channels, this is the likely cause (see issue #2)"
|
|
646
533
|
)
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
line_slot: int,
|
|
655
|
-
n_rows: int,
|
|
656
|
-
duplicated_keys: set,
|
|
657
|
-
channels: Dict[int, ChannelRecord],
|
|
658
|
-
) -> bool:
|
|
659
|
-
"""
|
|
660
|
-
Whether a line has a channel proving its rows are in some fixed order.
|
|
661
|
-
|
|
662
|
-
Parameters
|
|
663
|
-
----------
|
|
664
|
-
line_slot : int
|
|
665
|
-
The line's slot.
|
|
666
|
-
n_rows : int
|
|
667
|
-
The row count of the duplicated copies being judged.
|
|
668
|
-
duplicated_keys : set of (int, int)
|
|
669
|
-
Keys with more than one blob -- excluded, since their own
|
|
670
|
-
order is what is in question.
|
|
671
|
-
channels : dict of {int : ChannelRecord}
|
|
672
|
-
Channel records by slot.
|
|
673
|
-
|
|
674
|
-
Returns
|
|
675
|
-
-------
|
|
676
|
-
bool
|
|
677
|
-
True if some single-copy, numeric, non-array channel of
|
|
678
|
-
`n_rows` values on this line is stored in monotone
|
|
679
|
-
(non-decreasing) order and isn't constant -- typically an
|
|
680
|
-
ID, date or time channel.
|
|
681
|
-
"""
|
|
682
|
-
for (ls, cs), blob in sorted(self._ensure_blob_index().items()):
|
|
683
|
-
channel = channels.get(cs)
|
|
684
|
-
if (
|
|
685
|
-
ls != line_slot or (ls, cs) in duplicated_keys or channel is None
|
|
686
|
-
or channel.is_string or channel.is_array
|
|
687
|
-
):
|
|
534
|
+
return
|
|
535
|
+
first_offset = min(by_offset)
|
|
536
|
+
skipped: List[Tuple[int, int]] = []
|
|
537
|
+
invalid: List[Tuple[int, int]] = []
|
|
538
|
+
for key in copies:
|
|
539
|
+
line_slot, channel_slot = key
|
|
540
|
+
if line_slot not in real_lines:
|
|
688
541
|
continue
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
542
|
+
status, blob = directory.resolve(
|
|
543
|
+
line_slot * self.chans_max + channel_slot, by_offset, first_offset, self.page_size,
|
|
544
|
+
)
|
|
545
|
+
if status == DIRECTORY_LIVE:
|
|
546
|
+
index[key] = blob
|
|
547
|
+
elif status == DIRECTORY_ABSENT:
|
|
548
|
+
skipped.append(key)
|
|
549
|
+
elif status == DIRECTORY_INVALID:
|
|
550
|
+
invalid.append(key)
|
|
551
|
+
if skipped and not self.include_unlisted_blobs:
|
|
552
|
+
for key in skipped:
|
|
553
|
+
del index[key]
|
|
554
|
+
channel_names = {c.index: c.name for c in self.channels}
|
|
555
|
+
by_channel: Dict[int, int] = {}
|
|
556
|
+
for _, channel_slot in skipped:
|
|
557
|
+
by_channel[channel_slot] = by_channel.get(channel_slot, 0) + 1
|
|
558
|
+
listing = ", ".join(
|
|
559
|
+
f"{channel_names.get(cs, f'#{cs}')!r} ({n})" for cs, n in sorted(by_channel.items())[:5]
|
|
560
|
+
)
|
|
561
|
+
more = f" and {len(by_channel) - 5} more channel(s)" if len(by_channel) > 5 else ""
|
|
562
|
+
self._warn_pairs(
|
|
563
|
+
f"skipped {len(skipped)} (line, channel) blob(s) in {len(by_channel)} channel(s) "
|
|
564
|
+
f"[{listing}{more}] that the file's blob directory does not list as live. Pass "
|
|
565
|
+
f"include_unlisted_blobs=True to read them anyway"
|
|
566
|
+
)
|
|
567
|
+
if invalid:
|
|
568
|
+
self._warn_pairs(
|
|
569
|
+
f"{len(invalid)} (line, channel) pair(s) have a blob-directory entry that does not "
|
|
570
|
+
f"point at a blob with the right index and size ({self._describe_pairs(invalid)}) "
|
|
571
|
+
f"-- using the last copy in chain order for them"
|
|
572
|
+
)
|
|
710
573
|
|
|
711
|
-
|
|
712
|
-
-----
|
|
713
|
-
GDBParseWarning
|
|
714
|
-
Once, naming how many real (line, channel) pairs are
|
|
715
|
-
affected and a few examples (and, for `"row_order"`, which
|
|
716
|
-
ones were switched). Nothing is emitted if every duplicate
|
|
717
|
-
is in an administrative slot.
|
|
718
|
-
"""
|
|
574
|
+
def _describe_pairs(self, keys: Sequence[Tuple[int, int]], limit: int = 3) -> str:
|
|
719
575
|
line_names = {line.index: line.name for line in self.lines}
|
|
720
576
|
channel_names = {c.index: c.name for c in self.channels}
|
|
577
|
+
named = [
|
|
578
|
+
f"line {line_names[ls]!r} channel {channel_names.get(cs, f'#{cs}')!r}"
|
|
579
|
+
for ls, cs in keys if ls in line_names
|
|
580
|
+
]
|
|
581
|
+
more = f" and {len(named) - limit} more" if len(named) > limit else ""
|
|
582
|
+
return ", ".join(named[:limit]) + more
|
|
721
583
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
for ls, cs in keys if ls in line_names
|
|
726
|
-
]
|
|
727
|
-
more = f" and {len(named) - limit} more" if len(named) > limit else ""
|
|
728
|
-
return ", ".join(named[:limit]) + more
|
|
729
|
-
|
|
730
|
-
n_real = sum(1 for ls, _ in duplicated if ls in line_names)
|
|
731
|
-
if not n_real:
|
|
732
|
-
return
|
|
733
|
-
head = (
|
|
734
|
-
f"{self.path}: {n_real} (line, channel) pair(s) have more than one "
|
|
735
|
-
f"blob in the blob chain ({describe(duplicated)})"
|
|
736
|
-
)
|
|
737
|
-
if self.duplicate_blobs == "row_order":
|
|
738
|
-
tail = (
|
|
739
|
-
f" -- duplicate_blobs='row_order': switched {len(overridden)} pair(s) to "
|
|
740
|
-
f"an earlier copy because the last copy was the same values in a "
|
|
741
|
-
f"markedly rougher row order ({describe(overridden)}); kept the last "
|
|
742
|
-
f"copy in chain order for the other {n_real - len(overridden)}. This "
|
|
743
|
-
f"is a heuristic, not a decoded field (see issue #2)"
|
|
744
|
-
if overridden else
|
|
745
|
-
f" -- duplicate_blobs='row_order': no pair needed switching, so the "
|
|
746
|
-
f"last copy in chain order was kept for all of them (see issue #2)"
|
|
747
|
-
)
|
|
748
|
-
else:
|
|
749
|
-
tail = (
|
|
750
|
-
" -- using the last one in chain order, which is not always the "
|
|
751
|
-
"current copy. If a channel's rows look scrambled against the line's "
|
|
752
|
-
"other channels, this is the likely cause; duplicate_blobs='row_order' "
|
|
753
|
-
"can pick the acquisition-order copy (see issue #2)"
|
|
754
|
-
)
|
|
755
|
-
warnings.warn(head + tail, GDBParseWarning, stacklevel=4)
|
|
584
|
+
def _warn_pairs(self, message: str) -> None:
|
|
585
|
+
# stacklevel: here -> _select_live_blobs -> _ensure_blob_index -> public method -> caller
|
|
586
|
+
warnings.warn(f"{self.path}: {message}", GDBParseWarning, stacklevel=5)
|
|
756
587
|
|
|
757
588
|
def _calibrate_line_indices(self) -> None:
|
|
758
589
|
"""
|
|
759
590
|
Correct a possible small, fixed off-by-N in every line's index.
|
|
760
591
|
|
|
761
|
-
|
|
762
|
-
`
|
|
592
|
+
Only used when `read_lines` had to fall back to the
|
|
593
|
+
`find_line_table` heuristic (`self._lines_exact` is False): a
|
|
594
|
+
file whose line table is located exactly already has true
|
|
595
|
+
indices. See `find_line_table`'s and `read_lines`'s docstrings
|
|
596
|
+
in `gdb_reader.py`. Checks, for a handful of small integer shifts,
|
|
763
597
|
which one makes the most already-found lines actually have at
|
|
764
598
|
least one real data blob on disk for *some* channel -- then
|
|
765
599
|
applies the winning shift to every `LineRecord.index` in
|
|
@@ -443,11 +443,15 @@ def header_fields(data: bytes) -> dict:
|
|
|
443
443
|
Returns
|
|
444
444
|
-------
|
|
445
445
|
dict
|
|
446
|
-
Maps each of `"chans_max"
|
|
447
|
-
`"
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
446
|
+
Maps each of `"chans_max"` (word 24), `"blobs_max"` (28),
|
|
447
|
+
`"lines_max"` (36), `"users_max"` (40), `"index_slots"` (44,
|
|
448
|
+
the total number of blob-directory slots), `"data_slots"` (48,
|
|
449
|
+
the number of those that address (line, channel) data blobs),
|
|
450
|
+
`"page_size"` (100), and `"comp_level"` (120) to its decoded
|
|
451
|
+
int32 value. See docs/spec.md section 2 for the full table
|
|
452
|
+
with a confidence rating per word, and
|
|
453
|
+
docs/provenance/notes.md section 6.1/6.1b for the derivations
|
|
454
|
+
and the still-unknown offsets.
|
|
451
455
|
|
|
452
456
|
Warns
|
|
453
457
|
-----
|
|
@@ -466,7 +470,8 @@ def header_fields(data: bytes) -> dict:
|
|
|
466
470
|
(`DB_COMP_SIZE`) IS confirmed real zlib.
|
|
467
471
|
"""
|
|
468
472
|
result = {}
|
|
469
|
-
for name, offset in (("chans_max", 24), ("
|
|
473
|
+
for name, offset in (("chans_max", 24), ("blobs_max", 28), ("lines_max", 36),
|
|
474
|
+
("users_max", 40), ("index_slots", 44), ("data_slots", 48),
|
|
470
475
|
("page_size", 100), ("comp_level", 120)):
|
|
471
476
|
try:
|
|
472
477
|
result[name] = struct.unpack_from("<i", data, offset)[0]
|
|
@@ -713,10 +718,10 @@ class LineRecord:
|
|
|
713
718
|
-----
|
|
714
719
|
**[LIKELY]/[UNKNOWN]** -- much less firmly established than
|
|
715
720
|
`ChannelRecord`: only the name (relative +32) and category code
|
|
716
|
-
(relative +108) fields are decoded
|
|
717
|
-
(`
|
|
718
|
-
|
|
719
|
-
|
|
721
|
+
(relative +108) fields are decoded. The table is located exactly
|
|
722
|
+
(`exact_line_table_start`, **[CONFIRMED]** on the corpus) and only
|
|
723
|
+
falls back to the `find_line_table` heuristic scan when that
|
|
724
|
+
cannot be validated. See docs/spec.md section 3.2 and docs/provenance/notes.md
|
|
720
725
|
section 6.3.
|
|
721
726
|
|
|
722
727
|
`eq=False` keeps the default identity-based `__eq__`/`__hash__`
|
|
@@ -785,23 +790,25 @@ def find_line_table(data: bytes, search_window: Tuple[int, Optional[int]] = (128
|
|
|
785
790
|
|
|
786
791
|
Notes
|
|
787
792
|
-----
|
|
793
|
+
**Prefer `exact_line_table_start`** (via `read_lines`): the line
|
|
794
|
+
table's start follows exactly from `lines_max` and the channel
|
|
795
|
+
table's position (docs/spec.md section 2.1), which this heuristic
|
|
796
|
+
predates. This scan is now only the fallback for a file where that
|
|
797
|
+
arithmetic cannot be validated.
|
|
798
|
+
|
|
788
799
|
Unlike `find_channel_table`, there's no known default-name anchor
|
|
789
800
|
(the line table has nothing analogous to the channel table's
|
|
790
|
-
"SUPER" user record immediately after it)
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
(docs/provenance/log.md Session 1 section 1.16,
|
|
794
|
-
docs/provenance/notes.md section 6.3). This is therefore a
|
|
795
|
-
heuristic **[LIKELY]** scan, not the structurally-proven technique
|
|
796
|
-
used for the channel table: it looks for a run of 128-byte records
|
|
801
|
+
"SUPER" user record immediately after it), so this is a
|
|
802
|
+
heuristic **[LIKELY]** scan, not a structurally-proven technique:
|
|
803
|
+
it looks for a run of 128-byte records
|
|
797
804
|
whose relative +32 field looks like a clean, NUL-terminated,
|
|
798
805
|
printable line name and whose relative +108 category field
|
|
799
806
|
matches one of the two confirmed real values (100=NORMAL/FLIGHT,
|
|
800
807
|
200=GROUP), then returns the earliest such record in the run with
|
|
801
808
|
the most hits at a consistent 128-byte phase.
|
|
802
809
|
|
|
803
|
-
**Known limitation, found by real-file testing
|
|
804
|
-
here:** if a table's true first slot(s) don't carry a category
|
|
810
|
+
**Known limitation, found by real-file testing (fixed by using
|
|
811
|
+
`exact_line_table_start` instead, not here):** if a table's true first slot(s) don't carry a category
|
|
805
812
|
code in {100, 200}, this returns a start that's one or more slots
|
|
806
813
|
too late -- every subsequent `LineRecord.index` is then off by
|
|
807
814
|
that same fixed amount, which breaks blob_index lookups by line
|
|
@@ -847,6 +854,60 @@ def find_line_table(data: bytes, search_window: Tuple[int, Optional[int]] = (128
|
|
|
847
854
|
return min(phase_hits[best_phase])
|
|
848
855
|
|
|
849
856
|
|
|
857
|
+
_LINE_TABLE_GAP = 24 # [CONFIRMED] bytes between the end of the line table and the
|
|
858
|
+
# channel table -- docs/spec.md section 2.1
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
def exact_line_table_start(data: bytes, lines_max: Optional[int]) -> Optional[int]:
|
|
862
|
+
"""
|
|
863
|
+
Compute the line table's start from the header and the channel table.
|
|
864
|
+
|
|
865
|
+
Parameters
|
|
866
|
+
----------
|
|
867
|
+
data : bytes
|
|
868
|
+
The file's bytes up to at least the end of the channel table
|
|
869
|
+
(`read_lines` passes everything before the blob region).
|
|
870
|
+
lines_max : int or None
|
|
871
|
+
The line-table capacity (header word 36); `None` or non-positive
|
|
872
|
+
means unknown.
|
|
873
|
+
|
|
874
|
+
Returns
|
|
875
|
+
-------
|
|
876
|
+
int or None
|
|
877
|
+
Byte offset of the line table's first record, or `None` if the
|
|
878
|
+
arithmetic cannot be validated (unknown `lines_max`, no
|
|
879
|
+
locatable channel table, a start before the header ends, or no
|
|
880
|
+
line-shaped record anywhere in the computed table) -- the
|
|
881
|
+
caller then falls back to `find_line_table`.
|
|
882
|
+
|
|
883
|
+
Notes
|
|
884
|
+
-----
|
|
885
|
+
The line table is `lines_max` 128-byte slots followed by a 24-byte
|
|
886
|
+
gap and then the channel table, so its start is
|
|
887
|
+
`find_channel_table(data) - 24 - lines_max * 128`. **[CONFIRMED]**
|
|
888
|
+
on all 23 real files examined (docs/spec.md section 2.1;
|
|
889
|
+
docs/provenance/notes.md section 6.1b): the computed start is
|
|
890
|
+
always a slot boundary that matches the heuristic's start, or is an
|
|
891
|
+
earlier slot the heuristic missed. The at-least-one-line-record
|
|
892
|
+
check only guards a file whose layout does not follow this
|
|
893
|
+
arithmetic; it is not needed for any real file seen.
|
|
894
|
+
"""
|
|
895
|
+
if not lines_max or lines_max <= 0:
|
|
896
|
+
return None
|
|
897
|
+
try:
|
|
898
|
+
channel_start = find_channel_table(data)
|
|
899
|
+
except ValueError:
|
|
900
|
+
return None
|
|
901
|
+
start = channel_start - _LINE_TABLE_GAP - lines_max * SYMBOL_RECORD_SIZE
|
|
902
|
+
if start < 256:
|
|
903
|
+
return None
|
|
904
|
+
for i in range(lines_max):
|
|
905
|
+
rec = _parse_line_record(data, start + i * SYMBOL_RECORD_SIZE, i)
|
|
906
|
+
if rec.name and rec.name_is_clean and rec.category_code in DB_CATEGORY_LINE_NAMES:
|
|
907
|
+
return start
|
|
908
|
+
return None
|
|
909
|
+
|
|
910
|
+
|
|
850
911
|
def read_lines(path: str) -> List[LineRecord]:
|
|
851
912
|
"""
|
|
852
913
|
Decode the line symbol table.
|
|
@@ -869,30 +930,59 @@ def read_lines(path: str) -> List[LineRecord]:
|
|
|
869
930
|
|
|
870
931
|
Notes
|
|
871
932
|
-----
|
|
872
|
-
|
|
873
|
-
`
|
|
874
|
-
|
|
875
|
-
|
|
933
|
+
The table is located **exactly** whenever possible (see
|
|
934
|
+
`exact_line_table_start`): it holds `lines_max` (header word 36)
|
|
935
|
+
128-byte slots and ends 24 bytes before the channel table, so its
|
|
936
|
+
start is `channel_table_start - 24 - lines_max * 128`
|
|
937
|
+
(**[CONFIRMED]** on every real file in the corpus, docs/spec.md
|
|
938
|
+
section 2.1). Every one of the `lines_max` slots is then examined,
|
|
939
|
+
and `LineRecord.index` is the true slot number, so
|
|
940
|
+
`LineRecord.index` is exact and blob_index lookups keyed on it
|
|
941
|
+
are right.
|
|
942
|
+
|
|
943
|
+
Only when that arithmetic cannot be validated (a header without
|
|
944
|
+
`lines_max`, or no line-shaped record at the computed position) does
|
|
945
|
+
this fall back to the older heuristic (see `find_line_table`):
|
|
946
|
+
**[LIKELY]**, less firmly established, reading forward until 8
|
|
876
947
|
consecutive records fail to look like either a populated line
|
|
877
|
-
record or clean unused capacity
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
948
|
+
record or clean unused capacity. In that fallback
|
|
949
|
+
**`LineRecord.index` can be off by a small, fixed amount** when the
|
|
950
|
+
scan starts one or more slots late -- `.name` is still correct but
|
|
951
|
+
`.index` is wrong. `GDB` (in `gdb.py`) corrects this against the
|
|
952
|
+
actual blob chain only in that case.
|
|
953
|
+
|
|
954
|
+
A populated slot whose category is neither `100` nor `200` (for
|
|
955
|
+
example the `65636` slot 0 of one 1991 file) is not returned, as
|
|
956
|
+
before -- but, unlike the fallback, it no longer shifts the
|
|
957
|
+
indices of the lines after it.
|
|
958
|
+
"""
|
|
959
|
+
return _read_lines(path)[0]
|
|
960
|
+
|
|
961
|
+
|
|
962
|
+
def _read_lines(path: str) -> Tuple[List[LineRecord], bool]:
|
|
963
|
+
"""
|
|
964
|
+
Decode the line symbol table, also reporting how it was located.
|
|
965
|
+
|
|
966
|
+
Parameters
|
|
967
|
+
----------
|
|
968
|
+
path : str
|
|
969
|
+
Path to the `.gdb` file.
|
|
970
|
+
|
|
971
|
+
Returns
|
|
972
|
+
-------
|
|
973
|
+
lines : list of LineRecord
|
|
974
|
+
As `read_lines`.
|
|
975
|
+
exact : bool
|
|
976
|
+
True if the table was located by `exact_line_table_start`
|
|
977
|
+
(indices are exact); False if the heuristic fallback was used
|
|
978
|
+
(indices may need `GDB`'s blob-chain calibration).
|
|
889
979
|
"""
|
|
890
980
|
with open(path, "rb") as f:
|
|
891
981
|
header = f.read(4096)
|
|
892
982
|
if not check_magic(header):
|
|
893
983
|
_warn(f"{path}: does not start with the expected '!CBD' magic -- "
|
|
894
984
|
f"not a recognized .gdb file, returning no lines")
|
|
895
|
-
return []
|
|
985
|
+
return [], False
|
|
896
986
|
blob_start = blob_region_start(header)
|
|
897
987
|
f.seek(0, 2)
|
|
898
988
|
size = f.tell()
|
|
@@ -900,14 +990,24 @@ def read_lines(path: str) -> List[LineRecord]:
|
|
|
900
990
|
read_size = blob_start if (blob_start is not None and 0 < blob_start <= size) else min(size, 20_000_000)
|
|
901
991
|
data = f.read(read_size)
|
|
902
992
|
|
|
993
|
+
lines_max = header_fields(header).get("lines_max")
|
|
994
|
+
exact_start = exact_line_table_start(data, lines_max)
|
|
995
|
+
if exact_start is not None:
|
|
996
|
+
lines = []
|
|
997
|
+
for i in range(lines_max):
|
|
998
|
+
rec = _parse_line_record(data, exact_start + i * SYMBOL_RECORD_SIZE, i)
|
|
999
|
+
if rec.name and rec.name_is_clean and rec.category_code in DB_CATEGORY_LINE_NAMES:
|
|
1000
|
+
lines.append(rec)
|
|
1001
|
+
return lines, True
|
|
1002
|
+
|
|
903
1003
|
try:
|
|
904
1004
|
table_start = find_line_table(data, search_window=(128, len(data)))
|
|
905
1005
|
except ValueError as e:
|
|
906
1006
|
_warn(f"{path}: could not locate the line symbol table ({e}) -- "
|
|
907
1007
|
f"returning no lines")
|
|
908
|
-
return []
|
|
1008
|
+
return [], False
|
|
909
1009
|
|
|
910
|
-
lines
|
|
1010
|
+
lines = []
|
|
911
1011
|
consecutive_bad = 0
|
|
912
1012
|
i = 0
|
|
913
1013
|
while True:
|
|
@@ -929,7 +1029,7 @@ def read_lines(path: str) -> List[LineRecord]:
|
|
|
929
1029
|
consecutive_bad += 1
|
|
930
1030
|
if consecutive_bad >= 8:
|
|
931
1031
|
break
|
|
932
|
-
return lines
|
|
1032
|
+
return lines, False
|
|
933
1033
|
|
|
934
1034
|
|
|
935
1035
|
BLOB_MAGIC = b"\xcc\xcc\x00\xff"
|
|
@@ -1301,6 +1401,145 @@ def find_blob(path: str, line_slot: int, channel_slot: int, chans_max: Optional[
|
|
|
1301
1401
|
return None
|
|
1302
1402
|
|
|
1303
1403
|
|
|
1404
|
+
DIRECTORY_OFFSET = 280 # [CONFIRMED] first blob-directory slot -- docs/spec.md section 2.2
|
|
1405
|
+
_DIRECTORY_SLOT_DTYPE = np.dtype([("word", "<u4"), ("n_pages", "<u2")]) # 6 bytes, unaligned
|
|
1406
|
+
_DIRECTORY_LIVE_FLAG = 0x8 # top nibble of a live (line, channel) entry's 32-bit word
|
|
1407
|
+
|
|
1408
|
+
DIRECTORY_LIVE = "live"
|
|
1409
|
+
DIRECTORY_ABSENT = "absent"
|
|
1410
|
+
DIRECTORY_INVALID = "invalid"
|
|
1411
|
+
DIRECTORY_OUTSIDE = "outside"
|
|
1412
|
+
|
|
1413
|
+
|
|
1414
|
+
@dataclass(eq=False)
|
|
1415
|
+
class BlobDirectory:
|
|
1416
|
+
"""
|
|
1417
|
+
The persisted blob directory: which blob is the live one for each slot.
|
|
1418
|
+
|
|
1419
|
+
Attributes
|
|
1420
|
+
----------
|
|
1421
|
+
data_slots : int
|
|
1422
|
+
Number of directory slots that address (line, channel) data
|
|
1423
|
+
blobs (header word 48, `lines_max * chans_max`); slot `i` is
|
|
1424
|
+
the blob whose `blob_index` is `i`.
|
|
1425
|
+
entries : dict of {int : (int, int)}
|
|
1426
|
+
Every **non-zero** data slot as `(32-bit word, n_pages)`, keyed
|
|
1427
|
+
by slot. A zero slot is simply absent from this dict.
|
|
1428
|
+
|
|
1429
|
+
Notes
|
|
1430
|
+
-----
|
|
1431
|
+
**[CONFIRMED]** layout, **[LIKELY]** interpretation
|
|
1432
|
+
(docs/spec.md section 2.2; docs/provenance/notes.md section 6.1c).
|
|
1433
|
+
The directory is an array of 6-byte slots starting at file offset
|
|
1434
|
+
280. A live entry is `(0x80000000 | start page, n_pages)` where
|
|
1435
|
+
the start page is relative to the first blob (`(offset - blob
|
|
1436
|
+
region start) / page_size`). Across the real corpus it addressed
|
|
1437
|
+
100% of the real (line, channel) blobs of 20 of 22 files, and for
|
|
1438
|
+
every duplicated pair with an independent oracle (13 of 13) it
|
|
1439
|
+
pointed at the correct copy. A slot that is all-zero belongs to a
|
|
1440
|
+
blob the file does not list as live.
|
|
1441
|
+
"""
|
|
1442
|
+
|
|
1443
|
+
data_slots: int
|
|
1444
|
+
entries: dict
|
|
1445
|
+
|
|
1446
|
+
def resolve(self, blob_index: int, blobs_by_offset: dict, first_offset: int, page_size: int):
|
|
1447
|
+
"""
|
|
1448
|
+
Look up the live blob for `blob_index`.
|
|
1449
|
+
|
|
1450
|
+
Parameters
|
|
1451
|
+
----------
|
|
1452
|
+
blob_index : int
|
|
1453
|
+
The slot, `line_slot * chans_max + channel_slot`.
|
|
1454
|
+
blobs_by_offset : dict of {int : BlobHeader}
|
|
1455
|
+
Every blob of the chain walk, keyed by absolute `offset`.
|
|
1456
|
+
first_offset : int
|
|
1457
|
+
Absolute offset of the first blob (`blob_region_start`).
|
|
1458
|
+
page_size : int
|
|
1459
|
+
The file's page size.
|
|
1460
|
+
|
|
1461
|
+
Returns
|
|
1462
|
+
-------
|
|
1463
|
+
status : str
|
|
1464
|
+
`"live"`: the entry's start page lands on a walked blob
|
|
1465
|
+
header whose `blob_index` equals `blob_index` and whose
|
|
1466
|
+
`n_pages` equals the entry's page count (the strict
|
|
1467
|
+
check). `"absent"`: the slot is all-zero. `"invalid"`: the
|
|
1468
|
+
slot is non-zero but fails the strict check. `"outside"`:
|
|
1469
|
+
`blob_index` is not a data slot at all (an administrative
|
|
1470
|
+
slot), so the directory says nothing about it.
|
|
1471
|
+
blob : BlobHeader or None
|
|
1472
|
+
The live blob for `"live"`, otherwise `None`.
|
|
1473
|
+
"""
|
|
1474
|
+
if not 0 <= blob_index < self.data_slots:
|
|
1475
|
+
return DIRECTORY_OUTSIDE, None
|
|
1476
|
+
entry = self.entries.get(blob_index)
|
|
1477
|
+
if entry is None:
|
|
1478
|
+
return DIRECTORY_ABSENT, None
|
|
1479
|
+
word, n_pages = entry
|
|
1480
|
+
if word >> 28 != _DIRECTORY_LIVE_FLAG:
|
|
1481
|
+
return DIRECTORY_INVALID, None
|
|
1482
|
+
blob = blobs_by_offset.get(first_offset + (word & 0x7FFFFFFF) * page_size)
|
|
1483
|
+
if blob is None or blob.blob_index != blob_index or blob.n_pages != n_pages:
|
|
1484
|
+
return DIRECTORY_INVALID, None
|
|
1485
|
+
return DIRECTORY_LIVE, blob
|
|
1486
|
+
|
|
1487
|
+
|
|
1488
|
+
def read_blob_directory(path: str) -> Optional[BlobDirectory]:
|
|
1489
|
+
"""
|
|
1490
|
+
Read the persisted blob directory.
|
|
1491
|
+
|
|
1492
|
+
Parameters
|
|
1493
|
+
----------
|
|
1494
|
+
path : str
|
|
1495
|
+
Path to the `.gdb` file.
|
|
1496
|
+
|
|
1497
|
+
Returns
|
|
1498
|
+
-------
|
|
1499
|
+
BlobDirectory or None
|
|
1500
|
+
The directory, or `None` when the file has none to trust: a bad
|
|
1501
|
+
magic or truncated header, header words that are inconsistent
|
|
1502
|
+
with the layout (`data_slots != lines_max * chans_max`, or the
|
|
1503
|
+
data slots would run past the blob region), or a directory
|
|
1504
|
+
whose every data slot is zero. Absence is not an anomaly, so
|
|
1505
|
+
nothing is warned.
|
|
1506
|
+
|
|
1507
|
+
Notes
|
|
1508
|
+
-----
|
|
1509
|
+
See `BlobDirectory`. Only the data slots (`0 <= slot <
|
|
1510
|
+
data_slots`) are read; the registry blob-symbol slots and the
|
|
1511
|
+
cache slots after them are not used by the reader.
|
|
1512
|
+
"""
|
|
1513
|
+
with open(path, "rb") as f:
|
|
1514
|
+
header = f.read(4096)
|
|
1515
|
+
if not check_magic(header):
|
|
1516
|
+
return None
|
|
1517
|
+
fields = header_fields(header)
|
|
1518
|
+
chans_max, lines_max, data_slots, page_size = (
|
|
1519
|
+
fields["chans_max"], fields["lines_max"], fields["data_slots"], fields["page_size"],
|
|
1520
|
+
)
|
|
1521
|
+
if None in (chans_max, lines_max, data_slots, page_size):
|
|
1522
|
+
return None
|
|
1523
|
+
blob_start = blob_region_start(header)
|
|
1524
|
+
if (
|
|
1525
|
+
blob_start is None or data_slots <= 0 or data_slots != lines_max * chans_max
|
|
1526
|
+
or DIRECTORY_OFFSET + data_slots * _DIRECTORY_SLOT_DTYPE.itemsize > blob_start
|
|
1527
|
+
):
|
|
1528
|
+
return None
|
|
1529
|
+
f.seek(DIRECTORY_OFFSET)
|
|
1530
|
+
raw = f.read(data_slots * _DIRECTORY_SLOT_DTYPE.itemsize)
|
|
1531
|
+
if len(raw) != data_slots * _DIRECTORY_SLOT_DTYPE.itemsize:
|
|
1532
|
+
return None
|
|
1533
|
+
slots = np.frombuffer(raw, dtype=_DIRECTORY_SLOT_DTYPE)
|
|
1534
|
+
nonzero = np.nonzero((slots["word"] != 0) | (slots["n_pages"] != 0))[0]
|
|
1535
|
+
if len(nonzero) == 0:
|
|
1536
|
+
return None
|
|
1537
|
+
entries = {
|
|
1538
|
+
int(i): (int(slots["word"][i]), int(slots["n_pages"][i])) for i in nonzero
|
|
1539
|
+
}
|
|
1540
|
+
return BlobDirectory(data_slots=data_slots, entries=entries)
|
|
1541
|
+
|
|
1542
|
+
|
|
1304
1543
|
def _element_width(channel: ChannelRecord) -> Optional[int]:
|
|
1305
1544
|
"""
|
|
1306
1545
|
Byte width of one element of `channel`'s data.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|