Class PosixAttributePreserver
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
-
Nested Class Summary
Nested ClassesModifier and TypeClassDescriptionstatic interfaceCallbacks areplaceInPlace(java.io.File, byte[], com.ultikits.ultitools.utils.PosixAttributePreserver.ReplaceInPlaceListener)caller supplies so each replace-in-place site can log accurate, resource-specific wording for exactly how the operation was declined or degraded -- this shared operation does not know or care whethertargetis a language catalogue or a provenance sidecar, only the caller does. -
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionstatic booleancopyIfSupported(File source, File target, Runnable onSourceAttributesUnreadable, Runnable onPermissionCopyFailure, BiConsumer<String, String> onOwnershipCopyFailure) Copiessource's POSIX permissions and owner/group ontotarget, if the filesystem exposes aPosixFileAttributeViewfor it.private static booleanisOperatorPinnedReadOnly(File file) Returns whetherfileshould be treated as pinned read-only by its operator, and therefore never refreshed byreplaceInPlace(java.io.File, byte[], com.ultikits.ultitools.utils.PosixAttributePreserver.ReplaceInPlaceListener)--falsefor a file that does not exist yet (nothing to pin).static booleanreplaceInPlace(File target, byte[] content, PosixAttributePreserver.ReplaceInPlaceListener listener) Replacestarget's content withcontent, atomically, preserving everything abouttargetan operator may have deliberately set up -- its POSIX permissions/owner/group (seecopyIfSupported(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.
-
Constructor Details
-
PosixAttributePreserver
private PosixAttributePreserver()
-
-
Method Details
-
replaceInPlace
public static boolean replaceInPlace(File target, byte[] content, PosixAttributePreserver.ReplaceInPlaceListener listener) Replacestarget's content withcontent, atomically, preserving everything abouttargetan operator may have deliberately set up -- its POSIX permissions/owner/group (seecopyIfSupported(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 andResourceHashSidecar#writeAllrun the IDENTICAL operation instead of two independent copies of it: check operator-pinned-read-only (refuse if so); check symbolic link (refuse if so -- seePosixAttributePreserver.ReplaceInPlaceListener.onSymbolicLink()for the exact semantics this refusal implements); create a temp file intarget'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 viacopyIfSupported(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); writecontentto the staging file; move it intotarget's place withStandardCopyOption.REPLACE_EXISTINGandStandardCopyOption.ATOMIC_MOVE. The staging file is always cleaned up in afinallyblock, 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 triviallycontent- the exact bytestargetshould contain after a successful calllistener- receives exactly one callback describing why the replacement was declined or degraded, or none at all on an unqualified success- Returns:
trueiftargetnow containscontent;falseif the replacement was refused or failed for any reason --targetis guaranteed unchanged in that case
-
isOperatorPinnedReadOnly
Returns whetherfileshould be treated as pinned read-only by its operator, and therefore never refreshed byreplaceInPlace(java.io.File, byte[], com.ultikits.ultitools.utils.PosixAttributePreserver.ReplaceInPlaceListener)--falsefor 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 withFiles.isWritable(java.nio.file.Path)alone.Files.isWritable(java.nio.file.Path)alone reflects only this PROCESS's effective ability to write, which is unconditionallytrueunder a privileged JVM (root, orCAP_DAC_OVERRIDE) regardless of the file's own mode bits -- a server running privileged would otherwise silently ignore an operator'schmod 0444pin. 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 anyUltiToolsPlugininstance 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) Copiessource's POSIX permissions and owner/group ontotarget, if the filesystem exposes aPosixFileAttributeViewfor it.Round on
PosixAttributePreserver.java:104(threadPRRT_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 yieldsnull, orFiles.getFileAttributeView(java.nio.file.Path, java.lang.Class<V>, java.nio.file.LinkOption...)itself throwsUnsupportedOperationException) means there is genuinely nothing to preserve -- proceeding without copying is correct (the Windows / exotic-filesystem case). The second case (sourceView.readAttributes()throwsIOException, 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 lettargetkeepFile.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 returnsfalsefor 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),targetis left with the JVM's own default attributes and this method returnstruewithout invoking any callback.target- the newly created staging file about to be moved intosource's placeonSourceAttributesUnreadable- invoked (with no arguments) if a POSIX view exists forsourcebut 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) ifsource's permission bits were read successfully but could not be applied totarget; the replacement still proceeds afterward (best-effort)onOwnershipCopyFailure- invoked with(ownerName, groupName)iftarget's owner/group could not be made to matchsource's- Returns:
trueif the replacement may proceed;falseif it must be abandoned becausesource's attributes could not be read, or its ownership could not be replicated
-