You've already forked library-registry
mirror of
https://github.com/arduino/library-registry.git
synced 2025-07-07 14:41:10 +03:00
With the current submission system, it's common for people to provide the tag/release URL rather than the repository URL. I think it likely the same sort of thing will occur with the new system. Previously, a very naive check was done for the path component of the URL to have two levels. It turns out that the Gitlab repository URLs have more levels than that. The superior approach is to use `git ls-remote` to verify that the URL is a Git repository.
391 lines
12 KiB
Go
391 lines
12 KiB
Go
// Copyright 2020 ARDUINO SA (http://www.arduino.cc/)
|
|
//
|
|
// This software is released under the GNU General Public License version 3.
|
|
// The terms of this license can be found at:
|
|
// https://www.gnu.org/licenses/gpl-3.0.en.html
|
|
//
|
|
// You can be released from the requirements of the above licenses by purchasing
|
|
// a commercial license. Buying such a license is mandatory if you want to
|
|
// modify or otherwise use the software for commercial activities involving the
|
|
// Arduino software without disclosing the source code of your own applications.
|
|
// To purchase a commercial license, send an email to license@arduino.cc.
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
|
|
"github.com/sourcegraph/go-diff/diff"
|
|
|
|
"github.com/arduino/go-paths-helper"
|
|
properties "github.com/arduino/go-properties-orderedmap"
|
|
)
|
|
|
|
// Git hosts that are supported for library repositories.
|
|
var supportedHosts []string = []string{
|
|
"bitbucket.org",
|
|
"github.com",
|
|
"gitlab.com",
|
|
}
|
|
|
|
// Libraries under these organizations will have the "Arduino" type and be linted with Arduino Lint in the "official" setting.
|
|
var officialOrganizations []string = []string{
|
|
"github.com/arduino",
|
|
"github.com/arduino-libraries",
|
|
"github.com/bcmi-labs",
|
|
"github.com/vidor-libraries",
|
|
}
|
|
|
|
// Libraries under these organizations will have the "Partner" type.
|
|
var partnerOrganizations []string = []string{
|
|
"github.com/Azure",
|
|
"github.com/ms-iot",
|
|
"github.com/ameltech",
|
|
}
|
|
|
|
// Libraries under these organizations will have the "Recommended" type.
|
|
var recommendedOrganizations []string = []string{
|
|
"github.com/adafruit",
|
|
}
|
|
|
|
// requestType is the type of the request data.
|
|
type requestType struct {
|
|
Type string `json:"type"` // Request type.
|
|
Submissions []submissionType `json:"submissions"` // Data for submitted libraries.
|
|
IndexEntry string `json:"indexEntry"` // Entry that will be made to the Library Manager index source file when the submission is accepted.
|
|
}
|
|
|
|
// submissionType is the type of the data for each individual library submitted in the request.
|
|
type submissionType struct {
|
|
SubmissionURL string `json:"submissionURL"` // Library repository URL as submitted by user. Used to identify the submission to the user.
|
|
NormalizedURL string `json:"normalizedURL"` // Submission URL in the standardized format that will be used in the index entry.
|
|
Name string `json:"name"` // Library name.
|
|
Official bool `json:"official"` // Whether the library is official.
|
|
Tag string `json:"tag"` // Name of the submission repository's latest tag, which is used as the basis for the index entry and validation.
|
|
Error string `json:"error"` // Error message.
|
|
}
|
|
|
|
// Command line flags.
|
|
var baseRef = flag.String("baseref", "", "")
|
|
var repoPath = flag.String("repopath", "", "")
|
|
var listName = flag.String("listname", "", "")
|
|
|
|
func main() {
|
|
// Validate flag input.
|
|
flag.Parse()
|
|
|
|
if *baseRef == "" {
|
|
errorExit("--baseref flag is required")
|
|
}
|
|
|
|
if *repoPath == "" {
|
|
errorExit("--repopath flag is required")
|
|
}
|
|
|
|
if *listName == "" {
|
|
errorExit("--listname flag is required")
|
|
}
|
|
|
|
listPath := paths.New(*repoPath, *listName)
|
|
exist, err := listPath.ExistCheck()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
if !exist {
|
|
errorExit(fmt.Sprintf("list file %s not found", listPath))
|
|
}
|
|
|
|
// Get the PR diff.
|
|
err = os.Chdir(*repoPath)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
rawDiff, err := exec.Command("git", "diff", "--unified=0", "--ignore-blank-lines", "--ignore-space-at-eol", *baseRef).Output()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// Parse the PR diff.
|
|
var request requestType
|
|
var submissionURLs []string
|
|
request.Type, submissionURLs = parseDiff(rawDiff, *listName)
|
|
|
|
// Process the submissions.
|
|
var indexEntries []string
|
|
for _, submissionURL := range submissionURLs {
|
|
submission, indexEntry := populateSubmission(submissionURL, listPath)
|
|
request.Submissions = append(request.Submissions, submission)
|
|
indexEntries = append(indexEntries, indexEntry)
|
|
}
|
|
|
|
// Assemble the index entry for the submissions.
|
|
request.IndexEntry = strings.Join(indexEntries, "%0A")
|
|
|
|
// Marshal the request data into a JSON document.
|
|
var marshalledRequest bytes.Buffer
|
|
jsonEncoder := json.NewEncoder(io.Writer(&marshalledRequest))
|
|
// By default, the json package HTML-sanitizes strings during marshalling (https://golang.org/pkg/encoding/json/#Marshal)
|
|
// It's not possible to change this behavior when using the simple json.MarshalIndent() approach.
|
|
jsonEncoder.SetEscapeHTML(false)
|
|
jsonEncoder.SetIndent("", "") // Single line.
|
|
err = jsonEncoder.Encode(request)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
fmt.Println(marshalledRequest.String())
|
|
}
|
|
|
|
// errorExit prints the error message in a standardized format and exits with status 1.
|
|
func errorExit(message string) {
|
|
fmt.Printf("ERROR: %s\n", message)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// parseDiff parses the request diff and returns the request type and list of submission URLs.
|
|
func parseDiff(rawDiff []byte, listName string) (string, []string) {
|
|
var submissionURLs []string
|
|
|
|
diffs, err := diff.ParseMultiFileDiff(rawDiff)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
if (len(diffs) != 1) || (diffs[0].OrigName[2:] != listName) || (diffs[0].OrigName[2:] != diffs[0].NewName[2:]) { // Git diffs have a a/ or b/ prefix on file names.
|
|
// This is not a Library Manager submission.
|
|
return "other", nil
|
|
}
|
|
|
|
var addedCount int
|
|
var deletedCount int
|
|
// Get the added URLs from the diff
|
|
for _, hunk := range diffs[0].Hunks {
|
|
hunkBody := string(hunk.Body)
|
|
for _, rawDiffLine := range strings.Split(hunkBody, "\n") {
|
|
diffLine := strings.TrimRight(rawDiffLine, " \t")
|
|
if len(diffLine) < 2 {
|
|
continue // Ignore blank lines.
|
|
}
|
|
|
|
switch diffLine[0] {
|
|
case '+':
|
|
addedCount++
|
|
submissionURLs = append(submissionURLs, strings.TrimSpace(diffLine[1:]))
|
|
case '-':
|
|
deletedCount++
|
|
default:
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
var requestType string
|
|
if addedCount > 0 && deletedCount == 0 {
|
|
requestType = "submission"
|
|
} else if addedCount == 0 && deletedCount > 0 {
|
|
requestType = "removal"
|
|
} else {
|
|
requestType = "modification"
|
|
}
|
|
|
|
return requestType, submissionURLs
|
|
}
|
|
|
|
// populateSubmission does the checks on the submission that aren't provided by Arduino Lint and gathers the necessary data on it.
|
|
func populateSubmission(submissionURL string, listPath *paths.Path) (submissionType, string) {
|
|
indexSourceSeparator := "|"
|
|
var submission submissionType
|
|
|
|
submission.SubmissionURL = submissionURL
|
|
|
|
// Normalize and validate submission URL.
|
|
submissionURLObject, err := url.Parse(submission.SubmissionURL)
|
|
if err != nil {
|
|
submission.Error = fmt.Sprintf("Invalid submission URL (%s)", err)
|
|
return submission, ""
|
|
}
|
|
|
|
// Check if URL is accessible.
|
|
httpResponse, err := http.Get(submissionURLObject.String())
|
|
if err != nil {
|
|
submission.Error = fmt.Sprintf("Unable to load submission URL: %s", err)
|
|
return submission, ""
|
|
}
|
|
if httpResponse.StatusCode != http.StatusOK {
|
|
submission.Error = "Unable to load submission URL. Is the repository public?"
|
|
return submission, ""
|
|
}
|
|
|
|
// Resolve redirects and normalize.
|
|
normalizedURLObject := normalizeURL(httpResponse.Request.URL)
|
|
|
|
submission.NormalizedURL = normalizedURLObject.String()
|
|
|
|
// Check if URL is from a supported Git host.
|
|
if !uRLIsUnder(normalizedURLObject, supportedHosts) {
|
|
submission.Error = normalizedURLObject.Host + " is not currently supported as a Git hosting website for Library Manager.%0ASee: https://github.com/arduino/Arduino/wiki/Library-Manager-FAQ#how-can-i-add-my-library-to-library-manager"
|
|
return submission, ""
|
|
}
|
|
|
|
// Check if URL is a Git repository
|
|
err = exec.Command("git", "ls-remote", normalizedURLObject.String()).Run()
|
|
if err != nil {
|
|
if _, ok := err.(*exec.ExitError); ok {
|
|
submission.Error = "Submission URL is not a Git clone URL (e.g., https://github.com/arduino-libraries/Servo)."
|
|
return submission, ""
|
|
}
|
|
|
|
panic(err)
|
|
}
|
|
|
|
// Check if the URL is already in the index.
|
|
listLines, err := listPath.ReadFileAsLines()
|
|
occurrences := 0
|
|
for _, listURL := range listLines {
|
|
listURLObject, err := url.Parse(strings.TrimSpace(listURL))
|
|
if err != nil {
|
|
panic(err) // All list items have already passed parsing so something is broken if this happens.
|
|
}
|
|
|
|
normalizedListURLObject := normalizeURL(listURLObject)
|
|
if normalizedListURLObject.String() == normalizedURLObject.String() {
|
|
occurrences++
|
|
if occurrences > 1 {
|
|
submission.Error = "Submission URL is already in the Library Manager index."
|
|
return submission, ""
|
|
}
|
|
}
|
|
}
|
|
|
|
// Determine the library types attributes.
|
|
submission.Official = uRLIsUnder(normalizedURLObject, officialOrganizations)
|
|
var types []string
|
|
if submission.Official {
|
|
types = append(types, "Arduino")
|
|
}
|
|
if uRLIsUnder(normalizedURLObject, partnerOrganizations) {
|
|
types = append(types, "Partner")
|
|
}
|
|
if uRLIsUnder(normalizedURLObject, recommendedOrganizations) {
|
|
types = append(types, "Recommended")
|
|
}
|
|
if types == nil {
|
|
types = append(types, "Contributed")
|
|
}
|
|
|
|
submissionClonePath, err := paths.MkTempDir("", "")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
err = exec.Command("git", "clone", "--depth", "1", normalizedURLObject.String(), submissionClonePath.String()).Run()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// Determine latest tag name in submission repo
|
|
err = os.Chdir(submissionClonePath.String())
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
err = exec.Command("git", "fetch", "--tags").Run()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
tagList, err := exec.Command("git", "rev-list", "--tags", "--max-count=1").Output()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
if string(tagList) == "" {
|
|
submission.Error = "The repository has no tags. You need to create a [release](https://docs.github.com/en/github/administering-a-repository/managing-releases-in-a-repository) or [tag](https://git-scm.com/docs/git-tag) that matches the `version` value in the library's library.properties file."
|
|
return submission, ""
|
|
}
|
|
latestTag, err := exec.Command("git", "describe", "--tags", strings.TrimSpace(string(tagList))).Output()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
submission.Tag = strings.TrimSpace(string(latestTag))
|
|
|
|
// Checkout latest tag.
|
|
err = exec.Command("git", "checkout", submission.Tag).Run()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// Get submission library name. It is necessary to record this in the index source entry because the library is locked to this name.
|
|
libraryPropertiesPath := submissionClonePath.Join("library.properties")
|
|
if !libraryPropertiesPath.Exist() {
|
|
submission.Error = "Library is missing a library.properties metadata file."
|
|
return submission, ""
|
|
}
|
|
libraryProperties, err := properties.LoadFromPath(libraryPropertiesPath)
|
|
if err != nil {
|
|
submission.Error = fmt.Sprintf("Invalid library.properties file (%s)", err)
|
|
return submission, ""
|
|
}
|
|
var ok bool
|
|
submission.Name, ok = libraryProperties.GetOk("name")
|
|
if !ok {
|
|
submission.Error = "library.properties is missing a name field"
|
|
return submission, ""
|
|
}
|
|
|
|
// Assemble Library Manager index source entry string
|
|
indexEntry := strings.Join(
|
|
[]string{
|
|
submission.NormalizedURL,
|
|
strings.Join(types, ","),
|
|
submission.Name,
|
|
},
|
|
indexSourceSeparator,
|
|
)
|
|
|
|
return submission, indexEntry
|
|
}
|
|
|
|
// normalizeURL converts the URL into the standardized format used in the index.
|
|
func normalizeURL(rawURL *url.URL) url.URL {
|
|
normalizedPath := strings.TrimRight(rawURL.Path, "/")
|
|
if !strings.HasSuffix(normalizedPath, ".git") {
|
|
normalizedPath += ".git"
|
|
}
|
|
|
|
return url.URL{
|
|
Scheme: "https",
|
|
Host: rawURL.Host,
|
|
Path: normalizedPath,
|
|
}
|
|
}
|
|
|
|
func uRLIsUnder(childURL url.URL, parentCandidates []string) bool {
|
|
for _, parentCandidate := range parentCandidates {
|
|
if !strings.HasSuffix(parentCandidate, "/") {
|
|
parentCandidate += "/"
|
|
}
|
|
parentCandidateURL, err := url.Parse("https://" + parentCandidate)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
isUnderPath, err := paths.New(childURL.Path).IsInsideDir(paths.New(parentCandidateURL.Path))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
if (childURL.Host == parentCandidateURL.Host) && isUnderPath {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|