tftp 0.0.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.
Files changed (70) hide show
  1. tftp/AGENTS.md +761 -0
  2. tftp/README.md +218 -0
  3. tftp/__init__.py +180 -0
  4. tftp/__main__.py +6 -0
  5. tftp/_sockets.py +36 -0
  6. tftp/aio/__init__.py +22 -0
  7. tftp/aio/bridge.py +214 -0
  8. tftp/aio/client.py +426 -0
  9. tftp/aio/server.py +314 -0
  10. tftp/backends/__init__.py +19 -0
  11. tftp/backends/http.py +142 -0
  12. tftp/backends/memory.py +93 -0
  13. tftp/backends/pipe.py +192 -0
  14. tftp/backends/proxy.py +162 -0
  15. tftp/capture/__init__.py +74 -0
  16. tftp/capture/events.py +161 -0
  17. tftp/capture/filters.py +149 -0
  18. tftp/capture/flows.py +290 -0
  19. tftp/capture/frames.py +189 -0
  20. tftp/capture/live.py +54 -0
  21. tftp/capture/pcap.py +225 -0
  22. tftp/cli/__init__.py +56 -0
  23. tftp/cli/capture.py +130 -0
  24. tftp/cli/common.py +240 -0
  25. tftp/cli/handlers.py +147 -0
  26. tftp/cli/relay.py +117 -0
  27. tftp/cli/serve.py +237 -0
  28. tftp/cli/transfer.py +174 -0
  29. tftp/client.py +769 -0
  30. tftp/errors.py +205 -0
  31. tftp/listing.py +123 -0
  32. tftp/netascii.py +149 -0
  33. tftp/options/__init__.py +89 -0
  34. tftp/options/base.py +175 -0
  35. tftp/options/builtin.py +318 -0
  36. tftp/options/negotiate.py +134 -0
  37. tftp/options/policy.py +96 -0
  38. tftp/options/profiles.py +74 -0
  39. tftp/options/registry.py +64 -0
  40. tftp/packet/__init__.py +40 -0
  41. tftp/packet/codec.py +194 -0
  42. tftp/packet/enums.py +32 -0
  43. tftp/path/__init__.py +34 -0
  44. tftp/path/_stream.py +195 -0
  45. tftp/path/local.py +221 -0
  46. tftp/path/uri.py +94 -0
  47. tftp/py.typed +0 -0
  48. tftp/relay/__init__.py +24 -0
  49. tftp/relay/core.py +429 -0
  50. tftp/relay/routing.py +148 -0
  51. tftp/relay/session.py +165 -0
  52. tftp/result.py +88 -0
  53. tftp/server/__init__.py +20 -0
  54. tftp/server/base.py +321 -0
  55. tftp/server/core.py +430 -0
  56. tftp/server/handler.py +290 -0
  57. tftp/server/listener.py +198 -0
  58. tftp/server/policy.py +63 -0
  59. tftp/server/session.py +320 -0
  60. tftp/server/stats.py +63 -0
  61. tftp/transfer/__init__.py +31 -0
  62. tftp/transfer/base.py +241 -0
  63. tftp/transfer/receiver.py +212 -0
  64. tftp/transfer/sender.py +175 -0
  65. tftp/uri.py +95 -0
  66. tftp-0.0.0.dist-info/METADATA +260 -0
  67. tftp-0.0.0.dist-info/RECORD +70 -0
  68. tftp-0.0.0.dist-info/WHEEL +4 -0
  69. tftp-0.0.0.dist-info/entry_points.txt +5 -0
  70. tftp-0.0.0.dist-info/licenses/LICENSE +21 -0
