glp/parsers/golang/godata.go

144 lines
3.6 KiB
Go

package golang
import (
// stdlib
"bytes"
"encoding/xml"
"io"
"log"
"net/http"
"strings"
"sync"
// local
"go.dev.pztrn.name/glp/configuration"
"go.dev.pztrn.name/glp/httpclient"
"go.dev.pztrn.name/glp/structs"
)
var (
goDatas map[string]*godata
goDatasMutex sync.Mutex
)
// This structure used for caching data about dependencies and prevent
// unneeded requests.
type godata struct {
SourceURLDirTemplate string
SourceURLFileTemplate string
VCSPath string
VCS string
}
// attrValue returns the attribute value for the case-insensitive key
// `name', or the empty string if nothing is found.
func attrValue(attrs []xml.Attr, name string) string {
for _, a := range attrs {
if strings.EqualFold(a.Name.Local, name) {
return a.Value
}
}
return ""
}
// Gets go-import and go-source data and fill it in dependency.
func getGoData(dependency *structs.Dependency) {
// Check if information about that dependency already cached.
// Use cached data if so.
goDatasMutex.Lock()
depInfo, cached := goDatas[dependency.Name+"@"+dependency.Version]
goDatasMutex.Unlock()
if cached {
dependency.VCS.SourceURLDirTemplate = depInfo.SourceURLDirTemplate
dependency.VCS.SourceURLFileTemplate = depInfo.SourceURLFileTemplate
dependency.VCS.VCS = depInfo.VCS
dependency.VCS.VCSPath = depInfo.VCSPath
return
}
// Dependencies are imported using URL which can be called with
// "?go-get=1" parameter to obtain required VCS data.
req, _ := http.NewRequest("GET", "http://"+dependency.Name, nil)
q := req.URL.Query()
q.Add("go-get", "1")
req.URL.RawQuery = q.Encode()
respBody := httpclient.GET(req)
if respBody == nil {
return
}
// HTML is hard to parse properly statically, so we will go
// line-by-line for <head> parsing.
resp := bytes.NewBuffer(respBody)
// Adopted headers parsing algo from Go itself.
// See https://github.com/golang/go/blob/95e1ea4598175a3461f40d00ce47a51e5fa6e5ea/src/cmd/go/internal/get/discovery.go
decoder := xml.NewDecoder(resp)
decoder.Strict = false
for {
token, err := decoder.Token()
if err != nil {
if err != io.EOF {
log.Fatalln("Failed to parse dependency's go-source and go-import things:", err.Error())
}
break
}
if e, ok := token.(xml.StartElement); ok && strings.EqualFold(e.Name.Local, "body") {
break
}
if e, ok := token.(xml.EndElement); ok && strings.EqualFold(e.Name.Local, "head") {
break
}
e, ok := token.(xml.StartElement)
if !ok || !strings.EqualFold(e.Name.Local, "meta") {
continue
}
// Check if we haven't found "go-import" or "go-source" in token's
// attributes.
if attrValue(e.Attr, "name") != "go-import" && attrValue(e.Attr, "name") != "go-source" {
continue
}
// Parse go-import data first.
if attrValue(e.Attr, "name") == "go-import" {
if f := strings.Fields(attrValue(e.Attr, "content")); len(f) == 3 {
dependency.VCS.VCS = f[1]
dependency.VCS.VCSPath = f[2]
}
}
// Then - go-source data.
if attrValue(e.Attr, "name") == "go-source" {
if f := strings.Fields(attrValue(e.Attr, "content")); len(f) == 4 {
dependency.VCS.SourceURLDirTemplate = f[2]
dependency.VCS.SourceURLFileTemplate = f[3]
}
}
}
if configuration.Cfg.Log.Debug {
log.Printf("go-import and go-source data parsed: %+v\n", dependency.VCS)
}
// Cache parsed data.
goDatasMutex.Lock()
goDatas[dependency.Name+"@"+dependency.Version] = &godata{
SourceURLDirTemplate: dependency.VCS.SourceURLDirTemplate,
SourceURLFileTemplate: dependency.VCS.SourceURLFileTemplate,
VCS: dependency.VCS.VCS,
VCSPath: dependency.VCS.VCSPath,
}
goDatasMutex.Unlock()
}