File 0002-compare-improve-tar_time-truncation.patch of Package go-mtree
From 604ab428638cf6515f0734306ca1c6142de1c047 Mon Sep 17 00:00:00 2001
From: Aleksa Sarai <cyphar@cyphar.com>
Date: Tue, 9 Sep 2025 17:55:02 +1000
Subject: [PATCH 02/25] compare: improve tar_time truncation
Rather than parsing the value as a float and then truncating it, just
parse it as an integer in the first place (this also adds some
validation that we are parsing a reasonable-looking value).
While we're at it, add some integration tests for this code to make sure
this quite complicated special-case behaviour doesn't regress.
Signed-off-by: Aleksa Sarai <cyphar@cyphar.com>
---
compare.go | 29 +++++++++----
test/cli/0012trunc.sh | 96 +++++++++++++++++++++++++++++++++++++++++++
2 files changed, 116 insertions(+), 9 deletions(-)
create mode 100644 test/cli/0012trunc.sh
diff --git a/compare.go b/compare.go
index b73f17429edf..81aba20dddbd 100644
--- a/compare.go
+++ b/compare.go
@@ -4,7 +4,6 @@ import (
"encoding/json"
"fmt"
"slices"
- "strconv"
)
// XXX: Do we need a Difference interface to make it so people can do var x
@@ -253,23 +252,35 @@ func compareEntry(oldEntry, newEntry Entry) ([]KeyDelta, error) {
// Make a new tar_time.
if diffs["tar_time"].Old == nil {
- time, err := strconv.ParseFloat(timeStateT.Old.Value(), 64)
- if err != nil {
- return nil, fmt.Errorf("failed to parse old time: %s", err)
+ var (
+ timeSec, timeNsec int64
+ // used to check for trailing characters
+ trailing rune
+ )
+ val := timeStateT.Old.Value()
+ n, _ := fmt.Sscanf(val, "%d.%d%c", &timeSec, &timeNsec, &trailing)
+ if n != 2 {
+ return nil, fmt.Errorf("failed to parse old time: invalid format %q", val)
}
newTime := new(KeyVal)
- *newTime = KeyVal(fmt.Sprintf("tar_time=%d.000000000", int64(time)))
+ *newTime = KeyVal(fmt.Sprintf("tar_time=%d.%9.9d", timeSec, 0))
diffs["tar_time"].Old = newTime
} else if diffs["tar_time"].New == nil {
- time, err := strconv.ParseFloat(timeStateT.New.Value(), 64)
- if err != nil {
- return nil, fmt.Errorf("failed to parse new time: %s", err)
+ var (
+ timeSec, timeNsec int64
+ // used to check for trailing characters
+ trailing rune
+ )
+ val := timeStateT.New.Value()
+ n, _ := fmt.Sscanf(val, "%d.%d%c", &timeSec, &timeNsec, &trailing)
+ if n != 2 {
+ return nil, fmt.Errorf("failed to parse new time: invalid format %q", val)
}
newTime := new(KeyVal)
- *newTime = KeyVal(fmt.Sprintf("tar_time=%d.000000000", int64(time)))
+ *newTime = KeyVal(fmt.Sprintf("tar_time=%d.%9.9d", timeSec, 0))
diffs["tar_time"].New = newTime
} else {
diff --git a/test/cli/0012trunc.sh b/test/cli/0012trunc.sh
new file mode 100644
index 000000000000..a9a745981361
--- /dev/null
+++ b/test/cli/0012trunc.sh
@@ -0,0 +1,96 @@
+#!/bin/bash
+set -ex
+
+name=$(basename $0)
+root="$(dirname $(dirname $(dirname $0)))"
+gomtree=$(go run ${root}/test/realpath/main.go ${root}/gomtree)
+t=$(mktemp -d /tmp/go-mtree.XXXXXX)
+
+echo "[${name}] Running in ${t}"
+
+## Make sure that comparing manifests with tar_time and time are correctly
+## truncated.
+
+pushd ${root}
+mkdir -p ${t}/root
+
+date="2025-09-05T13:05:10" # POSIX format for "touch -d".
+
+echo "less than .5" >${t}/root/lowerhalf
+touch -d "$date.1000" ${t}/root/lowerhalf
+echo "more than .5" >${t}/root/upperhalf
+touch -d "$date.8000" ${t}/root/upperhalf
+echo "no subsecond" >${t}/root/tartime
+touch -d "$date.0000" ${t}/root/tartime
+
+keywords=type,uid,gid,nlink,link,mode,flags,xattr,size,sha256
+
+# Generate regular manifests with time and tar_time.
+${gomtree} -c -k ${keywords},time -p ${t}/root -f ${t}/time.mtree
+${gomtree} -c -k ${keywords},tar_time -p ${t}/root -f ${t}/tartime.mtree
+
+# Make sure that tar_time truncates the value.
+unix="$(date -d ${date} +%s)"
+grep -q "lowerhalf.*tar_time=$unix.000000000" ${t}/tartime.mtree
+grep -q "upperhalf.*tar_time=$unix.000000000" ${t}/tartime.mtree
+grep -q "tartime.*tar_time=$unix.000000000" ${t}/tartime.mtree
+
+# Validation with both manifests should still succeed.
+${gomtree} validate -p ${t}/root -f ${t}/time.mtree
+${gomtree} validate -p ${t}/root -f ${t}/tartime.mtree
+# Manifest comparison should also succeed.
+${gomtree} validate -f ${t}/tartime.mtree -f ${t}/time.mtree
+${gomtree} validate -f ${t}/time.mtree -f ${t}/tartime.mtree
+
+# Truncate the on-disk timestamps manually.
+touch -d "$date.0000" ${t}/root/lowerhalf
+touch -d "$date.0000" ${t}/root/upperhalf
+touch -d "$date.0000" ${t}/root/tartime
+
+# Only the tar_time manifest should succeed.
+(! ${gomtree} validate -p ${t}/root -f ${t}/time.mtree)
+${gomtree} validate -p ${t}/root -f ${t}/tartime.mtree
+${gomtree} validate -k ${keywords},time -p ${t}/root -f ${t}/tartime.mtree
+# ... unless you force the usage of tar_time.
+${gomtree} validate -k ${keywords},tar_time -p ${t}/root -f ${t}/time.mtree
+
+# The same goes for if you generate the manifests and compare them instead.
+${gomtree} -c -k ${keywords},time -p ${t}/root -f ${t}/time-trunc.mtree
+${gomtree} -c -k ${keywords},tar_time -p ${t}/root -f ${t}/tartime-trunc.mtree
+# Comparing time with time should fail ...
+(! ${gomtree} validate -f ${t}/time.mtree -f ${t}/time-trunc.mtree)
+(! ${gomtree} validate -f ${t}/time-trunc.mtree -f ${t}/time.mtree)
+# ... tar_time with tar_time should succeed ...
+${gomtree} validate -f ${t}/tartime.mtree -f ${t}/tartime-trunc.mtree
+${gomtree} validate -f ${t}/tartime-trunc.mtree -f ${t}/tartime.mtree
+# ... old tar_time with new time should succeed ...
+${gomtree} validate -f ${t}/tartime.mtree -f ${t}/time-trunc.mtree
+${gomtree} validate -f ${t}/time-trunc.mtree -f ${t}/tartime.mtree
+# ... and new tar_time against old time should succeed.
+${gomtree} validate -f ${t}/tartime-trunc.mtree -f ${t}/time.mtree
+${gomtree} validate -f ${t}/time.mtree -f ${t}/tartime-trunc.mtree
+
+# Change the timestamp entirely.
+touch -d "1997-03-25T13:40:00" ${t}/root/lowerhalf
+touch -d "1997-03-25T13:40:00" ${t}/root/upperhalf
+touch -d "1997-03-25T13:40:00" ${t}/root/tartime
+
+# Now all validations should fail.
+(! ${gomtree} validate -p ${t}/root -f ${t}/time.mtree)
+(! ${gomtree} validate -p ${t}/root -f ${t}/tartime.mtree)
+(! ${gomtree} validate -k ${keywords},tar_time -p ${t}/root -f ${t}/time.mtree)
+(! ${gomtree} validate -k ${keywords},time -p ${t}/root -f ${t}/tartime.mtree)
+
+# Ditto for generating the manifests and comparing them.
+${gomtree} -c -k ${keywords},time -p ${t}/root -f ${t}/time-change.mtree
+${gomtree} -c -k ${keywords},tar_time -p ${t}/root -f ${t}/tartime-change.mtree
+
+# Try all combinations.
+lefts=( ${t}/{tar,}time{,-trunc}.mtree )
+rights=( ${t}/{tar,}time-change.mtree )
+for left in "${lefts[@]}"; do
+ for right in "${rights[@]}"; do
+ (! ${gomtree} validate -f ${left} -f ${right})
+ (! ${gomtree} validate -f ${right} -f ${left})
+ done
+done
--
2.51.0