Files
histsdk/scripts/decode-del-tep-capture.py
T
Joseph Doherty c1b1b3d23b R1.11 DelTep capture + R1.3/R1.4/R1.12/R1.13 bounded out
DelTep (extended-property delete) — wire format captured + serializer
golden-proven, but live delete is server-blocked and NOT exposed publicly:
- Captured the DelTep inBuff via a cross-session trick (harness add-tep gains
  --tep-skip-add + read-for-sync before --tep-delete; Capture-DeleteTagExtended
  Properties.ps1 / decode-del-tep-capture.py). Layout = same group framing as
  AddTEx but property-name-only (no 0x43 value) + 0x00 group trailer.
- SerializeDeleteRequest + 4 golden tests pin the server-accepted buffer.
- A decisive experiment shows SDK-added properties ARE deletable (the native
  client read-syncs and deletes one), so SDK-add is complete; the SDK's own
  DelTep is rejected by CHistStorage::DeleteTagExtendedProperties even with
  byte-identical inBuff, matching mode/handle, GetTgByNm+GetTepByNm prime, open
  channel, and 60s retries. Root cause: the native multiplexes services over one
  connection (per-connection working set); the SDK's per-service WCF channels
  don't reproduce it. Kept as documented-but-blocked internal orchestrator path;
  no public HistorianClient delete API.

Bounded out with evidence (no code; docs + roadmap + probe):
- R1.12 localized-property write — no op on 2020 (mirror of R1.6); no
  *LocalizedPropert*/TagLocalized* symbol in any current/*.dll.
- R1.13 non-analog tag create — GATED; native AddTag rejects every non-analog
  type client-side (ValidationFailed, before any WCF op): SingleByteString,
  DoubleByteString, Int1 all fail, Float works. No Discrete type in the native
  enum, no TagType setter. No wire request to capture.
- R1.3 timezone + R1.4 EventStorageMode — re-confirmed 2023R2/gRPC-only from
  the Runtime DB schema (no timezone param, no EventStorageMode anywhere) and a
  parameter-op probe (GetSystemParameter + GETRP return null/throw for every
  candidate; only HistorianVersion works).

238 unit tests pass; full solution builds with 0 warnings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
2026-06-21 11:26:21 -04:00

106 lines
3.3 KiB
Python

"""Decode the DeleteTagExtendedProperties (DelTep) WCF inBuff (HCAL R1.11 delete half).
Reads the Run-B capture produced by scripts/Capture-DeleteTagExtendedProperties.ps1 and dumps the
DelTep WriteMessage body so the delete-request framing (tag name + property names + the
delete-from-server flag) can be read off and compared to the AddTEx serializer.
Output is diagnostic. Sanitize before copying into docs/.
"""
import base64
import json
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
CAPDIR = REPO_ROOT / "artifacts" / "reverse-engineering" / "instrumented-wcf-del-tep"
CAP = CAPDIR / "del-tep-capture-latest.ndjson"
TAG = "RetestSdkWriteDelTepSdk"
PROP = "SdkDelProp"
OP_DEL = b"DelTep"
OP_GET = b"GetTepByNm"
def hexdump(label, buf, base=0):
print(f"=== {label}: {len(buf)} bytes ===")
for off in range(0, len(buf), 16):
c = buf[off:off + 16]
hp = " ".join(f"{x:02X}" for x in c)
ap = "".join(chr(x) if 32 <= x < 127 else "." for x in c)
print(f" {base + off:04X} {hp:<48} |{ap}|")
print()
def ascii_strings(buf, minlen=3):
out, cur, start = [], [], 0
for i, x in enumerate(buf):
if 32 <= x < 127:
if not cur:
start = i
cur.append(chr(x))
else:
if len(cur) >= minlen:
out.append((start, "".join(cur)))
cur = []
if len(cur) >= minlen:
out.append((start, "".join(cur)))
return out
def u16_strings(buf, minlen=3):
out, i = [], 0
while i < len(buf) - 1:
j, chars = i, []
while j < len(buf) - 1 and 32 <= buf[j] < 127 and buf[j + 1] == 0:
chars.append(chr(buf[j]))
j += 2
if len(chars) >= minlen:
out.append((i, "".join(chars)))
i = j
else:
i += 1
return out
def main() -> int:
if not CAP.exists():
print(f"Missing capture: {CAP}\nRun scripts/Capture-DeleteTagExtendedProperties.ps1 first.")
return 1
records = []
for line in CAP.open(encoding="utf-8-sig"):
if line.strip():
records.append(json.loads(line))
print(f"== {len(records)} MDAS bodies captured (Run B) ==")
for idx, rec in enumerate(records):
body = base64.b64decode(rec["Base64"])
flags = []
if OP_DEL in body:
flags.append("DelTep")
if OP_GET in body:
flags.append("GetTepByNm")
if TAG.encode("ascii") in body or TAG.encode("utf-16-le") in body:
flags.append("TAG")
if PROP.encode("ascii") in body or PROP.encode("utf-16-le") in body:
flags.append("PROP")
print(f" [{idx:02d}] {rec.get('Phase'):26s} len={len(body):5d} {','.join(flags)}")
print("\n== DelTep request(s) ==")
for idx, rec in enumerate(records):
body = base64.b64decode(rec["Base64"])
if rec.get("Phase") == "WCF.WriteMessage.Body" and OP_DEL in body:
hexdump(f"[{idx}] DelTep WriteMessage", body)
print(" UTF-16 strings:")
for off, s in u16_strings(body):
print(f" 0x{off:04X} {s!r}")
print(" ASCII strings:")
for off, s in ascii_strings(body):
print(f" 0x{off:04X} {s!r}")
print()
return 0
if __name__ == "__main__":
sys.exit(main())