package buildctx import ( "os" "path/filepath" "strings" "syscall" "testing" ) func TestStageCopiesOnlySafeUnignoredContext(t *testing.T) { checkout := t.TempDir() write(t, filepath.Join(checkout, ".ci", "Containerfile"), "FROM alpine\nCOPY . /src\n") write(t, filepath.Join(checkout, ".ci", "image", ".containerignore"), "ignored\n*.secret\n") write(t, filepath.Join(checkout, ".ci", "image", "keep"), "ok") write(t, filepath.Join(checkout, ".ci", "image", "ignored", "file"), "no") write(t, filepath.Join(checkout, ".ci", "image", "token.secret"), "no") containerfile, context, err := Stage(checkout, ".ci/Containerfile", ".ci/image", filepath.Join(t.TempDir(), "stage"), 100, 100) if err != nil { t.Fatal(err) } if _, err := os.Stat(containerfile); err != nil { t.Fatal(err) } if _, err := os.Stat(filepath.Join(context, "keep")); err != nil { t.Fatal(err) } for _, name := range []string{"ignored", "token.secret"} { if _, err := os.Stat(filepath.Join(context, name)); !os.IsNotExist(err) { t.Fatalf("ignored %s exists: %v", name, err) } } } func TestStageRejectsEscapesMetadataSpecialFilesAndExternalLinks(t *testing.T) { for name, setup := range map[string]func(t *testing.T, root string){ "traversal": func(t *testing.T, root string) { write(t, filepath.Join(root, ".ci", "Containerfile"), "FROM scratch") }, "containerfile-git": func(t *testing.T, root string) { write(t, filepath.Join(root, ".ci", "image", ".git", "Containerfile"), "FROM scratch") }, "external-link": func(t *testing.T, root string) { if err := os.Symlink("/etc/passwd", filepath.Join(root, ".ci", "image", "link")); err != nil { t.Fatal(err) } }, "special": func(t *testing.T, root string) { if err := syscall.Mkfifo(filepath.Join(root, ".ci", "image", "pipe"), 0o600); err != nil { t.Fatal(err) } }, } { t.Run(name, func(t *testing.T) { root := t.TempDir() write(t, filepath.Join(root, ".ci", "Containerfile"), "FROM scratch") if err := os.MkdirAll(filepath.Join(root, ".ci", "image"), 0o755); err != nil { t.Fatal(err) } setup(t, root) containerfile := ".ci/Containerfile" if name == "traversal" { containerfile = "../Containerfile" } if name == "containerfile-git" { containerfile = ".ci/image/.git/Containerfile" } _, _, err := Stage(root, containerfile, ".ci/image", filepath.Join(t.TempDir(), "stage"), 100, 100) if err == nil { t.Fatal("unsafe input accepted") } }) } } func TestStageBoundsBytesAndAllowsInternalLink(t *testing.T) { root := t.TempDir() write(t, filepath.Join(root, ".ci", "Containerfile"), "FROM scratch") write(t, filepath.Join(root, ".ci", "image", "file"), strings.Repeat("x", 40)) if err := os.Symlink("file", filepath.Join(root, ".ci", "image", "link")); err != nil { t.Fatal(err) } _, context, err := Stage(root, ".ci/Containerfile", ".ci/image", filepath.Join(t.TempDir(), "stage"), 20, 100) if err == nil || !strings.Contains(err.Error(), "byte limit") || context != "" { t.Fatalf("err=%v context=%q", err, context) } _, context, err = Stage(root, ".ci/Containerfile", ".ci/image", filepath.Join(t.TempDir(), "stage-ok"), 100, 100) if err != nil { t.Fatal(err) } if target, err := os.Readlink(filepath.Join(context, "link")); err != nil || target != "file" { t.Fatalf("link=%q err=%v", target, err) } } func TestStageAllowsRootContextExcludesGitAndHonorsNegation(t *testing.T) { root := t.TempDir() write(t, filepath.Join(root, "Containerfile"), "FROM scratch") write(t, filepath.Join(root, ".git", "config"), "private") write(t, filepath.Join(root, ".containerignore"), "ignored/\n!ignored/keep\n") write(t, filepath.Join(root, "ignored", "drop"), "no") write(t, filepath.Join(root, "ignored", "keep"), "yes") _, context, err := Stage(root, "Containerfile", ".", filepath.Join(t.TempDir(), "stage"), 1000, 100) if err != nil { t.Fatal(err) } if _, err := os.Stat(filepath.Join(context, ".git")); !os.IsNotExist(err) { t.Fatalf("Git metadata staged: %v", err) } if _, err := os.Stat(filepath.Join(context, "ignored", "keep")); err != nil { t.Fatalf("negated path missing: %v", err) } if _, err := os.Stat(filepath.Join(context, "ignored", "drop")); !os.IsNotExist(err) { t.Fatalf("ignored path staged: %v", err) } } func TestStageBoundsFilesAndRejectsIgnoreSymlink(t *testing.T) { root := t.TempDir() write(t, filepath.Join(root, "Containerfile"), "FROM scratch") write(t, filepath.Join(root, "one"), "1") write(t, filepath.Join(root, "two"), "2") if _, _, err := Stage(root, "Containerfile", ".", filepath.Join(t.TempDir(), "limited"), 100, 2); err == nil || !strings.Contains(err.Error(), "file limit") { t.Fatalf("file limit err=%v", err) } ignoreRoot := t.TempDir() write(t, filepath.Join(ignoreRoot, "Containerfile"), "FROM scratch") write(t, filepath.Join(ignoreRoot, "outside"), "x") if err := os.Symlink("outside", filepath.Join(ignoreRoot, ".containerignore")); err != nil { t.Fatal(err) } if _, _, err := Stage(ignoreRoot, "Containerfile", ".", filepath.Join(t.TempDir(), "ignore-link"), 100, 100); err == nil || !strings.Contains(err.Error(), "not a regular") { t.Fatalf("ignore symlink err=%v", err) } } func TestStageCountsContainerfileInByteLimit(t *testing.T) { root := t.TempDir() write(t, filepath.Join(root, ".ci", "Containerfile"), strings.Repeat("x", 60)) write(t, filepath.Join(root, ".ci", "image", "file"), strings.Repeat("x", 50)) _, _, err := Stage(root, ".ci/Containerfile", ".ci/image", filepath.Join(t.TempDir(), "stage"), 100, 100) if err == nil || !strings.Contains(err.Error(), "byte limit") { t.Fatalf("err=%v", err) } } func write(t *testing.T, path, value string) { t.Helper() if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(path, []byte(value), 0o644); err != nil { t.Fatal(err) } }