SwadhyAI Notes

All Java notes

Java Files

›Short versionOpen if you only have a minute.
  • A Path or a File names a location. Neither one creates a file or holds its bytes.
  • Files is the NIO.2 class that creates, reads, writes, moves, and deletes, and it throws an IOException that names the reason.
  • A relative path is completed from the process working directory, user.dir, which changes with the way the program was launched.
  • resolve drops its base when the other path is absolute. normalize removes . and .. on the name list, before any startsWith check.
  • A path stays inside a root only after resolve, normalize, and startsWith, in that order. toRealPath() is what sees a symlink that points outside.

A program that keeps notes has to name a place on disk. In current Java that name is a . is the class that does something with the name. The older type from java.io still names a location, and a lot of existing code asks for it.

NIO.2, the file API added in Java 7, split those jobs on purpose. The path is data. The operation is a method that can fail in a specific way. The bytes of the file are a third thing, and they show up only when a later page reads or writes them.

A name, then a question

This builds a path from two elements and asks whether anything is at that location. The file, if it appears, appears because some earlier run created it.

Main.java
import java.nio.file.Files;
import java.nio.file.Path;

public class Main {
    public static void main(String[] args) {
        Path notes = Path.of("notes", "today.txt");

        System.out.println(notes.toAbsolutePath());
        System.out.println(Files.exists(notes));
        System.out.println(Files.isRegularFile(notes));
        System.out.println(Files.isDirectory(notes));
    }
}

Path.of joins the elements with the separator this operating system uses. toAbsolutePath() prefixes the . Run the class from /Users/me/app and the absolute form is /Users/me/app/notes/today.txt.

Terminal
$ pwd
/Users/me/app
$ java Main
/Users/me/app/notes/today.txt
false
false
false

Three falses means the path names nothing yet. exists is true for a file or a directory. isRegularFile is the check you want before treating the path as a document. A directory can exist at the same name.

The same questions on a File are exists(), isFile(), and isDirectory(). They return booleans. When you need the failure’s name, stay with Files.

Absolute, relative, and ..

An is already anchored. A relative path is a suffix waiting for a base. resolve attaches that suffix, with the exception above: an absolute argument replaces the base.

is the step that makes .. visible to a later check. This method is the guard for a folder your program owns. The caller passes a user string. The method returns a path that still begins with the root, or it throws.

InsideRoot.java
import java.nio.file.Path;

static Path insideRoot(Path root, String userInput) {
    Path base = root.toAbsolutePath().normalize();
    Path candidate = base.resolve(userInput).normalize();

    if (!candidate.startsWith(base) || candidate.equals(base)) {
        throw new IllegalArgumentException("path escapes the root");
    }
    return candidate;
}

startsWith compares name elements. It is a path-prefix test. The string /app/uploads-extra does not start with the path /app/uploads, because uploads-extra and uploads are different names. Skipping normalize defeats the test for .., which is the question under this block.

candidate.equals(base) refuses the root itself. A delete or a replace aimed at the root would take the whole tree. The equal path has already survived startsWith, since a path starts with itself.

This check is lexical. A symlink inside the root can still point at a directory outside it. toRealPath() resolves links and .. against the disk, and it throws if the path is missing. For a file that already exists, normalize, then toRealPath(), then startsWith on the real root. Creating a new name has to stop at the parent, because the new file has no real path yet. The create and delete pages use those two forms.

Path.of("/app/uploads").resolve("../../etc/passwd") is checked with startsWith before normalize. Does the check accept it?

Yes. The unresolved name list still begins with app and uploads, and startsWith only compares that leading sequence. normalize() rewrites the path to /etc/passwd. The same startsWith then rejects it. The order is resolve, then normalize, then startsWith.

What the later pages add

Creating a file is a separate request from naming it. Writing then has to choose a charset, and a crash during that write can leave a short file, which is why a finished text file is published with an atomic move. Reading decodes those same bytes. Deleting a directory removes nothing until the directory is empty, unless you walk the tree and delete children first.