You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
First issue of a remote file access family: metadata and listing here, content reading in #146, downloadFile in #147, CLI subcommands in #148. This one introduces the shared foundation (the client.file(...) entry point, the RemoteFileInfo type, and the base64-framed PowerShell helper) that the other three build on.
Context
WinRM has no file-access verb of its own — nothing like SFTP's OPENDIR/READ. Reading files on a remote host over WinRM therefore means reusing one of the two channels the client already speaks:
WMI/WQL — CIM_DataFile and CIM_Directory expose real metadata (FileSize, LastModified, CreationDate, Readable, Hidden, System, Archive, Extension), and the client's WQL layer already supports the association form needed to list a directory:
SELECT Name, FileSize, LastModified FROM ASSOCIATORS OF {Win32_Directory.Name="C:\\Windows\\Temp"} WHERE ResultClass = CIM_DataFile
Workable, but: the WMI file provider is notoriously slow (it walks the filesystem inside WMI, and an unindexed CIM_DataFile query on a large tree can take minutes), recursion means one association query per directory, timestamps come back as CIM DMTF datetimes, and it cannot read content at all.
The command shell — the channel ShellFileCopy already uses to upload files (chunked base64 through cmd.exe, decoded with certutil, digest-verified). Listing and reading are the same trick in reverse, and it is fast, recursive, and filters server-side.
This family takes the shell path and keeps WMI as a documented alternative for metadata-only cases. That keeps the zero-dependency promise, needs no extra port, and reuses the transport hardening (quota retries, envelope chunking, incremental decoding) already in the codebase.
Proposed API
A per-path entry point, sibling of wql(...) / command(...):
try (WinRMClientclient = WinRMClient.builder("server01.acme.com")
.credentials("ACME\\admin", password)
.build()) {
// One path: existence and propertiesif (client.file("C:\\Windows\\Temp\\collect.log").exists()) {
RemoteFileInfoinfo = client.file("C:\\Windows\\Temp\\collect.log").info().orElseThrow();
System.out.println(info.size() + " bytes, modified " + info.lastModified());
}
// A directory: listing with filtersList<RemoteFileInfo> logs = client.list("C:\\inetpub\\logs")
.glob("*.log") // server-side -Filter
.recursive() // off by default
.maxDepth(3)
.filesOnly() // or directoriesOnly()
.modifiedAfter(Instant.now().minus(Duration.ofDays(1)))
.minSize(1024L)
.execute();
// Large trees stream, like WQL rowstry (Stream<RemoteFileInfo> tree = client.list("D:\\data").recursive().stream()) {
tree.filter(RemoteFileInfo::isDirectory).forEach(System.out::println);
}
}
RemoteFileInfo — an immutable value type in the style of WqlRow (Java 11, so a final class with accessors, not a record):
raw FileAttributes value, for anything not exposed above
Requirements
The remote helper — base64-framed PowerShell, not dir
Never parse dir output: its column layout, date format and decimal separator are locale-dependent, and it has no timestamps beyond minute granularity. Use PowerShell (Get-ChildItem / [IO.FileInfo]).
Invoke it with powershell -NoProfile -NonInteractive -EncodedCommand <base64 UTF-16LE>. This sidesteps cmd.exe quoting entirely (no ^/"/% escaping games with user-supplied paths) and is immune to the console code page problems fixed in Command output is mis-decoded on non-English Windows: shell pinned to CP437, output decoded as the ANSI code page #142. Respect the existing MAX_COMMAND_LENGTH (8000) budget from ShellFileCopy: the scripts must stay terse; if one grows past the budget, upload it as a script file with the existing ShellFileCopy path and invoke it by name.
The script emits pure ASCII: it builds its records as UTF-8 bytes and writes them back as base64, so non-ASCII file names survive whatever the remote console encoding is. The client decodes base64 → UTF-8 → records. No charset guessing anywhere in this path.
Locale-independent field encoding inside the records: timestamps as .Ticks (UTC), sizes and attributes as plain integers, one record per line, tab-delimited with the path last. Windows forbids control characters (0x00–0x1F) in file names, so tab and newline are safe delimiters — state that reasoning in the code comment.
PowerShell may be unavailable or constrained (AppLocker, Constrained Language Mode, PowerShell removed). Detect the failure and report it as a clear WinRMClientException naming the cause, instead of surfacing a raw cmd.exe error. Document the WMI/ASSOCIATORS OF alternative in that case.
Behavior
Access-denied entries must not be silently dropped: Get-ChildItem on a tree with protected subdirectories partially fails. Collect those paths and expose them (e.g. RemoteFileList.inaccessible() from execute(), and a onInaccessible(Consumer<String>) hook for stream()) so a caller can tell "no matches" from "could not look".
Do not follow reparse points/junctions/symlinks when recursing (cycle risk, and C:\Documents and Settings-style legacy junctions loop straight back). Expose them as entries with isReparsePoint() true.
Filters are pushed server-side where PowerShell supports it cheaply (-Filter for the glob, -Recurse, -Depth, -File/-Directory); size and time predicates are applied by the script (Where-Object) so a big tree is not shipped over the wire just to be discarded locally. Document which filter is evaluated where.
glob(String) is a Windows wildcard pattern (*, ?), not a regex and not a full path — document that -Filter uses the legacy 8.3-aware matcher, so *.log also matches x.log1-style short names; offer regex(String) only if a follow-up needs it.
info() returns Optional.empty() for a missing path — a non-existent file is not an error. Genuine failures (access denied on the path itself, invalid path, unreachable) throw.
Long paths (>260 chars) must not silently truncate: use the \\?\-aware .NET APIs and cover one such path in the live test.
Tests & docs
FakeWsmanServer-based tests: scripted base64 record payloads → parsed RemoteFileInfo values, including a non-ASCII name, a >4 GB size, a hidden+system entry, a reparse point, an empty directory, and a partially-inaccessible tree.
Record-parser unit tests for malformed/truncated payloads (must fail with a clear exception, never with a half-populated object).
WinRMLiveTest coverage against a real host (anaxagore): list C:\Windows\Temp, a recursive listing with maxDepth, and a non-ASCII file name.
New src/site/markdown/files.md page (remote file access: listing, properties, the shell-vs-WMI trade-off, limitations) linked from index.md; README gets the one-liner + snippet.
Acceptance criteria
client.file(path).exists()/.info() and client.list(dir) with all documented filters work against a real host and against FakeWsmanServer.
Non-ASCII file names round-trip correctly regardless of the remote machine's code page.
A recursive listing of a tree containing an access-denied subdirectory returns the readable entries and reports the inaccessible paths.
Recursion terminates on a junction loop.
mvn verify site green: no checkstyle/PMD/SpotBugs findings, full Javadoc, files.md published.
Context
WinRM has no file-access verb of its own — nothing like SFTP's
OPENDIR/READ. Reading files on a remote host over WinRM therefore means reusing one of the two channels the client already speaks:WMI/WQL —
CIM_DataFileandCIM_Directoryexpose real metadata (FileSize,LastModified,CreationDate,Readable,Hidden,System,Archive,Extension), and the client's WQL layer already supports the association form needed to list a directory:Workable, but: the WMI file provider is notoriously slow (it walks the filesystem inside WMI, and an unindexed
CIM_DataFilequery on a large tree can take minutes), recursion means one association query per directory, timestamps come back as CIMDMTFdatetimes, and it cannot read content at all.The command shell — the channel
ShellFileCopyalready uses to upload files (chunked base64 throughcmd.exe, decoded withcertutil, digest-verified). Listing and reading are the same trick in reverse, and it is fast, recursive, and filters server-side.This family takes the shell path and keeps WMI as a documented alternative for metadata-only cases. That keeps the zero-dependency promise, needs no extra port, and reuses the transport hardening (quota retries, envelope chunking, incremental decoding) already in the codebase.
Proposed API
A per-path entry point, sibling of
wql(...)/command(...):RemoteFileInfo— an immutable value type in the style ofWqlRow(Java 11, so a final class with accessors, not a record):String path()String name()boolean isDirectory()Attributeslong size()Length(0 for directories)Instant lastModified(),created(),lastAccessed()*TimeUtcboolean isHidden(),isSystem(),isReadOnly(),isArchive(),isReparsePoint()Attributesbit flagsint attributes()FileAttributesvalue, for anything not exposed aboveRequirements
The remote helper — base64-framed PowerShell, not
dirdiroutput: its column layout, date format and decimal separator are locale-dependent, and it has no timestamps beyond minute granularity. Use PowerShell (Get-ChildItem/[IO.FileInfo]).powershell -NoProfile -NonInteractive -EncodedCommand <base64 UTF-16LE>. This sidestepscmd.exequoting entirely (no^/"/%escaping games with user-supplied paths) and is immune to the console code page problems fixed in Command output is mis-decoded on non-English Windows: shell pinned to CP437, output decoded as the ANSI code page #142. Respect the existingMAX_COMMAND_LENGTH(8000) budget fromShellFileCopy: the scripts must stay terse; if one grows past the budget, upload it as a script file with the existingShellFileCopypath and invoke it by name..Ticks(UTC), sizes and attributes as plain integers, one record per line, tab-delimited with the path last. Windows forbids control characters (0x00–0x1F) in file names, so tab and newline are safe delimiters — state that reasoning in the code comment.WinRMClientExceptionnaming the cause, instead of surfacing a rawcmd.exeerror. Document the WMI/ASSOCIATORS OFalternative in that case.Behavior
Get-ChildItemon a tree with protected subdirectories partially fails. Collect those paths and expose them (e.g.RemoteFileList.inaccessible()fromexecute(), and aonInaccessible(Consumer<String>)hook forstream()) so a caller can tell "no matches" from "could not look".C:\Documents and Settings-style legacy junctions loop straight back). Expose them as entries withisReparsePoint()true.-Filterfor the glob,-Recurse,-Depth,-File/-Directory); size and time predicates are applied by the script (Where-Object) so a big tree is not shipped over the wire just to be discarded locally. Document which filter is evaluated where.glob(String)is a Windows wildcard pattern (*,?), not a regex and not a full path — document that-Filteruses the legacy 8.3-aware matcher, so*.logalso matchesx.log1-style short names; offerregex(String)only if a follow-up needs it.stream()builds on the streaming command terminal of Streaming APIs (phase 2): stream()/start() terminal methods on the fluent WinRMClient #111: records are parsed and yielded as the output chunks arrive, memory bounded by the parse buffer, not by the tree size. Must be closed (try-with-resources) likeWqlRequest.stream().info()returnsOptional.empty()for a missing path — a non-existent file is not an error. Genuine failures (access denied on the path itself, invalid path, unreachable) throw.\\server\share\...) work when the caller's credentials can reach them, i.e. only with credential delegation (Kerberos credential delegation: allowDelegation() and CLI --allow-delegate (winrs -allowdelegate) #141) — document the second-hop limitation rather than pretending it works.\\?\-aware .NET APIs and cover one such path in the live test.Tests & docs
FakeWsmanServer-based tests: scripted base64 record payloads → parsedRemoteFileInfovalues, including a non-ASCII name, a >4 GB size, a hidden+system entry, a reparse point, an empty directory, and a partially-inaccessible tree.WinRMLiveTestcoverage against a real host (anaxagore): listC:\Windows\Temp, a recursive listing withmaxDepth, and a non-ASCII file name.src/site/markdown/files.mdpage (remote file access: listing, properties, the shell-vs-WMI trade-off, limitations) linked fromindex.md; README gets the one-liner + snippet.Acceptance criteria
client.file(path).exists()/.info()andclient.list(dir)with all documented filters work against a real host and againstFakeWsmanServer.mvn verify sitegreen: no checkstyle/PMD/SpotBugs findings, full Javadoc,files.mdpublished.🤖 Generated with Claude Code