Files
Hippolyzer/hippolyzer/lib/base/legacy_inv.py
2021-04-30 17:30:24 +00:00

256 lines
8.1 KiB
Python

"""
Parse the horrible legacy inventory format
It's typically only used for object contents now.
"""
from __future__ import annotations
import abc
import dataclasses
import datetime as dt
import itertools
import logging
import re
import weakref
from typing import *
from hippolyzer.lib.base.datatypes import UUID
LOG = logging.getLogger(__name__)
MAGIC_ID = UUID("3c115e51-04f4-523c-9fa6-98aff1034730")
def _parse_str(val: str):
return val.rstrip("|")
def _int_from_hex(val: str):
return int(val, 16)
def _parse_date(val: str):
return dt.datetime.utcfromtimestamp(int(val))
class InventoryParsingError(Exception):
pass
def _inv_field(spec: Union[Callable, Type], *, default=dataclasses.MISSING, init=True, repr=True, # noqa
hash=None, compare=True) -> dataclasses.Field: # noqa
"""Describe a field in the inventory schema and the shape of its value"""
return dataclasses.field(
metadata={"spec": spec}, default=default, init=init,
repr=repr, hash=hash, compare=compare
)
# The schema is meant to allow multi-line strings, but in practice
# it does not due to scanf() shenanigans. This is fine.
_INV_TOKEN_RE = re.compile(r'\A\s*([^\s]+)(\s+([^\t\r\n]+))?$')
def _parse_inv_line(line: str):
g = _INV_TOKEN_RE.search(line)
if not g:
raise InventoryParsingError("%r doesn't match the token regex" % line)
return g.group(1), g.group(3)
def _yield_inv_tokens(line_iter: Iterator[str]):
in_bracket = False
for line in line_iter:
line = line.strip()
if not line:
continue
try:
key, val = _parse_inv_line(line)
except InventoryParsingError:
# Can happen if there's a malformed multi-line string, just
# skip by it.
LOG.warning(f"Found invalid inventory line {line!r}")
continue
if key == "{":
if in_bracket:
LOG.warning("Found multiple opening brackets inside structure, "
"was a nested structure not handled?")
in_bracket = True
continue
if key == "}":
in_bracket = False
break
yield key, val
if in_bracket:
raise LOG.warning("Reached EOF while inside a bracket")
class InventoryModel:
def __init__(self):
self.containers: Dict[UUID, InventoryContainerBase] = {}
self.items: Dict[UUID, InventoryItem] = {}
self.root: Optional[InventoryContainerBase] = None
@classmethod
def from_str(cls, text: str):
return cls.from_iter(iter(text.splitlines()))
@classmethod
def from_bytes(cls, data: bytes):
return cls.from_str(data.decode("utf8"))
@classmethod
def from_iter(cls, line_iter: Iterator[str]) -> InventoryModel:
model = cls()
for key, value in _yield_inv_tokens(line_iter):
if key == "inv_object":
obj = InventoryObject.from_iter(line_iter)
if obj is not None:
model.add_container(obj)
elif key == "inv_category":
cat = InventoryCategory.from_iter(line_iter)
if cat is not None:
model.add_container(cat)
elif key == "inv_item":
item = InventoryItem.from_iter(line_iter)
if item is not None:
model.add_item(item)
else:
LOG.warning("Unknown key {0}".format(key))
model.reparent_nodes()
return model
def add_container(self, container: InventoryContainerBase):
self.containers[container.node_id] = container
container.model = weakref.proxy(self)
def add_item(self, item: InventoryItem):
self.items[item.item_id] = item
item.model = weakref.proxy(self)
def reparent_nodes(self):
self.root = None
for container in self.containers.values():
container.children.clear()
if container.parent_id == UUID():
self.root = container
for obj in itertools.chain(self.items.values(), self.containers.values()):
if not obj.parent_id or obj.parent_id == UUID():
continue
parent_container = self.containers.get(obj.parent_id)
if not parent_container:
LOG.warning("{0} had an invalid parent {1}".format(obj, obj.parent_id))
continue
parent_container.children.append(obj)
@dataclasses.dataclass
class InventoryBase(abc.ABC):
@classmethod
def _fields_dict(cls):
return {f.name: f for f in dataclasses.fields(cls)}
@classmethod
def from_iter(cls, line_iter: Iterator[str]):
fields = cls._fields_dict()
obj = {}
for key, val in _yield_inv_tokens(line_iter):
if key in fields:
field: dataclasses.Field = fields[key]
spec = field.metadata.get("spec")
# Not a real key, an internal var on our dataclass
if not spec:
LOG.warning(f"Internal key {key!r}")
continue
# some kind of nested structure like sale_info
if isinstance(spec, type) and issubclass(spec, InventoryBase):
obj[key] = spec.from_iter(line_iter)
else:
obj[key] = spec(val)
else:
LOG.warning(f"Unknown key {key!r}")
# Bad entry, ignore
# TODO: Check on these. might be symlinks or something.
if obj.get("type") == "-1":
LOG.warning(f"Skipping bad object with type == -1: {obj!r}")
return None
return cls(**obj) # type: ignore
@dataclasses.dataclass
class InventoryPermissions(InventoryBase):
base_mask: int = _inv_field(_int_from_hex)
owner_mask: int = _inv_field(_int_from_hex)
group_mask: int = _inv_field(_int_from_hex)
everyone_mask: int = _inv_field(_int_from_hex)
next_owner_mask: int = _inv_field(_int_from_hex)
creator_id: UUID = _inv_field(UUID)
owner_id: UUID = _inv_field(UUID)
last_owner_id: UUID = _inv_field(UUID)
group_id: UUID = _inv_field(UUID)
@dataclasses.dataclass
class InventorySaleInfo(InventoryBase):
sale_type: str = _inv_field(str)
sale_price: int = _inv_field(int)
@dataclasses.dataclass
class InventoryNodeBase(InventoryBase):
ID_ATTR: ClassVar[str]
parent_id: Optional[UUID] = _inv_field(UUID)
model: Optional[InventoryModel] = dataclasses.field(default=None, init=False)
@property
def node_id(self) -> UUID:
return getattr(self, self.ID_ATTR)
@property
def parent(self):
return self.model.containers.get(self.parent_id)
@dataclasses.dataclass
class InventoryContainerBase(InventoryNodeBase):
type: str = _inv_field(str)
name: str = _inv_field(_parse_str)
children: List[InventoryNodeBase] = dataclasses.field(default_factory=list, init=False)
@dataclasses.dataclass
class InventoryObject(InventoryContainerBase):
ID_ATTR: ClassVar[str] = "obj_id"
obj_id: UUID = _inv_field(UUID)
@dataclasses.dataclass
class InventoryCategory(InventoryContainerBase):
ID_ATTR: ClassVar[str] = "cat_id"
cat_id: UUID = _inv_field(UUID)
pref_type: str = _inv_field(str)
owner_id: UUID = _inv_field(UUID)
version: int = _inv_field(int)
@dataclasses.dataclass
class InventoryItem(InventoryNodeBase):
ID_ATTR: ClassVar[str] = "item_id"
item_id: UUID = _inv_field(UUID)
type: str = _inv_field(str)
inv_type: str = _inv_field(str)
flags: int = _inv_field(_int_from_hex)
name: str = _inv_field(_parse_str)
desc: str = _inv_field(_parse_str)
creation_date: dt.datetime = _inv_field(_parse_date)
permissions: InventoryPermissions = _inv_field(InventoryPermissions)
sale_info: InventorySaleInfo = _inv_field(InventorySaleInfo)
asset_id: Optional[UUID] = _inv_field(UUID, default=None)
shadow_id: Optional[UUID] = _inv_field(UUID, default=None)
@property
def true_asset_id(self) -> UUID:
if self.asset_id is not None:
return self.asset_id
return self.shadow_id ^ MAGIC_ID