tftp/AGENTS.md ADDED
@@ -0,0 +1,761 @@
1
+ # `tftp` — public API header
2
+
3
+ Header-file-style reference for the `tftp` package: every public export with
4
+ its signature, contract and gotchas, so the package can be used without
5
+ reading its source. It ships inside the package and is self-contained; read it
6
+ (or the README beside it) with `importlib.resources.files("tftp")`.
7
+ Development documentation lives with the source at
8
+ <https://github.com/jose-pr/pytftp>.
9
+
10
+ Everything below is importable from `tftp` directly. The subpackages only
11
+ group it: `tftp.packet` (wire format), `tftp.options` (negotiation),
12
+ `tftp.transfer` (the I/O-free engine), `tftp.client`, `tftp.server`,
13
+ `tftp.netascii`, `tftp.errors`, `tftp.result`, and `tftp.cli` (needs the `cli`
14
+ extra). Modules starting with `_` are internal.
15
+
16
+ `tftp.__version__` — the installed distribution's version.
17
+
18
+ ## Protocol coverage
19
+
20
+ | Spec | What | Notes |
21
+ | --- | --- | --- |
22
+ | RFC 1350 | RRQ, WRQ, DATA, ACK, ERROR; octet and netascii | `mail` mode is refused with ERROR 4 |
23
+ | RFC 1123 4.2.3.1 | Sorcerer's Apprentice fix | duplicate ACKs never trigger a resend (windowsize 1) |
24
+ | RFC 1123 4.2.3.2 | exponential backoff | each consecutive retransmission waits `backoff` times longer, capped |
25
+ | RFC 1123 4.2.3.4 | broadcast requests ignored | server, when pktinfo reports the destination |
26
+ | RFC 2347 | option extension, OACK, ERROR 8 | unknown options are ignored, as the RFC requires |
27
+ | RFC 2348 | `blksize` 8..65464 | server clamps to its `max_blksize` |
28
+ | RFC 2349 | `timeout` (1..255 s), `tsize` | `tsize` 0 is never sent in an OACK (curl rejects it) |
29
+ | RFC 7440 | `windowsize` 1..65535 | server clamps to its `max_windowsize` (default 64) |
30
+ | tftp-hpa | `blksize2`, `utimeout`, `rollover`, `cookie` | **off unless a server allows them** (`ServerOptions(allowed=...)`, a profile) |
31
+ | Microsoft | `mstfwindow` (bootmgr/WDS variable window) | off unless allowed; runs a fixed window of 4 (the in-transfer resize is unpublished) |
32
+ | pytftp | `x-list` (directory listing), `x-mtime` (modification time) | off unless allowed (`LISTING_OPTIONS`); other servers ignore them |
33
+ | — | block-number rollover | blocks wrap after 65535 (to 0, or 1 with `rollover`); file size is unlimited |
34
+ | RFC 1350 §6 | dallying | server re-ACKs a repeated last DATA for one timeout |
35
+
36
+ Not implemented: RFC 2090 multicast, PXE MTFTP.
37
+
38
+ ## Client
39
+
40
+ **`Client(host, port=69, *, timeout=1.0, retries=5, blksize=1428, windowsize=None, tsize=True, rollover=None, timeout_option=True, family=0, local_address=None, fallback=True, dally=False, backoff=2.0, max_timeout=None, max_duration=None, strict_source=True, utimeout=False, extra_options=None, registry=None, on_negotiated=None)`**
41
+
42
+ - `host` — a string (name or address; `"[v6]"`, `"host:port"` and
43
+ `"[v6]:port"` are accepted, and a port written there overrides `port`), an
44
+ `ipaddress` address or interface (its address), or a `netimps.Host`. Kept
45
+ as given in `client.host`.
46
+ - `timeout` — seconds before a retransmission. Requested from the server as
47
+ `timeout` when whole (1..255); a fractional one is requested as
48
+ `utimeout` only with `utimeout=True`, otherwise not at all. Nothing is
49
+ requested with `timeout_option=False`.
50
+ - `retries` — retransmissions of one packet before `TransferTimeout`.
51
+ - `backoff`, `max_timeout` — each consecutive retransmission (of the
52
+ request too) waits `backoff` times longer, up to `max_timeout` (default
53
+ 8 × `timeout`); progress resets the wait. `backoff=1` disables it.
54
+ - `max_duration` — seconds a whole transfer may take; `None` is unlimited.
55
+ - `strict_source` — the first answer must come from the address the request
56
+ was sent to. `False` accepts a multi-homed server answering from another
57
+ address (the transfer then locks on to that address and port).
58
+ - `blksize` — requested; `None` asks for nothing (512). The default 1428 fits
59
+ one Ethernet frame on IPv4 and IPv6. `"mtu"` sizes it to the MTU of the
60
+ interface the route to the server uses (1428 when unknown).
61
+ - `on_negotiated(negotiated, peer)` — called once the server has answered
62
+ the request (OACK, first DATA or ACK 0) and **before any data moves**;
63
+ `peer` is the server's transfer address. If it raises, the server is sent
64
+ an ERROR (the `TftpError`'s code, else 0) and the exception propagates.
65
+ - `extra_options` — further options to request verbatim (`{"blksize2": 4096}`,
66
+ `{"cookie": "x"}`, custom ones). A known option's answer is validated by
67
+ its handler (`registry`, default `DEFAULT_REGISTRY`); an unknown one's
68
+ answer lands in `result.negotiated.extra`. Naming a built-in twice (here
69
+ and as a keyword) raises `ValueError`.
70
+ - `windowsize` — requested RFC 7440 window; `None` asks for nothing (1).
71
+ - `tsize` — ask for the size (RRQ) or announce it (WRQ, when the source size
72
+ can be determined).
73
+ - `rollover` — request rollover to 0 or 1; `None` asks for nothing, wraps to 0,
74
+ and follows a server that wraps to 1.
75
+ - `family` — `socket.AF_INET`/`AF_INET6` to force one; `0` is whatever the host
76
+ resolves to first.
77
+ - `local_address` — `(address, port)` to send from; the address in any form
78
+ `host` takes.
79
+ - `fallback` — if the server answers the *request* with ERROR 8 (options
80
+ refused), ask again once without options.
81
+ - `dally` — after the last ACK of a download, keep re-ACKing a repeated last
82
+ DATA for one timeout. Costs that much time per download.
83
+ - Options are validated at construction: an out-of-range `blksize`,
84
+ `windowsize` or `rollover` raises `ValueError` there.
85
+ - **Not thread-safe**: one `Client` per thread. Each transfer opens its own
86
+ socket, so sequential transfers on one client are fine.
87
+
88
+ Methods (each returns a `TransferResult` unless noted):
89
+
90
+ - **`download(filename, dest, *, mode="octet", progress=None)`** — `dest` is a
91
+ path or a writable binary file. A path is created/truncated and **removed
92
+ again if the transfer fails**.
93
+ - **`get(filename, *, mode="octet") -> bytes`** — download into memory.
94
+ - **`upload(filename, source, *, mode="octet", progress=None)`** — `source` is
95
+ a path, a readable binary file (`readinto` or `read`), or bytes-like.
96
+ - **`put(filename, data, *, mode="octet")`** — upload bytes.
97
+ - **`size(filename, *, mode="octet") -> int | None`** — the file's size
98
+ without transferring it: an RRQ asking only for `tsize`, abandoned with
99
+ ERROR 8 once the server answers (a server counts that as `declined`, not
100
+ failed). `None` when the server reports no sizes, unless the file fits in
101
+ the first 512-byte block. Raises like a download (`FileNotFound`...).
102
+ - **`stat(filename, *, mode="octet") -> RemoteStat(size, mtime, is_dir)`** —
103
+ one probe like `size()`, also asking for `x-mtime` and `x-list`: a server
104
+ allowing them reports the modification time (seconds since the epoch) and
105
+ recognises a directory (`size` is then `None`); others leave `mtime`
106
+ `None` and report a directory as `FileNotFound`. A request refused with
107
+ ERROR 8 is retried as a plain size probe (with `fallback`). `filename`
108
+ `""` is the root.
109
+ - **`listdir(dirname="") -> list[ListEntry(name, is_dir, size, mtime)]`** —
110
+ a directory listing from a server speaking `x-list`. `NotADirectoryError`
111
+ when the name is a file (the transfer is abandoned at once);
112
+ `FileNotFound` when it does not exist — which is also what a server
113
+ without listing support answers for a directory.
114
+ - **`path(*segments, mode="octet") -> TftpPath`** — see Paths (needs the
115
+ `path` extra).
116
+ - `mode` is `"octet"` or `"netascii"`; `"binary"`/`"ascii"` are aliases. Any
117
+ other value raises `ValueError`.
118
+ - `progress(done_bytes, total_or_None)` is called after each packet that moved
119
+ data; `total` is the negotiated `tsize`.
120
+ - Raises `RemoteError` (the server sent ERROR), `TransferTimeout`,
121
+ (as the subclass for its code: `FileNotFound`, `AccessViolation`, ...),
122
+ `ProtocolError` (the server broke the protocol, e.g. an OACK with a larger
123
+ `blksize` than requested — the client sends ERROR 8 first), or `OSError` for
124
+ local failures (resolution, the local file).
125
+
126
+ **`download(host, filename, dest, *, port=69, mode="octet", progress=None, **client_options)`**
127
+ and **`upload(host, filename, source, ...)`** — one-shot wrappers;
128
+ `client_options` go to `Client`.
129
+
130
+ **`MODES`** — `("octet", "netascii")`.
131
+
132
+ ## Server
133
+
134
+ **`Server(root_or_handler, host=None, port=69, *, writable=False, create=True, overwrite=False, timeout=1.0, retries=5, options=None, max_sessions=None, reply_from_request_address=True, dally=True, on_complete=None, limits=None, ignore_broadcast=True, backoff=2.0, max_timeout=None, open_in_thread=None, workers=8, port_range=None, interface=None)`**
135
+
136
+ - `root_or_handler` — a directory (wrapped in `FileSystemHandler` with
137
+ `writable`, `create`, `overwrite`) or any handler object (see below).
138
+ - `host` — `None` (default) or `"::"` listens on IPv6 **and IPv4** with a dual-stack
139
+ socket, falling back to `"0.0.0.0"` on a host without IPv6. `"0.0.0.0"` is
140
+ IPv4 only; a specific address (`"192.0.2.10"`, `"[fe80::1%eth0]"`) listens
141
+ there alone. An `ipaddress` address or `netimps.Host` works too.
142
+ - `port` — `0` picks a free port; read it from `server_address`.
143
+ - `timeout`, `retries` — per transfer, unless the client negotiates `timeout`.
144
+ - `options` — a `ServerOptions` policy.
145
+ - `max_sessions` — concurrent transfers; beyond it a request gets ERROR 0
146
+ `"server busy"`. `None` is unlimited, **except on Windows, where it defaults
147
+ to 500** because `select()` handles at most 512 sockets.
148
+ - `reply_from_request_address` — each transfer's socket is bound to the
149
+ address the request was sent to (pktinfo), so a multi-homed host or a VIP
150
+ answers from the address the client used. Where the platform cannot report
151
+ it, replies come from the listening address, or from the routing table's
152
+ choice when listening on a wildcard. Check `supports_pktinfo`.
153
+ - `dally` — keep a finished upload's socket for one timeout to re-ACK a
154
+ repeated last DATA.
155
+ - `on_complete(result)` — called with a `TransferResult` after **every**
156
+ transfer, refused and failed ones included (`result.error` set). Exceptions
157
+ it raises are logged and swallowed.
158
+ - `limits` — a `ServerLimits` (below).
159
+ - `ignore_broadcast` — silently drop requests addressed to a broadcast
160
+ (limited or subnet) or multicast address. Needs pktinfo to see the
161
+ destination; without it every request looks unicast.
162
+ - `backoff`, `max_timeout` — as for `Client`, per transfer.
163
+ - `open_in_thread`, `workers` — call the handler's `open_read`/`open_write`
164
+ in a pool of `workers` threads so a handler that blocks (HTTP, an upstream
165
+ server) never stalls other transfers. `None` decides per handler: one
166
+ with `_tftp_fast_open_ = True` (`FileSystemHandler`, `MemoryHandler`)
167
+ opens inline on the loop, anything else in a worker. A retransmitted
168
+ request is still recognised while its open is pending.
169
+ - `interface` — listen on one network adapter: a name (`"eth0"`), a
170
+ `netimps.Interface`, its MAC, or one of its addresses. The listener binds
171
+ **one address** of it: the one named, else the adapter's primary IPv4
172
+ address (non-loopback preferred), else its IPv6 one; `host="0.0.0.0"` or
173
+ `"::"` picks the family. Another `host`, or an adapter netimps cannot
174
+ resolve, is a `ValueError`. Requests to the adapter's other addresses are
175
+ not received; run one server per family or address as needed.
176
+ - `port_range` — `(low, high)` inclusive, a `range`, or a `PortRange`:
177
+ transfer sockets take their ports from it (round-robin, so a just-released
178
+ port is reused last), for a firewall to allow. With every port taken a
179
+ request gets ERROR 0 `"server busy"` (counted `refused`). `None` lets the
180
+ OS choose.
181
+ - Raises `OSError` if the port cannot be bound (port 69 needs privileges on
182
+ POSIX).
183
+
184
+ Lifecycle:
185
+
186
+ - **`serve_forever()`** — run the event loop in the calling thread until
187
+ `shutdown()`. One thread serves every transfer.
188
+ - **`shutdown()`** — stop `serve_forever`; safe from any thread, a handler or
189
+ `on_complete`. Transfers in flight get ERROR 0 `"server shutting down"`
190
+ (their result's `error` is `TransferAborted`).
191
+ - **`start() -> Server`** — run `serve_forever` in a daemon thread.
192
+ - **`stop(timeout=5.0)`** — `shutdown()` and join the `start()` thread.
193
+ - **`close()`** — stop and release every socket. Also the context-manager
194
+ exit. A closed server cannot serve again.
195
+
196
+ Statistics: **`stats`** (a `Stats`: `stats["completed"]`, `snapshot()`)
197
+ counts `requests`, `refused` (refused before a transfer, including limits
198
+ and `server busy`), `started`, `completed`, `failed`, `bytes_sent`,
199
+ `bytes_received`, `retransmits`; **`stats_snapshot()`** returns them plus
200
+ `active`. Thread-safe; meant for a metrics exporter. A `Relay` has the same,
201
+ with `bytes_to_clients`/`bytes_from_clients`.
202
+
203
+ Properties: `server_address` (the bound `(host, port, ...)`, valid after
204
+ close), `supports_pktinfo`, `dual_stack`, `active_sessions`.
205
+
206
+ Behaviour worth knowing:
207
+
208
+ - Every transfer runs on its **own UDP socket** (its transfer ID). A packet
209
+ from any other address/port gets ERROR 5 and does not disturb the transfer.
210
+ - A repeated request from the same client address and port while its transfer
211
+ is running is ignored (it is a retransmission).
212
+ - Datagrams on the listening port that are not RRQ/WRQ are **dropped without
213
+ reply** (no reflection). A malformed RRQ/WRQ gets ERROR 4.
214
+ - A v4 client of a dual-stack listener is served from a plain IPv4 socket, so
215
+ `result.peer` shows `"192.0.2.7"`, never `"::ffff:192.0.2.7"`.
216
+ - Socket buffers are grown to hold two windows (Windows defaults to 64 KiB,
217
+ which a large window overflows).
218
+
219
+ **`PortRange(low, high)`** — `PortRange.of(value)` takes `None`, a pair, a
220
+ `range` or a `PortRange`; `len()`, iteration (every port once, from the
221
+ round-robin cursor). `ValueError` outside 1..65535 or with `low > high`.
222
+
223
+ **`ServerLimits(max_request_size=1024, max_filename_length=512, max_options=16, max_option_length=255, max_sessions_per_client=None, max_duration=None)`**
224
+ — bounds on untrusted input. A request over a size/name/option limit gets
225
+ ERROR 4 from the listening port; a client over `max_sessions_per_client`
226
+ (counted per address, any port) gets ERROR 0 `"server busy"`.
227
+ `max_duration` ends a transfer that runs longer (ERROR 0 to the peer,
228
+ `TransferTimeout` in the result).
229
+
230
+ **`ServerOptions(max_blksize=65464, max_windowsize=64, max_window_bytes=4 MiB, allowed=None, refused=(), fit_mtu=False, registry=None)`**
231
+ — the negotiation policy.
232
+
233
+ - A larger `blksize`/`windowsize` request is answered with the maximum
234
+ (which the RFCs allow). `windowsize` is also lowered so one window of the
235
+ negotiated block size fits `max_window_bytes` — the memory a sender holds.
236
+ - `allowed` — option names ever acknowledged; `None` is `STANDARD_OPTIONS`
237
+ (`blksize`, `timeout`, `tsize`, `windowsize`). Add `EXTENSION_OPTIONS`
238
+ names to accept extensions: tftp-hpa's `blksize2`, `utimeout`,
239
+ `rollover`, `cookie`; Microsoft's `mstfwindow`; pytftp's `x-list`,
240
+ `x-mtime` (`LISTING_OPTIONS`). A name not in `registry` raises
241
+ `ValueError`.
242
+ - `refused` — never acknowledged even if allowed: for firmware that asks for
243
+ an option and then mishandles it (`refused={"windowsize"}`).
244
+ - `fit_mtu` — lower `blksize` so a DATA packet fits the MTU of the interface
245
+ the request arrived on (IP + UDP + 4-byte header): 1468 on IPv4 and 1448
246
+ on IPv6 for a 1500 MTU (`netimps.max_udp_payload` − 4). Needs pktinfo for
247
+ the interface; boot ROMs often cannot reassemble fragments.
248
+ - `accepts(name)` — `name` in `allowed` and not in `refused`.
249
+
250
+ ## Options, registry and profiles
251
+
252
+ Every option is an **`OptionHandler`** (`name`, `standard`) with
253
+ `negotiate(value, ctx) -> str | None` (server: the value to acknowledge, or
254
+ `None` to leave it out — RFC 2347's refusal; set fields on `ctx.result`) and
255
+ `accept(requested, acked, ctx)` (client: validate and apply; raise
256
+ `tftp.options.refuse(msg)` for ERROR 8). `ServerContext` carries `result`,
257
+ `requested`, `acked`, `policy`, `is_read`, `size`, `mtu`, `ipv6`, `stream`
258
+ (what the handler opened for an RRQ, else `None`) and `max_blksize` (policy
259
+ limit after `fit_mtu`); `ClientContext` carries
260
+ `result`, `requested`, `is_read`. Custom values go in `ctx.result.extra`.
261
+
262
+ **`OptionRegistry(handlers=BUILTIN_OPTIONS)`** — `register(handler,
263
+ replace=False)` (a duplicate name raises), `unregister(name)`, `get(name)`,
264
+ `in`, iteration, `names()`, `standard()`, `copy()`. **Registration order is
265
+ negotiation order** (`windowsize` after the block size it depends on).
266
+ `DEFAULT_REGISTRY` is what servers and clients use unless given another;
267
+ `register_option(handler)` adds to it. A custom option must also be in a
268
+ server's `allowed`.
269
+
270
+ Built-ins: `blksize`, `timeout`, `tsize`, `windowsize` (standard), and
271
+ `blksize2` (largest power of two ≤ the request and the limit; ignored when
272
+ `blksize` was acknowledged), `utimeout` (10 000..255 000 000 µs),
273
+ `rollover` (0/1), `cookie` (echoed unchanged; a client refuses a changed
274
+ one), `mstfwindow` (`Mstfwindow`: answers `31416` with `27182` and runs a
275
+ window of 4 unless `windowsize` was also acknowledged; a client refuses any
276
+ other answer), `x-list` (`XList`: acknowledged `1` only when the RRQ's stream
277
+ is a listing, `_tftp_listing_`), `x-mtime` (`XMtime`: an RRQ's OACK carries
278
+ the stream's `mtime` attribute or `fstat` time, whole seconds; omitted when
279
+ unknown). `stream_mtime(stream)` is that lookup.
280
+
281
+ **Profiles** — `Profile(name, server, client)`: `.server` is a
282
+ `ServerOptions`, `.client` a fresh dict of `Client` keyword arguments.
283
+ `PROFILES` maps names to the five presets:
284
+
285
+ | profile | server | client |
286
+ | --- | --- | --- |
287
+ | `STRICT` | standard options | `fallback=False` |
288
+ | `DEFAULT` | standard options | library defaults |
289
+ | `PXE` | standard + `rollover`, `utimeout`; `fit_mtu=True` | `blksize=1428` |
290
+ | `HPA` | tftp-hpa's extensions | `utimeout=True` |
291
+ | `LEGACY` | standard, `windowsize` refused | no options at all, `strict_source=False` |
292
+
293
+ ```python
294
+ tftp.Server("/srv/tftp", options=tftp.PXE.server)
295
+ tftp.Client("192.0.2.1", **tftp.LEGACY.client)
296
+ ```
297
+
298
+ ## Handlers
299
+
300
+ A handler is any object with:
301
+
302
+ - **`open_read(context) -> reader`** — a binary reader with `readinto` or
303
+ `read`, and `close`. `tsize` is answered when the reader has an integer
304
+ `size` attribute, a real `fileno()`, or is seekable.
305
+ - **`open_write(context, size) -> writer`** — a binary writer with `write` and
306
+ `close`. `size` is the client's announced `tsize` or `None`. **`close` is
307
+ called after the last block is written and before it is acknowledged**, so
308
+ an exception there reaches the client as ERROR. If the writer has `abort()`,
309
+ a failed transfer calls it instead of `close()`.
310
+
311
+ **Slow or asynchronous streams.** A reader's `readinto`/`read`, a writer's
312
+ `write` and its `close` may raise `tftp.WouldBlock` when nothing is ready:
313
+ the transfer pauses (a writer's block stays unacknowledged, which is
314
+ backpressure on the client) without blocking the loop. A stream that has
315
+ `set_wakeup(callback)` is given a thread-safe callback to call when it can
316
+ make progress again; without it a stalled transfer only resumes when the
317
+ peer retransmits. A paused transfer is subject to `max_duration`, not to
318
+ peer retries.
319
+
320
+ Either may raise `TftpError(code, message)` to refuse with that ERROR, or
321
+ `OSError`, mapped by errno (`ENOENT` → 1, `EACCES`/`EPERM` → 2, `ENOSPC` → 3,
322
+ `EEXIST` → 6). The OS message is **not** sent (it could disclose paths). Any
323
+ other exception is logged and sent as ERROR 0. Handlers run on the event-loop
324
+ thread: **a slow handler stalls every transfer**.
325
+
326
+ `write` receives a `memoryview` of a reused buffer when the writer is a
327
+ standard file object (`io.IOBase`) or declares `_tftp_copies_ = True`, and
328
+ `bytes` otherwise — so a writer that keeps references is safe by default.
329
+
330
+ **`RequestContext`** — `request` (the `Request`), `peer` (client address
331
+ tuple), `local_address` (the destination address as text, or `None` without
332
+ pktinfo), `interface_index` (or 0), `interface` (that interface as a
333
+ `netimps.Interface` — name, addresses, MTU — or `None`), `listing` (the RRQ asks for `x-list=1`
334
+ and the server allows it); shortcuts `filename`, `mode`, `options`.
335
+
336
+ **`FileSystemHandler(root, *, writable=False, create=True, overwrite=False, backslash=True, max_upload=None)`**
337
+
338
+ - Serves files under `root` (which must exist). Leading `/` is stripped, so
339
+ `/boot/x` is `root/boot/x`. With `backslash`, `\` is a separator too (Windows
340
+ boot loaders send `\boot\bcd`).
341
+ - **Containment**: `..` components are refused (ERROR 2), and the resolved
342
+ path, symlinks included, must stay inside `root` (ERROR 2). On Windows,
343
+ drive letters, `:` streams and reserved device names (`CON`, `NUL`, ...) are
344
+ refused too.
345
+ - Reads: a missing file is ERROR 1, and so is a directory — unless
346
+ `context.listing`, when a directory (`""`, `.` or `/` for the root) is
347
+ answered with a `DirectoryListing`.
348
+ - Writes: refused unless `writable` (ERROR 2); an existing file needs
349
+ `overwrite` (else ERROR 6); a new file needs `create` (else ERROR 1); the
350
+ directory must exist (directories are never created). An announced `tsize`
351
+ above `max_upload` or the free disk space is ERROR 3, and so is an upload
352
+ that grows past `max_upload`.
353
+ - Uploads go through `AtomicWriter`: **a failed or partial upload never
354
+ appears or replaces anything**.
355
+ - `resolve(filename) -> str` exposes the mapping (raises `TftpError`).
356
+
357
+ **`tftp.listing`** — the `x-list` format: UTF-8 lines `<f|d> <size> <mtime|-> <name>`
358
+ (`%`, CR, LF in names as `%25`, `%0D`, `%0A`). **`ListEntry(name, is_dir,
359
+ size, mtime=None)`**, **`format_listing(entries) -> bytes`**,
360
+ **`parse_listing(data) -> list`** (malformed lines skipped),
361
+ **`DirectoryListing(directory, root=None)`** (a `BytesIO` with `size`,
362
+ `mtime`, `_tftp_listing_`; sorted by name; leaves out symlinks resolving
363
+ outside `root` and in-progress uploads `.name.*.part`), `LIST_OPTION`,
364
+ `MTIME_OPTION`.
365
+
366
+ **`AtomicWriter(path, overwrite=True)`** — writes to a hidden temp file beside
367
+ `path`; `close()` renames it into place (refusing with ERROR 6 if
368
+ `overwrite=False` and `path` appeared meanwhile); `abort()` deletes it.
369
+
370
+ ## asyncio (`tftp.aio`)
371
+
372
+ **`AsyncClient(...)`** — `Client`'s arguments and rules (options, backoff,
373
+ fallback, `trace`, `on_negotiated`); coroutine methods `download(filename,
374
+ dest, *, mode, progress)`, `get`, `upload(filename, source, *, mode,
375
+ progress)`, `put`, `size`, `stat` (both in the executor), `listdir`, and the async generator **`stream(filename, *, mode,
376
+ buffer=1 MiB)`** yielding chunks as they arrive (a slow consumer holds ACKs
377
+ back; at most `buffer` bytes are held). Name resolution runs in the
378
+ executor. Destinations: a path, a binary file (written on the loop — fine
379
+ for local files), or an **async writer** (`async write(data)`, or `write` +
380
+ `async drain()` like `asyncio.StreamWriter`); `download` returns only once
381
+ an async writer has taken every byte (the writer is not closed). Sources: a
382
+ path, bytes, a binary file, or an **async reader** (`async read(n)`) or async
383
+ iterable of bytes. Cancelling the task sends the server ERROR 0. Each
384
+ attempt (including the option fallback) uses a fresh socket.
385
+
386
+ **`AsyncServer(root_or_handler, host=None, port=69, *, executor=None, **server_options)`**
387
+ — `Server`'s arguments except `open_in_thread`/`workers`. `async with
388
+ AsyncServer(...) as server: await server.serve_forever()`, or `await
389
+ server.start()` … `await server.stop()`; `shutdown()` is thread-safe; `await
390
+ close()`. Handlers:
391
+
392
+ - `open_read`/`open_write` may be `async def` (awaited on the loop), or
393
+ synchronous: those marked `_tftp_fast_open_` (file, memory) run inline,
394
+ others in `executor` (default: the loop's) — so `HttpHandler` and
395
+ `UpstreamHandler` work unchanged (their `Pipe` wake-ups are thread-safe).
396
+ - The returned stream may be an async reader/iterable (RRQ) or async writer
397
+ (WRQ); a writer is closed after the last block, and the final ACK waits
398
+ until everything is written.
399
+ - pktinfo is kept on every loop: the listener is read with netimps'
400
+ `UdpEndpoint.arecv` (`add_reader`, or a readiness thread where the loop
401
+ has none — Windows' Proactor loop).
402
+ - On Windows the default `max_sessions` is 500 (a selector loop's
403
+ `select()`).
404
+
405
+ **`AsyncReaderBridge(source, capacity=1 MiB, size=None)`** /
406
+ **`AsyncWriterBridge(sink, capacity=1 MiB, close_sink=True)`** — the
407
+ adapters doing that (engine-side `readinto`/`write`/`close` raising
408
+ `WouldBlock`, `set_wakeup`; writer `await finish()`); create them on the
409
+ running loop. `is_async_reader(obj)` / `is_async_writer(obj)` say which
410
+ objects they take.
411
+
412
+ ## Paths (`tftp.path`, `path` extra)
413
+
414
+ `pip install tftp[path]` adds `pathlib-next[uri]`. Importing `tftp` never
415
+ loads it.
416
+
417
+ **`TftpPath(*segments, client, mode="octet")`** — a `pathlib_next.Path`
418
+ bound to a `Client` (`client.path("boot", "x")`). Joined like a
419
+ `PurePosixPath` (`\` counts as `/`); the path text is the filename sent,
420
+ so `/boot/x` and `boot/x` stay distinct. Joining onto a `TftpPath` keeps its
421
+ client; `with_client()`, `with_mode()`; `client`, `transfer_mode`. Equality
422
+ and hashing include the server (host, port). `as_uri()` is the `tftp://`
423
+ URL. `relative_to()` works on the path text.
424
+
425
+ **`TftpUriPath`** — the `tftp://host[:port]/path[;mode=netascii]` scheme
426
+ for `pathlib_next.uri.UriPath`, registered through the
427
+ `pathlib_next.schemes` entry point, so `UriPath("tftp://...")` returns one
428
+ without importing `tftp`. `filename`, `transfer_mode`;
429
+ `with_options(**client_options)` / `with_client(client)` choose the
430
+ `Client` (default: the URI's host and port, library defaults). The URI host
431
+ may be an address object; `Client` accepts one.
432
+
433
+ Both support what TFTP can do, plus listing against a server speaking
434
+ `x-list`:
435
+
436
+ - `open("r")` streams a download; `open("w")` streams an upload that
437
+ completes (and raises the server's error) on `close()`; `open("x")`
438
+ first probes the size and raises `FileExistsError` if the file exists
439
+ (not atomic). `a` and `+` modes raise `NotImplementedError`. `open()`
440
+ returns only once the server has answered, so `FileNotFoundError`/
441
+ `PermissionError` surface there. At most 1 MiB is buffered; an abandoned
442
+ read sends the server an ERROR.
443
+ - `read_bytes`, `read_text`, `write_bytes`, `write_text` (text newline
444
+ handling is pathlib's), `copy()`/`move()` to and from any pathlib_next path.
445
+ - `stat()` is one `Client.stat()` probe (`FileStat`: `st_size` and
446
+ `st_mtime` 0 when unknown, directory mode for a listed directory);
447
+ `exists()`, `is_file()`, `is_dir()`.
448
+ - `iterdir()`, `walk()`, `glob()`/`rglob()` and `copy(..., recursive=True)`
449
+ need a server allowing `x-list` (one listing per directory, with each
450
+ entry's type, size and time, so no per-entry probe). Against other servers
451
+ a directory raises `FileNotFoundError`; listing a file raises
452
+ `NotADirectoryError`.
453
+ - Deleting, renaming, creating directories and permissions raise
454
+ `NotImplementedError`.
455
+ - TFTP errors become pathlib's: `FileNotFoundError`, `PermissionError`,
456
+ `FileExistsError`, `OSError(ENOSPC)`, `TimeoutError`, else `OSError(EIO)`.
457
+
458
+ ## Backends (`tftp.backends`)
459
+
460
+ - **`MemoryHandler(files=None, *, writable=False, overwrite=True, max_upload=None)`**
461
+ — serves `files` (name → bytes; names normalised so `/a\\b` is `a/b`);
462
+ uploads replace an entry only when complete. Thread-safe `files` updates.
463
+ - **`HttpHandler(base_url=None, *, url_for=None, writable=False, headers=None, timeout=10.0, buffer=1 MiB, opener=None)`**
464
+ — TFTP-to-HTTP(S) gateway on `urllib`: GET `base_url + quote(name)`
465
+ (or `url_for(context)`), streamed; `Content-Length` answers `tsize`;
466
+ WRQ → `PUT` (chunked without `tsize`) when `writable`; the final ACK
467
+ waits for the PUT's response. HTTP 404/410 → ERROR 1, 401/403 → 2,
468
+ 409 → 6, 413/507 → 3, other → 0. `..` in a name → ERROR 2.
469
+ - **`UpstreamHandler(upstream, *, client_options=None, buffer=1 MiB, stall_timeout=30.0, writable=False)`**
470
+ — terminating proxy: `upstream` is `"host"`, `"host:port"`, an address
471
+ object or `netimps.Host`, `(host, port)`, or `upstream(context)` returning
472
+ one. Each side
473
+ negotiates independently (`client_options` for the upstream `Client`);
474
+ only bytes cross, through a bounded pipe, so a slow client slows the
475
+ upstream. The RRQ is answered after the upstream answers: its ERROR
476
+ reaches the client with the same code and text, its `tsize` passes
477
+ through. A WRQ's final ACK waits for the upstream's final ACK.
478
+ - **`Pipe(capacity=1 MiB, size=None)`** — the bounded byte pipe between a
479
+ worker thread and a transfer, for your own handlers. Transfer side
480
+ (non-blocking, `WouldBlock`): `readinto`, `write`, `close`, `abort`,
481
+ `set_wakeup`. Worker side (blocking): `put(data, timeout)`,
482
+ `get(n, timeout)`/`read(n)`, `finish(error=None)`. `for_upload()` makes
483
+ `close()` wait (`WouldBlock`) until the worker calls
484
+ `set_result(error=None)`, and re-raise its error. `size` answers `tsize`.
485
+
486
+ ## Relay (`tftp.relay`)
487
+
488
+ **`Relay(route, host=None, port=69, *, idle_timeout=30.0, max_lifetime=3600.0, linger=2.0, upstream_source=None, limits=None, max_sessions=None, ignore_broadcast=True, reply_from_request_address=True, trace=None, on_session_end=None, port_range=None, interface=None)`**
489
+ — a transparent application relay (there is no standard TFTP relay). The
490
+ request is forwarded **byte for byte** from a fresh upstream-side socket; the
491
+ upstream's TID is learned from its first answer (from the address asked);
492
+ datagrams then cross unchanged between `client <-> relay(client-side TID)` and
493
+ `relay(upstream-side TID) <-> upstream`, so options and extensions this
494
+ library does not know work end to end. Each side negotiates nothing with the
495
+ relay — the client sees the upstream's OACK. For different settings per side
496
+ use `UpstreamHandler` (a terminating proxy) instead.
497
+
498
+ - `interface` — listen on one adapter, as for `Server`.
499
+ - `port_range` — as for `Server`, for both sockets of each relayed transfer
500
+ (client side and upstream side).
501
+ - `route` — an upstream (`"host"`, `"host:port"`, `"[v6]:port"`, an address
502
+ object or `netimps.Host`, `(host, port)`, `Upstream`) or `route(request, context) -> upstream | None`;
503
+ `None` refuses with ERROR 2 `"no route"`. Hostnames are resolved per
504
+ request (cached 60 s). Routes run on the relay's loop: keep them fast.
505
+ - A transfer ends on: an ERROR either way (after ≤1 s), the final DATA/ACK
506
+ exchange (the block size followed from an OACK's `blksize`/`blksize2`, then
507
+ `linger`), `idle_timeout` without traffic, or `max_lifetime`. Keep
508
+ `idle_timeout` above the longest timeout × retries a peer may use.
509
+ - A repeated request from the same client address/port is forwarded again
510
+ only while the upstream has not answered. Strays on either leg get ERROR 5.
511
+ - Shutdown sends ERROR 0 `"relay shutting down"` to both sides of each
512
+ transfer. Windows caps `max_sessions` at 250 by default (two sockets each).
513
+ - `on_session_end(RelaySummary)` — `session`, `client`, `upstream` (the
514
+ learned TID), `filename`, `operation`, `mode`, `bytes_to_client`,
515
+ `bytes_from_client`, `packets`, `duration`, `reason` (`"complete"`,
516
+ `"error"`, `"idle"`, `"lifetime"`, `"shutdown"`), `error` (`(code,
517
+ message)` of an ERROR that passed through, or `None`).
518
+ - Lifecycle and properties as for `Server`: `serve_forever`, `shutdown`,
519
+ `start`, `stop`, `close`, context manager; `server_address`,
520
+ `supports_pktinfo`, `active_sessions`.
521
+
522
+ Routing helpers (`tftp.relay`): **`upstream(value) -> Upstream(host, port)`**
523
+ (`host` normalised by `netimps.normalize_host`: brackets dropped, a
524
+ `"host:port"` split);
525
+ **`by_subnet({network: upstream})`** (keys: anything `netimps.parse(...,
526
+ IPNetwork)` takes — CIDR strings, `ipaddress` networks, interfaces, addresses
527
+ as /32 or /128, `(address, prefix)`; client address, longest prefix; mapped
528
+ v4 matches v4 networks); **`by_prefix({prefix: upstream})`** (filename,
529
+ longest prefix, `\` = `/`, leading separators ignored); **`by_interface({key:
530
+ upstream})`** (keys: an interface index, a `netimps.Interface`, or the local
531
+ address the request was sent to — string, `ipaddress` address or interface,
532
+ `netimps.Host`; needs pktinfo); **`RouteTable(routes, default=None)`** (first
533
+ match wins).
534
+
535
+ ## Packet events (`tftp.capture`)
536
+
537
+ **`PacketEvent(time, direction, local, remote, data, role="capture", session=None, leg=None)`**
538
+ — one datagram. `direction` is `"in"`/`"out"` from `role`'s view
539
+ (`"seen"` for a passive capture); `session` correlates one transfer's events;
540
+ `leg` is the relay side. Properties: `opcode`, `opcode_name`, `block`,
541
+ `payload_size`, `summary` (one line, never raises), `source`, `destination`;
542
+ methods `decode()`, `format()` (a human line), `to_dict(payload=False)` (JSON-
543
+ ready metadata; DATA payloads only as hex with `payload=True`).
544
+ `summarize(data)` is the one-line description on its own.
545
+
546
+ **Trace hooks** — `Client(trace=)`, `Server(trace=)`, `Relay(trace=)` take
547
+ `trace(PacketEvent)`, called for every datagram received and sent, on the
548
+ thread doing the I/O; exceptions it raises are logged, never propagated.
549
+ Roles are `"client"`, `"server"`, `"relay"`; one `session` id per transfer
550
+ (`c…`, `s…`, `r…`). A server traces a transfer's request as arriving at the
551
+ listening address. Requests a server refuses before a transfer exists are
552
+ not traced. Off (`None`) costs nothing per packet.
553
+
554
+ **`PcapWriter(path_or_binary_file)`** — writes events (it is a ready trace
555
+ hook: `Server(..., trace=PcapWriter("t.pcap"))`) or `write(time, source,
556
+ destination, payload)` as pcap, link type RAW, with synthesized IPv4/IPv6 and
557
+ UDP headers and valid checksums, so Wireshark/tshark decode it as TFTP
558
+ (v4-mapped addresses are written as IPv4). Context manager; `close()`.
559
+
560
+ **Reading captures** — pcap and pcapng (both byte orders, µs/ns and
561
+ `if_tsresol`, several interfaces), from a path or a stream (a live
562
+ `tcpdump -i eth0 -U -w - udp` pipe). Link types: Ethernet (VLAN/QinQ),
563
+ RAW, IPv4, IPv6, Linux SLL/SLL2, BSD NULL/LOOP. IPv4 and IPv6 fragments are
564
+ reassembled (a large `blksize` fragments on the wire). Non-UDP is skipped.
565
+
566
+ - **`read_frames(source) -> Iterator[(time, linktype, frame)]`**,
567
+ **`read_datagrams(source) -> Iterator[UdpDatagram(time, source, destination, payload)]`**
568
+ — `CaptureFormatError` (a `ValueError`) for anything that is not a capture.
569
+ - **`FlowTracker(ports=(69,), keep_payloads=True)`** — `feed(datagram) ->
570
+ PacketEvent | None` (role `"capture"`, direction `"seen"`; `None` for UDP
571
+ that is neither TFTP traffic of a known transfer nor to/from a request
572
+ port), `feed_all(datagrams)`, `.transfers`. A transfer starts at an
573
+ RRQ/WRQ to a request port and follows the server's answer (its TID; from
574
+ another address too, within 10 s) between that TID and the client's
575
+ address/port. `::ffff:a.b.c.d` and `a.b.c.d` count as one host.
576
+ - **`CapturedTransfer`** — `session`, `client`, `server`, `server_tid`,
577
+ `filename`, `mode`, `operation`, `requested`, `acknowledged`, `blksize`,
578
+ `windowsize`, `tsize`, `error` (`(code, message, "client"|"server")`),
579
+ `complete` (final DATA seen and ACKed), `packets`, `retransmissions` (DATA
580
+ seen again), `request_retransmissions`, `bytes`, `missing_blocks`,
581
+ `started`, `ended`, `duration`; `data(decode_netascii=True)` returns the
582
+ file up to the first gap (block numbers followed across rollover);
583
+ `to_dict()`.
584
+ - **`analyze(source, *, ports=(69,), filter=None, keep_payloads=True) -> Analysis(events, transfers)`**
585
+ — a whole capture (path, stream, or datagrams) at once.
586
+ - **`sniff(interface=None, *, stop=None) -> Iterator[UdpDatagram]`** — live,
587
+ Linux only (`AF_PACKET`, needs `CAP_NET_RAW`); `live_capture_supported()`.
588
+ Elsewhere pipe `tcpdump`/`dumpcap -w -` into `read_datagrams`.
589
+
590
+ **Filters** — **`compile_filter(text) -> predicate(event)`**: `key=value`
591
+ clauses joined by `and`, `,` for "any of", `!=` to negate; empty matches all.
592
+ Keys: `op` (opcode name), `host`/`src`/`dst` (address, CIDR, `addr:port`,
593
+ `[v6]:port`, `:port`; mapped v4 matches v4), `port`, `file` (shell pattern,
594
+ requests only), `block`, `code` (ERROR code), `session`, `leg`, `direction`.
595
+ `FilterError` (a `ValueError`) for an unknown key or malformed value.
596
+
597
+ ## Results and errors
598
+
599
+ **`TransferResult`** — `filename`, `operation` (`"read"` for RRQ, `"write"`
600
+ for WRQ, on both sides), `mode`, `peer`, `local`, `bytes` (payload bytes on
601
+ the wire; netascii-encoded size in netascii mode), `blocks`, `retransmits`,
602
+ `duration` (seconds), `negotiated`, `error` (or `None`); properties `ok` and
603
+ `throughput` (bytes/s).
604
+
605
+ **`Negotiated`** — what a transfer ran with: `blksize`, `windowsize`,
606
+ `timeout` (seconds), `tsize` (or `None`), `rollover`, and `options` (the OACK
607
+ as sent/received; empty when RFC 1350 defaults applied).
608
+
609
+ Exceptions: **`TftpError(code=0, message="")`** (base; `.code` is an
610
+ `ErrorCode` when the value is known, `.message` defaults to the code's
611
+ standard text), **`RemoteError`** (the peer sent ERROR; always raised as
612
+ the subclass for its code — `FileNotFound` 1, `AccessViolation` 2,
613
+ `DiskFull` 3, `IllegalOperation` 4, `UnknownTransferId` 5,
614
+ `FileAlreadyExists` 6, `NoSuchUser` 7, `OptionNegotiationError` 8 — and
615
+ plain `RemoteError` for 0 and unknown codes; `RemoteError.from_code(code,
616
+ message)` builds one), **`ProtocolError`** (the peer broke the protocol;
617
+ code 4, or 8 for option problems), **`TransferTimeout`** (also a
618
+ `TimeoutError`; retries exhausted or `max_duration` passed),
619
+ **`TransferAborted`** (cancelled locally: `abort()`, server shutdown).
620
+ `MalformedPacket` is a `ValueError`.
621
+
622
+ ## Wire format
623
+
624
+ `Opcode` (`RRQ`=1 … `OACK`=6) and `ErrorCode` (`NOT_DEFINED`=0 …
625
+ `OPTION_REFUSED`=8) are `IntEnum`s. Packet types are `NamedTuple`s:
626
+ `Request(opcode, filename, mode, options, raw=b"")` (`.is_read`; option
627
+ names lower-cased, first occurrence wins, mode lower-cased; `raw` is the
628
+ datagram as received, `.raw_options` every pair as sent — original case,
629
+ order, duplicates — and `.encode()` returns `raw` or a fresh encoding, so a
630
+ relay forwards unknown options untouched), `Data(block, data)`, `Ack(block)`,
631
+ `Error(code, message)`, `OptionAck(options)`.
632
+
633
+ - **`decode(bytes) -> Packet`** — raises `MalformedPacket`. Tolerates a
634
+ missing final NUL and drops a dangling option name.
635
+ - **`encode_request(opcode, filename, mode="octet", options=None)`**,
636
+ **`encode_data(block, data)`**, **`encode_ack(block)`**,
637
+ **`encode_error(code, message="")`**, **`encode_oack(options)`** — option
638
+ values are sent as `str(value)`; NUL inside a string raises `ValueError`.
639
+ - Strings are UTF-8 with `surrogateescape`, so any byte sequence round-trips
640
+ and real UTF-8 names decode naturally.
641
+ - Constants: `DEFAULT_BLKSIZE` 512, `MIN_BLKSIZE` 8, `MAX_BLKSIZE` 65464,
642
+ `MAX_WINDOWSIZE` 65535, `STANDARD_OPTIONS`, `EXTENSION_OPTIONS`,
643
+ `LISTING_OPTIONS` (`x-list`, `x-mtime`), `SUPPORTED_OPTIONS` (standard and
644
+ extensions).
645
+
646
+ ## URIs (RFC 3617)
647
+
648
+ - **`parse_url(url) -> TftpURL(host, port, filename, mode)`** — the file name
649
+ is the percent-decoded path after the authority's `/`; `;mode=netascii`
650
+ selects the mode (default `octet`); the port defaults to 69. Anything else
651
+ (another scheme, no host or file, an unknown parameter, `mode=mail`) raises
652
+ `ValueError`.
653
+ - **`format_url(host, filename, port=69, mode="octet")`** — the inverse
654
+ (`str(TftpURL)` too); `host` may be an address or interface object or a
655
+ `netimps.Host`; IPv6 hosts are bracketed.
656
+ - **`download_url(url, dest, *, progress=None, **client_options)`**,
657
+ **`upload_url(url, source, ...)`** — one-shot transfers by URL.
658
+
659
+ ## Netascii
660
+
661
+ Local text uses LF; on the wire a line ends CR LF and a bare CR is CR NUL.
662
+ The translation is lossless both ways (a local `\r\n` travels as `\r\0\r\n`).
663
+ **`NetasciiReader(raw)`** (`readinto`, `close`) encodes a binary reader;
664
+ **`NetasciiWriter(raw)`** (`write`, `flush`, `close`, `abort`) decodes into a
665
+ binary writer, holding a trailing CR until it sees what follows. Both handle a
666
+ CR on any block boundary. `tftp.netascii` also has `encode`, `decode` and
667
+ `encoded_size(fileobj)` (seekable; position preserved).
668
+
669
+ ## Transfer engine
670
+
671
+ **`Sender(send, read, negotiated, retries, now, oack=None, **kw)`** and
672
+ **`Receiver(send, write, negotiated, retries, now, reply=None, complete=None, **kw)`**
673
+ (`kw`: `backoff=2.0`, `max_timeout=None`, `expires=None` — an absolute
674
+ deadline in the caller's clock)
675
+ are one side of the DATA/ACK exchange **without any I/O**: packets leave
676
+ through `send(packet)`, arrive through `handle(buffer, n, now)`, and the caller
677
+ calls `on_timeout(now)` once `deadline` (in the caller's clock) passes, and
678
+ `resume(now)` when a stalled source/sink (`WouldBlock`) is ready again;
679
+ `abort(message)` cancels. State: `done`, `error`, `stalled`, `bytes`,
680
+ `blocks`, `retransmits`, `deadline`. A repeated OACK is tolerated: a
681
+ receiver re-sends its ACK 0, a sender ignores it (its own timeout resends
682
+ DATA 1). Build `read`
683
+ and `write` with `tftp.transfer.as_readinto(fileobj)` / `as_write(fileobj)`.
684
+ This is what the client and server drive; use it to run TFTP over another
685
+ transport or event loop.
686
+
687
+ ## Command line (`cli` extra)
688
+
689
+ `pip install tftp[cli]` adds duho. The `pytftp` console script is installed
690
+ either way; without the extra it prints one line naming the extra and exits 1.
691
+ `python -m tftp` is equivalent.
692
+
693
+ ```
694
+ pytftp get HOST REMOTE [LOCAL|-] [-p PORT] [-m octet|netascii] [-b BLKSIZE] [-w WINDOW]
695
+ pytftp get tftp://HOST[:PORT]/FILE [LOCAL|-] [-t TIMEOUT] [-r RETRIES] [--no-tsize] [--no-options]
696
+ [--compat PROFILE] [-4|-6] [--json] [--trace] [--pcap FILE]
697
+ pytftp put HOST LOCAL|- [REMOTE] (same options; or put tftp://HOST/FILE LOCAL|-)
698
+ pytftp ls HOST [DIR] | tftp://HOST/DIR (same options) [--json]
699
+ pytftp serve [ROOT] [--http URL | --upstream HOST[:PORT]] [-l ADDRESS | --interface NIC] [-p PORT] [-W/--write]
700
+ [--overwrite] [--no-create] [--compat PROFILE | --max-blksize N --max-windowsize N
701
+ --allow OPTION... --refuse OPTION... --fit-mtu] [--listing] [--max-sessions N]
702
+ [--max-per-client N] [--port-range LOW:HIGH] [--per-client] [--ignore-case]
703
+ [--remap REGEX=REPLACEMENT]... [--json] [--trace] [--pcap FILE]
704
+ pytftp relay [UPSTREAM] [--route-subnet CIDR=HOST[:PORT]]... [--route-prefix PREFIX=HOST[:PORT]]...
705
+ [-l ADDRESS | --interface NIC] [-p PORT] [--idle-timeout S] [--max-sessions N] [--port-range LOW:HIGH]
706
+ [--json] [--trace] [--pcap FILE]
707
+ pytftp capture FILE|- | -i IFACE [-p PORT]... [-f FILTER] [--no-packets] [--transfers]
708
+ [--extract DIR] [--json] [--payload]
709
+ ```
710
+
711
+ - `-b 0` / `-w 0` request no `blksize` / `windowsize`; defaults are 1428 and 0.
712
+ `--compat PROFILE` (`strict`, `default`, `pxe`, `hpa`, `legacy`) replaces the
713
+ option flags with the profile's settings (client and server alike).
714
+ - `get` writes to the remote file's basename by default; `-` is stdout.
715
+ `put` from `-` (stdin) needs a remote name. A `tftp://` URL replaces HOST and
716
+ the remote name (and sets the mode with `;mode=netascii`).
717
+ - `--trace` prints every datagram on stderr; `--pcap FILE` writes them as a
718
+ capture (Wireshark-readable).
719
+ - `serve --http URL` is the HTTP gateway, `serve --upstream` the terminating
720
+ proxy; `--write` enables uploads for both. `--allow` adds extension options
721
+ to the standard four; `--refuse` removes any.
722
+ - `--interface NIC` (serve, relay) listens on that adapter's IPv4 address
723
+ (`-l ::` for its IPv6 one).
724
+ - `serve --listing` allows `x-list`/`x-mtime` (with `--compat` too), which
725
+ `pytftp ls` and `TftpPath.iterdir()` need. `--port-range` pins transfer
726
+ ports. Directory serving only: `--per-client` serves `ROOT/<client
727
+ address>/` when it exists (IPv6 `:` written `-`), else `ROOT`;
728
+ `--ignore-case` finds names whatever their case (the exact name wins).
729
+ `--remap REGEX=REPLACEMENT` (repeatable, split at the first `=`) rewrites
730
+ requested names with the first matching rule, for any source. These are
731
+ CLI conveniences, not library API.
732
+ - `ls` prints `type size time name` per entry (`--json`: a list of entries);
733
+ exit 1 for a file, a missing name, or a server without listing.
734
+ - `relay` needs an UPSTREAM (the default route) or routes; prefix routes are
735
+ tried before subnet routes. `--json` prints one line per finished transfer
736
+ (its `RelaySummary`).
737
+ - `capture` reads a pcap/pcapng file, a live pipe on stdin (`tcpdump -i eth0 -U
738
+ -w - udp | pytftp capture -`), or (Linux) an interface. Packets print as they
739
+ are decoded; `--transfers` adds a summary per transfer at the end;
740
+ `--extract DIR` writes each transfer's file as `<session>-<name>`
741
+ (`.partial` when incomplete). Ctrl-C ends a live capture and still prints
742
+ the summaries.
743
+ - Exit codes: 0 success, 1 the transfer failed or the port could not be
744
+ bound (error text on stderr), 2 a caller error (bad argument, missing file,
745
+ unreadable capture, bad filter).
746
+ - `--json`: one JSON object on stdout per transfer, session or packet;
747
+ diagnostics always go to stderr.
748
+ - `serve` and `relay` log each transfer at INFO on stderr (`-v`/`-q` adjust)
749
+ and their final counters when stopped.
750
+
751
+ ## Dependencies
752
+
753
+ `netimps` (required, no dependencies of its own; imported lazily, on the
754
+ first transfer or server): pktinfo receive and reply sockets (`UdpEndpoint`,
755
+ also `arecv` for `AsyncServer`), broadcast/multicast checks, MTU payload
756
+ sizing, the retransmission timer (`Backoff`), socket binding,
757
+ host:port parsing, address/network types (`AddressLike`, `IPNetworkLike`,
758
+ `Host`, `Interface`: what the address-taking parameters accept), bind-error
759
+ hints. Importing netimps installs its additive `recvmsg`/`sendmsg`
760
+ socket patch on Windows unless `NETIMPS_NO_SOCKET_PATCH=1` is set first.
761
+ `duho` (optional, `cli` extra).