параша

This commit is contained in:
itexpert228 2026-03-13 10:21:38 +03:00
parent 8db46a6b45
commit 38e92af8a3
No known key found for this signature in database
9 changed files with 779 additions and 11 deletions

View file

@ -15,6 +15,9 @@ zsvo build recipes/zlib.yaml
# Install package built by zsvo # Install package built by zsvo
zsvo install /path/to/name-version.pkg.tar.zst zsvo install /path/to/name-version.pkg.tar.zst
# Auto-build from Debian source and install
zsvo install neofetch
# Install to custom root # Install to custom root
zsvo install --root /mnt/root /path/to/name-version.pkg.tar.zst zsvo install --root /mnt/root /path/to/name-version.pkg.tar.zst
@ -71,6 +74,7 @@ Notes:
- `deps` are simple package names (no version solver yet). - `deps` are simple package names (no version solver yet).
- `{{pkgdir}}` (and `${pkgdir}`) points to staging DESTDIR. - `{{pkgdir}}` (and `${pkgdir}`) points to staging DESTDIR.
- If `source.debian_dsc` (or `.dsc` URL) is used, zsvo downloads only `*.orig*.tar.*` archives from that source package and ignores Debian patch archives (`debian.tar.*` / `diff.gz`). - If `source.debian_dsc` (or `.dsc` URL) is used, zsvo downloads only `*.orig*.tar.*` archives from that source package and ignores Debian patch archives (`debian.tar.*` / `diff.gz`).
- `zsvo install <name>` auto-resolves Debian source over HTTP from Debian `Sources` indexes, builds package in `--work-dir` and installs it.
- Package metadata is stored in `.zsvo.yml` (not `.PKGINFO`). - Package metadata is stored in `.zsvo.yml` (not `.PKGINFO`).
## Architecture ## Architecture

View file

