#!/usr/bin/env python3.14# -*- coding: utf-8 -*-"""Shared JSON persistence and untrusted-text hygiene for the service stores.Every store file lives in its service's private state dir (`/var/lib/euler-auth`,`/var/lib/euler-msg`), is owned by that service user alone, and is writtenatomically at mode `0600` — the service is the sole reader and writer; adminoperations go through the admin API, never through these files.:func:`sanitize` is here rather than beside any one store because both the storesthat hold **user-authored free text** need it — the invite-request queue(:mod:`euler_site.auth.requests`) and the message spool(:mod:`euler_webshell.msg.store`) — and the second must not import the first(web-server-guide § Messaging)."""from__future__importannotations__all__=['load_json','sanitize','save_json']importjsonimportosimporttempfilefrompathlibimportPathfromtypingimportAnydefload_json(path:Path)->dict[str,Any]:"""Return the JSON object at *path*; an empty dict if absent or invalid."""try:data:Any=json.loads(path.read_text(encoding='utf-8'))except(OSError,json.JSONDecodeError):return{}returndataifisinstance(data,dict)else{}defsave_json(path:Path,data:dict[str,Any])->None:"""Write *data* to *path* atomically (tmp file + rename) at mode `0600`."""path.parent.mkdir(parents=True,exist_ok=True)fd,tmp_name=tempfile.mkstemp(dir=path.parent,prefix=f'.{path.name}.')try:withos.fdopen(fd,'w',encoding='utf-8')ashandle:json.dump(data,handle,indent=2,sort_keys=True)handle.write('\n')os.chmod(tmp_name,0o600)os.replace(tmp_name,path)exceptBaseException:os.unlink(tmp_name)raisedefsanitize(text:str,max_len:int,*,allow_newlines:bool=False)->str:"""Strip control characters, cap length, and trim — for untrusted free text. Drops C0/C1 control characters (so a name, a remark or a message subject cannot smuggle newlines into a notification-mail header or control codes into a terminal listing); tabs become spaces. With *allow_newlines* the record keeps its line breaks (remarks, message bodies), which stay in the mail **body** — or in an autoescaped template — where they are inert. The cap is applied last, so the stored value never exceeds *max_len*. """out:list[str]=[]forchintext:ifch=='\n':out.append(chifallow_newlineselse' ')elifch=='\t':out.append(' ')elifch<' 'orch=='\x7f'or'\x80'<=ch<='\x9f':continueelse:out.append(ch)return''.join(out).strip()[:max_len]