Conversation
XCFramework BuildThis PR's XCFramework is available for testing. Add the following to your .package(url: "https://github.com/wordpress-mobile/GutenbergKit", branch: "pr-build/688")Built from 5620d2b |
9e30369 to
2e73cda
Compare
`formFields` decodes every non-file form part as UTF-8. That can only be lossless because of who is on the other end, and nothing in the code enforces it -- so write it down on both platforms, in plain terms: only the editor's own page can reach the server, the browser guarantees form-field text is valid Unicode, and raw bytes always arrive carrying a filename, which routes them to the file rather than to a field. Pin the third condition with tests, since it is the one this code could break on its own. The bodies are ordered the way `uploadToServer` actually emits them -- file first, then additionalData -- and assert both the uploaded filename and the decoded fields, so the suite fails if either the partition or the file-selection rule changes. A second test covers the other half: valid UTF-8 round-trips, so real captions and titles are unaffected. The `buildMultipart` comments also claimed raw bytes were appended so a non-UTF-8 value would be "forwarded verbatim rather than coerced". That is not why -- such a value cannot reach them. On iOS they avoid a failable `String(data:encoding:)` whose `?? ""` would quietly drop a whole field; on both platforms they keep the re-encode byte-for-byte identical to the passthrough it replaces.
2e73cda to
5620d2b
Compare
dcalhoun
left a comment
There was a problem hiding this comment.
Looks good. Captured a few inline suggestions from Claude, but none of them are blocking.
| /// 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. |
There was a problem hiding this comment.
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. first has 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 :301 relays the original body verbatim, so it survives there.)
The rule's conclusion is still safe — :216 excludes 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.
| /// 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. | |
| /// 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 excluded from | |
| /// the fields, so raw bytes never reach this decode. (The first such part is the | |
| /// file; on this path any others are dropped.) |
Same wording on MediaUploadServer.kt:614-616.
| // 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`.) |
There was a problem hiding this comment.
Finding from Claude:
This says bad bytes can't reach multipartBodyStream, but multipartBodyPreservesNonUTF8FieldValue — "forwards a non-UTF-8 field value verbatim", MediaUploadServerTests.swift:836 — calls this function with exactly those bytes. The rationale this replaced was the text that test pointed at.
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 multipartBodyPreservesNonUTF8FieldValue."
Same on MediaUploadServer.kt:719.
Stacked on #687. Last of nine PRs splitting #621.
What?
Writes down why decoding the editor's form fields as UTF-8 can't corrupt them, and adds tests for the one condition this code could break on its own.
Why?
formFieldsreads every non-file form value as UTF-8. UTF-8 decoding doesn't fail on bad bytes — it quietly swaps in a replacement character. So if bad bytes ever did arrive, a caption would come back mangled and nothing would report an error.That can't happen today, but only because of who is on the other end, and nothing in the code says so. Three things have to stay true:
If one of those stops being true, iOS and Android mangle the value differently, so there's no single behaviour that could be documented in place of the rule. Turning off each platform's filename check shows it: the same three bad bytes (
ED A0 80) come back as three replacement characters on iOS and one on Android. Both measured, not guessed.How?
formFields, on both platforms.String(data:encoding:)returns nil on bad UTF-8, and the?? ""you'd reach for behind it would throw away the whole field. On both platforms, using the raw bytes keeps this path sending exactly the same request as the plain pass-through it stands in for, so an upload doesn't change shape just because the image got resized.Testing Instructions
The tests cover condition 3, the only one this code controls. A request carrying a second part that looks like a Blob — it has a filename, and its bytes aren't valid UTF-8 — must not show up in
fields, and must not get uploaded in place of the real file.Both test bodies put
filefirst and the other fields after, which is the order the editor actually sends them. Keep it that way. The server treats the first part that has a filename as the file, so moving the Blob ahead offilemakes the Blob the upload and throws the real file away — while the check onfieldsstill passes, so nothing catches it.The Blob part reaches neither
fieldsnor the request sent on to WordPress; it is dropped. This PR doesn't change that.swift test— 396 tests green:Gutenberg:testDebugUnitTest— 30 tests inMediaUploadServerTestgreen