Read Real Bytes and Keep Paths

JavaBeginner
Practice Now

Introduction

An archive can contain identical pictures under different names, and different pictures with the same size. You will build the byte-reading part of a reusable duplicate finder and save a complete inventory without changing the originals.

You should know Java classes, methods, lists, exceptions, and basic file reading. This walkthrough introduces binary input, automatic resource closure and SHA-256. The prepared directory adapter and command runner are supporting code; you implement the digest. This is a read-only exercise, not a disk cleanup tool. The author estimates 50 minutes, including reading and ordinary corrections.

Read the Bytes, Not the Filename

In this step, you will inspect real images, follow the digest's actual caller, and implement a small stream-reading method. First predict the image result: can two different filenames contain identical bytes, and can equal-length files contain different bytes?

Open WebIDE. Choose File → Open Folder..., enter /home/labex/project/duplicates in the path field and click OK. Wait until EXPLORER shows DUPLICATES. Choose Terminal → New Terminal to open WebIDE's integrated terminal. Source editor tabs and this terminal panel are inside WebIDE; there is no separate product Terminal tab to find. Every lab starts independently with its own files.

cd changes this shell's working directory. Repeat it if you open another terminal:

cd /home/labex/project/duplicates

The prepared run.sh compiles saved src/*.java into a fresh temporary build directory, then runs the Java class named after it. The compiler uses -encoding UTF-8; compiler and Java child processes use C.UTF-8, and Java also uses UTF-8 text defaults. This supports the supplied UTF-8 paths without changing global shell settings. No build-tool installation is needed.

./run.sh Sample

Sample decodes the real PNG files through ImageIO and compares their complete byte arrays. Expect red = renamed: true and red = blue: false. All three images have the same dimensions and byte length. Under data/gallery, red.png and copies/renamed.png are equal despite their names; blue.png has different content. These are original synthetic images, with no external assets. You may open the PNGs from the file tree for another view, but the required evidence is the actual decoding and byte result. No separate image-preview server is supplied.

Sample does not call Sha256 or Digester. It is a normal image/byte example, not proof that your digest works. Open src/Digester.java in the file tree. An interface describes a method a caller can use: digest(Path) returns a hexadecimal fingerprint or throws IOException when it cannot read the file. Path identifies a filesystem path; an InputStream reads binary bytes without decoding text.

Open Sha256.java. implements Digester connects this implementation to that public contract. Its prepared digest method creates a SHA-256 object and a 1024-byte buffer, calls the unfinished readIntoDigest method, then formats the finished digest as hexadecimal. The outer catch adapts the checked algorithm exception into IOException. & 255 treats a signed Java byte as its unsigned value for two hexadecimal characters. These supporting lines are ready; your work is the complete binary-read operation.

Now open Inventory.java. Find Digester digester = new Sha256() and then digester.digest(path) inside the file loop. Unlike Sample, this caller really uses the method you will implement. The command takes Inventory ROOT REPORT: the first argument is the directory to scan, the second is a report outside that directory. reports already exists beside data.

The next command deliberately fails with exit status 2 because the stream-reading method is still unfinished:

./run.sh Inventory data/gallery reports/inventory.tsv

Expect Scan failed: Implement counted binary reading; any old report is not current. This failure tells you where the missing work lies. It must not be interpreted as a successful empty inventory.

Before editing, make a source-only backup. cp copies its first path to its second. Make this once, before your first edit; do not later overwrite it with a broken version.

cp src/Sha256.java work/Sha256.java.before-edit

Return to the Sha256.java editor tab. Click inside the source, press Ctrl+F (Command+F on macOS) and search for private void readIntoDigest. Close the editor's Find field with its × button, then click the source again. Otherwise typing may change search text instead of Java. Select only the throw new IOException(...) statement inside that method; keep its signature and enclosing braces.

A reusable buffer can contain old bytes after a short read. read(buffer) returns the valid byte count for this read, or -1 at end of file. An empty file immediately returns -1; a 4099-byte file fills four 1024-byte buffers and then supplies only three valid bytes. Updating with the entire buffer for that final read would include stale tail bytes.

Write the complete resource-owning loop in place of the throw:

try (InputStream input = Files.newInputStream(path)) {
    int count;
    while ((count = input.read(buffer)) != -1) {
        sha.update(buffer, 0, count);
    }
}

The assignment stores the current count before the loop tests it. The update starts at offset zero and uses exactly that count. Try-with-resources closes the stream on success and on a read error; closing releases the file resource, rather than promising immediate garbage collection. Do not catch an unreadable file and return a pretend fingerprint.

Choose File → Save for Sha256.java, and wait until its tab shows the close ×. Auto Save may be enabled; leave it as it is. Return to the integrated terminal and read the actual disk version before compiling:

cat src/Sha256.java

cat displays the saved file without editing it. Confirm that the new loop is inside readIntoDigest. If you still see the throw, return to the correct file tab, choose File → Save, confirm the saved state and repeat the read.

Saved Sha256 method in the duplicates workspace

In EXPLORER, DUPLICATES → src → Sha256.java identifies the file. The readIntoDigest breadcrumb locates the method, and the tab's close × indicates the saved state. Keep the prepared outer digest method; change only the stream-reading body.

Now run the real caller again:

./run.sh Inventory data/gallery reports/inventory.tsv

Expect Inventory complete: 11 files. Read the actual result:

cat reports/inventory.tsv

The TSV header is PATH, SIZE, SHA256, with tabs separating columns. The two empty-file rows have size zero and a valid digest; tail.bin and nested/tail-copy.bin each have size 4099. The copies must have equal digests. This observation now exercises your saved implementation, not just a successful compilation.

Use the prepared command-line SHA-256 tool as an independent comparison. sha256sum reads each named file and prints its fingerprint followed by its path:

sha256sum data/gallery/empty-a data/gallery/tail.bin

Compare each printed fingerprint with its corresponding inventory row. The empty file must match, and so must the partial final buffer of tail.bin. Rerunning Sample would not detect an incorrect update count; these two current digest values do.

If compilation fails, read the named source file and line in the terminal. Open that file and fix that local edit: for example, a missing semicolon can produce ';' expected. Choose File → Save, confirm the saved state and use cat src/Sha256.java before rerunning the launcher. A failed build is not a digest result. If you need to restart this source edit, cp work/Sha256.java.before-edit src/Sha256.java restores only the unfinished source; reopen it and repeat the method edit. It never restores or overwrites data or reports. Do not rerun setup or reset the VM. An incorrect working directory is fixed with the absolute cd above.

Save Every Path and Its Fingerprint

In this step, you will interpret the complete inventory and recover from an ordinary input-path error. Continue in the same WebIDE integrated terminal at /home/labex/project/duplicates; keep the source edit you already tested.

Read the current report again:

cat reports/inventory.tsv

Compare the rows for red.png, copies/renamed.png and copies/third.png: all three paths share a digest. blue.png has the same size but a different digest. Both empty files have the same valid digest. tail.bin and its 4099-byte copy match after a partial final read. Same-name north/readme.txt and south/readme.txt remain different rows with different content. A filename or size can help select candidates, but neither proves equality. A later lab will also confirm actual bytes after selecting digest candidates.

Open Inputs.java to identify its supporting boundaries, rather than rewriting it. Inputs.files walks only the supplied directory, closes its directory stream and retains every ordinary-file path. It rejects symbolic links, special files and paths containing tabs or newlines instead of following or silently skipping them. Different hardlinked paths remain separate paths. Stable ordinary files/directories with spaces and UTF-8 names are supported; do not change inputs during a scan. Attribute checks detect some size, modification-time or identity changes, but do not freeze the tree or create a concurrent snapshot.

In Inventory.java, each relative path, size and digest becomes a report row only after the read succeeds. root.relativize(path) makes the path relative to the scan root. The prepared Inputs.write creates a new sibling temporary file, writes UTF-8, and replaces the report directory entry. It never opens an existing report hardlink for writing, which protects original bytes even through that link. This is not a crash-durability guarantee. Keep the output outside the scanned root so it cannot become its own input.

The next command intentionally uses a missing root. Expect Scan failed, exit status 2, and no new successful report. A failed scan differs from a successful scan with zero results; any older report is not current evidence.

./run.sh Inventory data/missing reports/missing.tsv

Correct only the input path and write a fresh report:

./run.sh Inventory data/gallery reports/recovered.tsv

Expect Inventory complete: 11 files. Read failures follow the same rule: keep the error visible and return failure, rather than omitting unreadable files and announcing a complete result.

The checks exercise your current source on isolated empty, short and partial-buffer inputs, including a 1025-byte fixture, and read the real inventory. A saved report cannot excuse a subsequently broken digest. All originals must keep their paths, bytes, permissions, ownership, modification times and file identities; never delete or replace them. Ordinary reading may update access times. The complete candidate inventory is now ready for actual byte confirmation.

Summary

You read binary files with try-with-resources, updated SHA-256 with the actual read count, and saved a complete UTF-8 inventory. Names, sizes and digests are candidate facts; actual byte comparison will confirm duplicate groups.