pynab icon indicating copy to clipboard operation
pynab copied to clipboard

par2 reader

Open ukharley opened this issue 7 years ago • 0 comments

Get release name from par2 file. Agan, needs someone to intergrate it as I'm not up to the task.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""A native python implementation of the par2 file format.
This is only intended to be able to read packets in par2, not execute
repair, verify, or create new par2 files.

Found on GitHubGist made some mods for python 3."""

import struct

PACKET_HEADER = ("<"
    "8s"  # MAGIC: PAR2\x00PKT
    "Q"   # unsigned 64bit length of entire packet in bytes
    "16s" # md5 of entire packet except first 3 fields
    "16s" # 'setid';  hash of the body of the main packet
    "16s" # packet type
)

FILE_DESCRIPTION_PACKET = ("<64s" # PACKET_HEADER
    "16s" # fileid, hash of [hash16k, length, name]
    "16s" # hashfull;  hash of the whole file (which?)
    "16s" # hash16k;  hash of the first 16k of the file (which?)
    "Q"   # length of the file
)

class Header(object):
    fmt = PACKET_HEADER
    def __init__(self, par2file, offset=0):
        self.raw = par2file[offset:offset+struct.calcsize(self.fmt)]
        parts = struct.unpack(self.fmt, self.raw)
        self.magic = parts[0]
        self.length = parts[1]
        self.hash = parts[2]
        self.setid = parts[3]
        self.type = parts[4]

    def verify(self):
        return self.magic == 'PAR2\x00PKT'

class UnknownPar2Packet(object):
    fmt = PACKET_HEADER
    def __init__(self, par2file, offset=0):
        self.raw = par2file[offset:offset+struct.calcsize(self.fmt)]
        self.header = Header(self.raw)

class FileDescriptionPacket(object):
    header_type = b'PAR 2.0\x00FileDesc'
    fmt = FILE_DESCRIPTION_PACKET

    def __init__(self, par2file, offset=0):
        name_start = offset+struct.calcsize(self.fmt)
        self.raw = par2file[offset:name_start]
        parts = struct.unpack(self.fmt, self.raw)
        self.header = Header(parts[0])
        packet = par2file[offset:offset+self.header.length]
        self.fileid = parts[1]
        self.file_hashfull = parts[2]
        self.file_hash16k = parts[3]
        self.file_length = parts[4]
        self.name = packet[struct.calcsize(self.fmt):].strip(b'\x00')


class Par2File(object):
    def __init__(self, obj_or_path):
        if isinstance(obj_or_path, (str, bytes)):
            with open(obj_or_path, "rb") as binary_file:
                # Read the whole file at once
                self.contents = binary_file.read()
        else:
            self.contents = obj_or_path.read()
        self.packets = self.read_packets()

    def read_packets(self):
        offset = 0
        filelen = len(self.contents)
        packets = []
        while offset < filelen:
            header = Header(self.contents, offset)
            if header.type == FileDescriptionPacket.header_type:
                packets.append(FileDescriptionPacket(self.contents, offset))
            else:
                packets.append(UnknownPar2Packet(self.contents, offset))
            offset += header.length
        return packets

    def filenames(self):
        return [p.name for p in self.packets if isinstance(p, FileDescriptionPacket)]

pf = Par2File(".\p.par2") # get a test par2 and put it in same folder as this module
fn = pf.filenames()
print(fn[0])

ukharley avatar Aug 12 '17 13:08 ukharley