Class PosixAttributePreserver

java.lang.Object
com.ultikits.ultitools.utils.PosixAttributePreserver

@Internal public final class PosixAttributePreserver extends Object
Shared implementation of the "replace a file in place, preserving what the operator set up" operation used by every createTempFile + atomic-Files.move(java.nio.file.Path, java.nio.file.Path, java.nio.file.CopyOption...) replace-in-place site in this framework.

Round following PosixAttributePreserver.java:104 (thread PRRT_kwDOIcF9Es6i2I49, symlink finding on ResourceHashSidecar.java:320, P2): this class originally covered only POSIX attribute preservation (copyIfSupported(java.io.File, java.io.File, java.lang.Runnable, java.lang.Runnable, java.util.function.BiConsumer<java.lang.String, java.lang.String>)), extracted from UltiToolsPlugin#writeBytes in round 3 so ResourceHashSidecar#writeAll could reuse it. That left the OTHER properties of the same operation -- the operator-pinned-read-only refusal and the symlink refusal -- copied only into writeBytes, so a reviewer kept finding sibling gaps in the sidecar's path one property at a time across four rounds: attribute preservation (round 3), an unreadable-attributes edge case (round 6), and finally symlink handling. replaceInPlace(java.io.File, byte[], com.ultikits.ultitools.utils.PosixAttributePreserver.ReplaceInPlaceListener) now owns the WHOLE operation -- both refusal checks, the attribute-preservation step, the atomic write itself, and cleanup -- so a third replace-in-place site gets every property for free by calling one method, instead of a reviewer (or a human) having to notice each property was missing one at a time. copyIfSupported(java.io.File, java.io.File, java.lang.Runnable, java.lang.Runnable, java.util.function.BiConsumer<java.lang.String, java.lang.String>) remains public and unchanged for any caller that only needs the attribute-copy step in isolation; replaceInPlace(java.io.File, byte[], com.ultikits.ultitools.utils.PosixAttributePreserver.ReplaceInPlaceListener) calls it internally as one part of the larger operation.

File.createTempFile(java.lang.String, java.lang.String, java.io.File) always creates its staging file with THIS process's own default owner and a restrictive default mode, so a naive replace silently strips a hardened or shared-ownership file's permissions and identity the moment it is legitimately refreshed. This was originally fixed only inside UltiToolsPlugin#writeBytes for language catalogues (Codex rounds 8 and 9, P2, discussion_r4012703529 and discussion_r4013501574).

Permission bits are best-effort: a failure to read or apply them only invokes onPermissionCopyFailure so the caller can warn, and the replacement still proceeds. Owner/group are NOT best-effort: an unprivileged JVM process can generally chown a file's GROUP to one it already belongs to, but never its USER owner to a different user at all -- so a failure to match BOTH after the attempt makes copyIfSupported(java.io.File, java.io.File, java.lang.Runnable, java.lang.Runnable, java.util.function.BiConsumer<java.lang.String, java.lang.String>) return false. Every caller MUST treat that as "abandon the whole replacement" (never move the staging file into place), the same conservative choice already made for an operator-pinned read-only file or a symlinked target -- see replaceInPlace(java.io.File, byte[], com.ultikits.ultitools.utils.PosixAttributePreserver.ReplaceInPlaceListener)'s own javadoc for the full contract.

