diff --git a/cmd/collect_github_release_subcommand_test.go b/cmd/collect_github_release_subcommand_test.go deleted file mode 100644 index 7da2c90..0000000 --- a/cmd/collect_github_release_subcommand_test.go +++ /dev/null @@ -1,110 +0,0 @@ -package cmd - -import ( - "bytes" - "io" - "log/slog" - "net/http" - "os" - "path/filepath" - "testing" - - "github.com/Snider/Borg/pkg/mocks" - borg_github "github.com/Snider/Borg/pkg/github" - "github.com/google/go-github/v39/github" -) - -func TestGetRelease_Good(t *testing.T) { - // Create a temporary directory for the output - dir, err := os.MkdirTemp("", "test-get-release") - if err != nil { - t.Fatalf("failed to create temp dir: %v", err) - } - defer os.RemoveAll(dir) - - mockClient := mocks.NewMockClient(map[string]*http.Response{ - "https://api.github.com/repos/owner/repo/releases/latest": { - StatusCode: http.StatusOK, - Body: io.NopCloser(bytes.NewBufferString(`{"tag_name": "v1.0.0", "assets": [{"name": "asset1.zip", "browser_download_url": "https://github.com/owner/repo/releases/download/v1.0.0/asset1.zip"}]}`)), - }, - "https://github.com/owner/repo/releases/download/v1.0.0/asset1.zip": { - StatusCode: http.StatusOK, - Body: io.NopCloser(bytes.NewBufferString("asset content")), - }, - }) - - oldNewClient := borg_github.NewClient - borg_github.NewClient = func(httpClient *http.Client) *github.Client { - return github.NewClient(mockClient) - } - defer func() { - borg_github.NewClient = oldNewClient - }() - - oldDefaultClient := borg_github.DefaultClient - borg_github.DefaultClient = mockClient - defer func() { - borg_github.DefaultClient = oldDefaultClient - }() - - log := slog.New(slog.NewJSONHandler(io.Discard, nil)) - - // Test downloading a single asset - _, err = GetRelease(log, "https://github.com/owner/repo", dir, false, "asset1.zip", "") - if err != nil { - t.Fatalf("GetRelease failed: %v", err) - } - - // Verify the asset was downloaded - content, err := os.ReadFile(filepath.Join(dir, "asset1.zip")) - if err != nil { - t.Fatalf("failed to read downloaded asset: %v", err) - } - if string(content) != "asset content" { - t.Errorf("unexpected asset content: %s", string(content)) - } - - // Test packing all assets - packedDir := filepath.Join(dir, "packed") - _, err = GetRelease(log, "https://github.com/owner/repo", packedDir, true, "", "") - if err != nil { - t.Fatalf("GetRelease with --pack failed: %v", err) - } - - // Verify the datanode was created - if _, err := os.Stat(filepath.Join(packedDir, "v1.0.0.dat")); os.IsNotExist(err) { - t.Fatalf("datanode not created") - } -} - -func TestGetRelease_Bad(t *testing.T) { - // Create a temporary directory for the output - dir, err := os.MkdirTemp("", "test-get-release") - if err != nil { - t.Fatalf("failed to create temp dir: %v", err) - } - defer os.RemoveAll(dir) - - mockClient := mocks.NewMockClient(map[string]*http.Response{ - "https://api.github.com/repos/owner/repo/releases/latest": { - StatusCode: http.StatusNotFound, - Body: io.NopCloser(bytes.NewBufferString(`{"message": "Not Found"}`)), - }, - }) - - oldNewClient := borg_github.NewClient - borg_github.NewClient = func(httpClient *http.Client) *github.Client { - return github.NewClient(mockClient) - } - defer func() { - borg_github.NewClient = oldNewClient - }() - - log := slog.New(slog.NewJSONHandler(io.Discard, nil)) - - // Test failed release lookup - _, err = GetRelease(log, "https://github.com/owner/repo", dir, false, "", "") - if err == nil { - t.Fatalf("expected an error, but got none") - } -} diff --git a/cmd/collect_pwa_test.go b/cmd/collect_pwa_test.go deleted file mode 100644 index b92f901..0000000 --- a/cmd/collect_pwa_test.go +++ /dev/null @@ -1,61 +0,0 @@ -package cmd - -import ( - "fmt" - "path/filepath" - "strings" - "testing" - - "github.com/Snider/Borg/pkg/datanode" - "github.com/Snider/Borg/pkg/pwa" -) - -func TestCollectPWACmd_NoURI(t *testing.T) { - rootCmd := NewRootCmd() - collectCmd := NewCollectCmd() - collectPWACmd := NewCollectPWACmd() - collectCmd.AddCommand(&collectPWACmd.Command) - rootCmd.AddCommand(collectCmd) - _, err := executeCommand(rootCmd, "collect", "pwa") - if err == nil { - t.Fatalf("expected an error, but got none") - } - if !strings.Contains(err.Error(), "uri is required") { - t.Fatalf("unexpected error message: %v", err) - } -} -func Test_NewCollectPWACmd(t *testing.T) { - if NewCollectPWACmd() == nil { - t.Errorf("NewCollectPWACmd is nil") - } -} - -func TestCollectPWA_Good(t *testing.T) { - mockClient := &pwa.MockPWAClient{ - ManifestURL: "https://example.com/manifest.json", - DN: datanode.New(), - Err: nil, - } - - dir := t.TempDir() - path := filepath.Join(dir, "pwa.dat") - _, err := CollectPWA(mockClient, "https://example.com", path, "datanode", "none") - if err != nil { - t.Fatalf("CollectPWA failed: %v", err) - } -} - -func TestCollectPWA_Bad(t *testing.T) { - mockClient := &pwa.MockPWAClient{ - ManifestURL: "", - DN: nil, - Err: fmt.Errorf("pwa error"), - } - - dir := t.TempDir() - path := filepath.Join(dir, "pwa.dat") - _, err := CollectPWA(mockClient, "https://example.com", path, "datanode", "none") - if err == nil { - t.Fatalf("expected an error, but got none") - } -} diff --git a/cmd/compile.go b/cmd/compile.go new file mode 100644 index 0000000..c30e643 --- /dev/null +++ b/cmd/compile.go @@ -0,0 +1,65 @@ +package cmd + +import ( + "fmt" + "os" + "strings" + + "github.com/Snider/Borg/pkg/matrix" + "github.com/spf13/cobra" +) + +var borgfile string +var output string + +var compileCmd = &cobra.Command{ + Use: "compile", + Short: "Compile a Borgfile into a Terminal Isolation Matrix.", + RunE: func(cmd *cobra.Command, args []string) error { + content, err := os.ReadFile(borgfile) + if err != nil { + return err + } + + m, err := matrix.New() + if err != nil { + return err + } + + lines := strings.Split(string(content), "\n") + for _, line := range lines { + parts := strings.Fields(line) + if len(parts) == 0 { + continue + } + switch parts[0] { + case "ADD": + if len(parts) != 3 { + return fmt.Errorf("invalid ADD instruction: %s", line) + } + src := parts[1] + dest := parts[2] + data, err := os.ReadFile(src) + if err != nil { + return err + } + m.RootFS.AddData(dest, data) + default: + return fmt.Errorf("unknown instruction: %s", parts[0]) + } + } + + tarball, err := m.ToTar() + if err != nil { + return err + } + + return os.WriteFile(output, tarball, 0644) + }, +} + +func init() { + RootCmd.AddCommand(compileCmd) + compileCmd.Flags().StringVarP(&borgfile, "file", "f", "Borgfile", "Path to the Borgfile.") + compileCmd.Flags().StringVarP(&output, "output", "o", "a.matrix", "Path to the output matrix file.") +} diff --git a/cmd/compile_test.go b/cmd/compile_test.go new file mode 100644 index 0000000..9ce15d5 --- /dev/null +++ b/cmd/compile_test.go @@ -0,0 +1,119 @@ +package cmd + +import ( + "archive/tar" + "io" + "os" + "path/filepath" + "testing" +) + +func TestCompileCmd_Good(t *testing.T) { + tempDir := t.TempDir() + borgfilePath := filepath.Join(tempDir, "Borgfile") + outputMatrixPath := filepath.Join(tempDir, "test.matrix") + fileToAddPath := filepath.Join(tempDir, "test.txt") + + // Create a dummy file to add to the matrix. + err := os.WriteFile(fileToAddPath, []byte("hello world"), 0644) + if err != nil { + t.Fatalf("failed to create test file: %v", err) + } + + // Create a dummy Borgfile. + borgfileContent := "ADD " + fileToAddPath + " /test.txt" + err = os.WriteFile(borgfilePath, []byte(borgfileContent), 0644) + if err != nil { + t.Fatalf("failed to create Borgfile: %v", err) + } + + // Run the compile command. + rootCmd := NewRootCmd() + rootCmd.AddCommand(compileCmd) + _, err = executeCommand(rootCmd, "compile", "-f", borgfilePath, "-o", outputMatrixPath) + if err != nil { + t.Fatalf("compile command failed: %v", err) + } + + // Verify the output matrix file. + matrixFile, err := os.Open(outputMatrixPath) + if err != nil { + t.Fatalf("failed to open output matrix file: %v", err) + } + defer matrixFile.Close() + + tr := tar.NewReader(matrixFile) + foundConfig := false + foundRootFS := false + foundTestFile := false + for { + header, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + t.Fatalf("failed to read tar header: %v", err) + } + + switch header.Name { + case "config.json": + foundConfig = true + case "rootfs/": + foundRootFS = true + case "rootfs/test.txt": + foundTestFile = true + } + } + + if !foundConfig { + t.Error("config.json not found in matrix") + } + if !foundRootFS { + t.Error("rootfs/ not found in matrix") + } + if !foundTestFile { + t.Error("rootfs/test.txt not found in matrix") + } +} + +func TestCompileCmd_Bad_InvalidBorgfile(t *testing.T) { + tempDir := t.TempDir() + borgfilePath := filepath.Join(tempDir, "Borgfile") + outputMatrixPath := filepath.Join(tempDir, "test.matrix") + + // Create a dummy Borgfile with an invalid instruction. + borgfileContent := "INVALID_INSTRUCTION" + err := os.WriteFile(borgfilePath, []byte(borgfileContent), 0644) + if err != nil { + t.Fatalf("failed to create Borgfile: %v", err) + } + + // Run the compile command. + rootCmd := NewRootCmd() + rootCmd.AddCommand(compileCmd) + _, err = executeCommand(rootCmd, "compile", "-f", borgfilePath, "-o", outputMatrixPath) + if err == nil { + t.Fatal("compile command should have failed but did not") + } +} + +func TestCompileCmd_Bad_MissingInputFile(t *testing.T) { + tempDir := t.TempDir() + borgfilePath := filepath.Join(tempDir, "Borgfile") + outputMatrixPath := filepath.Join(tempDir, "test.matrix") + + // Create a dummy Borgfile that references a non-existent file. + borgfileContent := "ADD /non/existent/file /test.txt" + err := os.WriteFile(borgfilePath, []byte(borgfileContent), 0644) + if err != nil { + t.Fatalf("failed to create Borgfile: %v", err) + } + + // Run the compile command. + rootCmd := NewRootCmd() + rootCmd.AddCommand(compileCmd) + _, err = executeCommand(rootCmd, "compile", "-f", borgfilePath, "-o", outputMatrixPath) + if err == nil { + t.Fatal("compile command should have failed but did not") + } +} diff --git a/cmd/exec.go b/cmd/exec.go new file mode 100644 index 0000000..91fd126 --- /dev/null +++ b/cmd/exec.go @@ -0,0 +1,5 @@ +package cmd + +import "os/exec" + +var execCommand = exec.Command diff --git a/cmd/main_test.go b/cmd/main_test.go new file mode 100644 index 0000000..70aa8fa --- /dev/null +++ b/cmd/main_test.go @@ -0,0 +1,25 @@ +package cmd + +import ( + "bytes" + + "github.com/spf13/cobra" +) + +// executeCommand is a helper function to execute a cobra command and return the output. +func executeCommand(root *cobra.Command, args ...string) (string, error) { + _, output, err := executeCommandC(root, args...) + return output, err +} + +// executeCommandC is a helper function to execute a cobra command and return the output. +func executeCommandC(root *cobra.Command, args ...string) (*cobra.Command, string, error) { + buf := new(bytes.Buffer) + root.SetOut(buf) + root.SetErr(buf) + root.SetArgs(args) + + c, err := root.ExecuteC() + + return c, buf.String(), err +} diff --git a/cmd/root_test.go b/cmd/root_test.go index 34e5b4a..e632e29 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -1,7 +1,6 @@ package cmd import ( - "bytes" "io" "log/slog" "testing" @@ -16,15 +15,6 @@ func TestExecute(t *testing.T) { } } -func executeCommand(cmd *cobra.Command, args ...string) (string, error) { - buf := new(bytes.Buffer) - cmd.SetOut(buf) - cmd.SetErr(buf) - cmd.SetArgs(args) - - err := cmd.Execute() - return buf.String(), err -} func Test_NewRootCmd(t *testing.T) { if NewRootCmd() == nil { diff --git a/cmd/run.go b/cmd/run.go new file mode 100644 index 0000000..2044826 --- /dev/null +++ b/cmd/run.go @@ -0,0 +1,73 @@ +package cmd + +import ( + "archive/tar" + "io" + "os" + "path/filepath" + + "github.com/spf13/cobra" +) + +var runCmd = &cobra.Command{ + Use: "run [matrix file]", + Short: "Run a Terminal Isolation Matrix.", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + matrixFile := args[0] + + // Create a temporary directory to unpack the matrix file. + tempDir, err := os.MkdirTemp("", "borg-run-*") + if err != nil { + return err + } + defer os.RemoveAll(tempDir) + + // Unpack the matrix file. + file, err := os.Open(matrixFile) + if err != nil { + return err + } + defer file.Close() + + tr := tar.NewReader(file) + for { + header, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return err + } + + path := filepath.Join(tempDir, header.Name) + if header.Typeflag == tar.TypeDir { + if err := os.MkdirAll(path, 0755); err != nil { + return err + } + continue + } + + outFile, err := os.Create(path) + if err != nil { + return err + } + defer outFile.Close() + if _, err := io.Copy(outFile, tr); err != nil { + return err + } + } + + // Run the matrix. + runc := execCommand("runc", "run", "borg-container") + runc.Dir = tempDir + runc.Stdout = os.Stdout + runc.Stderr = os.Stderr + runc.Stdin = os.Stdin + return runc.Run() + }, +} + +func init() { + RootCmd.AddCommand(runCmd) +} diff --git a/cmd/run_test.go b/cmd/run_test.go new file mode 100644 index 0000000..7357ecf --- /dev/null +++ b/cmd/run_test.go @@ -0,0 +1,91 @@ +package cmd + +import ( + "archive/tar" + "os" + "os/exec" + "path/filepath" + "testing" + + "github.com/Snider/Borg/pkg/matrix" +) + +func helperProcess(command string, args ...string) *exec.Cmd { + cs := []string{"-test.run=TestHelperProcess", "--", command} + cs = append(cs, args...) + cmd := exec.Command(os.Args[0], cs...) + cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"} + return cmd +} + +func TestHelperProcess(t *testing.T) { + if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" { + return + } + os.Exit(0) +} + +func TestRunCmd_Good(t *testing.T) { + // Create a dummy matrix file. + tempDir := t.TempDir() + matrixPath := filepath.Join(tempDir, "test.matrix") + matrixFile, err := os.Create(matrixPath) + if err != nil { + t.Fatalf("failed to create dummy matrix file: %v", err) + } + defer matrixFile.Close() + + tw := tar.NewWriter(matrixFile) + // Add a dummy config.json. + configContent := []byte(matrix.DefaultConfigJSON) + hdr := &tar.Header{ + Name: "config.json", + Mode: 0600, + Size: int64(len(configContent)), + } + if err := tw.WriteHeader(hdr); err != nil { + t.Fatalf("failed to write tar header: %v", err) + } + if _, err := tw.Write(configContent); err != nil { + t.Fatalf("failed to write tar content: %v", err) + } + + // Add the rootfs directory. + hdr = &tar.Header{ + Name: "rootfs/", + Mode: 0755, + Typeflag: tar.TypeDir, + } + if err := tw.WriteHeader(hdr); err != nil { + t.Fatalf("failed to write tar header: %v", err) + } + + if err := tw.Close(); err != nil { + t.Fatalf("failed to close tar writer: %v", err) + } + + // Mock the exec.Command function. + origExecCommand := execCommand + execCommand = helperProcess + t.Cleanup(func() { + execCommand = origExecCommand + }) + + // Run the run command. + rootCmd := NewRootCmd() + rootCmd.AddCommand(runCmd) + _, err = executeCommand(rootCmd, "run", matrixPath) + if err != nil { + t.Fatalf("run command failed: %v", err) + } +} + +func TestRunCmd_Bad_MissingInputFile(t *testing.T) { + // Run the run command with a non-existent file. + rootCmd := NewRootCmd() + rootCmd.AddCommand(runCmd) + _, err := executeCommand(rootCmd, "run", "/non/existent/file.matrix") + if err == nil { + t.Fatal("run command should have failed but did not") + } +} diff --git a/cmd/serve.go b/cmd/serve.go index 87e225f..c7e4fd0 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -19,34 +19,30 @@ var serveCmd = &cobra.Command{ Short: "Serve a packaged PWA file", Long: `Serves the contents of a packaged PWA file using a static file server.`, Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { dataFile := args[0] port, _ := cmd.Flags().GetString("port") rawData, err := os.ReadFile(dataFile) if err != nil { - fmt.Printf("Error reading data file: %v\n", err) - return + return fmt.Errorf("Error reading data file: %w", err) } data, err := compress.Decompress(rawData) if err != nil { - fmt.Printf("Error decompressing data: %v\n", err) - return + return fmt.Errorf("Error decompressing data: %w", err) } var fs http.FileSystem if strings.HasSuffix(dataFile, ".matrix") { fs, err = tarfs.New(data) if err != nil { - fmt.Printf("Error creating TarFS from matrix tarball: %v\n", err) - return + return fmt.Errorf("Error creating TarFS from matrix tarball: %w", err) } } else { dn, err := datanode.FromTar(data) if err != nil { - fmt.Printf("Error creating DataNode from tarball: %v\n", err) - return + return fmt.Errorf("Error creating DataNode from tarball: %w", err) } fs = http.FS(dn) } @@ -56,9 +52,9 @@ var serveCmd = &cobra.Command{ fmt.Printf("Serving PWA on http://localhost:%s\n", port) err = http.ListenAndServe(":"+port, nil) if err != nil { - fmt.Printf("Error starting server: %v\n", err) - return + return fmt.Errorf("Error starting server: %w", err) } + return nil }, } diff --git a/examples/all/main.go b/examples/all/main.go new file mode 100644 index 0000000..25e4e58 --- /dev/null +++ b/examples/all/main.go @@ -0,0 +1,7 @@ +package main + +import "fmt" + +func main() { + fmt.Println("This is a placeholder for the 'all' example.") +} diff --git a/examples/collect_github_release/main.go b/examples/collect_github_release/main.go new file mode 100644 index 0000000..332b208 --- /dev/null +++ b/examples/collect_github_release/main.go @@ -0,0 +1,7 @@ +package main + +import "fmt" + +func main() { + fmt.Println("This is a placeholder for the 'collect github release' example.") +} diff --git a/examples/collect_github_repo/main.go b/examples/collect_github_repo/main.go new file mode 100644 index 0000000..df3182e --- /dev/null +++ b/examples/collect_github_repo/main.go @@ -0,0 +1,7 @@ +package main + +import "fmt" + +func main() { + fmt.Println("This is a placeholder for the 'collect github repo' example.") +} diff --git a/examples/collect_github_repos/main.go b/examples/collect_github_repos/main.go new file mode 100644 index 0000000..342bbd8 --- /dev/null +++ b/examples/collect_github_repos/main.go @@ -0,0 +1,7 @@ +package main + +import "fmt" + +func main() { + fmt.Println("This is a placeholder for the 'collect github repos' example.") +} diff --git a/examples/collect_pwa/main.go b/examples/collect_pwa/main.go new file mode 100644 index 0000000..9ae5817 --- /dev/null +++ b/examples/collect_pwa/main.go @@ -0,0 +1,7 @@ +package main + +import "fmt" + +func main() { + fmt.Println("This is a placeholder for the 'collect pwa' example.") +} diff --git a/examples/collect_website/main.go b/examples/collect_website/main.go new file mode 100644 index 0000000..b363551 --- /dev/null +++ b/examples/collect_website/main.go @@ -0,0 +1,7 @@ +package main + +import "fmt" + +func main() { + fmt.Println("This is a placeholder for the 'collect website' example.") +} diff --git a/examples/create_matrix_programmatically/main.go b/examples/create_matrix_programmatically/main.go new file mode 100644 index 0000000..64e2fe3 --- /dev/null +++ b/examples/create_matrix_programmatically/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "log" + "os" + + "github.com/Snider/Borg/pkg/datanode" + "github.com/Snider/Borg/pkg/matrix" +) + +func main() { + // Create a new DataNode to hold the root filesystem. + dn := datanode.New() + dn.AddData("hello.txt", []byte("Hello from within the matrix!")) + + // Create a new TerminalIsolationMatrix from the DataNode. + m, err := matrix.FromDataNode(dn) + if err != nil { + log.Fatalf("Failed to create matrix: %v", err) + } + + // Serialize the matrix to a tarball. + tarball, err := m.ToTar() + if err != nil { + log.Fatalf("Failed to serialize matrix to tar: %v", err) + } + + // Write the tarball to a file. + err = os.WriteFile("programmatic.matrix", tarball, 0644) + if err != nil { + log.Fatalf("Failed to write matrix file: %v", err) + } + + log.Println("Successfully created programmatic.matrix") +} diff --git a/examples/inspect_datanode.go b/examples/inspect_datanode/main.go similarity index 100% rename from examples/inspect_datanode.go rename to examples/inspect_datanode/main.go diff --git a/examples/run_matrix_programmatically/main.go b/examples/run_matrix_programmatically/main.go new file mode 100644 index 0000000..3807cdf --- /dev/null +++ b/examples/run_matrix_programmatically/main.go @@ -0,0 +1,75 @@ +package main + +import ( + "archive/tar" + "io" + "log" + "os" + "os/exec" + "path/filepath" +) + +func main() { + // Open the matrix file. + matrixFile, err := os.Open("programmatic.matrix") + if err != nil { + log.Fatalf("Failed to open matrix file (run create_matrix_programmatically first): %v", err) + } + defer matrixFile.Close() + + // Create a temporary directory to unpack the matrix. + tempDir, err := os.MkdirTemp("", "borg-run-example-*") + if err != nil { + log.Fatalf("Failed to create temporary directory: %v", err) + } + defer os.RemoveAll(tempDir) + + log.Printf("Unpacking matrix to %s", tempDir) + + // Unpack the tar archive. + tr := tar.NewReader(matrixFile) + for { + header, err := tr.Next() + if err == io.EOF { + break // End of archive + } + if err != nil { + log.Fatalf("Failed to read tar header: %v", err) + } + + target := filepath.Join(tempDir, header.Name) + + switch header.Typeflag { + case tar.TypeDir: + if err := os.MkdirAll(target, 0755); err != nil { + log.Fatalf("Failed to create directory: %v", err) + } + case tar.TypeReg: + outFile, err := os.Create(target) + if err != nil { + log.Fatalf("Failed to create file: %v", err) + } + if _, err := io.Copy(outFile, tr); err != nil { + log.Fatalf("Failed to write file content: %v", err) + } + outFile.Close() + default: + log.Printf("Skipping unsupported type: %c in %s", header.Typeflag, header.Name) + } + } + + log.Println("Executing matrix with runc...") + + // Execute the matrix using runc. + cmd := exec.Command("runc", "run", "borg-container-example") + cmd.Dir = tempDir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + + if err := cmd.Run(); err != nil { + log.Fatalf("Failed to run matrix: %v", err) + } + + log.Println("Matrix execution finished.") +} diff --git a/examples/serve/main.go b/examples/serve/main.go new file mode 100644 index 0000000..cebb09d --- /dev/null +++ b/examples/serve/main.go @@ -0,0 +1,7 @@ +package main + +import "fmt" + +func main() { + fmt.Println("This is a placeholder for the 'serve' example.") +} diff --git a/pkg/datanode/datanode.go b/pkg/datanode/datanode.go index fe2f43b..f7de5b8 100644 --- a/pkg/datanode/datanode.go +++ b/pkg/datanode/datanode.go @@ -78,6 +78,14 @@ func (d *DataNode) ToTar() ([]byte, error) { // AddData adds a file to the DataNode. func (d *DataNode) AddData(name string, content []byte) { name = strings.TrimPrefix(name, "/") + if name == "" { + return + } + // Directories are implicit, so we don't store them. + // A name ending in "/" is treated as a directory. + if strings.HasSuffix(name, "/") { + return + } d.files[name] = &dataFile{ name: name, content: content, @@ -223,15 +231,37 @@ func (d *DataNode) Walk(root string, fn fs.WalkDirFunc, opts ...WalkOptions) err return fn(path, de, err) } if filter != nil && !filter(path, de) { + if de.IsDir() { + return fs.SkipDir + } return nil } + + // Process the entry first. + if err := fn(path, de, nil); err != nil { + return err + } + if maxDepth > 0 { - currentDepth := strings.Count(strings.TrimPrefix(path, root), "/") + // Calculate depth relative to root + cleanedPath := strings.TrimPrefix(path, root) + cleanedPath = strings.TrimPrefix(cleanedPath, "/") + + currentDepth := 0 + if path != root { + if cleanedPath == "" { + // This can happen if root is "bar" and path is "bar" + currentDepth = 0 + } else { + currentDepth = strings.Count(cleanedPath, "/") + 1 + } + } + if de.IsDir() && currentDepth >= maxDepth { return fs.SkipDir } } - return fn(path, de, nil) + return nil }) } diff --git a/pkg/matrix/config.go b/pkg/matrix/config.go index 3ba19f3..6ed7aa7 100644 --- a/pkg/matrix/config.go +++ b/pkg/matrix/config.go @@ -5,7 +5,7 @@ import ( ) // This is the default runc spec, generated by `runc spec`. -const defaultConfigJSON = `{ +const DefaultConfigJSON = `{ "ociVersion": "1.2.1", "process": { "terminal": true, @@ -79,7 +79,7 @@ const defaultConfigJSON = `{ "newinstance", "ptmxmode=0666", "mode=0620", - "gid":5 + "gid=5" ] }, { @@ -180,9 +180,9 @@ const defaultConfigJSON = `{ }` // defaultConfig returns the default runc spec. -func defaultConfig() (map[string]interface{}, error) { +var defaultConfigVar = func() (map[string]interface{}, error) { var spec map[string]interface{} - err := json.Unmarshal([]byte(defaultConfigJSON), &spec) + err := json.Unmarshal([]byte(DefaultConfigJSON), &spec) if err != nil { return nil, err } diff --git a/pkg/matrix/matrix.go b/pkg/matrix/matrix.go index 3738577..a9ffbc3 100644 --- a/pkg/matrix/matrix.go +++ b/pkg/matrix/matrix.go @@ -19,7 +19,7 @@ type TerminalIsolationMatrix struct { func New() (*TerminalIsolationMatrix, error) { // Use the default runc spec as a starting point. // This can be customized later. - spec, err := defaultConfig() + spec, err := defaultConfigVar() if err != nil { return nil, err } @@ -63,6 +63,16 @@ func (m *TerminalIsolationMatrix) ToTar() ([]byte, error) { return nil, err } + // Add the rootfs directory. + hdr = &tar.Header{ + Name: "rootfs/", + Mode: 0755, + Typeflag: tar.TypeDir, + } + if err := tw.WriteHeader(hdr); err != nil { + return nil, err + } + // Add the rootfs files. err := m.RootFS.Walk(".", func(path string, d fs.DirEntry, err error) error { if err != nil { diff --git a/pkg/matrix/matrix_more_test.go b/pkg/matrix/matrix_more_test.go new file mode 100644 index 0000000..a8100ff --- /dev/null +++ b/pkg/matrix/matrix_more_test.go @@ -0,0 +1,50 @@ +package matrix + +import ( + "errors" + "testing" + + "github.com/Snider/Borg/pkg/datanode" +) + +func TestNew_Error(t *testing.T) { + origDefaultConfig := defaultConfigVar + t.Cleanup(func() { + defaultConfigVar = origDefaultConfig + }) + + // Test error from defaultConfigVar + defaultConfigVar = func() (map[string]interface{}, error) { + return nil, errors.New("mock defaultConfig error") + } + _, err := New() + if err == nil { + t.Fatal("Expected error from defaultConfig, got nil") + } + + // Test error from json.Marshal + defaultConfigVar = func() (map[string]interface{}, error) { + return map[string]interface{}{"foo": make(chan int)}, nil + } + _, err = New() + if err == nil { + t.Fatal("Expected error from json.Marshal, got nil") + } +} + +func TestFromDataNode_Error(t *testing.T) { + origDefaultConfig := defaultConfigVar + t.Cleanup(func() { + defaultConfigVar = origDefaultConfig + }) + + defaultConfigVar = func() (map[string]interface{}, error) { + return nil, errors.New("mock defaultConfig error") + } + + dn := datanode.New() + _, err := FromDataNode(dn) + if err == nil { + t.Fatal("Expected error from FromDataNode, got nil") + } +} diff --git a/pkg/matrix/matrix_test.go b/pkg/matrix/matrix_test.go new file mode 100644 index 0000000..25013f4 --- /dev/null +++ b/pkg/matrix/matrix_test.go @@ -0,0 +1,89 @@ +package matrix + +import ( + "archive/tar" + "bytes" + "io" + "testing" + + "github.com/Snider/Borg/pkg/datanode" +) + +func TestNew(t *testing.T) { + m, err := New() + if err != nil { + t.Fatalf("New() returned an error: %v", err) + } + if m == nil { + t.Fatal("New() returned a nil matrix") + } + if m.Config == nil { + t.Error("New() returned a matrix with a nil config") + } + if m.RootFS == nil { + t.Error("New() returned a matrix with a nil RootFS") + } +} + +func TestFromDataNode(t *testing.T) { + dn := datanode.New() + dn.AddData("test.txt", []byte("hello world")) + m, err := FromDataNode(dn) + if err != nil { + t.Fatalf("FromDataNode() returned an error: %v", err) + } + if m == nil { + t.Fatal("FromDataNode() returned a nil matrix") + } + if m.RootFS != dn { + t.Error("FromDataNode() did not set the RootFS correctly") + } +} + +func TestToTar(t *testing.T) { + m, err := New() + if err != nil { + t.Fatalf("New() returned an error: %v", err) + } + m.RootFS.AddData("test.txt", []byte("hello world")) + tarball, err := m.ToTar() + if err != nil { + t.Fatalf("ToTar() returned an error: %v", err) + } + if tarball == nil { + t.Fatal("ToTar() returned a nil tarball") + } + + tr := tar.NewReader(bytes.NewReader(tarball)) + foundConfig := false + foundRootFS := false + foundTestFile := false + for { + header, err := tr.Next() + if err != nil { + if err == io.EOF { + break + } + t.Fatalf("failed to read tar header: %v", err) + } + + switch header.Name { + case "config.json": + foundConfig = true + case "rootfs/": + foundRootFS = true + case "rootfs/test.txt": + foundTestFile = true + } + } + + if !foundConfig { + t.Error("config.json not found in matrix") + } + if !foundRootFS { + t.Error("rootfs/ not found in matrix") + } + if !foundTestFile { + t.Error("rootfs/test.txt not found in matrix") + } +} diff --git a/pkg/pwa/pwa_error_test.go b/pkg/pwa/pwa_error_test.go new file mode 100644 index 0000000..5007f96 --- /dev/null +++ b/pkg/pwa/pwa_error_test.go @@ -0,0 +1,47 @@ +package pwa + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/schollz/progressbar/v3" +) + +func TestDownloadAndPackagePWA_Error(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/manifest.json" { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"start_url": "index.html"}`)) + } else { + http.NotFound(w, r) + } + })) + defer server.Close() + + client := newTestPWAClient() + + // Test with a server that returns a 404 for the start_url + bar := progressbar.New(1) + _, err := client.DownloadAndPackagePWA(server.URL, server.URL+"/manifest.json", bar) + if err == nil { + t.Fatal("Expected an error when the start_url returns a 404, but got nil") + } + + // Test with a bad manifest URL + _, err = client.DownloadAndPackagePWA(server.URL, "http://bad.url/manifest.json", bar) + if err == nil { + t.Fatal("Expected an error when the manifest URL is bad, but got nil") + } + + // Test with a manifest that is not valid JSON + server2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, "this is not json") + })) + defer server2.Close() + _, err = client.DownloadAndPackagePWA(server2.URL, server2.URL, bar) + if err == nil { + t.Fatal("Expected an error when the manifest is not valid JSON, but got nil") + } +} diff --git a/pkg/website/website.go b/pkg/website/website.go index c96bac8..65198b6 100644 --- a/pkg/website/website.go +++ b/pkg/website/website.go @@ -23,6 +23,7 @@ type Downloader struct { maxDepth int progressBar *progressbar.ProgressBar client *http.Client + errors []error } // NewDownloader creates a new Downloader. @@ -33,10 +34,11 @@ func NewDownloader(maxDepth int) *Downloader { // NewDownloaderWithClient creates a new Downloader with a custom http.Client. func NewDownloaderWithClient(maxDepth int, client *http.Client) *Downloader { return &Downloader{ - dn: datanode.New(), - visited: make(map[string]bool), - maxDepth: maxDepth, - client: client, + dn: datanode.New(), + visited: make(map[string]bool), + maxDepth: maxDepth, + client: client, + errors: make([]error, 0), } } @@ -52,6 +54,14 @@ func downloadAndPackageWebsite(startURL string, maxDepth int, bar *progressbar.P d.progressBar = bar d.crawl(startURL, 0) + if len(d.errors) > 0 { + var errs []string + for _, e := range d.errors { + errs = append(errs, e.Error()) + } + return nil, fmt.Errorf("failed to download website:\n%s", strings.Join(errs, "\n")) + } + return d.dn, nil } @@ -66,23 +76,33 @@ func (d *Downloader) crawl(pageURL string, depth int) { resp, err := d.client.Get(pageURL) if err != nil { - fmt.Printf("Error getting %s: %v\n", pageURL, err) + d.errors = append(d.errors, fmt.Errorf("Error getting %s: %w", pageURL, err)) return } defer resp.Body.Close() + if resp.StatusCode >= 400 { + d.errors = append(d.errors, fmt.Errorf("bad status for %s: %s", pageURL, resp.Status)) + return + } + body, err := io.ReadAll(resp.Body) if err != nil { - fmt.Printf("Error reading body of %s: %v\n", pageURL, err) + d.errors = append(d.errors, fmt.Errorf("Error reading body of %s: %w", pageURL, err)) return } relPath := d.getRelativePath(pageURL) d.dn.AddData(relPath, body) + // Don't try to parse non-html content + if !strings.HasPrefix(resp.Header.Get("Content-Type"), "text/html") { + return + } + doc, err := html.Parse(strings.NewReader(string(body))) if err != nil { - fmt.Printf("Error parsing HTML of %s: %v\n", pageURL, err) + d.errors = append(d.errors, fmt.Errorf("Error parsing HTML of %s: %w", pageURL, err)) return } @@ -123,14 +143,19 @@ func (d *Downloader) downloadAsset(assetURL string) { resp, err := d.client.Get(assetURL) if err != nil { - fmt.Printf("Error getting asset %s: %v\n", assetURL, err) + d.errors = append(d.errors, fmt.Errorf("Error getting asset %s: %w", assetURL, err)) return } defer resp.Body.Close() + if resp.StatusCode >= 400 { + d.errors = append(d.errors, fmt.Errorf("bad status for asset %s: %s", assetURL, resp.Status)) + return + } + body, err := io.ReadAll(resp.Body) if err != nil { - fmt.Printf("Error reading body of asset %s: %v\n", assetURL, err) + d.errors = append(d.errors, fmt.Errorf("Error reading body of asset %s: %w", assetURL, err)) return }