Skip to content

Commit e5c6c6f

Browse files
authored
Fix inode time stamps. (#334)
* Cast values to those that are outlined in the kernel docs: - Seconds as signed int32 - Extension as unsigned int32 * Decode the timestamp using the formula provided by the kernel doc. * Add function to encode timestamps using the inverse of the kernel doc formula and write them to their targets. * Expand `TestChtimes` to include all valid time ranges outlined in the kernel docs.
1 parent c384667 commit e5c6c6f

File tree

2 files changed

+101
-68
lines changed

2 files changed

+101
-68
lines changed

filesystem/ext4/ext4_test.go

Lines changed: 58 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -557,36 +557,65 @@ func TestChtimes(t *testing.T) {
557557
if err != nil {
558558
t.Fatalf("error opening file %s: %v", newfile, err)
559559
}
560-
fileImpl, ok := fileIntf.(*File)
561-
if !ok {
562-
t.Fatalf("could not cast to ext4.File")
563-
}
564-
ctime := fileImpl.createTime.Add(-30 * time.Minute)
565-
atime := fileImpl.accessTime.Add(60 * time.Minute)
566-
mtime := fileImpl.modifyTime.Add(120 * time.Minute)
560+
fileIntf.Close()
567561

568-
if err := fs.Chtimes(newfile, ctime, atime, mtime); err != nil {
569-
t.Fatalf("error changing times on file %s: %v", newfile, err)
570-
}
571-
// now check that it was updated
572-
// get existing times
573-
fileIntf, err = fs.OpenFile(newfile, os.O_RDONLY)
574-
if err != nil {
575-
t.Fatalf("error opening file %s: %v", newfile, err)
576-
}
577-
// ext4 only supports second-level time resolution
562+
// ext4 supports 34-bit seconds and 30-bit nanoseconds
563+
// We use 91 nanoseconds because it has the lowest 2 bits set (binary 1011011),
564+
// which tests the bit-packing logic.
565+
nano := 91
578566

579-
fileImpl, ok = fileIntf.(*File)
580-
if !ok {
581-
t.Fatalf("could not cast to ext4.File")
582-
}
583-
if fileImpl.createTime.Unix() != ctime.Unix() {
584-
t.Errorf("mismatched create time, actual %v expected %v", fileImpl.createTime, ctime)
585-
}
586-
if fileImpl.accessTime.Unix() != atime.Unix() {
587-
t.Errorf("mismatched access time, actual %v expected %v", fileImpl.accessTime, atime)
588-
}
589-
if fileImpl.modifyTime.Unix() != mtime.Unix() {
590-
t.Errorf("mismatched modify time, actual %v expected %v", fileImpl.modifyTime, mtime)
567+
tests := []struct {
568+
name string
569+
t time.Time
570+
}{
571+
{"1901-1969", time.Date(1930, 1, 1, 0, 0, 0, nano, time.UTC)},
572+
{"1970-2038", time.Date(2026, 1, 1, 0, 0, 0, nano, time.UTC)},
573+
{"2038-2106", time.Date(2050, 1, 1, 0, 0, 0, nano, time.UTC)},
574+
{"2106-2174", time.Date(2120, 1, 1, 0, 0, 0, nano, time.UTC)},
575+
{"2174-2242", time.Date(2200, 1, 1, 0, 0, 0, nano, time.UTC)},
576+
{"2242-2310", time.Date(2280, 1, 1, 0, 0, 0, nano, time.UTC)},
577+
{"2310-2378", time.Date(2350, 1, 1, 0, 0, 0, nano, time.UTC)},
578+
{"2378-2446", time.Date(2440, 1, 1, 0, 0, 0, nano, time.UTC)},
579+
}
580+
581+
for _, tt := range tests {
582+
t.Run(tt.name, func(t *testing.T) {
583+
if err := fs.Chtimes(newfile, tt.t, tt.t, tt.t); err != nil {
584+
t.Fatalf("error changing times on file %s: %v", newfile, err)
585+
}
586+
587+
// now check that it was updated
588+
fileIntf, err = fs.OpenFile(newfile, os.O_RDONLY)
589+
if err != nil {
590+
t.Fatalf("error opening file %s: %v", newfile, err)
591+
}
592+
defer fileIntf.Close()
593+
594+
fileImpl, ok := fileIntf.(*File)
595+
if !ok {
596+
t.Fatalf("could not cast to ext4.File")
597+
}
598+
599+
if fileImpl.createTime.Unix() != tt.t.Unix() {
600+
t.Errorf("mismatched create time seconds, actual %d (%v) expected %d (%v)", fileImpl.createTime.Unix(), fileImpl.createTime, tt.t.Unix(), tt.t)
601+
}
602+
if fileImpl.createTime.Nanosecond() != tt.t.Nanosecond() {
603+
t.Errorf("mismatched create time nanoseconds, actual %d expected %d", fileImpl.createTime.Nanosecond(), tt.t.Nanosecond())
604+
}
605+
606+
if fileImpl.accessTime.Unix() != tt.t.Unix() {
607+
t.Errorf("mismatched access time seconds, actual %d (%v) expected %d (%v)", fileImpl.accessTime.Unix(), fileImpl.accessTime, tt.t.Unix(), tt.t)
608+
}
609+
if fileImpl.accessTime.Nanosecond() != tt.t.Nanosecond() {
610+
t.Errorf("mismatched access time nanoseconds, actual %d expected %d", fileImpl.accessTime.Nanosecond(), tt.t.Nanosecond())
611+
}
612+
613+
if fileImpl.modifyTime.Unix() != tt.t.Unix() {
614+
t.Errorf("mismatched modify time seconds, actual %d (%v) expected %d (%v)", fileImpl.modifyTime.Unix(), fileImpl.modifyTime, tt.t.Unix(), tt.t)
615+
}
616+
if fileImpl.modifyTime.Nanosecond() != tt.t.Nanosecond() {
617+
t.Errorf("mismatched modify time nanoseconds, actual %d expected %d", fileImpl.modifyTime.Nanosecond(), tt.t.Nanosecond())
618+
}
619+
})
591620
}
592621
}

filesystem/ext4/inode.go

Lines changed: 43 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -195,31 +195,29 @@ func inodeFromBytes(b []byte, sb *superblock, number uint32) (*inode, error) {
195195
// Thus, the full 64-bit timestamp value is constructed as follows:
196196
// original 32 bits (0:4) are seconds. Add (to the left) 2 more bits from the 32
197197
// the remaining 30 bites are nanoseconds
198-
accessTimeSeconds := uint64(binary.LittleEndian.Uint32(b[0x8:0xc]))
199-
changeTimeSeconds := uint64(binary.LittleEndian.Uint32(b[0xc:0x10]))
200-
modifyTimeSeconds := uint64(binary.LittleEndian.Uint32(b[0x10:0x14]))
201-
createTimeSeconds := uint64(binary.LittleEndian.Uint32(b[0x90:0x94]))
202-
203-
accessTimeExtra := uint64(binary.LittleEndian.Uint32(b[0x8c:0x90]))
204-
changeTimeExtra := uint64(binary.LittleEndian.Uint32(b[0x84:0x88]))
205-
modifyTimeExtra := uint64(binary.LittleEndian.Uint32(b[0x88:0x8c]))
206-
createTimeExtra := uint64(binary.LittleEndian.Uint32(b[0x94:0x98]))
207-
208-
accessTimeHigh := accessTimeExtra & 0x3
209-
changeTimeHigh := changeTimeExtra & 0x3
210-
modifyTimeHigh := modifyTimeExtra & 0x3
211-
createTimeHigh := createTimeExtra & 0x3
212-
213-
accessTimeSeconds = (accessTimeHigh << 32) | accessTimeSeconds
214-
changeTimeSeconds = (changeTimeHigh << 32) | changeTimeSeconds
215-
modifyTimeSeconds = (modifyTimeHigh << 32) | modifyTimeSeconds
216-
createTimeSeconds = (createTimeHigh << 32) | createTimeSeconds
217-
218-
// now get the nanoseconds by using the upper 30 bites
219-
accessTimeNanoseconds := accessTimeExtra >> 2
220-
changeTimeNanoseconds := changeTimeExtra >> 2
221-
modifyTimeNanoseconds := modifyTimeExtra >> 2
222-
createTimeNanoseconds := createTimeExtra >> 2
198+
accessTimeSeconds := int32(binary.LittleEndian.Uint32(b[0x8:0xc]))
199+
changeTimeSeconds := int32(binary.LittleEndian.Uint32(b[0xc:0x10]))
200+
modifyTimeSeconds := int32(binary.LittleEndian.Uint32(b[0x10:0x14]))
201+
createTimeSeconds := int32(binary.LittleEndian.Uint32(b[0x90:0x94]))
202+
203+
accessTimeExtra := binary.LittleEndian.Uint32(b[0x8c:0x90])
204+
changeTimeExtra := binary.LittleEndian.Uint32(b[0x84:0x88])
205+
modifyTimeExtra := binary.LittleEndian.Uint32(b[0x88:0x8c])
206+
createTimeExtra := binary.LittleEndian.Uint32(b[0x94:0x98])
207+
208+
decodeTimestamp := func(seconds int32, extra uint32) (int64, int64) {
209+
// The formula derived from the kernel documentation table is:
210+
// Decoded = int64(int32(lower)) + (int64(extra_bits) << 32)
211+
sec := int64(seconds) + (int64(extra&0x3) << 32)
212+
// Nanoseconds are in the upper 30 bits
213+
nano := int64(extra >> 2)
214+
return sec, nano
215+
}
216+
217+
atimeSec, atimeNano := decodeTimestamp(accessTimeSeconds, accessTimeExtra)
218+
ctimeSec, ctimeNano := decodeTimestamp(changeTimeSeconds, changeTimeExtra)
219+
mtimeSec, mtimeNano := decodeTimestamp(modifyTimeSeconds, modifyTimeExtra)
220+
crtimeSec, crtimeNano := decodeTimestamp(createTimeSeconds, createTimeExtra)
223221

224222
flagsNum := binary.LittleEndian.Uint32(b[0x20:0x24])
225223

@@ -287,10 +285,10 @@ func inodeFromBytes(b []byte, sb *superblock, number uint32) (*inode, error) {
287285
version: binary.LittleEndian.Uint64(version),
288286
inodeSize: binary.LittleEndian.Uint16(b[0x80:0x82]) + minInodeSize,
289287
deletionTime: binary.LittleEndian.Uint32(b[0x14:0x18]),
290-
accessTime: time.Unix(int64(accessTimeSeconds), int64(accessTimeNanoseconds)),
291-
changeTime: time.Unix(int64(changeTimeSeconds), int64(changeTimeNanoseconds)),
292-
modifyTime: time.Unix(int64(modifyTimeSeconds), int64(modifyTimeNanoseconds)),
293-
createTime: time.Unix(int64(createTimeSeconds), int64(createTimeNanoseconds)),
288+
accessTime: time.Unix(atimeSec, atimeNano),
289+
changeTime: time.Unix(ctimeSec, ctimeNano),
290+
modifyTime: time.Unix(mtimeSec, mtimeNano),
291+
createTime: time.Unix(crtimeSec, crtimeNano),
294292
extendedAttributeBlock: binary.LittleEndian.Uint64(extendedAttributeBlock),
295293
project: binary.LittleEndian.Uint32(b[0x9c:0x100]),
296294
extents: allExtents,
@@ -332,17 +330,23 @@ func (i *inode) toBytes(sb *superblock) []byte {
332330
binary.LittleEndian.PutUint64(version, i.version)
333331
binary.LittleEndian.PutUint64(extendedAttributeBlock, i.extendedAttributeBlock)
334332

335-
// there is some odd stuff that ext4 does with nanoseconds. We might need this in the future.
333+
// ext4 timestamps are 32 bits of seconds, plus an extra 32-bit field
334+
// containing 30 bits of nanoseconds and 2 bits of extended seconds.
336335
// See https://ext4.wiki.kernel.org/index.php/Ext4_Disk_Layout#Inode_Timestamps
337-
// binary.LittleEndian.PutUint32(accessTime[4:8], (i.accessTimeNanoseconds<<2)&accessTime[4])
338-
binary.LittleEndian.PutUint64(accessTime, uint64(i.accessTime.Unix()))
339-
binary.LittleEndian.PutUint32(accessTime[4:8], uint32(i.accessTime.Nanosecond()))
340-
binary.LittleEndian.PutUint64(createTime, uint64(i.createTime.Unix()))
341-
binary.LittleEndian.PutUint32(createTime[4:8], uint32(i.createTime.Nanosecond()))
342-
binary.LittleEndian.PutUint64(changeTime, uint64(i.changeTime.Unix()))
343-
binary.LittleEndian.PutUint32(changeTime[4:8], uint32(i.changeTime.Nanosecond()))
344-
binary.LittleEndian.PutUint64(modifyTime, uint64(i.modifyTime.Unix()))
345-
binary.LittleEndian.PutUint32(modifyTime[4:8], uint32(i.modifyTime.Nanosecond()))
336+
encodeAndWriteTimestamp := func(t time.Time, target []byte) {
337+
seconds := t.Unix()
338+
nanos := uint32(t.Nanosecond())
339+
high := uint32((seconds-int64(int32(seconds)))>>32) & 0x3
340+
extra := (nanos << 2) | high
341+
342+
binary.LittleEndian.PutUint32(target[0:4], uint32(seconds))
343+
binary.LittleEndian.PutUint32(target[4:8], extra)
344+
}
345+
346+
encodeAndWriteTimestamp(i.accessTime, accessTime)
347+
encodeAndWriteTimestamp(i.createTime, createTime)
348+
encodeAndWriteTimestamp(i.changeTime, changeTime)
349+
encodeAndWriteTimestamp(i.modifyTime, modifyTime)
346350

347351
blocks := make([]byte, 8)
348352
binary.LittleEndian.PutUint64(blocks, i.blocks)

0 commit comments

Comments
 (0)