public only so UltiToolsPlugin (a different package) and ResourceHashSidecar can share it -- internal framework plumbing, not a documented capability (same rationale as ResourceHashSidecar's own class javadoc): it appears nowhere in FEATURES.md as its own row, and this milestone's own D-09 decision rejects shipping new public surface in 6.3.0 for exactly this reason.

Since:
6.3.0
  • Constructor Details

    • PosixAttributePreserver

      private PosixAttributePreserver()
  • Method Details

    • replaceInPlace

      public static boolean replaceInPlace(File target, byte[] content, PosixAttributePreserver.ReplaceInPlaceListener listener)
      Replaces target's content with content, atomically, preserving everything about target an operator may have deliberately set up -- its POSIX permissions/owner/group (see copyIfSupported(java.io.File, java.io.File, java.lang.Runnable, java.lang.Runnable, java.util.function.BiConsumer<java.lang.String, java.lang.String>)), whether it has been pinned read-only, and whether it is a symbolic link.

      This is UltiToolsPlugin#writeBytes's own algorithm, generalized so both the language-file path and ResourceHashSidecar#writeAll run the IDENTICAL operation instead of two independent copies of it: check operator-pinned-read-only (refuse if so); check symbolic link (refuse if so -- see PosixAttributePreserver.ReplaceInPlaceListener.onSymbolicLink() for the exact semantics this refusal implements); create a temp file in target's own parent directory (creating that directory first if it does not exist yet, needed for a sidecar's very first write); copy POSIX attributes onto the staging file via copyIfSupported(java.io.File, java.io.File, java.lang.Runnable, java.lang.Runnable, java.util.function.BiConsumer<java.lang.String, java.lang.String>) (abandon on failure); write content to the staging file; move it into target's place with StandardCopyOption.REPLACE_EXISTING and StandardCopyOption.ATOMIC_MOVE. The staging file is always cleaned up in a finally block, whether the move succeeded (a no-op, since a successful move already renamed it away) or any guard above aborted first.

      Parameters:
      target - the file to replace; if it does not exist yet, this is treated as the caller's first write and the read-only/symlink checks both pass trivially
      content - the exact bytes target should contain after a successful call
      listener - receives exactly one callback describing why the replacement was declined or degraded, or none at all on an unqualified success
      Returns:
      true if target now contains content; false if the replacement was refused or failed for any reason -- target is guaranteed unchanged in that case
    • isOperatorPinnedReadOnly

      private static boolean isOperatorPinnedReadOnly(File file)
      Returns whether file should be treated as pinned read-only by its operator, and therefore never refreshed by replaceInPlace(java.io.File, byte[], com.ultikits.ultitools.utils.PosixAttributePreserver.ReplaceInPlaceListener) -- false for a file that does not exist yet (nothing to pin).

      replaceInPlace(java.io.File, byte[], com.ultikits.ultitools.utils.PosixAttributePreserver.ReplaceInPlaceListener)'s atomic move replaces a DIRECTORY ENTRY, which on POSIX only ever consults the containing directory's write permission, never the target file's own -- so an untouched file the operator made read-only (a deliberate hardening signal) would otherwise be silently overwritten anyway if checked with Files.isWritable(java.nio.file.Path) alone.

      Files.isWritable(java.nio.file.Path) alone reflects only this PROCESS's effective ability to write, which is unconditionally true under a privileged JVM (root, or CAP_DAC_OVERRIDE) regardless of the file's own mode bits -- a server running privileged would otherwise silently ignore an operator's chmod 0444 pin. This method therefore also inspects the raw POSIX write bits directly as a fallback signal, independent of what this particular process happens to be privileged to do: if none of owner/group/other carries write permission, the operator's intent is unambiguous. A non-POSIX filesystem, or any failure reading the permissions, falls back to the (already passed) process-relative answer -- this fallback only ever STRENGTHENS the read-only determination, never weakens it.

      Moved here verbatim from UltiToolsPlugin#isOperatorPinnedReadOnly (Codex rounds 8 and 10) as part of consolidating the whole replace-in-place operation into one place -- this method never used any UltiToolsPlugin instance state, so the move changes nothing about its behaviour.

      Parameters:
      file - the file about to be replaced
      Returns:
      whether the replacement must be refused because the file is operator-pinned read-only
    • copyIfSupported

      public static boolean copyIfSupported(File source, File target, Runnable onSourceAttributesUnreadable, Runnable onPermissionCopyFailure, BiConsumer<String,String> onOwnershipCopyFailure)
      Copies source's POSIX permissions and owner/group onto target, if the filesystem exposes a PosixFileAttributeView for it.

      Round on PosixAttributePreserver.java:104 (thread PRRT_kwDOIcF9Es6i12fM, P2): "no POSIX view for this filesystem" and "a POSIX view exists but reading it failed" are deliberately kept as two separate failure modes with two separate outcomes, not folded into one catch as an earlier version of this method did. The first case (the view lookup yields null, or Files.getFileAttributeView(java.nio.file.Path, java.lang.Class<V>, java.nio.file.LinkOption...) itself throws UnsupportedOperationException) means there is genuinely nothing to preserve -- proceeding without copying is correct (the Windows / exotic-filesystem case). The second case ( sourceView.readAttributes() throws IOException, e.g. a transient NFS error) means the opposite: a POSIX identity DOES exist to preserve, and this call could not learn what it was. Treating that the same as "nothing to preserve" would silently let target keep File.createTempFile(java.lang.String, java.lang.String, java.io.File)'s process-default identity -- reopening, on this error path, exactly the hole the ownership-preservation fix (Codex rounds 8/9) closed on the happy path. This method therefore returns false for an unreadable source view, the same outcome already returned when ownership cannot be replicated, so the caller's strict "abandon the whole replacement" half runs instead of silently proceeding.

      Parameters:
      source - the file about to be replaced; its attributes are the ones to preserve. If it does not exist yet (the caller's first write -- nothing to copy attributes FROM), target is left with the JVM's own default attributes and this method returns true without invoking any callback.
      target - the newly created staging file about to be moved into source's place
      onSourceAttributesUnreadable - invoked (with no arguments) if a POSIX view exists for source but reading its attributes failed -- the replacement MUST be abandoned; see the class/method javadoc above for why this is distinct from "no POSIX view at all"
      onPermissionCopyFailure - invoked (with no arguments) if source's permission bits were read successfully but could not be applied to target; the replacement still proceeds afterward (best-effort)
      onOwnershipCopyFailure - invoked with (ownerName, groupName) if target's owner/group could not be made to match source's
      Returns:
      true if the replacement may proceed; false if it must be abandoned because source's attributes could not be read, or its ownership could not be replicated