Add class for resource filters in place of lambdas

This is easier to debug (printing out a lambda doesn't show what values
it checks against) and makes it easier to check that the filter values
are valid.
This commit is contained in:
dgelessus 2019-12-21 03:46:45 +01:00
parent 2b0bbb19ed
commit 2abf6e2a06

View File

@ -83,47 +83,76 @@ def bytes_escape(bs: bytes, *, quote: typing.Optional[str]=None) -> str:
return "".join(out) return "".join(out)
def filter_to_predicate(filter: str) -> typing.Callable[[api.Resource], bool]: MIN_RESOURCE_ID = -0x8000
if len(filter) == 4: MAX_RESOURCE_ID = 0x7fff
restype = filter.encode("ascii")
return lambda res: res.type == restype class ResourceFilter(object):
elif filter[0] == filter[-1] == "'": type: bytes
restype = bytes_unescape(filter[1:-1]) min_id: int
return lambda res: res.type == restype max_id: int
else: name: typing.Optional[bytes]
pos = filter.find("'", 1)
if pos == -1: @classmethod
raise ValueError(f"Invalid filter {filter!r}: Resource type must be single-quoted") def from_string(cls, filter: str) -> "ResourceFilter":
elif filter[pos + 1] != " ": if len(filter) == 4:
raise ValueError(f"Invalid filter {filter!r}: Resource type and ID must be separated by a space") restype = filter.encode("ascii")
return cls(restype, MIN_RESOURCE_ID, MAX_RESOURCE_ID, None)
restype_str, resid_str = filter[:pos + 1], filter[pos + 2:] elif filter[0] == filter[-1] == "'":
restype = bytes_unescape(filter[1:-1])
if not restype_str[0] == restype_str[-1] == "'": return cls(restype, MIN_RESOURCE_ID, MAX_RESOURCE_ID, None)
raise ValueError( else:
f"Invalid filter {filter!r}: Resource type is not a single-quoted type identifier: {restype_str!r}") pos = filter.find("'", 1)
restype = bytes_unescape(restype_str[1:-1]) if pos == -1:
raise ValueError(f"Invalid filter {filter!r}: Resource type must be single-quoted")
elif filter[pos + 1] != " ":
raise ValueError(f"Invalid filter {filter!r}: Resource type and ID must be separated by a space")
restype_str, resid_str = filter[:pos + 1], filter[pos + 2:]
if not restype_str[0] == restype_str[-1] == "'":
raise ValueError(
f"Invalid filter {filter!r}: Resource type is not a single-quoted type identifier: {restype_str!r}")
restype = bytes_unescape(restype_str[1:-1])
if resid_str[0] != "(" or resid_str[-1] != ")":
raise ValueError(f"Invalid filter {filter!r}: Resource ID must be parenthesized")
resid_str = resid_str[1:-1]
if resid_str[0] == resid_str[-1] == '"':
name = bytes_unescape(resid_str[1:-1])
return cls(restype, MIN_RESOURCE_ID, MAX_RESOURCE_ID, name)
elif ":" in resid_str:
if resid_str.count(":") > 1:
raise ValueError(f"Invalid filter {filter!r}: Too many colons in ID range expression: {resid_str!r}")
start_str, end_str = resid_str.split(":")
start, end = int(start_str), int(end_str)
return cls(restype, start, end, None)
else:
resid = int(resid_str)
return cls(restype, resid, resid, None)
def __init__(self, restype: bytes, min_id: int, max_id: int, name: typing.Optional[bytes]) -> None:
super().__init__()
if len(restype) != 4: if len(restype) != 4:
raise ValueError( raise ValueError(f"Invalid filter: Type code must be exactly 4 bytes long, not {len(restype)} bytes: {restype!r}")
f"Invalid filter {filter!r}: Type identifier must be 4 bytes after replacing escapes, got {len(restype)} bytes: {restype!r}") elif min_id < MIN_RESOURCE_ID:
raise ValueError(f"Invalid filter: Resource ID lower bound ({min_id}) cannot be lower than {MIN_RESOURCE_ID}")
elif max_id > MAX_RESOURCE_ID:
raise ValueError(f"Invalid filter: Resource ID upper bound ({max_id}) cannot be greater than {MAX_RESOURCE_ID}")
elif min_id > max_id:
raise ValueError(f"Invalid filter: Resource ID lower bound ({min_id}) cannot be greater than upper bound ({max_id})")
if resid_str[0] != "(" or resid_str[-1] != ")": self.type = restype
raise ValueError(f"Invalid filter {filter!r}: Resource ID must be parenthesized") self.min_id = min_id
resid_str = resid_str[1:-1] self.max_id = max_id
self.name = name
if resid_str[0] == resid_str[-1] == '"':
name = bytes_unescape(resid_str[1:-1]) def __repr__(self) -> str:
return lambda res: res.type == restype and res.name == name return f"{type(self).__name__}({self.type!r}, {self.min_id!r}, {self.max_id!r}, {self.name!r})"
elif ":" in resid_str:
if resid_str.count(":") > 1: def matches(self, res: api.Resource) -> bool:
raise ValueError(f"Invalid filter {filter!r}: Too many colons in ID range expression: {resid_str!r}") return res.type == self.type and self.min_id <= res.id <= self.max_id and (self.name is None or res.name == self.name)
start_str, end_str = resid_str.split(":")
start, end = int(start_str), int(end_str)
return lambda res: res.type == restype and start <= res.id <= end
else:
resid = int(resid_str)
return lambda res: res.type == restype and res.id == resid
def filter_resources(rf: api.ResourceFile, filters: typing.Sequence[str]) -> typing.Iterable[api.Resource]: def filter_resources(rf: api.ResourceFile, filters: typing.Sequence[str]) -> typing.Iterable[api.Resource]:
if not filters: if not filters:
@ -131,11 +160,11 @@ def filter_resources(rf: api.ResourceFile, filters: typing.Sequence[str]) -> typ
for reses in rf.values(): for reses in rf.values():
yield from reses.values() yield from reses.values()
else: else:
preds = [filter_to_predicate(filter) for filter in filters] filter_objs = [ResourceFilter.from_string(filter) for filter in filters]
for reses in rf.values(): for reses in rf.values():
for res in reses.values(): for res in reses.values():
if any(pred(res) for pred in preds): if any(filter_obj.matches(res) for filter_obj in filter_objs):
yield res yield res
def hexdump(data: bytes) -> None: def hexdump(data: bytes) -> None: