fix(state-db): keep non-UTF-8 POSIX paths readable in the read-only URI
state_db_file_uri() quoted the POSIX path as a str; an undecodable filename byte is carried as a surrogateescape code point, which quote() rejects with UnicodeEncodeError, and get_cli_sessions() swallowed that as an empty session list. Quote os.fsencode() bytes instead (what Path.as_uri() did on master). Regression test: a b'\xff' HERMES_HOME component opens read-only. Maintainer fold-in during release gating (Codex re-gate finding). Co-authored-by: ruizanthony <ruizanthony@users.noreply.github.com>
This commit is contained in:
1 parent
c8b1f87583
commit
8635487d9b
2 files changed
+36
-2
No files matched your search
@@ -1,11 +1,12 @@
|
|||||||
"""Shared helpers for reading Hermes Agent sessions from state.db."""
|
"""Shared helpers for reading Hermes Agent sessions from state.db."""
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import sys
|
import sys
|
||||||
from contextlib import closing
|
from contextlib import closing
|
||||||
from pathlib import Path, PurePosixPath, PureWindowsPath
|
from pathlib import Path, PurePosixPath, PureWindowsPath
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote, quote_from_bytes
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -56,7 +57,13 @@ def state_db_file_uri(db_path, platform: str | None = None) -> str:
|
|||||||
# ``file:///C:/...`` shape (``Path.as_uri()`` leaves it unescaped too).
|
# ``file:///C:/...`` shape (``Path.as_uri()`` leaves it unescaped too).
|
||||||
return "file://" + quote(posix_path, safe="/:")
|
return "file://" + quote(posix_path, safe="/:")
|
||||||
posix_path = PurePosixPath(str(db_path)).as_posix()
|
posix_path = PurePosixPath(str(db_path)).as_posix()
|
||||||
return "file://" + quote(posix_path, safe="/")
|
# Quote the filesystem BYTES, not the str: a POSIX path component that is
|
||||||
|
# not valid UTF-8 is carried in the str as ``surrogateescape`` code points,
|
||||||
|
# which ``quote(str)`` rejects with UnicodeEncodeError (the caller then
|
||||||
|
# treats the db as unreadable and every agent-backed session vanishes).
|
||||||
|
# ``Path.as_uri()`` — what master used — percent-encodes os.fsencode()
|
||||||
|
# bytes; this keeps that behavior.
|
||||||
|
return "file://" + quote_from_bytes(os.fsencode(posix_path), safe="/")
|
||||||
|
|
||||||
|
|
||||||
def state_db_readonly_uri(db_path, platform: str | None = None) -> str:
|
def state_db_readonly_uri(db_path, platform: str | None = None) -> str:
|
||||||
|
|||||||
@@ -207,6 +207,33 @@ def test_empty_authority_uri_opens_on_this_platform(tmp_path):
|
|||||||
conn.execute("INSERT INTO t VALUES (1)")
|
conn.execute("INSERT INTO t VALUES (1)")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(sys.platform == "win32", reason="POSIX filesystem-bytes path")
|
||||||
|
def test_non_utf8_posix_path_still_opens_readonly(tmp_path):
|
||||||
|
"""A non-UTF-8 path component must not make every agent session vanish.
|
||||||
|
|
||||||
|
Python carries undecodable POSIX filename bytes as ``surrogateescape``
|
||||||
|
code points; ``quote(str)`` rejects those with UnicodeEncodeError, which
|
||||||
|
the listing path swallows as "no sessions". ``Path.as_uri()`` (the pre-PR
|
||||||
|
shape) percent-encoded the fsencode() bytes, so the URI builder must too.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
odd_dir = tmp_path / os.fsdecode(b"hermes-\xff-home")
|
||||||
|
odd_dir.mkdir()
|
||||||
|
except (OSError, UnicodeError):
|
||||||
|
pytest.skip("filesystem rejects non-UTF-8 names")
|
||||||
|
path = odd_dir / "state.db"
|
||||||
|
with closing(sqlite3.connect(str(path))) as conn:
|
||||||
|
conn.execute("CREATE TABLE t(x)")
|
||||||
|
conn.execute("INSERT INTO t VALUES (1)")
|
||||||
|
conn.commit()
|
||||||
|
uri = agent_sessions.state_db_readonly_uri(path.resolve())
|
||||||
|
assert "%FF" in uri
|
||||||
|
with closing(agent_sessions.open_state_db_readonly(path)) as conn:
|
||||||
|
assert conn.execute("SELECT COUNT(*) FROM t").fetchone() == (1,)
|
||||||
|
with pytest.raises(sqlite3.OperationalError):
|
||||||
|
conn.execute("INSERT INTO t VALUES (2)")
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
# 3. The index maintenance script imports and runs without fcntl.
|
# 3. The index maintenance script imports and runs without fcntl.
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in new issue
Block a user