feat: add firejailed support #106
| @@ -11,7 +11,7 @@ | ||||
|     <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="14">coverage</text> | ||||
|         <text x="86" y="15" fill="#010101" fill-opacity=".3">17.5%</text> | ||||
|         <text x="86" y="14">17.5%</text> | ||||
|         <text x="86" y="15" fill="#010101" fill-opacity=".3">19.0%</text> | ||||
|         <text x="86" y="14">19.0%</text> | ||||
|     </g> | ||||
| </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 { | ||||
| 		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 | ||||
|  | ||||
| 	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 | ||||
| } | ||||
|  | ||||
| func normalizeContents(contents []*files.Content) { | ||||
| 	for _, content := range contents { | ||||
| 		content.Destination = filepath.Join("/", content.Destination) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| var RegexpALRPackageName = regexp.MustCompile(`^(?P<package>[^+]+)\+alr-(?P<repo>.+)$`) | ||||
|  | ||||
| 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" | ||||
| msgstr "" | ||||
|  | ||||
| #: internal/build/firejail.go:59 | ||||
| msgid "Applying FireJail integration" | ||||
| msgstr "" | ||||
|  | ||||
| #: internal/build/script_executor.go:145 | ||||
| msgid "Building package metadata" | ||||
| msgstr "" | ||||
|  | ||||
| #: internal/build/script_executor.go:275 | ||||
| #: internal/build/script_executor.go:285 | ||||
| msgid "Executing prepare()" | ||||
| msgstr "" | ||||
|  | ||||
| #: internal/build/script_executor.go:284 | ||||
| #: internal/build/script_executor.go:294 | ||||
| msgid "Executing build()" | ||||
| 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()" | ||||
| msgstr "" | ||||
|  | ||||
|   | ||||
| @@ -5,15 +5,15 @@ | ||||
| msgid "" | ||||
| msgstr "" | ||||
| "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" | ||||
| "Language-Team: Russian\n" | ||||
| "Language: ru\n" | ||||
| "MIME-Version: 1.0\n" | ||||
| "Content-Type: text/plain; charset=UTF-8\n" | ||||
| "Content-Transfer-Encoding: 8bit\n" | ||||
| "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" | ||||
| "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" | ||||
| "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " | ||||
| "n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" | ||||
| "X-Generator: Gtranslator 48.0\n" | ||||
|  | ||||
| #: build.go:42 | ||||
| @@ -231,19 +231,23 @@ msgid "AutoReq is not implemented for this package format, so it's skipped" | ||||
| msgstr "" | ||||
| "AutoReq не реализовано для этого формата пакета, поэтому будет пропущено" | ||||
|  | ||||
| #: internal/build/firejail.go:59 | ||||
| msgid "Applying FireJail integration" | ||||
| msgstr "Применение интеграции FireJail" | ||||
|  | ||||
| #: internal/build/script_executor.go:145 | ||||
| msgid "Building package metadata" | ||||
| msgstr "Сборка метаданных пакета" | ||||
|  | ||||
| #: internal/build/script_executor.go:275 | ||||
| #: internal/build/script_executor.go:285 | ||||
| msgid "Executing prepare()" | ||||
| msgstr "Выполнение prepare()" | ||||
|  | ||||
| #: internal/build/script_executor.go:284 | ||||
| #: internal/build/script_executor.go:294 | ||||
| msgid "Executing 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()" | ||||
| msgstr "Выполнение %s()" | ||||
|  | ||||
| @@ -352,8 +356,8 @@ msgid "" | ||||
| "This command is deprecated and would be removed in the future, use \"%s\" " | ||||
| "instead!" | ||||
| msgstr "" | ||||
| "Эта команда устарела и будет удалена в будущем, используйте вместо нее \"%s" | ||||
| "\"!" | ||||
| "Эта команда устарела и будет удалена в будущем, используйте вместо нее " | ||||
| "\"%s\"!" | ||||
|  | ||||
| #: internal/db/db.go:76 | ||||
| msgid "Database version mismatch; resetting" | ||||
|   | ||||
| @@ -68,6 +68,9 @@ type Package struct { | ||||
| 	AutoProv         OverridableField[[]string] `sh:"auto_prov" xorm:"-"` | ||||
| 	AutoReqSkipList  OverridableField[[]string] `sh:"auto_req_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 { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user