-
Notifications
You must be signed in to change notification settings - Fork 7
docs: state the rule that makes the media field decode safe #688
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -462,6 +462,22 @@ final class MediaUploadServer: Sendable { | |
| /// | ||
| /// A list rather than a dictionary so repeated names (e.g. a `field[]` array) | ||
| /// survive verbatim, in the order the editor sent them. | ||
| /// | ||
| /// This decode can't mangle anything, but only because of who is on the other end — | ||
| /// nothing in the code enforces it. Three things have to stay true: | ||
| /// | ||
| /// 1. Only the editor's own web page can reach this server. It listens on loopback, | ||
| /// and every request has to carry a per-session token. | ||
| /// 2. Text the editor puts in a form field is already valid Unicode. The browser | ||
| /// guarantees that when the value is set, so it cannot hand us bad bytes. | ||
| /// 3. The only way a browser can put *raw* bytes in a form is a file or a Blob, and | ||
| /// those always arrive with a filename. Anything with a filename is handled as | ||
| /// the file, never as a field — so raw bytes never reach this decode. | ||
| /// | ||
| /// If one of those stops being true, bad bytes quietly turn into replacement | ||
| /// characters, and the platforms don't even agree on how many: `ED A0 80` becomes | ||
| /// three of them here and one on Android. There is no single behavior worth | ||
| /// documenting, so the tests pin rule 3 instead. | ||
| private static func formFields(from parts: [MultipartPart]) async throws -> [MediaUploadField] { | ||
| var fields: [MediaUploadField] = [] | ||
| for part in parts { | ||
|
|
@@ -750,11 +766,12 @@ class InternalMediaClient: @unchecked Sendable { | |
| mimeType: String, | ||
| extraFields: [(name: String, value: Data)] | ||
| ) throws -> (InputStream, Int) { | ||
| // Serialize the non-file parts (post, additionalData) into the preamble | ||
| // ahead of the streamed file. They are small, so keeping them in memory is | ||
| // fine; `contentLength` counts them via `preamble.count`. Field values are | ||
| // appended as raw bytes (not through String) so a non-UTF-8 value is | ||
| // forwarded verbatim rather than coerced to empty. | ||
| // The non-file parts (post, additionalData) go into the preamble ahead of the streamed | ||
| // file; they're small, and `contentLength` counts them via `preamble.count`. Their | ||
| // values are appended as raw bytes rather than through `String(data:encoding:)`, which | ||
| // returns nil on bad UTF-8 — and the `?? ""` you'd reach for behind it would quietly | ||
| // drop a whole field. Raw bytes also keep this re-encode byte-for-byte identical to | ||
| // the plain passthrough it replaces. (Bad bytes can't get here; see `formFields`.) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Finding from Claude: This says bad bytes can't reach The risk is someone reads the new sentence and deletes the test as vacuous. Scoping it would keep both true, e.g. "Bad bytes can't reach here via the server; this function's own handling of them is pinned by Same on |
||
| var preamble = Data() | ||
| for field in extraFields { | ||
| preamble.append(Data("--\(boundary)\r\n".utf8)) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Finding from Claude:
Two predicates doing two different jobs on the same array:
:210—parts.first(where: { $0.filename != nil })picks the file: the first filename-bearing part.:216—parts.filter { $0.filename == nil }picks the fields: only the filename-less ones.So a second filename-bearing part is neither.
firsthas already returned, and the filter excludes it for having a filename — it falls out of the request entirely. (On this path, at least; the passthrough at:301relays the original body verbatim, so it survives there.)The rule's conclusion is still safe —
:216excludes every filename-bearing part, so raw bytes can't reach the decode. It's the stated reason that's off: "never as a field" holds for all of them, but "handled as the file" holds only for the first, and implies a second Blob gets processed as an upload when it's actually dropped.Same wording on
MediaUploadServer.kt:614-616.