diff --git a/.gitignore b/.gitignore index 42c2a95..d84d97f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .recipes *.zip +*.swp LinuxServer Windows WindowsServer \ No newline at end of file diff --git a/ContentLib/RecipePatches/Recipe_DS_Drive_10K_Fluid.json b/ContentLib/RecipePatches/Recipe_DS_Drive_10K_Fluid.json index 6ca75d8..90ddb16 100644 Binary files a/ContentLib/RecipePatches/Recipe_DS_Drive_10K_Fluid.json and b/ContentLib/RecipePatches/Recipe_DS_Drive_10K_Fluid.json differ diff --git a/ContentLib/RecipePatches/Recipe_DS_Drive_20K_Fluid.json b/ContentLib/RecipePatches/Recipe_DS_Drive_20K_Fluid.json index ab8c256..5e97599 100644 Binary files a/ContentLib/RecipePatches/Recipe_DS_Drive_20K_Fluid.json and b/ContentLib/RecipePatches/Recipe_DS_Drive_20K_Fluid.json differ diff --git a/magefile.go b/magefile.go index 3992d38..4f4aa2d 100644 --- a/magefile.go +++ b/magefile.go @@ -9,7 +9,6 @@ import ( "encoding/json" "fmt" "io" - "io/ioutil" "os" "path/filepath" "strings" @@ -28,11 +27,15 @@ const ( schemaDir = "./schema" ) -var binaryExtensions = map[string]bool{ - ".png": true, ".jpg": true, ".jpeg": true, ".bmp": true, ".gif": true, - ".dds": true, ".tga": true, ".psd": true, ".fbx": true, ".uasset": true, - ".umap": true, -} +var ( + binaryExtensions = map[string]bool{ + ".png": true, ".jpg": true, ".jpeg": true, ".bmp": true, ".gif": true, + ".dds": true, ".tga": true, ".psd": true, ".fbx": true, ".uasset": true, + ".umap": true, + } + + targetPlatforms = []string{"Windows", "WindowsServer", "LinuxServer"} +) var Default = Build @@ -40,115 +43,172 @@ func Build() { mg.SerialDeps(Validate, Package) } +// Package creates distribution packages for all target platforms func Package() error { fmt.Println("Packaging files...") - for _, dir := range []string{"Windows", "WindowsServer", "LinuxServer"} { - if err := os.MkdirAll(dir, 0755); err != nil { - return fmt.Errorf("creating %s: %w", dir, err) - } - if err := sh.Run("cp", "-r", contentDir, pluginFile, dir+"/"); err != nil { - return fmt.Errorf("copying to %s: %w", dir, err) - } + + if err := createPlatformDirectories(); err != nil { + return fmt.Errorf("failed to create platform directories: %w", err) } - if err := createZip(zipName, "Windows", "WindowsServer", "LinuxServer"); err != nil { - return fmt.Errorf("creating zip: %w", err) + + if err := createZip(zipName, targetPlatforms...); err != nil { + return fmt.Errorf("failed to create zip archive: %w", err) } - return Clean("Windows", "WindowsServer", "LinuxServer") + + return Clean(targetPlatforms...) } +func createPlatformDirectories() error { + for _, platform := range targetPlatforms { + if err := os.MkdirAll(platform, 0755); err != nil { + return fmt.Errorf("failed to create directory %s: %w", platform, err) + } + + if err := sh.Run("cp", "-r", contentDir, pluginFile, platform+"/"); err != nil { + return fmt.Errorf("failed to copy files to %s: %w", platform, err) + } + } + return nil +} + +// Validate checks all files for proper formatting and schema compliance func Validate() error { fmt.Println("Validating files...") - compiler := jsonschema.NewCompiler() - compiler.LoadURL = func(url string) (io.ReadCloser, error) { - return nil, fmt.Errorf("remote schema loading disabled: %s", url) - } - if err := filepath.Walk(schemaDir, func(path string, info os.FileInfo, err error) error { - if err != nil || info.IsDir() || filepath.Ext(path) != ".json" { - return err - } - relPath, err := filepath.Rel(schemaDir, path) - if err != nil { - return err - } - id := schemaBaseURL + filepath.ToSlash(relPath) - data, err := ioutil.ReadFile(path) - if err != nil { - return err - } - return compiler.AddResource(id, bytes.NewReader(data)) - }); err != nil { - return fmt.Errorf("failed to load schemas: %w", err) - } - - schema, err := compiler.Compile(schemaBaseURL + "CL_Recipe.json") + schema, err := setupSchemaValidator() if err != nil { - return fmt.Errorf("invalid main schema: %w", err) + return fmt.Errorf("schema setup failed: %w", err) } - var failed []string - paths := []string{pluginFile} - _ = filepath.Walk(contentDir, func(path string, info os.FileInfo, err error) error { - if err == nil && !info.IsDir() { - paths = append(paths, path) - } - return nil - }) - - for _, path := range paths { - if err := validateFile(path, schema); err != nil { - failed = append(failed, fmt.Sprintf("%s: %v", path, err)) - } + filesToValidate, err := collectFilesToValidate() + if err != nil { + return fmt.Errorf("failed to collect files for validation: %w", err) } - if len(failed) > 0 { - fmt.Println("Validation errors:") - for _, msg := range failed { - fmt.Println(" -", msg) - } - return fmt.Errorf("%d file(s) failed validation", len(failed)) + validationErrors := validateFiles(filesToValidate, schema) + + if len(validationErrors) > 0 { + logValidationErrors(validationErrors) + return fmt.Errorf("%d file(s) failed validation", len(validationErrors)) } fmt.Println("All files validated successfully.") return nil } -func validateFile(path string, schema *jsonschema.Schema) error { - if binaryExtensions[filepath.Ext(path)] { - return nil +func setupSchemaValidator() (*jsonschema.Schema, error) { + compiler := jsonschema.NewCompiler() + compiler.LoadURL = func(url string) (io.ReadCloser, error) { + return nil, fmt.Errorf("remote schema loading disabled: %s", url) } - if filepath.Ext(path) == ".json" { - return validateJSON(path, schema) + + if err := loadLocalSchemas(compiler); err != nil { + return nil, fmt.Errorf("failed to load schemas: %w", err) } - return validateEncoding(path) + + return compiler.Compile(schemaBaseURL + "CL_Recipe.json") } -func validateJSON(path string, schema *jsonschema.Schema) error { - data, err := ioutil.ReadFile(path) +func loadLocalSchemas(compiler *jsonschema.Compiler) error { + return filepath.Walk(schemaDir, func(path string, info os.FileInfo, err error) error { + if err != nil || info.IsDir() || filepath.Ext(path) != ".json" { + return err + } + + relPath, err := filepath.Rel(schemaDir, path) + if err != nil { + return err + } + + id := schemaBaseURL + filepath.ToSlash(relPath) + data, err := os.ReadFile(path) + if err != nil { + return err + } + + return compiler.AddResource(id, bytes.NewReader(data)) + }) +} + +func collectFilesToValidate() ([]string, error) { + var files []string + files = append(files, pluginFile) + + err := filepath.Walk(contentDir, func(path string, info os.FileInfo, err error) error { + if err == nil && !info.IsDir() { + files = append(files, path) + } + return err + }) + + return files, err +} + +func validateFiles(files []string, schema *jsonschema.Schema) []string { + var validationErrors []string + + for _, file := range files { + if err := validateFile(file, schema); err != nil { + validationErrors = append(validationErrors, fmt.Sprintf("%s: %v", file, err)) + } + } + + return validationErrors +} + +func logValidationErrors(errors []string) { + fmt.Println("Validation errors:") + for _, msg := range errors { + fmt.Println(" -", msg) + } +} + +func validateFile(path string, schema *jsonschema.Schema) error { + ext := filepath.Ext(path) + + if binaryExtensions[ext] { + return nil + } + + if ext == ".json" { + return validateJSONFile(path, schema) + } + + return validateTextFileEncoding(path) +} + +func validateJSONFile(path string, schema *jsonschema.Schema) error { + cleanJSON, err := readAndCleanJSONFile(path) if err != nil { return err } - data = sanitizeJSONBytes(data) - - var lines [][]byte - for _, line := range bytes.Split(data, []byte("\n")) { - trim := bytes.TrimSpace(line) - if !bytes.HasPrefix(trim, []byte("//")) && len(trim) > 0 { - lines = append(lines, line) - } - } - clean := bytes.Join(lines, []byte("\n")) var v interface{} - if err := json.Unmarshal(clean, &v); err != nil { - return err + if err := json.Unmarshal(cleanJSON, &v); err != nil { + return fmt.Errorf("JSON parsing error: %w", err) } - return schema.Validate(v) + + if err := schema.Validate(v); err != nil { + return fmt.Errorf("schema validation error: %w", err) + } + + return nil +} + +func readAndCleanJSONFile(path string) ([]byte, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("failed to read file: %w", err) + } + + data = sanitizeJSONBytes(data) + return removeJSONComments(data), nil } func sanitizeJSONBytes(data []byte) []byte { data = bytes.TrimPrefix(data, []byte{0xEF, 0xBB, 0xBF}) var out bytes.Buffer + for len(data) > 0 { r, size := utf8.DecodeRune(data) if r == utf8.RuneError && size == 1 || r == '\x00' { @@ -158,17 +218,31 @@ func sanitizeJSONBytes(data []byte) []byte { out.WriteRune(r) data = data[size:] } + return out.Bytes() } -func validateEncoding(path string) error { - data, err := ioutil.ReadFile(path) - if err != nil { - return err +func removeJSONComments(data []byte) []byte { + var lines [][]byte + + for _, line := range bytes.Split(data, []byte("\n")) { + trim := bytes.TrimSpace(line) + if !bytes.HasPrefix(trim, []byte("//")) && len(trim) > 0 { + lines = append(lines, line) + } } + + return bytes.Join(lines, []byte("\n")) +} + +func validateTextFileEncoding(path string) error { + data, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("failed to read file: %w", err) + } + for i := 0; i < len(data); { if data[i] > 127 { - // DO NOT declare "size", it is unused and will cause a compiler error r, _ := utf8.DecodeRune(data[i:]) if r == utf8.RuneError { return fmt.Errorf("invalid UTF-8 sequence at position %d", i) @@ -177,32 +251,39 @@ func validateEncoding(path string) error { } i++ } + return nil } -func Clean(list ...string) error { +// Clean removes temporary directories +func Clean(dirs ...string) error { fmt.Println("Cleaning up...") - for _, f := range list { - if err := os.RemoveAll(f); err != nil { - return fmt.Errorf("failed to remove %s: %w", f, err) + + for _, dir := range dirs { + if err := os.RemoveAll(dir); err != nil { + return fmt.Errorf("failed to remove %s: %w", dir, err) } } + return nil } func createZip(zipPath string, dirs ...string) error { zipFile, err := os.Create(zipPath) if err != nil { - return fmt.Errorf("creating zip file: %w", err) + return fmt.Errorf("failed to create zip file: %w", err) } defer zipFile.Close() + zipWriter := zip.NewWriter(zipFile) defer zipWriter.Close() + for _, dir := range dirs { if err := addDirToZip(zipWriter, dir); err != nil { - return fmt.Errorf("adding %s to zip: %w", dir, err) + return fmt.Errorf("failed to add directory %s to zip: %w", dir, err) } } + return nil } @@ -211,45 +292,57 @@ func addDirToZip(zipWriter *zip.Writer, dirPath string) error { if err != nil { return err } + relPath, err := filepath.Rel(dirPath, filePath) if err != nil { return err } + zipPath := filepath.Join(filepath.Base(dirPath), relPath) - header, err := zip.FileInfoHeader(info) - if err != nil { - return err - } - header.Name = filepath.ToSlash(zipPath) - header.Method = zip.Deflate - if info.IsDir() { - header.Name += "/" - _, err := zipWriter.CreateHeader(header) - return err - } - file, err := os.Open(filePath) - if err != nil { - return err - } - defer file.Close() - writer, err := zipWriter.CreateHeader(header) - if err != nil { - return err - } - if binaryExtensions[strings.ToLower(filepath.Ext(filePath))] { - _, err = io.Copy(writer, file) - return err - } - content, err := ioutil.ReadAll(file) - if err != nil { - return err - } - content = convertToDOSLineEndings(content) - _, err = writer.Write(content) - return err + return addFileToZip(zipWriter, filePath, zipPath, info) }) } +func addFileToZip(zipWriter *zip.Writer, filePath, zipPath string, info os.FileInfo) error { + header, err := zip.FileInfoHeader(info) + if err != nil { + return err + } + + header.Name = filepath.ToSlash(zipPath) + header.Method = zip.Deflate + + if info.IsDir() { + header.Name += "/" + _, err := zipWriter.CreateHeader(header) + return err + } + + file, err := os.Open(filePath) + if err != nil { + return err + } + defer file.Close() + + writer, err := zipWriter.CreateHeader(header) + if err != nil { + return err + } + + if binaryExtensions[strings.ToLower(filepath.Ext(filePath))] { + _, err = io.Copy(writer, file) + return err + } + + content, err := io.ReadAll(file) + if err != nil { + return err + } + + _, err = writer.Write(convertToDOSLineEndings(content)) + return err +} + func convertToDOSLineEndings(content []byte) []byte { normalized := bytes.ReplaceAll(content, []byte("\r\n"), []byte("\n")) return bytes.ReplaceAll(normalized, []byte("\n"), []byte("\r\n"))