Repository navigation
Expand file tree
/
Copy pathinstall-hook.py
More file actions
executable file
·172 lines (147 loc) · 5.73 KB
/
Copy pathinstall-hook.py
File metadata and controls
executable file
·172 lines (147 loc) · 5.73 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
#!/usr/bin/env python3
"""Install the theme-set memory hook without following any path it is given.
Started by Run (omakit/Run.qml) from ImagePicker.qml when the picker loads, so
it writes without a user asking it to. It publishes into Omarchy's hook
directory, which the plugin does not own, so the destination is derived here
rather than trusted: the caller supplies a name, and the directory chain is
walked with checked no-follow descriptors.
Usage: install-hook.py <source> <destination>
"""
import errno
import os
import secrets
import stat
import sys
from safe_paths import (
fail,
directory_identity,
open_chain,
require_home,
revalidate_directory,
write_all,
)
HOOK_DIRECTORY = (
(".config", "configuration directory"),
("omarchy", "Omarchy configuration directory"),
("hooks", "Omarchy hook directory"),
("theme-set.d", "theme-set hook directory"),
)
HOOK_MODE = 0o755
MAX_HOOK_BYTES = 64 * 1024
def validate_arguments(source_path, destination_path):
for label, value in (("source", source_path), ("destination", destination_path)):
if not value or not os.path.isabs(value) or len(value) > 4096:
fail(f"Invalid hook {label} path")
def expected_directory(home):
return os.path.join(home, *(name for name, _ in HOOK_DIRECTORY))
def hook_name(home, destination_path):
"""Accept only a name directly inside Omarchy's theme-set hook directory.
The destination is not resolved. It is compared literally against the one
directory this helper is allowed to publish into, and only its final
component is kept; the chain itself is reopened below.
"""
directory, name = os.path.split(destination_path)
if directory != expected_directory(home):
fail("Hook destination is outside the theme-set hook directory")
if not name or name in {".", ".."} or "/" in name or len(name.encode()) > 255:
fail("Invalid hook filename")
return name
def read_source(source_path):
fd = os.open(source_path, os.O_RDONLY | os.O_NOFOLLOW | os.O_CLOEXEC)
try:
info = os.fstat(fd)
if not stat.S_ISREG(info.st_mode):
fail("Hook source is not a regular file")
if info.st_uid != os.getuid():
fail("Hook source is not owned by the current user")
if info.st_size > MAX_HOOK_BYTES:
fail(f"Hook source is too large ({info.st_size} bytes)")
blocks = []
while True:
block = os.read(fd, 65536)
if not block:
break
blocks.append(block)
return b"".join(blocks)
finally:
os.close(fd)
def already_current(directory_fd, name, payload):
"""True when the destination is a regular file with the wanted bytes and mode.
A symbolic link, a directory or any other kind of entry is never current:
it is replaced, and it is never opened for writing.
"""
try:
fd = os.open(name, os.O_RDONLY | os.O_NOFOLLOW | os.O_CLOEXEC, dir_fd=directory_fd)
except FileNotFoundError:
return False
except OSError as error:
if error.errno in {errno.ELOOP, errno.ENXIO}:
return False
raise
try:
info = os.fstat(fd)
if not stat.S_ISREG(info.st_mode):
return False
if stat.S_IMODE(info.st_mode) != HOOK_MODE:
return False
if info.st_size != len(payload):
return False
return os.read(fd, len(payload) + 1) == payload
finally:
os.close(fd)
def publish(directory_fd, identity, name, payload):
temporary = f".theme-manager-hook-{os.getpid()}-{secrets.token_hex(8)}.tmp"
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW | os.O_CLOEXEC
fd = os.open(temporary, flags, 0o600, dir_fd=directory_fd)
try:
try:
write_all(fd, payload)
os.fchmod(fd, HOOK_MODE)
os.fsync(fd)
finally:
os.close(fd)
# The descriptor cannot have been redirected, but the directory could
# have been made group-writable since the walk; check before publishing.
revalidate_directory(directory_fd, identity, "theme-set hook directory")
# rename replaces the name, so a planted symbolic link at the
# destination is unlinked rather than written through.
os.rename(temporary, name, src_dir_fd=directory_fd, dst_dir_fd=directory_fd)
temporary = ""
os.fsync(directory_fd)
finally:
if temporary:
try:
os.unlink(temporary, dir_fd=directory_fd)
except FileNotFoundError:
pass
def install(source_path, destination_path):
validate_arguments(source_path, destination_path)
home = require_home()
name = hook_name(home, destination_path)
payload = read_source(source_path)
directory_fds = []
try:
directory_fd = open_chain(home, HOOK_DIRECTORY, directory_fds)
identity = directory_identity(directory_fd)
if already_current(directory_fd, name, payload):
return os.path.join(expected_directory(home), name)
publish(directory_fd, identity, name, payload)
finally:
for fd in reversed(directory_fds):
os.close(fd)
return os.path.join(expected_directory(home), name)
def main():
if len(sys.argv) != 3:
print("Usage: install-hook.py <source> <destination>", file=sys.stderr)
return 2
try:
install(sys.argv[1], sys.argv[2])
except (OSError, RuntimeError, ValueError) as error:
if isinstance(error, OSError) and error.errno == errno.ELOOP:
print("Refusing to follow a symbolic link", file=sys.stderr)
else:
print(str(error), file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())