fix: contain generated files and assets and harden SQL rendering
This commit is contained in:
@@ -180,8 +180,8 @@ func TestWriterGenerateFilenameOutputPathForms(t *testing.T) {
|
||||
{"no output path", "", "users.txt"},
|
||||
{"existing dir", dir, filepath.Join(dir, "users.txt")},
|
||||
{"trailing separator", filepath.Join(dir, "new") + string(filepath.Separator), filepath.Join(dir, "new", "users.txt")},
|
||||
{"file path uses its dir", filepath.Join(dir, "x.out"), filepath.Join(dir, "users.txt")},
|
||||
{"bare file name", "x.out", "users.txt"},
|
||||
{"nonexistent path is root", filepath.Join(dir, "x.out"), filepath.Join(dir, "x.out", "users.txt")},
|
||||
{"bare path is root", "x.out", filepath.Join("x.out", "users.txt")},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -217,3 +217,123 @@ func TestWriterWriteSchemaAndTable(t *testing.T) {
|
||||
t.Errorf("WriteTable: %q", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiFileRejectsUnsafeFilenamesBeforeWriting(t *testing.T) {
|
||||
for _, name := range []string{"../escape", "/absolute", `..\escape`, `C:\escape`, `\\server\share\file`, "a/../escape", "a//escape", "CON", "nul.txt", "COM¹.txt", "file:stream", "trailing.", "trailing ", "line\nbreak"} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
out := filepath.Join(t.TempDir(), "new-output")
|
||||
w, err := newTestWriter(t, "content", "schema", "{{.Name}}", out)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
db := models.InitDatabase("unsafe")
|
||||
db.Schemas = []*models.Schema{models.InitSchema("valid"), models.InitSchema(name)}
|
||||
if err := w.WriteDatabase(db); err == nil {
|
||||
t.Fatal("expected rejected filename")
|
||||
}
|
||||
if _, err := os.Stat(out); !os.IsNotExist(err) {
|
||||
t.Fatalf("validation failure must not create output: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiFileCollisionsDoNotOverwrite(t *testing.T) {
|
||||
for _, names := range [][]string{{"same.txt", "same.txt"}, {"SAME.txt", "same.txt"}, {"file", "file/child.txt"}} {
|
||||
out := t.TempDir()
|
||||
seed := filepath.Join(out, "same.txt")
|
||||
if err := os.WriteFile(seed, []byte("original"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
w, err := newTestWriter(t, "replacement", "schema", "{{.Name}}", out)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
db := models.InitDatabase("collision")
|
||||
for _, name := range names {
|
||||
db.Schemas = append(db.Schemas, models.InitSchema(name))
|
||||
}
|
||||
if err := w.WriteDatabase(db); err == nil || !strings.Contains(err.Error(), "collision") {
|
||||
t.Fatalf("expected collision error: %v", err)
|
||||
}
|
||||
got, err := os.ReadFile(seed)
|
||||
if err != nil || string(got) != "original" {
|
||||
t.Fatalf("existing output changed: %q %v", got, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiFileRejectsSymlinksBeforeAnyWrites(t *testing.T) {
|
||||
for _, kind := range []string{"parent", "leaf", "root"} {
|
||||
t.Run(kind, func(t *testing.T) {
|
||||
out, external := t.TempDir(), t.TempDir()
|
||||
secret := filepath.Join(external, "secret.txt")
|
||||
if err := os.WriteFile(secret, []byte("original"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
link, target := filepath.Join(out, "link"), external
|
||||
pattern := "{{.Name}}/secret.txt"
|
||||
if kind == "leaf" {
|
||||
target = secret
|
||||
pattern = "{{.Name}}"
|
||||
}
|
||||
if kind == "root" {
|
||||
link = filepath.Join(out, "root")
|
||||
out = link
|
||||
pattern = "{{.Name}}"
|
||||
}
|
||||
if err := os.Symlink(target, link); err != nil {
|
||||
t.Skip(err)
|
||||
}
|
||||
w, err := newTestWriter(t, "replacement", "schema", pattern, out)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
db := models.InitDatabase("links")
|
||||
db.Schemas = []*models.Schema{models.InitSchema("first"), models.InitSchema("link")}
|
||||
if err := w.WriteDatabase(db); err == nil {
|
||||
t.Fatal("expected symlink rejection")
|
||||
}
|
||||
got, err := os.ReadFile(secret)
|
||||
if err != nil || string(got) != "original" {
|
||||
t.Fatalf("symlink target changed: %q %v", got, err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(out, "first")); !os.IsNotExist(err) {
|
||||
t.Fatalf("preflight must precede writes: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiFileNewDirectoryAndNestedFiles(t *testing.T) {
|
||||
out := filepath.Join(t.TempDir(), "new-output")
|
||||
w, err := newTestWriter(t, "{{.Table.Name}}", "table", "{{.ParentSchema.Name}}/{{.Name}}.txt", out)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := w.WriteDatabase(modeDB()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := os.ReadFile(filepath.Join(out, "a", "t1.txt"))
|
||||
if err != nil || string(got) != "t1" {
|
||||
t.Fatalf("nested output: %q %v", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiFileRootAncestorSymlinkRejected(t *testing.T) {
|
||||
parent, external := t.TempDir(), t.TempDir()
|
||||
link := filepath.Join(parent, "link")
|
||||
if err := os.Symlink(external, link); err != nil {
|
||||
t.Skip(err)
|
||||
}
|
||||
w, err := newTestWriter(t, "content", "schema", "{{.Name}}.txt", filepath.Join(link, "new"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := w.WriteDatabase(modeDB()); err == nil {
|
||||
t.Fatal("root ancestor symlink must fail")
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(external, "new")); !os.IsNotExist(err) {
|
||||
t.Fatalf("must not create external directories: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user