Repository navigation
Harden File construction: explicit factories, strict array shapes, deprecate implicit filesystem access - #308
Conversation
File::__construct() no longer treats an existing local path as a file to read. Local files are loaded only through the explicit File::fromLocalPath() factory. New factories File::fromUrl(), File::fromDataUri() and File::fromBase64() make the kind of input explicit and never touch the filesystem. File::fromArray() now honours fileType: remote files require an http(s) url, inline files require base64 data and a mimeType; anything else is rejected. File, MessagePart and Message implement structural isArrayShape() checks, so shape probing no longer constructs objects (and no longer reads files). AbstractOpenAiCompatibleImageGenerationModel builds result files only via File::fromUrl()/fromBase64() and wraps invalid values in ResponseException. PromptBuilder::withFile() and MessageBuilder::withFile() keep accepting URL | data URI | base64 | local path strings through the new FileInputUtil, which rejects non-http(s) schemes before consulting the filesystem. Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. Unlinked AccountsThe following contributors have not linked their GitHub and WordPress.org accounts: @tunelko, @jiwhan8655. Contributors, please read how to link your accounts to ensure your work is properly credited in WordPress releases. If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. Props Bot commandsYou can trigger Props Bot actions by commenting on this PR:
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## trunk #308 +/- ##
============================================
+ Coverage 86.58% 87.12% +0.54%
- Complexity 1383 1463 +80
============================================
Files 69 70 +1
Lines 4449 4615 +166
============================================
+ Hits 3852 4021 +169
+ Misses 597 594 -3
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
gziolo
left a comment
There was a problem hiding this comment.
I used Codex and Claude to help verify these findings, focusing on backward compatibility implications for public interfaces. The inline comments cover three compatibility concerns and one small error-message inconsistency.
…ayloads
- File::__construct() keeps detecting an existing local file path (after
rejecting non-http(s) schemes) and emits an E_USER_DEPRECATED notice
pointing to File::fromLocalPath(); removal is left for a breaking release.
- Empty payloads are supported consistently: an empty data URI round-trips
through toArray()/fromArray(), File::fromBase64('') and an empty local
file produce an empty inline file, and withFile('') with a MIME type works
as before.
- Message::isArrayShape() no longer requires consecutive part keys, matching
fromArray() and toArray().
- The image response parser reports data[$index] for the missing-key case
as well.
- Extra MessagePart tests for wrongly typed keys.
Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
- Message::isArrayShape() only accepts the user and model roles and rejects user messages with function-call parts and model messages with function-response parts, matching fromArray(). - File::isArrayShape()/fromArray() require an inferable MIME type for remote entries without one, as before (no I/O involved); a null mimeType is treated like an absent one. - Tests that point at a ".txt" fixture now create it, so they detect a regression where the file is actually read. - Architecture note names the filesystem-free factories and the deprecated constructor behaviour concisely. Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
JasonTheAdams
left a comment
There was a problem hiding this comment.
One memory regression to address; finding and recommendation inline.
| } | ||
|
|
||
| // Routing through the data URI form makes the constructor take a deterministic, filesystem-free path. | ||
| return new self(sprintf('data:%s;base64,%s', $mimeType, $base64)); |
There was a problem hiding this comment.
[P2] Avoid reparsing the full base64 payload. Wrapping it in a data URI and passing it through the constructor creates several large temporary strings. On PHP 8.4, loading a 24 MiB local file peaked at 60 MiB on the base revision versus 184 MiB here, causing a fatal error with a 128 MiB limit. This also affects existing new File($path, $mimeType) and withFile() calls.
Recommendation: initialize the validated inline fields directly, without creating and reparsing a full data URI.
There was a problem hiding this comment.
Confirmed, thanks. The data URI copy plus the two regex captures of the payload were the cause. I changed it so that fromBase64() now creates an empty inline file via data:<mime>;base64, and assigns the validated payload directly, so no copy of it is embedded or re-parsed. With this change using 24Mib file the peak is back to 58 MiB on all three paths (constructor, fromLocalPath(), fromBase64())
testLargePayloadsDoNotMultiplyPeakMemory with an 8 MiB payload asserts the peak stays bounded.
Embedding the validated payload in a data URI and passing it through the constructor created several additional copies of a potentially large string (data URI, regex captures), tripling peak memory for large files on every path that ends in fromBase64(), including fromLocalPath() and the deprecated constructor path. Build an empty inline file through the data URI path instead and attach the payload directly. Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
|
@propsbot refresh |
gziolo
left a comment
There was a problem hiding this comment.
Looks good to me. The review feedback has been addressed, including the memory regression.
One small nit: please update the PR description to reflect that local-path construction remains supported with a deprecation notice, and empty payloads are accepted.
Summary
Hardening of the
FileDTO and the code paths that construct it from data the SDK did not produce itself (array-shaped input and provider API responses).Today
File::__construct()guesses what a string is by trying, in order, URL, data URI, existing local file path, and plain base64. That makes the constructor's behaviour depend on the filesystem, which is surprising for a DTO and means callers have to be careful about where a string came from. This PR makes the kind of input explicit for the code paths that handle external data, and keeps the generic constructor backward compatible while marking its implicit path detection as deprecated.Changes
FileFile::fromUrl(),File::fromDataUri(),File::fromBase64()andFile::fromLocalPath(). OnlyfromLocalPath()reads from the filesystem, and it rejects stream wrappers.fromBase64()initialises the inline fields directly, so large payloads are not copied into a data URI and parsed again.E_USER_DEPRECATEDnotice pointing toFile::fromLocalPath(). Removal is left for a breaking release.new File('', $mime),File::fromBase64('', $mime), an empty data URI and an empty local file all produce an empty inline file that round-trips throughtoArray()/fromArray().fromArray()now honoursfileType:remoterequires an http(s)url,inlinerequires base64base64Dataand amimeType. AnullmimeTypeis treated as absent (remote files still infer it from the URL extension). Mismatched or wrongly typed keys raise the SDKInvalidArgumentExceptioninstead of aTypeError.isArrayShape()is a structural check that mirrors thefromArray()rules without constructing the object.\z/Dso trailing newlines are not accepted.MessagePart,Message: structuralisArrayShape()overrides and type guards infromArray(), so shape probing has no side effects and nested files follow the strictFilerules. TheMessagecheck mirrors the role/part rules of the constructor and, likefromArray(), does not requirepartskeys to be consecutive.AbstractOpenAiCompatibleImageGenerationModel: result files are built viaFile::fromUrl()/File::fromBase64(), and invalid or missing values become aResponseExceptionnaming the offendingdata[i]entry.FileInputUtil::fromString()(new): the resolver behindPromptBuilder::withFile()andMessageBuilder::withFile(). The string form ofwithFile()keeps accepting URL, data URI, base64 and local path inputs exactly as before; the docblocks now say that local path resolution reads from the filesystem and should only be used with developer-controlled strings.MimeType::isValid():Dmodifier.Filefactories added to the architecture diagram, a short note on which constructors touch the filesystem, and the traditional-API file example now shows a valid message part shape.Backward compatibility
new File('/existing/path')keeps working and emits one silenced deprecation notice. UseFile::fromLocalPath()going forward. The builder methodswithFile()are unaffected.new File('file:///path'),new File('ftp://...')and other non-http(s) schemes now throw.File::fromArray()rejects arrays whosefileTypedoes not match the keys present, and inline shapes without amimeType.Message::isArrayShape()is structural, so a role/part-incompatible message array now fails insidefromArray()with a precise message instead of falling through to the list-of-parts branch inPromptBuilder.data[i]instead ofchoices[i].No public signature changes.
n.e.x.tplaceholders andAiClient::VERSIONare left for the release PR.Testing
composer lintandcomposer test:unitpass (1294 tests).fromArray()/isArrayShape()forFile,MessagePartandMessage(including non-consecutive part keys andnullmimeType), the image response parser,FileInputUtil, builder tests confirmingwithFile()behaviour is unchanged, and a regression test bounding peak memory for large payloads.Use of AI Tools
AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Mythos 5.1 (implementation), GPT-6 Astra (independent review)
Used for: Identifying the finding requiering the hardening, planning for the hardening solution and code / test implementations. Reviews, iterating, tweaking and testing done by me.