projectaria-debug-sharing 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- projectaria_debug_sharing/__init__.py +22 -0
- projectaria_debug_sharing/debug_sharing/__init__.py +15 -0
- projectaria_debug_sharing/debug_sharing/_internal/__init__.py +13 -0
- projectaria_debug_sharing/debug_sharing/_internal/delivery.py +252 -0
- projectaria_debug_sharing/debug_sharing/_internal/file_access.py +130 -0
- projectaria_debug_sharing/debug_sharing/_internal/manifest.py +133 -0
- projectaria_debug_sharing/debug_sharing/_internal/plain_files.py +208 -0
- projectaria_debug_sharing/debug_sharing/_internal/report_command.py +560 -0
- projectaria_debug_sharing/debug_sharing/_internal/spool.py +673 -0
- projectaria_debug_sharing/debug_sharing/cli.py +183 -0
- projectaria_debug_sharing/debug_sharing/collectors.py +472 -0
- projectaria_debug_sharing/debug_sharing/command.py +36 -0
- projectaria_debug_sharing/debug_sharing/facade.py +435 -0
- projectaria_debug_sharing/debug_sharing/flytrap.py +327 -0
- projectaria_debug_sharing/debug_sharing/submitter.py +103 -0
- projectaria_debug_sharing/debug_sharing/types.py +172 -0
- projectaria_debug_sharing-0.1.0.dist-info/METADATA +30 -0
- projectaria_debug_sharing-0.1.0.dist-info/RECORD +22 -0
- projectaria_debug_sharing-0.1.0.dist-info/WHEEL +5 -0
- projectaria_debug_sharing-0.1.0.dist-info/entry_points.txt +2 -0
- projectaria_debug_sharing-0.1.0.dist-info/licenses/LICENSE +201 -0
- projectaria_debug_sharing-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
Reading plain files without following links, for collectors whose paths a user
|
|
17
|
+
or another process could swap underneath them.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import errno
|
|
23
|
+
import fnmatch
|
|
24
|
+
import logging
|
|
25
|
+
import os
|
|
26
|
+
import time
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from stat import S_ISDIR, S_ISLNK, S_ISREG
|
|
29
|
+
from typing import BinaryIO, Final
|
|
30
|
+
|
|
31
|
+
logger: logging.Logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
ZIP_EARLIEST: Final[tuple[int, ...]] = (1980, 1, 1, 0, 0, 0)
|
|
34
|
+
ZIP_LATEST: Final[tuple[int, ...]] = (2107, 12, 31, 23, 59, 58)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def open_plain_file(path: Path) -> BinaryIO:
|
|
38
|
+
"""
|
|
39
|
+
Opens `path` only if it is a plain file itself, not a link to one, and is
|
|
40
|
+
still the file that was opened.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
The open handle, which the caller closes.
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
IsADirectoryError: `path` is a directory.
|
|
47
|
+
OSError: `path` is a link, FIFO or other special file, or was swapped
|
|
48
|
+
for another file while being opened.
|
|
49
|
+
"""
|
|
50
|
+
handle = os.fdopen(os.open(path, _read_flags()), "rb")
|
|
51
|
+
try:
|
|
52
|
+
opened = os.fstat(handle.fileno())
|
|
53
|
+
if S_ISDIR(opened.st_mode):
|
|
54
|
+
raise IsADirectoryError(errno.EISDIR, "Is a directory", str(path))
|
|
55
|
+
if not _is_same_plain_file(opened, path.lstat()):
|
|
56
|
+
raise OSError(errno.EINVAL, "Refusing anything but a plain file", str(path))
|
|
57
|
+
except BaseException:
|
|
58
|
+
handle.close()
|
|
59
|
+
raise
|
|
60
|
+
return handle
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def open_directory(path: Path) -> int:
|
|
64
|
+
"""
|
|
65
|
+
A handle on the directory itself, so entries are opened relative to it
|
|
66
|
+
and a root swapped afterwards is not followed.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
The directory's file descriptor, which the caller closes.
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
OSError: `path` is a symlink, or the platform has no directory handles
|
|
73
|
+
(Windows), where there is no race-free way to read it.
|
|
74
|
+
"""
|
|
75
|
+
if os.open not in os.supports_dir_fd or not hasattr(os, "O_DIRECTORY"):
|
|
76
|
+
raise OSError(
|
|
77
|
+
errno.ENOTSUP, "No race-free directory handle on this platform", str(path)
|
|
78
|
+
)
|
|
79
|
+
try:
|
|
80
|
+
return os.open(path, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
|
|
81
|
+
except NotADirectoryError:
|
|
82
|
+
if path.is_symlink():
|
|
83
|
+
raise OSError(errno.ELOOP, "Refusing a symlinked directory", str(path))
|
|
84
|
+
raise
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def list_own_plain_files(dir_fd: int, pattern: str = "*") -> list[str]:
|
|
88
|
+
"""
|
|
89
|
+
Lists the plain files directly in the directory that match `pattern` and
|
|
90
|
+
belong to this user. Other users' files in a shared directory are not ours
|
|
91
|
+
to upload. An entry that cannot be examined is skipped with a warning.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
dir_fd: The directory, as from `open_directory`.
|
|
95
|
+
pattern: A glob the file names must match.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
The file names, newest first.
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
OSError: The directory cannot be listed.
|
|
102
|
+
"""
|
|
103
|
+
entries = []
|
|
104
|
+
with os.scandir(dir_fd) as found:
|
|
105
|
+
for entry in found:
|
|
106
|
+
try:
|
|
107
|
+
info = entry.stat(follow_symlinks=False)
|
|
108
|
+
except OSError as e:
|
|
109
|
+
logger.warning("Skipping %s: %s", entry.name, e.strerror or e)
|
|
110
|
+
continue
|
|
111
|
+
if (
|
|
112
|
+
S_ISREG(info.st_mode)
|
|
113
|
+
and _belongs_to_user(info)
|
|
114
|
+
and fnmatch.fnmatchcase(entry.name, pattern)
|
|
115
|
+
):
|
|
116
|
+
entries.append((info.st_mtime, entry.name))
|
|
117
|
+
return [name for _, name in sorted(entries, reverse=True)]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def open_plain_file_at(
|
|
121
|
+
name: str, dir_fd: int
|
|
122
|
+
) -> tuple[BinaryIO, os.stat_result] | None:
|
|
123
|
+
"""
|
|
124
|
+
Opens a directory entry listed by `list_own_plain_files`, checking the
|
|
125
|
+
opened file is still that entry and still this user's.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
name: The entry's name.
|
|
129
|
+
dir_fd: The directory, as from `open_directory`.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
The open handle, which the caller closes, and its stat; or None if
|
|
133
|
+
the entry is no longer this user's plain file.
|
|
134
|
+
|
|
135
|
+
Raises:
|
|
136
|
+
OSError: The entry cannot be opened or examined, as when it is a link
|
|
137
|
+
or has been removed.
|
|
138
|
+
"""
|
|
139
|
+
handle = os.fdopen(os.open(name, _read_flags(), dir_fd=dir_fd), "rb")
|
|
140
|
+
try:
|
|
141
|
+
opened = os.fstat(handle.fileno())
|
|
142
|
+
current = os.stat(name, dir_fd=dir_fd, follow_symlinks=False)
|
|
143
|
+
except BaseException:
|
|
144
|
+
handle.close()
|
|
145
|
+
raise
|
|
146
|
+
if not (_is_same_plain_file(opened, current) and _belongs_to_user(opened)):
|
|
147
|
+
handle.close()
|
|
148
|
+
return None
|
|
149
|
+
return handle, opened
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def zip_date_time(modified_time_sec: float) -> tuple[int, ...]:
|
|
153
|
+
"""
|
|
154
|
+
Args:
|
|
155
|
+
modified_time_sec: A modification time, in seconds since the epoch.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
The local time, clamped to what a ZIP entry can store.
|
|
159
|
+
"""
|
|
160
|
+
try:
|
|
161
|
+
local = tuple(time.localtime(modified_time_sec)[:6])
|
|
162
|
+
except (OverflowError, OSError, ValueError):
|
|
163
|
+
return ZIP_EARLIEST if modified_time_sec < 0 else ZIP_LATEST
|
|
164
|
+
return min(max(local, ZIP_EARLIEST), ZIP_LATEST)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def describe_entry(path: Path) -> dict[str, object]:
|
|
168
|
+
"""
|
|
169
|
+
Describes a directory entry without following it if it is a link.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
path: The entry.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Its name and kind, and its size if it is a plain file.
|
|
176
|
+
|
|
177
|
+
Raises:
|
|
178
|
+
OSError: The entry cannot be examined.
|
|
179
|
+
"""
|
|
180
|
+
info = path.lstat()
|
|
181
|
+
entry: dict[str, object] = {"name": path.name, "is_dir": S_ISDIR(info.st_mode)}
|
|
182
|
+
if S_ISLNK(info.st_mode):
|
|
183
|
+
entry["is_symlink"] = True
|
|
184
|
+
elif S_ISREG(info.st_mode):
|
|
185
|
+
entry["size_bytes"] = info.st_size
|
|
186
|
+
return entry
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _read_flags() -> int:
|
|
190
|
+
"""O_NONBLOCK, so opening a FIFO planted at the path does not hang."""
|
|
191
|
+
return (
|
|
192
|
+
os.O_RDONLY
|
|
193
|
+
| getattr(os, "O_NOFOLLOW", 0)
|
|
194
|
+
| getattr(os, "O_NONBLOCK", 0)
|
|
195
|
+
| getattr(os, "O_BINARY", 0)
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _is_same_plain_file(opened: os.stat_result, current: os.stat_result) -> bool:
|
|
200
|
+
return S_ISREG(opened.st_mode) and (opened.st_dev, opened.st_ino) == (
|
|
201
|
+
current.st_dev,
|
|
202
|
+
current.st_ino,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _belongs_to_user(info: os.stat_result) -> bool:
|
|
207
|
+
"""Always True on Windows, which has no POSIX owner to compare."""
|
|
208
|
+
return not hasattr(os, "getuid") or info.st_uid == os.getuid()
|