44package composer
55
66import (
7+ "archive/tar"
78 "archive/zip"
9+ "compress/bzip2"
10+ "compress/gzip"
11+ "errors"
812 "io"
13+ "io/fs"
914 "path"
1015 "regexp"
1116 "strings"
2934 ErrInvalidVersion = util .NewInvalidArgumentErrorf ("package version is invalid" )
3035)
3136
32- // Package represents a Composer package
33- type Package struct {
37+ // PackageInfo represents Composer package info
38+ type PackageInfo struct {
39+ Filename string
40+
3441 Name string
3542 Version string
3643 Type string
@@ -44,7 +51,7 @@ type Metadata struct {
4451 Description string `json:"description,omitempty"`
4552 Readme string `json:"readme,omitempty"`
4653 Keywords []string `json:"keywords,omitempty"`
47- Comments Comments `json:"_comments ,omitempty"`
54+ Comments Comments `json:"_comment ,omitempty"`
4855 Homepage string `json:"homepage,omitempty"`
4956 License Licenses `json:"license,omitempty"`
5057 Authors []Author `json:"authors,omitempty"`
@@ -75,7 +82,7 @@ func (l *Licenses) UnmarshalJSON(data []byte) error {
7582 if err := json .Unmarshal (data , & values ); err != nil {
7683 return err
7784 }
78- * l = Licenses ( values )
85+ * l = values
7986 }
8087 return nil
8188}
@@ -97,7 +104,7 @@ func (c *Comments) UnmarshalJSON(data []byte) error {
97104 if err := json .Unmarshal (data , & values ); err != nil {
98105 return err
99106 }
100- * c = Comments ( values )
107+ * c = values
101108 }
102109 return nil
103110}
@@ -111,46 +118,131 @@ type Author struct {
111118
112119var nameMatch = regexp .MustCompile (`\A[a-z0-9]([_\.-]?[a-z0-9]+)*/[a-z0-9](([_\.]?|-{0,2})[a-z0-9]+)*\z` )
113120
114- // ParsePackage parses the metadata of a Composer package file
115- func ParsePackage (r io.ReaderAt , size int64 ) (* Package , error ) {
116- archive , err := zip .NewReader (r , size )
121+ type ReadSeekAt interface {
122+ io.Reader
123+ io.ReaderAt
124+ io.Seeker
125+ Size () int64
126+ }
127+
128+ func readPackageFileZip (r ReadSeekAt , filename string , limit int ) ([]byte , error ) {
129+ archive , err := zip .NewReader (r , r .Size ())
117130 if err != nil {
118131 return nil , err
119132 }
120133
121134 for _ , file := range archive .File {
122- if strings .Count (file .Name , "/" ) > 1 {
123- continue
124- }
125- if strings .HasSuffix (strings .ToLower (file .Name ), "composer.json" ) {
135+ filePath := path .Clean (file .Name )
136+ if util .AsciiEqualFold (filePath , filename ) {
126137 f , err := archive .Open (file .Name )
127138 if err != nil {
128139 return nil , err
129140 }
130141 defer f .Close ()
131142
132- return ParseComposerFile (archive , path .Dir (file .Name ), f )
143+ return util .ReadWithLimit (f , limit )
144+ }
145+ }
146+ return nil , fs .ErrNotExist
147+ }
148+
149+ func readPackageFileTar (r io.Reader , filename string , limit int ) ([]byte , error ) {
150+ tarReader := tar .NewReader (r )
151+ for {
152+ header , err := tarReader .Next ()
153+ if err == io .EOF {
154+ break
155+ } else if err != nil {
156+ return nil , err
157+ }
158+
159+ filePath := path .Clean (header .Name )
160+ if util .AsciiEqualFold (filePath , filename ) {
161+ return util .ReadWithLimit (tarReader , limit )
133162 }
134163 }
135- return nil , ErrMissingComposerFile
164+ return nil , fs . ErrNotExist
136165}
137166
138- // ParseComposerFile parses a composer.json file to retrieve the metadata of a Composer package
139- func ParseComposerFile (archive * zip.Reader , pathPrefix string , r io.Reader ) (* Package , error ) {
167+ const (
168+ pkgExtZip = ".zip"
169+ pkgExtTarGz = ".tar.gz"
170+ pkgExtTarBz2 = ".tar.bz2"
171+ )
172+
173+ func detectPackageExtName (r ReadSeekAt ) (string , error ) {
174+ headBytes := make ([]byte , 4 )
175+ _ , err := r .ReadAt (headBytes , 0 )
176+ if err != nil {
177+ return "" , err
178+ }
179+ _ , err = r .Seek (0 , io .SeekStart )
180+ if err != nil {
181+ return "" , err
182+ }
183+ switch {
184+ case headBytes [0 ] == 'P' && headBytes [1 ] == 'K' :
185+ return pkgExtZip , nil
186+ case string (headBytes [:3 ]) == "BZh" :
187+ return pkgExtTarBz2 , nil
188+ case headBytes [0 ] == 0x1f && headBytes [1 ] == 0x8b :
189+ return pkgExtTarGz , nil
190+ }
191+ return "" , util .NewInvalidArgumentErrorf ("not a valid package file" )
192+ }
193+
194+ func readPackageFile (pkgExt string , r ReadSeekAt , filename string , limit int ) ([]byte , error ) {
195+ _ , err := r .Seek (0 , io .SeekStart )
196+ if err != nil {
197+ return nil , err
198+ }
199+
200+ switch pkgExt {
201+ case pkgExtZip :
202+ return readPackageFileZip (r , filename , limit )
203+ case pkgExtTarBz2 :
204+ bzip2Reader := bzip2 .NewReader (r )
205+ return readPackageFileTar (bzip2Reader , filename , limit )
206+ case pkgExtTarGz :
207+ gzReader , err := gzip .NewReader (r )
208+ if err != nil {
209+ return nil , err
210+ }
211+ return readPackageFileTar (gzReader , filename , limit )
212+ }
213+ return nil , util .NewInvalidArgumentErrorf ("not a valid package file" )
214+ }
215+
216+ // ParsePackage parses the metadata of a Composer package file
217+ func ParsePackage (r ReadSeekAt , optVersion ... string ) (* PackageInfo , error ) {
218+ pkgExt , err := detectPackageExtName (r )
219+ if err != nil {
220+ return nil , err
221+ }
222+ dataComposerJSON , err := readPackageFile (pkgExt , r , "composer.json" , 10 * 1024 * 1024 )
223+ if errors .Is (err , fs .ErrNotExist ) {
224+ return nil , ErrMissingComposerFile
225+ } else if err != nil {
226+ return nil , err
227+ }
228+
140229 var cj struct {
141230 Name string `json:"name"`
142231 Version string `json:"version"`
143232 Type string `json:"type"`
144233 Metadata
145234 }
146- if err := json .NewDecoder ( r ). Decode ( & cj ); err != nil {
235+ if err := json .Unmarshal ( dataComposerJSON , & cj ); err != nil {
147236 return nil , err
148237 }
149238
150239 if ! nameMatch .MatchString (cj .Name ) {
151240 return nil , ErrInvalidName
152241 }
153242
243+ if cj .Version == "" {
244+ cj .Version = util .OptionalArg (optVersion )
245+ }
154246 if cj .Version != "" {
155247 if _ , err := version .NewSemver (cj .Version ); err != nil {
156248 return nil , ErrInvalidVersion
@@ -168,17 +260,23 @@ func ParseComposerFile(archive *zip.Reader, pathPrefix string, r io.Reader) (*Pa
168260 if cj .Readme == "" {
169261 cj .Readme = "README.md"
170262 }
171- f , err := archive .Open (path .Join (pathPrefix , cj .Readme ))
172- if err == nil {
173- // 10kb limit for readme content
174- buf , _ := io .ReadAll (io .LimitReader (f , 10 * 1024 ))
175- cj .Readme = string (buf )
176- _ = f .Close ()
177- } else {
263+ dataReadmeMd , _ := readPackageFile (pkgExt , r , cj .Readme , 10 * 1024 )
264+
265+ // FIXME: legacy problem, the "Readme" field is abused, it should always be the path to the readme file
266+ if len (dataReadmeMd ) == 0 {
178267 cj .Readme = ""
268+ } else {
269+ cj .Readme = string (dataReadmeMd )
179270 }
180271
181- return & Package {
272+ // FIXME: legacy format: strings.ToLower(fmt.Sprintf("%s.%s.zip", strings.ReplaceAll(cp.Name, "/", "-"), cp.Version)), doesn't read good
273+ pkgFilename := strings .ReplaceAll (cj .Name , "/" , "-" )
274+ if cj .Version != "" {
275+ pkgFilename += "." + cj .Version
276+ }
277+ pkgFilename += pkgExt
278+ return & PackageInfo {
279+ Filename : pkgFilename ,
182280 Name : cj .Name ,
183281 Version : cj .Version ,
184282 Type : cj .Type ,
0 commit comments