- DBF 고친 칸만 다시 씀(한글 깨짐 · 2칸을 1칸으로 · 3칸 87.745% 덮어쓰기 없앰) · 단가 → 내역 행 → 소계 → 직공비 다시 셈 · 행 끼우기·지우기 - SIxMemo(COSTN.SMT · PRJLST.SMT) 블록 구조 유지 · 넘치면 뒤에 붙이고 가리키는 값 갱신 - STC 다시 묶기(같은 파일은 원본 바이트) · 검사 · 자동 백업·복원 · 새 공사 · 복사등록(차수 인계) · 해당분 압축 - BDQTY 역변환(쓰기 준비) · 모듈 dbf_rw · dbf_memo · dbf_crypt · stc_archive · stc_save · stc_read · stc_project · api_file · scr_file - 지킴 시험 test_stmate_file_roundtrip · file_project · file_payload — 실무 STC 7건 왕복 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
194 lines
6.8 KiB
Python
194 lines
6.8 KiB
Python
# -*- 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바이트 = <HII 꼴(8 · PRJLST 둘째 행 1), 바이트 길이, 블록 번호> · 빈 메모 = 공백 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("<HII", ptr[:10])
|
|
return ln > 0 and blk >= HEAD_BLOCKS
|
|
|
|
|
|
def ptr_unpack(ptr: bytes) -> tuple[int, int, int]:
|
|
return struct.unpack("<HII", ptr[:10])
|
|
|
|
|
|
def ptr_pack(tag: int, ln: int, blk: int) -> bytes:
|
|
return struct.pack("<HII", tag, ln, blk)
|
|
|
|
|
|
class SixMemo:
|
|
"""SMT 파일 한 개 — buf 를 직접 고침."""
|
|
|
|
def __init__(self, b: bytes | None = None, block_size: int = 64):
|
|
if b and b[8:16] == SIG:
|
|
self.buf = bytearray(b)
|
|
else:
|
|
self.buf = bytearray(block_size * HEAD_BLOCKS)
|
|
self.buf[8:16] = SIG
|
|
struct.pack_into("<II", self.buf, 0, HEAD_BLOCKS, block_size)
|
|
self.dirty = False
|
|
|
|
@property
|
|
def bs(self) -> int:
|
|
return struct.unpack_from("<I", self.buf, 4)[0] or 64
|
|
|
|
@property
|
|
def next_free(self) -> int:
|
|
return struct.unpack_from("<I", self.buf, 0)[0]
|
|
|
|
def _blocks(self, n: int) -> 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("<I", self.buf, 0, blk + self._blocks(len(data)))
|
|
return ptr_pack(tag, len(data), blk)
|
|
|
|
def to_bytes(self) -> 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
|