@ -2,32 +2,84 @@ package cmd
import ( import (
"fmt" "fmt"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"zsvo/pkg/builder"
"zsvo/pkg/debian"
"zsvo/pkg/installer" "zsvo/pkg/installer"
"zsvo/pkg/recipe"
) )
var InstallCmd = &cobra.Command{ var InstallCmd = &cobra.Command{
Use: "install <package> [package...]", Use: "install <package> [package...]",
Short: "Install package(s)", Short: "Install package(s)",
Long: `Install one or more packages from package files`, Long: `Install one or more packages from local files or auto-build from Debian source by package name`,
Args: cobra.MinimumNArgs(1), Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
// Create installer
rootDir, _ := cmd.Flags().GetString("root") rootDir, _ := cmd.Flags().GetString("root")
if rootDir == "" { if rootDir == "" {
rootDir = "/" rootDir = "/"
} }
workDir, _ := cmd.Flags().GetString("work-dir")
if strings.TrimSpace(workDir) == "" {
workDir = "/tmp/pkg-work"
}
autoSource, _ := cmd.Flags().GetBool("auto-source")
installTargets := make([]string, 0, len(args))
var resolver *debian.Resolver
var b *builder.Builder
if autoSource {
resolver = debian.NewResolver()
b = builder.NewBuilder(workDir)
}
for _, target := range args {
isFile, err := isInstallFileTarget(target)
if err != nil {
return err
}
if isFile {
installTargets = append(installTargets, target)
continue
}
if !autoSource {
return fmt.Errorf(
"%s is not a package file; pass a file path or enable --auto-source",
target,
)
}
fmt.Printf("Resolving Debian source for %s...\n", target)
srcInfo, err := resolver.ResolveSource(target)
if err != nil {
return err
}
rcp := autoRecipeFromDebian(srcInfo)
fmt.Printf("Building %s from %s...\n", rcp.GetPackageName(), srcInfo.DSCURL)
if err := b.Build(rcp); err != nil {
return fmt.Errorf("failed to auto-build %s: %w", target, err)
}
builtPackage := filepath.Join(rcp.GetPackageDir(workDir), rcp.GetPackageFileName())
fmt.Printf("Built package: %s\n", builtPackage)
installTargets = append(installTargets, builtPackage)
}
i := installer.NewInstaller(rootDir) i := installer.NewInstaller(rootDir)
// Install packages if len(installTargets) == 1 {
if len(args) == 1 { fmt.Printf("Installing package from %s...\n", installTargets[0])
fmt.Printf("Installing package from %s...\n", args[0])
} else { } else {
fmt.Printf("Installing %d packages...\n", len(args)) fmt.Printf("Installing %d packages...\n", len(installTargets))
} }
if err := i.InstallMany(args); err != nil { if err := i.InstallMany(installTargets); err != nil {
return fmt.Errorf("failed to install packages: %w", err) return fmt.Errorf("failed to install packages: %w", err)
} }
@ -38,4 +90,80 @@ var InstallCmd = &cobra.Command{
func init() { func init() {
InstallCmd.Flags().StringP("root", "r", "/", "Root directory for installation") InstallCmd.Flags().StringP("root", "r", "/", "Root directory for installation")
InstallCmd.Flags().StringP("work-dir", "w", "/tmp/pkg-work", "Working directory for source builds")
InstallCmd.Flags().Bool("auto-source", true, "Auto-build package names from Debian source")
}
func isInstallFileTarget(target string) (bool, error) {
target = strings.TrimSpace(target)
if target == "" {
return false, fmt.Errorf("install target cannot be empty")
}
// Explicit file-like targets are validated as paths.
if looksLikeFilePath(target) {
info, err := os.Stat(target)
if err != nil {
return false, fmt.Errorf("package file %s not found: %w", target, err)
}
if info.IsDir() {
return false, fmt.Errorf("package file %s is a directory", target)
}
return true, nil
}
// If a same-name local file exists, use it as package archive.
if info, err := os.Stat(target); err == nil {
if info.IsDir() {
return false, fmt.Errorf("package file %s is a directory", target)
}
return true, nil
}
return false, nil
}
func looksLikeFilePath(target string) bool {
return strings.Contains(target, string(os.PathSeparator)) ||
strings.HasPrefix(target, ".") ||
strings.HasSuffix(target, ".pkg.tar.zst") ||
strings.HasSuffix(target, ".zov")
}
func autoRecipeFromDebian(src *debian.SourceInfo) *recipe.Recipe {
name := src.SourcePackage
if name == "" {
name = src.RequestedPackage
}
version := src.UpstreamVersion
if version == "" {
version = "0"
}
installCmd := fmt.Sprintf(
"if [ -f build/cmake_install.cmake ]; then DESTDIR={{pkgdir}} cmake --install build; "+
"elif [ -f build/meson-private/coredata.dat ]; then DESTDIR={{pkgdir}} meson install -C build; "+
"elif [ -f Makefile ] || [ -f makefile ] || [ -f GNUmakefile ]; then make DESTDIR={{pkgdir}} PREFIX=/usr install; "+
"elif [ -f %s ]; then install -Dm755 %s {{pkgdir}}/usr/bin/%s; fi",
name,
name,
name,
)
return &recipe.Recipe{
Name: name,
Version: version,
Description: fmt.Sprintf("Auto-generated build recipe from Debian source %s", src.DSCURL),
Source: recipe.Source{
DebianDSC: src.DSCURL,
Sha256: src.DSCSHA256,
},
Build: []string{
"[ -f configure ] && ./configure --prefix=/usr || true",
"if [ -f CMakeLists.txt ]; then cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr && cmake --build build -j${jobs}; " +
"elif [ -f meson.build ]; then meson setup build --prefix=/usr && meson compile -C build -j${jobs}; " +
"elif [ -f Makefile ] || [ -f makefile ] || [ -f GNUmakefile ]; then make -j${jobs}; fi",
},
Install: []string{installCmd},
}
} }

71
cmd/install_test.go Normal file
View file

@ -0,0 +1,71 @@
package cmd
import (
"os"
"path/filepath"
"testing"
"zsvo/pkg/debian"
)
func TestIsInstallFileTarget_File(t *testing.T) {
t.Parallel()
tmpDir := t.TempDir()
pkgPath := filepath.Join(tmpDir, "demo.pkg.tar.zst")
if err := os.WriteFile(pkgPath, []byte("x"), 0o644); err != nil {
t.Fatalf("failed to write temp package: %v", err)
}
isFile, err := isInstallFileTarget(pkgPath)
if err != nil {
t.Fatalf("isInstallFileTarget() error = %v", err)
}
if !isFile {
t.Fatalf("expected file target")
}
}
func TestIsInstallFileTarget_Name(t *testing.T) {
t.Parallel()
isFile, err := isInstallFileTarget("neofetch")
if err != nil {
t.Fatalf("isInstallFileTarget() error = %v", err)
}
if isFile {
t.Fatalf("expected package-name target")
}
}
func TestIsInstallFileTarget_MissingPath(t *testing.T) {
t.Parallel()
_, err := isInstallFileTarget("./missing.pkg.tar.zst")
if err == nil {
t.Fatalf("expected error for missing file path")
}
}
func TestAutoRecipeFromDebian(t *testing.T) {
t.Parallel()
r := autoRecipeFromDebian(&debian.SourceInfo{
RequestedPackage: "neofetch",
SourcePackage: "neofetch",
DSCURL: "https://deb.debian.org/debian/pool/main/n/neofetch/neofetch_7.1.0-4.dsc",
DSCSHA256: "deadbeef",
DebianVersion: "7.1.0-4",
UpstreamVersion: "7.1.0",
})
if r.Name != "neofetch" || r.Version != "7.1.0" {
t.Fatalf("unexpected recipe identity: %s %s", r.Name, r.Version)
}
if r.Source.DebianDSC == "" {
t.Fatalf("expected debian dsc source")
}
if len(r.Build) == 0 || len(r.Install) == 0 {
t.Fatalf("expected auto recipe build/install commands")
}
}

View file

@ -15,7 +15,7 @@ var rootCmd = &cobra.Command{
Available commands: Available commands:
build Build a package from recipe build Build a package from recipe
install Install package(s) from package files install Install package(s) from local files or auto-build by name
upgrade Upgrade package(s) from package files upgrade Upgrade package(s) from package files
remove Remove installed package(s) remove Remove installed package(s)
list List installed packages list List installed packages

367
pkg/debian/source.go Normal file
View file

@ -0,0 +1,367 @@
package debian
import (
"bufio"
"compress/gzip"
"fmt"
"io"
"net/http"
"path"
"regexp"
"strings"
"time"
"github.com/ulikunitz/xz"
)
var packageNamePattern = regexp.MustCompile(`^[a-z0-9][a-z0-9+.-]*$`)
const defaultDebianMirror = "https://deb.debian.org/debian"
var (
defaultSuites = []string{"stable", "testing", "unstable"}
defaultComponents = []string{"main", "contrib", "non-free", "non-free-firmware"}
)
// SourceInfo describes Debian source package coordinates resolved over HTTP.
type SourceInfo struct {
RequestedPackage string
SourcePackage string
DSCURL string
DSCSHA256 string
DebianVersion string
UpstreamVersion string
Suite string
Component string
}
// Resolver queries Debian source metadata over HTTP.
type Resolver struct {
client *http.Client
mirrors []string
suites []string
components []string
}
// ResolverOption customizes resolver behavior.
type ResolverOption func(*Resolver)
func WithHTTPClient(client *http.Client) ResolverOption {
return func(r *Resolver) {
if client != nil {
r.client = client
}
}
}
func WithMirrors(mirrors []string) ResolverOption {
return func(r *Resolver) {
r.mirrors = normalizeList(mirrors)
}
}
func WithSuites(suites []string) ResolverOption {
return func(r *Resolver) {
r.suites = normalizeList(suites)
}
}
func WithComponents(components []string) ResolverOption {
return func(r *Resolver) {
r.components = normalizeList(components)
}
}
func NewResolver(opts ...ResolverOption) *Resolver {
r := &Resolver{
client: &http.Client{Timeout: 30 * time.Second},
mirrors: []string{
defaultDebianMirror,
},
suites: append([]string(nil), defaultSuites...),
components: append([]string(nil), defaultComponents...),
}
for _, opt := range opts {
if opt != nil {
opt(r)
}
}
if len(r.mirrors) == 0 {
r.mirrors = []string{defaultDebianMirror}
}
if len(r.suites) == 0 {
r.suites = append([]string(nil), defaultSuites...)
}
if len(r.components) == 0 {
r.components = append([]string(nil), defaultComponents...)
}
return r
}
func (r *Resolver) ResolveSource(pkg string) (*SourceInfo, error) {
pkg = strings.TrimSpace(strings.ToLower(pkg))
if !packageNamePattern.MatchString(pkg) {
return nil, fmt.Errorf("invalid package name %q", pkg)
}
checked := make([]string, 0, len(r.mirrors)*len(r.suites)*len(r.components))
for _, mirror := range r.mirrors {
for _, suite := range r.suites {
for _, component := range r.components {
record, err := r.findPackageInIndex(mirror, suite, component, pkg)
checked = append(checked, fmt.Sprintf("%s:%s/%s", mirror, suite, component))
if err != nil {
continue
}
return &SourceInfo{
RequestedPackage: pkg,
SourcePackage: record.Package,
DSCURL: strings.TrimRight(mirror, "/") + "/" + path.Join(record.Directory, record.DSCName),
DSCSHA256: record.DSCSHA256,
DebianVersion: record.Version,
UpstreamVersion: normalizeUpstreamVersion(record.Version),
Suite: suite,
Component: component,
}, nil
}
}
}
return nil, fmt.Errorf(
"source package %s not found via HTTP (checked: %s)",
pkg,
strings.Join(checked, ", "),
)
}
type sourceRecord struct {
Package string
Version string
Directory string
DSCName string
DSCSHA256 string
}
func (r *Resolver) findPackageInIndex(mirror, suite, component, pkg string) (*sourceRecord, error) {
variants := []struct {
ext string
decoder func(io.Reader) (io.Reader, error)
}{
{ext: "xz", decoder: decodeXZ},
{ext: "gz", decoder: decodeGzip},
{ext: "", decoder: passthrough},
}
var firstErr error
for _, v := range variants {
indexURL := buildSourcesURL(mirror, suite, component, v.ext)
record, err := r.findInSingleIndex(indexURL, v.decoder, pkg)
if err == nil {
return record, nil
}
if firstErr == nil {
firstErr = err
}
}
if firstErr == nil {
firstErr = fmt.Errorf("could not read sources index")
}
return nil, firstErr
}
func (r *Resolver) findInSingleIndex(indexURL string, decoder func(io.Reader) (io.Reader, error), pkg string) (*sourceRecord, error) {
resp, err := r.client.Get(indexURL)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("http %d", resp.StatusCode)
}
reader, err := decoder(resp.Body)
if err != nil {
return nil, err
}
scanner := bufio.NewScanner(reader)
scanner.Buffer(make([]byte, 0, 64*1024), 8*1024*1024)
paragraph := make([]string, 0, 32)
flush := func() (*sourceRecord, bool, error) {
if len(paragraph) == 0 {
return nil, false, nil
}
rec, err := parseSourcesParagraph(paragraph)
paragraph = paragraph[:0]
if err != nil {
return nil, false, nil
}
if rec.Package == pkg {
return &rec, true, nil
}
return nil, false, nil
}
for scanner.Scan() {
line := scanner.Text()
if strings.TrimSpace(line) == "" {
rec, ok, err := flush()
if err != nil {
return nil, err
}
if ok {
return rec, nil
}
continue
}
paragraph = append(paragraph, line)
}
if err := scanner.Err(); err != nil {
return nil, err
}
rec, ok, err := flush()
if err != nil {
return nil, err
}
if ok {
return rec, nil
}
return nil, fmt.Errorf("package not found in %s", indexURL)
}
func buildSourcesURL(mirror, suite, component, ext string) string {
mirror = strings.TrimRight(strings.TrimSpace(mirror), "/")
suite = strings.Trim(strings.TrimSpace(suite), "/")
component = strings.Trim(strings.TrimSpace(component), "/")
name := "Sources"
if ext != "" {
name += "." + ext
}
return mirror + "/" + path.Join("dists", suite, component, "source", name)
}
func parseSourcesParagraph(lines []string) (sourceRecord, error) {
fields := make(map[string]string)
var currentKey string
for _, raw := range lines {
if strings.HasPrefix(raw, " ") || strings.HasPrefix(raw, "\t") {
if currentKey == "" {
continue
}
fields[currentKey] += "\n" + strings.TrimSpace(raw)
continue
}
idx := strings.IndexByte(raw, ':')
if idx <= 0 {
continue
}
key := strings.TrimSpace(raw[:idx])
value := strings.TrimSpace(raw[idx+1:])
fields[key] = value
currentKey = key
}
pkg := fields["Package"]
ver := fields["Version"]
dir := fields["Directory"]
if pkg == "" || ver == "" || dir == "" {
return sourceRecord{}, fmt.Errorf("missing required fields")
}
dscName, dscHash := parseChecksumsForDSC(fields["Checksums-Sha256"])
if dscName == "" {
return sourceRecord{}, fmt.Errorf("no dsc in checksums")
}
dir = strings.Trim(strings.TrimSpace(dir), "/")
return sourceRecord{
Package: pkg,
Version: ver,
Directory: dir,
DSCName: dscName,
DSCSHA256: dscHash,
}, nil
}
func parseChecksumsForDSC(raw string) (string, string) {
for _, line := range strings.Split(raw, "\n") {
fields := strings.Fields(strings.TrimSpace(line))
if len(fields) < 3 {
continue
}
name := fields[2]
if strings.HasSuffix(strings.ToLower(name), ".dsc") {
return name, strings.ToLower(fields[0])
}
}
return "", ""
}
func decodeXZ(r io.Reader) (io.Reader, error) {
return xz.NewReader(r)
}
func decodeGzip(r io.Reader) (io.Reader, error) {
return gzip.NewReader(r)
}
func passthrough(r io.Reader) (io.Reader, error) {
return r, nil
}
func normalizeUpstreamVersion(debianVersion string) string {
v := strings.TrimSpace(debianVersion)
if v == "" {
return "0"
}
if idx := strings.IndexByte(v, ':'); idx >= 0 {
v = v[idx+1:]
}
if idx := strings.LastIndex(v, "-"); idx > 0 {
v = v[:idx]
}
replacer := strings.NewReplacer(
"/", "_",
":", "_",
" ", "_",
)
v = strings.TrimSpace(replacer.Replace(v))
if v == "" {
return "0"
}
return v
}
func normalizeList(in []string) []string {
set := make(map[string]struct{}, len(in))
out := make([]string, 0, len(in))
for _, s := range in {
s = strings.TrimSpace(s)
if s == "" {
continue
}
if _, exists := set[s]; exists {
continue
}
set[s] = struct{}{}
out = append(out, s)
}
return out
}

156
pkg/debian/source_test.go Normal file
View file

@ -0,0 +1,156 @@
package debian
import (
"bytes"
"compress/gzip"
"fmt"
"io"
"net/http"
"strings"
"testing"
)
type roundTripFunc func(*http.Request) (*http.Response, error)
func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
return f(req)
}
func TestResolveSourceHTTPFallbackToGzip(t *testing.T) {
t.Parallel()
sources := strings.TrimSpace(`
Package: neofetch
Version: 7.1.0-4
Directory: pool/main/n/neofetch
Checksums-Sha256:
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 1978 neofetch_7.1.0-4.dsc
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 73791 neofetch_7.1.0.orig.tar.gz
`) + "\n"
var gz bytes.Buffer
zw := gzip.NewWriter(&gz)
if _, err := zw.Write([]byte(sources)); err != nil {
t.Fatalf("failed to write gzip sources: %v", err)
}
if err := zw.Close(); err != nil {
t.Fatalf("failed to close gzip writer: %v", err)
}
client := &http.Client{
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
switch req.URL.Path {
case "/debian/dists/stable/main/source/Sources.xz":
return httpResponse(req, http.StatusNotFound, []byte("not found")), nil
case "/debian/dists/stable/main/source/Sources.gz":
return httpResponse(req, http.StatusOK, gz.Bytes()), nil
default:
return httpResponse(req, http.StatusNotFound, []byte("missing")), nil
}
}),
}
r := NewResolver(
WithHTTPClient(client),
WithMirrors([]string{"https://mirror.example/debian"}),
WithSuites([]string{"stable"}),
WithComponents([]string{"main"}),
)
info, err := r.ResolveSource("neofetch")
if err != nil {
t.Fatalf("ResolveSource() error = %v", err)
}
if info.SourcePackage != "neofetch" {
t.Fatalf("unexpected source package: %s", info.SourcePackage)
}
if info.DebianVersion != "7.1.0-4" {
t.Fatalf("unexpected debian version: %s", info.DebianVersion)
}
if info.UpstreamVersion != "7.1.0" {
t.Fatalf("unexpected upstream version: %s", info.UpstreamVersion)
}
wantURL := "https://mirror.example/debian/pool/main/n/neofetch/neofetch_7.1.0-4.dsc"
if info.DSCURL != wantURL {
t.Fatalf("unexpected dsc url:\nwant: %s\ngot: %s", wantURL, info.DSCURL)
}
}
func TestResolveSourceHTTPNotFound(t *testing.T) {
t.Parallel()
client := &http.Client{
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
return httpResponse(req, http.StatusNotFound, []byte("not found")), nil
}),
}
r := NewResolver(
WithHTTPClient(client),
WithMirrors([]string{"https://mirror.example/debian"}),
WithSuites([]string{"stable"}),
WithComponents([]string{"main"}),
)
_, err := r.ResolveSource("no-such-package")
if err == nil {
t.Fatalf("expected not found error")
}
if !strings.Contains(err.Error(), "not found") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestParseSourcesParagraph(t *testing.T) {
t.Parallel()
paragraph := []string{
"Package: foo",
"Version: 1:2.3.4-5",
"Directory: pool/main/f/foo",
"Checksums-Sha256:",
" aaaa 10 foo_1:2.3.4-5.dsc",
" bbbb 20 foo_2.3.4.orig.tar.gz",
}
rec, err := parseSourcesParagraph(paragraph)
if err != nil {
t.Fatalf("parseSourcesParagraph() error = %v", err)
}
if rec.Package != "foo" || rec.Version != "1:2.3.4-5" {
t.Fatalf("unexpected record identity: %s %s", rec.Package, rec.Version)
}
if rec.Directory != "pool/main/f/foo" {
t.Fatalf("unexpected directory: %s", rec.Directory)
}
if rec.DSCName != "foo_1:2.3.4-5.dsc" {
t.Fatalf("unexpected dsc name: %s", rec.DSCName)
}
if rec.DSCSHA256 != "aaaa" {
t.Fatalf("unexpected dsc hash: %s", rec.DSCSHA256)
}
if got := normalizeUpstreamVersion(rec.Version); got != "2.3.4" {
t.Fatalf("unexpected normalized version: %s", got)
}
}
func TestResolveSourceInvalidPackageName(t *testing.T) {
t.Parallel()
r := NewResolver()
_, err := r.ResolveSource("bad/name")
if err == nil {
t.Fatalf("expected package name validation error")
}
}
func httpResponse(req *http.Request, status int, body []byte) *http.Response {
return &http.Response{
StatusCode: status,
Body: io.NopCloser(bytes.NewReader(body)),
Header: make(http.Header),
Request: req,
Status: fmt.Sprintf("%d %s", status, http.StatusText(status)),
}
}

