feat: add firejailed support
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				Pre-commit / pre-commit (pull_request) Successful in 7m40s
				
			
		
		
	
	
				
					
				
			
		
			All checks were successful
		
		
	
	Pre-commit / pre-commit (pull_request) Successful in 7m40s
				
			This commit is contained in:
		| @@ -11,7 +11,7 @@ | |||||||
|     <g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"> |     <g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"> | ||||||
|         <text x="33.5" y="15" fill="#010101" fill-opacity=".3">coverage</text> |         <text x="33.5" y="15" fill="#010101" fill-opacity=".3">coverage</text> | ||||||
|         <text x="33.5" y="14">coverage</text> |         <text x="33.5" y="14">coverage</text> | ||||||
|         <text x="86" y="15" fill="#010101" fill-opacity=".3">17.5%</text> |         <text x="86" y="15" fill="#010101" fill-opacity=".3">19.0%</text> | ||||||
|         <text x="86" y="14">17.5%</text> |         <text x="86" y="14">19.0%</text> | ||||||
|     </g> |     </g> | ||||||
| </svg> | </svg> | ||||||
|   | |||||||
| Before Width: | Height: | Size: 926 B After Width: | Height: | Size: 926 B | 
							
								
								
									
										41
									
								
								e2e-tests/firejailed_package_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								e2e-tests/firejailed_package_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | |||||||
|  | // ALR - Any Linux Repository | ||||||
|  | // Copyright (C) 2025 The ALR Authors | ||||||
|  | // | ||||||
|  | // This program is free software: you can redistribute it and/or modify | ||||||
|  | // it under the terms of the GNU General Public License as published by | ||||||
|  | // the Free Software Foundation, either version 3 of the License, or | ||||||
|  | // (at your option) any later version. | ||||||
|  | // | ||||||
|  | // This program is distributed in the hope that it will be useful, | ||||||
|  | // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | // GNU General Public License for more details. | ||||||
|  | // | ||||||
|  | // You should have received a copy of the GNU General Public License | ||||||
|  | // along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  |  | ||||||
|  | //go:build e2e | ||||||
|  |  | ||||||
|  | package e2etests_test | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	"github.com/efficientgo/e2e" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestE2EFirejailedPackage(t *testing.T) { | ||||||
|  | 	dockerMultipleRun( | ||||||
|  | 		t, | ||||||
|  | 		"firejailed-package", | ||||||
|  | 		COMMON_SYSTEMS, | ||||||
|  | 		func(t *testing.T, r e2e.Runnable) { | ||||||
|  | 			defaultPrepare(t, r) | ||||||
|  | 			execShouldNoError(t, r, "alr", "build", "-p", fmt.Sprintf("%s/firejailed-pkg", REPO_NAME_FOR_E2E_TESTS)) | ||||||
|  | 			execShouldError(t, r, "alr", "build", "-p", fmt.Sprintf("%s/firejailed-pkg-incorrect", REPO_NAME_FOR_E2E_TESTS)) | ||||||
|  | 			execShouldNoError(t, r, "sh", "-c", "dpkg -c *.deb | grep -q '/usr/lib/alr/firejailed/_usr_bin_danger.sh'") | ||||||
|  | 			execShouldNoError(t, r, "sh", "-c", "dpkg -c *.deb | grep -q '/usr/lib/alr/firejailed/_usr_bin_danger.sh.profile'") | ||||||
|  | 		}, | ||||||
|  | 	) | ||||||
|  | } | ||||||
							
								
								
									
										237
									
								
								internal/build/firejail.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										237
									
								
								internal/build/firejail.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,237 @@ | |||||||
