code.gitea.io/gitea@v1.22.3/modules/packages/debian/metadata.go (about) 1 // Copyright 2023 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package debian 5 6 import ( 7 "archive/tar" 8 "bufio" 9 "compress/gzip" 10 "io" 11 "net/mail" 12 "regexp" 13 "strings" 14 15 "code.gitea.io/gitea/modules/util" 16 "code.gitea.io/gitea/modules/validation" 17 18 "github.com/blakesmith/ar" 19 "github.com/klauspost/compress/zstd" 20 "github.com/ulikunitz/xz" 21 ) 22 23 const ( 24 PropertyDistribution = "debian.distribution" 25 PropertyComponent = "debian.component" 26 PropertyArchitecture = "debian.architecture" 27 PropertyControl = "debian.control" 28 PropertyRepositoryIncludeInRelease = "debian.repository.include_in_release" 29 30 SettingKeyPrivate = "debian.key.private" 31 SettingKeyPublic = "debian.key.public" 32 33 RepositoryPackage = "_debian" 34 RepositoryVersion = "_repository" 35 36 controlTar = "control.tar" 37 ) 38 39 var ( 40 ErrMissingControlFile = util.NewInvalidArgumentErrorf("control file is missing") 41 ErrUnsupportedCompression = util.NewInvalidArgumentErrorf("unsupported compression algorithm") 42 ErrInvalidName = util.NewInvalidArgumentErrorf("package name is invalid") 43 ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid") 44 ErrInvalidArchitecture = util.NewInvalidArgumentErrorf("package architecture is invalid") 45 46 // https://www.debian.org/doc/debian-policy/ch-controlfields.html#source 47 namePattern = regexp.MustCompile(`\A[a-z0-9][a-z0-9+-.]+\z`) 48 // https://www.debian.org/doc/debian-policy/ch-controlfields.html#version 49 versionPattern = regexp.MustCompile(`\A(?:[0-9]:)?[a-zA-Z0-9.+~]+(?:-[a-zA-Z0-9.+-~]+)?\z`) 50 ) 51 52 type Package struct { 53 Name string 54 Version string 55 Architecture string 56 Control string 57 Metadata *Metadata 58 } 59 60 type Metadata struct { 61 Maintainer string `json:"maintainer,omitempty"` 62 ProjectURL string `json:"project_url,omitempty"` 63 Description string `json:"description,omitempty"` 64 Dependencies []string `json:"dependencies,omitempty"` 65 } 66 67 // ParsePackage parses the Debian package file 68 // https://manpages.debian.org/bullseye/dpkg-dev/deb.5.en.html 69 func ParsePackage(r io.Reader) (*Package, error) { 70 arr := ar.NewReader(r) 71 72 for { 73 hd, err := arr.Next() 74 if err == io.EOF { 75 break 76 } 77 if err != nil { 78 return nil, err 79 } 80 81 if strings.HasPrefix(hd.Name, controlTar) { 82 var inner io.Reader 83 // https://man7.org/linux/man-pages/man5/deb-split.5.html#FORMAT 84 // The file names might contain a trailing slash (since dpkg 1.15.6). 85 switch strings.TrimSuffix(hd.Name[len(controlTar):], "/") { 86 case "": 87 inner = arr 88 case ".gz": 89 gzr, err := gzip.NewReader(arr) 90 if err != nil { 91 return nil, err 92 } 93 defer gzr.Close() 94 95 inner = gzr 96 case ".xz": 97 xzr, err := xz.NewReader(arr) 98 if err != nil { 99 return nil, err 100 } 101 102 inner = xzr 103 case ".zst": 104 zr, err := zstd.NewReader(arr) 105 if err != nil { 106 return nil, err 107 } 108 defer zr.Close() 109 110 inner = zr 111 default: 112 return nil, ErrUnsupportedCompression 113 } 114 115 tr := tar.NewReader(inner) 116 for { 117 hd, err := tr.Next() 118 if err == io.EOF { 119 break 120 } 121 if err != nil { 122 return nil, err 123 } 124 125 if hd.Typeflag != tar.TypeReg { 126 continue 127 } 128 129 if hd.FileInfo().Name() == "control" { 130 return ParseControlFile(tr) 131 } 132 } 133 } 134 } 135 136 return nil, ErrMissingControlFile 137 } 138 139 // ParseControlFile parses a Debian control file to retrieve the metadata 140 func ParseControlFile(r io.Reader) (*Package, error) { 141 p := &Package{ 142 Metadata: &Metadata{}, 143 } 144 145 key := "" 146 var depends strings.Builder 147 var control strings.Builder 148 149 s := bufio.NewScanner(io.TeeReader(r, &control)) 150 for s.Scan() { 151 line := s.Text() 152 153 trimmed := strings.TrimSpace(line) 154 if trimmed == "" { 155 continue 156 } 157 158 if line[0] == ' ' || line[0] == '\t' { 159 switch key { 160 case "Description": 161 p.Metadata.Description += line 162 case "Depends": 163 depends.WriteString(trimmed) 164 } 165 } else { 166 parts := strings.SplitN(trimmed, ":", 2) 167 if len(parts) < 2 { 168 continue 169 } 170 171 key = parts[0] 172 value := strings.TrimSpace(parts[1]) 173 switch key { 174 case "Package": 175 p.Name = value 176 case "Version": 177 p.Version = value 178 case "Architecture": 179 p.Architecture = value 180 case "Maintainer": 181 a, err := mail.ParseAddress(value) 182 if err != nil || a.Name == "" { 183 p.Metadata.Maintainer = value 184 } else { 185 p.Metadata.Maintainer = a.Name 186 } 187 case "Description": 188 p.Metadata.Description = value 189 case "Depends": 190 depends.WriteString(value) 191 case "Homepage": 192 if validation.IsValidURL(value) { 193 p.Metadata.ProjectURL = value 194 } 195 } 196 } 197 } 198 if err := s.Err(); err != nil { 199 return nil, err 200 } 201 202 if !namePattern.MatchString(p.Name) { 203 return nil, ErrInvalidName 204 } 205 if !versionPattern.MatchString(p.Version) { 206 return nil, ErrInvalidVersion 207 } 208 if p.Architecture == "" { 209 return nil, ErrInvalidArchitecture 210 } 211 212 dependencies := strings.Split(depends.String(), ",") 213 for i := range dependencies { 214 dependencies[i] = strings.TrimSpace(dependencies[i]) 215 } 216 p.Metadata.Dependencies = dependencies 217 218 p.Control = strings.TrimSpace(control.String()) 219 220 return p, nil 221 }