"""Small shared helpers: times, scopes, the compressed-text detector and Harbor's error type.""" import datetime import fnmatch import re import time # Run-together words such as "full222", "stub23HarnessBug" or "correctOffsetsWereWrong". Hex tokens (commit hashes, # addresses) and ordinary identifiers (PascalCase names, g_snake_case globals) are not counted. SQUASHED = re.compile(r"[A-Za-z]{3,}\d{2,}[A-Za-z]*|\b[a-z]{3,}[A-Z][a-z]{2,}[A-Z][a-z]{2,}[A-Za-z]*\b") HEXLIKE = re.compile(r"(?:0x)?[0-9A-Fa-f]+") PRIORITIES = {"urgent": 0, "high": 1, "normal": 2, "low": 3} PRIORITY_NAMES = {v: k for k, v in PRIORITIES.items()} class HarborError(Exception): """A refused operation; the message says why and what to do instead.""" def squashed_tokens(text): found = [] for token in re.split(r"[\s,;:()\[\]{}<>\"'`/|=+]+", text): token = token.strip(".!?-") if not token or HEXLIKE.fullmatch(token) or "_" in token: continue if SQUASHED.search(token): found.append(token) return found def utc(ts=None): return datetime.datetime.fromtimestamp(time.time() if ts is None else ts, datetime.timezone.utc) def iso(dt): return dt.strftime("%Y-%m-%dT%H:%M:%SZ") def parse_iso(text): return datetime.datetime.strptime(text, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=datetime.timezone.utc) def _span(text): m = re.fullmatch(r"\s*(\d+(?:\.\d+)?)\s*([mhd])\s*", str(text)) if not m: return None return datetime.timedelta(seconds=float(m.group(1)) * {"m": 60, "h": 3600, "d": 86400}[m.group(2)]) def parse_since(text, now): """'8h', '90m', '2d' or an ISO time -> ISO time in the past.""" span = _span(text) if span is not None: return iso(now - span) try: return iso(parse_iso(str(text).strip())) except ValueError: raise HarborError(f"since {text!r}: use 8h, 90m, 2d or 2026-10-02T13:25:00Z") def parse_until(text, now): """'30m', '2h', '1d' or an ISO time -> ISO time in the future (due dates, 'away until').""" span = _span(text) if span is not None: return iso(now + span) try: return iso(parse_iso(str(text).strip())) except ValueError: raise HarborError(f"time {text!r}: use 30m, 2h, 1d or 2026-10-03T13:00:00Z") def parse_priority(value): if isinstance(value, int) and value in PRIORITY_NAMES: return value key = str(value or "normal").lower() if key not in PRIORITIES: raise HarborError(f"priority must be one of {', '.join(PRIORITIES)}") return PRIORITIES[key] def parse_scope(scope): """'fn:0x529350' / '0x529350' / 'range:0x440000-0x47FFFF' -> (canonical, lo, hi); others keep lo/hi None.""" text = scope.strip() m = re.fullmatch(r"(?:fn:)?(0x[0-9A-Fa-f]+)", text) if m: value = int(m.group(1), 16) return f"fn:0x{value:06X}", value, value m = re.fullmatch(r"range:(0x[0-9A-Fa-f]+)-(0x[0-9A-Fa-f]+)", text) if m: lo, hi = int(m.group(1), 16), int(m.group(2), 16) if lo > hi: raise HarborError(f"range {text}: start is after end") return f"range:0x{lo:06X}-0x{hi:06X}", lo, hi m = re.fullmatch(r"(file|name|topic|task):(.+)", text) if m: value = m.group(2).strip() f = re.match(r"(?:.*[/\\])?([0-9A-Fa-f]{6})_[^/\\]*\.c$", value) if m.group(1) == "file" else None if f: # a function's file: it stands for that address address = int(f.group(1), 16) return f"file:{value}", address, address return f"{m.group(1)}:{value}", None, None raise HarborError(f"scope {scope!r}: use fn:0xADDR, range:0xLO-0xHI, name:Symbol, file:path, task:text or topic:text") def scopes_overlap(a, b): """a, b: (scope, lo, hi).""" if a[1] is not None and b[1] is not None: return a[1] <= b[2] and b[1] <= a[2] if a[1] is not None or b[1] is not None: return False ka, va = a[0].split(":", 1) kb, vb = b[0].split(":", 1) if ka != kb: return False if ka == "file": va, vb = va.replace("\\", "/").lower(), vb.replace("\\", "/").lower() return va == vb or fnmatch.fnmatch(va, vb) or fnmatch.fnmatch(vb, va) return va.lower() == vb.lower() def first_line(text, limit=160): line = (text or "").strip().split("\n", 1)[0] return line if len(line) <= limit else line[:limit - 1] + "…"