Skip to content

Commit

Permalink
Also add XVA
Browse files Browse the repository at this point in the history
  • Loading branch information
Schamper committed Nov 6, 2024
1 parent fafc16a commit 390196c
Showing 1 changed file with 136 additions and 0 deletions.
136 changes: 136 additions & 0 deletions dissect/archive/xva.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import hashlib
import tarfile
from bisect import bisect_right
from xml.etree import ElementTree

Check warning on line 4 in dissect/archive/xva.py

View check run for this annotation

Codecov / codecov/patch

dissect/archive/xva.py#L1-L4

Added lines #L1 - L4 were not covered by tests

from dissect.util.stream import AlignedStream

Check warning on line 6 in dissect/archive/xva.py

View check run for this annotation

Codecov / codecov/patch

dissect/archive/xva.py#L6

Added line #L6 was not covered by tests

BLOCK_SIZE = 1024 * 1024

Check warning on line 8 in dissect/archive/xva.py

View check run for this annotation

Codecov / codecov/patch

dissect/archive/xva.py#L8

Added line #L8 was not covered by tests


class XVA:

Check warning on line 11 in dissect/archive/xva.py

View check run for this annotation

Codecov / codecov/patch

dissect/archive/xva.py#L11

Added line #L11 was not covered by tests
"""XVA reader.
XenCenter export format. Basically a tar file with "blocks" of 1MB.
"""

def __init__(self, fh):

Check warning on line 17 in dissect/archive/xva.py

View check run for this annotation

Codecov / codecov/patch

dissect/archive/xva.py#L17

Added line #L17 was not covered by tests
# We don't have to cache tar members, tarfile already does that for us
self.tar = tarfile.open(fileobj=fh)
self._ova = None

Check warning on line 20 in dissect/archive/xva.py

View check run for this annotation

Codecov / codecov/patch

dissect/archive/xva.py#L19-L20

Added lines #L19 - L20 were not covered by tests

@property
def ova(self):
if not self._ova:
ova_member = self.tar.getmember("ova.xml")
ova_fh = self.tar.extractfile(ova_member)
self._ova = ElementTree.fromstring(ova_fh.read())
return self._ova

Check warning on line 28 in dissect/archive/xva.py

View check run for this annotation

Codecov / codecov/patch

dissect/archive/xva.py#L22-L28

Added lines #L22 - L28 were not covered by tests

def disks(self):
return [

Check warning on line 31 in dissect/archive/xva.py

View check run for this annotation

Codecov / codecov/patch

dissect/archive/xva.py#L30-L31

Added lines #L30 - L31 were not covered by tests
el.text
for el in self.ova.findall(
"*//member/name[.='VDI']/../..//name[.='type']/..value[.='Disk']/../..//name[.='VDI']/../value"
)
]

def open(self, ref, verify=False):
size = int(

Check warning on line 39 in dissect/archive/xva.py

View check run for this annotation

Codecov / codecov/patch

dissect/archive/xva.py#L38-L39

Added lines #L38 - L39 were not covered by tests
self.ova.find(f"*//member/name[.='id']/../value[.='{ref}']/../..//name[.='virtual_size']/../value").text
)
return XVAStream(self, ref, size, verify)

Check warning on line 42 in dissect/archive/xva.py

View check run for this annotation

Codecov / codecov/patch

dissect/archive/xva.py#L42

Added line #L42 was not covered by tests


class XVAStream(AlignedStream):

Check warning on line 45 in dissect/archive/xva.py

View check run for this annotation

Codecov / codecov/patch

dissect/archive/xva.py#L45

Added line #L45 was not covered by tests
"""XVA stream.
XenServer usually just streams an XVA file right into an output file, so our use-case requires a bit
more trickery. We generally don't stream directly into an output file, but try to create a file-like
object for other code to use.
The numbers for the block files (weirdly) don't represent offsets. It's possible for a block file
to be 0 sized, in which case you should "add" that block to the stream, and continue on to the next.
The next block might have a number + 1 of what your current offset is, but it will still contain the
data for that current offset. For this reason we build a lookup list with offsets.
"""

def __init__(self, xva, ref, size, verify=False):
self.xva = xva
self.ref = ref
self.verify = verify

Check warning on line 61 in dissect/archive/xva.py

View check run for this annotation

Codecov / codecov/patch

dissect/archive/xva.py#L58-L61

Added lines #L58 - L61 were not covered by tests

index = 0
offset = 0
self._lookup = []
self._members = []
for block_index, block_member, checksum_member in _iter_block_files(xva, ref):
if block_index > index + 1:
skipped = block_index - (index + 1)
offset += skipped * BLOCK_SIZE

Check warning on line 70 in dissect/archive/xva.py

View check run for this annotation

Codecov / codecov/patch

dissect/archive/xva.py#L63-L70

Added lines #L63 - L70 were not covered by tests

if block_member.size != 0:
self._lookup.append(offset)
self._members.append((block_member, checksum_member))

Check warning on line 74 in dissect/archive/xva.py

View check run for this annotation

Codecov / codecov/patch

dissect/archive/xva.py#L72-L74

Added lines #L72 - L74 were not covered by tests

offset += block_member.size

Check warning on line 76 in dissect/archive/xva.py

View check run for this annotation

Codecov / codecov/patch

dissect/archive/xva.py#L76

Added line #L76 was not covered by tests

index = block_index

Check warning on line 78 in dissect/archive/xva.py

View check run for this annotation

Codecov / codecov/patch

dissect/archive/xva.py#L78

Added line #L78 was not covered by tests

super().__init__(size, align=BLOCK_SIZE)

Check warning on line 80 in dissect/archive/xva.py

View check run for this annotation

Codecov / codecov/patch

dissect/archive/xva.py#L80

Added line #L80 was not covered by tests

def _read(self, offset, length):
result = []

Check warning on line 83 in dissect/archive/xva.py

View check run for this annotation

Codecov / codecov/patch

dissect/archive/xva.py#L82-L83

Added lines #L82 - L83 were not covered by tests

while length > 0:

Check warning on line 85 in dissect/archive/xva.py

View check run for this annotation

Codecov / codecov/patch

dissect/archive/xva.py#L85

Added line #L85 was not covered by tests
# This method is probably sub-optimal, but it's fairly low effort and we rarely encounter XVA anyway
block_idx = bisect_right(self._lookup, offset)
nearest_offset = self._lookup[block_idx - 1]

Check warning on line 88 in dissect/archive/xva.py

View check run for this annotation

Codecov / codecov/patch

dissect/archive/xva.py#L87-L88

Added lines #L87 - L88 were not covered by tests

if offset >= nearest_offset + BLOCK_SIZE:
result.append(b"\x00" * BLOCK_SIZE)

Check warning on line 91 in dissect/archive/xva.py

View check run for this annotation

Codecov / codecov/patch

dissect/archive/xva.py#L90-L91

Added lines #L90 - L91 were not covered by tests
else:
block_member, checksum_member = self._members[block_idx - 1]
buf = self.xva.tar.extractfile(block_member).read()

Check warning on line 94 in dissect/archive/xva.py

View check run for this annotation

Codecov / codecov/patch

dissect/archive/xva.py#L93-L94

Added lines #L93 - L94 were not covered by tests

if self.verify:
if checksum_member is None:
raise ValueError(f"No checksum for {block_member.name}")

Check warning on line 98 in dissect/archive/xva.py

View check run for this annotation

Codecov / codecov/patch

dissect/archive/xva.py#L96-L98

Added lines #L96 - L98 were not covered by tests

if (

Check warning on line 100 in dissect/archive/xva.py

View check run for this annotation

Codecov / codecov/patch

dissect/archive/xva.py#L100

Added line #L100 was not covered by tests
checksum_member.name.endswith("checksum")
and hashlib.sha1(buf).hexdigest() != self.xva.tar.extractfile(checksum_member).read().decode()
):
raise ValueError(f"Invalid checksum for {checksum_member.name}")

Check warning on line 104 in dissect/archive/xva.py

View check run for this annotation

Codecov / codecov/patch

dissect/archive/xva.py#L104

Added line #L104 was not covered by tests
else:
raise NotImplementedError(f"Unsupported checksum: {checksum_member.name}")

Check warning on line 106 in dissect/archive/xva.py

View check run for this annotation

Codecov / codecov/patch

dissect/archive/xva.py#L106

Added line #L106 was not covered by tests

result.append(buf)

Check warning on line 108 in dissect/archive/xva.py

View check run for this annotation

Codecov / codecov/patch

dissect/archive/xva.py#L108

Added line #L108 was not covered by tests

offset += BLOCK_SIZE
length -= BLOCK_SIZE

Check warning on line 111 in dissect/archive/xva.py

View check run for this annotation

Codecov / codecov/patch

dissect/archive/xva.py#L110-L111

Added lines #L110 - L111 were not covered by tests

return b"".join(result)

Check warning on line 113 in dissect/archive/xva.py

View check run for this annotation

Codecov / codecov/patch

dissect/archive/xva.py#L113

Added line #L113 was not covered by tests


def _iter_block_files(xva, ref):
member_index = None
block_member = None
checksum_member = None

Check warning on line 119 in dissect/archive/xva.py

View check run for this annotation

Codecov / codecov/patch

dissect/archive/xva.py#L116-L119

Added lines #L116 - L119 were not covered by tests

for member in xva.tar.getmembers():
if not member.name.startswith(ref):
continue

Check warning on line 123 in dissect/archive/xva.py

View check run for this annotation

Codecov / codecov/patch

dissect/archive/xva.py#L121-L123

Added lines #L121 - L123 were not covered by tests

index = int(member.name.split("/")[-1].split(".")[0])
if member_index is None:
member_index = index

Check warning on line 127 in dissect/archive/xva.py

View check run for this annotation

Codecov / codecov/patch

dissect/archive/xva.py#L125-L127

Added lines #L125 - L127 were not covered by tests

if member_index != index:
yield (member_index, block_member, checksum_member)
member_index = index

Check warning on line 131 in dissect/archive/xva.py

View check run for this annotation

Codecov / codecov/patch

dissect/archive/xva.py#L129-L131

Added lines #L129 - L131 were not covered by tests

if member.name.endswith(("checksum", "xxhash")):
checksum_member = member

Check warning on line 134 in dissect/archive/xva.py

View check run for this annotation

Codecov / codecov/patch

dissect/archive/xva.py#L133-L134

Added lines #L133 - L134 were not covered by tests
else:
block_member = member

Check warning on line 136 in dissect/archive/xva.py

View check run for this annotation

Codecov / codecov/patch

dissect/archive/xva.py#L136

Added line #L136 was not covered by tests

0 comments on commit 390196c

Please sign in to comment.