View file

@ -287,15 +287,31 @@ func isDebianOrigArchive(name string) bool {
} }
rest := name[idx+len(".orig"):] rest := name[idx+len(".orig"):]
if strings.HasPrefix(rest, ".tar.") { if hasTarSuffix(rest) {
return true return true
} }
if strings.HasPrefix(rest, "-") && strings.Contains(rest, ".tar.") { if strings.HasPrefix(rest, "-") {
tarIdx := strings.Index(rest, ".tar")
if tarIdx >= 0 {
componentTar := rest[tarIdx:]
if hasTarSuffix(componentTar) {
return true return true
} }
}
}
return false return false
} }
func hasTarSuffix(s string) bool {
if !strings.HasPrefix(s, ".tar") {
return false
}
if len(s) == len(".tar") {
return true
}
return strings.HasPrefix(s[len(".tar"):], ".")
}
func isDebianDSCURL(raw string) bool { func isDebianDSCURL(raw string) bool {
raw = strings.TrimSpace(strings.ToLower(raw)) raw = strings.TrimSpace(strings.ToLower(raw))
if raw == "" { if raw == "" {

View file

@ -86,6 +86,32 @@ func TestDownloadAndExtractDebianDSCChecksumMismatch(t *testing.T) {
} }
} }
func TestIsDebianOrigArchive(t *testing.T) {
t.Parallel()
tests := []struct {
name string
want bool
}{
{name: "pkg_1.0.orig.tar.gz", want: true},
{name: "pkg_1.0.orig.tar.xz", want: true},
{name: "pkg_1.0.orig.tar", want: true},
{name: "pkg_1.0.orig-data.tar.zst", want: true},
{name: "pkg_1.0-1.debian.tar.xz", want: false},
{name: "pkg_1.0-1.diff.gz", want: false},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if got := isDebianOrigArchive(tt.name); got != tt.want {
t.Fatalf("isDebianOrigArchive(%q) = %v, want %v", tt.name, got, tt.want)
}
})
}
}
type roundTripFunc func(*http.Request) (*http.Response, error) type roundTripFunc func(*http.Request) (*http.Response, error)
func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {

BIN
zsvo

Binary file not shown.