bitflicker64 commented on code in PR #3192:
URL: https://github.com/apache/hugegraph/pull/3192#discussion_r3997764805


##########
hugegraph-server/hugegraph-dist/docker/props.awk:
##########
@@ -0,0 +1,263 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# props.awk — read and rewrite Java ".properties" files with the grammar
+# HugeConfig (commons-configuration over JDK Properties) applies, so the
+# entrypoint and the server agree on what a mounted file means.  grep/sed
+# rewrites do not: they see `\`-escaped keys, `:` separators, continuation
+# lines and duplicate definitions differently, which is how a mounted
+# config ends up with two definitions of one key.
+#
+# One invocation, selected with the `PROPS_MODE` environment variable:
+#
+#   PROPS_MODE=get  PROPS_KEY=K PROPS_FILE=F
+#       print the value of K's first logical definition
+#   PROPS_MODE=set  PROPS_KEY=K PROPS_FILE=F
+#       replace K's first definition in place, drop every other
+#       definition of K, append one when the file has none.  The new
+#       value arrives pre-encoded in PROPS_VALUE_ENCODED (an environment
+#       variable, so secrets never appear in `ps` output or in awk's
+#       argv), and -v is not used for it so awk cannot mangle its
+#       backslash escapes.
+#
+# Grammar implemented (java.util.Properties line reader + the
+# first-definition-wins rule Configuration.getString applies):
+#   - '#' / '!' comments and blank lines
+#   - '=' / ':' / whitespace separators, with whitespace then an optional
+#     single '=' or ':' accepted as one separator
+#   - continuations: a physical line ending in an odd number of
+#     backslashes joins the next line (its leading whitespace stripped)
+#   - backslash escapes in keys and values, including \uXXXX
+#   - duplicate logical keys resolve to the first definition
+#
+# Rewrites keep every untouched line byte-for-byte (comments, blank
+# lines, unrelated entries), and replace the first definition where it
+# stands, so mounted configs stay reviewable in git diffs.
+
+function die(msg) {
+    printf "props.awk: %s\n", msg > "/dev/stderr"
+    exit 1
+}
+
+function hex_digit(c) {
+    return index("0123456789abcdef", tolower(c)) - 1
+}
+
+# \uXXXX is a UTF-16 code unit in Java.  Values here are effectively
+# ISO-8859-1, so codes above 0xFF are kept as their literal escape text
+# rather than being mangled through a single-byte sprintf.
+function unescape(s,    out, i, n, c, code, j, d, ok) {
+    out = ""
+    n = length(s)
+    for (i = 1; i <= n; i++) {
+        c = substr(s, i, 1)
+        if (c != "\\") { out = out c; continue }
+        if (i == n) break
+        i++
+        c = substr(s, i, 1)
+        if (c == "u" && i + 4 <= n) {
+            code = 0
+            ok = 1
+            for (j = 1; j <= 4; j++) {
+                d = hex_digit(substr(s, i + j, 1))
+                if (d < 0) { ok = 0; break }
+                code = code * 16 + d
+            }
+            if (ok) {
+                i += 4
+                if (code <= 255) out = out sprintf("%c", code)
+                else out = out substr(s, i - 5, 6)
+                continue
+            }
+        }
+        if (c == "t") out = out "\t"
+        else if (c == "n") out = out "\n"
+        else if (c == "r") out = out "\r"
+        else if (c == "f") out = out "\f"
+        else out = out c
+    }
+    return out
+}
+
+# A physical line is continued when it ends in an odd number of
+# backslashes (an even count escapes itself).
+function trailing_backslashes(s,    n, k) {
+    n = length(s)
+    k = 0
+    while (k < n && substr(s, n - k, 1) == "\\") k++
+    return k
+}
+
+function is_skipped(raw) {
+    return raw ~ /^[ \t]*([#!]|$)/
+}
+
+# Split a logical line into its raw (still-escaped) key and value parts.
+# Results land in K_RAW / V_RAW because awk returns one value.
+function split_kv(s,    n, i, c, esc, sep_at, rest) {
+    n = length(s)
+    esc = 0
+    sep_at = 0
+    for (i = 1; i <= n; i++) {
+        c = substr(s, i, 1)
+        if (esc) { esc = 0; continue }
+        if (c == "\\") { esc = 1; continue }
+        if (c == "=" || c == ":" || c == " " || c == "\t") { sep_at = i; break 
}
+    }
+    if (sep_at == 0) {
+        K_RAW = s
+        V_RAW = ""
+        return
+    }
+    K_RAW = substr(s, 1, sep_at - 1)
+    rest = substr(s, sep_at)
+    c = substr(rest, 1, 1)
+    if (c == "=" || c == ":") {
+        rest = substr(rest, 2)
+    } else {
+        sub(/^[ \t]+/, "", rest)
+        c = substr(rest, 1, 1)
+        if (c == "=" || c == ":") rest = substr(rest, 2)
+    }
+    sub(/^[ \t]+/, "", rest)
+    V_RAW = rest
+}
+
+function shquote(s) {
+    gsub(/'/, "'\\''", s)
+    return "'" s "'"
+}
+
+# Load `file` into per-block arrays: one block per comment/blank line or
+# logical entry, spanning exactly the physical lines it occupies.
+function props_load(file,    raw, rc, nl, stripped, next_raw, start, logical) {
+    NLINES = 0
+    while ((rc = (getline raw < file)) > 0) {
+        NLINES++
+        RAW[NLINES] = raw
+    }
+    if (rc == -1)
+        die("cannot read " file)
+    close(file)
+
+    NBLOCK = 0
+    for (nl = 1; nl <= NLINES; nl++) {
+        raw = RAW[nl]
+        # CRLF: java.util.Properties drops the line terminator, so one
+        # trailing CR is stripped for parsing only.  RAW[] keeps the byte
+        # so props_set replays untouched lines byte-for-byte.
+        stripped = raw
+        sub(/\r$/, "", stripped)
+        if (is_skipped(stripped)) {
+            NBLOCK++
+            BTYPE[NBLOCK] = "skip"
+            BFIRST[NBLOCK] = nl
+            BLAST[NBLOCK] = nl
+            continue
+        }
+        start = nl
+        logical = stripped
+        while (trailing_backslashes(logical) % 2 == 1 && nl < NLINES) {
+            logical = substr(logical, 1, length(logical) - 1)
+            nl++
+            next_raw = RAW[nl]
+            sub(/\r$/, "", next_raw)
+            sub(/^[ \t]+/, "", next_raw)
+            logical = logical next_raw
+        }
+        # java.util.Properties ignores whitespace before the key; strip it
+        # so split_kv's separator scan agrees (an indented key used to be
+        # read as a key whose name started with a space, and a set then
+        # appended a second definition of the real key).
+        sub(/^[ \t]+/, "", logical)
+        split_kv(logical)
+        NBLOCK++
+        BTYPE[NBLOCK] = "entry"
+        BFIRST[NBLOCK] = start
+        BLAST[NBLOCK] = nl
+        BKEY[NBLOCK] = unescape(K_RAW)
+        # Values stay in their on-disk escaped form.  get Prop callers feed
+        # the result straight back into set, which would corrupt a decoded
+        # value by re-writing its backslashes as literals; keys are
+        # unescaped because they are matched against plain names.
+        BVAL[NBLOCK] = V_RAW
+    }
+}
+
+function props_set(file, key, enc_val,    tmp, cmd, b, first, ln) {
+    props_load(file)
+    first = 0
+    for (b = 1; b <= NBLOCK; b++) {
+        if (BTYPE[b] == "entry" && BKEY[b] == key) {
+            if (first == 0) first = b
+            else BDROP[b] = 1
+        }
+    }
+    # Atomic rewrite: the original is never truncated.  Everything lands
+    # in a sibling temp file that is closed and renamed over the original.
+    tmp = file ".tmp"
+    for (b = 1; b <= NBLOCK; b++) {
+        if (BDROP[b]) continue
+        if (b == first) {
+            printf "%s=%s\n", key, enc_val > tmp
+        } else {
+            for (ln = BFIRST[b]; ln <= BLAST[b]; ln++)
+                print RAW[ln] > tmp
+        }
+    }
+    if (first == 0)
+        printf "%s=%s\n", key, enc_val > tmp
+    close(tmp)
+    cmd = "mv -- " shquote(tmp) " " shquote(file)

Review Comment:
   ⚠️ Swapping the temp file in with `mv` replaces the config's inode. Before 
this change, `printf >>` kept it, and so did the mode-preserving `sed -i`. 
Three things change as a result.
   
   1. Permissions are lost. Measured at this head, with the key absent:
   
   ```
   $ printf 'a=1\n' > f.properties && chmod 600 f.properties
   $ PROPS_MODE=set PROPS_KEY=auth.admin_pa PROPS_VALUE_ENCODED=s3cret 
PROPS_FILE=f.properties awk -f props.awk /dev/null
   $ ls -l f.properties
   -rw-r--r--  f.properties      # was -rw-------, now holds auth.admin_pa
   ```
   
   The `.tmp` file is created with the default umask, so the secret sits in a 
world-readable file even before the rename.
   
   2. A symlinked config (`PROPS_FILE=link.properties`) is replaced by a 
regular file, and the target it pointed to is left unchanged.
   
   3. A config bind-mounted as a single file (`-v 
./rest-server.properties:/hugegraph-server/conf/rest-server.properties`) cannot 
be renamed over, because rename(2) returns EBUSY on a mount point. `die()` then 
exits 1, and under `set -euo pipefail` the entrypoint aborts. Before this 
change, the append path (`printf >>`) worked there for keys that were absent, 
such as `init_store.enabled` or `restserver.url`. Mounted configs are the case 
this PR targets.
   
   Requested change: once `tmp` is fully written and closed, copy it back onto 
the original instead of renaming it, e.g. `cmd = "cat -- " shquote(tmp) " > " 
shquote(file) " && rm -f -- " shquote(tmp)`. That keeps the inode, mode, 
symlink and bind mount. Because the new content is complete before the original 
is truncated, it also keeps most of the protection the temp file was added for. 
Please also add a unit case that checks a 0600 file keeps its mode after 
`set_prop`.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to