# -*- coding: utf-8 -*- """SIxMemo(COSTN.SMT · PRJLST.SMT · QCODE.SMT) 무손실 읽기·고쳐 쓰기 + PRJLST.SMT 키=값 본문 갱신. 근거: SIxMemo_구조.md · 25_설정키_전수와_식템플릿.md §1 · 실무 STC 7건 실측 구조(실측): [0..4) 다음 빈 블록 번호(LE) = ceil(파일 길이 / 블록 크기) [4..8) 블록 크기 = 64 [8..16) "SIxMemo\\0" · 나머지 0 → 512바이트(8블록) 머리 메모 = 블록 경계에서 시작 · 블록 패딩 없이 다음 메모는 다음 블록부터 · 마지막 메모 뒤 패딩 없음 DBF 메모 칸 10바이트 = · 빈 메모 = 공백 10 고쳐 쓰기(SIx 동작과 같게): 새 본문이 차지하던 블록 수 안에 들면 그 자리에 덮어씀(남는 옛 바이트 유지 — 실무 COSTN.SMT 패딩에 옛 글자가 남은 것과 같음) · 넘치면 '다음 빈 블록'에 붙이고 머리 갱신. """ from __future__ import annotations import re import struct SIG = b"SIxMemo\x00" BLANK_PTR = b" " * 10 HEAD_BLOCKS = 8 DEFAULT_TAG = 8 def ptr_valid(ptr: bytes) -> bool: if len(ptr) < 10 or ptr[:10] == BLANK_PTR or not ptr[:10].strip(b" \x00"): return False _tag, ln, blk = struct.unpack(" 0 and blk >= HEAD_BLOCKS def ptr_unpack(ptr: bytes) -> tuple[int, int, int]: return struct.unpack(" bytes: return struct.pack(" int: return struct.unpack_from(" int: return struct.unpack_from(" int: return max(1, -(-n // self.bs)) def read(self, ptr: bytes) -> bytes: if not ptr_valid(ptr): return b"" _tag, ln, blk = ptr_unpack(ptr) return bytes(self.buf[blk * self.bs : blk * self.bs + ln]) def write(self, ptr: bytes, data: bytes) -> bytes: """본문 data 를 쓰고 새 10바이트 포인터 반환(같으면 원래 포인터).""" if not data: return BLANK_PTR valid = ptr_valid(ptr) tag = ptr_unpack(ptr)[0] if valid else DEFAULT_TAG if valid and self.read(ptr) == data: return bytes(ptr[:10]) self.dirty = True if valid: _t, ln, blk = ptr_unpack(ptr) if self._blocks(len(data)) <= self._blocks(ln): st = blk * self.bs if len(self.buf) < st + len(data): self.buf.extend(b"\x00" * (st + len(data) - len(self.buf))) self.buf[st : st + len(data)] = data return ptr_pack(tag, len(data), blk) blk = max(self.next_free, -(-len(self.buf) // self.bs)) self.buf.extend(b"\x00" * (blk * self.bs - len(self.buf))) self.buf.extend(data) struct.pack_into(" bytes: return bytes(self.buf) def memo_text_bytes(text: str) -> bytes: """화면 글(\\n) → 저장 본문(\\r\\n · cp949 엄격).""" norm = str(text).replace("\r\n", "\n").replace("\r", "\n").replace("\n", "\r\n") try: return norm.encode("cp949") except UnicodeEncodeError as e: raise ValueError(f"메모에 cp949 로 쓸 수 없는 글자: {e.object[e.start : e.end]!r}") from e def same_memo(a: bytes, b: bytes) -> bool: n = lambda x: x.rstrip(b"\x00").replace(b"\r\n", b"\n").replace(b"\r", b"\n") # noqa: E731 return n(a) == n(b) # ── PRJLST.SMT 본문 = [섹션] 과 키=값 줄(CRLF) ── _KEY_LINE = re.compile(r"^([A-Za-z0-9_]+)=(.*)$") def ini_items(text: str) -> list[tuple[str, str, str]]: """(섹션, 키, 값) 목록 — 본문 순서 그대로.""" out, sec = [], "" for ln in text.splitlines(): s = ln.strip() if s.startswith("[") and s.endswith("]"): sec = s[1:-1] continue m = _KEY_LINE.match(s) if m: out.append((sec, m.group(1), m.group(2).strip())) return out def delphi_num(v: float) -> str: """Delphi FloatToStr 꼴(유효숫자 15) — 실측 '47149628.1818182' · '16.5' · '0'.""" if float(v) == int(v) and abs(v) < 1e15: return str(int(v)) return f"{v:.15g}" def _same_ini_value(old: str, new: object) -> bool: try: return float(old) == float(delphi_num(float(new))) # type: ignore[arg-type] except (TypeError, ValueError): return old.strip() == str(new).strip() def ini_value_text(new: object) -> str: if isinstance(new, bool): return "1" if new else "0" if isinstance(new, (int, float)): return delphi_num(float(new)) return str(new).strip() def ini_update(text: str, updates: dict) -> tuple[str, list[str]]: """키 값을 그 줄 자리에서 바꿈 · 없는 키는 접두 차수의 [n_KANJUB] 끝(없으면 [KANJUB])에 덧붙임.""" lines = text.split("\r\n") changed: list[str] = [] index = {} sec_end: dict[str, int] = {} sec = "" for i, ln in enumerate(lines): s = ln.strip() if s.startswith("[") and s.endswith("]"): sec = s[1:-1] continue m = _KEY_LINE.match(s) if m: index.setdefault(m.group(1), i) sec_end[sec] = i for k, v in updates.items(): if v is None: continue if k in index: old = _KEY_LINE.match(lines[index[k]].strip()).group(2) if _same_ini_value(old, v): continue lines[index[k]] = f"{k}={ini_value_text(v)}" else: m = re.match(r"^(\d+)_", k) want = f"{m.group(1)}_KANJUB" if m else "KANJUB" if want in sec_end: at = sec_end[want] + 1 else: tail = 1 if lines and lines[-1] == "" else 0 at = len(lines) - tail lines.insert(at, f"[{want}]") at += 1 lines.insert(at, f"{k}={ini_value_text(v)}") for s2, e in sec_end.items(): if e >= at: sec_end[s2] = e + 1 sec_end[want] = at index = {kk: (ii + 1 if ii >= at else ii) for kk, ii in index.items()} index[k] = at changed.append(k) return "\r\n".join(lines), changed