|  | // ALR - Any Linux Repository | ||||||
|  | // Copyright (C) 2025 The ALR Authors | ||||||
|  | // | ||||||
|  | // This program is free software: you can redistribute it and/or modify | ||||||
|  | // it under the terms of the GNU General Public License as published by | ||||||
|  | // the Free Software Foundation, either version 3 of the License, or | ||||||
|  | // (at your option) any later version. | ||||||
|  | // | ||||||
|  | // This program is distributed in the hope that it will be useful, | ||||||
|  | // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | // GNU General Public License for more details. | ||||||
|  | // | ||||||
|  | // You should have received a copy of the GNU General Public License | ||||||
|  | // along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  |  | ||||||
|  | package build | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"io" | ||||||
|  | 	"log/slog" | ||||||
|  | 	"os" | ||||||
|  | 	"path/filepath" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
|  | 	"github.com/goreleaser/nfpm/v2/files" | ||||||
|  | 	"github.com/leonelquinteros/gotext" | ||||||
|  |  | ||||||
|  | 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/osutils" | ||||||
|  | 	"gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh" | ||||||
|  | 	"gitea.plemya-x.ru/Plemya-x/ALR/pkg/types" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	firejailedDir     = "/usr/lib/alr/firejailed" | ||||||
|  | 	defaultDirMode    = 0o755 | ||||||
|  | 	defaultScriptMode = 0o755 | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	ErrInvalidDestination = errors.New("invalid destination path") | ||||||
|  | 	ErrMissingProfile     = errors.New("default profile is missing") | ||||||
|  | 	ErrEmptyPackageName   = errors.New("package name cannot be empty") | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var binaryDirectories = []string{ | ||||||
|  | 	"/usr/bin/", | ||||||
|  | 	"/bin/", | ||||||
|  | 	"/usr/local/bin/", | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func applyFirejailIntegration( | ||||||
|  | 	vars *alrsh.Package, | ||||||
|  | 	dirs types.Directories, | ||||||
|  | 	contents []*files.Content, | ||||||
|  | ) ([]*files.Content, error) { | ||||||
|  | 	slog.Info(gotext.Get("Applying FireJail integration"), "package", vars.Name) | ||||||
|  |  | ||||||
|  | 	if err := createFirejailedDirectory(dirs.PkgDir); err != nil { | ||||||
|  | 		return nil, fmt.Errorf("failed to create firejailed directory: %w", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	newContents, err := processBinaryFiles(vars, contents, dirs) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("failed to process binary files: %w", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return append(contents, newContents...), nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func createFirejailedDirectory(pkgDir string) error { | ||||||
|  | 	firejailedPath := filepath.Join(pkgDir, firejailedDir) | ||||||
|  | 	return os.MkdirAll(firejailedPath, defaultDirMode) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func processBinaryFiles(pkg *alrsh.Package, contents []*files.Content, dirs types.Directories) ([]*files.Content, error) { | ||||||
|  | 	var newContents []*files.Content | ||||||
|  |  | ||||||
|  | 	for _, content := range contents { | ||||||
|  | 		if content.Type == "dir" { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if !isBinaryFile(content.Destination) { | ||||||
|  | 			slog.Debug("content not binary file", "content", content) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		slog.Debug("process content", "content", content) | ||||||
|  |  | ||||||
|  | 		newContent, err := createFirejailedBinary(pkg, content, dirs) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, fmt.Errorf("failed to create firejailed binary for %s: %w", content.Destination, err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if newContent != nil { | ||||||
|  | 			newContents = append(newContents, newContent...) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return newContents, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func isBinaryFile(destination string) bool { | ||||||
|  | 	for _, binDir := range binaryDirectories { | ||||||
|  | 		if strings.HasPrefix(destination, binDir) { | ||||||
|  | 			return true | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func createFirejailedBinary( | ||||||
|  | 	pkg *alrsh.Package, | ||||||
|  | 	content *files.Content, | ||||||
|  | 	dirs types.Directories, | ||||||
|  | ) ([]*files.Content, error) { | ||||||
|  | 	origFilePath, err := generateFirejailedPath(content.Destination) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	profiles := pkg.FireJailProfiles.Resolved() | ||||||
|  | 	sourceProfilePath, ok := profiles[content.Destination] | ||||||
|  |  | ||||||
|  | 	if !ok { | ||||||
|  | 		sourceProfilePath, ok = profiles["default"] | ||||||
|  | 		if !ok { | ||||||
|  | 			return nil, errors.New("default profile is missing") | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	sourceProfilePath = filepath.Join(dirs.ScriptDir, sourceProfilePath) | ||||||
|  | 	dest, err := createFirejailProfilePath(content.Destination) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	err = createProfile(filepath.Join(dirs.PkgDir, dest), sourceProfilePath) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := osutils.Move(content.Source, filepath.Join(dirs.PkgDir, origFilePath)); err != nil { | ||||||
|  | 		return nil, fmt.Errorf("failed to move original binary: %w", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Create wrapper script | ||||||
|  | 	if err := createWrapperScript(content.Source, origFilePath, dest); err != nil { | ||||||
|  | 		return nil, fmt.Errorf("failed to create wrapper script: %w", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	profile, err := getContentFromPath(dest, dirs.PkgDir) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	bin, err := getContentFromPath(origFilePath, dirs.PkgDir) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return []*files.Content{ | ||||||
|  | 		bin, | ||||||
|  | 		profile, | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func getContentFromPath(path, base string) (*files.Content, error) { | ||||||
|  | 	absPath := filepath.Join(base, path) | ||||||
|  |  | ||||||
|  | 	fi, err := os.Lstat(absPath) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("failed to get file info: %w", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return &files.Content{ | ||||||
|  | 		Source:      absPath, | ||||||
|  | 		Destination: path, | ||||||
|  | 		FileInfo: &files.ContentFileInfo{ | ||||||
|  | 			MTime: fi.ModTime(), | ||||||
|  | 			Mode:  fi.Mode(), | ||||||
|  | 			Size:  fi.Size(), | ||||||
|  | 		}, | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func generateSafeName(destination string) (string, error) { | ||||||
|  | 	cleanPath := strings.TrimPrefix(destination, ".") | ||||||
|  | 	if cleanPath == "" { | ||||||
|  | 		return "", fmt.Errorf("invalid destination path: %s", destination) | ||||||
|  | 	} | ||||||
|  | 	return strings.ReplaceAll(cleanPath, "/", "_"), nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func generateFirejailedPath(destination string) (string, error) { | ||||||
|  | 	safeName, err := generateSafeName(destination) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	return filepath.Join(firejailedDir, safeName), nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func createProfile(destProfilePath, profilePath string) error { | ||||||
|  | 	srcFile, err := os.Open(profilePath) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	defer srcFile.Close() | ||||||
|  |  | ||||||
|  | 	destFile, err := os.Create(destProfilePath) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	defer destFile.Close() | ||||||
|  |  | ||||||
|  | 	_, err = io.Copy(destFile, srcFile) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	return destFile.Sync() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func createWrapperScript(scriptPath, origFilePath, profilePath string) error { | ||||||
|  | 	scriptContent := fmt.Sprintf("#!/bin/bash\nexec firejail --profile=%q %q \"$@\"\n", profilePath, origFilePath) | ||||||
|  | 	return os.WriteFile(scriptPath, []byte(scriptContent), defaultDirMode) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func createFirejailProfilePath(binaryPath string) (string, error) { | ||||||
|  | 	name, err := generateSafeName(binaryPath) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	return filepath.Join(firejailedDir, fmt.Sprintf("%s.profile", name)), nil | ||||||
|  | } | ||||||
							
								
								
									
										316
									
								
								internal/build/firejail_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										316
									
								
								internal/build/firejail_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,316 @@ | |||||||
|  | // ALR - Any Linux Repository | ||||||
|  | // Copyright (C) 2025 The ALR Authors | ||||||
|  | // | ||||||
|  | // This program is free software: you can redistribute it and/or modify | ||||||
|  | // it under the terms of the GNU General Public License as published by | ||||||
|  | // the Free Software Foundation, either version 3 of the License, or | ||||||
|  | // (at your option) any later version. | ||||||
|  | // | ||||||
|  | // This program is distributed in the hope that it will be useful, | ||||||
|  | // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | // GNU General Public License for more details. | ||||||
|  | // | ||||||
|  | // You should have received a copy of the GNU General Public License | ||||||
|  | // along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  |  | ||||||
|  | package build | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"os" | ||||||
|  | 	"path/filepath" | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	"github.com/goreleaser/nfpm/v2/files" | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  |  | ||||||
|  | 	"gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh" | ||||||
|  | 	"gitea.plemya-x.ru/Plemya-x/ALR/pkg/types" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestIsBinaryFile(t *testing.T) { | ||||||
|  | 	tests := []struct { | ||||||
|  | 		name        string | ||||||
|  | 		destination string | ||||||
|  | 		expected    bool | ||||||
|  | 	}{ | ||||||
|  | 		{"usr/bin binary", "/usr/bin/test", true}, | ||||||
|  | 		{"bin binary", "/bin/test", true}, | ||||||
|  | 		{"usr/local/bin binary", "/usr/local/bin/test", true}, | ||||||
|  | 		{"lib file", "/usr/lib/test.so", false}, | ||||||
|  | 		{"etc file", "/etc/config", false}, | ||||||
|  | 		{"empty destination", "", false}, | ||||||
|  | 		{"root level file", "./test", false}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, tt := range tests { | ||||||
|  | 		t.Run(tt.name, func(t *testing.T) { | ||||||
|  | 			result := isBinaryFile(tt.destination) | ||||||
|  | 			assert.Equal(t, tt.expected, result) | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestGenerateSafeName(t *testing.T) { | ||||||
|  | 	tests := []struct { | ||||||
|  | 		name        string | ||||||
|  | 		destination string | ||||||
|  | 		expected    string | ||||||
|  | 		expectError bool | ||||||
|  | 	}{ | ||||||
|  | 		{"usr/bin path", "./usr/bin/test", "_usr_bin_test", false}, | ||||||
|  | 		{"bin path", "./bin/test", "_bin_test", false}, | ||||||
|  | 		{"nested path", "./usr/local/bin/app", "_usr_local_bin_app", false}, | ||||||
|  | 		{"path with spaces", "./usr/bin/my app", "_usr_bin_my app", false}, | ||||||
|  | 		{"empty after trim", ".", "", true}, | ||||||
|  | 		{"empty string", "", "", true}, | ||||||
|  | 		{"only dots", "..", ".", false}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, tt := range tests { | ||||||
|  | 		t.Run(tt.name, func(t *testing.T) { | ||||||
|  | 			result, err := generateSafeName(tt.destination) | ||||||
|  | 			if tt.expectError { | ||||||
|  | 				assert.Error(t, err) | ||||||
|  | 			} else { | ||||||
|  | 				assert.NoError(t, err) | ||||||
|  | 				assert.Equal(t, tt.expected, result) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestCreateWrapperScript(t *testing.T) { | ||||||
|  | 	tests := []struct { | ||||||
|  | 		name            string | ||||||
|  | 		origFilePath    string | ||||||
|  | 		profilePath     string | ||||||
|  | 		expectedContent string | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			"basic wrapper", | ||||||
|  | 			"/usr/lib/alr/firejailed/_usr_bin_test", | ||||||
|  | 			"/usr/lib/alr/firejailed/_usr_bin_test.profile", | ||||||
|  | 			"#!/bin/bash\nexec firejail --profile=\"/usr/lib/alr/firejailed/_usr_bin_test.profile\" \"/usr/lib/alr/firejailed/_usr_bin_test\" \"$@\"\n", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			"path with spaces", | ||||||
|  | 			"/usr/lib/alr/firejailed/_usr_bin_my_app", | ||||||
|  | 			"/usr/lib/alr/firejailed/_usr_bin_my_app.profile", | ||||||
|  | 			"#!/bin/bash\nexec firejail --profile=\"/usr/lib/alr/firejailed/_usr_bin_my_app.profile\" \"/usr/lib/alr/firejailed/_usr_bin_my_app\" \"$@\"\n", | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, tt := range tests { | ||||||
|  | 		t.Run(tt.name, func(t *testing.T) { | ||||||
|  | 			tmpDir := t.TempDir() | ||||||
|  | 			scriptPath := filepath.Join(tmpDir, "wrapper.sh") | ||||||
|  |  | ||||||
|  | 			err := createWrapperScript(scriptPath, tt.origFilePath, tt.profilePath) | ||||||
|  |  | ||||||
|  | 			assert.NoError(t, err) | ||||||
|  | 			assert.FileExists(t, scriptPath) | ||||||
|  |  | ||||||
|  | 			content, err := os.ReadFile(scriptPath) | ||||||
|  | 			assert.NoError(t, err) | ||||||
|  | 			assert.Equal(t, tt.expectedContent, string(content)) | ||||||
|  |  | ||||||
|  | 			// Check file permissions | ||||||
|  | 			info, err := os.Stat(scriptPath) | ||||||
|  | 			assert.NoError(t, err) | ||||||
|  | 			assert.Equal(t, os.FileMode(defaultDirMode), info.Mode()) | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestCreateFirejailedBinary(t *testing.T) { | ||||||
|  | 	tests := []struct { | ||||||
|  | 		name        string | ||||||
|  | 		setupFunc   func(string) (*alrsh.Package, *files.Content, types.Directories) | ||||||
|  | 		expectError bool | ||||||
|  | 		errorMsg    string | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			"successful creation with default profile", | ||||||
|  | 			func(tmpDir string) (*alrsh.Package, *files.Content, types.Directories) { | ||||||
|  | 				pkgDir := filepath.Join(tmpDir, "pkg") | ||||||
|  | 				scriptDir := filepath.Join(tmpDir, "scripts") | ||||||
|  | 				os.MkdirAll(pkgDir, 0o755) | ||||||
|  | 				os.MkdirAll(scriptDir, 0o755) | ||||||
|  |  | ||||||
|  | 				srcBinary := filepath.Join(tmpDir, "test-binary") | ||||||
|  | 				os.WriteFile(srcBinary, []byte("#!/bin/bash\necho test"), 0o755) | ||||||
|  |  | ||||||
|  | 				defaultProfile := filepath.Join(scriptDir, "default.profile") | ||||||
|  | 				os.WriteFile(defaultProfile, []byte("include /etc/firejail/default.profile\nnet none"), 0o644) | ||||||
|  |  | ||||||
|  | 				pkg := &alrsh.Package{ | ||||||
|  | 					Name: "test-pkg", | ||||||
|  | 					FireJailProfiles: alrsh.OverridableFromMap(map[string]map[string]string{ | ||||||
|  | 						"": {"default": "default.profile"}, | ||||||
|  | 					}), | ||||||
|  | 				} | ||||||
|  | 				alrsh.ResolvePackage(pkg, []string{""}) | ||||||
|  |  | ||||||
|  | 				content := &files.Content{ | ||||||
|  | 					Source:      srcBinary, | ||||||
|  | 					Destination: "./usr/bin/test-binary", | ||||||
|  | 					Type:        "file", | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				dirs := types.Directories{PkgDir: pkgDir, ScriptDir: scriptDir} | ||||||
|  | 				return pkg, content, dirs | ||||||
|  | 			}, | ||||||
|  | 			false, | ||||||
|  | 			"", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			"successful creation with specific profile", | ||||||
|  | 			func(tmpDir string) (*alrsh.Package, *files.Content, types.Directories) { | ||||||
|  | 				pkgDir := filepath.Join(tmpDir, "pkg") | ||||||
|  | 				scriptDir := filepath.Join(tmpDir, "scripts") | ||||||
|  | 				os.MkdirAll(pkgDir, 0o755) | ||||||
|  | 				os.MkdirAll(scriptDir, 0o755) | ||||||
|  |  | ||||||
|  | 				srcBinary := filepath.Join(tmpDir, "special-binary") | ||||||
|  | 				os.WriteFile(srcBinary, []byte("#!/bin/bash\necho special"), 0o755) | ||||||
|  |  | ||||||
|  | 				defaultProfile := filepath.Join(scriptDir, "default.profile") | ||||||
|  | 				os.WriteFile(defaultProfile, []byte("include /etc/firejail/default.profile"), 0o644) | ||||||
|  |  | ||||||
|  | 				specialProfile := filepath.Join(scriptDir, "special.profile") | ||||||
|  | 				os.WriteFile(specialProfile, []byte("include /etc/firejail/default.profile\nnet none\nprivate-tmp"), 0o644) | ||||||
|  |  | ||||||
|  | 				pkg := &alrsh.Package{ | ||||||
|  | 					Name: "test-pkg", | ||||||
|  | 					FireJailProfiles: alrsh.OverridableFromMap(map[string]map[string]string{ | ||||||
|  | 						"": {"default": "default.profile", "/usr/bin/special-binary": "special.profile"}, | ||||||
|  | 					}), | ||||||
|  | 				} | ||||||
|  | 				alrsh.ResolvePackage(pkg, []string{""}) | ||||||
|  |  | ||||||
|  | 				content := &files.Content{ | ||||||
|  | 					Source:      srcBinary, | ||||||
|  | 					Destination: "./usr/bin/special-binary", | ||||||
|  | 					Type:        "file", | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				dirs := types.Directories{PkgDir: pkgDir, ScriptDir: scriptDir} | ||||||
|  | 				return pkg, content, dirs | ||||||
|  | 			}, | ||||||
|  | 			false, | ||||||
|  | 			"", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			"missing default profile", | ||||||
|  | 			func(tmpDir string) (*alrsh.Package, *files.Content, types.Directories) { | ||||||
|  | 				pkgDir := filepath.Join(tmpDir, "pkg") | ||||||
|  | 				scriptDir := filepath.Join(tmpDir, "scripts") | ||||||
|  | 				os.MkdirAll(pkgDir, 0o755) | ||||||
|  | 				os.MkdirAll(scriptDir, 0o755) | ||||||
|  |  | ||||||
|  | 				srcBinary := filepath.Join(tmpDir, "test-binary") | ||||||
|  | 				os.WriteFile(srcBinary, []byte("#!/bin/bash\necho test"), 0o755) | ||||||
|  |  | ||||||
|  | 				pkg := &alrsh.Package{ | ||||||
|  | 					Name:             "test-pkg", | ||||||
|  | 					FireJailProfiles: alrsh.OverridableFromMap(map[string]map[string]string{"": {}}), | ||||||
|  | 				} | ||||||
|  | 				alrsh.ResolvePackage(pkg, []string{""}) | ||||||
|  |  | ||||||
|  | 				content := &files.Content{Source: srcBinary, Destination: "./usr/bin/test-binary", Type: "file"} | ||||||
|  | 				dirs := types.Directories{PkgDir: pkgDir, ScriptDir: scriptDir} | ||||||
|  | 				return pkg, content, dirs | ||||||
|  | 			}, | ||||||
|  | 			true, | ||||||
|  | 			"default profile is missing", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			"profile file not found", | ||||||
|  | 			func(tmpDir string) (*alrsh.Package, *files.Content, types.Directories) { | ||||||
|  | 				pkgDir := filepath.Join(tmpDir, "pkg") | ||||||
|  | 				scriptDir := filepath.Join(tmpDir, "scripts") | ||||||
|  | 				os.MkdirAll(pkgDir, 0o755) | ||||||
|  | 				os.MkdirAll(scriptDir, 0o755) | ||||||
|  |  | ||||||
|  | 				srcBinary := filepath.Join(tmpDir, "test-binary") | ||||||
|  | 				os.WriteFile(srcBinary, []byte("#!/bin/bash\necho test"), 0o755) | ||||||
|  |  | ||||||
|  | 				pkg := &alrsh.Package{ | ||||||
|  | 					Name:             "test-pkg", | ||||||
|  | 					FireJailProfiles: alrsh.OverridableFromMap(map[string]map[string]string{"": {"default": "nonexistent.profile"}}), | ||||||
|  | 				} | ||||||
|  | 				alrsh.ResolvePackage(pkg, []string{""}) | ||||||
|  |  | ||||||
|  | 				content := &files.Content{Source: srcBinary, Destination: "./usr/bin/test-binary", Type: "file"} | ||||||
|  | 				dirs := types.Directories{PkgDir: pkgDir, ScriptDir: scriptDir} | ||||||
|  | 				return pkg, content, dirs | ||||||
|  | 			}, | ||||||
|  | 			true, | ||||||
|  | 			"", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			"invalid destination path", | ||||||
|  | 			func(tmpDir string) (*alrsh.Package, *files.Content, types.Directories) { | ||||||
|  | 				pkgDir := filepath.Join(tmpDir, "pkg") | ||||||
|  | 				scriptDir := filepath.Join(tmpDir, "scripts") | ||||||
|  | 				os.MkdirAll(pkgDir, 0o755) | ||||||
|  | 				os.MkdirAll(scriptDir, 0o755) | ||||||
|  |  | ||||||
|  | 				srcBinary := filepath.Join(tmpDir, "test-binary") | ||||||
|  | 				os.WriteFile(srcBinary, []byte("#!/bin/bash\necho test"), 0o755) | ||||||
|  |  | ||||||
|  | 				defaultProfile := filepath.Join(scriptDir, "default.profile") | ||||||
|  | 				os.WriteFile(defaultProfile, []byte("include /etc/firejail/default.profile"), 0o644) | ||||||
|  |  | ||||||
|  | 				pkg := &alrsh.Package{Name: "test-pkg", FireJailProfiles: alrsh.OverridableFromMap(map[string]map[string]string{"": {"default": "default.profile"}})} | ||||||
|  | 				alrsh.ResolvePackage(pkg, []string{""}) | ||||||
|  |  | ||||||
|  | 				content := &files.Content{Source: srcBinary, Destination: ".", Type: "file"} | ||||||
|  | 				dirs := types.Directories{PkgDir: pkgDir, ScriptDir: scriptDir} | ||||||
|  | 				return pkg, content, dirs | ||||||
|  | 			}, | ||||||
|  | 			true, | ||||||
|  | 			"", | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, tt := range tests { | ||||||
|  | 		t.Run(tt.name, func(t *testing.T) { | ||||||
|  | 			tmpDir := t.TempDir() | ||||||
|  | 			pkg, content, dirs := tt.setupFunc(tmpDir) | ||||||
|  |  | ||||||
|  | 			err := createFirejailedDirectory(dirs.PkgDir) | ||||||
|  | 			assert.NoError(t, err) | ||||||
|  |  | ||||||
|  | 			result, err := createFirejailedBinary(pkg, content, dirs) | ||||||
|  |  | ||||||
|  | 			if tt.expectError { | ||||||
|  | 				assert.Error(t, err) | ||||||
|  | 				if tt.errorMsg != "" { | ||||||
|  | 					assert.Contains(t, err.Error(), tt.errorMsg) | ||||||
|  | 				} | ||||||
|  | 				assert.Nil(t, result) | ||||||
|  | 			} else { | ||||||
|  | 				assert.NoError(t, err) | ||||||
|  | 				assert.Len(t, result, 2) | ||||||
|  |  | ||||||
|  | 				binContent := result[0] | ||||||
|  | 				assert.Contains(t, binContent.Destination, "usr/lib/alr/firejailed/") | ||||||
|  | 				assert.FileExists(t, binContent.Source) | ||||||
|  |  | ||||||
|  | 				profileContent := result[1] | ||||||
|  | 				assert.Contains(t, profileContent.Destination, "usr/lib/alr/firejailed/") | ||||||
|  | 				assert.Contains(t, profileContent.Destination, ".profile") | ||||||
|  | 				assert.FileExists(t, profileContent.Source) | ||||||
|  |  | ||||||
|  | 				assert.FileExists(t, content.Source) | ||||||
|  | 				wrapperBytes, err := os.ReadFile(content.Source) | ||||||
|  | 				assert.NoError(t, err) | ||||||
|  | 				wrapper := string(wrapperBytes) | ||||||
|  | 				assert.Contains(t, wrapper, "#!/bin/bash") | ||||||
|  | 				assert.Contains(t, wrapper, "firejail --profile=") | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -248,6 +248,16 @@ func buildPkgMetadata( | |||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	normalizeContents(contents) | ||||||
|  |  | ||||||
|  | 	if vars.FireJailed.Resolved() { | ||||||
|  | 		contents, err = applyFirejailIntegration(vars, dirs, contents) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	pkgInfo.Overridables.Contents = contents | 	pkgInfo.Overridables.Contents = contents | ||||||
|  |  | ||||||
| 	if len(vars.AutoProv.Resolved()) == 1 && decoder.IsTruthy(vars.AutoProv.Resolved()[0]) { | 	if len(vars.AutoProv.Resolved()) == 1 && decoder.IsTruthy(vars.AutoProv.Resolved()[0]) { | ||||||
|   | |||||||
| @@ -154,6 +154,12 @@ func buildContents(vars *alrsh.Package, dirs types.Directories, preferedContents | |||||||
| 	return contents, nil | 	return contents, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func normalizeContents(contents []*files.Content) { | ||||||
|  | 	for _, content := range contents { | ||||||
|  | 		content.Destination = filepath.Join("/", content.Destination) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
| var RegexpALRPackageName = regexp.MustCompile(`^(?P<package>[^+]+)\+alr-(?P<repo>.+)$`) | var RegexpALRPackageName = regexp.MustCompile(`^(?P<package>[^+]+)\+alr-(?P<repo>.+)$`) | ||||||
|  |  | ||||||
| func getBasePkgInfo(vars *alrsh.Package, input interface { | func getBasePkgInfo(vars *alrsh.Package, input interface { | ||||||
|   | |||||||
| @@ -220,19 +220,23 @@ msgstr "" | |||||||
| msgid "AutoReq is not implemented for this package format, so it's skipped" | msgid "AutoReq is not implemented for this package format, so it's skipped" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: internal/build/firejail.go:59 | ||||||
|  | msgid "Applying FireJail integration" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: internal/build/script_executor.go:145 | #: internal/build/script_executor.go:145 | ||||||
| msgid "Building package metadata" | msgid "Building package metadata" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: internal/build/script_executor.go:275 | #: internal/build/script_executor.go:285 | ||||||
| msgid "Executing prepare()" | msgid "Executing prepare()" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: internal/build/script_executor.go:284 | #: internal/build/script_executor.go:294 | ||||||
| msgid "Executing build()" | msgid "Executing build()" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: internal/build/script_executor.go:313 internal/build/script_executor.go:333 | #: internal/build/script_executor.go:323 internal/build/script_executor.go:343 | ||||||
| msgid "Executing %s()" | msgid "Executing %s()" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|   | |||||||
| @@ -5,15 +5,15 @@ | |||||||
| msgid "" | msgid "" | ||||||
| msgstr "" | msgstr "" | ||||||
| "Project-Id-Version: unnamed project\n" | "Project-Id-Version: unnamed project\n" | ||||||
| "PO-Revision-Date: 2025-05-16 20:47+0300\n" | "PO-Revision-Date: 2025-06-15 16:05+0300\n" | ||||||
| "Last-Translator: Maxim Slipenko <maks1ms@alt-gnome.ru>\n" | "Last-Translator: Maxim Slipenko <maks1ms@alt-gnome.ru>\n" | ||||||
| "Language-Team: Russian\n" | "Language-Team: Russian\n" | ||||||
| "Language: ru\n" | "Language: ru\n" | ||||||
| "MIME-Version: 1.0\n" | "MIME-Version: 1.0\n" | ||||||
| "Content-Type: text/plain; charset=UTF-8\n" | "Content-Type: text/plain; charset=UTF-8\n" | ||||||
| "Content-Transfer-Encoding: 8bit\n" | "Content-Transfer-Encoding: 8bit\n" | ||||||
| "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " | ||||||
| "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" | "n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" | ||||||
| "X-Generator: Gtranslator 48.0\n" | "X-Generator: Gtranslator 48.0\n" | ||||||
|  |  | ||||||
| #: build.go:42 | #: build.go:42 | ||||||
| @@ -231,19 +231,23 @@ msgid "AutoReq is not implemented for this package format, so it's skipped" | |||||||
| msgstr "" | msgstr "" | ||||||
| "AutoReq не реализовано для этого формата пакета, поэтому будет пропущено" | "AutoReq не реализовано для этого формата пакета, поэтому будет пропущено" | ||||||
|  |  | ||||||
|  | #: internal/build/firejail.go:59 | ||||||
|  | msgid "Applying FireJail integration" | ||||||
|  | msgstr "Применение интеграции FireJail" | ||||||
|  |  | ||||||
| #: internal/build/script_executor.go:145 | #: internal/build/script_executor.go:145 | ||||||
| msgid "Building package metadata" | msgid "Building package metadata" | ||||||
| msgstr "Сборка метаданных пакета" | msgstr "Сборка метаданных пакета" | ||||||
|  |  | ||||||
| #: internal/build/script_executor.go:275 | #: internal/build/script_executor.go:285 | ||||||
| msgid "Executing prepare()" | msgid "Executing prepare()" | ||||||
| msgstr "Выполнение prepare()" | msgstr "Выполнение prepare()" | ||||||
|  |  | ||||||
| #: internal/build/script_executor.go:284 | #: internal/build/script_executor.go:294 | ||||||
| msgid "Executing build()" | msgid "Executing build()" | ||||||
| msgstr "Выполнение build()" | msgstr "Выполнение build()" | ||||||
|  |  | ||||||
| #: internal/build/script_executor.go:313 internal/build/script_executor.go:333 | #: internal/build/script_executor.go:323 internal/build/script_executor.go:343 | ||||||
| msgid "Executing %s()" | msgid "Executing %s()" | ||||||
| msgstr "Выполнение %s()" | msgstr "Выполнение %s()" | ||||||
|  |  | ||||||
| @@ -352,8 +356,8 @@ msgid "" | |||||||
| "This command is deprecated and would be removed in the future, use \"%s\" " | "This command is deprecated and would be removed in the future, use \"%s\" " | ||||||
| "instead!" | "instead!" | ||||||
| msgstr "" | msgstr "" | ||||||
| "Эта команда устарела и будет удалена в будущем, используйте вместо нее \"%s" | "Эта команда устарела и будет удалена в будущем, используйте вместо нее " | ||||||
| "\"!" | "\"%s\"!" | ||||||
|  |  | ||||||
| #: internal/db/db.go:76 | #: internal/db/db.go:76 | ||||||
| msgid "Database version mismatch; resetting" | msgid "Database version mismatch; resetting" | ||||||
|   | |||||||
| @@ -68,6 +68,9 @@ type Package struct { | |||||||
| 	AutoProv         OverridableField[[]string] `sh:"auto_prov" xorm:"-"` | 	AutoProv         OverridableField[[]string] `sh:"auto_prov" xorm:"-"` | ||||||
| 	AutoReqSkipList  OverridableField[[]string] `sh:"auto_req_skiplist" xorm:"-"` | 	AutoReqSkipList  OverridableField[[]string] `sh:"auto_req_skiplist" xorm:"-"` | ||||||
| 	AutoProvSkipList OverridableField[[]string] `sh:"auto_prov_skiplist" xorm:"-"` | 	AutoProvSkipList OverridableField[[]string] `sh:"auto_prov_skiplist" xorm:"-"` | ||||||
|  |  | ||||||
|  | 	FireJailed       OverridableField[bool]              `sh:"firejailed" xorm:"-"` | ||||||
|  | 	FireJailProfiles OverridableField[map[string]string] `sh:"firejail_profiles" xorm:"-"` | ||||||
| } | } | ||||||
|  |  | ||||||
| type Scripts struct { | type Scripts struct { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user