deb822 refactor #1
24
.github/workflows/test.yaml
vendored
24
.github/workflows/test.yaml
vendored
@@ -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 ./...
|
||||
|
155
apt/apt.go
155
apt/apt.go
@@ -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
|
||||
}
|
||||
|
@@ -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
151
apt/repo.go
Normal 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
44
apt/repo_test.go
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
@@ -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
124
apt/sum.go
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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,
|
||||
|
69
ar/write.go
69
ar/write.go
@@ -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
1169340
deb822/content/Packages
Normal file
File diff suppressed because one or more lines are too long
194
deb822/deb822.go
194
deb822/deb822.go
@@ -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
150
deb822/reader.go
Normal 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
37
deb822/reader_test.go
Normal 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
96
deb822/writer.go
Normal 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
|
||||
}
|
@@ -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)
|
||||
}
|
||||
}
|
36
dpkg/fs.go
36
dpkg/fs.go
@@ -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
|
||||
}
|
235
dpkg/header.go
235
dpkg/header.go
@@ -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
182
dpkg/package.go
Normal 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
|
||||
}
|
@@ -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
59
dpkg/reader_test.go
Normal 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")
|
||||
}
|
||||
}
|
BIN
dpkg/testdata/libssh-4_0.8.7-1+deb10u1_amd64.deb
vendored
BIN
dpkg/testdata/libssh-4_0.8.7-1+deb10u1_amd64.deb
vendored
Binary file not shown.
BIN
dpkg/testdata/libssh2-1-dev_1.8.0-2.1_amd64.deb
vendored
BIN
dpkg/testdata/libssh2-1-dev_1.8.0-2.1_amd64.deb
vendored
Binary file not shown.
BIN
dpkg/testdata/libssh2-1_1.10.0-3+b1_amd64.deb
vendored
BIN
dpkg/testdata/libssh2-1_1.10.0-3+b1_amd64.deb
vendored
Binary file not shown.
@@ -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
115
dpkg/writer.go
Normal 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
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user