deb822 refactor #1

Merged
Sirherobrine23 merged 1 commits from deb822 into main 2025-06-23 01:07:02 +00:00
26 changed files with 1170568 additions and 1024 deletions
Showing only changes of commit 8cf5fcbfc3 - Show all commits

View File

@@ -1,22 +1,26 @@
name: Golang test
on:
push:
branches:
- main
pull_request:
branches:
- main
- main
paths:
- "**.go"
- "**.go"
jobs:
go-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
name: Checkout code
- uses: actions/checkout@v3
name: Checkout code
- uses: actions/setup-go@v4
with:
go-version-file: "go.mod"
cache: true
- uses: actions/setup-go@v4
with:
go-version-file: "go.mod"
cache: true
- name: Test
run: go test -timeout 0 -v ./...
- name: Test
timeout-minutes: 10
run: go test -timeout 10m -v ./...

View File

@@ -1,50 +1,52 @@
package apt
import (
"fmt"
"io"
"iter"
"net/url"
"strings"
"sirherobrine23.com.br/sirherobrine23/go-dpkg/deb822"
"sirherobrine23.com.br/sirherobrine23/go-dpkg/internal/scanner"
)
// APT repository source from sources.list(5) or deb822 file
type AptSource struct {
Enabled bool `json:"enabled"` // Repository is enabled
Types []string `json:"source_type"` // Files origem, example: deb, deb-src
URIs []*url.URL `json:"uris"` // Repository Root URL
Suites []string `json:"suites"` // Repository suite or dist name
Components []string `json:"components"` // Repository Components
SignedBy string `json:"signed_by,omitempty"` // GPG Key path
Arch string `json:"arch,omitempty"` // Arch to get Packages
Extra deb822.Deb822 `json:"extra"` // Other options
Enabled bool `json:"enabled"` // Repository is enabled
Types []string `json:"source_type"` // Files origem, example: deb, deb-src
URIs []*url.URL `json:"uris"` // Repository Root URL
Suites []string `json:"suites"` // Repository suite or dist name
Components []string `json:"components"` // Repository Components
SignedBy string `json:"signed_by,omitempty"` // GPG Key path
Arch string `json:"arch,omitempty"` // Arch to get Packages
Extra map[string]string `json:"extra"` // Other options
}
// Process Debian sources.list(5) or deb822 styles
func ParseSourcelist(body string) (sources []AptSource, err error) {
newBody, sourceType := "", "deb822"
// Process Debian sources in one line or deb822 styles
func ParseSourcelist(body string) (sources []*AptSource, err error) {
sourceType := "deb822"
for line := range strings.SplitSeq(body, "\n") {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "#") {
continue
}
newBody += line + "\n"
if sourceType == "deb822" && (strings.HasPrefix(line, "deb") || strings.HasPrefix(line, "deb-src")) {
} else if sourceType == "deb822" && (strings.HasPrefix(line, "deb") || strings.HasPrefix(line, "deb-src")) {
sourceType = "apt"
}
}
switch sourceType {
case "deb822":
return parseDeb822(newBody)
return ParseDeb822(strings.NewReader(body))
default:
return parseApt(newBody)
return ParseOneLine(strings.NewReader(body))
}
}
func parseApt(body string) (sources []AptSource, err error) {
for line := range strings.SplitSeq(body, "\n") {
if line == "" {
// Return source list from one line style
func ParseOneLine(r io.Reader) (sources []*AptSource, err error) {
for line := range splitSeq(r, "\n") {
line = strings.TrimSpace(line)
if line == "" || line[0] == '#' {
continue
}
@@ -59,7 +61,7 @@ func parseApt(body string) (sources []AptSource, err error) {
if len(fields) < 3 || !(fields[0] == "deb" || fields[0] == "deb-src") {
continue
}
source := AptSource{Enabled: true, Types: []string{fields[0]}, Extra: deb822.Deb822{}}
source := &AptSource{Enabled: true, Types: []string{fields[0]}, Extra: map[string]string{}}
fields = fields[1:]
if config != "" {
@@ -72,7 +74,7 @@ func parseApt(body string) (sources []AptSource, err error) {
case "arch":
source.Arch = values[1]
default:
source.Extra[values[0]] = deb822.SingleLine(values[1])
source.Extra[values[0]] = values[1]
}
}
}
@@ -89,57 +91,72 @@ func parseApt(body string) (sources []AptSource, err error) {
return
}
func parseDeb822(body string) (sources []AptSource, err error) {
for sourceBody := range strings.SplitSeq(body, "\n\n") {
keys, err := deb822.Parse([]byte(sourceBody))
if err != nil {
return nil, err
}
source := AptSource{Enabled: true, Extra: deb822.Deb822{}}
if enabled, ok := keys["Enabled"]; ok && enabled.String() == "no" {
source.Enabled = false
}
if _, ok := keys["Types"]; !ok {
return nil, fmt.Errorf("missing Types key")
}
source.Types = strings.Fields(keys["Types"].String())
if _, ok := keys["URIs"]; !ok {
return nil, fmt.Errorf("missing URIs key")
}
for _, uri := range strings.Fields(keys["URIs"].String()) {
uriRepository, err := url.Parse(uri)
if err != nil {
return nil, err
// Return source list in deb822 file format
func ParseDeb822(r io.Reader) (sources []*AptSource, err error) {
reader := deb822.NewReader(r)
var previusSource *AptSource
for {
var key, value string
key, value, err = reader.Next()
switch err {
default:
return
case nil:
case io.EOF:
err = nil
return
case deb822.ErrNextDeb822:
previusSource = nil
if key == "" {
continue
}
source.URIs = append(source.URIs, uriRepository)
}
if _, ok := keys["Suites"]; !ok {
return nil, fmt.Errorf("missing Suites key")
}
source.Suites = strings.Fields(keys["Suites"].String())
if _, ok := keys["Components"]; !ok {
return nil, fmt.Errorf("missing Components key")
}
source.Components = strings.Fields(keys["Components"].String())
if signedBy, ok := keys["Signed-By"]; ok {
source.SignedBy = signedBy.String()
if previusSource == nil {
previusSource = &AptSource{Enabled: true, Extra: map[string]string{}}
sources = append(sources, previusSource)
}
delete(keys, "Enabled")
delete(keys, "Types")
delete(keys, "URIs")
delete(keys, "Suites")
delete(keys, "Components")
delete(keys, "Signed-By")
source.Extra = keys
sources = append(sources, source)
switch key {
case "Enabled":
if value == "no" {
previusSource.Enabled = false
}
case "Types":
previusSource.Types = strings.Fields(value)
case "URIs":
for _, uri := range strings.Fields(value) {
uriRepository, err := url.Parse(uri)
if err != nil {
return nil, err
}
previusSource.URIs = append(previusSource.URIs, uriRepository)
}
case "Suites":
previusSource.Suites = strings.Fields(value)
case "Components":
previusSource.Components = strings.Fields(value)
case "Signed-By":
previusSource.SignedBy = value
default:
if key != "" {
previusSource.Extra[key] = value
}
}
}
}
func splitSeq(r io.Reader, sep string) iter.Seq[string] {
return func(yield func(string) bool) {
sc := scanner.NewScannerSplit(r, scanner.SplitSep(sep))
for sc.Scan() {
t := sc.Text()
if !yield(t[:len(t)-len(sep)]) {
break
}
}
if err := sc.Err(); err != nil {
panic(err)
}
}
return
}

View File

@@ -2,34 +2,10 @@ package apt
import (
"encoding/json"
"net/url"
"strings"
"testing"
)
func TestParsePackages(t *testing.T) {
pkgSum := Sum{
File: "main/binary-amd64/Packages.gz",
}
pkgs := pkgSum.Pkgs("stable", &AptSource{
Suites: []string{"main", "stable"},
URIs: []*url.URL{
{
Scheme: "http",
Host: "ftp.debian.org",
Path: "/debian",
},
},
})
for _, err := range pkgs {
if err != nil {
t.Error(err)
return
}
break
}
}
var (
sourceDeb822 = `Types: deb deb-src
URIs: https://deb.debian.org/debian
@@ -57,34 +33,23 @@ deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.
)
func TestParseSources(t *testing.T) {
sources, err := ParseSourcelist(sourceApt)
if err != nil {
t.Error(err)
return
}
d, _ := json.MarshalIndent(sources, "", " ")
t.Logf("Sources apt:\n%s", d)
t.Run("source_list", func(t *testing.T) {
sources, err := ParseOneLine(strings.NewReader(sourceApt))
if err != nil {
t.Error(err)
return
}
d, _ := json.MarshalIndent(sources, "", " ")
t.Logf("Sources apt:\n%s", d)
})
if sources, err = ParseSourcelist(sourceDeb822); err != nil {
t.Error(err)
return
}
d, _ = json.MarshalIndent(sources, "", " ")
t.Logf("Sources deb822:\n%s", d)
}
func TestRelease(t *testing.T) {
sources, err := ParseSourcelist(sourceDeb822)
if err != nil {
t.Skip(err)
return
}
rel, err := sources[0].Release()
if err != nil {
t.Error(err)
return
}
d, _ := json.MarshalIndent(rel, "", " ")
t.Log(string(d))
t.Run("deb822", func(t *testing.T) {
sources, err := ParseDeb822(strings.NewReader(sourceDeb822))
if err != nil {
t.Error(err)
return
}
d, _ := json.MarshalIndent(sources, "", " ")
t.Logf("Sources deb822:\n%s", d)
})
}

151
apt/repo.go Normal file
View File

@@ -0,0 +1,151 @@
package apt
import (
"errors"
"fmt"
"io"
"net/http"
"net/url"
"path"
"strings"
"time"
"sirherobrine23.com.br/sirherobrine23/go-dpkg/deb822"
opengpg_crypto "github.com/ProtonMail/gopenpgp/v3/crypto"
)
var (
ErrDownServer error = errors.New("server is shutdown or not avaible")
ErrPackagesNotFound error = errors.New("packages not found")
)
// Release file from dists/<Suite>/Release, dists/<Suite>/InRelease or dists/<Suite>/Release.gpg
type AptRelease struct {
Origin string `json:"origin"`
Label string `json:"label"`
Suite string `json:"suite"`
Codename string `json:"codename,omitempty"`
Date time.Time `json:"date,omitzero"`
AcquireByHash bool `json:"acquire-by-hash"`
Archs []string `json:"architectures,omitempty"`
Components []string `json:"components,omitempty"`
Description string `json:"description,omitempty"`
Extra map[string]string `json:"extra,omitempty"`
MD5 ReleaseSum `json:"md5,omitempty"`
SHA1 ReleaseSum `json:"sha1,omitempty"`
SHA256 ReleaseSum `json:"sha256,omitempty"`
SHA512 ReleaseSum `json:"sha512,omitempty"`
}
type SuiteAptRelease map[string]*AptRelease
// Get [*AptRelease] from AptSource.Suites
func (apt AptSource) Release() (SuiteAptRelease, error) {
suites := SuiteAptRelease{}
for _, uriMain := range apt.URIs {
for _, suite := range apt.Suites {
var debRelease map[string]string
// Attemp get packages with InRelease
{
res, err := http.Get(uriMain.ResolveReference(&url.URL{Path: path.Join(uriMain.Path, "dists", suite, "InRelease")}).String())
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode == http.StatusOK {
body, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
if message, err := opengpg_crypto.NewPGPMessageFromArmored(string(body)); err == nil {
if debRelease, err = deb822.NewReader(message.NewReader()).Deb822(); err != nil && !(err == io.EOF || err == deb822.ErrNextDeb822) {
return nil, err
}
}
}
}
// Get packages with Release file
if debRelease == nil {
res, err := http.Get(uriMain.ResolveReference(&url.URL{Path: path.Join(uriMain.Path, "dists", suite, "Release")}).String())
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode == http.StatusOK {
if debRelease, err = deb822.NewReader(res.Body).Deb822(); err != nil && !(err == io.EOF || err == deb822.ErrNextDeb822) {
return nil, err
}
}
}
// Without Release info
if debRelease == nil {
return nil, fmt.Errorf("cannot get Release file")
}
release := &AptRelease{
Label: debRelease["Label"],
Archs: strings.Fields(debRelease["Architectures"]),
Description: debRelease["Description"],
Components: strings.Fields(debRelease["Components"]),
Codename: debRelease["Codename"],
Origin: debRelease["Origin"],
Suite: debRelease["Suite"],
AcquireByHash: debRelease["Acquire-By-Hash"] == "yes",
Extra: debRelease,
}
var err error
if date, ok := debRelease["Date"]; ok {
if release.Date, err = time.Parse(time.RFC1123, date); err != nil {
return nil, err
}
}
if value, ok := debRelease["MD5Sum"]; ok {
if release.MD5, err = SumLines(value); err != nil {
return nil, err
}
}
if value, ok := debRelease["SHA1"]; ok {
if release.SHA1, err = SumLines(value); err != nil {
return nil, err
}
}
if value, ok := debRelease["SHA256"]; ok {
if release.SHA256, err = SumLines(value); err != nil {
return nil, err
}
}
if value, ok := debRelease["SHA512"]; ok {
if release.SHA512, err = SumLines(value); err != nil {
return nil, err
}
}
delete(debRelease, "Label")
delete(debRelease, "Architectures")
delete(debRelease, "Description")
delete(debRelease, "Components")
delete(debRelease, "Codename")
delete(debRelease, "Origin")
delete(debRelease, "Suite")
delete(debRelease, "Acquire-By-Hash")
delete(debRelease, "Date")
delete(debRelease, "MD5Sum")
delete(debRelease, "SHA1")
delete(debRelease, "SHA256")
delete(debRelease, "SHA512")
suites[suite] = release
}
}
if len(suites) > 0 {
return suites, nil
}
return nil, ErrDownServer
}

44
apt/repo_test.go Normal file
View File

@@ -0,0 +1,44 @@
package apt
import (
"encoding/json"
"net/url"
"testing"
)
func TestRelease(t *testing.T) {
sources, err := ParseSourcelist(sourceDeb822)
if err != nil {
t.Skip(err)
return
}
rel, err := sources[0].Release()
if err != nil {
t.Error(err)
return
}
d, _ := json.MarshalIndent(rel, "", " ")
t.Log(string(d))
}
func TestParsePackages(t *testing.T) {
pkgSum := Sum{File: "main/binary-amd64/Packages.gz"}
pkgs := pkgSum.Pkgs("stable", &AptSource{
Suites: []string{"main", "stable"},
URIs: []*url.URL{
{
Scheme: "http",
Host: "ftp.debian.org",
Path: "/debian",
},
},
})
for _, err := range pkgs {
if err != nil {
t.Error(err)
return
}
}
}

View File

@@ -1,331 +0,0 @@
package apt
import (
"bytes"
"errors"
"fmt"
"io"
"iter"
"net/http"
"net/url"
"os"
"path"
"slices"
"strconv"
"strings"
"time"
"sirherobrine23.com.br/sirherobrine23/go-dpkg/deb822"
"sirherobrine23.com.br/sirherobrine23/go-dpkg/dpkg"
"sirherobrine23.com.br/sirherobrine23/go-dpkg/internal/descompress"
"sirherobrine23.com.br/sirherobrine23/go-dpkg/internal/scanner"
opengpg_crypto "github.com/ProtonMail/gopenpgp/v3/crypto"
)
var (
ErrDownServer error = errors.New("server is shutdown or not avaible")
ErrPackagesNotFound error = errors.New("packages not found")
)
// Package represents a software package in the APT repository context.
// It embeds a dpkg.Package and adds a poolURL field, which specifies
// the URL location of the package in the repository pool.
type Package struct {
*dpkg.Package
poolURL *url.URL // Package pool url
}
// Get file URL
func (pkg *Package) URL() *url.URL { return pkg.poolURL.ResolveReference(&url.URL{}) }
// Return .deb Downloader
func (pkg *Package) Download() (io.ReadCloser, error) {
res, err := http.Get(pkg.poolURL.String())
if err != nil {
return nil, err
} else if res.StatusCode != 200 {
return res.Body, errors.New("invalid request code")
}
return res.Body, nil
}
// Package sum
type Sum struct {
File string
Hash string
Size int64
}
// Return packages from sum file
func (sum Sum) Pkgs(SuiteName string, AptSrc *AptSource) iter.Seq2[*Package, error] {
return func(yield func(*Package, error) bool) {
if path.Base(sum.File[:len(sum.File)-len(path.Ext(sum.File))]) != "Packages" {
yield(nil, fmt.Errorf("this sum require have Package last element"))
return
} else if !slices.Contains(AptSrc.Suites, SuiteName) {
yield(nil, fmt.Errorf("invalid suite"))
return
}
r, metaIndex, metaURI := io.Reader(nil), 0, (*url.URL)(nil)
for metaIndex, metaURI = range AptSrc.URIs {
metaURI = metaURI.ResolveReference(&url.URL{Path: path.Join(metaURI.Path, "dists", SuiteName, sum.File)})
res, err := http.Get(metaURI.String())
if err != nil {
yield(nil, err)
return
} else if res.StatusCode != 200 {
if metaIndex == len(AptSrc.URIs)-1 {
yield(nil, fmt.Errorf("http client return %d", res.StatusCode))
return
}
continue
}
defer res.Body.Close()
if r, err = descompress.Descompress(res.Body); err != nil {
yield(nil, fmt.Errorf("cannot descompress body: %s", err))
return
}
}
if metaURI == nil {
yield(nil, fmt.Errorf("cannot get Packages file"))
return
}
pkgReader := scanner.NewScannerSplit(r, func(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
}
if i := bytes.Index(data, []byte("\n\n")); i > 0 {
// We have a full newline-terminated line.
return i + 2, data[0 : i+2], nil
}
// If we're at EOF, we have a final, non-terminated line. Return it.
if atEOF {
return len(data), data, nil
}
// Request more data.
return 0, nil, nil
})
for pkgReader.Scan() {
pkg, err := dpkg.ParsePackage(pkgReader.Bytes())
if err != nil {
yield(nil, fmt.Errorf("cannot get package from Packages: %s", err))
return
}
poolUrl := metaURI.ResolveReference(&url.URL{Path: path.Join(metaURI.Path, pkg.Extra.Get("Filename").String())})
if !yield(&Package{Package: pkg, poolURL: poolUrl}, nil) {
return
}
}
if err := pkgReader.Err(); err != nil {
yield(nil, err)
}
}
}
// Return packages avaible in target
//
// Deprecated: Use [Sum.Pkgs] for get packages from Packages file
func (sum Sum) Packages(suite string, src *AptSource) ([]*Package, error) {
pkgs := []*Package{}
for pkg, err := range sum.Pkgs(suite, src) {
if err != nil {
return nil, err
}
pkgs = append(pkgs, pkg)
}
return pkgs, nil
}
type ReleaseSum map[string][]Sum
func ParseSumLines(lines []string) (ReleaseSum, error) {
sums := ReleaseSum{}
for _, name := range lines {
hash := name[:strings.Index(name, " ")]
name = strings.TrimSpace(name[len(hash):])
sizeStr := name[:strings.Index(name, " ")]
name = strings.TrimSpace(name[len(sizeStr):])
size, err := strconv.ParseInt(sizeStr, 10, 64)
if err != nil {
return nil, err
}
ext := path.Ext(name)
mapText := name[:len(name)-len(ext)]
sums[mapText] = append(sums[mapText], Sum{
Hash: hash,
Size: size,
File: name,
})
}
return sums, nil
}
// Release file from dists/<Suite>/Release, dists/<Suite>/InRelease or dists/<Suite>/Release.gpg
type AptRelease struct {
Origin string `json:"origin"`
Label string `json:"label"`
Suite string `json:"suite"`
Codename string `json:"codename,omitempty"`
Date time.Time `json:"date,omitzero"`
AcquireByHash bool `json:"acquire-by-hash"`
Archs []string `json:"architectures,omitempty"`
Components []string `json:"components,omitempty"`
Description string `json:"description,omitempty"`
Extra deb822.Deb822 `json:"extra,omitempty"`
MD5 ReleaseSum `json:"md5,omitempty"`
SHA1 ReleaseSum `json:"sha1,omitempty"`
SHA256 ReleaseSum `json:"sha256,omitempty"`
SHA512 ReleaseSum `json:"sha512,omitempty"`
}
type SuiteAptRelease map[string]*AptRelease
// Get [*AptRelease] from AptSource.Suites
func (apt AptSource) Release() (SuiteAptRelease, error) {
suites := SuiteAptRelease{}
for _, uriMain := range apt.URIs {
for _, suite := range apt.Suites {
var debRelease deb822.Deb822
// Get Packages if Signer key exists in disk
if apt.SignedBy != "" {
if gpgKey, err := os.ReadFile(apt.SignedBy); err == nil && len(gpgKey) > 0 {
key, err := opengpg_crypto.NewKeyFromArmored(string(gpgKey))
if err != nil {
return nil, err
}
res, err := http.Get(uriMain.ResolveReference(&url.URL{Path: path.Join(uriMain.Path, "dists", suite, "Release.gpg")}).String())
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode == http.StatusOK {
body, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
msg := opengpg_crypto.NewPGPSplitMessage(key.GetFingerprintBytes(), body)
if debRelease, err = deb822.NewParse(msg.NewReader()); err != nil {
return nil, err
}
}
}
}
// Attemp get packages with InRelease
if debRelease == nil {
res, err := http.Get(uriMain.ResolveReference(&url.URL{Path: path.Join(uriMain.Path, "dists", suite, "InRelease")}).String())
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode == http.StatusOK {
body, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
if message, err := opengpg_crypto.NewPGPMessageFromArmored(string(body)); err == nil {
if debRelease, err = deb822.NewParse(message.NewReader()); err != nil {
return nil, err
}
}
}
}
// Get packages with Release file
if debRelease == nil {
res, err := http.Get(uriMain.ResolveReference(&url.URL{Path: path.Join(uriMain.Path, "dists", suite, "Release")}).String())
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode == http.StatusOK {
if debRelease, err = deb822.NewParse(res.Body); err != nil {
return nil, err
}
}
}
// Without Release info
if debRelease == nil {
return nil, fmt.Errorf("cannot get Release file")
}
release := &AptRelease{
Label: debRelease.Get("Label").String(),
Archs: strings.Fields(debRelease.Get("Architectures").String()),
Description: debRelease.Get("Description").String(),
Components: strings.Fields(debRelease.Get("Components").String()),
Codename: debRelease.Get("Codename").String(),
Origin: debRelease.Get("Origin").String(),
Suite: debRelease.Get("Suite").String(),
AcquireByHash: debRelease.Get("Acquire-By-Hash").ToBool(),
Extra: debRelease,
}
var err error
if date, ok := debRelease["Date"]; ok {
if release.Date, err = time.Parse(time.RFC1123, date.String()); err != nil {
return nil, err
}
}
if value, ok := debRelease["MD5Sum"]; ok {
if release.MD5, err = ParseSumLines(value.ToSlice()); err != nil {
return nil, err
}
}
if value, ok := debRelease["SHA1"]; ok {
if release.SHA1, err = ParseSumLines(value.ToSlice()); err != nil {
return nil, err
}
}
if value, ok := debRelease["SHA256"]; ok {
if release.SHA256, err = ParseSumLines(value.ToSlice()); err != nil {
return nil, err
}
}
if value, ok := debRelease["SHA512"]; ok {
if release.SHA512, err = ParseSumLines(value.ToSlice()); err != nil {
return nil, err
}
}
delete(debRelease, "Label")
delete(debRelease, "Architectures")
delete(debRelease, "Description")
delete(debRelease, "Components")
delete(debRelease, "Codename")
delete(debRelease, "Origin")
delete(debRelease, "Suite")
delete(debRelease, "Acquire-By-Hash")
delete(debRelease, "Date")
delete(debRelease, "MD5Sum")
delete(debRelease, "SHA1")
delete(debRelease, "SHA256")
delete(debRelease, "SHA512")
suites[suite] = release
}
}
if len(suites) > 0 {
return suites, nil
}
return nil, ErrDownServer
}

124
apt/sum.go Normal file
View File

@@ -0,0 +1,124 @@
package apt
import (
"fmt"
"io"
"iter"
"net/http"
"net/url"
"path"
"strconv"
"strings"
"sirherobrine23.com.br/sirherobrine23/go-dpkg/deb822"
"sirherobrine23.com.br/sirherobrine23/go-dpkg/dpkg"
"sirherobrine23.com.br/sirherobrine23/go-dpkg/internal/descompress"
)
// ReleaseSum represents a mapping from a string key (such as a file name or section name)
// to a slice of Sum values, where each Sum contains checksum information for the corresponding entry.
// This is typically used to store and organize checksums for files listed in an APT Release file.
type ReleaseSum map[string][]Sum
// Files sum from Release
type Sum struct {
File string
Hash string
Size int64
}
// SumLines parses a string containing multiple checksum lines (typically from a Debian Release file)
// and returns a ReleaseSum map containing the parsed sums. Each line is expected to contain a hash, a size,
// and a filename, separated by spaces. The function splits each line, extracts the hash, size, and file name,
// and appends the result to the ReleaseSum map keyed by the file name without its extension.
// Returns an error if any size value cannot be parsed as an integer.
func SumLines(data string) (ReleaseSum, error) {
sums := ReleaseSum{}
for name := range deb822.ToSlice(data) {
hash := name[:strings.Index(name, " ")]
name = strings.TrimSpace(name[len(hash):])
sizeStr := name[:strings.Index(name, " ")]
name = strings.TrimSpace(name[len(sizeStr):])
size, err := strconv.ParseInt(sizeStr, 10, 64)
if err != nil {
return nil, err
}
ext := path.Ext(name)
mapText := name[:len(name)-len(ext)]
sums[mapText] = append(sums[mapText], Sum{
Hash: hash,
Size: size,
File: name,
})
}
return sums, nil
}
func (sum Sum) IsBinaryPackage() bool {
if strings.ToLower(path.Base(sum.File[:len(sum.File)-len(path.Ext(sum.File))])) == "packages" {
nodes := strings.Split(sum.File, "/")
if len(nodes) == 3 && strings.HasPrefix(nodes[1], "binary-") {
return true
}
}
return false
}
// Return packages from sum file
func (sum Sum) Pkgs(SuiteName string, AptSrc *AptSource) iter.Seq2[*dpkg.Package, error] {
return func(yield func(*dpkg.Package, error) bool) {
if !sum.IsBinaryPackage() {
yield(nil, fmt.Errorf("this sum require have Package last element"))
return
}
r, metaIndex, metaURI := io.Reader(nil), 0, (*url.URL)(nil)
for metaIndex, metaURI = range AptSrc.URIs {
metaURI = metaURI.ResolveReference(&url.URL{Path: path.Join(metaURI.Path, "dists", SuiteName, sum.File)})
res, err := http.Get(metaURI.String())
if err != nil {
yield(nil, err)
return
} else if res.StatusCode != 200 {
if metaIndex == len(AptSrc.URIs)-1 {
yield(nil, fmt.Errorf("http client return %d", res.StatusCode))
return
}
continue
}
defer res.Body.Close()
if r, err = descompress.Descompress(res.Body); err != nil {
yield(nil, fmt.Errorf("cannot descompress body: %s", err))
return
}
}
if metaURI == nil {
yield(nil, fmt.Errorf("cannot get Packages file"))
return
}
pkgReader := deb822.NewReader(r)
for {
pkgData, err := pkgReader.Deb822()
switch err {
case deb822.ErrNextDeb822:
continue
case io.EOF:
return
case nil:
pkg := &dpkg.Package{}
err = pkg.FromMap(pkgData)
if !yield(pkg, err) {
return
}
default:
yield(nil, err)
return
}
}
}
}

View File

@@ -17,13 +17,8 @@ var (
func TestWriter(t *testing.T) {
// Buffer to write to
buff := &bytes.Buffer{}
ar, err := NewWriter(buff)
if err != nil {
t.Error(err)
return
}
_, err = ar.WriteFile(fileContent, &Header{
buff := new(bytes.Buffer)
_, err := NewWriter(buff).WriteFile(fileContent, &Header{
Filename: "test.txt/",
Owner: 1000,
Group: 1000,

View File

@@ -1,39 +1,47 @@
package ar
import "io"
import (
"bytes"
"io"
"sync/atomic"
)
type Writer struct {
w io.Writer
size int64
previusHeader *Header
writeMagic *atomic.Bool
}
func NewWriter(w io.Writer) (wr *Writer, err error) {
// Write ar magic header
if _, err = w.Write([]byte(arMagic)); err == nil {
wr = &Writer{w: w, size: 0}
func NewWriter(w io.Writer) *Writer {
p := &atomic.Bool{}
p.Store(true)
return &Writer{
w: w,
size: 0,
writeMagic: p,
}
return
}
func (w *Writer) WriteFile(data []byte, hdr *Header) (n int64, err error) {
if w.size <= 0 {
if err = w.WriteHeader(hdr); err == nil {
n2, err2 := w.Write(data)
n = int64(n2)
if err2 != nil {
err = err2
}
}
func (w *Writer) WriteFile(data []byte, hdr *Header) (int64, error) {
if w.size > 0 {
return 0, io.ErrUnexpectedEOF
}
return
n, err := w.WriteHeader(hdr)
if err != nil {
return n, err
}
n2, err := io.Copy(w, bytes.NewBuffer(data))
return n + n2, err
}
func (w *Writer) Write(p []byte) (n int, err error) {
if w.size <= 0 {
if w.writeMagic.Load() {
return 0, io.ErrUnexpectedEOF
} else if w.size <= 0 {
return 0, io.EOF
} else if int64(len(p)) > w.size {
p = p[0:w.size]
p = p[:w.size]
}
n, err = w.w.Write(p)
w.size -= int64(n)
@@ -43,12 +51,29 @@ func (w *Writer) Write(p []byte) (n int, err error) {
return
}
func (w *Writer) WriteHeader(hdr *Header) error {
func (w *Writer) WriteHeader(hdr *Header) (n int64, err error) {
if w.writeMagic.Swap(false) {
// Write ar magic header
b, err := w.w.Write([]byte(arMagic))
if err != nil {
return int64(b), err
}
n += int64(b)
}
if w.size > 0 {
return io.ErrShortWrite
return 0, io.ErrShortWrite
}
w.size = hdr.Size
w.previusHeader = hdr
_, err := w.w.Write(hdr.gnuHeader()) // Writed gnu header
return err
buff := hdr.gnuHeader()
for len(buff) > 0 {
n2, err := w.w.Write(buff)
n += int64(n2)
if err != nil {
return n, err
}
buff = buff[n2:]
}
return
}

1169340
deb822/content/Packages Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,159 +1,65 @@
package deb822
import (
"fmt"
"io"
"slices"
"strconv"
"errors"
"iter"
"strings"
"sirherobrine23.com.br/sirherobrine23/go-dpkg/internal/scanner"
"unicode"
)
type Source interface {
IsMultiline() bool
IsBool() bool
IsInt() bool
Len() int
Line(int) string
String() string
ToSlice() []string
ToBool() bool
ToInt() int
}
var (
_ Source = SingleLine("")
_ Source = NumberLine(0)
_ Source = Multiline{}
ErrInvalidDeb822 = errors.New("invalid deb822, check file if is valid or in deb822 style")
ErrNextDeb822 = errors.New("current deb822 have multiples of deb822")
)
type SingleLine string
func (SingleLine) IsInt() bool { return false }
func (SingleLine) ToInt() int { return 0 }
func (s SingleLine) IsMultiline() bool { return false }
func (s SingleLine) IsBool() bool { return slices.Contains([]string{"yes", "no"}, string(s)) }
func (s SingleLine) ToBool() bool { return s.IsBool() && s.String() == "yes" }
func (s SingleLine) String() string { return string(s) }
func (s SingleLine) Line(int) string { return string(s) }
func (s SingleLine) Len() int { return len(s) }
func (s SingleLine) ToSlice() []string {
if s == "" {
return nil
// Format value string to description
func FromDescription(value string) (lines [2]string) {
n2 := strings.SplitN(value, "\n", 2)
if len(n2) >= 1 {
lines[0] = n2[0]
}
return []string{string(s)}
}
type NumberLine int
func (s NumberLine) IsMultiline() bool { return false }
func (s NumberLine) IsBool() bool { return false }
func (s NumberLine) IsInt() bool { return true }
func (s NumberLine) String() string { return strconv.Itoa(int(s)) }
func (s NumberLine) Line(int) string { return s.String() }
func (s NumberLine) Len() int { return len(s.String()) }
func (s NumberLine) ToSlice() []string { return []string{s.String()} }
func (s NumberLine) ToInt() int { return int(s) }
func (s NumberLine) ToBool() bool { return s == 1 }
type Multiline []string
func (m Multiline) IsMultiline() bool { return len(m) > 1 }
func (m Multiline) IsBool() bool { return slices.Contains([]string{"yes", "no"}, m.String()) }
func (m Multiline) ToBool() bool { return m.IsBool() && m.String() == "yes" }
func (m Multiline) String() string { return strings.Join(m, "\n") }
func (m Multiline) Len() int { return len(m) }
func (m Multiline) ToSlice() []string { return m }
func (Multiline) IsInt() bool { return true }
func (Multiline) ToInt() int { return 0 }
func (m Multiline) Line(i int) string {
if i < 0 || i >= len(m) {
return ""
}
return m[i]
}
// Debian control file
type Deb822 map[string]Source
// Parse control file
func Parse(body []byte) (Deb822, error) {
deb822Map := Deb822{}
if err := deb822Map.UnmarshalBinary(body); err != nil {
return nil, err
}
return deb822Map, nil
}
// Get key, if not exists return SingleLine
func (deb822Map Deb822) Get(key string) Source {
if value, ok := deb822Map[key]; ok {
return value
}
return SingleLine("")
}
// Unmarshall Debian822
func (deb822Map *Deb822) UnmarshalBinary(body []byte) error {
if err := newParse(deb822Map, strings.NewReader(strings.TrimSpace(string(body)))); err != nil {
return err
}
return nil
}
// Parse Debian822 with stream file
func NewParse(r io.Reader) (Deb822, error) {
deb822Map := Deb822{}
if err := newParse(&deb822Map, r); err != nil {
return nil, err
}
return deb822Map, nil
}
// parse Debian822 file
func newParse(deb822Map *Deb822, r io.Reader) error {
multilineKey, scann := "", scanner.NewScanner(r)
for scann.Scan() {
line := scann.Text()
if line == "" {
break // New deb822 ignore
} else if strings.HasPrefix(line, "#") || strings.HasPrefix(line, "-") {
continue // Ignore line
} else if line[0] == ' ' || line[0] == '\t' {
if multilineKey == "" {
return fmt.Errorf("invalid control file, line: %s", line)
} else if line = strings.TrimLeft(line, " \t"); line == "." {
line = "" // Replace . with empty line
if len(n2) >= 2 {
lines[1] = n2[1]
secLines := strings.Split(strings.TrimLeftFunc(lines[1], unicode.IsSpace), "\n")
switch len(secLines) {
case 0:
case 1:
secLines[0] = strings.TrimLeftFunc(secLines[0], unicode.IsSpace)
default:
cut := strings.IndexFunc(secLines[1], func(r rune) bool { return !unicode.IsSpace(r) })
for i := range secLines {
if i == 0 {
continue
}
secLines[i] = secLines[i][cut:]
if secLines[i] == "." {
secLines[i] = ""
}
}
}
lines[1] = strings.Join(secLines, "\n")
}
return
}
func ToSlice(data string) iter.Seq[string] {
return func(yield func(string) bool) {
lines := strings.Split(strings.TrimSpace(data), "\n")
var cut int
if len(lines) > 1 {
cut = max(0, strings.IndexFunc(lines[1], func(r rune) bool { return !unicode.IsSpace(r) }))
}
for i := range lines {
if i != 0 && len(lines[i]) >= cut {
lines[i] = lines[i][cut:]
if lines[i] == "." {
lines[i] = ""
}
}
if !yield(lines[i]) {
break
}
(*deb822Map)[multilineKey] = append(Multiline((*deb822Map)[multilineKey].ToSlice()), line)
continue
}
keys := strings.SplitN(line, ":", 2)
if len(keys) != 2 {
return fmt.Errorf("invalid line: %s", line)
}
multilineKey, line = keys[0], keys[1]
if line == "" {
(*deb822Map)[multilineKey] = Multiline{}
continue
} else if line[0] == ' ' || line[0] == '\t' {
line = line[1:]
}
if _, ok := (*deb822Map)[multilineKey]; !ok {
(*deb822Map)[multilineKey] = SingleLine(line)
if nu, err := strconv.Atoi(line); err == nil {
(*deb822Map)[multilineKey] = NumberLine(nu)
}
continue
}
(*deb822Map)[multilineKey] = append(Multiline((*deb822Map)[multilineKey].ToSlice()), line)
}
return scann.Err()
}

150
deb822/reader.go Normal file
View File

@@ -0,0 +1,150 @@
package deb822
import (
"io"
"iter"
"strings"
"sync"
"unicode"
"sirherobrine23.com.br/sirherobrine23/go-dpkg/internal/scanner"
)
// Deb822 reader
type Reader struct {
r io.Reader
scan *scanner.Scanner
sync *sync.Mutex
previusData string
}
// Create new reader to deb822 file
func NewReader(r io.Reader) *Reader {
return &Reader{
scan: scanner.NewScanner(r),
sync: &sync.Mutex{},
}
}
// Total bytes read
func (r *Reader) TotalRead() int64 { return r.scan.TotalRead() }
func (r *Reader) Reset() {
r.scan.ResetReader(r.r)
r.previusData = ""
}
// Read next key and value
func (r *Reader) Next() (key, value string, err error) {
r.sync.Lock()
defer r.sync.Unlock()
if line := r.previusData; len(line) > 0 {
switch line[0] {
case '#', '-':
case ' ', '\t', '\n':
return "", "", ErrInvalidDeb822
default:
keyDelimit := strings.IndexRune(line, ':')
if keyDelimit < 1 {
return "", "", ErrInvalidDeb822
}
key, value = line[:keyDelimit], strings.TrimLeftFunc(line[keyDelimit+1:], unicode.IsSpace)
}
r.previusData = ""
}
if key == "" {
if !r.scan.Scan() && r.scan.Err() == nil {
return "", "", io.EOF
} else if err := r.scan.Err(); err != nil {
return "", "", err
}
line := r.scan.Text()
switch line[0] {
case '#', '-':
return r.Next()
case '\n':
return "", "", ErrNextDeb822
case ' ', '\t':
return "", "", ErrInvalidDeb822
default:
keyDelimit := strings.IndexRune(line, ':')
if keyDelimit < 1 {
return "", "", ErrInvalidDeb822
}
key, value = line[:keyDelimit], strings.TrimLeftFunc(line[keyDelimit+1:], unicode.IsSpace)
}
}
// Check if valid key
if strings.ContainsFunc(key, unicode.IsSpace) {
return "", "", ErrInvalidDeb822
}
// Reader key value
for r.scan.Scan() {
line := r.scan.Text()
if line == "" {
err = ErrNextDeb822
break
}
switch line[0] {
case '#', '-':
continue
case '\n':
err = ErrNextDeb822
return
case ' ', '\t':
value += "\n" + line
default:
r.previusData = line
return
}
}
return
}
// return a iter.seq2[[2]string{key, value}, error] to current deb822 file
func (r *Reader) Seq() iter.Seq2[[2]string, error] {
return func(yield func([2]string, error) bool) {
endReader := false
for !endReader {
k, v, err := r.Next()
switch err {
case nil: // return value and key
case io.EOF, ErrNextDeb822: // done loop
endReader = true
default: // return error end stop reader
yield([2]string{k, v}, nil)
return
}
if !yield([2]string{k, v}, err) {
return
}
}
}
}
// Get current deb822 file
func (r *Reader) Deb822() (values map[string]string, err error) {
values = make(map[string]string)
var kv [2]string
for kv, err = range r.Seq() {
if err != nil {
if err == ErrNextDeb822 {
err = nil
if len(kv) > 0 && kv[0] != "" {
values[kv[0]] = kv[1]
}
break
}
return
}
values[kv[0]] = kv[1]
}
return
}

37
deb822/reader_test.go Normal file
View File

@@ -0,0 +1,37 @@
package deb822
import (
"io"
"os"
"testing"
)
func TestReader(t *testing.T) {
file, err := os.Open("./content/Packages")
if err != nil {
t.Skipf("cannot open Packages file to test reader: %v", err)
return
}
defer file.Close()
r := NewReader(file)
var pkgCount int
for {
_, _, err := r.Next()
switch err {
case nil:
case io.EOF:
t.Logf("Pkgs reader: %d", pkgCount)
return
case ErrInvalidDeb822:
t.Error(err)
return
case ErrNextDeb822:
pkgCount++
continue
default:
t.Error(err)
return
}
}
}

96
deb822/writer.go Normal file
View File

@@ -0,0 +1,96 @@
package deb822
import (
"io"
"iter"
"strings"
"sync"
"unicode"
)
type Writer struct {
w io.Writer
locker *sync.Mutex
}
func NewWriter(w io.Writer) *Writer {
return &Writer{
w: w,
locker: &sync.Mutex{},
}
}
func (w *Writer) Add(key, value string) (total int64, err error) {
w.locker.Lock()
defer w.locker.Unlock()
// Check if valid key
if strings.ContainsFunc(key, unicode.IsSpace) {
return 0, ErrInvalidDeb822
}
var n int64
for n != int64(len(key)) {
b, err := w.w.Write([]byte(key)[n:])
n += int64(b)
if err != nil {
return n, err
}
}
total = n
n = 0
for n != 2 {
b, err := w.w.Write([]byte(": ")[n:])
n += int64(b)
if err != nil {
return total + n, err
}
}
total += n
{
value = strings.TrimSpace(value)
if strings.Count(value, "\n") > 1 {
lines := strings.SplitN(value, "\n", 2)
lines[0] = strings.TrimSpace(lines[0])
lines[1] = strings.TrimSpace(lines[1])
newLines := strings.Split(lines[1], "\n")
for i := range newLines {
if strings.TrimSpace(newLines[i]) == "" {
newLines[i] = " ."
continue
}
newLines[i] = " " + newLines[i]
}
lines[1] = strings.Join(newLines, "\n")
value = strings.Join(lines, "\n")
}
}
n = 0
for n != int64(len(value)) {
b, err := w.w.Write([]byte(value)[n:])
n += int64(b)
if err != nil {
return total + n, err
}
}
total += n
b, err := w.w.Write([]byte("\n"))
if err != nil {
return total + int64(b), err
}
total += int64(b)
return
}
func (w *Writer) FromIter(r iter.Seq2[string, string]) error {
for key, value := range r {
if _, err := w.Add(key, value); err != nil {
return err
}
}
return nil
}

View File

@@ -1,32 +0,0 @@
package dpkg
import (
"embed"
"encoding/json"
"testing"
)
//go:embed testdata/*.deb
var datatest embed.FS
func TestGetDebInfo(t *testing.T) {
dpkgFiles, err := datatest.ReadDir("testdata")
if err != nil {
t.Skip(err)
return
}
for _, fileList := range dpkgFiles {
f, _ := datatest.Open("testdata/" + fileList.Name())
dpkgInfo, tar, err := ParseDpkg(f)
if err != nil {
f.Close()
t.Error(err)
return
}
tar.Close()
f.Close()
d, _ := json.MarshalIndent(dpkgInfo, "", " ")
t.Logf("%s:\n%s", fileList.Name(), d)
}
}

View File

@@ -1,36 +0,0 @@
package dpkg
import (
"io/fs"
"os"
)
// Open dpkg file with [io/fs.FS]
func OpenFS(sys fs.FS, name string) (*Dpkg, error) {
file, err := sys.Open(name)
if err != nil {
return nil, err
}
defer file.Close()
pkg, data, err := ParseDpkg(file)
if err != nil {
return nil, err
}
data.Close()
return pkg, nil
}
// Open local file
func Open(name string) (*Dpkg, error) {
file, err := os.Open(name)
if err != nil {
return nil, err
}
defer file.Close()
pkg, data, err := ParseDpkg(file)
if err != nil {
return nil, err
}
data.Close()
return pkg, nil
}

View File

@@ -1,235 +0,0 @@
package dpkg
import (
"archive/tar"
"bytes"
"compress/gzip"
"encoding"
"errors"
"fmt"
"io"
"strings"
"sirherobrine23.com.br/sirherobrine23/go-dpkg/ar"
"sirherobrine23.com.br/sirherobrine23/go-dpkg/deb822"
)
var ErrInvalidDebian = errors.New("invalid debian file")
type Maintainer struct {
Name string `json:"name,omitempty"`
Contact string `json:"contact,omitempty"`
}
func (main Maintainer) String() string {
if main.Contact == "" {
return main.Name
}
return fmt.Sprintf("%s <%s>", main.Name, main.Contact)
}
type Package struct {
Name string `json:"name"`
Version string `json:"version"`
Architecture string `json:"arch"`
Maintainer Maintainer `json:"maintainer"`
Description [2]string `json:"description"`
OriginalMaintainer *Maintainer `json:"original_maintainer,omitempty"`
Extra deb822.Deb822 `json:"extra"`
}
func ParsePackage(data []byte) (*Package, error) {
pkg := &Package{}
if err := pkg.UnmarshalBinary(data); err != nil {
return nil, err
}
return pkg, nil
}
func (pkg Package) MarshalBinary() ([]byte, error) {
lines := []string{
fmt.Sprintf("Package: %s", pkg.Name),
fmt.Sprintf("Version: %s", pkg.Version),
fmt.Sprintf("Architecture: %s", pkg.Architecture),
fmt.Sprintf("Maintainer: %s", pkg.Maintainer.String()),
}
if pkg.OriginalMaintainer != nil {
lines = append(lines, fmt.Sprintf("Original-Maintainer: %s", pkg.OriginalMaintainer.String()))
}
for key, value := range pkg.Extra {
switch v := value.(type) {
case fmt.Stringer:
lines = append(lines, fmt.Sprintf("%s: %s", key, v.String()))
case encoding.TextMarshaler:
data, err := v.MarshalText()
if err != nil {
return nil, err
}
lines = append(lines, fmt.Sprintf("%s: %s", key, string(data)))
default:
if v.IsMultiline() {
lines = append(lines, fmt.Sprintf("%s: %s", key, strings.Join(v.ToSlice(), "\n")))
continue
}
lines = append(lines, fmt.Sprintf("%s: %s", key, v))
}
}
lines = append(lines, fmt.Sprintf("Description: %s", pkg.Description[0]))
if pkg.Description[1] != "" {
descLines := strings.Split(strings.TrimSpace(pkg.Description[1]), "\n")
for index := range descLines {
if strings.TrimSpace(descLines[index]) == "" {
descLines[index] = "."
continue
}
}
lines = append(lines, fmt.Sprintf(" %s", strings.Join(descLines, "\n ")))
}
return []byte(strings.Join(append(lines, ""), "\n")), nil
}
func (pkg *Package) UnmarshalBinary(body []byte) error {
*pkg = Package{Extra: deb822.Deb822{}} // Reset package struct
mapped, err := deb822.Parse(body)
if err != nil {
return err
}
pkg.Name = mapped["Package"].String()
pkg.Version = mapped["Version"].String()
pkg.Architecture = mapped["Architecture"].String()
pkg.Maintainer = Maintainer{}
if value, ok := mapped["Maintainer"]; ok {
maintainer := strings.SplitN(value.String(), "<", 2)
pkg.Maintainer.Name = strings.TrimSpace(maintainer[0])
if len(maintainer) > 1 {
pkg.Maintainer.Contact = strings.TrimRight(maintainer[1], ">")
}
}
if value, ok := mapped["Original-Maintainer"]; ok {
pkg.OriginalMaintainer = &Maintainer{}
maintainer := strings.SplitN(value.String(), "<", 2)
pkg.OriginalMaintainer.Name = strings.TrimSpace(maintainer[0])
if len(maintainer) > 1 {
pkg.OriginalMaintainer.Contact = strings.TrimRight(maintainer[1], ">")
}
}
if value, ok := mapped["Description"]; ok {
pkg.Description[0] = value.Line(0)
if value.IsMultiline() {
pkg.Description[1] = strings.TrimSpace(strings.Join(value.ToSlice()[1:], "\n"))
}
}
// Remove used keys
delete(mapped, "Package")
delete(mapped, "Version")
delete(mapped, "Architecture")
delete(mapped, "Maintainer")
delete(mapped, "Original-Maintainer")
delete(mapped, "Description")
pkg.Extra = mapped
return nil
}
type Scripts struct {
PreInstall []byte
PreRemove []byte
PostInstall []byte
PostRemove []byte
}
func (scr *Scripts) WriteToTar(prefix string, t *tar.Writer) error {
if prefix != "" && prefix[len(prefix)-1] != '-' {
prefix += "-"
}
// Install
if len(scr.PreInstall) > 0 {
if err := t.WriteHeader(&tar.Header{Name: fmt.Sprintf("%spreinst", prefix), Size: int64(len(scr.PreInstall)), Mode: 0755}); err != nil {
return err
} else if _, err = t.Write(scr.PreInstall); err != nil {
return err
}
}
if len(scr.PostInstall) > 0 {
if err := t.WriteHeader(&tar.Header{Name: fmt.Sprintf("%spostinst", prefix), Size: int64(len(scr.PostInstall)), Mode: 0755}); err != nil {
return err
} else if _, err = t.Write(scr.PostInstall); err != nil {
return err
}
}
// Remove
if len(scr.PreRemove) > 0 {
if err := t.WriteHeader(&tar.Header{Name: fmt.Sprintf("%sprerm", prefix), Size: int64(len(scr.PreRemove)), Mode: 0755}); err != nil {
return err
} else if _, err = t.Write(scr.PreRemove); err != nil {
return err
}
}
if len(scr.PostRemove) > 0 {
if err := t.WriteHeader(&tar.Header{Name: fmt.Sprintf("%spostrm", prefix), Size: int64(len(scr.PostRemove)), Mode: 0755}); err != nil {
return err
} else if _, err = t.Write(scr.PostRemove); err != nil {
return err
}
}
return nil
}
type Dpkg struct {
Size int64 // data.tar size
Ext string // data.tar extension
Pkg *Package // Package info
Scripts, OldScripts, NewScripts *Scripts // Scripts to add to control.tar
}
func (dpkg *Dpkg) WriteTo(w *ar.Writer) error {
controlFile, err := dpkg.Pkg.MarshalBinary()
if err != nil {
return err
}
controlBuffer := &bytes.Buffer{}
control := tar.NewWriter(controlBuffer)
// Control file
control.WriteHeader(&tar.Header{Name: "control", Size: int64(len(controlFile)), Mode: 0644})
control.Write(controlFile)
// Scripts
if dpkg.Scripts != nil {
if err = dpkg.Scripts.WriteToTar("", control); err != nil {
return err
}
}
if dpkg.NewScripts != nil {
if err = dpkg.NewScripts.WriteToTar("new-", control); err != nil {
return err
}
}
if dpkg.OldScripts != nil {
if err = dpkg.OldScripts.WriteToTar("old-", control); err != nil {
return err
}
}
control.Close()
tarControlBuggerGZ := &bytes.Buffer{}
gz := gzip.NewWriter(tarControlBuggerGZ)
if _, err = io.Copy(gz, controlBuffer); err != nil {
return err
}
gz.Close()
if _, err := w.WriteFile(tarControlBuggerGZ.Bytes(), &ar.Header{Filename: "control.tar.gz", Size: int64(len(tarControlBuggerGZ.Bytes()))}); err != nil {
return err
}
return nil
}

182
dpkg/package.go Normal file
View File

@@ -0,0 +1,182 @@
package dpkg
import (
"errors"
"fmt"
"io"
"net/http"
"net/url"
"path"
"strings"
"sirherobrine23.com.br/sirherobrine23/go-dpkg/deb822"
)
var ErrPoolFilename = errors.New("Package not have filename field")
// Package
type Package struct {
Name string `json:"package"` // Package name
Version string `json:"version"` // Package version
Architecture string `json:"arch"` // Architecture
Description [2]string `json:"description"` // Package Description
Maintainer string `json:"maintainer"` // Maintainer
Extra map[string]string `json:",omitempty"` // Extra fields
}
func (pkg Package) WriteTo(w io.Writer) (n int64, err error) {
// Check for empty imputs
if pkg.Name == "" {
err = fmt.Errorf("empty package name")
return
} else if pkg.Version == "" {
err = fmt.Errorf("empty package version")
return
} else if pkg.Architecture == "" {
err = fmt.Errorf("empty package architecture")
return
} else if pkg.Maintainer == "" {
err = fmt.Errorf("empty package maintainer name")
return
} else if pkg.Description[0] == "" || pkg.Description[1] == "" {
err = fmt.Errorf("empty package description")
return
}
wr := deb822.NewWriter(w)
var n2 int64
if n2, err = wr.Add("Package", pkg.Name); err != nil {
return
}
n += n2
if n2, err = wr.Add("Version", pkg.Version); err != nil {
return
}
n += n2
if n2, err = wr.Add("Architecture", pkg.Architecture); err != nil {
return
}
n += n2
if n2, err = wr.Add("Maintainer", pkg.Maintainer); err != nil {
return
}
n += n2
for key, value := range pkg.Extra {
if n2, err = wr.Add(key, value); err != nil {
return
}
n += n2
}
if n2, err = wr.Add("Description", strings.Join(pkg.Description[:], "\n")); err != nil {
return
}
n += n2
return 0, nil
}
func (pkg *Package) ReadFrom(r io.Reader) (n int64, err error) {
rd := deb822.NewReader(r)
for {
var key, value string
if key, value, err = rd.Next(); err != nil {
if err == io.EOF || err == deb822.ErrNextDeb822 {
err = nil
}
break
}
pkg.FromKeyValue(key, value)
}
switch {
case pkg.Name == "":
err = fmt.Errorf("empty package name")
case pkg.Version == "":
err = fmt.Errorf("empty package version")
case pkg.Architecture == "":
err = fmt.Errorf("empty package architecture")
case pkg.Maintainer == "":
err = fmt.Errorf("empty package maintainer name")
case pkg.Description[0] == "":
err = fmt.Errorf("empty package description")
}
return
}
func (pkg Package) Sizeof() int64 {
n, _ := pkg.WriteTo(io.Discard)
return n
}
func (pkg *Package) FromMap(m map[string]string) error {
for key, value := range m {
pkg.FromKeyValue(key, value)
}
switch {
case pkg.Name == "":
return fmt.Errorf("empty package name")
case pkg.Version == "":
return fmt.Errorf("empty package version")
case pkg.Architecture == "":
return fmt.Errorf("empty package architecture")
case pkg.Maintainer == "":
return fmt.Errorf("empty package maintainer name")
case pkg.Description[0] == "":
return fmt.Errorf("empty package description")
}
return nil
}
func (pkg *Package) FromKeyValue(key, value string) {
switch key {
case "Package":
pkg.Name = value
case "Version":
pkg.Version = value
case "Architecture":
pkg.Architecture = value
case "Maintainer":
pkg.Maintainer = value
case "Description":
pkg.Description = deb822.FromDescription(value)
default:
if pkg.Extra == nil {
pkg.Extra = make(map[string]string)
}
pkg.Extra[key] = value
}
}
// Get file URL
func (pkg Package) Url(pool *url.URL) (*url.URL, error) {
if filename, ok := pkg.Extra["Filename"]; ok {
fromPool := pool.ResolveReference(&url.URL{
Path: path.Join(pool.Path, filename),
})
return fromPool, nil
}
return nil, ErrPoolFilename
}
// Return .deb Downloader
func (pkg *Package) Download(pool *url.URL) (io.ReadCloser, error) {
pkgUrl, err := pkg.Url(pool)
if err != nil {
return nil, err
}
res, err := http.Get(pkgUrl.String())
if err != nil {
return nil, err
} else if res.StatusCode != 200 {
return res.Body, errors.New("invalid request code")
}
return res.Body, nil
}

View File

@@ -2,6 +2,7 @@ package dpkg
import (
"archive/tar"
"errors"
"fmt"
"io"
"path"
@@ -10,6 +11,14 @@ import (
"sirherobrine23.com.br/sirherobrine23/go-dpkg/internal/descompress"
)
var ErrInvalidDebian = errors.New("invalid debian file")
type Dpkg struct {
DataSize int64 `json:"data_size"` // data.tar size
DataExt string `json:"data_ext"` // data.tar extension
Pkg *Package `json:"package"` // Package info
}
func tarReader(r io.Reader) (*tar.Reader, io.Closer, error) {
tarballFile, err := descompress.Descompress(r)
if err != nil {
@@ -18,8 +27,13 @@ func tarReader(r io.Reader) (*tar.Reader, io.Closer, error) {
return tar.NewReader(tarballFile), tarballFile, nil
}
// Parse a dpkg file and return a Dpkg struct and data.tar reader without compression
func ParseDpkg(r io.Reader) (*Dpkg, io.ReadCloser, error) {
// NewReader parses a Debian package (.deb) file from the provided io.Reader and returns a Dpkg struct,
// an io.ReadCloser for the package's data archive, and an error if any occurs during parsing.
//
// The function expects the input to be an ar archive containing at least the "debian-binary",
// "control.tar.*", and "data.tar.*" files. It validates the archive structure, extracts and parses
// the control information, and prepares a reader for the data archive.
func NewReader(r io.Reader) (*Dpkg, io.ReadCloser, error) {
dpkgFile, err := ar.NewReader(r)
if err != nil {
return nil, nil, err
@@ -56,62 +70,9 @@ func ParseDpkg(r io.Reader) (*Dpkg, io.ReadCloser, error) {
switch path.Base(head.Name) {
case "control":
header, err := io.ReadAll(controller)
if err != nil {
return nil, nil, err
} else if err = dpkg.Pkg.UnmarshalBinary(header); err != nil {
return nil, nil, err
}
case "postinst", "preinst", "prerm", "postrm":
script, err := io.ReadAll(controller)
if err != nil {
return nil, nil, err
} else if dpkg.Scripts == nil {
dpkg.Scripts = &Scripts{}
}
switch path.Base(head.Name) {
case "postinst":
dpkg.Scripts.PostInstall = script
case "preinst":
dpkg.Scripts.PreInstall = script
case "prerm":
dpkg.Scripts.PostRemove = script
case "postrm":
dpkg.Scripts.PostRemove = script
}
case "old-postinst", "old-preinst", "old-prerm", "old-postrm":
script, err := io.ReadAll(controller)
if err != nil {
return nil, nil, err
} else if dpkg.OldScripts == nil {
dpkg.OldScripts = &Scripts{}
}
switch path.Base(head.Name) {
case "old-postinst":
dpkg.OldScripts.PostInstall = script
case "old-preinst":
dpkg.OldScripts.PreInstall = script
case "old-prerm":
dpkg.OldScripts.PostRemove = script
case "old-postrm":
dpkg.OldScripts.PostRemove = script
}
case "new-postinst", "new-preinst", "new-prerm", "new-postrm":
script, err := io.ReadAll(controller)
if err != nil {
return nil, nil, err
} else if dpkg.NewScripts == nil {
dpkg.NewScripts = &Scripts{}
}
switch path.Base(head.Name) {
case "new-postinst":
dpkg.NewScripts.PostInstall = script
case "new-preinst":
dpkg.NewScripts.PreInstall = script
case "new-prerm":
dpkg.NewScripts.PostRemove = script
case "new-postrm":
dpkg.NewScripts.PostRemove = script
dpkg.Pkg = &Package{}
if _, err = dpkg.Pkg.ReadFrom(controller); err != nil {
return nil, nil, fmt.Errorf("cannot read control file: %s", err)
}
default:
if !head.FileInfo().Mode().IsDir() {
@@ -134,8 +95,8 @@ func ParseDpkg(r io.Reader) (*Dpkg, io.ReadCloser, error) {
return nil, nil, ErrInvalidDebian
}
dpkg.Ext = path.Ext(head.Filename)
dpkg.Size = head.Size
dpkg.DataExt = path.Ext(head.Filename)
dpkg.DataSize = head.Size
data, err := descompress.Descompress(dpkgFile)
return dpkg, data, err
}

59
dpkg/reader_test.go Normal file
View File

@@ -0,0 +1,59 @@
package dpkg_test
import (
"encoding/json"
"net/url"
"testing"
"sirherobrine23.com.br/sirherobrine23/go-dpkg/apt"
"sirherobrine23.com.br/sirherobrine23/go-dpkg/dpkg"
)
func TestReaderDpkg(t *testing.T) {
pkgSum := apt.Sum{File: "main/binary-amd64/Packages.gz"}
pkgs := pkgSum.Pkgs("stable", &apt.AptSource{
Suites: []string{"main", "stable"},
URIs: []*url.URL{
{
Scheme: "http",
Host: "ftp.debian.org",
Path: "/debian",
},
},
})
curlExec := false
for pkg, err := range pkgs {
if err != nil {
t.Error(err)
return
} else if pkg.Name == "curl" {
curlExec = true
} else {
continue
}
deb, err := pkg.Download(&url.URL{Scheme: "http", Host: "ftp.debian.org", Path: "/debian"})
if err != nil {
t.Errorf("cannot get curl package: %s", err)
return
}
defer deb.Close()
pkgInfo, tarRead, err := dpkg.NewReader(deb)
if err != nil {
t.Error(err)
return
}
tarRead.Close()
pkgJson, _ := json.MarshalIndent(pkgInfo, "", " ")
t.Logf("Package info:\n%s", pkgJson)
break
}
if !curlExec {
t.Error("curl package not found")
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,24 +0,0 @@
package dpkg
import (
"fmt"
"io"
"sirherobrine23.com.br/sirherobrine23/go-dpkg/ar"
)
// NewDpkg creates a new dpkg file and return a writer to write the data.tar file
func NewDpkg(w io.Writer, dpkg *Dpkg) (wr io.Writer, err error) {
dpkgFile, err := ar.NewWriter(w)
if err != nil {
return nil, err
} else if _, err = dpkgFile.WriteFile([]byte("2.0\n"), &ar.Header{Filename: "debian-binary", Mode: 0644, Size: 4}); err != nil {
return
} else if err = dpkg.WriteTo(dpkgFile); err != nil {
return
} else if err = dpkgFile.WriteHeader(&ar.Header{Filename: fmt.Sprintf("data.tar%s", dpkg.Ext), Mode: 0644, Size: dpkg.Size}); err != nil {
return
}
wr = dpkgFile
return
}

115
dpkg/writer.go Normal file
View File

@@ -0,0 +1,115 @@
package dpkg
import (
"archive/tar"
"compress/gzip"
"fmt"
"io"
"io/fs"
"os"
"path"
"strings"
"sirherobrine23.com.br/sirherobrine23/go-dpkg/ar"
)
// Package info
type WriteInfo struct {
Pkg *Package // Package info
ExtraFile map[string]func() (fs.File, error) // file info to add to control.tar
DataExt string // Data extension, exp: `.gz`
DataSize int64 // Data size
}
// Writer to data.tar
type DpkgWriter struct {
Info *WriteInfo
ar *ar.Writer
}
func (w *DpkgWriter) Write(p []byte) (n int, err error) { return w.ar.Write(p) }
// NewWrite creates a new DpkgWriter that writes a Debian package archive to the provided io.Writer.
// It constructs the control archive (control.tar.gz) with the package control information and any extra files,
// then writes the required ar archive members ("debian-binary", "control.tar.gz", and "data.tar.*").
// The Info parameter must not be nil and should contain all necessary package metadata and data.
// Returns a pointer to the created DpkgWriter or an error if any step fails.
func NewWrite(w io.Writer, Info *WriteInfo) (*DpkgWriter, error) {
if Info == nil {
return nil, fmt.Errorf("info is nil, set package info to write dpkg package")
}
tmpControl, err := os.CreateTemp("", "control*.tgz")
if err != nil {
return nil, err
}
defer os.Remove(tmpControl.Name())
defer tmpControl.Close()
gz := gzip.NewWriter(tmpControl)
defer gz.Close()
controlTar := tar.NewWriter(gz)
defer controlTar.Close()
if err := controlTar.WriteHeader(&tar.Header{Name: "control", Size: Info.Pkg.Sizeof(), Mode: 0644}); err != nil {
return nil, err
} else if _, err := Info.Pkg.WriteTo(controlTar); err != nil {
return nil, err
}
for name, fileOpen := range Info.ExtraFile {
if strings.ToLower(path.Base(name)) == "control" {
continue
}
f, err := fileOpen()
if err != nil {
return nil, err
}
defer f.Close()
info, err := f.Stat()
if err != nil {
return nil, err
}
tarHead, err := tar.FileInfoHeader(info, "")
if err != nil {
return nil, err
}
tarHead.Name = name
if err := controlTar.WriteHeader(tarHead); err != nil {
return nil, err
} else if _, err := io.Copy(controlTar, f); err != nil {
return nil, err
}
f.Close()
}
if err := controlTar.Close(); err != nil {
return nil, err
} else if err := gz.Close(); err != nil {
return nil, err
}
tmpControl.Seek(0, 0)
stat, err := tmpControl.Stat()
if err != nil {
return nil, err
}
arw := ar.NewWriter(w)
if _, err = arw.WriteFile([]byte("2.0\n"), &ar.Header{Filename: "debian-binary", Mode: 0644, Size: 4}); err != nil {
return nil, err
} else if _, err = arw.WriteHeader(&ar.Header{Filename: "control.tar.gz", Mode: 0644, Size: stat.Size()}); err != nil {
return nil, err
} else if _, err = io.Copy(arw, tmpControl); err != nil {
return nil, err
}
tmpControl.Close()
os.Remove(tmpControl.Name())
if _, err = arw.WriteHeader(&ar.Header{Filename: fmt.Sprintf("data.tar%s", Info.DataExt), Mode: 0644, Size: Info.DataSize}); err != nil {
return nil, err
}
return &DpkgWriter{ar: arw, Info: Info}, nil
}

View File

@@ -6,6 +6,7 @@ package scanner
import (
"bufio"
"bytes"
"errors"
"io"
)
@@ -27,6 +28,7 @@ import (
// on a reader, should use [bufio.Reader] instead.
type Scanner struct {
r io.Reader // The reader provided by the client.
totalRead int64 // Total number of bytes read.
split SplitFunc // The function to split the tokens.
token []byte // Last token returned by split.
buf []byte // Buffer used as argument to split.
@@ -214,6 +216,7 @@ func (s *Scanner) Scan() bool {
s.setErr(ErrBadReadCount)
break
}
s.totalRead += int64(n)
s.end += n
if err != nil {
s.setErr(err)
@@ -263,3 +266,31 @@ func (s *Scanner) Split(split SplitFunc) {
}
s.split = split
}
// Return total reader
func (s Scanner) TotalRead() int64 { return s.totalRead }
// Clean reader scanner
func (s *Scanner) ResetReader(r io.Reader) {
s.r = r
s.totalRead = 0
s.err = nil
s.scanCalled = false
s.done = false
}
func SplitSep(sep string) SplitFunc {
return func(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
}
if i := bytes.Index(data, []byte(sep)); i >= 0 {
frag := data[:i+len(sep)]
return len(frag), frag, nil
}
if atEOF {
return len(data), data, nil
}
return 0, nil, nil
}
}