Remote file access (2/4): read remote file content — whole file, byte ranges (offset/length), text with charset, streaming
Nobody has claimed this yet.
Assessment
- Difficulty
- 5/5
- Estimated time
- Over a week
- Newbie friendliness
- 38/100
- Issue type
- Feature
- Clarity
- Mostly clear
- Activity status
- Quiet
- Tech stack
- java, powershell
- Domain
- api, backend-api-design, networking
Research direction
Start at the client.file(path) entry point and review RemoteFileInfo, ShellFileCopy, the streaming command terminal from #111, and the base64 helper from #145. Use FakeWsmanServer and WinRMLiveTest as the primary test entry points, then update files.md, README, and file-transfers.md. Done means whole-file, ranged, negative-offset, streaming, digest, failure, cap, and live-host cases pass with mvn verify site.
Written by the indexing model from the issue text.
Description
Second issue of the remote file access family, building on the
client.file(path)entry point, theRemoteFileInfotype and the base64-framed PowerShell helper introduced by #145.
Context
Listing tells you a remote file is there; this issue reads its bytes. Same channel as the upload path in ShellFileCopy, run in reverse: the remote side base64-encodes the requested byte range, the client decodes it as the output chunks arrive.
Two properties make base64 the right framing rather than an accident of history:
- it is binary-safe — arbitrary bytes survive a channel that is fundamentally a text console, with none of the code page hazards of #142; and
- it is ASCII-only, so the client never has to know or guess the remote console encoding to recover the exact bytes.
The cost is the 4/3 inflation plus the shell's own overhead: this is a mechanism for configuration files, logs and small data files, not a bulk transport. That must be stated in the API docs, exactly as the upload path already states it.
Proposed API
Terminals on the client.file(path) request:
// Whole file, bytes
byte[] content = client.file("C:\\Windows\\Temp\\collect.bin").readBytes();
// Whole file, text with an explicit charset (no guessing, no default)
String text = client.file("C:\\inetpub\\logs\\u_ex260729.log")
.readText(StandardCharsets.UTF_8);
// A range: from a position, for a length — the point of the exercise for big logs
byte[] tail = client.file("D:\\logs\\huge.log")
.offset(1_073_741_824L) // absolute byte position
.length(65_536) // bytes to read; omit for "to the end"
.readBytes();
// Streaming, so memory stays bounded by the buffer and not by the file
try (InputStream in = client.file("D:\\logs\\huge.log").openStream()) {
...
}
try (BufferedReader reader = client.file("D:\\logs\\huge.log")
.charset(StandardCharsets.UTF_8)
.openReader()) {
reader.lines().filter(l -> l.contains("ERROR")).forEach(System.out::println);
}
// Integrity, reusing the digest probe ShellFileCopy already has
String sha256 = client.file("C:\\Windows\\Temp\\collect.ps1").digest("SHA256");
offset(long)/length(long) compose with every read terminal.
Negative offsets mean "from the end" — decided, and part of the scope:
// The tail case: last 8 KiB of a log, one seek, no scan
byte[] tail = client.file("D:\\logs\\huge.log").offset(-8192).readBytes();
Exact semantics to implement and document:
offset(-n)resolves server-side tomax(0, size - n), so a file shorter thannreturns the whole file, never an error and never a short read from a negative position.length(...)still applies after the resolution and still clamps at EOF:offset(-8192).length(1024)returns the first 1 KiB of the last 8 KiB.- The size is read inside the same remote invocation as the seek (from the open
FileStream), not by a separateinfo()round trip — otherwise a growing log would be tailed from a stale position. offset(0)is the start of the file; there is no "negative zero" case to special-case.
Requirements
The remote read
- Open with a share mode that tolerates other writers:
[IO.File]::Open($p, 'Open', 'Read', 'ReadWrite'), not[IO.File]::OpenRead. A log being written by a running service is the single most common thing a caller wants to read, andOpenReadfails on it. Files held with a truly exclusive lock (pagefile.sys, live registry hives) still fail — map that to a clear exception naming the sharing violation. - Seek + bounded read, so a range costs a seek and not a full scan: position the stream at
offset, read at mostlengthbytes, base64 the buffer, write it out. Never read the whole file to serve a range. - Chunked, streaming output: the script writes base64 in fixed-size blocks so the client can decode incrementally through the streaming command terminal (#111) instead of buffering the whole payload. Keep each write within the wire framing the shell already handles, and mind
MaxEnvelopeSize(153 600) for the command itself, not just the output. -EncodedCommandinvocation and pure-ASCII output, exactly as in #145 — nocmd.exequoting of caller-supplied paths, no dependence on the console code page.certutil -encodefallback for whole-file reads when PowerShell is unavailable or constrained (strip the-----BEGIN CERTIFICATE-----framing it adds). Ranged reads have nocertutilequivalent: fail with a message that says so.
Semantics to pin down and document
- Ranges are byte ranges, not character ranges. A range boundary can split a multi-byte character, so
readText(Charset)on a ranged read may yieldU+FFFDat the edges. Say this in the Javadoc; do not try to be clever about it. - No BOM stripping in
readBytes(). ForreadText/openReader, either strip a leading BOM or don't — pick one, document it, test it. offsetpast EOF returns empty, not an error;lengthbeyond EOF returns what exists. A file that grows or shrinks between two ranged reads is the caller's problem, but note it: this is not a snapshot.- Empty file, zero
length, and 0-byte range are valid and return an empty result. - Size guard: an unbounded
readBytes()on a multi-gigabyte file must not OOM the JVM silently. Enforce a documented default cap (e.g. 64 MiB) with an explicitmaxBytes(long)override, and point callers atopenStream()/downloadFilefor anything larger. - Throughput expectation documented with a measured figure from the live host, next to the existing "designed for small script files, not bulk data" wording in
file-transfers.md. - Timeout semantics follow the existing split: the blocking terminals use the wall-clock deadline;
openStream()/openReader()use the inactivity semantics of the streaming terminals.
Tests & docs
FakeWsmanServertests: whole-file read, ranged read (mid-file, at EOF, past EOF, zero length), negative offset (larger than, equal to and smaller than the file size, and combined withlength), binary content with every byte value 0–255, a multi-byte character split across two chunks (must decode correctly through the incremental path), the size cap, and the sharing-violation and missing-file failures.- Digest agreement test:
readBytes()of a file uploaded withuploadFilematches the local bytes, anddigest("SHA256")matches the local digest. WinRMLiveTest: read a real file whole, read a range from a large file (assert the range is served by a seek, i.e. duration does not scale with the file size), and read a file currently held open by another process.files.mdgains the read section; README gains the snippet;file-transfers.mdcross-links the reverse direction.
Acceptance criteria
- Whole-file and ranged reads return byte-exact content for binary and text files, verified by digest against the local original.
- A range near the end of a large file is served without transferring the whole file, both with an absolute offset and with
offset(-n). - A log file open for writing by another process can be read.
- The default size cap prevents an accidental unbounded read, and
openStream()reads a file larger than the cap with bounded memory. mvn verify sitegreen: no checkstyle/PMD/SpotBugs findings, full Javadoc, docs updated.
🤖 Generated with Claude Code
- Dominant language
- Java
- Stars
- 11
- Forks
- 4
- Avg merge
- 5d 5h
- Merged PRs (30d)
- 6
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from MetricsHub/winrm-java
-
Difficulty 5/5 Over a week Newbie friendliness 35/100
MetricsHub/winrm-java#148 ·
-
Difficulty 5/5 Over a week Newbie friendliness 35/100
MetricsHub/winrm-java#147 ·
-
Difficulty 5/5 Over a week Newbie friendliness 35/100
MetricsHub/winrm-java#145 ·
-
Kerberos credential delegation: allowDelegation() and CLI --allow-delegate (winrs -allowdelegate) Open
Difficulty 4/5 3-5 days Newbie friendliness 48/100
MetricsHub/winrm-java#141 ·
-
Difficulty 4/5 3-5 days Newbie friendliness 68/100
MetricsHub/winrm-java#139 ·
All issues in MetricsHub/winrm-java
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
infinispan/infinispan#18150 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
-
untriaged
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
opensearch-project/k-NN#3597 ·
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 82/100