forked from rosa/hakurei
Compare commits
39 Commits
wip-irdump
...
pkgserver
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a38b614c6 | |||
| 3286fff076 | |||
| fd1884a84b | |||
| fe6424bd6d | |||
| 004ac511a9 | |||
| ba17f9d4f3 | |||
| ea62f64b8f | |||
| 86669363ac | |||
| 6f5b7964f4 | |||
| a195c3760c | |||
| cfe52dce82 | |||
| 8483d8a005 | |||
| 5bc5aed024 | |||
| 9465649d13 | |||
| 33c461aa67 | |||
| dee0204fc0 | |||
| 2f916ed0c0 | |||
| 55ce3a2f90 | |||
| 3f6a07ef59 | |||
| 02941e7c23 | |||
| b9601881b7 | |||
| 58596f0af5 | |||
| 02cde40289 | |||
| 5014534884 | |||
| 13cf99ced4 | |||
| 6bfb258fd0 | |||
| b649645189 | |||
| 3ddba4e21f | |||
| 40a906c6c2 | |||
| 06894e2104 | |||
| 56f0392b86 | |||
| e2315f6c1a | |||
| e4aee49eb0 | |||
| 6c03cc8b8a | |||
| 59ade6a86b | |||
| 59ab493035 | |||
| d80a3346e2 | |||
| 327a34aacb | |||
| ea7c6b3b48 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
# 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
|
||||||
|
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
176
cmd/pkgserver/api.go
Normal file
176
cmd/pkgserver/api.go
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
183
cmd/pkgserver/api_test.go
Normal file
183
cmd/pkgserver/api_test.go
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
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),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
105
cmd/pkgserver/index.go
Normal file
105
cmd/pkgserver/index.go
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
114
cmd/pkgserver/main.go
Normal file
114
cmd/pkgserver/main.go
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
96
cmd/pkgserver/main_test.go
Normal file
96
cmd/pkgserver/main_test.go
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
81
cmd/pkgserver/search.go
Normal file
81
cmd/pkgserver/search.go
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
33
cmd/pkgserver/ui.go
Normal file
33
cmd/pkgserver/ui.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
57
cmd/pkgserver/ui/index.html
Normal file
57
cmd/pkgserver/ui/index.html
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<!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>
|
||||||
331
cmd/pkgserver/ui/index.ts
Normal file
331
cmd/pkgserver/ui/index.ts
Normal file
@@ -0,0 +1,331 @@
|
|||||||
|
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()
|
||||||
|
})
|
||||||
|
}
|
||||||
BIN
cmd/pkgserver/ui/static/favicon.ico
Normal file
BIN
cmd/pkgserver/ui/static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
21
cmd/pkgserver/ui/static/style.css
Normal file
21
cmd/pkgserver/ui/static/style.css
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
8
cmd/pkgserver/ui/tsconfig.json
Normal file
8
cmd/pkgserver/ui/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2024",
|
||||||
|
"strict": true,
|
||||||
|
"alwaysStrict": true,
|
||||||
|
"outDir": "static"
|
||||||
|
}
|
||||||
|
}
|
||||||
9
cmd/pkgserver/ui_full.go
Normal file
9
cmd/pkgserver/ui_full.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
//go:build frontend
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "embed"
|
||||||
|
|
||||||
|
//go:generate tsc -p ui
|
||||||
|
//go:embed ui/*
|
||||||
|
var content embed.FS
|
||||||
7
cmd/pkgserver/ui_stub.go
Normal file
7
cmd/pkgserver/ui_stub.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
//go:build !frontend
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "testing/fstest"
|
||||||
|
|
||||||
|
var content fstest.MapFS
|
||||||
@@ -1,293 +0,0 @@
|
|||||||
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