Class OperatorFileWriter
Why it cannot overwrite operator content. Every call declares the paths it owns (OwnedPaths).
The gate reads the file at write time through ConfigDocument.load(Path), applies the caller's edit
to that fresh document, renders the whole document, and then verifies the result instead of trusting
the renderer:
- the rendered text parses back to exactly the edited values, and outside the owned value paths the edited values equal the file's;
- owned spans are computed on both sides from SnakeYAML node marks: a value path's span runs from its
key's line to the last line of its value, plus the comment lines directly above the key when the key
is inserted or removed or its comment is owned; an owned comment's span is those comment lines - or,
for a framework comment run (
OwnedPaths.Builder.frameworkComment(List, int)), only the run's last lines, so the comment lines above the run are unowned and must come out unchanged (#604); an inserted key whose nearest key in the file is a section the operator left with no value (messages:, nothing after the colon but a comment) also owns that section's one line, and the section's value may change only from nothing to a mapping holding exactly the inserted keys - the line keeps the key as written, the comment text and its line terminator, and only its whitespace may be normalized: a space before the colon, the spaces before the comment, and trailing spaces after the comment or at the end of a line without one (#620; owned-span rule revision 3, maintainer decision 2026-10-06). A section written as an empty value (~,null,{}) is the operator's value and is never owned this way; - the lines outside the owned spans - each compared as text including its line terminator - are the same sequence on both sides (the line diff restricted to unowned lines; stricter than a plain longest-common-subsequence diff, which could match an unowned line against an owned one);
- byte-order mark and final line break are unchanged;
- no owned span shares a line with a node of a path it does not own (a flow collection holding an owned and an unowned key on one line is refused, not partially rewritten).
EmitterException
for a layout it cannot emit, for example) is refused the same way (#624). If any check fails nothing is written,
one WARNING names the absolute file, the keys the write would have changed (a key below a secret-shaped key
is redacted) and the reason - never a value - and the caller keeps its in-memory value. Layout is never
normalized and no operator value, key or comment is repaired to make a write pass.
When the caller passes the fingerprint of the bytes it last read and the file no longer holds them, the
outcome is OperatorFileWriter.Outcome.FILE_CHANGED. Immediately before publishing, after rendering and verifying, the
gate reads the target again - after the new content is staged, under the lock every load and write holds - and
abandons the write when the bytes moved; the residual window between that read and the atomic rename cannot be
closed between an editor and the JVM. Publication goes through
AtomicConfigWriter; a file the gate read absent is created exclusively (a hard link of the staged file,
or a re-check and a move that does not replace where links are refused), so a file that appeared meanwhile
is never replaced. Files that are unreadable or unparseable are never written.
- Since:
- 6.3.0
-
Nested Class Summary
Nested ClassesModifier and TypeClassDescriptionprivate static final classThe owned paths the edit actually changed: values (inserted, set or removed) and comments.private static final classThe sections an insert finds empty (#620, #610 item 2).static enumWhat a write did.static final classThe outcome of one write, its one-phrase reason, and the document now on disk when there is one.private static final classOne side of the comparison: its document, node tree, lines, nodes by line and owned-line marks.private static final classThe file as the gate read it at write time, or why the write cannot start.static final classA verified write whose content is staged and not yet published - or, when nothing needed staging, the write's settled result.(package private) static enumThe steps of a gated write that call into the YAML library, as the fault seam names them. -
Field Summary
FieldsModifier and TypeFieldDescriptionstatic final StringThe expected fingerprint of a file that was absent when the caller read it.private static final Stringprivate static final Stringprivate static final Stringprivate static final String(package private) static final AtomicReference<Consumer<OperatorFileWriter.Step>> Test seam: called with eachOperatorFileWriter.Stepof every gated write, so a test can throw a runtime failure there as the YAML library would; empty in production.private static final Loggerprivate static final Stringprivate static final StringFiles already named in an anchored-file warning this server run (canonical paths). -
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionprivate static OperatorFileWriter.Resultprivate static voidcollect(org.yaml.snakeyaml.nodes.Node node, List<Object> path, Map<Integer, List<List<Object>>> result, List<String> lines) private static intcommentLineCount(ConfigDocument document, List<String> path) How many lines directly above the key atpathare its own comment indocument: the comment linesConfigDocument.blockComment(List)reports, without the blank lines above them (those are kept by every comment write).private static booleancommentOrBlank(List<String> lines, int from, int to) Whether every line in[from, to)is a comment line or blank;falsewhen out of range.private static StringcompareUnowned(OperatorFileWriter.Side left, OperatorFileWriter.Side right) Check 3: the unowned lines of both sides are the same sequence, each line with its terminator.private static org.yaml.snakeyaml.nodes.Nodeprivate static booleanprivate static Stringdecode(byte[] bytes) Key names for a warning: a key below a secret-shaped key is redacted; comment paths are marked.private static booleanendsWithLineBreak(String text) private static OperatorFileWriter.Resultfail(OperatorFileWriter.Outcome outcome, Path absolute, OwnedPaths owned, List<String> keys, String reason) private static voidfault(OperatorFileWriter.Step step) private static org.yaml.snakeyaml.nodes.NodeTupleThe tuple of the key atpath(the last duplicate wins, as on read), ornull.private static booleanhasByteOrderMark(String text) private static Stringidentity(org.yaml.snakeyaml.nodes.Node key) private static booleanisFlow(org.yaml.snakeyaml.nodes.Node node) private static booleanisLineBreak(String text) private static intThe last line ofnodeinlines.Splitstextafter every line break SnakeYAML counts (\r\n,\r,\n, U+0085, U+2028, U+2029), keeping the break with its line, so a node mark's line indexes this list.private static Objectprivate static StringmarkExpandedSection(OperatorFileWriter.Side left, OperatorFileWriter.Side right, List<String> section, boolean leftBom, boolean rightBom) The owned span of an expanded section (OperatorFileWriter.EmptySections, #620): its key line on both sides and nothing else - the inserted keys below it are marked as inserted values bymarkSpans(com.ultikits.ultitools.config.document.OperatorFileWriter.Side, com.ultikits.ultitools.config.document.OperatorFileWriter.Side, com.ultikits.ultitools.config.document.OperatorFileWriter.Changes, com.ultikits.ultitools.config.document.OwnedPaths).private static StringmarkSpans(OperatorFileWriter.Side left, OperatorFileWriter.Side right, OperatorFileWriter.Changes changes, OwnedPaths owned) Checks 2 and 5: marks every owned span on both sides, refusing a span that shares a line with an unowned key.nodesByLine(org.yaml.snakeyaml.nodes.Node tree, List<String> lines) For every line, the paths of the nodes on it: keys, scalars and flow collections (a list index is an Integer).private static int[]ownedCommentLines(OwnedPaths owned, OperatorFileWriter.Side left, OperatorFileWriter.Side right, List<String> path) The owned comment lines directly above the key atpath, before and after the edit: the whole comment, or - for a framework comment run - the run's length in the file as read and the length of what replaced it.private static ConfigDocumentparseOrNull(String text) private static voidprivate static byte[]readOrNull(Path file) private static voidrecord(org.yaml.snakeyaml.nodes.Node node, List<Object> path, Map<Integer, List<List<Object>>> result, List<String> lines) private static Stringprivate static OperatorFileWriter.Resultprivate static OperatorFileWriter.ResultrefuseAnchored(Path absolute, OwnedPaths owned) An anchored file is refused on every write but named once per file per server run.private static void(package private) static voidForgets which anchored files were named, for tests that start a fresh "server run".private static StringruntimeFailure(OwnedPaths owned, String step, RuntimeException failure) The refusal reason for a runtime failure inside a gated write (#624): the keys, the step that failed and the failure's class, never a value.private static booleansameLine(List<String> before, int b, boolean[] beforeOwned, List<String> after, int a, boolean[] afterOwned) Whether an unowned line is unchanged: equal text including its terminator.private static String[]sectionLineParts(String line, org.yaml.snakeyaml.nodes.Node key, boolean byteOrderMark) A section line split into the text up to the end of its key, the comment after its colon (""when none) and its line terminator;nullparts are never returned.static OperatorFileWriter.Stagedstage(Path file, OwnedPaths owned, String expectedFingerprint, Consumer<ConfigDocument> edit) Aswrite(Path, OwnedPaths, String, Consumer), but stops once the verified content is staged (written and forced to a temporary file beside the target), so several files can be prepared before any of them is replaced.(package private) static OperatorFileWriter.Stagedstage(Path file, OwnedPaths owned, String expectedFingerprint, Consumer<ConfigDocument> edit, AtomicConfigWriter.FileOperations files) private static booleanstartsWith(List<Object> path, List<String> prefix) private static voidtopmostAbsentPrefix(ConfigDocument original, List<String> path) unowned(boolean[] owned) private static booleanusesAnchors(org.yaml.snakeyaml.nodes.Node tree) Anchors, aliases (a node reached twice) and merge keys; an empty document (nulltree) has none.private static booleanusesAnchors(org.yaml.snakeyaml.nodes.Node node, Set<org.yaml.snakeyaml.nodes.Node> seen) private static Stringverify(String text, String rendered, ConfigDocument original, ConfigDocument candidate, OperatorFileWriter.Changes changes, OwnedPaths owned, List<List<String>> expanded) private static StringverifyValues(ConfigDocument original, ConfigDocument candidate, String rendered, List<List<String>> changedValues, List<List<String>> expanded, boolean wholeFile) Check 1: the rendered text holds exactly the edited values, and the edit stayed inside its paths.private static voidprivate static intwithoutSeparatorLines(org.yaml.snakeyaml.nodes.ScalarNode scalar, int start, int end, List<String> lines) wouldChange(OperatorFileWriter.Snapshot snapshot, OwnedPaths owned, Consumer<ConfigDocument> edit) For a write that cannot start because the file changed: what it would have changed in the file as it is now, to name in the warning; empty when that cannot be told.static OperatorFileWriter.Resultwrite(Path file, OwnedPaths owned, String expectedFingerprint, Consumer<ConfigDocument> edit) Applieseditto the file as it is now and publishes the result only when every byte outsideownedstays as it is (see the class description for why this cannot overwrite operator content).(package private) static OperatorFileWriter.Resultwrite(Path file, OwnedPaths owned, String expectedFingerprint, Consumer<ConfigDocument> edit, AtomicConfigWriter.FileOperations files) Aswrite(Path, OwnedPaths, String, Consumer), with the publication steps as a test seam (null: real).private static OperatorFileWriter.Resultwritten(String rendered, ConfigDocument candidate, String read)
-
Field Details
-
ABSENT
The expected fingerprint of a file that was absent when the caller read it.- See Also:
-
LOGGER
-
SECRET_WORDS
-
ANCHORED
- See Also:
-
READING
- See Also:
-
EDITING
- See Also:
-
RENDERING
- See Also:
-
CHECKING
- See Also:
-
COMMENT_NOT_LOCATED
- See Also:
-
WARNED_ANCHORED
Files already named in an anchored-file warning this server run (canonical paths). -
FAULT
Test seam: called with eachOperatorFileWriter.Stepof every gated write, so a test can throw a runtime failure there as the YAML library would; empty in production.
-
-
Constructor Details
-
OperatorFileWriter
private OperatorFileWriter()
-
-
Method Details
-
write
public static OperatorFileWriter.Result write(Path file, OwnedPaths owned, String expectedFingerprint, Consumer<ConfigDocument> edit) throws IOException Applieseditto the file as it is now and publishes the result only when every byte outsideownedstays as it is (see the class description for why this cannot overwrite operator content).- Parameters:
file- the config fileowned- what this write may changeexpectedFingerprint- the SHA-256 (lower-case hex) of the bytes the caller last read, asConfigLoadResult.fingerprint()reports it,ABSENTfor a file that was absent, ornullfor no expectationedit- the change, applied to a freshly read document; it must change only owned paths- Returns:
- the outcome; a refusal or a changed file has already been logged once
- Throws:
IOException- if staging or publishing the verified text fails (the target is then asAtomicConfigWriterleaves it)
-
write
static OperatorFileWriter.Result write(Path file, OwnedPaths owned, String expectedFingerprint, Consumer<ConfigDocument> edit, AtomicConfigWriter.FileOperations files) throws IOException Aswrite(Path, OwnedPaths, String, Consumer), with the publication steps as a test seam (null: real).- Throws:
IOException
-
stage
public static OperatorFileWriter.Staged stage(Path file, OwnedPaths owned, String expectedFingerprint, Consumer<ConfigDocument> edit) throws IOException Aswrite(Path, OwnedPaths, String, Consumer), but stops once the verified content is staged (written and forced to a temporary file beside the target), so several files can be prepared before any of them is replaced.OperatorFileWriter.Staged.commit()then performs the last-moment check - the target must still hold exactly the bytes the write was verified against, read again under the lock every load and write holds - and the replacement or the exclusive creation;OperatorFileWriter.Staged.discard()releases a staged file that will not be committed. Every check of the class description applies unchanged, so a staged write cannot overwrite operator content either: a file saved between staging and commit is kept and the commit reportsOperatorFileWriter.Outcome.FILE_CHANGED.- Parameters:
file- the config fileowned- what this write may changeexpectedFingerprint- as forwrite(Path, OwnedPaths, String, Consumer)edit- the change, applied to a freshly read document; it must change only owned paths- Returns:
- the staged write; when nothing needed staging, it already holds its result (
OperatorFileWriter.Staged.isPending()isfalse) - a refusal or a changed file has then already been logged once - Throws:
IOException- if staging the verified text fails
-
stage
static OperatorFileWriter.Staged stage(Path file, OwnedPaths owned, String expectedFingerprint, Consumer<ConfigDocument> edit, AtomicConfigWriter.FileOperations files) throws IOException - Throws:
IOException
-
wouldChange
private static List<String> wouldChange(OperatorFileWriter.Snapshot snapshot, OwnedPaths owned, Consumer<ConfigDocument> edit) For a write that cannot start because the file changed: what it would have changed in the file as it is now, to name in the warning; empty when that cannot be told. Nothing is written. A newer file using anchors is never edited, not even in memory (review round 2 IN-R2-01), and an edit the YAML library cannot apply names the owned keys instead (#624). -
runtimeFailure
The refusal reason for a runtime failure inside a gated write (#624): the keys, the step that failed and the failure's class, never a value. -
fault
-
written
private static OperatorFileWriter.Result written(String rendered, ConfigDocument candidate, String read) -
fail
private static OperatorFileWriter.Result fail(OperatorFileWriter.Outcome outcome, Path absolute, OwnedPaths owned, List<String> keys, String reason) -
refuseAnchored
An anchored file is refused on every write but named once per file per server run. -
resetAnchorWarnings
static void resetAnchorWarnings()Forgets which anchored files were named, for tests that start a fresh "server run". -
refuse
private static OperatorFileWriter.Result refuse(Path absolute, OwnedPaths owned, List<String> keys, String reason) -
changed
private static OperatorFileWriter.Result changed(Path absolute, OwnedPaths owned, List<String> keys, String reason) -
warn
-
verify
private static String verify(String text, String rendered, ConfigDocument original, ConfigDocument candidate, OperatorFileWriter.Changes changes, OwnedPaths owned, List<List<String>> expanded) -
markExpandedSection
private static String markExpandedSection(OperatorFileWriter.Side left, OperatorFileWriter.Side right, List<String> section, boolean leftBom, boolean rightBom) The owned span of an expanded section (OperatorFileWriter.EmptySections, #620): its key line on both sides and nothing else - the inserted keys below it are marked as inserted values bymarkSpans(com.ultikits.ultitools.config.document.OperatorFileWriter.Side, com.ultikits.ultitools.config.document.OperatorFileWriter.Side, com.ultikits.ultitools.config.document.OperatorFileWriter.Changes, com.ultikits.ultitools.config.document.OwnedPaths). On both sides that line may hold no node of another path, and the rendered line must keep the operator's bytes of it: the text up to the end of the key, the comment after the colon (without trailing spaces), and the line terminator. Only whitespace on the line may change: a space before the colon, the spaces before the comment, and trailing spaces after the comment or at the end of a line without one - every render of such a line writeskey: # comment(gate-1 F2 of plan 17-75 stated this exactly); a last line without a line break gains one, as check 3 allows. -
sectionLineParts
private static String[] sectionLineParts(String line, org.yaml.snakeyaml.nodes.Node key, boolean byteOrderMark) A section line split into the text up to the end of its key, the comment after its colon (""when none) and its line terminator;nullparts are never returned. The comment is everything from the first#after the colon to the end of the line, without trailing spaces. -
markSpans
private static String markSpans(OperatorFileWriter.Side left, OperatorFileWriter.Side right, OperatorFileWriter.Changes changes, OwnedPaths owned) Checks 2 and 5: marks every owned span on both sides, refusing a span that shares a line with an unowned key. -
ownedCommentLines
private static int[] ownedCommentLines(OwnedPaths owned, OperatorFileWriter.Side left, OperatorFileWriter.Side right, List<String> path) The owned comment lines directly above the key atpath, before and after the edit: the whole comment, or - for a framework comment run - the run's length in the file as read and the length of what replaced it. The comment lines above a run are the same count on both sides and unowned, so check 3 requires them unchanged.- Returns:
{before, after}, ornullwhen the run does not fit the comment on either side
-
compareUnowned
Check 3: the unowned lines of both sides are the same sequence, each line with its terminator. -
sameLine
private static boolean sameLine(List<String> before, int b, boolean[] beforeOwned, List<String> after, int a, boolean[] afterOwned) Whether an unowned line is unchanged: equal text including its terminator. One separator is not operator content: the last line of a file without a final line break gains a line break when owned lines are inserted after it (and loses it when the owned lines after it are removed); the line's own text stays byte for byte, and check 4 keeps the file's final line break as it was. -
isLineBreak
-
verifyValues
private static String verifyValues(ConfigDocument original, ConfigDocument candidate, String rendered, List<List<String>> changedValues, List<List<String>> expanded, boolean wholeFile) Check 1: the rendered text holds exactly the edited values, and the edit stayed inside its paths. An expanded section (OperatorFileWriter.EmptySections) heldnulland must now hold nothing but the inserted keys: once they are stripped it is an empty mapping, which is read back as thenullit was. -
strip
-
pruneCreatedMapping
-
lookup
-
containsPath
-
remove
-
topmostAbsentPrefix
-
commentLineCount
How many lines directly above the key atpathare its own comment indocument: the comment linesConfigDocument.blockComment(List)reports, without the blank lines above them (those are kept by every comment write). The file header and a deeper-indented trailing comment of the previous section are not part of a key's comment, so they are never owned through it. -
commentOrBlank
Whether every line in[from, to)is a comment line or blank;falsewhen out of range. -
lastLine
The last line ofnodeinlines. A block scalar (|,>) ends at its last content line: SnakeYAML's end mark lies after the line breaks it consumed, which include the blank or whitespace-only lines that follow a clipped or stripped value; those lines are the operator's separators, not the value, and are never owned. With keep chomping (|+,>+) the blank lines the value keeps stay part of it (owned-span rule revision 1, review round 1 IN-02). -
withoutSeparatorLines
-
isFlow
private static boolean isFlow(org.yaml.snakeyaml.nodes.Node node) -
find
private static org.yaml.snakeyaml.nodes.NodeTuple find(org.yaml.snakeyaml.nodes.Node tree, List<String> path) The tuple of the key atpath(the last duplicate wins, as on read), ornull. -
nodesByLine
private static Map<Integer,List<List<Object>>> nodesByLine(org.yaml.snakeyaml.nodes.Node tree, List<String> lines) For every line, the paths of the nodes on it: keys, scalars and flow collections (a list index is an Integer). -
collect
-
record
-
startsWith
-
unowned
-
usesAnchors
private static boolean usesAnchors(org.yaml.snakeyaml.nodes.Node tree) Anchors, aliases (a node reached twice) and merge keys; an empty document (nulltree) has none. -
usesAnchors
private static boolean usesAnchors(org.yaml.snakeyaml.nodes.Node node, Set<org.yaml.snakeyaml.nodes.Node> seen) -
lines
Splitstextafter every line break SnakeYAML counts (\r\n,\r,\n, U+0085, U+2028, U+2029), keeping the break with its line, so a node mark's line indexes this list. -
hasByteOrderMark
-
endsWithLineBreak
-
compose
-
identity
-
parseOrNull
-
readOrNull
- Throws:
IOException
-
decode
-
describe
Key names for a warning: a key below a secret-shaped key is redacted; comment paths are marked. -
redacted
-