forked from rosa/hakurei
Compare commits
4 Commits
pkgserver
...
wip-irdump
| Author | SHA1 | Date | |
|---|---|---|---|
| 29aa6a6dbe | |||
| 75fd820946 | |||
| 6cedf857ad | |||
| 71852e84c6 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,7 +7,6 @@
|
|||||||
|
|
||||||
# go generate
|
# go generate
|
||||||
/cmd/hakurei/LICENSE
|
/cmd/hakurei/LICENSE
|
||||||
/cmd/pkgserver/ui/static/*.js
|
|
||||||
/internal/pkg/testdata/testtool
|
/internal/pkg/testdata/testtool
|
||||||
/internal/rosa/hakurei_current.tar.gz
|
/internal/rosa/hakurei_current.tar.gz
|
||||||
|
|
||||||
|
|||||||
76
cmd/irdump/main.go
Normal file
76
cmd/irdump/main.go
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"hakurei.app/command"
|
||||||
|
"hakurei.app/internal/pkg"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
log.SetFlags(0)
|
||||||
|
log.SetPrefix("irdump: ")
|
||||||
|
|
||||||
|
var (
|
||||||
|
flagOutput string
|
||||||
|
flagReal bool
|
||||||
|
flagHeader bool
|
||||||
|
flagForce bool
|
||||||
|
flagRaw bool
|
||||||
|
)
|
||||||
|
c := command.New(os.Stderr, log.Printf, "irdump", func(args []string) (err error) {
|
||||||
|
var input *os.File
|
||||||
|
if len(args) != 1 {
|
||||||
|
return errors.New("irdump requires 1 argument")
|
||||||
|
}
|
||||||
|
if input, err = os.Open(args[0]); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer input.Close()
|
||||||
|
|
||||||
|
var output *os.File
|
||||||
|
if flagOutput == "" {
|
||||||
|
output = os.Stdout
|
||||||
|
} else {
|
||||||
|
defer output.Close()
|
||||||
|
if output, err = os.Create(flagOutput); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var out string
|
||||||
|
if out, err = pkg.Disassemble(input, flagReal, flagHeader, flagForce, flagRaw); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err = output.WriteString(out); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}).Flag(
|
||||||
|
&flagOutput,
|
||||||
|
"o", command.StringFlag(""),
|
||||||
|
"Output file for asm (leave empty for stdout)",
|
||||||
|
).Flag(
|
||||||
|
&flagReal,
|
||||||
|
"r", command.BoolFlag(false),
|
||||||
|
"skip label generation; idents print real value",
|
||||||
|
).Flag(
|
||||||
|
&flagHeader,
|
||||||
|
"H", command.BoolFlag(false),
|
||||||
|
"display artifact headers",
|
||||||
|
).Flag(
|
||||||
|
&flagForce,
|
||||||
|
"f", command.BoolFlag(false),
|
||||||
|
"force display (skip validations)",
|
||||||
|
).Flag(
|
||||||
|
&flagRaw,
|
||||||
|
"R", command.BoolFlag(false),
|
||||||
|
"don't format output",
|
||||||
|
)
|
||||||
|
|
||||||
|
c.MustParse(os.Args[1:], func(err error) {
|
||||||
|
log.Fatal(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"path"
|
|
||||||
"strconv"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"hakurei.app/internal/info"
|
|
||||||
"hakurei.app/internal/rosa"
|
|
||||||
)
|
|
||||||
|
|
||||||
// for lazy initialisation of serveInfo
|
|
||||||
var (
|
|
||||||
infoPayload struct {
|
|
||||||
// Current package count.
|
|
||||||
Count int `json:"count"`
|
|
||||||
// Hakurei version, set at link time.
|
|
||||||
HakureiVersion string `json:"hakurei_version"`
|
|
||||||
}
|
|
||||||
infoPayloadOnce sync.Once
|
|
||||||
)
|
|
||||||
|
|
||||||
// handleInfo writes constant system information.
|
|
||||||
func handleInfo(w http.ResponseWriter, _ *http.Request) {
|
|
||||||
infoPayloadOnce.Do(func() {
|
|
||||||
infoPayload.Count = int(rosa.PresetUnexportedStart)
|
|
||||||
infoPayload.HakureiVersion = info.Version()
|
|
||||||
})
|
|
||||||
// TODO(mae): cache entire response if no additional fields are planned
|
|
||||||
writeAPIPayload(w, infoPayload)
|
|
||||||
}
|
|
||||||
|
|
||||||
// newStatusHandler returns a [http.HandlerFunc] that offers status files for
|
|
||||||
// viewing or download, if available.
|
|
||||||
func (index *packageIndex) newStatusHandler(disposition bool) http.HandlerFunc {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
m, ok := index.names[path.Base(r.URL.Path)]
|
|
||||||
if !ok || !m.HasReport {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
contentType := "text/plain; charset=utf-8"
|
|
||||||
if disposition {
|
|
||||||
contentType = "application/octet-stream"
|
|
||||||
|
|
||||||
// quoting like this is unsound, but okay, because metadata is hardcoded
|
|
||||||
contentDisposition := `attachment; filename="`
|
|
||||||
contentDisposition += m.Name + "-"
|
|
||||||
if m.Version != "" {
|
|
||||||
contentDisposition += m.Version + "-"
|
|
||||||
}
|
|
||||||
contentDisposition += m.ids + `.log"`
|
|
||||||
w.Header().Set("Content-Disposition", contentDisposition)
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", contentType)
|
|
||||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
|
||||||
if err := func() (err error) {
|
|
||||||
defer index.handleAccess(&err)()
|
|
||||||
_, err = w.Write(m.status)
|
|
||||||
return
|
|
||||||
}(); err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
http.Error(
|
|
||||||
w, "cannot deliver status, contact maintainers",
|
|
||||||
http.StatusInternalServerError,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleGet writes a slice of metadata with specified order.
|
|
||||||
func (index *packageIndex) handleGet(w http.ResponseWriter, r *http.Request) {
|
|
||||||
q := r.URL.Query()
|
|
||||||
limit, err := strconv.Atoi(q.Get("limit"))
|
|
||||||
if err != nil || limit > 100 || limit < 1 {
|
|
||||||
http.Error(
|
|
||||||
w, "limit must be an integer between 1 and 100",
|
|
||||||
http.StatusBadRequest,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
i, err := strconv.Atoi(q.Get("index"))
|
|
||||||
if err != nil || i >= len(index.sorts[0]) || i < 0 {
|
|
||||||
http.Error(
|
|
||||||
w, "index must be an integer between 0 and "+
|
|
||||||
strconv.Itoa(int(rosa.PresetUnexportedStart-1)),
|
|
||||||
http.StatusBadRequest,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
sort, err := strconv.Atoi(q.Get("sort"))
|
|
||||||
if err != nil || sort >= len(index.sorts) || sort < 0 {
|
|
||||||
http.Error(
|
|
||||||
w, "sort must be an integer between 0 and "+
|
|
||||||
strconv.Itoa(sortOrderEnd),
|
|
||||||
http.StatusBadRequest,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
values := index.sorts[sort][i:min(i+limit, len(index.sorts[sort]))]
|
|
||||||
writeAPIPayload(w, &struct {
|
|
||||||
Values []*metadata `json:"values"`
|
|
||||||
}{values})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (index *packageIndex) handleSearch(w http.ResponseWriter, r *http.Request) {
|
|
||||||
q := r.URL.Query()
|
|
||||||
limit, err := strconv.Atoi(q.Get("limit"))
|
|
||||||
if err != nil || limit > 100 || limit < 1 {
|
|
||||||
http.Error(
|
|
||||||
w, "limit must be an integer between 1 and 100",
|
|
||||||
http.StatusBadRequest,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
i, err := strconv.Atoi(q.Get("index"))
|
|
||||||
if err != nil || i >= len(index.sorts[0]) || i < 0 {
|
|
||||||
http.Error(
|
|
||||||
w, "index must be an integer between 0 and "+
|
|
||||||
strconv.Itoa(int(rosa.PresetUnexportedStart-1)),
|
|
||||||
http.StatusBadRequest,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
search, err := url.QueryUnescape(q.Get("search"))
|
|
||||||
if len(search) > 100 || err != nil {
|
|
||||||
http.Error(
|
|
||||||
w, "search must be a string between 0 and 100 characters long",
|
|
||||||
http.StatusBadRequest,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
desc := q.Get("desc") == "true"
|
|
||||||
n, res, err := index.performSearchQuery(limit, i, search, desc)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
writeAPIPayload(w, &struct {
|
|
||||||
Count int `json:"count"`
|
|
||||||
Values []searchResult `json:"values"`
|
|
||||||
}{n, res})
|
|
||||||
}
|
|
||||||
|
|
||||||
// apiVersion is the name of the current API revision, as part of the pattern.
|
|
||||||
const apiVersion = "v1"
|
|
||||||
|
|
||||||
// registerAPI registers API handler functions.
|
|
||||||
func (index *packageIndex) registerAPI(mux *http.ServeMux) {
|
|
||||||
mux.HandleFunc("GET /api/"+apiVersion+"/info", handleInfo)
|
|
||||||
mux.HandleFunc("GET /api/"+apiVersion+"/get", index.handleGet)
|
|
||||||
mux.HandleFunc("GET /api/"+apiVersion+"/search", index.handleSearch)
|
|
||||||
mux.HandleFunc("GET /api/"+apiVersion+"/status/", index.newStatusHandler(false))
|
|
||||||
mux.HandleFunc("GET /status/", index.newStatusHandler(true))
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeAPIPayload sets headers common to API responses and encodes payload as
|
|
||||||
// JSON for the response body.
|
|
||||||
func writeAPIPayload(w http.ResponseWriter, payload any) {
|
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
||||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
|
||||||
w.Header().Set("Pragma", "no-cache")
|
|
||||||
w.Header().Set("Expires", "0")
|
|
||||||
|
|
||||||
if err := json.NewEncoder(w).Encode(payload); err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
http.Error(
|
|
||||||
w, "cannot encode payload, contact maintainers",
|
|
||||||
http.StatusInternalServerError,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,183 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"slices"
|
|
||||||
"strconv"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"hakurei.app/internal/info"
|
|
||||||
"hakurei.app/internal/rosa"
|
|
||||||
)
|
|
||||||
|
|
||||||
// prefix is prepended to every API path.
|
|
||||||
const prefix = "/api/" + apiVersion + "/"
|
|
||||||
|
|
||||||
func TestAPIInfo(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
handleInfo(w, httptest.NewRequestWithContext(
|
|
||||||
t.Context(),
|
|
||||||
http.MethodGet,
|
|
||||||
prefix+"info",
|
|
||||||
nil,
|
|
||||||
))
|
|
||||||
|
|
||||||
resp := w.Result()
|
|
||||||
checkStatus(t, resp, http.StatusOK)
|
|
||||||
checkAPIHeader(t, w.Header())
|
|
||||||
|
|
||||||
checkPayload(t, resp, struct {
|
|
||||||
Count int `json:"count"`
|
|
||||||
HakureiVersion string `json:"hakurei_version"`
|
|
||||||
}{int(rosa.PresetUnexportedStart), info.Version()})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAPIGet(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
const target = prefix + "get"
|
|
||||||
|
|
||||||
index := newIndex(t)
|
|
||||||
newRequest := func(suffix string) *httptest.ResponseRecorder {
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
index.handleGet(w, httptest.NewRequestWithContext(
|
|
||||||
t.Context(),
|
|
||||||
http.MethodGet,
|
|
||||||
target+suffix,
|
|
||||||
nil,
|
|
||||||
))
|
|
||||||
return w
|
|
||||||
}
|
|
||||||
|
|
||||||
checkValidate := func(t *testing.T, suffix string, vmin, vmax int, wantErr string) {
|
|
||||||
t.Run("invalid", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
w := newRequest("?" + suffix + "=invalid")
|
|
||||||
resp := w.Result()
|
|
||||||
checkError(t, resp, wantErr, http.StatusBadRequest)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("min", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
w := newRequest("?" + suffix + "=" + strconv.Itoa(vmin-1))
|
|
||||||
resp := w.Result()
|
|
||||||
checkError(t, resp, wantErr, http.StatusBadRequest)
|
|
||||||
|
|
||||||
w = newRequest("?" + suffix + "=" + strconv.Itoa(vmin))
|
|
||||||
resp = w.Result()
|
|
||||||
checkStatus(t, resp, http.StatusOK)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("max", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
w := newRequest("?" + suffix + "=" + strconv.Itoa(vmax+1))
|
|
||||||
resp := w.Result()
|
|
||||||
checkError(t, resp, wantErr, http.StatusBadRequest)
|
|
||||||
|
|
||||||
w = newRequest("?" + suffix + "=" + strconv.Itoa(vmax))
|
|
||||||
resp = w.Result()
|
|
||||||
checkStatus(t, resp, http.StatusOK)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("limit", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
checkValidate(
|
|
||||||
t, "index=0&sort=0&limit", 1, 100,
|
|
||||||
"limit must be an integer between 1 and 100",
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("index", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
checkValidate(
|
|
||||||
t, "limit=1&sort=0&index", 0, int(rosa.PresetUnexportedStart-1),
|
|
||||||
"index must be an integer between 0 and "+strconv.Itoa(int(rosa.PresetUnexportedStart-1)),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("sort", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
checkValidate(
|
|
||||||
t, "index=0&limit=1&sort", 0, int(sortOrderEnd),
|
|
||||||
"sort must be an integer between 0 and "+strconv.Itoa(int(sortOrderEnd)),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
checkWithSuffix := func(name, suffix string, want []*metadata) {
|
|
||||||
t.Run(name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
w := newRequest(suffix)
|
|
||||||
resp := w.Result()
|
|
||||||
checkStatus(t, resp, http.StatusOK)
|
|
||||||
checkAPIHeader(t, w.Header())
|
|
||||||
checkPayloadFunc(t, resp, func(got *struct {
|
|
||||||
Count int `json:"count"`
|
|
||||||
Values []*metadata `json:"values"`
|
|
||||||
}) bool {
|
|
||||||
return got.Count == len(want) &&
|
|
||||||
slices.EqualFunc(got.Values, want, func(a, b *metadata) bool {
|
|
||||||
return (a.Version == b.Version ||
|
|
||||||
a.Version == rosa.Unversioned ||
|
|
||||||
b.Version == rosa.Unversioned) &&
|
|
||||||
a.HasReport == b.HasReport &&
|
|
||||||
a.Name == b.Name &&
|
|
||||||
a.Description == b.Description &&
|
|
||||||
a.Website == b.Website
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
checkWithSuffix("declarationAscending", "?limit=2&index=0&sort=0", []*metadata{
|
|
||||||
{
|
|
||||||
Metadata: rosa.GetMetadata(0),
|
|
||||||
Version: rosa.Std.Version(0),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Metadata: rosa.GetMetadata(1),
|
|
||||||
Version: rosa.Std.Version(1),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
checkWithSuffix("declarationAscending offset", "?limit=3&index=5&sort=0", []*metadata{
|
|
||||||
{
|
|
||||||
Metadata: rosa.GetMetadata(5),
|
|
||||||
Version: rosa.Std.Version(5),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Metadata: rosa.GetMetadata(6),
|
|
||||||
Version: rosa.Std.Version(6),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Metadata: rosa.GetMetadata(7),
|
|
||||||
Version: rosa.Std.Version(7),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
checkWithSuffix("declarationDescending", "?limit=3&index=0&sort=1", []*metadata{
|
|
||||||
{
|
|
||||||
Metadata: rosa.GetMetadata(rosa.PresetUnexportedStart - 1),
|
|
||||||
Version: rosa.Std.Version(rosa.PresetUnexportedStart - 1),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Metadata: rosa.GetMetadata(rosa.PresetUnexportedStart - 2),
|
|
||||||
Version: rosa.Std.Version(rosa.PresetUnexportedStart - 2),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Metadata: rosa.GetMetadata(rosa.PresetUnexportedStart - 3),
|
|
||||||
Version: rosa.Std.Version(rosa.PresetUnexportedStart - 3),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
checkWithSuffix("declarationDescending offset", "?limit=1&index=37&sort=1", []*metadata{
|
|
||||||
{
|
|
||||||
Metadata: rosa.GetMetadata(rosa.PresetUnexportedStart - 38),
|
|
||||||
Version: rosa.Std.Version(rosa.PresetUnexportedStart - 38),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"cmp"
|
|
||||||
"errors"
|
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"hakurei.app/internal/pkg"
|
|
||||||
"hakurei.app/internal/rosa"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
declarationAscending = iota
|
|
||||||
declarationDescending
|
|
||||||
nameAscending
|
|
||||||
nameDescending
|
|
||||||
sizeAscending
|
|
||||||
sizeDescending
|
|
||||||
|
|
||||||
sortOrderEnd = iota - 1
|
|
||||||
)
|
|
||||||
|
|
||||||
// packageIndex refers to metadata by name and various sort orders.
|
|
||||||
type packageIndex struct {
|
|
||||||
sorts [sortOrderEnd + 1][rosa.PresetUnexportedStart]*metadata
|
|
||||||
names map[string]*metadata
|
|
||||||
search searchCache
|
|
||||||
// Taken from [rosa.Report] if available.
|
|
||||||
handleAccess func(*error) func()
|
|
||||||
}
|
|
||||||
|
|
||||||
// metadata holds [rosa.Metadata] extended with additional information.
|
|
||||||
type metadata struct {
|
|
||||||
p rosa.PArtifact
|
|
||||||
*rosa.Metadata
|
|
||||||
|
|
||||||
// Populated via [rosa.Toolchain.Version], [rosa.Unversioned] is equivalent
|
|
||||||
// to the zero value. Otherwise, the zero value is invalid.
|
|
||||||
Version string `json:"version,omitempty"`
|
|
||||||
// Output data size, available if present in report.
|
|
||||||
Size int64 `json:"size,omitempty"`
|
|
||||||
// Whether the underlying [pkg.Artifact] is present in the report.
|
|
||||||
HasReport bool `json:"report"`
|
|
||||||
|
|
||||||
// Ident string encoded ahead of time.
|
|
||||||
ids string
|
|
||||||
// Backed by [rosa.Report], access must be prepared by HandleAccess.
|
|
||||||
status []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// populate deterministically populates packageIndex, optionally with a report.
|
|
||||||
func (index *packageIndex) populate(cache *pkg.Cache, report *rosa.Report) (err error) {
|
|
||||||
if report != nil {
|
|
||||||
defer report.HandleAccess(&err)()
|
|
||||||
index.handleAccess = report.HandleAccess
|
|
||||||
}
|
|
||||||
|
|
||||||
var work [rosa.PresetUnexportedStart]*metadata
|
|
||||||
index.names = make(map[string]*metadata)
|
|
||||||
for p := range rosa.PresetUnexportedStart {
|
|
||||||
m := metadata{
|
|
||||||
p: p,
|
|
||||||
|
|
||||||
Metadata: rosa.GetMetadata(p),
|
|
||||||
Version: rosa.Std.Version(p),
|
|
||||||
}
|
|
||||||
if m.Version == "" {
|
|
||||||
return errors.New("invalid version from " + m.Name)
|
|
||||||
}
|
|
||||||
if m.Version == rosa.Unversioned {
|
|
||||||
m.Version = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if cache != nil && report != nil {
|
|
||||||
id := cache.Ident(rosa.Std.Load(p))
|
|
||||||
m.ids = pkg.Encode(id.Value())
|
|
||||||
m.status, m.Size = report.ArtifactOf(id)
|
|
||||||
m.HasReport = m.Size >= 0
|
|
||||||
}
|
|
||||||
|
|
||||||
work[p] = &m
|
|
||||||
index.names[m.Name] = &m
|
|
||||||
}
|
|
||||||
|
|
||||||
index.sorts[declarationAscending] = work
|
|
||||||
index.sorts[declarationDescending] = work
|
|
||||||
slices.Reverse(index.sorts[declarationDescending][:])
|
|
||||||
|
|
||||||
index.sorts[nameAscending] = work
|
|
||||||
slices.SortFunc(index.sorts[nameAscending][:], func(a, b *metadata) int {
|
|
||||||
return strings.Compare(a.Name, b.Name)
|
|
||||||
})
|
|
||||||
index.sorts[nameDescending] = index.sorts[nameAscending]
|
|
||||||
slices.Reverse(index.sorts[nameDescending][:])
|
|
||||||
|
|
||||||
index.sorts[sizeAscending] = work
|
|
||||||
slices.SortFunc(index.sorts[sizeAscending][:], func(a, b *metadata) int {
|
|
||||||
return cmp.Compare(a.Size, b.Size)
|
|
||||||
})
|
|
||||||
index.sorts[sizeDescending] = index.sorts[sizeAscending]
|
|
||||||
slices.Reverse(index.sorts[sizeDescending][:])
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"hakurei.app/check"
|
|
||||||
"hakurei.app/command"
|
|
||||||
"hakurei.app/internal/pkg"
|
|
||||||
"hakurei.app/internal/rosa"
|
|
||||||
"hakurei.app/message"
|
|
||||||
)
|
|
||||||
|
|
||||||
const shutdownTimeout = 15 * time.Second
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
log.SetFlags(0)
|
|
||||||
log.SetPrefix("pkgserver: ")
|
|
||||||
|
|
||||||
var (
|
|
||||||
flagBaseDir string
|
|
||||||
flagAddr string
|
|
||||||
)
|
|
||||||
|
|
||||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
|
|
||||||
defer stop()
|
|
||||||
msg := message.New(log.Default())
|
|
||||||
|
|
||||||
c := command.New(os.Stderr, log.Printf, "pkgserver", func(args []string) error {
|
|
||||||
var (
|
|
||||||
cache *pkg.Cache
|
|
||||||
report *rosa.Report
|
|
||||||
)
|
|
||||||
switch len(args) {
|
|
||||||
case 0:
|
|
||||||
break
|
|
||||||
|
|
||||||
case 1:
|
|
||||||
baseDir, err := check.NewAbs(flagBaseDir)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
cache, err = pkg.Open(ctx, msg, 0, 0, 0, baseDir)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer cache.Close()
|
|
||||||
|
|
||||||
report, err = rosa.OpenReport(args[0])
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
return errors.New("pkgserver requires 1 argument")
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
var index packageIndex
|
|
||||||
index.search = make(searchCache)
|
|
||||||
if err := index.populate(cache, report); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
ticker := time.NewTicker(1 * time.Minute)
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
ticker.Stop()
|
|
||||||
return
|
|
||||||
case <-ticker.C:
|
|
||||||
index.search.clean()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
var mux http.ServeMux
|
|
||||||
uiRoutes(&mux)
|
|
||||||
index.registerAPI(&mux)
|
|
||||||
server := http.Server{
|
|
||||||
Addr: flagAddr,
|
|
||||||
Handler: &mux,
|
|
||||||
}
|
|
||||||
go func() {
|
|
||||||
<-ctx.Done()
|
|
||||||
c, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
|
|
||||||
defer cancel()
|
|
||||||
if err := server.Shutdown(c); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
return server.ListenAndServe()
|
|
||||||
}).Flag(
|
|
||||||
&flagBaseDir,
|
|
||||||
"b", command.StringFlag(""),
|
|
||||||
"base directory for cache",
|
|
||||||
).Flag(
|
|
||||||
&flagAddr,
|
|
||||||
"addr", command.StringFlag(":8067"),
|
|
||||||
"TCP network address to listen on",
|
|
||||||
)
|
|
||||||
c.MustParse(os.Args[1:], func(err error) {
|
|
||||||
if errors.Is(err, http.ErrServerClosed) {
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
log.Fatal(err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// newIndex returns the address of a newly populated packageIndex.
|
|
||||||
func newIndex(t *testing.T) *packageIndex {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
var index packageIndex
|
|
||||||
if err := index.populate(nil, nil); err != nil {
|
|
||||||
t.Fatalf("populate: error = %v", err)
|
|
||||||
}
|
|
||||||
return &index
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkStatus checks response status code.
|
|
||||||
func checkStatus(t *testing.T, resp *http.Response, want int) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
if resp.StatusCode != want {
|
|
||||||
t.Errorf(
|
|
||||||
"StatusCode: %s, want %s",
|
|
||||||
http.StatusText(resp.StatusCode),
|
|
||||||
http.StatusText(want),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkHeader checks the value of a header entry.
|
|
||||||
func checkHeader(t *testing.T, h http.Header, key, want string) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
if got := h.Get(key); got != want {
|
|
||||||
t.Errorf("%s: %q, want %q", key, got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkAPIHeader checks common entries set for API endpoints.
|
|
||||||
func checkAPIHeader(t *testing.T, h http.Header) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
checkHeader(t, h, "Content-Type", "application/json; charset=utf-8")
|
|
||||||
checkHeader(t, h, "Cache-Control", "no-cache, no-store, must-revalidate")
|
|
||||||
checkHeader(t, h, "Pragma", "no-cache")
|
|
||||||
checkHeader(t, h, "Expires", "0")
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkPayloadFunc checks the JSON response of an API endpoint by passing it to f.
|
|
||||||
func checkPayloadFunc[T any](
|
|
||||||
t *testing.T,
|
|
||||||
resp *http.Response,
|
|
||||||
f func(got *T) bool,
|
|
||||||
) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
var got T
|
|
||||||
r := io.Reader(resp.Body)
|
|
||||||
if testing.Verbose() {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
r = io.TeeReader(r, &buf)
|
|
||||||
defer func() { t.Helper(); t.Log(buf.String()) }()
|
|
||||||
}
|
|
||||||
if err := json.NewDecoder(r).Decode(&got); err != nil {
|
|
||||||
t.Fatalf("Decode: error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !f(&got) {
|
|
||||||
t.Errorf("Body: %#v", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkPayload checks the JSON response of an API endpoint.
|
|
||||||
func checkPayload[T any](t *testing.T, resp *http.Response, want T) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
checkPayloadFunc(t, resp, func(got *T) bool {
|
|
||||||
return reflect.DeepEqual(got, &want)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkError(t *testing.T, resp *http.Response, error string, code int) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
checkStatus(t, resp, code)
|
|
||||||
if got, _ := io.ReadAll(resp.Body); string(got) != fmt.Sprintln(error) {
|
|
||||||
t.Errorf("Body: %q, want %q", string(got), error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"cmp"
|
|
||||||
"maps"
|
|
||||||
"regexp"
|
|
||||||
"slices"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type searchCache map[string]searchCacheEntry
|
|
||||||
type searchResult struct {
|
|
||||||
NameIndices [][]int `json:"name_matches"`
|
|
||||||
DescIndices [][]int `json:"desc_matches,omitempty"`
|
|
||||||
Score float64 `json:"score"`
|
|
||||||
*metadata
|
|
||||||
}
|
|
||||||
type searchCacheEntry struct {
|
|
||||||
query string
|
|
||||||
results []searchResult
|
|
||||||
expiry time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func (index *packageIndex) performSearchQuery(limit int, i int, search string, desc bool) (int, []searchResult, error) {
|
|
||||||
query := search
|
|
||||||
if desc {
|
|
||||||
query += ";withDesc"
|
|
||||||
}
|
|
||||||
entry, ok := index.search[query]
|
|
||||||
if ok && len(entry.results) > 0 {
|
|
||||||
return len(entry.results), entry.results[min(i, len(entry.results)-1):min(i+limit, len(entry.results))], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
regex, err := regexp.Compile(search)
|
|
||||||
if err != nil {
|
|
||||||
return 0, make([]searchResult, 0), err
|
|
||||||
}
|
|
||||||
res := make([]searchResult, 0)
|
|
||||||
for p := range maps.Values(index.names) {
|
|
||||||
nameIndices := regex.FindAllIndex([]byte(p.Name), -1)
|
|
||||||
var descIndices [][]int = nil
|
|
||||||
if desc {
|
|
||||||
descIndices = regex.FindAllIndex([]byte(p.Description), -1)
|
|
||||||
}
|
|
||||||
if nameIndices == nil && descIndices == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
score := float64(indexsum(nameIndices)) / (float64(len(nameIndices)) + 1)
|
|
||||||
if desc {
|
|
||||||
score += float64(indexsum(descIndices)) / (float64(len(descIndices)) + 1) / 10.0
|
|
||||||
}
|
|
||||||
res = append(res, searchResult{
|
|
||||||
NameIndices: nameIndices,
|
|
||||||
DescIndices: descIndices,
|
|
||||||
Score: score,
|
|
||||||
metadata: p,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
slices.SortFunc(res[:], func(a, b searchResult) int { return -cmp.Compare(a.Score, b.Score) })
|
|
||||||
expiry := time.Now().Add(1 * time.Minute)
|
|
||||||
entry = searchCacheEntry{
|
|
||||||
query: search,
|
|
||||||
results: res,
|
|
||||||
expiry: expiry,
|
|
||||||
}
|
|
||||||
index.search[query] = entry
|
|
||||||
|
|
||||||
return len(res), res[i:min(i+limit, len(entry.results))], nil
|
|
||||||
}
|
|
||||||
func (s *searchCache) clean() {
|
|
||||||
maps.DeleteFunc(*s, func(_ string, v searchCacheEntry) bool {
|
|
||||||
return v.expiry.Before(time.Now())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
func indexsum(in [][]int) int {
|
|
||||||
sum := 0
|
|
||||||
for i := 0; i < len(in); i++ {
|
|
||||||
sum += in[i][1] - in[i][0]
|
|
||||||
}
|
|
||||||
return sum
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import "net/http"
|
|
||||||
|
|
||||||
func serveWebUI(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
|
||||||
w.Header().Set("Pragma", "no-cache")
|
|
||||||
w.Header().Set("Expires", "0")
|
|
||||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
|
||||||
w.Header().Set("X-XSS-Protection", "1")
|
|
||||||
w.Header().Set("X-Frame-Options", "DENY")
|
|
||||||
|
|
||||||
http.ServeFileFS(w, r, content, "ui/index.html")
|
|
||||||
}
|
|
||||||
func serveStaticContent(w http.ResponseWriter, r *http.Request) {
|
|
||||||
switch r.URL.Path {
|
|
||||||
case "/static/style.css":
|
|
||||||
http.ServeFileFS(w, r, content, "ui/static/style.css")
|
|
||||||
case "/favicon.ico":
|
|
||||||
http.ServeFileFS(w, r, content, "ui/static/favicon.ico")
|
|
||||||
case "/static/index.js":
|
|
||||||
http.ServeFileFS(w, r, content, "ui/static/index.js")
|
|
||||||
default:
|
|
||||||
http.NotFound(w, r)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func uiRoutes(mux *http.ServeMux) {
|
|
||||||
mux.HandleFunc("GET /{$}", serveWebUI)
|
|
||||||
mux.HandleFunc("GET /favicon.ico", serveStaticContent)
|
|
||||||
mux.HandleFunc("GET /static/", serveStaticContent)
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<link rel="stylesheet" href="static/style.css">
|
|
||||||
<title>Hakurei PkgServer</title>
|
|
||||||
<script src="static/index.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Hakurei PkgServer</h1>
|
|
||||||
<div class="top-controls" id="top-controls-regular">
|
|
||||||
<p>Showing entries <span id="entry-counter"></span>.</p>
|
|
||||||
<span id="search-bar">
|
|
||||||
<label for="search">Search: </label>
|
|
||||||
<input type="text" name="search" id="search"/>
|
|
||||||
<button onclick="doSearch()">Find</button>
|
|
||||||
<label for="include-desc">Include descriptions: </label>
|
|
||||||
<input type="checkbox" name="include-desc" id="include-desc" checked/>
|
|
||||||
</span>
|
|
||||||
<div><label for="count">Entries per page: </label><select name="count" id="count">
|
|
||||||
<option value="10">10</option>
|
|
||||||
<option value="20">20</option>
|
|
||||||
<option value="30">30</option>
|
|
||||||
<option value="50">50</option>
|
|
||||||
</select></div>
|
|
||||||
<div><label for="sort">Sort by: </label><select name="sort" id="sort">
|
|
||||||
<option value="0">Definition (ascending)</option>
|
|
||||||
<option value="1">Definition (descending)</option>
|
|
||||||
<option value="2">Name (ascending)</option>
|
|
||||||
<option value="3">Name (descending)</option>
|
|
||||||
<option value="4">Size (ascending)</option>
|
|
||||||
<option value="5">Size (descending)</option>
|
|
||||||
</select></div>
|
|
||||||
</div>
|
|
||||||
<div class="top-controls" id="search-top-controls" hidden>
|
|
||||||
<p>Showing search results <span id="search-entry-counter"></span> for query "<span id="search-query"></span>".</p>
|
|
||||||
<button onclick="exitSearch()">Back</button>
|
|
||||||
<div><label for="search-count">Entries per page: </label><select name="search-count" id="search-count">
|
|
||||||
<option value="10">10</option>
|
|
||||||
<option value="20">20</option>
|
|
||||||
<option value="30">30</option>
|
|
||||||
<option value="50">50</option>
|
|
||||||
</select></div>
|
|
||||||
<p>Sorted by best match</p>
|
|
||||||
</div>
|
|
||||||
<div class="page-controls"><a href="javascript:prevPage()">« Previous</a> <input type="text" class="page-number" value="1"/> <a href="javascript:nextPage()">Next »</a></div>
|
|
||||||
<table id="pkg-list">
|
|
||||||
<tr><td>Loading...</td></tr>
|
|
||||||
</table>
|
|
||||||
<div class="page-controls"><a href="javascript:prevPage()">« Previous</a> <input type="text" class="page-number" value="1"/> <a href="javascript:nextPage()">Next »</a></div>
|
|
||||||
<footer>
|
|
||||||
<p>©<a href="https://hakurei.app/">Hakurei</a> (<span id="hakurei-version">unknown</span>). Licensed under the MIT license.</p>
|
|
||||||
</footer>
|
|
||||||
<script>main();</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,331 +0,0 @@
|
|||||||
interface PackageIndexEntry {
|
|
||||||
name: string
|
|
||||||
size?: number
|
|
||||||
description?: string
|
|
||||||
website?: string
|
|
||||||
version?: string
|
|
||||||
report?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
function entryToHTML(entry: PackageIndexEntry | SearchResult): HTMLTableRowElement {
|
|
||||||
let v = entry.version != null ? `<span>${escapeHtml(entry.version)}</span>` : ""
|
|
||||||
let s = entry.size != null && entry.size > 0 ? `<p>Size: ${toByteSizeString(entry.size)} (${entry.size})</p>` : ""
|
|
||||||
let n: string
|
|
||||||
let d: string
|
|
||||||
if ('name_matches' in entry) {
|
|
||||||
n = `<h2>${nameMatches(entry as SearchResult)} ${v}</h2>`
|
|
||||||
} else {
|
|
||||||
n = `<h2>${escapeHtml(entry.name)} ${v}</h2>`
|
|
||||||
}
|
|
||||||
if ('desc_matches' in entry && STATE.getIncludeDescriptions()) {
|
|
||||||
d = descMatches(entry as SearchResult)
|
|
||||||
} else {
|
|
||||||
d = (entry as PackageIndexEntry).description != null ? `<p>${escapeHtml((entry as PackageIndexEntry).description)}</p>` : ""
|
|
||||||
}
|
|
||||||
let w = entry.website != null ? `<a href="${encodeURI(entry.website)}">Website</a>` : ""
|
|
||||||
let r = entry.report ? `Log (<a href=\"${encodeURI('/api/v1/status/' + entry.name)}\">View</a> | <a href=\"${encodeURI('/status/' + entry.name)}\">Download</a>)` : ""
|
|
||||||
let row = <HTMLTableRowElement>(document.createElement('tr'))
|
|
||||||
row.innerHTML = `<td>
|
|
||||||
${n}
|
|
||||||
${d}
|
|
||||||
${s}
|
|
||||||
${w}
|
|
||||||
${r}
|
|
||||||
</td>`
|
|
||||||
return row
|
|
||||||
}
|
|
||||||
|
|
||||||
function nameMatches(sr: SearchResult): string {
|
|
||||||
return markMatches(sr.name, sr.name_matches)
|
|
||||||
}
|
|
||||||
|
|
||||||
function descMatches(sr: SearchResult): string {
|
|
||||||
return markMatches(sr.description!, sr.desc_matches)
|
|
||||||
}
|
|
||||||
|
|
||||||
function markMatches(str: string, indices: [number, number][]): string {
|
|
||||||
if (indices == null) {
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
let out: string = ""
|
|
||||||
let j = 0
|
|
||||||
for (let i = 0; i < str.length; i++) {
|
|
||||||
if (j < indices.length) {
|
|
||||||
if (i === indices[j][0]) {
|
|
||||||
out += `<mark>${escapeHtmlChar(str[i])}`
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (i === indices[j][1]) {
|
|
||||||
out += `</mark>${escapeHtmlChar(str[i])}`
|
|
||||||
j++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out += escapeHtmlChar(str[i])
|
|
||||||
}
|
|
||||||
if (indices[j] !== undefined) {
|
|
||||||
out += "</mark>"
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
function toByteSizeString(bytes: number): string {
|
|
||||||
if (bytes == null) return `unspecified`
|
|
||||||
if (bytes < 1024) return `${bytes}B`
|
|
||||||
if (bytes < Math.pow(1024, 2)) return `${(bytes / 1024).toFixed(2)}kiB`
|
|
||||||
if (bytes < Math.pow(1024, 3)) return `${(bytes / Math.pow(1024, 2)).toFixed(2)}MiB`
|
|
||||||
if (bytes < Math.pow(1024, 4)) return `${(bytes / Math.pow(1024, 3)).toFixed(2)}GiB`
|
|
||||||
if (bytes < Math.pow(1024, 5)) return `${(bytes / Math.pow(1024, 4)).toFixed(2)}TiB`
|
|
||||||
return "not only is it big, it's large"
|
|
||||||
}
|
|
||||||
|
|
||||||
const API_VERSION = 1
|
|
||||||
const ENDPOINT = `/api/v${API_VERSION}`
|
|
||||||
|
|
||||||
interface InfoPayload {
|
|
||||||
count?: number
|
|
||||||
hakurei_version?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
async function infoRequest(): Promise<InfoPayload> {
|
|
||||||
const res = await fetch(`${ENDPOINT}/info`)
|
|
||||||
const payload = await res.json()
|
|
||||||
return payload as InfoPayload
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GetPayload {
|
|
||||||
values?: PackageIndexEntry[]
|
|
||||||
}
|
|
||||||
|
|
||||||
enum SortOrders {
|
|
||||||
DeclarationAscending,
|
|
||||||
DeclarationDescending,
|
|
||||||
NameAscending,
|
|
||||||
NameDescending
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getRequest(limit: number, index: number, sort: SortOrders): Promise<GetPayload> {
|
|
||||||
const res = await fetch(`${ENDPOINT}/get?limit=${limit}&index=${index}&sort=${sort.valueOf()}`)
|
|
||||||
const payload = await res.json()
|
|
||||||
return payload as GetPayload
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SearchResult extends PackageIndexEntry {
|
|
||||||
name_matches: [number, number][]
|
|
||||||
desc_matches: [number, number][]
|
|
||||||
score: number
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SearchPayload {
|
|
||||||
count?: number
|
|
||||||
values?: SearchResult[]
|
|
||||||
}
|
|
||||||
|
|
||||||
async function searchRequest(limit: number, index: number, search: string, desc: boolean): Promise<SearchPayload> {
|
|
||||||
const res = await fetch(`${ENDPOINT}/search?limit=${limit}&index=${index}&search=${encodeURIComponent(search)}&desc=${desc}`)
|
|
||||||
if (!res.ok) {
|
|
||||||
exitSearch()
|
|
||||||
alert("invalid search query!")
|
|
||||||
return Promise.reject(res.statusText)
|
|
||||||
}
|
|
||||||
const payload = await res.json()
|
|
||||||
return payload as SearchPayload
|
|
||||||
}
|
|
||||||
|
|
||||||
class State {
|
|
||||||
entriesPerPage: number = 10
|
|
||||||
entryIndex: number = 0
|
|
||||||
maxTotal: number = 0
|
|
||||||
maxEntries: number = 0
|
|
||||||
sort: SortOrders = SortOrders.DeclarationAscending
|
|
||||||
search: boolean = false
|
|
||||||
|
|
||||||
getEntriesPerPage(): number {
|
|
||||||
return this.entriesPerPage
|
|
||||||
}
|
|
||||||
|
|
||||||
setEntriesPerPage(entriesPerPage: number) {
|
|
||||||
this.entriesPerPage = entriesPerPage
|
|
||||||
this.setEntryIndex(Math.floor(this.getEntryIndex() / entriesPerPage) * entriesPerPage)
|
|
||||||
}
|
|
||||||
|
|
||||||
getEntryIndex(): number {
|
|
||||||
return this.entryIndex
|
|
||||||
}
|
|
||||||
|
|
||||||
setEntryIndex(entryIndex: number) {
|
|
||||||
this.entryIndex = entryIndex
|
|
||||||
this.updatePage()
|
|
||||||
this.updateRange()
|
|
||||||
this.updateListings()
|
|
||||||
}
|
|
||||||
|
|
||||||
getMaxTotal(): number {
|
|
||||||
return this.maxTotal
|
|
||||||
}
|
|
||||||
|
|
||||||
setMaxTotal(max: number) {
|
|
||||||
this.maxTotal = max
|
|
||||||
}
|
|
||||||
|
|
||||||
getSortOrder(): SortOrders {
|
|
||||||
return this.sort
|
|
||||||
}
|
|
||||||
|
|
||||||
setSortOrder(sortOrder: SortOrders) {
|
|
||||||
this.sort = sortOrder
|
|
||||||
this.setEntryIndex(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePage() {
|
|
||||||
let page = Math.ceil(((this.getEntryIndex() + this.getEntriesPerPage()) - 1) / this.getEntriesPerPage())
|
|
||||||
for (let e of document.getElementsByClassName("page-number")) {
|
|
||||||
(e as HTMLInputElement).value = String(page)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateRange() {
|
|
||||||
let max = Math.min(this.getEntryIndex() + this.getEntriesPerPage(), this.getMaxTotal())
|
|
||||||
document.getElementById("entry-counter")!.textContent = `${this.getEntryIndex() + 1}-${max} of ${this.getMaxTotal()}`
|
|
||||||
if (this.search) {
|
|
||||||
document.getElementById("search-entry-counter")!.textContent = `${this.getEntryIndex() + 1}-${max} of ${this.maxTotal}/${this.maxEntries}`
|
|
||||||
document.getElementById("search-query")!.innerHTML = `<code>${escapeHtml(this.getSearchQuery())}</code>`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getSearchQuery(): string {
|
|
||||||
let queryString = document.getElementById("search")!;
|
|
||||||
return (queryString as HTMLInputElement).value
|
|
||||||
}
|
|
||||||
|
|
||||||
getIncludeDescriptions(): boolean {
|
|
||||||
let includeDesc = document.getElementById("include-desc")!;
|
|
||||||
return (includeDesc as HTMLInputElement).checked
|
|
||||||
}
|
|
||||||
|
|
||||||
updateListings() {
|
|
||||||
if (this.search) {
|
|
||||||
searchRequest(this.getEntriesPerPage(), this.getEntryIndex(), this.getSearchQuery(), this.getIncludeDescriptions())
|
|
||||||
.then(res => {
|
|
||||||
let table = document.getElementById("pkg-list")!
|
|
||||||
table.innerHTML = ''
|
|
||||||
for (let row of res.values!) {
|
|
||||||
table.appendChild(entryToHTML(row))
|
|
||||||
}
|
|
||||||
STATE.maxTotal = res.count!
|
|
||||||
STATE.updateRange()
|
|
||||||
if(res.count! < 1) {
|
|
||||||
exitSearch()
|
|
||||||
alert("no results found!")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
getRequest(this.getEntriesPerPage(), this.getEntryIndex(), this.getSortOrder())
|
|
||||||
.then(res => {
|
|
||||||
let table = document.getElementById("pkg-list")!
|
|
||||||
table.innerHTML = ''
|
|
||||||
for (let row of res.values!) {
|
|
||||||
table.appendChild(entryToHTML(row))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let STATE: State
|
|
||||||
|
|
||||||
|
|
||||||
function lastPageIndex(): number {
|
|
||||||
return Math.floor(STATE.getMaxTotal() / STATE.getEntriesPerPage()) * STATE.getEntriesPerPage()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setPage(page: number) {
|
|
||||||
STATE.setEntryIndex(Math.max(0, Math.min(STATE.getEntriesPerPage() * (page - 1), lastPageIndex())))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function escapeHtml(str?: string): string {
|
|
||||||
let out: string = ''
|
|
||||||
if (str == undefined) return ""
|
|
||||||
for (let i = 0; i < str.length; i++) {
|
|
||||||
out += escapeHtmlChar(str[i])
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeHtmlChar(char: string): string {
|
|
||||||
if (char.length != 1) return char
|
|
||||||
switch (char[0]) {
|
|
||||||
case '&':
|
|
||||||
return "&"
|
|
||||||
case '<':
|
|
||||||
return "<"
|
|
||||||
case '>':
|
|
||||||
return ">"
|
|
||||||
case '"':
|
|
||||||
return """
|
|
||||||
case "'":
|
|
||||||
return "'"
|
|
||||||
default:
|
|
||||||
return char
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function firstPage() {
|
|
||||||
STATE.setEntryIndex(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
function prevPage() {
|
|
||||||
let index = STATE.getEntryIndex()
|
|
||||||
STATE.setEntryIndex(Math.max(0, index - STATE.getEntriesPerPage()))
|
|
||||||
}
|
|
||||||
|
|
||||||
function lastPage() {
|
|
||||||
STATE.setEntryIndex(lastPageIndex())
|
|
||||||
}
|
|
||||||
|
|
||||||
function nextPage() {
|
|
||||||
let index = STATE.getEntryIndex()
|
|
||||||
STATE.setEntryIndex(Math.min(lastPageIndex(), index + STATE.getEntriesPerPage()))
|
|
||||||
}
|
|
||||||
|
|
||||||
function doSearch() {
|
|
||||||
document.getElementById("top-controls-regular")!.toggleAttribute("hidden");
|
|
||||||
document.getElementById("search-top-controls")!.toggleAttribute("hidden");
|
|
||||||
STATE.search = true;
|
|
||||||
STATE.setEntryIndex(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function exitSearch() {
|
|
||||||
document.getElementById("top-controls-regular")!.toggleAttribute("hidden");
|
|
||||||
document.getElementById("search-top-controls")!.toggleAttribute("hidden");
|
|
||||||
STATE.search = false;
|
|
||||||
STATE.setMaxTotal(STATE.maxEntries)
|
|
||||||
STATE.setEntryIndex(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
function main() {
|
|
||||||
STATE = new State()
|
|
||||||
infoRequest()
|
|
||||||
.then(res => {
|
|
||||||
STATE.maxEntries = res.count!
|
|
||||||
STATE.setMaxTotal(STATE.maxEntries)
|
|
||||||
document.getElementById("hakurei-version")!.textContent = res.hakurei_version!
|
|
||||||
STATE.updateRange()
|
|
||||||
STATE.updateListings()
|
|
||||||
})
|
|
||||||
for (let e of document.getElementsByClassName("page-number")) {
|
|
||||||
e.addEventListener("change", (_) => {
|
|
||||||
setPage(parseInt((e as HTMLInputElement).value))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
document.getElementById("count")?.addEventListener("change", (event) => {
|
|
||||||
STATE.setEntriesPerPage(parseInt((event.target as HTMLSelectElement).value))
|
|
||||||
})
|
|
||||||
document.getElementById("sort")?.addEventListener("change", (event) => {
|
|
||||||
STATE.setSortOrder(parseInt((event.target as HTMLSelectElement).value))
|
|
||||||
})
|
|
||||||
document.getElementById("search")?.addEventListener("keyup", (event) => {
|
|
||||||
if (event.key === 'Enter') doSearch()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB |
@@ -1,21 +0,0 @@
|
|||||||
.page-number {
|
|
||||||
width: 2em;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.page-number {
|
|
||||||
width: 2em;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
html {
|
|
||||||
background-color: #2c2c2c;
|
|
||||||
color: ghostwhite;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (prefers-color-scheme: light) {
|
|
||||||
html {
|
|
||||||
background-color: #d3d3d3;
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ES2024",
|
|
||||||
"strict": true,
|
|
||||||
"alwaysStrict": true,
|
|
||||||
"outDir": "static"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
//go:build frontend
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import "embed"
|
|
||||||
|
|
||||||
//go:generate tsc -p ui
|
|
||||||
//go:embed ui/*
|
|
||||||
var content embed.FS
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
//go:build !frontend
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import "testing/fstest"
|
|
||||||
|
|
||||||
var content fstest.MapFS
|
|
||||||
293
internal/pkg/asm.go
Normal file
293
internal/pkg/asm.go
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
package pkg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"unique"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AsmMessage struct {
|
||||||
|
data AsmData
|
||||||
|
sig AsmSig
|
||||||
|
}
|
||||||
|
type AsmGenerator struct {
|
||||||
|
i uintptr
|
||||||
|
as chan AsmMessage
|
||||||
|
lbl []uintptr
|
||||||
|
}
|
||||||
|
type AsmSig int
|
||||||
|
|
||||||
|
const (
|
||||||
|
AsmSigEnd AsmSig = iota
|
||||||
|
AsmSigLbl
|
||||||
|
AsmSigHead
|
||||||
|
AsmSigVal
|
||||||
|
)
|
||||||
|
|
||||||
|
// GenerateAll collects all asm sent to this generator and outputs formatted data to the given [io.Writer].
|
||||||
|
func (g *AsmGenerator) GenerateAll(w io.Writer) error {
|
||||||
|
for a := <-g.as; a.sig != AsmSigEnd; a = <-g.as {
|
||||||
|
switch a.sig {
|
||||||
|
case AsmSigLbl:
|
||||||
|
|
||||||
|
break
|
||||||
|
case AsmSigHead:
|
||||||
|
|
||||||
|
break
|
||||||
|
case AsmSigVal:
|
||||||
|
_, err := w.Write([]byte(a.data.Line(g)))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
panic("invalid asm signal")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type AsmFormatter struct {
|
||||||
|
// if false, generate labels. if true, bare idents will be shown.
|
||||||
|
Real bool
|
||||||
|
// if true, header data and the dependencies list will be shown.
|
||||||
|
ShowHeader bool
|
||||||
|
// if true, don't generate raw byte data (only generate raw assembly).
|
||||||
|
Raw bool
|
||||||
|
}
|
||||||
|
type AsmData interface {
|
||||||
|
Line(*AsmGenerator) string
|
||||||
|
Offset(int)
|
||||||
|
}
|
||||||
|
|
||||||
|
type AsmDataNone struct{}
|
||||||
|
|
||||||
|
func (a AsmDataNone) Line(*AsmGenerator) string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
func (a AsmDataNone) Offset(int) {}
|
||||||
|
|
||||||
|
type AsmLine struct {
|
||||||
|
pos int
|
||||||
|
word int
|
||||||
|
kindData int64
|
||||||
|
valueData []byte
|
||||||
|
indent int
|
||||||
|
kind string
|
||||||
|
value string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l AsmLine) Line(gen *AsmGenerator) string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
func (l AsmLine) Offset(offset int) {
|
||||||
|
l.pos += offset
|
||||||
|
}
|
||||||
|
|
||||||
|
type AsmHeaderLine struct {
|
||||||
|
pos int
|
||||||
|
kind string
|
||||||
|
kindData int64
|
||||||
|
label string
|
||||||
|
id unique.Handle[ID]
|
||||||
|
}
|
||||||
|
|
||||||
|
var spacingLine = AsmLine{
|
||||||
|
pos: -1,
|
||||||
|
kindData: -1,
|
||||||
|
valueData: nil,
|
||||||
|
indent: 0,
|
||||||
|
kind: "",
|
||||||
|
value: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
func Disassemble(r io.Reader, real bool, showHeader bool, force bool, raw bool) (s string, err error) {
|
||||||
|
var lines []AsmLine
|
||||||
|
sb := new(strings.Builder)
|
||||||
|
header := true
|
||||||
|
pos := new(int)
|
||||||
|
|
||||||
|
for err == nil {
|
||||||
|
if header {
|
||||||
|
var kind uint64
|
||||||
|
var size uint64
|
||||||
|
var bsize []byte
|
||||||
|
p := *pos
|
||||||
|
if _, kind, err = nextUint64(r, pos); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if bsize, size, err = nextUint64(r, pos); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if showHeader {
|
||||||
|
lines = append(lines, AsmLine{p, 8, int64(kind), bsize, 0, "head " + intToKind(kind), ""})
|
||||||
|
}
|
||||||
|
for i := 0; uint64(i) < size; i++ {
|
||||||
|
var did ID
|
||||||
|
var dkind uint64
|
||||||
|
p := *pos
|
||||||
|
if _, dkind, err = nextUint64(r, pos); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if _, did, err = nextIdent(r, pos); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if showHeader {
|
||||||
|
lines = append(lines, AsmLine{p, 8, int64(dkind), nil, 1, intToKind(dkind), Encode(did)})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
header = false
|
||||||
|
}
|
||||||
|
var k uint32
|
||||||
|
p := *pos
|
||||||
|
if _, k, err = nextUint32(r, pos); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
kind := IRValueKind(k)
|
||||||
|
switch kind {
|
||||||
|
case IRKindEnd:
|
||||||
|
var a uint32
|
||||||
|
var ba []byte
|
||||||
|
if ba, a, err = nextUint32(r, pos); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if a&1 != 0 {
|
||||||
|
var sum Checksum
|
||||||
|
if _, sum, err = nextIdent(r, pos); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
lines = append(lines, AsmLine{p, 4, int64(kind), ba, 1, "end ", Encode(sum)})
|
||||||
|
} else {
|
||||||
|
lines = append(lines, AsmLine{p, 4, int64(kind), []byte{0, 0, 0, 0}, 1, "end ", ""})
|
||||||
|
}
|
||||||
|
lines = append(lines, spacingLine)
|
||||||
|
header = true
|
||||||
|
continue
|
||||||
|
|
||||||
|
case IRKindIdent:
|
||||||
|
var a []byte
|
||||||
|
// discard ancillary
|
||||||
|
if a, _, err = nextUint32(r, pos); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
var id ID
|
||||||
|
if _, id, err = nextIdent(r, pos); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
lines = append(lines, AsmLine{p, 4, int64(kind), a, 1, "id ", Encode(id)})
|
||||||
|
continue
|
||||||
|
case IRKindUint32:
|
||||||
|
var i uint32
|
||||||
|
var bi []byte
|
||||||
|
if bi, i, err = nextUint32(r, pos); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
lines = append(lines, AsmLine{p, 4, int64(kind), bi, 1, "int ", strconv.FormatUint(uint64(i), 10)})
|
||||||
|
case IRKindString:
|
||||||
|
var l uint32
|
||||||
|
var bl []byte
|
||||||
|
if bl, l, err = nextUint32(r, pos); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
s := make([]byte, l+(wordSize-(l)%wordSize)%wordSize)
|
||||||
|
var n int
|
||||||
|
if n, err = r.Read(s); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
*pos = *pos + n
|
||||||
|
|
||||||
|
lines = append(lines, AsmLine{p, 4, int64(kind), bl, 1, "str ", strconv.Quote(string(s[:l]))})
|
||||||
|
continue
|
||||||
|
default:
|
||||||
|
var bi []byte
|
||||||
|
if bi, _, err = nextUint32(r, pos); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
lines = append(lines, AsmLine{p, 4, int64(kind), bi, 1, "????", ""})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != io.EOF {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = nil
|
||||||
|
for _, line := range lines {
|
||||||
|
if raw {
|
||||||
|
if line.pos != -1 {
|
||||||
|
sb.WriteString(fmt.Sprintf("%s\t%s\n", line.kind, line.value))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if line.pos == -1 {
|
||||||
|
sb.WriteString("\n")
|
||||||
|
} else if line.word == 4 {
|
||||||
|
sb.WriteString(fmt.Sprintf("%06x: %04x %04x%s %s %s\n", line.pos, binary.LittleEndian.AppendUint32(nil, uint32(line.kindData)), line.valueData, headerSpacing(showHeader), line.kind, line.value))
|
||||||
|
} else {
|
||||||
|
kind := binary.LittleEndian.AppendUint64(nil, uint64(line.kindData))
|
||||||
|
value := line.valueData
|
||||||
|
if len(value) == 8 {
|
||||||
|
sb.WriteString(fmt.Sprintf("%06x: %04x %04x %04x %04x %s %s\n", line.pos, kind[:4], kind[4:], value[:4], value[4:], line.kind, line.value))
|
||||||
|
} else {
|
||||||
|
sb.WriteString(fmt.Sprintf("%06x: %04x %04x %s %s\n", line.pos, kind[:4], kind[4:], line.kind, line.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return sb.String(), err
|
||||||
|
}
|
||||||
|
func nextUint32(r io.Reader, pos *int) ([]byte, uint32, error) {
|
||||||
|
i := make([]byte, 4)
|
||||||
|
_, err := r.Read(i)
|
||||||
|
if err != nil {
|
||||||
|
return i, 0, err
|
||||||
|
}
|
||||||
|
p := *pos + 4
|
||||||
|
*pos = p
|
||||||
|
return i, binary.LittleEndian.Uint32(i), nil
|
||||||
|
}
|
||||||
|
func nextUint64(r io.Reader, pos *int) ([]byte, uint64, error) {
|
||||||
|
i := make([]byte, 8)
|
||||||
|
_, err := r.Read(i)
|
||||||
|
if err != nil {
|
||||||
|
return i, 0, err
|
||||||
|
}
|
||||||
|
p := *pos + 8
|
||||||
|
*pos = p
|
||||||
|
return i, binary.LittleEndian.Uint64(i), nil
|
||||||
|
}
|
||||||
|
func nextIdent(r io.Reader, pos *int) ([]byte, ID, error) {
|
||||||
|
i := make([]byte, 48)
|
||||||
|
if _, err := r.Read(i); err != nil {
|
||||||
|
return i, ID{}, err
|
||||||
|
}
|
||||||
|
p := *pos + 48
|
||||||
|
*pos = p
|
||||||
|
return i, ID(i), nil
|
||||||
|
}
|
||||||
|
func intToKind(i uint64) string {
|
||||||
|
switch Kind(i) {
|
||||||
|
case KindHTTPGet:
|
||||||
|
return "http"
|
||||||
|
case KindTar:
|
||||||
|
return "tar "
|
||||||
|
case KindExec:
|
||||||
|
return "exec"
|
||||||
|
case KindExecNet:
|
||||||
|
return "exen"
|
||||||
|
case KindFile:
|
||||||
|
return "file"
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("$%d ", i-KindCustomOffset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func headerSpacing(showHeader bool) string {
|
||||||
|
if showHeader {
|
||||||
|
return " "
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user