Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions internal/adapters/secondary/repository/repository_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"errors"
"io"
"os"
"path/filepath"
"testing"
"time"

Expand All @@ -27,13 +28,15 @@ func NewMockFileSystem() *MockFileSystem {
}

func (m *MockFileSystem) ReadFile(name string) ([]byte, error) {
name = filepath.ToSlash(name)
if data, ok := m.files[name]; ok {
return data, nil
}
return nil, errors.New("file not found")
}

func (m *MockFileSystem) WriteFile(name string, data []byte, _ os.FileMode) error {
name = filepath.ToSlash(name)
m.files[name] = data
return nil
}
Expand All @@ -43,6 +46,7 @@ func (m *MockFileSystem) MkdirAll(_ string, _ os.FileMode) error {
}

func (m *MockFileSystem) Stat(name string) (os.FileInfo, error) {
name = filepath.ToSlash(name)
if _, ok := m.files[name]; ok {
//nolint:nilnil // Mock for testing
return nil, nil
Expand All @@ -51,6 +55,7 @@ func (m *MockFileSystem) Stat(name string) (os.FileInfo, error) {
}

func (m *MockFileSystem) Remove(name string) error {
name = filepath.ToSlash(name)
delete(m.files, name)
return nil
}
Expand All @@ -60,6 +65,8 @@ func (m *MockFileSystem) RemoveAll(_ string) error {
}

func (m *MockFileSystem) Rename(oldpath, newpath string) error {
oldpath = filepath.ToSlash(oldpath)
newpath = filepath.ToSlash(newpath)
if data, ok := m.files[oldpath]; ok {
m.files[newpath] = data
delete(m.files, oldpath)
Expand All @@ -82,6 +89,7 @@ func (m *MockFileSystem) Create(_ string) (io.WriteCloser, error) {
}

func (m *MockFileSystem) Exists(name string) bool {
name = filepath.ToSlash(name)
_, ok := m.files[name]
return ok
}
Expand Down
11 changes: 11 additions & 0 deletions internal/core/services/installer/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"

"github.com/hashload/boss/pkg/pkgmanager"
Expand All @@ -25,6 +26,7 @@ import (
"github.com/hashload/boss/pkg/msg"
"github.com/hashload/boss/utils"
"github.com/hashload/boss/utils/librarypath"
"github.com/hashload/boss/utils/normalizer"
)

type installContext struct {
Expand Down Expand Up @@ -537,6 +539,15 @@ func (ic *installContext) checkoutAndUpdate(
}
ic.addWarning(fmt.Sprintf("%s: %s", dep.Name(), warnMsg))
}

// Normalize line endings to CRLF on Windows (Issue #197)
if runtime.GOOS == "windows" {
depDir := filepath.Join(ic.modulesDir, dep.Name())
if err := normalizer.NormalizeDirectoryLineEndings(depDir); err != nil {
msg.Debug("Failed to normalize line endings for %s: %v", dep.Name(), err)
}
}

return nil
}

Expand Down
71 changes: 71 additions & 0 deletions utils/normalizer/line_normalizer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package normalizer

import (
"bytes"
"io/fs"
"os"
"path/filepath"
"strings"

"github.com/hashload/boss/pkg/msg"
)

var delphiExtensions = map[string]bool{
".pas": true,
".inc": true,
".dfm": true,
".dpk": true,
".dproj": true,
}

// NormalizeDirectoryLineEndings walks the given directory and converts LF to CRLF in all Delphi-related files.
func NormalizeDirectoryLineEndings(dir string) error {
msg.Debug(" 🧹 Normalizing line endings to CRLF in %s", dir)
return filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}

ext := strings.ToLower(filepath.Ext(path))
if !delphiExtensions[ext] {
return nil
}

return NormalizeFileLineEndings(path)
})
}

// NormalizeFileLineEndings converts all LF to CRLF in a specific file.
func NormalizeFileLineEndings(filePath string) error {
content, err := os.ReadFile(filePath) // #nosec G304 -- Reading Delphi files from controlled package directories
if err != nil {
return err
}

// Check if there are any raw LFs (LFs not preceded by CR)
hasLF := false
for i, b := range content {
if b == '\n' {
if i == 0 || content[i-1] != '\r' {
hasLF = true
break
}
}
}

if !hasLF {
return nil // Already CRLF or has no LFs
}

msg.Debug(" Normalizing line endings for %s", filepath.Base(filePath))

// Normalize: replace all CRLF with LF, then replace all LF with CRLF
normalized := bytes.ReplaceAll(content, []byte("\r\n"), []byte("\n"))
crlfContent := bytes.ReplaceAll(normalized, []byte("\n"), []byte("\r\n"))

// Write back
return os.WriteFile(filePath, crlfContent, 0600)
}
58 changes: 58 additions & 0 deletions utils/normalizer/line_normalizer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package normalizer_test

import (
"bytes"
"os"
"path/filepath"
"testing"

"github.com/hashload/boss/utils/normalizer"
)

func TestNormalizeDirectoryLineEndings(t *testing.T) {
tempDir, err := os.MkdirTemp("", "boss-normalizer-test")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
defer os.RemoveAll(tempDir)

// 1. Create a Delphi .pas file with LF line endings
pasPath := filepath.Join(tempDir, "unit.pas")
pasContent := []byte("unit Test;\ninterface\n\nimplementation\nend.\n")
if err := os.WriteFile(pasPath, pasContent, 0600); err != nil {
t.Fatalf("failed to write pas file: %v", err)
}

// 2. Create a non-Delphi .txt file with LF line endings
txtPath := filepath.Join(tempDir, "readme.txt")
txtContent := []byte("hello\nworld\n")
if err := os.WriteFile(txtPath, txtContent, 0600); err != nil {
t.Fatalf("failed to write txt file: %v", err)
}

// Run the normalizer
if err := normalizer.NormalizeDirectoryLineEndings(tempDir); err != nil {
t.Fatalf("NormalizeDirectoryLineEndings failed: %v", err)
}

// 3. Verify .pas file was converted to CRLF
newPasContent, err := os.ReadFile(pasPath)
if err != nil {
t.Fatalf("failed to read pas file: %v", err)
}
if !bytes.Contains(newPasContent, []byte("\r\n")) {
t.Error("expected pas file to contain CRLF line endings")
}
if bytes.Contains(newPasContent, []byte("\n")) && !bytes.Contains(newPasContent, []byte("\r\n")) {
t.Error("expected pas file to have all LFs preceded by CR")
}

// 4. Verify .txt file was NOT converted (remains LF)
newTxtContent, err := os.ReadFile(txtPath)
if err != nil {
t.Fatalf("failed to read txt file: %v", err)
}
if bytes.Contains(newTxtContent, []byte("\r\n")) {
t.Error("expected txt file to NOT contain CRLF line endings")
}
}
Loading