34 lines
2.0 KiB
Python
34 lines
2.0 KiB
Python
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
"""Strict parser for the bounded Phase-1.0DQ inventory stream."""
|
|
from __future__ import annotations
|
|
from dataclasses import dataclass
|
|
import struct
|
|
|
|
MAGIC=b"CHI10DQ1";FRAME=struct.Struct("<8sIIiiIIQ24s");ENTRY=struct.Struct("<256sIIIIQQq24s")
|
|
HEADER=1;TERMINAL=2;ERROR=3;MAX_ENTRIES=256;MAX_WIRE=64+MAX_ENTRIES*320+64
|
|
class ProtocolError(ValueError):pass
|
|
@dataclass(frozen=True)
|
|
class InventoryEntry:name:str;file_type:int;mode:int;size:int;inode:int;mtime:int
|
|
def _frame(raw:bytes):
|
|
if len(raw)!=FRAME.size:raise ProtocolError("frame size")
|
|
value=FRAME.unpack(raw)
|
|
if value[0]!=MAGIC or value[1]!=1 or value[8]!=bytes(24):raise ProtocolError("frame identity")
|
|
return value
|
|
def parse_stream(raw:bytes)->tuple[InventoryEntry,...]:
|
|
if not isinstance(raw,bytes) or not 128<=len(raw)<=MAX_WIRE or (len(raw)-128)%ENTRY.size:raise ProtocolError("stream length")
|
|
first=_frame(raw[:64]);kind,status,saved,count,reserved,transferred=first[2:8]
|
|
if kind==ERROR:raise ProtocolError(f"target error {status}:{saved}")
|
|
if kind!=HEADER or status or saved or count or reserved or transferred:raise ProtocolError("header invalid")
|
|
entries=[]
|
|
for offset in range(64,len(raw)-64,ENTRY.size):
|
|
name_raw,name_length,file_type,mode,reserved,size,inode,mtime,padding=ENTRY.unpack(raw[offset:offset+ENTRY.size])
|
|
if not 0<name_length<256 or reserved or padding!=bytes(24) or any(name_raw[name_length:]):raise ProtocolError("entry encoding")
|
|
try:name=name_raw[:name_length].decode("utf-8")
|
|
except UnicodeDecodeError as exc:raise ProtocolError("entry utf8") from exc
|
|
if name in {".",".."} or "/" in name or "\\" in name or "\x00" in name:raise ProtocolError("entry name")
|
|
entries.append(InventoryEntry(name,file_type,mode,size,inode,mtime))
|
|
terminal=_frame(raw[-64:]);kind,status,saved,count,reserved,transferred=terminal[2:8]
|
|
if kind!=TERMINAL or status or saved or reserved or count!=len(entries) or transferred!=len(entries)*ENTRY.size:raise ProtocolError("terminal invalid")
|
|
return tuple(entries)
|