Skip to content

Commit 6bdf224

Browse files
MagicalTuxclaude
andcommitted
feat: improve writer with programmatic API, fix bugs, add round-trip tests
Fix permission bits to preserve setuid/setgid/sticky using modeToUnix(). Fix directory size to use squashfs +3 convention for unsquashfs compat. Fix empty directory handling (no fake header). Fix device number serialization (was hardcoded to 0). Add reader support for special inode types 4-14 (devices, fifos, sockets). Add programmatic writer API: AddFile, AddDirectory, AddSymlink, AddDevice, AddFifo, AddSocket, SetOwner, SetModTime with auto-creation of intermediate directories. Add comprehensive round-trip tests covering permissions, ownership, timestamps, symlinks, devices, fifos, sockets, and all compression types (GZip, ZSTD, XZ). All tests cross-validated with unsquashfs. Install squashfs-tools in CI for unsquashfs cross-validation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent cb6fb9f commit 6bdf224

7 files changed

Lines changed: 988 additions & 22 deletions

File tree

.github/workflows/test.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,13 @@ jobs:
3030
if: runner.os == 'Linux'
3131
run: |
3232
sudo apt-get update
33-
sudo apt-get install -y libfuse-dev
33+
sudo apt-get install -y libfuse-dev squashfs-tools
3434
3535
- name: Install dependencies (macOS)
3636
if: runner.os == 'macOS'
3737
run: |
3838
brew install --cask macfuse
39+
brew install squashfs
3940
4041
- name: Build
4142
run: go build -v ./...
@@ -75,7 +76,7 @@ jobs:
7576
- name: Install dependencies
7677
run: |
7778
sudo apt-get update
78-
sudo apt-get install -y libfuse-dev
79+
sudo apt-get install -y libfuse-dev squashfs-tools
7980
8081
- name: golangci-lint
8182
uses: golangci/golangci-lint-action@v9
@@ -101,7 +102,7 @@ jobs:
101102
- name: Install dependencies
102103
run: |
103104
sudo apt-get update
104-
sudo apt-get install -y libfuse-dev
105+
sudo apt-get install -y libfuse-dev squashfs-tools
105106
106107
- name: Run tests with coverage
107108
run: go test -v -tags "fuse xz zstd" -covermode=count -coverprofile=coverage.out ./...

inode.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ type Inode struct {
3636
XattrIdx uint32 // extended attribute index if present
3737
Sparse uint64 // sparse file information
3838

39+
Rdev uint32 // device number (for block/char device inodes)
40+
3941
// fragment information for file data that doesn't fill a complete block
4042
FragBlock uint32 // fragment block index
4143
FragOfft uint32 // offset within fragment block
@@ -373,6 +375,42 @@ func (sb *Superblock) GetInodeRef(inor inodeRef) (*Inode, error) {
373375
ino.SymTarget = buf
374376

375377
//log.Printf("squashfs: read symlink to %s", ino.SymTarget)
378+
case 4, 5: // Basic block device, basic char device
379+
err = binary.Read(r, sb.order, &ino.NLink)
380+
if err != nil {
381+
return nil, err
382+
}
383+
err = binary.Read(r, sb.order, &ino.Rdev)
384+
if err != nil {
385+
return nil, err
386+
}
387+
case 11, 12: // Extended block device, extended char device
388+
err = binary.Read(r, sb.order, &ino.NLink)
389+
if err != nil {
390+
return nil, err
391+
}
392+
err = binary.Read(r, sb.order, &ino.Rdev)
393+
if err != nil {
394+
return nil, err
395+
}
396+
err = binary.Read(r, sb.order, &ino.XattrIdx)
397+
if err != nil {
398+
return nil, err
399+
}
400+
case 6, 7: // Basic fifo, basic socket
401+
err = binary.Read(r, sb.order, &ino.NLink)
402+
if err != nil {
403+
return nil, err
404+
}
405+
case 13, 14: // Extended fifo, extended socket
406+
err = binary.Read(r, sb.order, &ino.NLink)
407+
if err != nil {
408+
return nil, err
409+
}
410+
err = binary.Read(r, sb.order, &ino.XattrIdx)
411+
if err != nil {
412+
return nil, err
413+
}
376414
default:
377415
log.Printf("squashfs: unsupported inode type %d", ino.Type)
378416
return ino, nil

unsquashfs_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package squashfs_test
2+
3+
import (
4+
"os"
5+
"os/exec"
6+
"path/filepath"
7+
"testing"
8+
)
9+
10+
// unsquashfsAvailable returns true if unsquashfs is on PATH.
11+
func unsquashfsAvailable() bool {
12+
_, err := exec.LookPath("unsquashfs")
13+
return err == nil
14+
}
15+
16+
// writeToTempFile writes data to a temporary file and returns its path.
17+
// The file is automatically cleaned up when the test ends.
18+
func writeToTempFile(t *testing.T, data []byte) string {
19+
t.Helper()
20+
f, err := os.CreateTemp(t.TempDir(), "squashfs-test-*.sqfs")
21+
if err != nil {
22+
t.Fatalf("failed to create temp file: %s", err)
23+
}
24+
if _, err := f.Write(data); err != nil {
25+
f.Close()
26+
t.Fatalf("failed to write temp file: %s", err)
27+
}
28+
if err := f.Close(); err != nil {
29+
t.Fatalf("failed to close temp file: %s", err)
30+
}
31+
return f.Name()
32+
}
33+
34+
// runUnsquashfs runs unsquashfs with the given squashfs data and arguments.
35+
// It skips the test if unsquashfs is not available.
36+
// Returns the combined stdout+stderr output.
37+
func runUnsquashfs(t *testing.T, sqfsData []byte, args ...string) string {
38+
t.Helper()
39+
if !unsquashfsAvailable() {
40+
t.Skip("unsquashfs not available")
41+
}
42+
tmpFile := writeToTempFile(t, sqfsData)
43+
fullArgs := append(args, tmpFile)
44+
cmd := exec.Command("unsquashfs", fullArgs...)
45+
out, err := cmd.CombinedOutput()
46+
if err != nil {
47+
t.Fatalf("unsquashfs %v failed: %s\noutput: %s", args, err, string(out))
48+
}
49+
return string(out)
50+
}
51+
52+
// unsquashfsListLong runs unsquashfs -lln on the data and returns the output.
53+
func unsquashfsListLong(t *testing.T, sqfsData []byte) string {
54+
t.Helper()
55+
return runUnsquashfs(t, sqfsData, "-lln")
56+
}
57+
58+
// unsquashfsExtract extracts the squashfs image to a temp directory and returns the path.
59+
func unsquashfsExtract(t *testing.T, sqfsData []byte) string {
60+
t.Helper()
61+
if !unsquashfsAvailable() {
62+
t.Skip("unsquashfs not available")
63+
}
64+
tmpFile := writeToTempFile(t, sqfsData)
65+
destDir := filepath.Join(t.TempDir(), "extracted")
66+
cmd := exec.Command("unsquashfs", "-d", destDir, "-no-xattrs", tmpFile)
67+
out, err := cmd.CombinedOutput()
68+
if err != nil {
69+
t.Fatalf("unsquashfs extract failed: %s\noutput: %s", err, string(out))
70+
}
71+
return destDir
72+
}

0 commit comments

Comments
 (0)