; rel="canonical"`)
- _, _ = io.WriteString(w, "// swift-tools-version: 5.9\n")
- }))
- defer upstream.Close()
-
- proxy, _, _, _ := setupTestProxy(t)
- handler := NewSwiftHandler(proxy, "https://proxy.example", upstream.URL+"/registry").Routes()
- req := httptest.NewRequest(http.MethodGet, "/APPLE/EXAMPLE/1.2.3/Package.swift?swift-version=5.9", nil)
- req.Header.Set("Accept", swiftAcceptManifest)
- w := httptest.NewRecorder()
- handler.ServeHTTP(w, req)
-
- if w.Code != http.StatusOK {
- t.Fatalf("status = %d, want 200", w.Code)
- }
- if gotAccept != swiftAcceptManifest {
- t.Errorf("upstream Accept = %q, want %q", gotAccept, swiftAcceptManifest)
- }
- links := strings.Join(w.Header().Values("Link"), ",")
- if !strings.Contains(links, "https://proxy.example/swift/apple/example/1.2.3/Package.swift?swift-version=5.8") {
- t.Errorf("internal manifest Link was not rewritten: %q", links)
- }
- if !strings.Contains(links, "https://github.com/apple/example") {
- t.Errorf("external canonical Link was changed: %q", links)
- }
- if got := w.Header().Get("Content-Version"); got != "1" {
- t.Errorf("Content-Version = %q, want 1", got)
- }
-}
-
-func TestSwiftSourceArchiveCachesAndPreservesSecurityMetadata(t *testing.T) {
- archive := []byte("swift source archive")
- checksumBytes := sha256.Sum256(archive)
- checksum := hex.EncodeToString(checksumBytes[:])
- signature := base64.StdEncoding.EncodeToString([]byte("signature"))
-
- upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- if r.URL.Path != "/registry/apple/example/1.2.3" {
- t.Errorf("metadata path = %q", r.URL.Path)
- }
- w.Header().Set("Content-Type", "application/json")
- _, _ = fmt.Fprintf(w, `{"id":"apple.example","version":"1.2.3","resources":[{"name":"source-archive","type":"application/zip","checksum":%q,"signing":{"signatureBase64Encoded":%q,"signatureFormat":"cms-1.0.0"}}]}`, checksum, signature)
- }))
- defer upstream.Close()
-
- proxy, db, _, fetcher := setupTestProxy(t)
- fetcher.artifact = &fetch.Artifact{
- Body: io.NopCloser(strings.NewReader(string(archive))),
- Size: int64(len(archive)),
- ContentType: "application/zip",
- }
- handler := NewSwiftHandler(proxy, "https://proxy.example", upstream.URL+"/registry").Routes()
-
- requestArchive := func(method string) *httptest.ResponseRecorder {
- req := httptest.NewRequest(method, "/apple/example/1.2.3.zip", nil)
- req.Header.Set("Accept", swiftAcceptArchive)
- w := httptest.NewRecorder()
- handler.ServeHTTP(w, req)
- return w
- }
-
- w := requestArchive(http.MethodGet)
- if w.Code != http.StatusOK {
- t.Fatalf("status = %d, want 200; body: %s", w.Code, w.Body.String())
- }
- if got := w.Body.Bytes(); string(got) != string(archive) {
- t.Errorf("archive body = %q", got)
- }
- if !fetcher.fetchCalled {
- t.Fatal("archive fetcher was not called")
- }
- if got := fetcher.fetchedURL; got != upstream.URL+"/registry/apple/example/1.2.3.zip" {
- t.Errorf("fetched URL = %q", got)
- }
- if got := fetcher.fetchedHeader.Get("Accept"); got != swiftAcceptArchive {
- t.Errorf("archive Accept = %q, want %q", got, swiftAcceptArchive)
- }
- if got := w.Header().Get("Digest"); got != "sha-256="+base64.StdEncoding.EncodeToString(checksumBytes[:]) {
- t.Errorf("Digest = %q", got)
- }
- if got := w.Header().Get("X-Swift-Package-Signature"); got != signature {
- t.Errorf("signature = %q", got)
- }
- if got := w.Header().Get("X-Swift-Package-Signature-Format"); got != "cms-1.0.0" {
- t.Errorf("signature format = %q", got)
- }
- if got := w.Header().Get("Content-Disposition"); got != `attachment; filename="example-1.2.3.zip"` {
- t.Errorf("Content-Disposition = %q", got)
- }
-
- packagePURL, versionPURL := packageurl.MakeCacheStrings("swift", "apple/example", "1.2.3")
- if strings.HasPrefix(packagePURL, "pkg:swift/") {
- t.Fatalf("registry identity produced source PURL %q", packagePURL)
- }
- versionRecord, err := db.GetVersionByPURL(versionPURL)
- if err != nil {
- t.Fatalf("cached Swift version %q not found: %v", versionPURL, err)
- }
- if versionRecord == nil {
- t.Fatalf("cached Swift version %q not found", versionPURL)
- }
- if versionRecord.PackagePURL != packagePURL {
- t.Errorf("cached package PURL = %q, want %q", versionRecord.PackagePURL, packagePURL)
- }
-
- fetcher.fetchCalled = false
- w = requestArchive(http.MethodHead)
- if w.Code != http.StatusOK {
- t.Fatalf("HEAD status = %d, want 200", w.Code)
- }
- if w.Body.Len() != 0 {
- t.Errorf("HEAD body length = %d, want 0", w.Body.Len())
- }
- if got := w.Header().Get("Content-Length"); got != fmt.Sprint(len(archive)) {
- t.Errorf("HEAD Content-Length = %q", got)
- }
-
- w = requestArchive(http.MethodGet)
- if w.Code != http.StatusOK || w.Body.String() != string(archive) {
- t.Fatalf("cached response = %d %q", w.Code, w.Body.Bytes())
- }
- if fetcher.fetchCalled {
- t.Error("cached archive contacted artifact upstream")
- }
-}
-
-func TestSwiftSourceArchiveRejectsChecksumMismatch(t *testing.T) {
- archive := []byte("unexpected archive")
- expectedChecksum := sha256.Sum256([]byte("expected archive"))
-
- upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
- w.Header().Set("Content-Type", "application/json")
- _, _ = fmt.Fprintf(w, `{"id":"apple.example","version":"1.2.3","resources":[{"name":"source-archive","type":"application/zip","checksum":%q}]}`, hex.EncodeToString(expectedChecksum[:]))
- }))
- defer upstream.Close()
-
- proxy, db, store, fetcher := setupTestProxy(t)
- fetcher.artifact = &fetch.Artifact{
- Body: io.NopCloser(strings.NewReader(string(archive))),
- Size: int64(len(archive)),
- ContentType: "application/zip",
- }
- handler := NewSwiftHandler(proxy, "https://proxy.example", upstream.URL).Routes()
-
- w := httptest.NewRecorder()
- handler.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/apple/example/1.2.3.zip", nil))
-
- if w.Code != http.StatusBadGateway {
- t.Fatalf("status = %d, want 502; body: %s", w.Code, w.Body.String())
- }
- if len(store.files) != 0 {
- t.Errorf("mismatched archive remained in storage: %v", store.files)
- }
- packagePURL, versionPURL := packageurl.MakeCacheStrings("swift", "apple/example", "1.2.3")
- cached, err := db.GetCachedArtifact(packagePURL, versionPURL, "example-1.2.3.zip")
- if err != nil {
- t.Fatalf("checking cache: %v", err)
- }
- if cached != nil {
- t.Error("mismatched archive gained a cache record")
- }
-}
-
-func TestSwiftSourceArchiveCanonicalizesPackageIdentity(t *testing.T) {
- archive := []byte("swift source archive")
- checksumBytes := sha256.Sum256(archive)
- checksum := hex.EncodeToString(checksumBytes[:])
- var metadataPaths []string
-
- upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- metadataPaths = append(metadataPaths, r.URL.Path)
- w.Header().Set("Content-Type", "application/json")
- _, _ = fmt.Fprintf(w, `{"id":"apple.example","version":"1.2.3","resources":[{"name":"source-archive","type":"application/zip","checksum":%q}]}`, checksum)
- }))
- defer upstream.Close()
-
- proxy, db, store, fetcher := setupTestProxy(t)
- handler := NewSwiftHandler(proxy, "https://proxy.example", upstream.URL).Routes()
- requestArchive := func(path string) {
- fetcher.artifact = &fetch.Artifact{
- Body: io.NopCloser(strings.NewReader(string(archive))),
- Size: int64(len(archive)),
- ContentType: "application/zip",
- }
- w := httptest.NewRecorder()
- handler.ServeHTTP(w, httptest.NewRequest(http.MethodGet, path, nil))
- if w.Code != http.StatusOK {
- t.Fatalf("GET %s status = %d, want 200; body: %s", path, w.Code, w.Body.String())
- }
- }
-
- requestArchive("/apple/example/1.2.3.zip")
- requestArchive("/APPLE/EXAMPLE/1.2.3.zip")
-
- if len(store.files) != 1 {
- t.Errorf("cached files = %d, want 1", len(store.files))
- }
- for _, path := range metadataPaths {
- if path != "/apple/example/1.2.3" {
- t.Errorf("metadata path = %q, want canonical lowercase path", path)
- }
- }
-
- canonicalPURL, _ := packageurl.MakeCacheStrings("swift", "apple/example", "1.2.3")
- canonical, err := db.GetPackageByPURL(canonicalPURL)
- if err != nil {
- t.Fatalf("getting canonical package: %v", err)
- }
- if canonical == nil {
- t.Fatalf("canonical package %q not found", canonicalPURL)
- }
-
- nonCanonicalPURL, _ := packageurl.MakeCacheStrings("swift", "APPLE/EXAMPLE", "1.2.3")
- if nonCanonicalPURL != canonicalPURL {
- t.Errorf("uppercase cache PURL = %q, want %q", nonCanonicalPURL, canonicalPURL)
- }
-}
-
-func TestSwiftSourceArchiveHeadLeavesStaleCacheForTheFetch(t *testing.T) {
- archive := []byte("cached archive")
- upstreamChecksum := sha256.Sum256([]byte("upstream archive"))
-
- var probed bool
- upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- if strings.HasSuffix(r.URL.Path, ".zip") {
- probed = true
- w.Header().Set("Content-Range", "bytes 0-0/456")
- w.WriteHeader(http.StatusPartialContent)
- _, _ = w.Write([]byte("x"))
- return
- }
- w.Header().Set("Content-Type", "application/json")
- _, _ = fmt.Fprintf(w, `{"id":"apple.example","version":"1.2.3","resources":[{"name":"source-archive","type":"application/zip","checksum":%q}]}`, hex.EncodeToString(upstreamChecksum[:]))
- }))
- defer upstream.Close()
-
- proxy, db, store, fetcher := setupTestProxy(t)
- fetcher.artifact = &fetch.Artifact{
- Body: io.NopCloser(strings.NewReader(string(archive))),
- Size: int64(len(archive)),
- ContentType: "application/zip",
- }
- packagePURL, versionPURL := packageurl.MakeCacheStrings("swift", "apple/example", "1.2.3")
- cached, err := proxy.getOrFetchArtifactFromURLWithCachePURLs(
- context.Background(), "swift", "apple/example", "1.2.3", "example-1.2.3.zip",
- packagePURL, versionPURL, upstream.URL+"/apple/example/1.2.3.zip", nil, "",
- )
- if err != nil {
- t.Fatalf("seeding cache: %v", err)
- }
- _ = cached.Reader.Close()
-
- handler := NewSwiftHandler(proxy, "https://proxy.example", upstream.URL).Routes()
- w := httptest.NewRecorder()
- handler.ServeHTTP(w, httptest.NewRequest(http.MethodHead, "/apple/example/1.2.3.zip", nil))
-
- if w.Code != http.StatusOK {
- t.Fatalf("status = %d, want 200; body: %s", w.Code, w.Body.String())
- }
- if !probed {
- t.Error("stale cache entry was not replaced by an upstream probe")
- }
- if got := w.Header().Get("Content-Length"); got != "456" {
- t.Errorf("Content-Length = %q, want 456 from upstream probe", got)
- }
- // HEAD leaves the stale entry alone; the next GET replaces it under the
- // coalescing key.
- if len(store.files) != 1 {
- t.Errorf("HEAD must leave the stale archive in storage, got %v", store.files)
- }
- if rec, _ := db.GetCachedArtifact(packagePURL, versionPURL, "example-1.2.3.zip"); rec == nil {
- t.Error("HEAD must leave the stale cache record in place")
- }
-
- fetcher.artifact = artifactBody("upstream archive")
- w = httptest.NewRecorder()
- handler.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/apple/example/1.2.3.zip", nil))
- if w.Code != http.StatusOK {
- t.Fatalf("GET status = %d, want 200; body: %s", w.Code, w.Body.String())
- }
- if w.Body.String() != "upstream archive" {
- t.Errorf("GET body = %q, want the refreshed archive", w.Body.String())
- }
- rec, _ := db.GetCachedArtifact(packagePURL, versionPURL, "example-1.2.3.zip")
- if rec == nil || rec.Artifact.Digest.Encoded() != hex.EncodeToString(upstreamChecksum[:]) {
- t.Errorf("cache record after GET = %+v, want the upstream checksum", rec)
- }
-}
-
-func TestSwiftSourceArchiveRequiresReleaseMetadata(t *testing.T) {
- upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
- http.Error(w, "unavailable", http.StatusServiceUnavailable)
- }))
- defer upstream.Close()
-
- proxy, _, store, fetcher := setupTestProxy(t)
- fetcher.artifact = &fetch.Artifact{
- Body: io.NopCloser(strings.NewReader("signed archive")),
- ContentType: "application/zip",
- }
- handler := NewSwiftHandler(proxy, "https://proxy.example", upstream.URL).Routes()
-
- w := httptest.NewRecorder()
- handler.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/apple/example/1.2.3.zip", nil))
-
- if w.Code != http.StatusBadGateway {
- t.Fatalf("status = %d, want 502; body: %s", w.Code, w.Body.String())
- }
- if fetcher.fetchCalled {
- t.Error("archive was fetched without release security metadata")
- }
- if len(store.files) != 0 {
- t.Errorf("archive was cached without release security metadata: %v", store.files)
- }
-}
-
-func TestSwiftSourceArchiveColdHeadUsesRangeGetAcrossRedirect(t *testing.T) {
- checksum := strings.Repeat("a", sha256.Size*2)
- var archiveAccept string
- var archiveMethod string
- var archiveRange string
- download := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- archiveMethod = r.Method
- archiveRange = r.Header.Get("Range")
- w.Header().Set("Content-Range", "bytes 0-0/123")
- w.WriteHeader(http.StatusPartialContent)
- _, _ = w.Write([]byte("x"))
- }))
- defer download.Close()
-
- upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- switch r.URL.Path {
- case "/apple/example/1.2.3":
- w.Header().Set("Content-Type", "application/json")
- _, _ = fmt.Fprintf(w, `{"id":"apple.example","version":"1.2.3","resources":[{"name":"source-archive","type":"application/zip","checksum":%q}]}`, checksum)
- case "/apple/example/1.2.3.zip":
- archiveAccept = r.Header.Get("Accept")
- http.Redirect(w, r, download.URL, http.StatusSeeOther)
- default:
- http.NotFound(w, r)
- }
- }))
- defer upstream.Close()
-
- proxy, _, _, _ := setupTestProxy(t)
- proxy.HTTPClient = upstream.Client()
- handler := NewSwiftHandler(proxy, "https://proxy.example", upstream.URL).Routes()
-
- w := httptest.NewRecorder()
- handler.ServeHTTP(w, httptest.NewRequest(http.MethodHead, "/apple/example/1.2.3.zip", nil))
-
- if w.Code != http.StatusOK {
- t.Fatalf("status = %d, want 200; body: %s", w.Code, w.Body.String())
- }
- if archiveMethod != http.MethodGet {
- t.Errorf("download method = %q, want GET", archiveMethod)
- }
- if archiveRange != "bytes=0-0" {
- t.Errorf("download Range = %q, want bytes=0-0", archiveRange)
- }
- if archiveAccept != swiftAcceptArchive {
- t.Errorf("upstream Accept = %q, want %q", archiveAccept, swiftAcceptArchive)
- }
- if got := w.Header().Get("Content-Length"); got != "123" {
- t.Errorf("Content-Length = %q, want 123", got)
- }
-}
-
-func TestSwiftIdentifiersAndPublishingUnsupported(t *testing.T) {
- upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- if r.URL.Path != "/registry/identifiers" {
- t.Errorf("upstream path = %q", r.URL.Path)
- }
- if got := r.URL.Query().Get("url"); got != "https://github.com/apple/example" {
- t.Errorf("lookup URL = %q", got)
- }
- w.Header().Set("Content-Type", "application/json")
- _, _ = io.WriteString(w, `{"identifiers":["apple.example"]}`)
- }))
- defer upstream.Close()
-
- proxy, _, _, _ := setupTestProxy(t)
- handler := NewSwiftHandler(proxy, "https://proxy.example", upstream.URL+"/registry").Routes()
-
- req := httptest.NewRequest(http.MethodGet, "/identifiers?url=https%3A%2F%2Fgithub.com%2Fapple%2Fexample", nil)
- w := httptest.NewRecorder()
- handler.ServeHTTP(w, req)
- if w.Code != http.StatusOK || !strings.Contains(w.Body.String(), "apple.example") {
- t.Fatalf("identifier response = %d %q", w.Code, w.Body.String())
- }
-
- req = httptest.NewRequest(http.MethodGet, "/identifiers", nil)
- w = httptest.NewRecorder()
- handler.ServeHTTP(w, req)
- if w.Code != http.StatusBadRequest {
- t.Errorf("missing URL status = %d, want 400", w.Code)
- }
-
- req = httptest.NewRequest(http.MethodPut, "/apple/example/1.2.3", strings.NewReader("ignored"))
- w = httptest.NewRecorder()
- handler.ServeHTTP(w, req)
- if w.Code != http.StatusMethodNotAllowed {
- t.Errorf("publish status = %d, want 405", w.Code)
- }
- if got := w.Header().Get("Allow"); got != "GET, HEAD" {
- t.Errorf("Allow = %q", got)
- }
-}
-
-func TestSwiftIdentifierValidation(t *testing.T) {
- tests := []struct {
- name string
- value string
- valid func(string) bool
- want bool
- }{
- {"scope", "apple", validSwiftScope, true},
- {"scope hyphen", "swift-server", validSwiftScope, true},
- {"scope underscore", "swift_server", validSwiftScope, false},
- {"scope repeated separator", "swift--server", validSwiftScope, false},
- {"package", "swift-argument_parser", validSwiftPackageName, true},
- {"package repeated separators", "swift-_argument", validSwiftPackageName, false},
- {"package trailing separator", "example-", validSwiftPackageName, false},
- {"package non-ASCII", "café", validSwiftPackageName, false},
- }
-
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- if got := test.valid(test.value); got != test.want {
- t.Errorf("validation of %q = %v, want %v", test.value, got, test.want)
- }
- })
- }
-}
diff --git a/internal/handler/upstream_test.go b/internal/handler/upstream_test.go
deleted file mode 100644
index 7839eaa..0000000
--- a/internal/handler/upstream_test.go
+++ /dev/null
@@ -1,130 +0,0 @@
-package handler
-
-import (
- "io"
- "net/http"
- "net/http/httptest"
- "testing"
-)
-
-func TestHandlerUpstreamConfiguration(t *testing.T) {
- const (
- proxyURL = "https://proxy.example.com/"
- baseURL = "https://upstream.example.com"
- )
- hex := NewHexHandlerWithUpstreams(nil, proxyURL, baseURL+"/hex/", baseURL+"/hex-api/")
- pypi := NewPyPIHandlerWithUpstreams(nil, proxyURL, baseURL+"/pypi/", baseURL+"/pypi-download/")
- nuget := NewNuGetHandlerWithUpstreams(nil, proxyURL, baseURL+"/nuget/", baseURL+"/nuget-search/")
- composer := NewComposerHandlerWithUpstreams(
- nil, proxyURL, baseURL+"/composer/", baseURL+"/composer-repository/",
- )
-
- got := map[string]string{
- "gem": NewGemHandlerWithUpstream(nil, proxyURL, baseURL+"/gem/").upstreamURL,
- "go": NewGoHandlerWithUpstream(nil, proxyURL, baseURL+"/go/").upstreamURL,
- "hex": hex.upstreamURL,
- "hex_api": hex.apiURL,
- "pub": NewPubHandlerWithUpstream(nil, proxyURL, baseURL+"/pub/").upstreamURL,
- "pypi": pypi.upstreamURL,
- "pypi_download": pypi.downloadURL,
- "nuget": nuget.upstreamURL,
- "nuget_search": nuget.searchURL,
- "composer": composer.upstreamURL,
- "composer_repository": composer.repoURL,
- "conan": NewConanHandlerWithUpstream(nil, proxyURL, baseURL+"/conan/").upstreamURL,
- "conda": NewCondaHandlerWithUpstream(nil, proxyURL, baseURL+"/conda/").upstreamURL,
- "cran": NewCRANHandlerWithUpstream(nil, proxyURL, baseURL+"/cran/").upstreamURL,
- "julia": NewJuliaHandlerWithUpstream(nil, baseURL+"/julia/").upstreamURL,
- "oci_default": NewContainerHandlerWithRegistry(nil, proxyURL, baseURL+"/oci/").registryURL,
- "rpm": NewRPMHandlerWithUpstream(nil, proxyURL, baseURL+"/rpm/").upstreamURL,
- }
-
- want := map[string]string{
- "gem": baseURL + "/gem",
- "go": baseURL + "/go",
- "hex": baseURL + "/hex",
- "hex_api": baseURL + "/hex-api",
- "pub": baseURL + "/pub",
- "pypi": baseURL + "/pypi",
- "pypi_download": baseURL + "/pypi-download",
- "nuget": baseURL + "/nuget",
- "nuget_search": baseURL + "/nuget-search",
- "composer": baseURL + "/composer",
- "composer_repository": baseURL + "/composer-repository",
- "conan": baseURL + "/conan",
- "conda": baseURL + "/conda",
- "cran": baseURL + "/cran",
- "julia": baseURL + "/julia",
- "oci_default": baseURL + "/oci",
- "rpm": baseURL + "/rpm",
- }
-
- for name, wantURL := range want {
- if gotURL := got[name]; gotURL != wantURL {
- t.Errorf("%s upstream = %q, want %q", name, gotURL, wantURL)
- }
- }
-}
-
-func TestConfiguredUpstreamURL(t *testing.T) {
- if got := configuredUpstreamURL("", "https://default.example.com/"); got != "https://default.example.com" {
- t.Errorf("empty configured URL = %q, want default", got)
- }
- if got := configuredUpstreamURL("https://custom.example.com/", "https://default.example.com"); got != "https://custom.example.com" {
- t.Errorf("configured URL = %q, want trimmed custom URL", got)
- }
- if got := configuredUpstreamURL("https://custom.example.com///", "https://default.example.com"); got != "https://custom.example.com" {
- t.Errorf("configured URL with trailing slashes = %q, want trimmed custom URL", got)
- }
-}
-
-func TestHexHandlerUsesConfiguredAPIUpstream(t *testing.T) {
- var requestedPath string
- upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- requestedPath = r.URL.Path
- _, _ = io.WriteString(w, `{"releases":[]}`)
- }))
- defer upstream.Close()
-
- h := NewHexHandlerWithUpstreams(
- &Proxy{HTTPClient: upstream.Client()},
- "https://proxy.example.com",
- upstream.URL+"/hex",
- upstream.URL+"/hex-api",
- )
- _, err := h.fetchFilteredVersions(httptest.NewRequest(http.MethodGet, "/", nil), "demo")
- if err != nil {
- t.Fatalf("fetchFilteredVersions failed: %v", err)
- }
- if requestedPath != "/hex-api/api/packages/demo" {
- t.Errorf("API path = %q, want %q", requestedPath, "/hex-api/api/packages/demo")
- }
-}
-
-func TestPyPIHandlerRewritesConfiguredDownloadUpstream(t *testing.T) {
- h := NewPyPIHandlerWithUpstreams(
- nil,
- "https://proxy.example.com",
- "https://upstream.example.com/pypi",
- "https://upstream.example.com/pypi",
- )
- body := []byte(`demo `)
- want := `demo `
- if got := string(h.rewriteSimpleHTML(body, nil)); got != want {
- t.Errorf("rewritten HTML = %q, want %q", got, want)
- }
-}
-
-func TestNuGetHandlerUsesConfiguredSearchUpstream(t *testing.T) {
- h := NewNuGetHandlerWithUpstreams(
- nil,
- "https://proxy.example.com",
- "https://upstream.example.com/nuget",
- "https://upstream.example.com/nuget-search",
- )
- req := httptest.NewRequest(http.MethodGet, "/query?q=demo", nil)
- want := "https://upstream.example.com/nuget-search/query?q=demo"
- if got := h.buildUpstreamURL(req); got != want {
- t.Errorf("search URL = %q, want %q", got, want)
- }
-}
diff --git a/internal/httpclient/access_log.go b/internal/httpclient/access_log.go
deleted file mode 100644
index 8e13f23..0000000
--- a/internal/httpclient/access_log.go
+++ /dev/null
@@ -1,74 +0,0 @@
-package httpclient
-
-import (
- "log/slog"
- "net/http"
- "net/url"
- "strings"
- "time"
-
- "github.com/git-pkgs/proxy/internal/accesslog"
-)
-
-type accessLogTransport struct {
- base http.RoundTripper
- accessLog *accesslog.Logger
- logger *slog.Logger
-}
-
-// NewAccessLogTransport records each upstream HTTP exchange around base.
-func NewAccessLogTransport(base http.RoundTripper, log *accesslog.Logger, logger *slog.Logger) http.RoundTripper {
- if base == nil {
- base = http.DefaultTransport
- }
- if logger == nil {
- logger = slog.Default()
- }
- if log == nil {
- return base
- }
- return &accessLogTransport{
- base: base,
- accessLog: log,
- logger: logger,
- }
-}
-
-func (t *accessLogTransport) RoundTrip(req *http.Request) (*http.Response, error) {
- start := time.Now()
- resp, err := t.base.RoundTrip(req)
-
- entry := accesslog.Entry{
- Event: accesslog.EventUpstream,
- RequestID: accesslog.RequestID(req.Context()),
- Method: req.Method,
- URL: accesslog.URLWithoutSecrets(req.URL),
- DurationMS: time.Since(start).Milliseconds(),
- }
- if resp != nil {
- entry.StatusCode = resp.StatusCode
- }
- if err != nil {
- entry.Error = errorWithoutSecrets(err, req.URL)
- }
- if writeErr := t.accessLog.Write(entry); writeErr != nil {
- t.logger.Error("failed to write access log", "error", writeErr)
- }
-
- return resp, err
-}
-
-func errorWithoutSecrets(err error, requestURL *url.URL) string {
- message := err.Error()
- if requestURL == nil {
- return message
- }
-
- cleanURL := accesslog.URLWithoutSecrets(requestURL)
- for _, value := range []string{requestURL.String(), requestURL.Redacted()} {
- if value != "" {
- message = strings.ReplaceAll(message, value, cleanURL)
- }
- }
- return message
-}
diff --git a/internal/httpclient/access_log_test.go b/internal/httpclient/access_log_test.go
deleted file mode 100644
index aa3a43c..0000000
--- a/internal/httpclient/access_log_test.go
+++ /dev/null
@@ -1,121 +0,0 @@
-package httpclient
-
-import (
- "bufio"
- "encoding/json"
- "errors"
- "io"
- "log/slog"
- "net/http"
- "os"
- "path/filepath"
- "strings"
- "testing"
-
- "github.com/git-pkgs/proxy/internal/accesslog"
-)
-
-type roundTripFunc func(*http.Request) (*http.Response, error)
-
-func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
- return f(req)
-}
-
-func TestAccessLogTransportRecordsUpstreamStatus(t *testing.T) {
- path := filepath.Join(t.TempDir(), "access.jsonl")
- accessLogger, err := accesslog.Open(path)
- if err != nil {
- t.Fatal(err)
- }
-
- base := roundTripFunc(func(req *http.Request) (*http.Response, error) {
- return &http.Response{
- StatusCode: http.StatusTooManyRequests,
- Body: io.NopCloser(strings.NewReader("rate limited")),
- Request: req,
- }, nil
- })
- client := &http.Client{Transport: NewAccessLogTransport(base, accessLogger, slog.Default())}
- req, err := http.NewRequest(http.MethodGet, "https://user:password@registry.example/package.tgz?token=secret", nil)
- if err != nil {
- t.Fatal(err)
- }
- req = req.WithContext(accesslog.WithRequestID(req.Context(), "request-123"))
-
- resp, err := client.Do(req)
- if err != nil {
- t.Fatal(err)
- }
- _ = resp.Body.Close()
- if err := accessLogger.Close(); err != nil {
- t.Fatal(err)
- }
-
- entry := readAccessLogEntry(t, path)
- if entry.Event != accesslog.EventUpstream {
- t.Errorf("event = %q, want %q", entry.Event, accesslog.EventUpstream)
- }
- if entry.RequestID != "request-123" {
- t.Errorf("request_id = %q, want %q", entry.RequestID, "request-123")
- }
- if entry.StatusCode != http.StatusTooManyRequests {
- t.Errorf("status_code = %d, want %d", entry.StatusCode, http.StatusTooManyRequests)
- }
- if entry.URL != "https://registry.example/package.tgz" {
- t.Errorf("url = %q, want URL without credentials or query", entry.URL)
- }
-}
-
-func TestAccessLogTransportRecordsUpstreamError(t *testing.T) {
- path := filepath.Join(t.TempDir(), "access.jsonl")
- accessLogger, err := accesslog.Open(path)
- if err != nil {
- t.Fatal(err)
- }
-
- wantErr := errors.New("GET https://user:password@registry.example/package.tgz?token=secret: connection refused")
- base := roundTripFunc(func(*http.Request) (*http.Response, error) {
- return nil, wantErr
- })
- client := &http.Client{Transport: NewAccessLogTransport(base, accessLogger, slog.Default())}
-
- _, err = client.Get("https://user:password@registry.example/package.tgz?token=secret")
- if !errors.Is(err, wantErr) {
- t.Fatalf("GET error = %v, want %v", err, wantErr)
- }
- if err := accessLogger.Close(); err != nil {
- t.Fatal(err)
- }
-
- entry := readAccessLogEntry(t, path)
- if entry.StatusCode != 0 {
- t.Errorf("status_code = %d, want 0", entry.StatusCode)
- }
- if strings.Contains(entry.Error, "password") || strings.Contains(entry.Error, "secret") {
- t.Errorf("error contains URL credentials or query: %q", entry.Error)
- }
- if !strings.Contains(entry.Error, "connection refused") {
- t.Errorf("error = %q, want connection failure", entry.Error)
- }
-}
-
-func readAccessLogEntry(t *testing.T, path string) accesslog.Entry {
- t.Helper()
-
- file, err := os.Open(path)
- if err != nil {
- t.Fatal(err)
- }
- defer func() { _ = file.Close() }()
-
- scanner := bufio.NewScanner(file)
- if !scanner.Scan() {
- t.Fatalf("access log is empty: %v", scanner.Err())
- }
-
- var entry accesslog.Entry
- if err := json.Unmarshal(scanner.Bytes(), &entry); err != nil {
- t.Fatalf("decoding access log: %v", err)
- }
- return entry
-}
diff --git a/internal/httpclient/transport.go b/internal/httpclient/transport.go
deleted file mode 100644
index 1ee9597..0000000
--- a/internal/httpclient/transport.go
+++ /dev/null
@@ -1,507 +0,0 @@
-// Package httpclient provides authentication-aware HTTP transports for upstream requests.
-package httpclient
-
-import (
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "net"
- "net/http"
- "net/url"
- "strings"
- "sync"
- "time"
-)
-
-const (
- defaultTokenLifetime = 60 * time.Second
- tokenExpirySkew = 5 * time.Second
- maxTokenResponseSize = 1 << 20
- shortTokenSkewDivisor = 10
- tokenMaxRetries = 3
- tokenRetryBaseDelay = 500 * time.Millisecond
-)
-
-// AuthFunc returns a configured authentication header for a URL.
-type AuthFunc func(url string) (headerName, headerValue string)
-
-// Transport adds configured authentication and follows OCI Bearer challenges.
-type Transport struct {
- base http.RoundTripper
- authForURL AuthFunc
- retryWait func(context.Context, time.Duration) error
-
- mu sync.Mutex
- tokens map[string]cachedToken
- challenges map[string]bearerChallenge
-}
-
-type cachedToken struct {
- value string
- expiresAt time.Time
-}
-
-type bearerChallenge struct {
- realm string
- service string
- scopes []string
-}
-
-type tokenResponse struct {
- Token string `json:"token"`
- AccessToken string `json:"access_token"`
- ExpiresIn int64 `json:"expires_in"`
- IssuedAt string `json:"issued_at"`
-}
-
-// NewTransport creates an authentication-aware transport around base.
-func NewTransport(base http.RoundTripper, authForURL AuthFunc) *Transport {
- if base == nil {
- base = http.DefaultTransport
- }
- return &Transport{
- base: base,
- authForURL: authForURL,
- retryWait: waitForRetry,
- tokens: make(map[string]cachedToken),
- challenges: make(map[string]bearerChallenge),
- }
-}
-
-// RoundTrip implements http.RoundTripper.
-func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
- hasExplicitAuthorization := req.Header.Get("Authorization") != ""
- outbound := cloneRequest(req)
- t.applyAuthentication(outbound, hasExplicitAuthorization)
-
- resp, err := t.base.RoundTrip(outbound)
- if err != nil || resp.StatusCode != http.StatusUnauthorized {
- return resp, err
- }
- if hasExplicitAuthorization {
- return resp, nil
- }
- if registryProtectionSpace(req.URL) == "" {
- return resp, nil
- }
-
- challenge, ok := parseBearerChallenge(resp.Header.Values("WWW-Authenticate"))
- if !ok || !canReplay(req) {
- return resp, nil
- }
-
- drainAndClose(resp.Body)
- token, err := t.token(req.Context(), challenge)
- if err != nil {
- return nil, fmt.Errorf("registry authentication: %w", err)
- }
- t.rememberChallenge(req.URL, challenge)
-
- retry, err := cloneRequestForRetry(req)
- if err != nil {
- return nil, err
- }
- t.applyConfiguredAuthentication(retry)
- retry.Header.Set("Authorization", "Bearer "+token)
- return t.base.RoundTrip(retry)
-}
-
-func (t *Transport) applyAuthentication(req *http.Request, hasExplicitAuthorization bool) {
- t.applyConfiguredAuthentication(req)
- if hasExplicitAuthorization {
- return
- }
- if token := t.cachedTokenForRequest(req.URL); token != "" {
- req.Header.Set("Authorization", "Bearer "+token)
- }
-}
-
-func (t *Transport) applyConfiguredAuthentication(req *http.Request) {
- if t.authForURL == nil {
- return
- }
- name, value := t.authForURL(req.URL.String())
- if name != "" && value != "" && req.Header.Get(name) == "" {
- req.Header.Set(name, value)
- }
-}
-
-func (t *Transport) token(ctx context.Context, challenge bearerChallenge) (string, error) {
- key := challenge.key()
- if token := t.cachedToken(key); token != "" {
- return token, nil
- }
-
- token, expiresAt, err := t.fetchToken(ctx, challenge)
- if err != nil {
- return "", err
- }
-
- t.cacheToken(key, cachedToken{value: token, expiresAt: expiresAt})
- return token, nil
-}
-
-func (t *Transport) cacheToken(key string, token cachedToken) {
- now := time.Now()
- t.mu.Lock()
- defer t.mu.Unlock()
-
- for cachedKey, cached := range t.tokens {
- if !now.Before(cached.expiresAt) {
- delete(t.tokens, cachedKey)
- }
- }
- t.tokens[key] = token
-}
-
-func (t *Transport) fetchToken(ctx context.Context, challenge bearerChallenge) (string, time.Time, error) {
- tokenURL, err := url.Parse(challenge.realm)
- if err != nil || !tokenURL.IsAbs() || (tokenURL.Scheme != "https" && tokenURL.Scheme != "http") {
- return "", time.Time{}, fmt.Errorf("invalid token realm %q", challenge.realm)
- }
-
- query := tokenURL.Query()
- if challenge.service != "" {
- query.Set("service", challenge.service)
- }
- for _, scope := range challenge.scopes {
- query.Add("scope", scope)
- }
- query.Set("client_id", "git-pkgs-proxy")
- tokenURL.RawQuery = query.Encode()
-
- req, err := http.NewRequestWithContext(ctx, http.MethodGet, tokenURL.String(), nil)
- if err != nil {
- return "", time.Time{}, err
- }
-
- client := &http.Client{Transport: configuredTransport{parent: t}}
- for attempt := 0; attempt <= tokenMaxRetries; attempt++ {
- resp, err := client.Do(req.Clone(ctx))
- if err != nil {
- requestErr := fmt.Errorf("requesting token: %w", err)
- if !shouldRetryTokenRequest(ctx, err) || attempt == tokenMaxRetries {
- return "", time.Time{}, requestErr
- }
- if err := t.waitForTokenRetry(ctx, attempt); err != nil {
- return "", time.Time{}, err
- }
- continue
- }
-
- if resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices {
- return decodeTokenResponse(resp)
- }
-
- responseErr := tokenResponseError(resp)
- if !shouldRetryTokenStatus(resp.StatusCode) || attempt == tokenMaxRetries {
- return "", time.Time{}, responseErr
- }
- if err := t.waitForTokenRetry(ctx, attempt); err != nil {
- return "", time.Time{}, err
- }
- }
-
- return "", time.Time{}, errors.New("token request retries exhausted")
-}
-
-func decodeTokenResponse(resp *http.Response) (string, time.Time, error) {
- defer func() { _ = resp.Body.Close() }()
-
- var payload tokenResponse
- if err := json.NewDecoder(io.LimitReader(resp.Body, maxTokenResponseSize)).Decode(&payload); err != nil {
- return "", time.Time{}, fmt.Errorf("decoding token response: %w", err)
- }
- token := payload.Token
- if token == "" {
- token = payload.AccessToken
- }
- if token == "" {
- return "", time.Time{}, fmt.Errorf("token response did not contain a token")
- }
-
- issuedAt := time.Now()
- if payload.IssuedAt != "" {
- if parsed, parseErr := time.Parse(time.RFC3339, payload.IssuedAt); parseErr == nil {
- issuedAt = parsed
- }
- }
- lifetime := time.Duration(payload.ExpiresIn) * time.Second
- if lifetime <= 0 {
- lifetime = defaultTokenLifetime
- }
- expiresAt := issuedAt.Add(lifetime).Add(-expirySkew(lifetime))
- return token, expiresAt, nil
-}
-
-func tokenResponseError(resp *http.Response) error {
- defer func() { _ = resp.Body.Close() }()
- body, _ := io.ReadAll(io.LimitReader(resp.Body, maxTokenResponseSize))
- return fmt.Errorf("token service returned %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
-}
-
-func shouldRetryTokenRequest(ctx context.Context, err error) bool {
- if ctx.Err() != nil || errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
- return false
- }
-
- var networkErr net.Error
- if !errors.As(err, &networkErr) {
- return false
- }
-
- var dnsErr *net.DNSError
- if errors.As(err, &dnsErr) {
- return dnsErr.IsTemporary || dnsErr.IsTimeout
- }
- return networkErr.Timeout()
-}
-
-func shouldRetryTokenStatus(status int) bool {
- return status == http.StatusTooManyRequests || status >= http.StatusInternalServerError
-}
-
-func (t *Transport) waitForTokenRetry(ctx context.Context, attempt int) error {
- delay := tokenRetryBaseDelay << attempt
- if t.retryWait != nil {
- return t.retryWait(ctx, delay)
- }
- return waitForRetry(ctx, delay)
-}
-
-func waitForRetry(ctx context.Context, delay time.Duration) error {
- timer := time.NewTimer(delay)
- defer timer.Stop()
-
- select {
- case <-ctx.Done():
- return ctx.Err()
- case <-timer.C:
- return nil
- }
-}
-
-type configuredTransport struct {
- parent *Transport
-}
-
-func (t configuredTransport) RoundTrip(req *http.Request) (*http.Response, error) {
- outbound := cloneRequest(req)
- t.parent.applyConfiguredAuthentication(outbound)
- return t.parent.base.RoundTrip(outbound)
-}
-
-func (t *Transport) cachedTokenForRequest(requestURL *url.URL) string {
- space := registryProtectionSpace(requestURL)
- if space == "" {
- return ""
- }
-
- t.mu.Lock()
- challenge, ok := t.challenges[space]
- t.mu.Unlock()
- if !ok {
- return ""
- }
- return t.cachedToken(challenge.key())
-}
-
-func (t *Transport) cachedToken(key string) string {
- now := time.Now()
- t.mu.Lock()
- defer t.mu.Unlock()
-
- token, ok := t.tokens[key]
- if !ok {
- return ""
- }
- if !now.Before(token.expiresAt) {
- delete(t.tokens, key)
- return ""
- }
- return token.value
-}
-
-func (t *Transport) rememberChallenge(requestURL *url.URL, challenge bearerChallenge) {
- space := registryProtectionSpace(requestURL)
- if space == "" {
- return
- }
- t.mu.Lock()
- t.challenges[space] = challenge
- t.mu.Unlock()
-}
-
-func (c bearerChallenge) key() string {
- return c.realm + "\x00" + c.service + "\x00" + strings.Join(c.scopes, "\x00")
-}
-
-func registryProtectionSpace(u *url.URL) string {
- const registryPrefix = "/v2/"
- if u == nil || !strings.HasPrefix(u.Path, registryPrefix) {
- return ""
- }
- rest := strings.TrimPrefix(u.Path, registryPrefix)
- end := len(rest)
- for _, marker := range []string{"/blobs/", "/manifests/", "/tags/", "/referrers/"} {
- if index := strings.Index(rest, marker); index >= 0 && index < end {
- end = index
- }
- }
- if end == len(rest) || end == 0 {
- return ""
- }
- return u.Scheme + "://" + u.Host + registryPrefix + rest[:end]
-}
-
-func parseBearerChallenge(values []string) (bearerChallenge, bool) {
- for _, value := range values {
- params, ok := bearerParameters(value)
- if !ok || params["realm"] == "" {
- continue
- }
- challenge := bearerChallenge{
- realm: params["realm"],
- service: params["service"],
- }
- if scope := params["scope"]; scope != "" {
- challenge.scopes = append(challenge.scopes, scope)
- }
- return challenge, true
- }
- return bearerChallenge{}, false
-}
-
-func bearerParameters(value string) (map[string]string, bool) {
- start := findAuthScheme(value, "Bearer")
- if start < 0 {
- return nil, false
- }
- rest := value[start+len("Bearer"):]
- params := make(map[string]string)
- for {
- rest = strings.TrimLeft(rest, " \t,")
- if rest == "" {
- break
- }
-
- keyEnd := strings.IndexAny(rest, "= \t,")
- if keyEnd <= 0 {
- break
- }
- key := strings.ToLower(rest[:keyEnd])
- rest = strings.TrimLeft(rest[keyEnd:], " \t")
- if rest == "" || rest[0] != '=' {
- break
- }
- rest = strings.TrimLeft(rest[1:], " \t")
-
- parsed, remaining, ok := parseAuthValue(rest)
- if !ok {
- return nil, false
- }
- params[key] = parsed
- rest = remaining
- }
- return params, true
-}
-
-func findAuthScheme(value, scheme string) int {
- inQuote := false
- escaped := false
- for index := 0; index+len(scheme) <= len(value); index++ {
- char := value[index]
- if escaped {
- escaped = false
- continue
- }
- if char == '\\' && inQuote {
- escaped = true
- continue
- }
- if char == '"' {
- inQuote = !inQuote
- continue
- }
- if inQuote || !strings.EqualFold(value[index:index+len(scheme)], scheme) {
- continue
- }
- beforeOK := index == 0 || value[index-1] == ',' || value[index-1] == ' ' || value[index-1] == '\t'
- after := index + len(scheme)
- afterOK := after < len(value) && (value[after] == ' ' || value[after] == '\t')
- if beforeOK && afterOK {
- return index
- }
- }
- return -1
-}
-
-func parseAuthValue(value string) (parsed, remaining string, ok bool) {
- if value == "" {
- return "", "", false
- }
- if value[0] != '"' {
- end := strings.IndexAny(value, " \t,")
- if end < 0 {
- return value, "", true
- }
- return value[:end], value[end:], end > 0
- }
-
- var builder strings.Builder
- escaped := false
- for index := 1; index < len(value); index++ {
- char := value[index]
- if escaped {
- builder.WriteByte(char)
- escaped = false
- continue
- }
- if char == '\\' {
- escaped = true
- continue
- }
- if char == '"' {
- return builder.String(), value[index+1:], true
- }
- builder.WriteByte(char)
- }
- return "", "", false
-}
-
-func cloneRequest(req *http.Request) *http.Request {
- clone := req.Clone(req.Context())
- clone.Header = req.Header.Clone()
- return clone
-}
-
-func canReplay(req *http.Request) bool {
- return req.Body == nil || req.GetBody != nil
-}
-
-func cloneRequestForRetry(req *http.Request) (*http.Request, error) {
- clone := cloneRequest(req)
- if req.Body == nil {
- return clone, nil
- }
- body, err := req.GetBody()
- if err != nil {
- return nil, fmt.Errorf("replaying authenticated request: %w", err)
- }
- clone.Body = body
- return clone, nil
-}
-
-func expirySkew(lifetime time.Duration) time.Duration {
- if lifetime < tokenExpirySkew*2 {
- return lifetime / shortTokenSkewDivisor
- }
- return tokenExpirySkew
-}
-
-func drainAndClose(body io.ReadCloser) {
- _, _ = io.Copy(io.Discard, io.LimitReader(body, maxTokenResponseSize))
- _ = body.Close()
-}
diff --git a/internal/httpclient/transport_test.go b/internal/httpclient/transport_test.go
deleted file mode 100644
index 01f3e8b..0000000
--- a/internal/httpclient/transport_test.go
+++ /dev/null
@@ -1,414 +0,0 @@
-package httpclient
-
-import (
- "context"
- "errors"
- "io"
- "net"
- "net/http"
- "net/http/httptest"
- "strings"
- "testing"
- "time"
-)
-
-type roundTripperFunc func(*http.Request) (*http.Response, error)
-
-func (fn roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) {
- return fn(req)
-}
-
-func TestTransportFollowsBearerChallengeAndCachesToken(t *testing.T) {
- var registryRequests int
- var tokenRequests int
- var server *httptest.Server
-
- server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- switch r.URL.Path {
- case "/token":
- tokenRequests++
- if got := r.URL.Query().Get("service"); got != "registry.test" {
- t.Errorf("service = %q, want %q", got, "registry.test")
- }
- if got := r.URL.Query().Get("scope"); got != "repository:library/test:pull" {
- t.Errorf("scope = %q, want %q", got, "repository:library/test:pull")
- }
- w.Header().Set("Content-Type", "application/json")
- _, _ = io.WriteString(w, `{"token":"registry-token","expires_in":3600}`)
- case "/v2/library/test/blobs/sha256:first", "/v2/library/test/blobs/sha256:second":
- registryRequests++
- if r.Header.Get("Authorization") != "Bearer registry-token" {
- w.Header().Set("WWW-Authenticate", `Bearer realm="`+server.URL+`/token",service="registry.test",scope="repository:library/test:pull"`)
- http.Error(w, "authentication required", http.StatusUnauthorized)
- return
- }
- _, _ = io.WriteString(w, "blob")
- default:
- http.NotFound(w, r)
- }
- }))
- defer server.Close()
-
- client := &http.Client{Transport: NewTransport(http.DefaultTransport, nil)}
- for _, digest := range []string{"sha256:first", "sha256:second"} {
- resp, err := client.Get(server.URL + "/v2/library/test/blobs/" + digest)
- if err != nil {
- t.Fatalf("GET %s: %v", digest, err)
- }
- body, readErr := io.ReadAll(resp.Body)
- _ = resp.Body.Close()
- if readErr != nil {
- t.Fatalf("read %s response: %v", digest, readErr)
- }
- if resp.StatusCode != http.StatusOK {
- t.Fatalf("GET %s status = %d, want %d", digest, resp.StatusCode, http.StatusOK)
- }
- if string(body) != "blob" {
- t.Errorf("GET %s body = %q, want %q", digest, body, "blob")
- }
- }
-
- if tokenRequests != 1 {
- t.Errorf("token requests = %d, want 1", tokenRequests)
- }
- if registryRequests != 3 {
- t.Errorf("registry requests = %d, want 3", registryRequests)
- }
-}
-
-func TestTransportRetriesTemporaryTokenLookupFailures(t *testing.T) {
- var registryRequests int
- var tokenRequests int
- var tokenLookupFailures int
- var server *httptest.Server
-
- server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- switch r.URL.Path {
- case "/token":
- tokenRequests++
- _, _ = io.WriteString(w, `{"token":"registry-token"}`)
- case "/v2/library/test/blobs/sha256:test":
- registryRequests++
- if r.Header.Get("Authorization") != "Bearer registry-token" {
- w.Header().Set("WWW-Authenticate", `Bearer realm="`+server.URL+`/token",service="registry.test",scope="repository:library/test:pull"`)
- http.Error(w, "authentication required", http.StatusUnauthorized)
- return
- }
- _, _ = io.WriteString(w, "blob")
- default:
- http.NotFound(w, r)
- }
- }))
- defer server.Close()
-
- base := roundTripperFunc(func(req *http.Request) (*http.Response, error) {
- if req.URL.Path == "/token" && tokenLookupFailures < 2 {
- tokenLookupFailures++
- return nil, &net.DNSError{Err: "server misbehaving", IsTemporary: true}
- }
- return http.DefaultTransport.RoundTrip(req)
- })
- transport := NewTransport(base, nil)
- transport.retryWait = func(context.Context, time.Duration) error { return nil }
- client := &http.Client{Transport: transport}
-
- resp, err := client.Get(server.URL + "/v2/library/test/blobs/sha256:test")
- if err != nil {
- t.Fatalf("GET blob: %v", err)
- }
- defer func() { _ = resp.Body.Close() }()
-
- if resp.StatusCode != http.StatusOK {
- t.Errorf("status = %d, want %d", resp.StatusCode, http.StatusOK)
- }
- if tokenLookupFailures != 2 {
- t.Errorf("token lookup failures = %d, want 2", tokenLookupFailures)
- }
- if tokenRequests != 1 {
- t.Errorf("token requests = %d, want 1", tokenRequests)
- }
- if registryRequests != 2 {
- t.Errorf("registry requests = %d, want 2", registryRequests)
- }
-}
-
-func TestTransportDoesNotRetryPermanentTokenLookupFailures(t *testing.T) {
- var tokenRequests int
- base := roundTripperFunc(func(*http.Request) (*http.Response, error) {
- tokenRequests++
- return nil, &net.DNSError{Err: "no such host"}
- })
- transport := NewTransport(base, nil)
- transport.retryWait = func(context.Context, time.Duration) error { return nil }
-
- _, _, err := transport.fetchToken(context.Background(), bearerChallenge{realm: "https://auth.example.test/token"})
- if err == nil {
- t.Fatal("fetchToken succeeded, want error")
- }
- if tokenRequests != 1 {
- t.Errorf("token requests = %d, want 1", tokenRequests)
- }
-}
-
-func TestTransportDoesNotRetryPermanentTokenFailures(t *testing.T) {
- var tokenRequests int
- server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- tokenRequests++
- http.Error(w, "invalid credentials", http.StatusUnauthorized)
- }))
- defer server.Close()
-
- transport := NewTransport(http.DefaultTransport, nil)
- _, _, err := transport.fetchToken(context.Background(), bearerChallenge{realm: server.URL + "/token"})
- if err == nil {
- t.Fatal("fetchToken succeeded, want error")
- }
- if tokenRequests != 1 {
- t.Errorf("token requests = %d, want 1", tokenRequests)
- }
-}
-
-func TestTransportRetriesTokenServiceFailures(t *testing.T) {
- for _, status := range []int{http.StatusTooManyRequests, http.StatusServiceUnavailable} {
- t.Run(http.StatusText(status), func(t *testing.T) {
- var tokenRequests int
- server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- tokenRequests++
- http.Error(w, "temporary token service failure", status)
- }))
- defer server.Close()
-
- var delays []time.Duration
- transport := NewTransport(http.DefaultTransport, nil)
- transport.retryWait = func(_ context.Context, delay time.Duration) error {
- delays = append(delays, delay)
- return nil
- }
-
- _, _, err := transport.fetchToken(context.Background(), bearerChallenge{realm: server.URL + "/token"})
- if err == nil {
- t.Fatal("fetchToken succeeded, want error")
- }
- if tokenRequests != tokenMaxRetries+1 {
- t.Errorf("token requests = %d, want %d", tokenRequests, tokenMaxRetries+1)
- }
- wantDelays := []time.Duration{500 * time.Millisecond, time.Second, 2 * time.Second}
- if len(delays) != len(wantDelays) {
- t.Fatalf("retry delays = %v, want %v", delays, wantDelays)
- }
- for index, want := range wantDelays {
- if delays[index] != want {
- t.Errorf("retry delay %d = %s, want %s", index, delays[index], want)
- }
- }
- })
- }
-}
-
-func TestTransportStopsTokenRetriesWhenWaitingIsCancelled(t *testing.T) {
- var tokenRequests int
- server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- tokenRequests++
- http.Error(w, "temporary token service failure", http.StatusServiceUnavailable)
- }))
- defer server.Close()
-
- ctx, cancel := context.WithCancel(context.Background())
- defer cancel()
- transport := NewTransport(http.DefaultTransport, nil)
- transport.retryWait = func(ctx context.Context, _ time.Duration) error {
- cancel()
- <-ctx.Done()
- return ctx.Err()
- }
-
- _, _, err := transport.fetchToken(ctx, bearerChallenge{realm: server.URL + "/token"})
- if !errors.Is(err, context.Canceled) {
- t.Errorf("fetchToken error = %v, want context canceled", err)
- }
- if tokenRequests != 1 {
- t.Errorf("token requests = %d, want 1", tokenRequests)
- }
-}
-
-func TestTransportAddsConfiguredAuthentication(t *testing.T) {
- server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- if got := r.Header.Get("X-Registry-Token"); got != "configured-token" {
- t.Errorf("X-Registry-Token = %q, want %q", got, "configured-token")
- }
- w.WriteHeader(http.StatusNoContent)
- }))
- defer server.Close()
-
- authForURL := func(url string) (string, string) {
- if strings.HasPrefix(url, server.URL) {
- return "X-Registry-Token", "configured-token"
- }
- return "", ""
- }
- client := &http.Client{Transport: NewTransport(http.DefaultTransport, authForURL)}
-
- resp, err := client.Get(server.URL + "/metadata")
- if err != nil {
- t.Fatalf("GET metadata: %v", err)
- }
- _ = resp.Body.Close()
- if resp.StatusCode != http.StatusNoContent {
- t.Errorf("status = %d, want %d", resp.StatusCode, http.StatusNoContent)
- }
-}
-
-func TestTransportPreservesExplicitAuthentication(t *testing.T) {
- server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- if got := r.Header.Get("Authorization"); got != "Bearer explicit-token" {
- t.Errorf("Authorization = %q, want %q", got, "Bearer explicit-token")
- }
- w.WriteHeader(http.StatusNoContent)
- }))
- defer server.Close()
-
- authForURL := func(string) (string, string) {
- return "Authorization", "Bearer configured-token"
- }
- client := &http.Client{Transport: NewTransport(http.DefaultTransport, authForURL)}
- req, err := http.NewRequest(http.MethodGet, server.URL+"/artifact", nil)
- if err != nil {
- t.Fatal(err)
- }
- req.Header.Set("Authorization", "Bearer explicit-token")
-
- resp, err := client.Do(req)
- if err != nil {
- t.Fatalf("GET artifact: %v", err)
- }
- _ = resp.Body.Close()
- if resp.StatusCode != http.StatusNoContent {
- t.Errorf("status = %d, want %d", resp.StatusCode, http.StatusNoContent)
- }
-}
-
-func TestTransportDoesNotReplaceExplicitAuthenticationAfterBearerChallenge(t *testing.T) {
- var registryRequests int
- var tokenRequests int
- var server *httptest.Server
-
- server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- switch r.URL.Path {
- case "/token":
- tokenRequests++
- _, _ = io.WriteString(w, `{"token":"registry-token"}`)
- case "/v2/library/test/blobs/sha256:test":
- registryRequests++
- if got := r.Header.Get("Authorization"); got != "Bearer explicit-token" {
- t.Errorf("Authorization = %q, want %q", got, "Bearer explicit-token")
- }
- w.Header().Set("WWW-Authenticate", `Bearer realm="`+server.URL+`/token"`)
- http.Error(w, "authentication required", http.StatusUnauthorized)
- default:
- http.NotFound(w, r)
- }
- }))
- defer server.Close()
-
- client := &http.Client{Transport: NewTransport(http.DefaultTransport, nil)}
- req, err := http.NewRequest(http.MethodGet, server.URL+"/v2/library/test/blobs/sha256:test", nil)
- if err != nil {
- t.Fatal(err)
- }
- req.Header.Set("Authorization", "Bearer explicit-token")
-
- resp, err := client.Do(req)
- if err != nil {
- t.Fatalf("GET blob: %v", err)
- }
- _ = resp.Body.Close()
- if resp.StatusCode != http.StatusUnauthorized {
- t.Errorf("status = %d, want %d", resp.StatusCode, http.StatusUnauthorized)
- }
- if registryRequests != 1 {
- t.Errorf("registry requests = %d, want 1", registryRequests)
- }
- if tokenRequests != 0 {
- t.Errorf("token requests = %d, want 0", tokenRequests)
- }
-}
-
-func TestTransportDoesNotForwardConfiguredAuthenticationOnTokenRedirect(t *testing.T) {
- destination := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- if got := r.Header.Get("X-Registry-Token"); got != "" {
- t.Errorf("redirected X-Registry-Token = %q, want empty", got)
- }
- _, _ = io.WriteString(w, `{"token":"registry-token"}`)
- }))
- defer destination.Close()
-
- source := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- if got := r.Header.Get("X-Registry-Token"); got != "configured-token" {
- t.Errorf("source X-Registry-Token = %q, want %q", got, "configured-token")
- }
- http.Redirect(w, r, destination.URL+"/token", http.StatusFound)
- }))
- defer source.Close()
-
- authForURL := func(rawURL string) (string, string) {
- if strings.HasPrefix(rawURL, source.URL) {
- return "X-Registry-Token", "configured-token"
- }
- return "", ""
- }
- transport := NewTransport(http.DefaultTransport, authForURL)
- token, _, err := transport.fetchToken(context.Background(), bearerChallenge{realm: source.URL + "/token"})
- if err != nil {
- t.Fatalf("fetchToken: %v", err)
- }
- if token != "registry-token" {
- t.Errorf("token = %q, want %q", token, "registry-token")
- }
-}
-
-func TestTransportPrunesExpiredTokens(t *testing.T) {
- transport := NewTransport(http.DefaultTransport, nil)
- transport.tokens["expired-unused"] = cachedToken{
- value: "expired-token",
- expiresAt: time.Now().Add(-time.Minute),
- }
- transport.cacheToken("current", cachedToken{
- value: "current-token",
- expiresAt: time.Now().Add(time.Minute),
- })
-
- if got := transport.cachedToken("current"); got != "current-token" {
- t.Errorf("cachedToken(current) = %q, want %q", got, "current-token")
- }
- if _, ok := transport.tokens["expired-unused"]; ok {
- t.Error("expired unused token was not pruned")
- }
-}
-
-func TestTransportDoesNotFollowBearerChallengeOutsideOCIRegistry(t *testing.T) {
- tokenRequests := 0
- var server *httptest.Server
- server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- if r.URL.Path == "/token" {
- tokenRequests++
- _, _ = io.WriteString(w, `{"token":"unexpected"}`)
- return
- }
- w.Header().Set("WWW-Authenticate", `Bearer realm="`+server.URL+`/token"`)
- http.Error(w, "authentication required", http.StatusUnauthorized)
- }))
- defer server.Close()
-
- client := &http.Client{Transport: NewTransport(http.DefaultTransport, nil)}
- resp, err := client.Get(server.URL + "/api/packages")
- if err != nil {
- t.Fatalf("GET API: %v", err)
- }
- _ = resp.Body.Close()
- if resp.StatusCode != http.StatusUnauthorized {
- t.Errorf("status = %d, want %d", resp.StatusCode, http.StatusUnauthorized)
- }
- if tokenRequests != 0 {
- t.Errorf("token requests = %d, want 0", tokenRequests)
- }
-}
diff --git a/internal/metrics/metrics.go b/internal/metrics/metrics.go
index aff5768..da8bde6 100644
--- a/internal/metrics/metrics.go
+++ b/internal/metrics/metrics.go
@@ -6,7 +6,6 @@ import (
"strconv"
"time"
- "github.com/git-pkgs/purl"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
@@ -121,48 +120,6 @@ var (
Help: "Number of currently active requests",
},
)
-
- IntegrityFailures = prometheus.NewCounterVec(
- prometheus.CounterOpts{
- Name: "proxy_integrity_failures_total",
- Help: "Cached artifacts that failed hash verification on read",
- },
- []string{"ecosystem"},
- )
-
- HealthProbeFailures = prometheus.NewCounterVec(
- prometheus.CounterOpts{
- Name: "proxy_health_probe_failures_total",
- Help: "Total number of storage health probe failures, by step (write|size|read|verify|delete).",
- },
- []string{"step"},
- )
-
- // Scanning metrics
- ScanDuration = prometheus.NewHistogramVec(
- prometheus.HistogramOpts{
- Name: "proxy_scan_duration_seconds",
- Help: "Pre-cache artifact scan duration in seconds, by ecosystem and scanner",
- Buckets: prometheus.DefBuckets,
- },
- []string{"ecosystem", "scanner"},
- )
-
- ScanBlocked = prometheus.NewCounterVec(
- prometheus.CounterOpts{
- Name: "proxy_scan_blocked_total",
- Help: "Total number of artifacts blocked by a pre-cache scan, by ecosystem and scanner",
- },
- []string{"ecosystem", "scanner"},
- )
-
- ScanErrors = prometheus.NewCounterVec(
- prometheus.CounterOpts{
- Name: "proxy_scan_errors_total",
- Help: "Total number of pre-cache scan errors, by ecosystem, scanner, and error type",
- },
- []string{"ecosystem", "scanner", "error_type"},
- )
)
func init() {
@@ -181,11 +138,6 @@ func init() {
StorageOperationDuration,
StorageErrors,
ActiveRequests,
- IntegrityFailures,
- HealthProbeFailures,
- ScanDuration,
- ScanBlocked,
- ScanErrors,
)
}
@@ -203,12 +155,12 @@ func RecordRequest(ecosystem string, status int, duration time.Duration) {
// RecordCacheHit increments cache hit counter.
func RecordCacheHit(ecosystem string) {
- CacheHits.WithLabelValues(purl.NormalizeEcosystem(ecosystem)).Inc()
+ CacheHits.WithLabelValues(ecosystem).Inc()
}
// RecordCacheMiss increments cache miss counter.
func RecordCacheMiss(ecosystem string) {
- CacheMisses.WithLabelValues(purl.NormalizeEcosystem(ecosystem)).Inc()
+ CacheMisses.WithLabelValues(ecosystem).Inc()
}
// RecordUpstreamFetch tracks upstream fetch duration.
@@ -226,37 +178,11 @@ func RecordStorageOperation(operation string, duration time.Duration) {
StorageOperationDuration.WithLabelValues(operation).Observe(duration.Seconds())
}
-// RecordIntegrityFailure increments the integrity failure counter.
-func RecordIntegrityFailure(ecosystem string) {
- IntegrityFailures.WithLabelValues(ecosystem).Inc()
-}
-
-// RecordHealthProbeFailure increments the health probe failure counter.
-// step is one of: "write", "size", "read", "verify", "delete".
-func RecordHealthProbeFailure(step string) {
- HealthProbeFailures.WithLabelValues(step).Inc()
-}
-
// RecordStorageError increments storage error counter.
func RecordStorageError(operation string) {
StorageErrors.WithLabelValues(operation).Inc()
}
-// RecordScanResult tracks a completed pre-cache scan call.
-func RecordScanResult(ecosystem, scannerName string, allowed bool, duration time.Duration) {
- ecosystem = purl.NormalizeEcosystem(ecosystem)
- ScanDuration.WithLabelValues(ecosystem, scannerName).Observe(duration.Seconds())
- if !allowed {
- ScanBlocked.WithLabelValues(ecosystem, scannerName).Inc()
- }
-}
-
-// RecordScanError increments the scan error counter.
-// errorType is one of: "error" (scanner call failed), "timeout", "cancelled".
-func RecordScanError(ecosystem, scannerName, errorType string) {
- ScanErrors.WithLabelValues(purl.NormalizeEcosystem(ecosystem), scannerName, errorType).Inc()
-}
-
// UpdateCacheStats updates cache size and artifact count gauges.
func UpdateCacheStats(sizeBytes, artifactCount int64) {
CacheSize.Set(float64(sizeBytes))
diff --git a/internal/metrics/metrics_test.go b/internal/metrics/metrics_test.go
index a445467..db7b097 100644
--- a/internal/metrics/metrics_test.go
+++ b/internal/metrics/metrics_test.go
@@ -6,7 +6,6 @@ import (
"time"
"github.com/prometheus/client_golang/prometheus"
- "github.com/prometheus/client_golang/prometheus/testutil"
dto "github.com/prometheus/client_model/go"
)
@@ -49,7 +48,7 @@ func TestRecordStorageOperations(t *testing.T) {
func TestUpdateCacheStats(t *testing.T) {
UpdateCacheStats(1024*1024*1024, 100) // 1GB, 100 artifacts
- UpdateCacheStats(0, 0) // Empty cache
+ UpdateCacheStats(0, 0) // Empty cache
// No panics = success
}
@@ -192,45 +191,22 @@ func TestMetricsEndpointOutput(t *testing.T) {
func TestMetricsLabeling(t *testing.T) {
// Test that different ecosystems are properly labeled
- ecosystems := []struct {
- input string
- label string
- }{
- {input: "npm", label: "npm"},
- {input: "pypi", label: "pypi"},
- {input: "cargo", label: "cargo"},
- {input: "gem", label: "rubygems"},
- }
+ ecosystems := []string{"npm", "pypi", "cargo", "gem"}
for _, eco := range ecosystems {
- RecordRequest(eco.input, 200, 10*time.Millisecond)
- RecordCacheHit(eco.input)
+ RecordRequest(eco, 200, 10*time.Millisecond)
+ RecordCacheHit(eco)
}
// Verify each ecosystem has metrics
for _, eco := range ecosystems {
- val := getMetricValue(t, CacheHits, eco.label)
+ val := getMetricValue(t, CacheHits, eco)
if val == 0 {
- t.Errorf("no cache hits recorded for %s", eco.label)
+ t.Errorf("no cache hits recorded for %s", eco)
}
}
}
-func TestCacheMetricLabelsAreNormalized(t *testing.T) {
- rubyHitsBefore := testutil.ToFloat64(CacheHits.WithLabelValues("rubygems"))
- composerMissesBefore := testutil.ToFloat64(CacheMisses.WithLabelValues("packagist"))
-
- RecordCacheHit("gem")
- RecordCacheMiss("composer")
-
- if diff := testutil.ToFloat64(CacheHits.WithLabelValues("rubygems")) - rubyHitsBefore; diff != 1 {
- t.Errorf("rubygems cache hits delta = %.0f, want 1", diff)
- }
- if diff := testutil.ToFloat64(CacheMisses.WithLabelValues("packagist")) - composerMissesBefore; diff != 1 {
- t.Errorf("packagist cache misses delta = %.0f, want 1", diff)
- }
-}
-
func TestMetricNames(t *testing.T) {
// Verify metric names follow Prometheus naming conventions
expectedMetrics := []string{
diff --git a/internal/mirror/job.go b/internal/mirror/job.go
deleted file mode 100644
index 70d7003..0000000
--- a/internal/mirror/job.go
+++ /dev/null
@@ -1,213 +0,0 @@
-package mirror
-
-import (
- "context"
- "crypto/rand"
- "encoding/json"
- "fmt"
- "sync"
- "time"
-)
-
-// JobState represents the current state of a mirror job.
-type JobState string
-
-const (
- JobStatePending JobState = "pending"
- JobStateRunning JobState = "running"
- JobStateComplete JobState = "complete"
- JobStateFailed JobState = "failed"
- JobStateCanceled JobState = "canceled"
-)
-
-const jobTTL = 1 * time.Hour
-const cleanupInterval = 5 * time.Minute //nolint:mnd // cleanup ticker
-
-// Job represents an async mirror operation.
-type Job struct {
- ID string `json:"id"`
- State JobState `json:"state"`
- Progress Progress `json:"progress"`
- CreatedAt time.Time `json:"created_at"`
- Error string `json:"error,omitempty"`
-
- cancel context.CancelFunc
-}
-
-// JobRequest is the JSON body for starting a mirror job via the API.
-type JobRequest struct {
- PURLs []string `json:"purls,omitempty"`
- SBOM json.RawMessage `json:"sbom,omitempty"`
- Registry string `json:"registry,omitempty"`
-}
-
-// JobStore manages in-memory mirror jobs.
-type JobStore struct {
- mu sync.RWMutex
- jobs map[string]*Job
- mirror *Mirror
- parentCtx context.Context
-}
-
-// NewJobStore creates a new job store. The parent context is used as the base
-// for all job contexts so that jobs are canceled when the server shuts down.
-func NewJobStore(ctx context.Context, m *Mirror) *JobStore {
- return &JobStore{
- jobs: make(map[string]*Job),
- mirror: m,
- parentCtx: ctx,
- }
-}
-
-// Create starts a new mirror job and returns its ID.
-func (js *JobStore) Create(req JobRequest) (string, error) {
- source, err := js.sourceFromRequest(req)
- if err != nil {
- return "", err
- }
-
- id := newJobID()
- ctx, cancel := context.WithCancel(js.parentCtx)
-
- job := &Job{
- ID: id,
- State: JobStatePending,
- CreatedAt: time.Now(),
- cancel: cancel,
- }
-
- js.mu.Lock()
- js.jobs[id] = job
- js.mu.Unlock()
-
- go js.runJob(ctx, cancel, job, source)
-
- return id, nil
-}
-
-// Get returns a snapshot of a job by ID. The returned copy is safe to
-// serialize without holding the lock.
-func (js *JobStore) Get(id string) *Job {
- js.mu.RLock()
- defer js.mu.RUnlock()
- job := js.jobs[id]
- if job == nil {
- return nil
- }
- snapshot := *job
- snapshot.cancel = nil // don't leak cancel func
- if len(job.Progress.Errors) > 0 {
- snapshot.Progress.Errors = make([]MirrorError, len(job.Progress.Errors))
- copy(snapshot.Progress.Errors, job.Progress.Errors)
- }
- return &snapshot
-}
-
-// Cancel cancels a running job.
-func (js *JobStore) Cancel(id string) bool {
- js.mu.Lock()
- defer js.mu.Unlock()
-
- job := js.jobs[id]
- if job == nil || job.cancel == nil {
- return false
- }
-
- if job.State != JobStatePending && job.State != JobStateRunning {
- return false
- }
-
- job.cancel()
- job.State = JobStateCanceled
- return true
-}
-
-// Cleanup removes completed/failed/canceled jobs older than jobTTL.
-func (js *JobStore) Cleanup() {
- js.mu.Lock()
- defer js.mu.Unlock()
- for id, job := range js.jobs {
- if job.State == JobStateComplete || job.State == JobStateFailed || job.State == JobStateCanceled {
- if time.Since(job.CreatedAt) > jobTTL {
- delete(js.jobs, id)
- }
- }
- }
-}
-
-// StartCleanup runs periodic cleanup of old jobs until the context is canceled.
-func (js *JobStore) StartCleanup(ctx context.Context) {
- ticker := time.NewTicker(cleanupInterval)
- defer ticker.Stop()
- for {
- select {
- case <-ctx.Done():
- return
- case <-ticker.C:
- js.Cleanup()
- }
- }
-}
-
-func (js *JobStore) runJob(ctx context.Context, cancel context.CancelFunc, job *Job, source Source) {
- defer cancel()
-
- js.mu.Lock()
- if job.State == JobStateCanceled {
- js.mu.Unlock()
- return
- }
- job.State = JobStateRunning
- js.mu.Unlock()
-
- progress, err := js.mirror.Run(ctx, source, func(p Progress) {
- js.mu.Lock()
- defer js.mu.Unlock()
- if job.State == JobStateRunning {
- job.Progress = p
- }
- })
-
- js.mu.Lock()
- defer js.mu.Unlock()
-
- // Cancel() may have already set the state; don't overwrite it.
- if job.State == JobStateCanceled {
- return
- }
-
- if err != nil {
- job.State = JobStateFailed
- job.Error = err.Error()
- return
- }
-
- job.Progress = *progress
- if progress.Failed > 0 && progress.Completed == 0 {
- job.State = JobStateFailed
- } else {
- job.State = JobStateComplete
- }
-}
-
-func (js *JobStore) sourceFromRequest(req JobRequest) (Source, error) { //nolint:ireturn // interface return is the design
- switch {
- case len(req.PURLs) > 0 && len(req.SBOM) > 0:
- return nil, fmt.Errorf("request must include only one of purls or sbom")
- case len(req.PURLs) > 0:
- return &PURLSource{PURLs: req.PURLs}, nil
- case len(req.SBOM) > 0:
- return &SBOMSource{Data: req.SBOM}, nil
- case req.Registry != "":
- return nil, fmt.Errorf("registry mirroring is not yet implemented; use purls instead")
- default:
- return nil, fmt.Errorf("request must include purls or sbom")
- }
-}
-
-// newJobID generates a random hex job ID.
-func newJobID() string {
- b := make([]byte, 16) //nolint:mnd // 128-bit ID
- _, _ = rand.Read(b)
- return fmt.Sprintf("%x", b)
-}
diff --git a/internal/mirror/job_test.go b/internal/mirror/job_test.go
deleted file mode 100644
index c0ac3f1..0000000
--- a/internal/mirror/job_test.go
+++ /dev/null
@@ -1,215 +0,0 @@
-package mirror
-
-import (
- "context"
- "encoding/json"
- "testing"
- "time"
-)
-
-func TestJobStoreCreateAndGet(t *testing.T) {
- m := setupTestMirror(t, 1)
- js := NewJobStore(context.Background(), m)
-
- id, err := js.Create(JobRequest{
- PURLs: []string{"pkg:npm/lodash@4.17.21"},
- })
- if err != nil {
- t.Fatalf("Create() error = %v", err)
- }
-
- if id == "" {
- t.Fatal("expected non-empty job ID")
- }
-
- // Wait for the job to start (it runs async)
- time.Sleep(100 * time.Millisecond)
-
- job := js.Get(id)
- if job == nil {
- t.Fatal("Get() returned nil")
- }
- if job.ID != id {
- t.Errorf("job ID = %q, want %q", job.ID, id)
- }
-}
-
-func TestJobStoreGetNotFound(t *testing.T) {
- m := setupTestMirror(t, 1)
- js := NewJobStore(context.Background(), m)
-
- job := js.Get("nonexistent")
- if job != nil {
- t.Errorf("expected nil for nonexistent job, got %v", job)
- }
-}
-
-func TestJobStoreCancelNotFound(t *testing.T) {
- m := setupTestMirror(t, 1)
- js := NewJobStore(context.Background(), m)
-
- if js.Cancel("nonexistent") {
- t.Error("expected Cancel to return false for nonexistent job")
- }
-}
-
-func TestJobStoreCreateInvalidRequest(t *testing.T) {
- m := setupTestMirror(t, 1)
- js := NewJobStore(context.Background(), m)
-
- _, err := js.Create(JobRequest{})
- if err == nil {
- t.Fatal("expected error for empty request")
- }
-}
-
-func TestJobStoreMultipleJobs(t *testing.T) {
- m := setupTestMirror(t, 1)
- js := NewJobStore(context.Background(), m)
-
- id1, err := js.Create(JobRequest{PURLs: []string{"pkg:npm/lodash@4.17.21"}})
- if err != nil {
- t.Fatalf("Create() error = %v", err)
- }
-
- id2, err := js.Create(JobRequest{PURLs: []string{"pkg:cargo/serde@1.0.0"}})
- if err != nil {
- t.Fatalf("Create() error = %v", err)
- }
-
- if id1 == id2 {
- t.Error("expected different job IDs")
- }
-
- job1 := js.Get(id1)
- job2 := js.Get(id2)
- if job1 == nil || job2 == nil {
- t.Fatal("expected both jobs to exist")
- }
-}
-
-func TestSourceFromRequestPURLs(t *testing.T) {
- m := setupTestMirror(t, 1)
- js := NewJobStore(context.Background(), m)
-
- source, err := js.sourceFromRequest(JobRequest{PURLs: []string{"pkg:npm/lodash@1.0.0"}})
- if err != nil {
- t.Fatalf("sourceFromRequest() error = %v", err)
- }
- if _, ok := source.(*PURLSource); !ok {
- t.Errorf("expected *PURLSource, got %T", source)
- }
-}
-
-func TestSourceFromRequestSBOM(t *testing.T) {
- m := setupTestMirror(t, 1)
- js := NewJobStore(context.Background(), m)
- sbom := json.RawMessage(`{"bomFormat":"CycloneDX","components":[]}`)
-
- source, err := js.sourceFromRequest(JobRequest{SBOM: sbom})
- if err != nil {
- t.Fatalf("sourceFromRequest() error = %v", err)
- }
- sbomSource, ok := source.(*SBOMSource)
- if !ok {
- t.Fatalf("expected *SBOMSource, got %T", source)
- }
- if got := string(sbomSource.Data); got != string(sbom) {
- t.Errorf("SBOM data = %q, want %q", got, sbom)
- }
-}
-
-func TestSourceFromRequestRejectsPURLsAndSBOM(t *testing.T) {
- m := setupTestMirror(t, 1)
- js := NewJobStore(context.Background(), m)
-
- _, err := js.sourceFromRequest(JobRequest{
- PURLs: []string{"pkg:npm/lodash@4.17.21"},
- SBOM: json.RawMessage(`{"bomFormat":"CycloneDX","components":[]}`),
- })
- if err == nil {
- t.Fatal("expected error when both purls and sbom are provided")
- }
-}
-
-func TestSourceFromRequestRegistryRejected(t *testing.T) {
- m := setupTestMirror(t, 1)
- js := NewJobStore(context.Background(), m)
-
- _, err := js.sourceFromRequest(JobRequest{Registry: "npm"})
- if err == nil {
- t.Fatal("expected error for registry request")
- }
-}
-
-func TestJobStoreCleanup(t *testing.T) {
- m := setupTestMirror(t, 1)
- js := NewJobStore(context.Background(), m)
-
- // Add a completed job with old CreatedAt
- js.mu.Lock()
- js.jobs["old-job"] = &Job{
- ID: "old-job",
- State: JobStateComplete,
- CreatedAt: time.Now().Add(-2 * time.Hour),
- }
- js.jobs["recent-job"] = &Job{
- ID: "recent-job",
- State: JobStateComplete,
- CreatedAt: time.Now(),
- }
- js.jobs["running-job"] = &Job{
- ID: "running-job",
- State: JobStateRunning,
- CreatedAt: time.Now().Add(-2 * time.Hour),
- }
- js.mu.Unlock()
-
- js.Cleanup()
-
- if js.Get("old-job") != nil {
- t.Error("expected old completed job to be cleaned up")
- }
- if js.Get("recent-job") == nil {
- t.Error("expected recent completed job to be kept")
- }
- if js.Get("running-job") == nil {
- t.Error("expected running job to be kept regardless of age")
- }
-}
-
-func TestJobStoreCancelPreservesStateAfterRunJob(t *testing.T) {
- m := setupTestMirror(t, 1)
- js := NewJobStore(context.Background(), m)
-
- // Create a job with a PURL that will fail (no real upstream in test)
- id, err := js.Create(JobRequest{PURLs: []string{"pkg:npm/nonexistent-pkg@0.0.0"}})
- if err != nil {
- t.Fatalf("Create() error = %v", err)
- }
-
- // Cancel immediately -- the job may already be running
- js.Cancel(id)
-
- // Wait for runJob goroutine to finish
- time.Sleep(200 * time.Millisecond)
-
- job := js.Get(id)
- if job == nil {
- t.Fatal("Get() returned nil")
- }
- if job.State != JobStateCanceled {
- t.Errorf("state = %q, want %q (cancel should not be overwritten by runJob)", job.State, JobStateCanceled)
- }
-}
-
-func TestNewJobIDUnique(t *testing.T) {
- ids := make(map[string]bool)
- for range 100 {
- id := newJobID()
- if ids[id] {
- t.Fatalf("duplicate job ID: %s", id)
- }
- ids[id] = true
- }
-}
diff --git a/internal/mirror/mirror.go b/internal/mirror/mirror.go
deleted file mode 100644
index b52c4f6..0000000
--- a/internal/mirror/mirror.go
+++ /dev/null
@@ -1,230 +0,0 @@
-// Package mirror provides selective package mirroring for pre-populating the proxy cache.
-package mirror
-
-import (
- "context"
- "fmt"
- "log/slog"
- "sync"
- "sync/atomic"
- "time"
-
- "github.com/git-pkgs/proxy/internal/database"
- "github.com/git-pkgs/proxy/internal/handler"
- "github.com/git-pkgs/proxy/internal/storage"
- "golang.org/x/sync/errgroup"
-)
-
-// Mirror pre-populates the proxy cache from various input sources.
-type Mirror struct {
- proxy *handler.Proxy
- db *database.DB
- storage storage.Storage
- logger *slog.Logger
- workers int
-}
-
-// New creates a new Mirror with the given dependencies.
-func New(proxy *handler.Proxy, db *database.DB, store storage.Storage, logger *slog.Logger, workers int) *Mirror {
- if workers < 1 {
- workers = 1
- }
- return &Mirror{
- proxy: proxy,
- db: db,
- storage: store,
- logger: logger,
- workers: workers,
- }
-}
-
-// Progress tracks the state of a mirror operation.
-type Progress struct {
- Total int64 `json:"total"`
- Completed int64 `json:"completed"`
- Skipped int64 `json:"skipped"`
- Failed int64 `json:"failed"`
- Bytes int64 `json:"bytes"`
- Errors []MirrorError `json:"errors,omitempty"`
- StartedAt time.Time `json:"started_at"`
- Phase string `json:"phase"`
-}
-
-// MirrorError records a single failed mirror attempt.
-type MirrorError struct {
- Ecosystem string `json:"ecosystem"`
- Name string `json:"name"`
- Version string `json:"version"`
- Error string `json:"error"`
-}
-
-type progressTracker struct {
- total atomic.Int64
- completed atomic.Int64
- skipped atomic.Int64
- failed atomic.Int64
- bytes atomic.Int64
- mu sync.Mutex
- errors []MirrorError
- startedAt time.Time
- phase atomic.Value // string
-}
-
-func newProgressTracker() *progressTracker {
- pt := &progressTracker{
- startedAt: time.Now(),
- }
- pt.phase.Store("resolving")
- return pt
-}
-
-const maxTrackedErrors = 1000
-const progressReportInterval = 500 * time.Millisecond //nolint:mnd // progress update frequency
-
-func (pt *progressTracker) addError(eco, name, version, err string) {
- pt.mu.Lock()
- if len(pt.errors) < maxTrackedErrors {
- pt.errors = append(pt.errors, MirrorError{
- Ecosystem: eco,
- Name: name,
- Version: version,
- Error: err,
- })
- }
- pt.mu.Unlock()
-}
-
-func (pt *progressTracker) snapshot() Progress {
- pt.mu.Lock()
- errs := make([]MirrorError, len(pt.errors))
- copy(errs, pt.errors)
- pt.mu.Unlock()
-
- phase, _ := pt.phase.Load().(string)
- return Progress{
- Total: pt.total.Load(),
- Completed: pt.completed.Load(),
- Skipped: pt.skipped.Load(),
- Failed: pt.failed.Load(),
- Bytes: pt.bytes.Load(),
- Errors: errs,
- StartedAt: pt.startedAt,
- Phase: phase,
- }
-}
-
-// ProgressFunc is called periodically with a snapshot of the current progress.
-type ProgressFunc func(Progress)
-
-// Run mirrors all packages from the source using a bounded worker pool.
-// It returns the final progress when complete. If onProgress is non-nil,
-// it is called with progress snapshots as work proceeds.
-func (m *Mirror) Run(ctx context.Context, source Source, onProgress ...ProgressFunc) (*Progress, error) {
- tracker := newProgressTracker()
-
- // Collect items from source
- var items []PackageVersion
- tracker.phase.Store("resolving")
- err := source.Enumerate(ctx, func(pv PackageVersion) error {
- items = append(items, pv)
- return nil
- })
- if err != nil {
- return nil, fmt.Errorf("enumerating packages: %w", err)
- }
-
- tracker.total.Store(int64(len(items)))
- tracker.phase.Store("downloading")
-
- // Start periodic progress reporting if a callback was provided
- var progressFn ProgressFunc
- if len(onProgress) > 0 && onProgress[0] != nil {
- progressFn = onProgress[0]
- }
- progressDone := make(chan struct{})
- if progressFn != nil {
- progressFn(tracker.snapshot()) // initial snapshot with total set
- go func() {
- ticker := time.NewTicker(progressReportInterval)
- defer ticker.Stop()
- for {
- select {
- case <-progressDone:
- return
- case <-ticker.C:
- progressFn(tracker.snapshot())
- }
- }
- }()
- }
-
- // Process items with bounded concurrency
- g, gctx := errgroup.WithContext(ctx)
- g.SetLimit(m.workers)
-
- for _, item := range items {
- g.Go(func() (err error) {
- defer func() {
- if r := recover(); r != nil {
- m.logger.Error("panic in mirror worker", "recover", r,
- "ecosystem", item.Ecosystem, "name", item.Name, "version", item.Version)
- tracker.failed.Add(1)
- tracker.addError(item.Ecosystem, item.Name, item.Version, fmt.Sprintf("panic: %v", r))
- }
- }()
- m.mirrorOne(gctx, item, tracker)
- return nil // never fail the group; errors are tracked
- })
- }
-
- _ = g.Wait()
-
- close(progressDone) // stop the progress reporter goroutine
-
- tracker.phase.Store("complete")
- p := tracker.snapshot()
-
- // Send final snapshot
- if progressFn != nil {
- progressFn(p)
- }
-
- return &p, nil
-}
-
-// RunDryRun enumerates what would be mirrored without downloading.
-func (m *Mirror) RunDryRun(ctx context.Context, source Source) ([]PackageVersion, error) {
- var items []PackageVersion
- err := source.Enumerate(ctx, func(pv PackageVersion) error {
- items = append(items, pv)
- return nil
- })
- return items, err
-}
-
-func (m *Mirror) mirrorOne(ctx context.Context, pv PackageVersion, tracker *progressTracker) {
- result, err := m.proxy.GetOrFetchArtifact(ctx, pv.Ecosystem, pv.Name, pv.Version, "")
- if err != nil {
- tracker.failed.Add(1)
- tracker.addError(pv.Ecosystem, pv.Name, pv.Version, err.Error())
- m.logger.Warn("mirror failed",
- "ecosystem", pv.Ecosystem, "name", pv.Name, "version", pv.Version, "error", err)
- return
- }
-
- if result.Reader != nil {
- _ = result.Reader.Close()
- }
-
- if result.Cached {
- tracker.skipped.Add(1)
- m.logger.Debug("already cached",
- "ecosystem", pv.Ecosystem, "name", pv.Name, "version", pv.Version)
- } else {
- tracker.completed.Add(1)
- tracker.bytes.Add(result.Artifact.Size)
- m.logger.Info("mirrored",
- "ecosystem", pv.Ecosystem, "name", pv.Name, "version", pv.Version,
- "size", result.Artifact.Size)
- }
-}
diff --git a/internal/mirror/mirror_test.go b/internal/mirror/mirror_test.go
deleted file mode 100644
index 3a6420f..0000000
--- a/internal/mirror/mirror_test.go
+++ /dev/null
@@ -1,254 +0,0 @@
-package mirror
-
-import (
- "context"
- "crypto/sha256"
- "database/sql"
- "log/slog"
- "os"
- "strings"
- "testing"
- "time"
-
- "github.com/git-pkgs/proxy/internal/database"
- "github.com/git-pkgs/proxy/internal/handler"
- "github.com/git-pkgs/proxy/internal/storage"
- "github.com/git-pkgs/registries/fetch"
-)
-
-// setupTestMirror creates a Mirror with real DB and filesystem storage for integration tests.
-func setupTestMirror(t *testing.T, workers int) *Mirror {
- t.Helper()
-
- dbPath := t.TempDir() + "/test.db"
- db, err := database.Create(dbPath)
- if err != nil {
- t.Fatalf("creating database: %v", err)
- }
- if err := db.MigrateSchema(); err != nil {
- t.Fatalf("migrating schema: %v", err)
- }
- t.Cleanup(func() { _ = db.Close() })
-
- storeDir := t.TempDir()
- store, err := storage.OpenBucket(context.Background(), "file://"+storeDir)
- if err != nil {
- t.Fatalf("opening storage: %v", err)
- }
-
- logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelWarn}))
- fetcher := fetch.NewFetcher()
- resolver := fetch.NewResolver()
- proxy := handler.NewProxy(db, store, fetcher, resolver, logger)
-
- return New(proxy, db, store, logger, workers)
-}
-
-const testPackageLodash = "lodash"
-
-type signedURLStorage struct {
- storage.Storage
-}
-
-func (signedURLStorage) SignedURL(context.Context, string, time.Duration) (string, error) {
- return "https://storage.example/artifact", nil
-}
-
-func TestMirrorRunEmptySource(t *testing.T) {
- m := setupTestMirror(t, 2)
-
- source := &PURLSource{PURLs: []string{}}
- progress, err := m.Run(context.Background(), source)
- if err != nil {
- t.Fatalf("Run() error = %v", err)
- }
-
- if progress.Total != 0 {
- t.Errorf("total = %d, want 0", progress.Total)
- }
- if progress.Phase != "complete" {
- t.Errorf("phase = %q, want %q", progress.Phase, "complete")
- }
-}
-
-func TestMirrorRunDryRun(t *testing.T) {
- m := setupTestMirror(t, 1)
-
- source := &PURLSource{
- PURLs: []string{
- "pkg:npm/lodash@4.17.21",
- "pkg:cargo/serde@1.0.0",
- },
- }
-
- items, err := m.RunDryRun(context.Background(), source)
- if err != nil {
- t.Fatalf("RunDryRun() error = %v", err)
- }
-
- if len(items) != 2 {
- t.Fatalf("got %d items, want 2", len(items))
- }
-
- // Dry run should not modify the database
- stats, err := m.db.GetCacheStats()
- if err != nil {
- t.Fatalf("GetCacheStats() error = %v", err)
- }
- if stats.TotalArtifacts != 0 {
- t.Errorf("artifacts = %d, want 0 (dry run should not cache)", stats.TotalArtifacts)
- }
-}
-
-func TestMirrorRunCanceled(t *testing.T) {
- m := setupTestMirror(t, 1)
-
- ctx, cancel := context.WithCancel(context.Background())
- cancel() // cancel immediately
-
- // Use a source that produces items but they'll all fail due to canceled context
- source := &PURLSource{
- PURLs: []string{"pkg:npm/lodash@4.17.21"},
- }
-
- progress, err := m.Run(ctx, source)
- if err != nil {
- t.Fatalf("Run() error = %v", err)
- }
-
- // With a canceled context, the fetch should fail
- if progress.Failed != 1 {
- t.Errorf("failed = %d, want 1", progress.Failed)
- }
-}
-
-func TestMirrorOneDirectServeCacheHit(t *testing.T) {
- m := setupTestMirror(t, 1)
- m.proxy.DirectServe = true
- m.proxy.Storage = signedURLStorage{Storage: m.storage}
-
- packagePURL := "pkg:npm/example"
- versionPURL := packagePURL + "@1.0.0"
- if err := m.db.UpsertPackage(&database.Package{
- PURL: packagePURL,
- Ecosystem: "npm",
- Name: "example",
- }); err != nil {
- t.Fatalf("UpsertPackage() error = %v", err)
- }
- if err := m.db.UpsertVersion(&database.Version{
- PURL: versionPURL,
- PackagePURL: packagePURL,
- }); err != nil {
- t.Fatalf("UpsertVersion() error = %v", err)
- }
- if err := m.db.UpsertArtifact(&database.Artifact{
- VersionPURL: versionPURL,
- Filename: "",
- UpstreamURL: "https://registry.example/artifact",
- StoragePath: sql.NullString{String: "npm/example/1.0.0/artifact", Valid: true},
- ContentHash: sql.NullString{String: strings.Repeat("a", sha256.Size*2), Valid: true},
- Size: sql.NullInt64{Int64: 1, Valid: true},
- FetchedAt: sql.NullTime{Time: time.Now(), Valid: true},
- }); err != nil {
- t.Fatalf("UpsertArtifact() error = %v", err)
- }
-
- tracker := newProgressTracker()
- m.mirrorOne(context.Background(), PackageVersion{
- Ecosystem: "npm",
- Name: "example",
- Version: "1.0.0",
- }, tracker)
-
- progress := tracker.snapshot()
- if progress.Skipped != 1 {
- t.Errorf("skipped = %d, want 1", progress.Skipped)
- }
- if progress.Failed != 0 {
- t.Errorf("failed = %d, want 0", progress.Failed)
- }
-}
-
-func TestProgressTrackerSnapshot(t *testing.T) {
- pt := newProgressTracker()
- pt.total.Store(10)
- pt.completed.Store(5)
- pt.skipped.Store(3)
- pt.failed.Store(2)
- pt.bytes.Store(1024)
- pt.phase.Store("downloading")
- pt.addError("npm", testPackageLodash, "4.17.21", "fetch failed")
-
- snap := pt.snapshot()
- if snap.Total != 10 {
- t.Errorf("total = %d, want 10", snap.Total)
- }
- if snap.Completed != 5 {
- t.Errorf("completed = %d, want 5", snap.Completed)
- }
- if snap.Skipped != 3 {
- t.Errorf("skipped = %d, want 3", snap.Skipped)
- }
- if snap.Failed != 2 {
- t.Errorf("failed = %d, want 2", snap.Failed)
- }
- if snap.Bytes != 1024 {
- t.Errorf("bytes = %d, want 1024", snap.Bytes)
- }
- if snap.Phase != "downloading" {
- t.Errorf("phase = %q, want %q", snap.Phase, "downloading")
- }
- if len(snap.Errors) != 1 {
- t.Fatalf("errors = %d, want 1", len(snap.Errors))
- }
- if snap.Errors[0].Name != testPackageLodash {
- t.Errorf("error name = %q, want %q", snap.Errors[0].Name, testPackageLodash)
- }
- if snap.StartedAt.IsZero() {
- t.Error("started_at should not be zero")
- }
-}
-
-func TestProgressTrackerConcurrentAccess(t *testing.T) {
- pt := newProgressTracker()
- done := make(chan struct{})
-
- for range 10 {
- go func() {
- pt.completed.Add(1)
- pt.addError("npm", "test", "1.0.0", "error")
- _ = pt.snapshot()
- done <- struct{}{}
- }()
- }
-
- timeout := time.After(5 * time.Second)
- for range 10 {
- select {
- case <-done:
- case <-timeout:
- t.Fatal("timed out waiting for goroutines")
- }
- }
-
- snap := pt.snapshot()
- if snap.Completed != 10 {
- t.Errorf("completed = %d, want 10", snap.Completed)
- }
- if len(snap.Errors) != 10 {
- t.Errorf("errors = %d, want 10", len(snap.Errors))
- }
-}
-
-func TestNewMirrorDefaultWorkers(t *testing.T) {
- m := New(nil, nil, nil, slog.Default(), 0)
- if m.workers != 1 {
- t.Errorf("workers = %d, want 1 (minimum)", m.workers)
- }
-
- m = New(nil, nil, nil, slog.Default(), -5)
- if m.workers != 1 {
- t.Errorf("workers = %d, want 1 (minimum)", m.workers)
- }
-}
diff --git a/internal/mirror/source.go b/internal/mirror/source.go
deleted file mode 100644
index 4269264..0000000
--- a/internal/mirror/source.go
+++ /dev/null
@@ -1,188 +0,0 @@
-package mirror
-
-import (
- "bytes"
- "context"
- "encoding/json"
- "fmt"
-
- cdx "github.com/CycloneDX/cyclonedx-go"
- "github.com/git-pkgs/purl"
- "github.com/git-pkgs/registries"
- _ "github.com/git-pkgs/registries/all"
- spdxjson "github.com/spdx/tools-golang/json"
- "github.com/spdx/tools-golang/spdx"
- spdxtv "github.com/spdx/tools-golang/tagvalue"
-)
-
-// PackageVersion identifies a specific package version to mirror.
-type PackageVersion struct {
- Ecosystem string
- Name string
- Version string
-}
-
-func (pv PackageVersion) String() string {
- return fmt.Sprintf("pkg:%s/%s@%s", pv.Ecosystem, pv.Name, pv.Version)
-}
-
-// Source produces PackageVersion items for mirroring.
-type Source interface {
- Enumerate(ctx context.Context, fn func(PackageVersion) error) error
-}
-
-// PURLSource yields packages from PURL strings.
-// Versioned PURLs produce a single item. Unversioned PURLs look up all versions from the registry.
-type PURLSource struct {
- PURLs []string
- RegClient *registries.Client
-}
-
-func (s *PURLSource) Enumerate(ctx context.Context, fn func(PackageVersion) error) error {
- client := s.RegClient
- if client == nil {
- client = registries.DefaultClient()
- }
-
- for _, purlStr := range s.PURLs {
- p, err := purl.Parse(purlStr)
- if err != nil {
- return fmt.Errorf("parsing PURL %q: %w", purlStr, err)
- }
-
- ecosystem := purl.PURLTypeToEcosystem(p.Type)
- name := p.Name
- if p.Namespace != "" {
- name = p.Namespace + "/" + p.Name
- }
-
- if p.Version != "" {
- if err := fn(PackageVersion{Ecosystem: ecosystem, Name: name, Version: p.Version}); err != nil {
- return err
- }
- continue
- }
-
- // Unversioned: enumerate all versions
- versions, err := s.fetchVersions(ctx, client, ecosystem, name)
- if err != nil {
- return fmt.Errorf("fetching versions for %s/%s: %w", ecosystem, name, err)
- }
- for _, v := range versions {
- if err := fn(PackageVersion{Ecosystem: ecosystem, Name: name, Version: v}); err != nil {
- return err
- }
- }
- }
- return nil
-}
-
-func (s *PURLSource) fetchVersions(ctx context.Context, client *registries.Client, ecosystem, name string) ([]string, error) {
- reg, err := registries.New(purl.EcosystemToPURLType(ecosystem), "", client)
- if err != nil {
- return nil, err
- }
- versions, err := reg.FetchVersions(ctx, name)
- if err != nil {
- return nil, err
- }
- result := make([]string, len(versions))
- for i, v := range versions {
- result[i] = v.Number
- }
- return result, nil
-}
-
-// SBOMSource extracts package versions from CycloneDX or SPDX SBOM data.
-type SBOMSource struct {
- Data []byte
- Name string
- RegClient *registries.Client
-}
-
-func (s *SBOMSource) Enumerate(ctx context.Context, fn func(PackageVersion) error) error {
- purls, err := s.extractPURLs()
- if err != nil {
- if s.Name != "" {
- return fmt.Errorf("parsing SBOM %s: %w", s.Name, err)
- }
- return fmt.Errorf("parsing SBOM: %w", err)
- }
-
- inner := &PURLSource{PURLs: purls, RegClient: s.RegClient}
- return inner.Enumerate(ctx, fn)
-}
-
-func (s *SBOMSource) extractPURLs() ([]string, error) {
- // Try CycloneDX first
- if purls, err := extractCycloneDXPURLs(s.Data); err == nil && len(purls) > 0 {
- return purls, nil
- }
-
- // Try SPDX JSON
- if purls, err := extractSPDXJSONPURLs(s.Data); err == nil && len(purls) > 0 {
- return purls, nil
- }
-
- // Try SPDX tag-value
- if purls, err := extractSPDXTVPURLs(s.Data); err == nil && len(purls) > 0 {
- return purls, nil
- }
-
- return nil, fmt.Errorf("could not parse SBOM as CycloneDX or SPDX")
-}
-
-func extractCycloneDXPURLs(data []byte) ([]string, error) {
- bom := new(cdx.BOM)
- if err := json.Unmarshal(data, bom); err != nil {
- // Try XML
- decoder := cdx.NewBOMDecoder(bytes.NewReader(data), cdx.BOMFileFormatXML)
- bom = new(cdx.BOM)
- if err := decoder.Decode(bom); err != nil {
- return nil, err
- }
- }
-
- if bom.Components == nil {
- return nil, nil
- }
-
- var purls []string
- for _, c := range *bom.Components {
- if c.PackageURL != "" {
- purls = append(purls, c.PackageURL)
- }
- }
- return purls, nil
-}
-
-func extractSPDXJSONPURLs(data []byte) ([]string, error) {
- doc, err := spdxjson.Read(bytes.NewReader(data))
- if err != nil {
- return nil, err
- }
- return extractSPDXDocPURLs(doc), nil
-}
-
-func extractSPDXTVPURLs(data []byte) ([]string, error) {
- doc, err := spdxtv.Read(bytes.NewReader(data))
- if err != nil {
- return nil, err
- }
- return extractSPDXDocPURLs(doc), nil
-}
-
-func extractSPDXDocPURLs(doc *spdx.Document) []string {
- if doc == nil {
- return nil
- }
- var purls []string
- for _, pkg := range doc.Packages {
- for _, ref := range pkg.PackageExternalReferences {
- if ref.RefType == "purl" {
- purls = append(purls, ref.Locator)
- }
- }
- }
- return purls
-}
diff --git a/internal/mirror/source_test.go b/internal/mirror/source_test.go
deleted file mode 100644
index fe56ca4..0000000
--- a/internal/mirror/source_test.go
+++ /dev/null
@@ -1,232 +0,0 @@
-package mirror
-
-import (
- "context"
- "encoding/json"
- "strings"
- "testing"
-)
-
-func TestPURLSourceVersioned(t *testing.T) {
- source := &PURLSource{
- PURLs: []string{
- "pkg:npm/lodash@4.17.21",
- "pkg:cargo/serde@1.0.0",
- "pkg:pypi/requests@2.31.0",
- },
- }
-
- var items []PackageVersion
- err := source.Enumerate(context.Background(), func(pv PackageVersion) error {
- items = append(items, pv)
- return nil
- })
- if err != nil {
- t.Fatalf("Enumerate() error = %v", err)
- }
-
- if len(items) != 3 {
- t.Fatalf("got %d items, want 3", len(items))
- }
-
- expected := []PackageVersion{
- {Ecosystem: "npm", Name: "lodash", Version: "4.17.21"},
- {Ecosystem: "cargo", Name: "serde", Version: "1.0.0"},
- {Ecosystem: "pypi", Name: "requests", Version: "2.31.0"},
- }
-
- for i, want := range expected {
- got := items[i]
- if got.Ecosystem != want.Ecosystem || got.Name != want.Name || got.Version != want.Version {
- t.Errorf("items[%d] = %v, want %v", i, got, want)
- }
- }
-}
-
-func TestPURLSourceScopedPackage(t *testing.T) {
- source := &PURLSource{
- PURLs: []string{"pkg:npm/%40babel/core@7.23.0"},
- }
-
- var items []PackageVersion
- err := source.Enumerate(context.Background(), func(pv PackageVersion) error {
- items = append(items, pv)
- return nil
- })
- if err != nil {
- t.Fatalf("Enumerate() error = %v", err)
- }
-
- if len(items) != 1 {
- t.Fatalf("got %d items, want 1", len(items))
- }
-
- if items[0].Name != "@babel/core" {
- t.Errorf("name = %q, want %q", items[0].Name, "@babel/core")
- }
- if items[0].Version != "7.23.0" {
- t.Errorf("version = %q, want %q", items[0].Version, "7.23.0")
- }
-}
-
-func TestPURLSourceInvalid(t *testing.T) {
- source := &PURLSource{
- PURLs: []string{"not-a-purl"},
- }
-
- err := source.Enumerate(context.Background(), func(pv PackageVersion) error {
- return nil
- })
- if err == nil {
- t.Fatal("expected error for invalid PURL")
- }
-}
-
-func TestPURLSourceCallbackError(t *testing.T) {
- source := &PURLSource{
- PURLs: []string{"pkg:npm/lodash@4.17.21"},
- }
-
- wantErr := context.Canceled
- err := source.Enumerate(context.Background(), func(pv PackageVersion) error {
- return wantErr
- })
- if err != wantErr {
- t.Fatalf("got error %v, want %v", err, wantErr)
- }
-}
-
-func TestPackageVersionString(t *testing.T) {
- pv := PackageVersion{Ecosystem: "npm", Name: "lodash", Version: "4.17.21"}
- got := pv.String()
- want := "pkg:npm/lodash@4.17.21"
- if got != want {
- t.Errorf("String() = %q, want %q", got, want)
- }
-}
-
-func TestSBOMSourceCycloneDXJSON(t *testing.T) {
- bom := map[string]any{
- "bomFormat": "CycloneDX",
- "specVersion": "1.4",
- "components": []map[string]any{
- {"type": "library", "name": "lodash", "version": "4.17.21", "purl": "pkg:npm/lodash@4.17.21"},
- {"type": "library", "name": "serde", "version": "1.0.0", "purl": "pkg:cargo/serde@1.0.0"},
- },
- }
-
- source := &SBOMSource{Data: marshalJSON(t, bom)}
-
- var items []PackageVersion
- err := source.Enumerate(context.Background(), func(pv PackageVersion) error {
- items = append(items, pv)
- return nil
- })
- if err != nil {
- t.Fatalf("Enumerate() error = %v", err)
- }
-
- if len(items) != 2 {
- t.Fatalf("got %d items, want 2", len(items))
- }
-
- if items[0].Ecosystem != "npm" || items[0].Name != "lodash" || items[0].Version != "4.17.21" {
- t.Errorf("items[0] = %v", items[0])
- }
- if items[1].Ecosystem != "cargo" || items[1].Name != "serde" || items[1].Version != "1.0.0" {
- t.Errorf("items[1] = %v", items[1])
- }
-}
-
-func TestSBOMSourceSPDXJSON(t *testing.T) {
- doc := map[string]any{
- "spdxVersion": "SPDX-2.3",
- "dataLicense": "CC0-1.0",
- "SPDXID": "SPDXRef-DOCUMENT",
- "name": "test",
- "documentNamespace": "https://example.com/test",
- "packages": []map[string]any{
- {
- "SPDXID": "SPDXRef-Package",
- "name": "lodash",
- "version": "4.17.21",
- "downloadLocation": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
- "externalRefs": []map[string]any{
- {
- "referenceCategory": "PACKAGE-MANAGER",
- "referenceType": "purl",
- "referenceLocator": "pkg:npm/lodash@4.17.21",
- },
- },
- },
- },
- }
-
- source := &SBOMSource{Data: marshalJSON(t, doc)}
-
- var items []PackageVersion
- err := source.Enumerate(context.Background(), func(pv PackageVersion) error {
- items = append(items, pv)
- return nil
- })
- if err != nil {
- t.Fatalf("Enumerate() error = %v", err)
- }
-
- if len(items) != 1 {
- t.Fatalf("got %d items, want 1", len(items))
- }
-
- if items[0].Name != "lodash" || items[0].Version != "4.17.21" {
- t.Errorf("items[0] = %v", items[0])
- }
-}
-
-func TestSBOMSourceEmptyData(t *testing.T) {
- source := &SBOMSource{}
-
- err := source.Enumerate(context.Background(), func(pv PackageVersion) error {
- return nil
- })
- if err == nil {
- t.Fatal("expected error for empty SBOM")
- }
-}
-
-func TestSBOMSourceInvalidFormat(t *testing.T) {
- source := &SBOMSource{Data: []byte("this is not an SBOM"), Name: "invalid.sbom"}
- err := source.Enumerate(context.Background(), func(pv PackageVersion) error {
- return nil
- })
- if err == nil {
- t.Fatal("expected error for invalid SBOM")
- }
- if !strings.Contains(err.Error(), "invalid.sbom") {
- t.Errorf("error = %q, want source name", err)
- }
-}
-
-func TestSBOMSourceEmptyCycloneDX(t *testing.T) {
- bom := map[string]any{
- "bomFormat": "CycloneDX",
- "specVersion": "1.4",
- }
- // This should fall through to SPDX parsing, which will also fail,
- // resulting in an error about not being able to parse
- source := &SBOMSource{Data: marshalJSON(t, bom)}
- err := source.Enumerate(context.Background(), func(pv PackageVersion) error {
- return nil
- })
- if err == nil {
- t.Fatal("expected error for empty SBOM")
- }
-}
-
-func marshalJSON(t *testing.T, v any) []byte {
- t.Helper()
- data, err := json.Marshal(v)
- if err != nil {
- t.Fatal(err)
- }
- return data
-}
diff --git a/internal/packageurl/packageurl.go b/internal/packageurl/packageurl.go
deleted file mode 100644
index 7110942..0000000
--- a/internal/packageurl/packageurl.go
+++ /dev/null
@@ -1,62 +0,0 @@
-// Package packageurl builds package URLs from ecosystem-native package names.
-package packageurl
-
-import (
- "strings"
-
- "github.com/git-pkgs/purl"
-)
-
-// Make constructs a package URL from an ecosystem-native package name.
-func Make(ecosystem, name, version string) *purl.PURL {
- return purl.MakePURL(ecosystem, name, version)
-}
-
-// MakeString constructs a package URL string. It returns an empty string when
-// the package identity cannot be represented as a PURL.
-func MakeString(ecosystem, name, version string) string {
- return purl.MakePURLString(ecosystem, name, version)
-}
-
-// WithVersionString returns a package PURL with its version replaced. It
-// returns an empty string when packagePURL is invalid.
-func WithVersionString(packagePURL, version string) string {
- pkg, err := purl.Parse(packagePURL)
- if err != nil {
- return ""
- }
- return pkg.WithVersion(version).String()
-}
-
-// MakeCacheStrings returns package and version PURLs suitable for artifact
-// cache records. Swift registry identities use an explicit generic PURL until
-// their source repository has been resolved. The result is independent of the
-// configured upstream so cache entries survive an upstream.swift change,
-// matching every other ecosystem.
-func MakeCacheStrings(ecosystem, name, version string) (packagePURL, versionPURL string) {
- if pkg := Make(ecosystem, name, ""); pkg != nil {
- return pkg.String(), pkg.WithVersion(version).String()
- }
- if purl.NormalizeEcosystem(ecosystem) != "swift" {
- return "", ""
- }
-
- identity, ok := swiftRegistryIdentity(name)
- if !ok {
- return "", ""
- }
-
- pkg := purl.New("generic", "swift-registry", identity, "", nil)
- return pkg.String(), pkg.WithVersion(version).String()
-}
-
-func swiftRegistryIdentity(name string) (string, bool) {
- scope, packageName, found := strings.Cut(name, "/")
- if !found {
- scope, packageName, found = strings.Cut(name, ".")
- }
- if !found || scope == "" || packageName == "" || strings.ContainsAny(packageName, "/.") {
- return "", false
- }
- return strings.ToLower(scope) + "." + strings.ToLower(packageName), true
-}
diff --git a/internal/packageurl/packageurl_test.go b/internal/packageurl/packageurl_test.go
deleted file mode 100644
index 8982288..0000000
--- a/internal/packageurl/packageurl_test.go
+++ /dev/null
@@ -1,75 +0,0 @@
-package packageurl
-
-import "testing"
-
-func TestMakeSwiftRegistryIdentityUnsupported(t *testing.T) {
- identities := []string{"apple.swift-argument-parser", "apple/swift-argument-parser"}
- for _, identity := range identities {
- t.Run(identity, func(t *testing.T) {
- if got := Make("swift", identity, "1.8.2"); got != nil {
- t.Errorf("Make() = %q, want nil", got.String())
- }
- if got := MakeString("swift", identity, "1.8.2"); got != "" {
- t.Errorf("MakeString() = %q, want empty string", got)
- }
- })
- }
-}
-
-func TestMakeStringSwiftSourceCoordinate(t *testing.T) {
- got := MakeString("swift", "github.com/apple/swift-package-manager", "1.7.0")
- want := "pkg:swift/github.com/apple/swift-package-manager@1.7.0"
- if got != want {
- t.Errorf("MakeString() = %q, want %q", got, want)
- }
-}
-
-func TestWithVersionStringPreservesQualifiers(t *testing.T) {
- packagePURL := "pkg:generic/swift-registry/apple.example?repository_url=https:%2F%2Fold.example%2Fswift"
- got := WithVersionString(packagePURL, "1.2.3")
- want := "pkg:generic/swift-registry/apple.example@1.2.3?repository_url=https:%2F%2Fold.example%2Fswift"
- if got != want {
- t.Errorf("WithVersionString() = %q, want %q", got, want)
- }
-
- if got := WithVersionString("not a purl", "1.2.3"); got != "" {
- t.Errorf("WithVersionString() = %q for invalid PURL, want empty string", got)
- }
-}
-
-func TestMakeCacheStringsSwiftRegistryIdentity(t *testing.T) {
- packagePURL, versionPURL := MakeCacheStrings("swift", "APPLE/EXAMPLE", "1.2.3")
-
- wantPackage := "pkg:generic/swift-registry/apple.example"
- if packagePURL != wantPackage {
- t.Errorf("package PURL = %q, want %q", packagePURL, wantPackage)
- }
- wantVersion := "pkg:generic/swift-registry/apple.example@1.2.3"
- if versionPURL != wantVersion {
- t.Errorf("version PURL = %q, want %q", versionPURL, wantVersion)
- }
-
- dottedPackage, dottedVersion := MakeCacheStrings("swift", "apple.example", "1.2.3")
- if dottedPackage != packagePURL || dottedVersion != versionPURL {
- t.Errorf("dotted identity cache PURLs = %q, %q; want %q, %q", dottedPackage, dottedVersion, packagePURL, versionPURL)
- }
-}
-
-func TestMakeCacheStringsUsesSourcePURLWhenAvailable(t *testing.T) {
- packagePURL, versionPURL := MakeCacheStrings("swift", "github.com/apple/swift-package-manager", "1.7.0")
-
- if packagePURL != "pkg:swift/github.com/apple/swift-package-manager" {
- t.Errorf("package PURL = %q", packagePURL)
- }
- if versionPURL != "pkg:swift/github.com/apple/swift-package-manager@1.7.0" {
- t.Errorf("version PURL = %q", versionPURL)
- }
-}
-
-func TestMakeStringDelegatesOtherEcosystems(t *testing.T) {
- got := MakeString("npm", "@babel/core", "7.23.0")
- want := "pkg:npm/%40babel/core@7.23.0"
- if got != want {
- t.Errorf("MakeString() = %q, want %q", got, want)
- }
-}
diff --git a/internal/scanner/group.go b/internal/scanner/group.go
deleted file mode 100644
index ed0be57..0000000
--- a/internal/scanner/group.go
+++ /dev/null
@@ -1,226 +0,0 @@
-package scanner
-
-import (
- "context"
- "errors"
- "fmt"
- "log/slog"
- "net/http"
- "sync"
- "time"
-
- "github.com/git-pkgs/proxy/internal/config"
- "github.com/git-pkgs/proxy/internal/metrics"
-)
-
-const (
- modeBlock = "block"
- modeMonitor = "monitor"
-
- defaultTimeout = 30 * time.Second
-)
-
-type entry struct {
- scanner Scanner
- mode string
- ecosystems map[string]struct{} // nil/empty means all ecosystems
-}
-
-// Group runs a set of configured Scanners concurrently and turns their
-// individual verdicts into a single decision.
-type Group struct {
- entries []entry
- timeout time.Duration
- failOpen bool
- logger *slog.Logger
-}
-
-// NewGroup builds a Group from cfg. If cfg.Enabled is false, the returned
-// Group has no entries and Enabled() reports false, so callers can skip
-// the scan path entirely.
-func NewGroup(cfg config.ScanningConfig, logger *slog.Logger) (*Group, error) {
- g := &Group{
- timeout: defaultTimeout,
- failOpen: cfg.FailOpen,
- logger: logger,
- }
- if !cfg.Enabled {
- return g, nil
- }
- if cfg.SigningKeyExpanded() == "" {
- return nil, fmt.Errorf("scanning.signing_key is required when scanning.enabled is true")
- }
- if d, err := time.ParseDuration(cfg.Timeout); err == nil && d > 0 {
- g.timeout = d
- }
-
- for _, sc := range cfg.Scanners {
- mode := sc.Mode
- if mode == "" {
- mode = modeBlock
- }
- if mode != modeBlock && mode != modeMonitor {
- return nil, fmt.Errorf("scanner %q: invalid mode %q (must be %q or %q)", sc.Name, mode, modeBlock, modeMonitor)
- }
-
- var ecosystems map[string]struct{}
- if len(sc.Ecosystems) > 0 {
- ecosystems = make(map[string]struct{}, len(sc.Ecosystems))
- for _, eco := range sc.Ecosystems {
- ecosystems[eco] = struct{}{}
- }
- }
-
- g.entries = append(g.entries, entry{
- scanner: NewHTTPScanner(sc.Name, sc.URL, sc.HeadersExpanded(), http.DefaultClient),
- mode: mode,
- ecosystems: ecosystems,
- })
- }
-
- return g, nil
-}
-
-// Enabled reports whether any scanner is configured.
-func (g *Group) Enabled() bool {
- return g != nil && len(g.entries) > 0
-}
-
-// Timeout returns the per-scan-call timeout used to bound the signed fetch
-// URL's validity.
-func (g *Group) Timeout() time.Duration {
- return g.timeout
-}
-
-func (g *Group) applicable(ecosystem string) []entry {
- var out []entry
- for _, e := range g.entries {
- if len(e.ecosystems) == 0 {
- out = append(out, e)
- continue
- }
- if _, ok := e.ecosystems[ecosystem]; ok {
- out = append(out, e)
- }
- }
- return out
-}
-
-// Scan runs every scanner applicable to req.Ecosystem concurrently, never
-// sequentially, and returns a single decision.
-//
-// The moment any "block" mode scanner reports Allowed: false (or errors,
-// unless FailOpen is set), Scan cancels a context shared by every
-// goroutine: in-flight calls to the other scanners are aborted rather than
-// waited out, since a single block already decides the outcome. Scan still
-// waits for all goroutines to observe that cancellation and return before
-// it itself returns, so no scan call outlives this method call.
-//
-// If nothing blocks, Scan waits for every "block" mode scanner to finish
-// before reporting Allowed: true — an allow decision can't be finalized
-// until all of them have answered. "monitor" mode scanners never gate the
-// wait or trigger cancellation: a monitor verdict of Allowed: false is
-// logged and folded into Result.Findings, but never blocks.
-func (g *Group) Scan(ctx context.Context, req Request) Result {
- entries := g.applicable(req.Ecosystem)
- if len(entries) == 0 {
- return Result{Allowed: true}
- }
-
- scanCtx, cancel := context.WithTimeout(ctx, g.timeout)
- defer cancel()
-
- var (
- mu sync.Mutex
- findings []Finding
- blocked *Result
- )
-
- var wg sync.WaitGroup
- for _, e := range entries {
- wg.Add(1)
- go func(e entry) {
- defer wg.Done()
- entryFindings, entryBlock := g.evaluate(scanCtx, req, e)
-
- mu.Lock()
- findings = append(findings, entryFindings...)
- if entryBlock != nil && blocked == nil {
- blocked = entryBlock
- cancel()
- }
- mu.Unlock()
- }(e)
- }
-
- wg.Wait()
-
- if blocked != nil {
- blocked.Findings = findings
- return *blocked
- }
-
- return Result{Allowed: true, Findings: findings}
-}
-
-// evaluate runs a single scanner and reports its findings plus, if this
-// scanner's verdict should block the artifact, the Result to block with
-// (nil otherwise). It never sets Result.Findings on a returned block
-// Result — the caller assembles Findings from every entry once all of them
-// have finished.
-func (g *Group) evaluate(scanCtx context.Context, req Request, e entry) (findings []Finding, block *Result) {
- start := time.Now()
- res, err := e.scanner.Scan(scanCtx, req)
- duration := time.Since(start)
-
- if err != nil {
- errType := "error"
- switch {
- case errors.Is(scanCtx.Err(), context.DeadlineExceeded):
- errType = "timeout"
- case errors.Is(scanCtx.Err(), context.Canceled):
- // scanCtx was cancelled because another scanner in the group
- // already decided the verdict, not because this call itself
- // timed out or failed on its own.
- errType = "cancelled"
- }
- metrics.RecordScanError(req.Ecosystem, e.scanner.Name(), errType)
-
- if errType == "cancelled" {
- // Another scanner already decided the verdict and cancelled
- // scanCtx; this call didn't fail on its own, so don't log it
- // as if it did.
- return nil, nil
- }
-
- if e.mode == modeMonitor || g.failOpen {
- g.logger.Warn("scanner call failed, treating as allowed",
- "scanner", e.scanner.Name(), "mode", e.mode, "error", err)
- return nil, nil
- }
-
- g.logger.Warn("scanner call failed, blocking artifact",
- "scanner", e.scanner.Name(), "mode", e.mode, "error", err)
- return nil, &Result{
- Allowed: false,
- Reason: fmt.Sprintf("scanner %q failed: %v", e.scanner.Name(), err),
- ScannerName: e.scanner.Name(),
- InfraError: true,
- }
- }
-
- metrics.RecordScanResult(req.Ecosystem, e.scanner.Name(), res.Allowed, duration)
-
- if e.mode == modeMonitor {
- if !res.Allowed {
- g.logger.Warn("monitor scanner flagged artifact",
- "scanner", e.scanner.Name(), "reason", res.Reason)
- }
- return res.Findings, nil
- }
-
- if !res.Allowed {
- return res.Findings, &Result{Allowed: false, Reason: res.Reason, ScannerName: e.scanner.Name()}
- }
- return res.Findings, nil
-}
diff --git a/internal/scanner/group_test.go b/internal/scanner/group_test.go
deleted file mode 100644
index e5b40ee..0000000
--- a/internal/scanner/group_test.go
+++ /dev/null
@@ -1,245 +0,0 @@
-package scanner
-
-import (
- "context"
- "errors"
- "io"
- "log/slog"
- "strings"
- "testing"
- "time"
-
- "github.com/git-pkgs/proxy/internal/config"
- "github.com/git-pkgs/proxy/internal/metrics"
- "github.com/prometheus/client_golang/prometheus/testutil"
-)
-
-func discardLogger() *slog.Logger {
- return slog.New(slog.NewTextHandler(io.Discard, nil))
-}
-
-// fakeScanner is a Scanner test double that can simulate a delay, a fixed
-// result or error, and report whether its context was cancelled before it
-// returned.
-type fakeScanner struct {
- name string
- delay time.Duration
- result Result
- err error
- cancelled *bool
-}
-
-func (f *fakeScanner) Name() string { return f.name }
-
-func (f *fakeScanner) Scan(ctx context.Context, _ Request) (Result, error) {
- select {
- case <-time.After(f.delay):
- case <-ctx.Done():
- if f.cancelled != nil {
- *f.cancelled = true
- }
- return Result{}, ctx.Err()
- }
- return f.result, f.err
-}
-
-func newTestGroup(entries []entry, failOpen bool) *Group {
- return &Group{
- entries: entries,
- timeout: time.Second,
- failOpen: failOpen,
- logger: discardLogger(),
- }
-}
-
-func TestGroup_Enabled(t *testing.T) {
- disabled, err := NewGroup(config.ScanningConfig{Enabled: false}, discardLogger())
- if err != nil {
- t.Fatalf("NewGroup() error: %v", err)
- }
- if disabled.Enabled() {
- t.Error("Enabled() = true for disabled config, want false")
- }
-
- enabled, err := NewGroup(config.ScanningConfig{
- Enabled: true,
- Timeout: "10s",
- SigningKey: "test-signing-key",
- Scanners: []config.ScannerConfig{
- {Name: "clamav", URL: "http://clamav.invalid", Mode: "block"},
- },
- }, discardLogger())
- if err != nil {
- t.Fatalf("NewGroup() error: %v", err)
- }
- if !enabled.Enabled() {
- t.Error("Enabled() = false for configured scanner, want true")
- }
-}
-
-func TestGroup_NewGroup_InvalidMode(t *testing.T) {
- _, err := NewGroup(config.ScanningConfig{
- Enabled: true,
- SigningKey: "test-signing-key",
- Scanners: []config.ScannerConfig{
- {Name: "bad", URL: "http://bad.invalid", Mode: "quarantine"},
- },
- }, discardLogger())
- if err == nil {
- t.Fatal("NewGroup() error = nil, want error for invalid mode")
- }
-}
-
-func TestGroup_NewGroup_MissingSigningKey(t *testing.T) {
- _, err := NewGroup(config.ScanningConfig{
- Enabled: true,
- Scanners: []config.ScannerConfig{
- {Name: "clamav", URL: "http://clamav.invalid", Mode: "block"},
- },
- }, discardLogger())
- if err == nil {
- t.Fatal("NewGroup() error = nil, want error for missing signing key")
- }
-}
-
-func TestGroup_Scan_NoApplicableScanners(t *testing.T) {
- g := newTestGroup([]entry{
- {
- scanner: &fakeScanner{name: "npm-only", result: Result{Allowed: false}},
- mode: modeBlock,
- ecosystems: map[string]struct{}{"npm": {}},
- },
- }, false)
-
- result := g.Scan(context.Background(), Request{Ecosystem: "pypi"})
- if !result.Allowed {
- t.Error("Allowed = false, want true when no scanner applies to the ecosystem")
- }
-}
-
-func TestGroup_Scan_Allowed(t *testing.T) {
- g := newTestGroup([]entry{
- {scanner: &fakeScanner{name: "clamav", result: Result{Allowed: true}}, mode: modeBlock},
- }, false)
-
- result := g.Scan(context.Background(), Request{Ecosystem: "npm"})
- if !result.Allowed {
- t.Error("Allowed = false, want true")
- }
-}
-
-func TestGroup_Scan_Blocked(t *testing.T) {
- g := newTestGroup([]entry{
- {scanner: &fakeScanner{name: "clamav", result: Result{Allowed: false, Reason: "malware"}}, mode: modeBlock},
- }, false)
-
- result := g.Scan(context.Background(), Request{Ecosystem: "npm"})
- if result.Allowed {
- t.Error("Allowed = true, want false")
- }
- if result.Reason != "malware" {
- t.Errorf("Reason = %q, want %q", result.Reason, "malware")
- }
- if result.ScannerName != "clamav" {
- t.Errorf("ScannerName = %q, want %q", result.ScannerName, "clamav")
- }
- if result.InfraError {
- t.Error("InfraError = true, want false — this is a genuine scanner verdict, not an infrastructure failure")
- }
-}
-
-func TestGroup_Scan_MonitorNeverBlocks(t *testing.T) {
- g := newTestGroup([]entry{
- {
- scanner: &fakeScanner{name: "trivy", result: Result{
- Allowed: false,
- Reason: "cve found",
- Findings: []Finding{{Severity: "medium", Title: "CVE-1234"}},
- }},
- mode: modeMonitor,
- },
- }, false)
-
- result := g.Scan(context.Background(), Request{Ecosystem: "npm"})
- if !result.Allowed {
- t.Error("Allowed = false, want true — monitor mode must never block")
- }
- if len(result.Findings) != 1 || result.Findings[0].Title != "CVE-1234" {
- t.Errorf("Findings = %+v, want the monitor scanner's finding folded in", result.Findings)
- }
-}
-
-func TestGroup_Scan_FirstBlockCancelsOthers(t *testing.T) {
- var slowSawCancel bool
- g := newTestGroup([]entry{
- {scanner: &fakeScanner{name: "fast-block", result: Result{Allowed: false, Reason: "blocked"}}, mode: modeBlock},
- {scanner: &fakeScanner{name: "slow", delay: 2 * time.Second, cancelled: &slowSawCancel}, mode: modeBlock},
- }, false)
-
- start := time.Now()
- result := g.Scan(context.Background(), Request{Ecosystem: "npm"})
- elapsed := time.Since(start)
-
- if result.Allowed {
- t.Error("Allowed = true, want false")
- }
- if elapsed >= 2*time.Second {
- t.Errorf("Scan() took %v, want it to return promptly once the slow scanner's context was cancelled", elapsed)
- }
- if !slowSawCancel {
- t.Error("slow scanner never observed context cancellation")
- }
-
- if got := testutil.ToFloat64(metrics.ScanErrors.WithLabelValues("npm", "slow", "cancelled")); got != 1 {
- t.Errorf("scan_errors{error_type=cancelled} = %v, want 1 — the slow scanner was aborted by a sibling's block, not by its own timeout", got)
- }
- if got := testutil.ToFloat64(metrics.ScanErrors.WithLabelValues("npm", "slow", "timeout")); got != 0 {
- t.Errorf("scan_errors{error_type=timeout} = %v, want 0 — intra-group cancellation must not be mislabelled as a timeout", got)
- }
-}
-
-func TestGroup_Scan_WaitsForAllBlockScannersBeforeAllowing(t *testing.T) {
- g := newTestGroup([]entry{
- {scanner: &fakeScanner{name: "fast", result: Result{Allowed: true}}, mode: modeBlock},
- {scanner: &fakeScanner{name: "slow", delay: 30 * time.Millisecond, result: Result{Allowed: true}}, mode: modeBlock},
- }, false)
-
- start := time.Now()
- result := g.Scan(context.Background(), Request{Ecosystem: "npm"})
- elapsed := time.Since(start)
-
- if !result.Allowed {
- t.Error("Allowed = false, want true")
- }
- if elapsed < 30*time.Millisecond {
- t.Errorf("Scan() returned after %v, want it to wait for the slower block scanner", elapsed)
- }
-}
-
-func TestGroup_Scan_ErrorFailClosedByDefault(t *testing.T) {
- g := newTestGroup([]entry{
- {scanner: &fakeScanner{name: "flaky", err: errors.New("connection refused")}, mode: modeBlock},
- }, false)
-
- result := g.Scan(context.Background(), Request{Ecosystem: "npm"})
- if result.Allowed {
- t.Error("Allowed = true, want false — default posture is fail-closed on scanner error")
- }
- if !result.InfraError {
- t.Error("InfraError = false, want true — the block came from a scanner call failure, not a verdict")
- }
- if !strings.Contains(result.Reason, "connection refused") {
- t.Errorf("Reason = %q, want it to include the underlying error for server-side logging", result.Reason)
- }
-}
-
-func TestGroup_Scan_ErrorFailOpen(t *testing.T) {
- g := newTestGroup([]entry{
- {scanner: &fakeScanner{name: "flaky", err: errors.New("connection refused")}, mode: modeBlock},
- }, true)
-
- result := g.Scan(context.Background(), Request{Ecosystem: "npm"})
- if !result.Allowed {
- t.Error("Allowed = false, want true — FailOpen must treat scanner errors as allowed")
- }
-}
diff --git a/internal/scanner/http.go b/internal/scanner/http.go
deleted file mode 100644
index 90a4e5e..0000000
--- a/internal/scanner/http.go
+++ /dev/null
@@ -1,97 +0,0 @@
-package scanner
-
-import (
- "bytes"
- "context"
- "encoding/json"
- "fmt"
- "net/http"
-)
-
-// HTTPScanner adapts an external HTTP scanning service to the Scanner
-// interface. It POSTs a small JSON notification describing the staged
-// artifact, including a signed fetch URL; the external service is
-// responsible for GETting that URL itself, running the real scan against
-// those bytes, and replying with a verdict before the request's deadline.
-//
-// Request body:
-//
-// {
-// "ecosystem": "npm", "name": "left-pad", "version": "1.0.0",
-// "filename": "left-pad-1.0.0.tgz", "purl": "pkg:npm/left-pad@1.0.0",
-// "content_type": "application/octet-stream", "size": 1234,
-// "fetch_url": "https://proxy.internal/_internal/scan-fetch?..."
-// }
-//
-// Response body:
-//
-// {
-// "allowed": true, "reason": "",
-// "findings": [{"severity": "high", "title": "...", "description": "..."}]
-// }
-//
-// Any compliant adapter — a trivy wrapper, a clamav-rest bridge, a Wiz
-// connector, or an in-house service — need only implement this contract.
-type HTTPScanner struct {
- name string
- url string
- headers map[string]string
- client *http.Client
-}
-
-// NewHTTPScanner creates an HTTPScanner named name that notifies url of
-// staged artifacts, attaching headers to every request (e.g. for auth).
-// If client is nil, http.DefaultClient is used.
-func NewHTTPScanner(name, url string, headers map[string]string, client *http.Client) *HTTPScanner {
- if client == nil {
- client = http.DefaultClient
- }
- return &HTTPScanner{name: name, url: url, headers: headers, client: client}
-}
-
-// Name returns the scanner's configured name.
-func (s *HTTPScanner) Name() string { return s.name }
-
-type httpScanResponse struct {
- Allowed bool `json:"allowed"`
- Reason string `json:"reason"`
- Findings []Finding `json:"findings"`
-}
-
-// Scan notifies the configured URL of req and waits for a verdict.
-func (s *HTTPScanner) Scan(ctx context.Context, req Request) (Result, error) {
- body, err := json.Marshal(req)
- if err != nil {
- return Result{}, fmt.Errorf("marshal scan request: %w", err)
- }
-
- httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, s.url, bytes.NewReader(body))
- if err != nil {
- return Result{}, fmt.Errorf("build scan request: %w", err)
- }
- httpReq.Header.Set("Content-Type", "application/json")
- for k, v := range s.headers {
- httpReq.Header.Set(k, v)
- }
-
- resp, err := s.client.Do(httpReq)
- if err != nil {
- return Result{}, fmt.Errorf("calling scanner %q: %w", s.name, err)
- }
- defer func() { _ = resp.Body.Close() }()
-
- if resp.StatusCode != http.StatusOK {
- return Result{}, fmt.Errorf("scanner %q returned status %d", s.name, resp.StatusCode)
- }
-
- var out httpScanResponse
- if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
- return Result{}, fmt.Errorf("decoding scanner %q response: %w", s.name, err)
- }
-
- return Result{
- Allowed: out.Allowed,
- Reason: out.Reason,
- Findings: out.Findings,
- }, nil
-}
diff --git a/internal/scanner/http_test.go b/internal/scanner/http_test.go
deleted file mode 100644
index 7597826..0000000
--- a/internal/scanner/http_test.go
+++ /dev/null
@@ -1,119 +0,0 @@
-package scanner
-
-import (
- "context"
- "encoding/json"
- "net/http"
- "net/http/httptest"
- "testing"
- "time"
-)
-
-func TestHTTPScanner_Scan(t *testing.T) {
- tests := []struct {
- name string
- respStatus int
- respBody string
- wantAllowed bool
- wantReason string
- wantErr bool
- }{
- {
- name: "allowed",
- respStatus: http.StatusOK,
- respBody: `{"allowed": true}`,
- wantAllowed: true,
- },
- {
- name: "blocked with reason and findings",
- respStatus: http.StatusOK,
- respBody: `{"allowed": false, "reason": "malware detected", "findings": [{"severity": "high", "title": "EICAR", "description": "test signature"}]}`,
- wantAllowed: false,
- wantReason: "malware detected",
- },
- {
- name: "non-200 status is an error",
- respStatus: http.StatusInternalServerError,
- respBody: `{}`,
- wantErr: true,
- },
- {
- name: "malformed JSON is an error",
- respStatus: http.StatusOK,
- respBody: `not json`,
- wantErr: true,
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- var got Request
- if err := json.NewDecoder(r.Body).Decode(&got); err != nil {
- t.Errorf("decode request body: %v", err)
- }
- if got.Ecosystem != "npm" || got.Name != "left-pad" || got.FetchURL == "" {
- t.Errorf("unexpected request body: %+v", got)
- }
- if r.Header.Get("Authorization") != "Bearer secret" {
- t.Errorf("missing/incorrect Authorization header: %q", r.Header.Get("Authorization"))
- }
-
- w.WriteHeader(tt.respStatus)
- _, _ = w.Write([]byte(tt.respBody))
- }))
- defer srv.Close()
-
- s := NewHTTPScanner("test", srv.URL, map[string]string{"Authorization": "Bearer secret"}, nil)
-
- result, err := s.Scan(context.Background(), Request{
- Ecosystem: "npm",
- Name: "left-pad",
- Version: "1.0.0",
- FetchURL: srv.URL + "/fetch",
- })
-
- if tt.wantErr {
- if err == nil {
- t.Fatalf("Scan() error = nil, want error")
- }
- return
- }
- if err != nil {
- t.Fatalf("Scan() unexpected error: %v", err)
- }
- if result.Allowed != tt.wantAllowed {
- t.Errorf("Allowed = %v, want %v", result.Allowed, tt.wantAllowed)
- }
- if result.Reason != tt.wantReason {
- t.Errorf("Reason = %q, want %q", result.Reason, tt.wantReason)
- }
- })
- }
-}
-
-func TestHTTPScanner_Scan_ContextTimeout(t *testing.T) {
- srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- time.Sleep(50 * time.Millisecond)
- w.WriteHeader(http.StatusOK)
- _, _ = w.Write([]byte(`{"allowed": true}`))
- }))
- defer srv.Close()
-
- s := NewHTTPScanner("slow", srv.URL, nil, nil)
-
- ctx, cancel := context.WithTimeout(context.Background(), 5*time.Millisecond)
- defer cancel()
-
- _, err := s.Scan(ctx, Request{Ecosystem: "npm", Name: "left-pad"})
- if err == nil {
- t.Fatal("Scan() error = nil, want timeout error")
- }
-}
-
-func TestHTTPScanner_Name(t *testing.T) {
- s := NewHTTPScanner("clamav", "http://example.invalid", nil, nil)
- if got := s.Name(); got != "clamav" {
- t.Errorf("Name() = %q, want %q", got, "clamav")
- }
-}
diff --git a/internal/scanner/scanner.go b/internal/scanner/scanner.go
deleted file mode 100644
index 380bb4d..0000000
--- a/internal/scanner/scanner.go
+++ /dev/null
@@ -1,65 +0,0 @@
-// Package scanner provides pluggable pre-cache artifact scanning.
-//
-// A Scanner inspects an artifact staged in the proxy's own storage before
-// it becomes visible to clients, and returns a verdict on whether it may
-// be cached. The proxy never uploads artifact bytes to a scanner directly:
-// it hands the scanner a short-lived signed URL and the scanner pulls the
-// bytes itself. See HTTPScanner for the built-in adapter that implements
-// this over a small HTTP/JSON contract, letting trivy, ClamAV, Wiz, or any
-// custom service integrate without the proxy needing built-in knowledge of
-// any specific tool.
-package scanner
-
-import "context"
-
-// Request describes a staged artifact awaiting a scan verdict.
-type Request struct {
- Ecosystem string `json:"ecosystem"`
- Name string `json:"name"`
- Version string `json:"version"`
- Filename string `json:"filename"`
- PURL string `json:"purl"`
- ContentType string `json:"content_type"`
- Size int64 `json:"size"`
-
- // FetchURL is a short-lived signed URL the scanner must GET itself to
- // retrieve the exact bytes staged in the proxy's storage.
- FetchURL string `json:"fetch_url"`
-}
-
-// Finding describes a single issue reported by a scanner.
-type Finding struct {
- Severity string
- Title string
- Description string
-}
-
-// Result is a scanner's verdict for a Request.
-type Result struct {
- Allowed bool
- Reason string
- Findings []Finding
-
- // ScannerName identifies which scanner produced this result. Set by
- // Group, not by individual Scanner implementations.
- ScannerName string
-
- // InfraError reports whether Allowed: false was forced by a scanner
- // call failing (network error, timeout, bad response) rather than an
- // actual verdict from the scanner. Set by Group. Callers that surface
- // Reason to untrusted clients must not do so when this is true: it may
- // contain raw connection errors (internal hostnames, ports) instead of
- // a verdict meant to be shown outside the proxy.
- InfraError bool
-}
-
-// Scanner is the extension point for pluggable pre-cache scanning.
-type Scanner interface {
- // Name identifies this scanner in logs and metrics.
- Name() string
-
- // Scan requests a verdict for req. Implementations must respect ctx
- // cancellation: Group cancels in-flight scans once a blocking verdict
- // has already been decided by another scanner.
- Scan(ctx context.Context, req Request) (Result, error)
-}
diff --git a/internal/server/api.go b/internal/server/api.go
index 992d736..a3ed9d8 100644
--- a/internal/server/api.go
+++ b/internal/server/api.go
@@ -12,12 +12,6 @@ import (
"github.com/go-chi/chi/v5"
)
-const (
- maxBodySize = 1 << 20 // 1 MB
- licenseCategoryUnknown = "unknown"
- defaultSortBy = "hits"
-)
-
// APIHandler provides REST endpoints for package enrichment data.
type APIHandler struct {
enrichment *enrichment.Service
@@ -135,70 +129,33 @@ type BulkResponse struct {
Packages map[string]*PackageResponse `json:"packages"`
}
-// HandlePackagePath dispatches /api/package/{ecosystem}/* to the appropriate handler.
-// Resolves namespaced package names (Composer vendor/name, npm @scope/name) from the path.
-func (h *APIHandler) HandlePackagePath(w http.ResponseWriter, r *http.Request) {
+// HandleGetPackage handles GET /api/package/{ecosystem}/{name}
+func (h *APIHandler) HandleGetPackage(w http.ResponseWriter, r *http.Request) {
ecosystem := chi.URLParam(r, "ecosystem")
- segments, err := packagePathSegments(r)
- if err != nil {
- badRequest(w, err.Error())
+ name := chi.URLParam(r, "name")
+
+ if ecosystem == "" || name == "" {
+ http.Error(w, "ecosystem and name are required", http.StatusBadRequest)
return
}
- if ecosystem == "" || len(segments) == 0 {
- badRequest(w, "ecosystem and name are required")
- return
- }
-
- // For the API, we don't have a DB to resolve names, so we use a heuristic:
- // the last segment that looks like a version (contains a digit) is the version,
- // everything before it is the name. If no version-like segment, it's all name.
- //
- // With 1 segment: package lookup (name only)
- // With 2+ segments: last segment is version, rest is name
- // Exception: if this is a namespaced ecosystem and we have exactly 2 segments,
- // it could be vendor/name with no version. The enrichment service handles
- // both cases (it will try to look up the package either way).
- if len(segments) == 1 {
- h.getPackage(w, r, ecosystem, segments[0])
- return
- }
-
- // Try the full path as a package name first via enrichment.
- // If it resolves, this is a package-only lookup.
- fullName := strings.Join(segments, "/")
- info, err := h.enrichment.EnrichPackage(r.Context(), ecosystem, fullName)
- if err == nil && info != nil {
- resp := &PackageResponse{
- Ecosystem: info.Ecosystem,
- Name: info.Name,
- LatestVersion: info.LatestVersion,
- License: info.License,
- LicenseCategory: string(h.enrichment.CategorizeLicense(info.License)),
- Description: info.Description,
- Homepage: info.Homepage,
- Repository: info.Repository,
- RegistryURL: info.RegistryURL,
+ // Handle scoped npm packages (e.g., @scope/name)
+ if strings.HasPrefix(name, "@") {
+ // The path is split, so we need to get the rest
+ rest := chi.URLParam(r, "rest")
+ if rest != "" {
+ name = name + "/" + rest
}
- writeJSON(w, resp)
- return
}
- // Otherwise, last segment is the version.
- name := strings.Join(segments[:len(segments)-1], "/")
- version := segments[len(segments)-1]
- h.getVersion(w, r, ecosystem, name, version)
-}
-
-func (h *APIHandler) getPackage(w http.ResponseWriter, r *http.Request, ecosystem, name string) {
info, err := h.enrichment.EnrichPackage(r.Context(), ecosystem, name)
if err != nil {
- writeError(w, http.StatusBadGateway, ErrCodeUpstream, "failed to enrich package")
+ http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if info == nil {
- notFound(w, "package not found")
+ http.Error(w, "package not found", http.StatusNotFound)
return
}
@@ -217,10 +174,20 @@ func (h *APIHandler) getPackage(w http.ResponseWriter, r *http.Request, ecosyste
writeJSON(w, resp)
}
-func (h *APIHandler) getVersion(w http.ResponseWriter, r *http.Request, ecosystem, name, version string) {
+// HandleGetVersion handles GET /api/package/{ecosystem}/{name}/{version}
+func (h *APIHandler) HandleGetVersion(w http.ResponseWriter, r *http.Request) {
+ ecosystem := chi.URLParam(r, "ecosystem")
+ name := chi.URLParam(r, "name")
+ version := chi.URLParam(r, "version")
+
+ if ecosystem == "" || name == "" || version == "" {
+ http.Error(w, "ecosystem, name, and version are required", http.StatusBadRequest)
+ return
+ }
+
result, err := h.enrichment.EnrichFull(r.Context(), ecosystem, name, version)
if err != nil {
- writeError(w, http.StatusBadGateway, ErrCodeUpstream, "failed to enrich version")
+ http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@@ -272,39 +239,25 @@ func (h *APIHandler) getVersion(w http.ResponseWriter, r *http.Request, ecosyste
writeJSON(w, resp)
}
-// HandleVulnsPath dispatches /api/vulns/{ecosystem}/* to the vulns handler.
-// Supports both {name} and {name}/{version} paths with namespaced package names.
-func (h *APIHandler) HandleVulnsPath(w http.ResponseWriter, r *http.Request) {
+// HandleGetVulns handles GET /api/vulns/{ecosystem}/{name}
+func (h *APIHandler) HandleGetVulns(w http.ResponseWriter, r *http.Request) {
ecosystem := chi.URLParam(r, "ecosystem")
- segments, err := packagePathSegments(r)
- if err != nil {
- badRequest(w, err.Error())
+ name := chi.URLParam(r, "name")
+ version := chi.URLParam(r, "version")
+
+ if ecosystem == "" || name == "" {
+ http.Error(w, "ecosystem and name are required", http.StatusBadRequest)
return
}
- if ecosystem == "" || len(segments) == 0 {
- badRequest(w, "ecosystem and name are required")
- return
- }
-
- // Last segment could be a version. Try full path as name first,
- // then split off the last segment as version.
- name := strings.Join(segments, "/")
- version := "0"
-
- if len(segments) > 1 {
- // Try enrichment with the full path as name.
- // If it doesn't resolve, assume last segment is version.
- info, err := h.enrichment.EnrichPackage(r.Context(), ecosystem, name)
- if err != nil || info == nil {
- name = strings.Join(segments[:len(segments)-1], "/")
- version = segments[len(segments)-1]
- }
+ // If no version specified, use "0" to get all vulnerabilities
+ if version == "" {
+ version = "0"
}
vulns, err := h.enrichment.CheckVulnerabilities(r.Context(), ecosystem, name, version)
if err != nil {
- writeError(w, http.StatusBadGateway, ErrCodeUpstream, "failed to check vulnerabilities")
+ http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@@ -330,25 +283,15 @@ func (h *APIHandler) HandleVulnsPath(w http.ResponseWriter, r *http.Request) {
}
// HandleOutdated handles POST /api/outdated
-// @Summary Check outdated packages
-// @Tags api
-// @Accept json
-// @Produce json
-// @Param request body OutdatedRequest true "Packages to check"
-// @Success 200 {object} OutdatedResponse
-// @Failure 400 {object} ErrorResponse
-// @Failure 500 {object} ErrorResponse
-// @Router /api/outdated [post]
func (h *APIHandler) HandleOutdated(w http.ResponseWriter, r *http.Request) {
- r.Body = http.MaxBytesReader(w, r.Body, maxBodySize)
var req OutdatedRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- badRequest(w, "invalid request body")
+ http.Error(w, "invalid request body", http.StatusBadRequest)
return
}
if len(req.Packages) == 0 {
- badRequest(w, "packages list is required")
+ http.Error(w, "packages list is required", http.StatusBadRequest)
return
}
@@ -376,25 +319,15 @@ func (h *APIHandler) HandleOutdated(w http.ResponseWriter, r *http.Request) {
}
// HandleBulkLookup handles POST /api/bulk
-// @Summary Bulk package lookup by PURL
-// @Tags api
-// @Accept json
-// @Produce json
-// @Param request body BulkRequest true "PURLs"
-// @Success 200 {object} BulkResponse
-// @Failure 400 {object} ErrorResponse
-// @Failure 500 {object} ErrorResponse
-// @Router /api/bulk [post]
func (h *APIHandler) HandleBulkLookup(w http.ResponseWriter, r *http.Request) {
- r.Body = http.MaxBytesReader(w, r.Body, maxBodySize)
var req BulkRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- badRequest(w, "invalid request body")
+ http.Error(w, "invalid request body", http.StatusBadRequest)
return
}
if len(req.PURLs) == 0 {
- badRequest(w, "purls list is required")
+ http.Error(w, "purls list is required", http.StatusBadRequest)
return
}
@@ -476,21 +409,12 @@ type SearchPackageResult struct {
}
// HandleSearch handles GET /api/search
-// @Summary Search cached packages
-// @Tags api
-// @Produce json
-// @Param q query string true "Query"
-// @Param ecosystem query string false "Ecosystem"
-// @Success 200 {object} SearchResponse
-// @Failure 400 {object} ErrorResponse
-// @Failure 500 {object} ErrorResponse
-// @Router /api/search [get]
func (h *APIHandler) HandleSearch(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query().Get("q")
ecosystem := r.URL.Query().Get("ecosystem")
if query == "" {
- badRequest(w, "query parameter 'q' is required")
+ http.Error(w, "query parameter 'q' is required", http.StatusBadRequest)
return
}
@@ -500,7 +424,7 @@ func (h *APIHandler) HandleSearch(w http.ResponseWriter, r *http.Request) {
// Search in database
results, err := h.db.SearchPackages(query, ecosystem, limit, (page-1)*limit)
if err != nil {
- internalError(w, "search failed")
+ http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@@ -544,7 +468,7 @@ func (h *APIHandler) HandleSearch(w http.ResponseWriter, r *http.Request) {
func writeJSON(w http.ResponseWriter, v any) {
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(v); err != nil {
- internalError(w, "failed to encode response")
+ http.Error(w, "failed to encode response", http.StatusInternalServerError)
}
}
@@ -573,32 +497,23 @@ type PackageListResult struct {
}
// HandlePackagesList handles GET /api/packages
-// @Summary List cached packages
-// @Tags api
-// @Produce json
-// @Param ecosystem query string false "Ecosystem"
-// @Param sort query string false "Sort" Enums(hits,name,size,cached_at,ecosystem,vulns)
-// @Success 200 {object} PackagesListResponse
-// @Failure 400 {object} ErrorResponse
-// @Failure 500 {object} ErrorResponse
-// @Router /api/packages [get]
func (h *APIHandler) HandlePackagesList(w http.ResponseWriter, r *http.Request) {
ecosystem := r.URL.Query().Get("ecosystem")
sortBy := r.URL.Query().Get("sort")
if sortBy == "" {
- sortBy = defaultSortBy
+ sortBy = "hits"
}
validSorts := map[string]bool{
- defaultSortBy: true,
- "name": true,
- "size": true,
- "cached_at": true,
- "ecosystem": true,
- "vulns": true,
+ "hits": true,
+ "name": true,
+ "size": true,
+ "cached_at": true,
+ "ecosystem": true,
+ "vulns": true,
}
if !validSorts[sortBy] {
- badRequest(w, "invalid sort parameter")
+ http.Error(w, "invalid sort parameter", http.StatusBadRequest)
return
}
@@ -607,7 +522,7 @@ func (h *APIHandler) HandlePackagesList(w http.ResponseWriter, r *http.Request)
packages, err := h.db.ListCachedPackages(ecosystem, sortBy, limit, (page-1)*limit)
if err != nil {
- internalError(w, "failed to list packages")
+ http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@@ -632,7 +547,7 @@ func (h *APIHandler) HandlePackagesList(w http.ResponseWriter, r *http.Request)
latestVersion = pkg.LatestVersion.String
}
license := ""
- licenseCategory := licenseCategoryUnknown
+ licenseCategory := "unknown"
if pkg.License.Valid {
license = pkg.License.String
if h.enrichment != nil {
diff --git a/internal/server/api_test.go b/internal/server/api_test.go
index 0494b2f..832f31a 100644
--- a/internal/server/api_test.go
+++ b/internal/server/api_test.go
@@ -9,16 +9,12 @@ import (
"net/http/httptest"
"os"
"path/filepath"
- "strings"
"testing"
"github.com/git-pkgs/proxy/internal/database"
"github.com/git-pkgs/proxy/internal/enrichment"
- "github.com/go-chi/chi/v5"
)
-const testEcosystemNPM = "npm"
-
func TestNewAPIHandler(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
svc := enrichment.New(logger)
@@ -32,66 +28,55 @@ func TestNewAPIHandler(t *testing.T) {
}
}
-func TestHandlePackagePath_MissingParams(t *testing.T) {
+func TestHandleGetPackage_MissingParams(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
svc := enrichment.New(logger)
h := NewAPIHandler(svc, nil)
- r := chi.NewRouter()
- r.Get("/api/package/{ecosystem}/*", h.HandlePackagePath)
-
req := httptest.NewRequest("GET", "/api/package//", nil)
+ req.SetPathValue("ecosystem", "")
+ req.SetPathValue("name", "")
+
w := httptest.NewRecorder()
- r.ServeHTTP(w, req)
+ h.HandleGetPackage(w, req)
- if w.Code != http.StatusBadRequest && w.Code != http.StatusNotFound {
- t.Errorf("expected status 400 or 404, got %d", w.Code)
+ if w.Code != http.StatusBadRequest {
+ t.Errorf("expected status %d, got %d", http.StatusBadRequest, w.Code)
}
}
-func TestHandlePackagePath_InvalidName(t *testing.T) {
+func TestHandleGetVersion_MissingParams(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
svc := enrichment.New(logger)
h := NewAPIHandler(svc, nil)
- r := chi.NewRouter()
- r.Get("/api/package/{ecosystem}/*", h.HandlePackagePath)
+ req := httptest.NewRequest("GET", "/api/package///", nil)
+ req.SetPathValue("ecosystem", "")
+ req.SetPathValue("name", "")
+ req.SetPathValue("version", "")
- tests := []struct {
- name string
- path string
- }{
- {"null byte", "/api/package/npm/lodash%00"},
- {"too long", "/api/package/npm/" + strings.Repeat("a", maxPackagePathLen+1)},
- }
+ w := httptest.NewRecorder()
+ h.HandleGetVersion(w, req)
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- req := httptest.NewRequest("GET", tt.path, nil)
- w := httptest.NewRecorder()
- r.ServeHTTP(w, req)
-
- if w.Code != http.StatusBadRequest {
- t.Errorf("expected status 400, got %d", w.Code)
- }
- })
+ if w.Code != http.StatusBadRequest {
+ t.Errorf("expected status %d, got %d", http.StatusBadRequest, w.Code)
}
}
-func TestHandleVulnsPath_MissingParams(t *testing.T) {
+func TestHandleGetVulns_MissingParams(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
svc := enrichment.New(logger)
h := NewAPIHandler(svc, nil)
- r := chi.NewRouter()
- r.Get("/api/vulns/{ecosystem}/*", h.HandleVulnsPath)
-
req := httptest.NewRequest("GET", "/api/vulns//", nil)
- w := httptest.NewRecorder()
- r.ServeHTTP(w, req)
+ req.SetPathValue("ecosystem", "")
+ req.SetPathValue("name", "")
- if w.Code != http.StatusBadRequest && w.Code != http.StatusNotFound {
- t.Errorf("expected status 400 or 404, got %d", w.Code)
+ w := httptest.NewRecorder()
+ h.HandleGetVulns(w, req)
+
+ if w.Code != http.StatusBadRequest {
+ t.Errorf("expected status %d, got %d", http.StatusBadRequest, w.Code)
}
}
@@ -109,25 +94,6 @@ func TestHandleOutdated_EmptyBody(t *testing.T) {
}
}
-func TestHandleOutdated_OversizedBody(t *testing.T) {
- logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
- svc := enrichment.New(logger)
- h := NewAPIHandler(svc, nil)
-
- // Send a body larger than 1 MB
- body := make([]byte, 2<<20)
- for i := range body {
- body[i] = 'x'
- }
- req := httptest.NewRequest("POST", "/api/outdated", bytes.NewReader(body))
- w := httptest.NewRecorder()
- h.HandleOutdated(w, req)
-
- if w.Code != http.StatusBadRequest {
- t.Errorf("expected status %d for oversized body, got %d", http.StatusBadRequest, w.Code)
- }
-}
-
func TestHandleOutdated_InvalidJSON(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
svc := enrichment.New(logger)
@@ -192,7 +158,7 @@ func TestWriteJSON(t *testing.T) {
func TestPackageResponseJSON(t *testing.T) {
resp := &PackageResponse{
- Ecosystem: testEcosystemNPM,
+ Ecosystem: "npm",
Name: "lodash",
LatestVersion: "4.17.21",
License: "MIT",
@@ -213,7 +179,7 @@ func TestPackageResponseJSON(t *testing.T) {
t.Fatalf("failed to unmarshal: %v", err)
}
- if decoded.Ecosystem != testEcosystemNPM {
+ if decoded.Ecosystem != "npm" {
t.Errorf("expected ecosystem npm, got %s", decoded.Ecosystem)
}
if decoded.Name != "lodash" {
@@ -324,7 +290,7 @@ func TestHandleSearch_WithNullValues(t *testing.T) {
pkg := &database.Package{
PURL: "pkg:npm/api-test",
- Ecosystem: testEcosystemNPM,
+ Ecosystem: "npm",
Name: "api-test",
}
if err := db.UpsertPackage(pkg); err != nil {
@@ -383,109 +349,3 @@ func TestHandleSearch_WithNullValues(t *testing.T) {
t.Errorf("expected 3 hits, got %d", result.Hits)
}
}
-
-func TestHandlePackagesListAPI(t *testing.T) {
- logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
- svc := enrichment.New(logger)
-
- dir := t.TempDir()
- dbPath := filepath.Join(dir, "test.db")
-
- db, err := database.Create(dbPath)
- if err != nil {
- t.Fatalf("Create failed: %v", err)
- }
- defer func() { _ = db.Close() }()
-
- // Seed two packages
- for _, name := range []string{"api-list-one", "api-list-two"} {
- pkg := &database.Package{
- PURL: "pkg:npm/" + name,
- Ecosystem: testEcosystemNPM,
- Name: name,
- }
- if err := db.UpsertPackage(pkg); err != nil {
- t.Fatalf("UpsertPackage failed: %v", err)
- }
- ver := &database.Version{
- PURL: "pkg:npm/" + name + "@1.0.0",
- PackagePURL: pkg.PURL,
- }
- if err := db.UpsertVersion(ver); err != nil {
- t.Fatalf("UpsertVersion failed: %v", err)
- }
- art := &database.Artifact{
- VersionPURL: ver.PURL,
- Filename: name + "-1.0.0.tgz",
- UpstreamURL: "https://registry.npmjs.org/" + name + "/-/" + name + "-1.0.0.tgz",
- StoragePath: sql.NullString{String: "/tmp/test.tgz", Valid: true},
- }
- if err := db.UpsertArtifact(art); err != nil {
- t.Fatalf("UpsertArtifact failed: %v", err)
- }
- }
-
- h := NewAPIHandler(svc, db)
-
- r := chi.NewRouter()
- r.Get("/api/packages", h.HandlePackagesList)
-
- req := httptest.NewRequest("GET", "/api/packages", nil)
- w := httptest.NewRecorder()
- r.ServeHTTP(w, req)
-
- if w.Code != http.StatusOK {
- t.Fatalf("expected status 200, got %d", w.Code)
- }
-
- var resp PackagesListResponse
- if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
- t.Fatalf("failed to decode response: %v", err)
- }
-
- if len(resp.Results) < 2 {
- t.Fatalf("expected at least 2 results, got %d", len(resp.Results))
- }
-
- if resp.SortBy != defaultSortBy {
- t.Errorf("expected default sort by hits, got %q", resp.SortBy)
- }
-
- found := false
- for _, pkg := range resp.Results {
- if pkg.Name == "api-list-one" || pkg.Name == "api-list-two" {
- found = true
- break
- }
- }
- if !found {
- t.Error("expected seeded packages in results")
- }
-}
-
-func TestHandlePackagesListAPI_InvalidSort(t *testing.T) {
- logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
- svc := enrichment.New(logger)
-
- dir := t.TempDir()
- dbPath := filepath.Join(dir, "test.db")
-
- db, err := database.Create(dbPath)
- if err != nil {
- t.Fatalf("Create failed: %v", err)
- }
- defer func() { _ = db.Close() }()
-
- h := NewAPIHandler(svc, db)
-
- r := chi.NewRouter()
- r.Get("/api/packages", h.HandlePackagesList)
-
- req := httptest.NewRequest("GET", "/api/packages?sort=invalid", nil)
- w := httptest.NewRecorder()
- r.ServeHTTP(w, req)
-
- if w.Code != http.StatusBadRequest {
- t.Errorf("expected status 400 for invalid sort, got %d", w.Code)
- }
-}
diff --git a/internal/server/breakers.go b/internal/server/breakers.go
deleted file mode 100644
index 383e0db..0000000
--- a/internal/server/breakers.go
+++ /dev/null
@@ -1,130 +0,0 @@
-package server
-
-import (
- "log/slog"
- "sync"
-
- "github.com/git-pkgs/proxy/internal/metrics"
-)
-
-// Gauge values for proxy_circuit_breaker_state. The fetcher reports only open
-// or closed, so half-open (1) is never published.
-const (
- breakerGaugeClosed = 0
- breakerGaugeOpen = 2
-)
-
-const (
- breakerStateOpen = "open"
- breakerStateClosed = "closed"
-)
-
-// breakerStateSource reports circuit breaker state per upstream registry, keyed
-// by the identifier the fetcher derives from the fetch URL, with values
-// breakerStateOpen or breakerStateClosed. Implemented by
-// fetch.CircuitBreakerFetcher.
-type breakerStateSource interface {
- GetBreakerState() map[string]string
-}
-
-// breakerMonitor mirrors the artifact fetcher's per-registry circuit breaker
-// state into Prometheus metrics, the health report, and the log.
-//
-// Breaker state lives only in the fetcher's memory. While a breaker is open
-// every artifact fetch it covers that misses the cache fails without reaching
-// the upstream, bar the one probe per backoff interval the breaker admits to
-// test recovery. That looks identical to an upstream outage from the outside:
-// metadata still serves (it does not go through the fetcher), other registries
-// still serve, and /health reports the database and storage as fine. Publishing
-// the state makes that distinguishable.
-type breakerMonitor struct {
- source breakerStateSource
- logger *slog.Logger
-
- // mu serializes snapshots. It guards seen, which holds one entry per
- // registry that has tripped at least once in this process — the only
- // registries published as metrics — and keeps each state read paired with
- // the updates it produces.
- mu sync.Mutex
- seen map[string]string
-}
-
-func newBreakerMonitor(source breakerStateSource, logger *slog.Logger) *breakerMonitor {
- if logger == nil {
- logger = slog.Default()
- }
- return &breakerMonitor{
- source: source,
- logger: logger,
- seen: map[string]string{},
- }
-}
-
-// snapshot returns the current state of every breaker the fetcher has created,
-// keyed by registry identifier, and mirrors it into the breaker metrics as a
-// side effect. It returns nil for a nil monitor so callers that build a Server
-// without a fetcher (tests) need no special case.
-//
-// The keys pass through unaltered into Prometheus labels and the /health body,
-// neither of which is authenticated, so they are only as safe to publish as the
-// fetcher makes them. It keys by the fetch URL's host, and where it cannot take
-// a host from that URL — a signed composer dist.url that fails to parse, say —
-// by an opaque keyed digest of the URL rather than the URL itself, so a
-// credential carried in one does not reach either endpoint
-// (github.com/git-pkgs/registries v0.9.0 and later).
-//
-// Only registries that have tripped at least once are published as metrics.
-// The fetcher creates a breaker per identifier it fetches under, and for some
-// ecosystems that identifier comes from upstream metadata rather than
-// configuration (composer takes it from a package's dist.url, helm from the
-// chart URLs in index.yaml), so publishing every one would let upstream content
-// grow the series count for the life of the process — the more so for URLs with
-// no host, which get an identifier apiece rather than sharing one. An
-// identifier that has never tripped carries no information a series could
-// convey; once it trips it keeps reporting, including the 0 that marks its
-// recovery. /health is a per-request response rather than a persistent series,
-// so it reports every breaker.
-//
-// Trips are counted on the closed→open transitions observed between calls,
-// because the fetcher exposes current state rather than trip events: a breaker
-// that opens and recovers entirely between two calls is not counted.
-func (m *breakerMonitor) snapshot() map[string]string {
- if m == nil || m.source == nil {
- return nil
- }
-
- m.mu.Lock()
- defer m.mu.Unlock()
-
- // Read under the lock. Two concurrent snapshots — a /health request and a
- // /metrics scrape landing during a transition — can otherwise apply their
- // reads to seen in the opposite order, counting one trip twice, logging a
- // close for a breaker that is still open, and leaving the gauge at 0 until
- // the next call.
- states := m.source.GetBreakerState()
-
- for registry, state := range states {
- previous, published := m.seen[registry]
-
- switch {
- case state == breakerStateOpen && previous != breakerStateOpen:
- metrics.RecordCircuitBreakerTrip(registry)
- m.logger.Error("circuit breaker open, artifact fetches for this registry "+
- "fail without contacting it", "registry", registry)
- case state == breakerStateClosed && previous == breakerStateOpen:
- m.logger.Info("circuit breaker closed", "registry", registry)
- case state == breakerStateClosed && !published:
- // Never tripped: nothing to publish.
- continue
- }
-
- gauge := breakerGaugeClosed
- if state == breakerStateOpen {
- gauge = breakerGaugeOpen
- }
- metrics.UpdateCircuitBreakerState(registry, gauge)
- m.seen[registry] = state
- }
-
- return states
-}
diff --git a/internal/server/breakers_test.go b/internal/server/breakers_test.go
deleted file mode 100644
index ae39759..0000000
--- a/internal/server/breakers_test.go
+++ /dev/null
@@ -1,348 +0,0 @@
-package server
-
-import (
- "context"
- "encoding/json"
- "io"
- "log/slog"
- "net/http"
- "net/http/httptest"
- "strings"
- "sync/atomic"
- "testing"
- "time"
-
- "github.com/git-pkgs/proxy/internal/metrics"
- "github.com/git-pkgs/registries/fetch"
- "github.com/prometheus/client_golang/prometheus"
- dto "github.com/prometheus/client_model/go"
-)
-
-// cbThreshold in registries/fetch: failures inside the breaker's rolling window
-// needed to trip it. The library counts the window, not a consecutive run, so
-// these fetches only have to land close together, which they do.
-const breakerTripFailures = 5
-
-// fakeBreakerSource reports breaker state without a real fetcher, so tests can
-// drive transitions that would otherwise need to wait out a 30s backoff.
-type fakeBreakerSource struct {
- states map[string]string
-}
-
-func (f *fakeBreakerSource) GetBreakerState() map[string]string {
- states := make(map[string]string, len(f.states))
- for registry, state := range f.states {
- states[registry] = state
- }
- return states
-}
-
-// newTrippedMonitor returns a monitor over a real circuit-breaker fetcher whose
-// breaker for the test server's host has been tripped by repeated 5xx
-// responses, plus that host.
-func newTrippedMonitor(t *testing.T) (*breakerMonitor, string) {
- t.Helper()
-
- upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
- w.WriteHeader(http.StatusBadGateway)
- }))
- t.Cleanup(upstream.Close)
-
- monitor, fetcher, host := newMonitorFor(t, upstream)
- for range breakerTripFailures {
- if _, err := fetcher.Fetch(context.Background(), upstream.URL+"/artifact.tgz"); err == nil {
- t.Fatal("fetch against a 502 upstream should fail")
- }
- }
- return monitor, host
-}
-
-// newMonitorFor builds a monitor over a circuit-breaker fetcher that talks to
-// srv through its own client, bypassing the SSRF dial gate that would otherwise
-// refuse the loopback address.
-func newMonitorFor(t *testing.T, srv *httptest.Server) (
- monitor *breakerMonitor, fetcher *fetch.CircuitBreakerFetcher, host string,
-) {
- t.Helper()
-
- base := fetch.NewFetcher(
- fetch.WithHTTPClient(srv.Client()),
- fetch.WithMaxRetries(0),
- )
- t.Cleanup(func() { _ = base.Close() })
-
- fetcher = fetch.NewCircuitBreakerFetcher(base)
- return newBreakerMonitor(fetcher, slog.New(slog.DiscardHandler)),
- fetcher,
- strings.TrimPrefix(srv.URL, "http://")
-}
-
-// metricValue returns the value of the series carrying registry=want, and
-// whether such a series exists at all. It collects rather than calling
-// WithLabelValues, which would create the series it is looking for.
-func metricValue(t *testing.T, collector prometheus.Collector, want string) (value float64, found bool) {
- t.Helper()
-
- ch := make(chan prometheus.Metric, 64)
- go func() {
- collector.Collect(ch)
- close(ch)
- }()
-
- for metric := range ch {
- var parsed dto.Metric
- if err := metric.Write(&parsed); err != nil {
- t.Fatalf("writing metric: %v", err)
- }
- for _, label := range parsed.GetLabel() {
- if label.GetName() != "registry" || label.GetValue() != want {
- continue
- }
- if gauge := parsed.GetGauge(); gauge != nil {
- return gauge.GetValue(), true
- }
- return parsed.GetCounter().GetValue(), true
- }
- }
- return 0, false
-}
-
-// resetSeries drops any series for host left behind by an earlier test, since
-// httptest ports can be reused within a process and the metrics registry is
-// global. Absolute trip counts stay meaningful after it.
-func resetSeries(host string) {
- metrics.CircuitBreakerState.DeleteLabelValues(host)
- metrics.CircuitBreakerTrips.DeleteLabelValues(host)
-}
-
-func TestBreakerMonitor_OpenBreakerReportedAndCounted(t *testing.T) {
- monitor, host := newTrippedMonitor(t)
- resetSeries(host)
-
- if state := monitor.snapshot()[host]; state != breakerStateOpen {
- t.Fatalf("state for %s = %q, want open", host, state)
- }
-
- gauge, found := metricValue(t, metrics.CircuitBreakerState, host)
- if !found {
- t.Fatalf("no state gauge published for %s", host)
- }
- if gauge != breakerGaugeOpen {
- t.Errorf("state gauge = %v, want %v", gauge, breakerGaugeOpen)
- }
- if trips, _ := metricValue(t, metrics.CircuitBreakerTrips, host); trips != 1 {
- t.Errorf("trips = %v, want 1", trips)
- }
-
- // A breaker that stays open is one trip, not one per scrape.
- monitor.snapshot()
- monitor.snapshot()
- if trips, _ := metricValue(t, metrics.CircuitBreakerTrips, host); trips != 1 {
- t.Errorf("trips after further snapshots = %v, want 1", trips)
- }
-}
-
-// A breaker per fetched host is created even for hosts that come from upstream
-// metadata (composer dist.url, helm chart URLs), so publishing a series for
-// every one of them would let upstream content grow the series count without
-// bound. Only registries that have actually tripped are published.
-func TestBreakerMonitor_HealthyRegistryPublishesNoSeries(t *testing.T) {
- upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
- _, _ = w.Write([]byte("artifact"))
- }))
- defer upstream.Close()
-
- monitor, fetcher, host := newMonitorFor(t, upstream)
- resetSeries(host)
-
- artifact, err := fetcher.Fetch(context.Background(), upstream.URL+"/artifact.tgz")
- if err != nil {
- t.Fatalf("fetch: %v", err)
- }
- _, _ = io.Copy(io.Discard, artifact.Body)
- _ = artifact.Body.Close()
-
- // The breaker exists and is reported to /health...
- if state := monitor.snapshot()[host]; state != breakerStateClosed {
- t.Fatalf("state for %s = %q, want closed", host, state)
- }
- // ...but carries no metric series.
- if _, found := metricValue(t, metrics.CircuitBreakerState, host); found {
- t.Errorf("state gauge published for %s, want none until it trips", host)
- }
- if _, found := metricValue(t, metrics.CircuitBreakerTrips, host); found {
- t.Errorf("trip counter published for %s, want none until it trips", host)
- }
-}
-
-// Once a registry has tripped it keeps reporting, so recovery is visible as a
-// transition to 0 rather than as a series that disappears.
-func TestBreakerMonitor_RecoveryReportedAfterTrip(t *testing.T) {
- const host = "recovering.example.com"
-
- resetSeries(host)
-
- source := &fakeBreakerSource{states: map[string]string{host: breakerStateOpen}}
- monitor := newBreakerMonitor(source, slog.New(slog.DiscardHandler))
-
- monitor.snapshot()
- if gauge, _ := metricValue(t, metrics.CircuitBreakerState, host); gauge != breakerGaugeOpen {
- t.Fatalf("state gauge = %v, want %v", gauge, breakerGaugeOpen)
- }
-
- source.states[host] = breakerStateClosed
- if state := monitor.snapshot()[host]; state != breakerStateClosed {
- t.Fatalf("state for %s = %q, want closed", host, state)
- }
- gauge, found := metricValue(t, metrics.CircuitBreakerState, host)
- if !found {
- t.Fatal("state gauge disappeared after recovery, want 0")
- }
- if gauge != breakerGaugeClosed {
- t.Errorf("state gauge = %v, want %v", gauge, breakerGaugeClosed)
- }
- if trips, _ := metricValue(t, metrics.CircuitBreakerTrips, host); trips != 1 {
- t.Errorf("trips = %v, want 1", trips)
- }
-
- // Tripping again after recovery counts a second trip.
- source.states[host] = breakerStateOpen
- monitor.snapshot()
- if trips, _ := metricValue(t, metrics.CircuitBreakerTrips, host); trips != 2 {
- t.Errorf("trips after re-trip = %v, want 2", trips)
- }
-}
-
-func TestBreakerMonitor_NilSafe(t *testing.T) {
- var nilMonitor *breakerMonitor
- if states := nilMonitor.snapshot(); states != nil {
- t.Errorf("nil monitor snapshot = %v, want nil", states)
- }
- if states := newBreakerMonitor(nil, nil).snapshot(); states != nil {
- t.Errorf("snapshot without a state source = %v, want nil", states)
- }
-}
-
-func TestHealthEndpoint_ReportsOpenBreaker(t *testing.T) {
- ts := newTestServer(t)
- defer ts.close()
-
- monitor, host := newTrippedMonitor(t)
- ts.server.breakers = monitor
-
- req := httptest.NewRequest("GET", "/health", nil)
- w := httptest.NewRecorder()
- ts.handler.ServeHTTP(w, req)
-
- // An unreachable upstream is not this proxy being unhealthy: the breaker
- // state is reported, but the probe still passes.
- if w.Code != http.StatusOK {
- t.Fatalf("status = %d, want 200; body: %s", w.Code, w.Body.String())
- }
-
- var resp HealthResponse
- if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
- t.Fatalf("decoding response: %v", err)
- }
- if resp.Status != "ok" {
- t.Errorf("status = %q, want ok", resp.Status)
- }
- if got := resp.CircuitBreakers[host]; got != breakerStateOpen {
- t.Errorf("circuit_breakers[%s] = %q, want open", host, got)
- }
-}
-
-func TestHealthEndpoint_OmitsBreakersWhenNoneExist(t *testing.T) {
- ts := newTestServer(t)
- defer ts.close()
-
- req := httptest.NewRequest("GET", "/health", nil)
- w := httptest.NewRecorder()
- ts.handler.ServeHTTP(w, req)
-
- if strings.Contains(w.Body.String(), "circuit_breakers") {
- t.Errorf("body should omit circuit_breakers when no breaker exists: %s", w.Body.String())
- }
-}
-
-// blockingBreakerSource holds each state read open until the test releases it,
-// and reports every entry, so a read that escaped the monitor lock announces
-// itself. inFlight is a second net: it catches an overlap anywhere in the test,
-// not just while the release channel is held.
-type blockingBreakerSource struct {
- states map[string]string
- entered chan struct{}
- release chan struct{}
- inFlight atomic.Int32
- overlap atomic.Bool
-}
-
-func (b *blockingBreakerSource) GetBreakerState() map[string]string {
- if b.inFlight.Add(1) > 1 {
- b.overlap.Store(true)
- }
- defer b.inFlight.Add(-1)
-
- b.entered <- struct{}{}
- <-b.release
-
- states := make(map[string]string, len(b.states))
- for registry, state := range b.states {
- states[registry] = state
- }
- return states
-}
-
-// /health requests and /metrics scrapes call snapshot concurrently. The state
-// read has to happen under the same lock as the seen updates it feeds: read
-// outside it, two snapshots straddling a transition can apply their reads to
-// seen in the opposite order and count one trip twice.
-func TestBreakerMonitor_SnapshotSerializesStateReads(t *testing.T) {
- const host = "serialized.example.com"
-
- resetSeries(host)
-
- source := &blockingBreakerSource{
- states: map[string]string{host: breakerStateOpen},
- entered: make(chan struct{}, 2),
- release: make(chan struct{}),
- }
- monitor := newBreakerMonitor(source, slog.New(slog.DiscardHandler))
-
- first := make(chan map[string]string, 1)
- go func() { first <- monitor.snapshot() }()
-
- // The first snapshot is inside GetBreakerState now, holding the lock.
- <-source.entered
-
- second := make(chan map[string]string, 1)
- go func() { second <- monitor.snapshot() }()
-
- // Nothing can release the first read, so the second cannot get past the
- // lock: an unlocked read would have queued an entry on the channel.
- select {
- case <-source.entered:
- t.Fatal("second snapshot read breaker state while the first still held the lock")
- case <-time.After(100 * time.Millisecond):
- }
-
- close(source.release)
-
- if state := (<-first)[host]; state != breakerStateOpen {
- t.Errorf("first snapshot state for %s = %q, want open", host, state)
- }
- // Only reached once the first snapshot has returned the lock.
- <-source.entered
- if state := (<-second)[host]; state != breakerStateOpen {
- t.Errorf("second snapshot state for %s = %q, want open", host, state)
- }
-
- if source.overlap.Load() {
- t.Error("two snapshots read breaker state concurrently")
- }
- // The same open breaker seen twice is one trip, which is what the paired
- // read and update buys.
- if trips, _ := metricValue(t, metrics.CircuitBreakerTrips, host); trips != 1 {
- t.Errorf("trips = %v, want 1", trips)
- }
-}
diff --git a/internal/server/browse.go b/internal/server/browse.go
index fc5e658..21f973a 100644
--- a/internal/server/browse.go
+++ b/internal/server/browse.go
@@ -1,7 +1,6 @@
package server
import (
- "bufio"
"encoding/json"
"fmt"
"io"
@@ -11,101 +10,20 @@ import (
"github.com/git-pkgs/archives"
"github.com/git-pkgs/archives/diff"
- "github.com/git-pkgs/magic"
"github.com/git-pkgs/proxy/internal/database"
- "github.com/git-pkgs/proxy/internal/handler"
+ "github.com/git-pkgs/purl"
"github.com/go-chi/chi/v5"
)
-const (
- contentTypePlainText = "text/plain; charset=utf-8"
- browseSniffSize = 512
-)
-
-// maxBrowseArchiveSize caps how much data openArchive will buffer for
-// prefix detection. Artifacts larger than this are rejected to prevent
-// memory exhaustion from a single request.
-const maxBrowseArchiveSize = 512 << 20 // 512 MB
-
-// firstBrowsableArtifact returns the first cached artifact that can be opened as
-// an archive, or nil if the version has none.
-//
-// A version's artifact list is not all archives: a PEP 658 core-metadata sidecar
-// resolves to the same name and version as the distribution it describes, so it
-// is cached under that version too. Sidecars are plain text, and because '-'
-// sorts before '.' one can even precede the real distribution in the
-// filename-ordered list, so selecting blindly would hand openArchive a file it
-// cannot parse.
-func firstBrowsableArtifact(artifacts []database.Artifact) *database.Artifact {
- for i := range artifacts {
- if artifacts[i].StoragePath.Valid && !isMetadataSidecar(artifacts[i].Filename) {
- return &artifacts[i]
- }
- }
-
- return nil
-}
-
-// isMetadataSidecar reports whether filename is a core-metadata sidecar rather
-// than a distribution archive.
-func isMetadataSidecar(filename string) bool {
- return strings.HasSuffix(filename, handler.PyPIMetadataSuffix)
-}
-
-// detectSingleRootDir returns the single top-level directory name if all files
-// in the archive live under one common directory (e.g. GitHub zipballs use
-// "repo-hash/"). Returns "" if there's no single root or the archive is flat.
-func detectSingleRootDir(reader archives.Reader) string {
- files, err := reader.List()
- if err != nil || len(files) == 0 {
+// getStripPrefix returns the path prefix to strip for a given ecosystem.
+// npm packages wrap content in a "package/" directory.
+func getStripPrefix(ecosystem string) string {
+ switch ecosystem {
+ case "npm":
+ return "package/"
+ default:
return ""
}
-
- var root string
- for _, f := range files {
- parts := strings.SplitN(f.Path, "/", 2) //nolint:mnd // split into dir + rest
- if len(parts) == 0 {
- continue
- }
- dir := parts[0]
- if root == "" {
- root = dir
- } else if dir != root {
- return ""
- }
- }
-
- if root == "" {
- return ""
- }
- return root + "/"
-}
-
-// openArchive opens a cached artifact as an archive reader, auto-detecting
-// and stripping a single top-level directory prefix (like GitHub zipballs).
-// For npm, the hardcoded "package/" prefix takes precedence.
-func openArchive(filename string, content io.Reader, ecosystem string) (archives.Reader, error) { //nolint:ireturn // wraps multiple archive implementations
- limited := io.LimitReader(content, maxBrowseArchiveSize+1)
- data, err := io.ReadAll(limited)
- if err != nil {
- return nil, fmt.Errorf("reading artifact: %w", err)
- }
- if int64(len(data)) > maxBrowseArchiveSize {
- return nil, fmt.Errorf("artifact too large for browsing (%d bytes)", len(data))
- }
-
- if ecosystem == "npm" {
- return archives.OpenBytesWithPrefix(filename, data, "package/")
- }
-
- probe, err := archives.OpenBytes(filename, data)
- if err != nil {
- return nil, err
- }
- prefix := detectSingleRootDir(probe)
- _ = probe.Close()
-
- return archives.OpenBytesWithPrefix(filename, data, prefix)
}
// BrowseListResponse contains the file listing for a directory in an archives.
@@ -125,122 +43,36 @@ type BrowseFileInfo struct {
// handleBrowseList returns a list of files in a directory within an archived package version.
// GET /api/browse/{ecosystem}/{name}/{version}?path=/some/dir
-// @Summary List files inside a cached artifact
-// @Description Lists files from the first cached artifact for a package version.
-// @Tags browse
-// @Produce json
-// @Param ecosystem path string true "Ecosystem"
-// @Param name path string true "Package name"
-// @Param version path string true "Version"
-// @Param path query string false "Directory path inside the archive"
-// @Success 200 {object} BrowseListResponse
-// @Failure 404 {object} ErrorResponse
-// @Failure 500 {object} ErrorResponse
-// @Router /ui/api/browse/{ecosystem}/{name}/{version} [get]
-// handleBrowsePath dispatches /api/browse/{ecosystem}/* to the appropriate browse handler.
-// It resolves namespaced package names by consulting the database.
-//
-// Supported paths:
-//
-// {name}/{version} -> browse list
-// {name}/{version}/file/{path} -> browse file
-func (s *Server) handleBrowsePath(w http.ResponseWriter, r *http.Request) {
+func (s *Server) handleBrowseList(w http.ResponseWriter, r *http.Request) {
ecosystem := chi.URLParam(r, "ecosystem")
- segments, err := packagePathSegments(r)
- if err != nil {
- badRequest(w, err.Error())
- return
- }
-
- if ecosystem == "" || len(segments) < 2 {
- badRequest(w, "ecosystem, name, and version required")
- return
- }
-
- // Check for /file/ in the path for browse file requests.
- fileIdx := -1
- for i, seg := range segments {
- if seg == "file" && i > 0 {
- fileIdx = i
- break
- }
- }
-
- if fileIdx >= 0 {
- // Everything before "file" is name+version, everything after is the file path.
- nameVersionSegments := segments[:fileIdx]
- filePath := strings.Join(segments[fileIdx+1:], "/")
-
- name, rest := resolvePackageName(s.db, ecosystem, nameVersionSegments)
- if name == "" && len(nameVersionSegments) >= 2 {
- name = strings.Join(nameVersionSegments[:len(nameVersionSegments)-1], "/")
- rest = nameVersionSegments[len(nameVersionSegments)-1:]
- }
- if len(rest) != 1 {
- notFound(w, "not found")
- return
- }
- s.browseFile(w, r, ecosystem, name, rest[0], filePath)
- return
- }
-
- // No /file/ segment: this is a browse list.
- name, rest := resolvePackageName(s.db, ecosystem, segments)
- if name == "" && len(segments) >= 2 {
- name = strings.Join(segments[:len(segments)-1], "/")
- rest = segments[len(segments)-1:]
- }
- if len(rest) != 1 {
- notFound(w, "not found")
- return
- }
- s.browseList(w, r, ecosystem, name, rest[0])
-}
-
-// handleComparePath dispatches /api/compare/{ecosystem}/* to the compare handler.
-// Supported paths: {name}/{fromVersion}/{toVersion}
-func (s *Server) handleComparePath(w http.ResponseWriter, r *http.Request) {
- ecosystem := chi.URLParam(r, "ecosystem")
- segments, err := packagePathSegments(r)
- if err != nil {
- badRequest(w, err.Error())
- return
- }
-
- if ecosystem == "" || len(segments) < 3 {
- badRequest(w, "ecosystem, name, fromVersion, and toVersion required")
- return
- }
-
- // The last two segments are fromVersion and toVersion.
- // Everything before that is the package name.
- name := strings.Join(segments[:len(segments)-2], "/")
- fromVersion := segments[len(segments)-2]
- toVersion := segments[len(segments)-1]
-
- s.compareDiff(w, r, ecosystem, name, fromVersion, toVersion)
-}
-
-func (s *Server) browseList(w http.ResponseWriter, r *http.Request, ecosystem, name, version string) {
+ name := chi.URLParam(r, "name")
+ version := chi.URLParam(r, "version")
dirPath := r.URL.Query().Get("path")
// Get the artifact for this version
- versionPURL := s.cachedVersionPURL(ecosystem, name, version)
+ versionPURL := purl.MakePURLString(ecosystem, name, version)
artifacts, err := s.db.GetArtifactsByVersionPURL(versionPURL)
if err != nil {
- notFound(w, "version not found")
+ http.Error(w, "version not found", http.StatusNotFound)
return
}
if len(artifacts) == 0 {
- notFound(w, "no artifacts cached")
+ http.Error(w, "no artifacts cached", http.StatusNotFound)
return
}
- cachedArtifact := firstBrowsableArtifact(artifacts)
+ // Find the first cached artifact
+ var cachedArtifact *database.Artifact
+ for i := range artifacts {
+ if artifacts[i].StoragePath.Valid {
+ cachedArtifact = &artifacts[i]
+ break
+ }
+ }
if cachedArtifact == nil {
- notFound(w, "artifact not cached")
+ http.Error(w, "artifact not cached", http.StatusNotFound)
return
}
@@ -248,16 +80,17 @@ func (s *Server) browseList(w http.ResponseWriter, r *http.Request, ecosystem, n
artifactReader, err := s.storage.Open(r.Context(), cachedArtifact.StoragePath.String)
if err != nil {
s.logger.Error("failed to read artifact from storage", "error", err)
- internalError(w, "failed to read artifact")
+ http.Error(w, "failed to read artifact", http.StatusInternalServerError)
return
}
defer func() { _ = artifactReader.Close() }()
- // Open archive with auto-detected prefix stripping
- archiveReader, err := openArchive(cachedArtifact.Filename, artifactReader, ecosystem)
+ // Open archive with appropriate prefix stripping
+ stripPrefix := getStripPrefix(ecosystem)
+ archiveReader, err := archives.OpenWithPrefix(cachedArtifact.Filename, artifactReader, stripPrefix)
if err != nil {
s.logger.Error("failed to open archive", "error", err, "filename", cachedArtifact.Filename)
- internalError(w, "failed to open archive")
+ http.Error(w, "failed to open archive", http.StatusInternalServerError)
return
}
defer func() { _ = archiveReader.Close() }()
@@ -266,7 +99,7 @@ func (s *Server) browseList(w http.ResponseWriter, r *http.Request, ecosystem, n
files, err := archiveReader.ListDir(dirPath)
if err != nil {
s.logger.Error("failed to list directory", "error", err, "path", dirPath)
- internalError(w, "failed to list directory")
+ http.Error(w, "failed to list directory", http.StatusInternalServerError)
return
}
@@ -292,42 +125,42 @@ func (s *Server) browseList(w http.ResponseWriter, r *http.Request, ecosystem, n
// handleBrowseFile returns the contents of a specific file within an archived package version.
// GET /api/browse/{ecosystem}/{name}/{version}/file/{filepath...}
-// @Summary Fetch a file inside a cached artifact
-// @Description Streams a single file from the cached artifact. The file path may contain slashes.
-// @Tags browse
-// @Produce application/octet-stream
-// @Param ecosystem path string true "Ecosystem"
-// @Param name path string true "Package name"
-// @Param version path string true "Version"
-// @Param filepath path string true "File path inside the archive"
-// @Success 200 {file} file
-// @Failure 400 {object} ErrorResponse
-// @Failure 404 {object} ErrorResponse
-// @Failure 500 {object} ErrorResponse
-// @Router /ui/api/browse/{ecosystem}/{name}/{version}/file/{filepath} [get]
-func (s *Server) browseFile(w http.ResponseWriter, r *http.Request, ecosystem, name, version, filePath string) {
+func (s *Server) handleBrowseFile(w http.ResponseWriter, r *http.Request) {
+ ecosystem := chi.URLParam(r, "ecosystem")
+ name := chi.URLParam(r, "name")
+ version := chi.URLParam(r, "version")
+
+ // Get the wildcard path
+ filePath := chi.URLParam(r, "*")
if filePath == "" {
- badRequest(w, "file path required")
+ http.Error(w, "file path required", http.StatusBadRequest)
return
}
// Get the artifact for this version
- versionPURL := s.cachedVersionPURL(ecosystem, name, version)
+ versionPURL := purl.MakePURLString(ecosystem, name, version)
artifacts, err := s.db.GetArtifactsByVersionPURL(versionPURL)
if err != nil {
- notFound(w, "version not found")
+ http.Error(w, "version not found", http.StatusNotFound)
return
}
if len(artifacts) == 0 {
- notFound(w, "no artifacts cached")
+ http.Error(w, "no artifacts cached", http.StatusNotFound)
return
}
- cachedArtifact := firstBrowsableArtifact(artifacts)
+ // Find the first cached artifact
+ var cachedArtifact *database.Artifact
+ for i := range artifacts {
+ if artifacts[i].StoragePath.Valid {
+ cachedArtifact = &artifacts[i]
+ break
+ }
+ }
if cachedArtifact == nil {
- notFound(w, "artifact not cached")
+ http.Error(w, "artifact not cached", http.StatusNotFound)
return
}
@@ -335,16 +168,17 @@ func (s *Server) browseFile(w http.ResponseWriter, r *http.Request, ecosystem, n
artifactReader, err := s.storage.Open(r.Context(), cachedArtifact.StoragePath.String)
if err != nil {
s.logger.Error("failed to read artifact from storage", "error", err)
- internalError(w, "failed to read artifact")
+ http.Error(w, "failed to read artifact", http.StatusInternalServerError)
return
}
defer func() { _ = artifactReader.Close() }()
- // Open archive with auto-detected prefix stripping
- archiveReader, err := openArchive(cachedArtifact.Filename, artifactReader, ecosystem)
+ // Open archive with appropriate prefix stripping
+ stripPrefix := getStripPrefix(ecosystem)
+ archiveReader, err := archives.OpenWithPrefix(cachedArtifact.Filename, artifactReader, stripPrefix)
if err != nil {
s.logger.Error("failed to open archive", "error", err, "filename", cachedArtifact.Filename)
- internalError(w, "failed to open archive")
+ http.Error(w, "failed to open archive", http.StatusInternalServerError)
return
}
defer func() { _ = archiveReader.Close() }()
@@ -353,128 +187,103 @@ func (s *Server) browseFile(w http.ResponseWriter, r *http.Request, ecosystem, n
fileReader, err := archiveReader.Extract(filePath)
if err != nil {
if strings.Contains(err.Error(), "not found") {
- notFound(w, "file not found")
+ http.Error(w, "file not found", http.StatusNotFound)
return
}
s.logger.Error("failed to extract file", "error", err, "path", filePath)
- internalError(w, "failed to extract file")
+ http.Error(w, "failed to extract file", http.StatusInternalServerError)
return
}
defer func() { _ = fileReader.Close() }()
- contentType, knownPath := detectContentTypeFromPath(filePath)
- var content io.Reader = fileReader
- if !knownPath {
- bufferedFile := bufio.NewReaderSize(fileReader, browseSniffSize)
- prefix, _ := bufferedFile.Peek(browseSniffSize)
- contentType = detectContentTypeFromPrefix(prefix)
- content = bufferedFile
- }
+ // Set content type based on file extension
+ contentType := detectContentType(filePath)
w.Header().Set("Content-Type", contentType)
- w.Header().Set("Content-Security-Policy", "sandbox")
- w.Header().Set("X-Content-Type-Options", "nosniff")
+ // Set filename for download
_, filename := path.Split(filePath)
w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=%q", filename))
// Stream the file
- _, _ = io.Copy(w, content)
+ _, _ = io.Copy(w, fileReader)
}
-func detectContentTypeFromPath(filename string) (string, bool) {
+// detectContentType returns an appropriate content type based on file extension.
+func detectContentType(filename string) string {
ext := strings.ToLower(path.Ext(filename))
switch ext {
// Text formats
case ".txt", ".md", ".markdown":
- return contentTypePlainText, true
- case ".html", ".htm", ".xhtml":
- return contentTypePlainText, true
+ return "text/plain; charset=utf-8"
+ case ".html", ".htm":
+ return "text/html; charset=utf-8"
case ".css":
- return "text/css; charset=utf-8", true
+ return "text/css; charset=utf-8"
case ".js", ".mjs":
- return "application/javascript; charset=utf-8", true
+ return "application/javascript; charset=utf-8"
case ".json":
- return "application/json; charset=utf-8", true
+ return "application/json; charset=utf-8"
case ".xml":
- return "application/xml; charset=utf-8", true
+ return "application/xml; charset=utf-8"
case ".yaml", ".yml":
- return "text/yaml; charset=utf-8", true
+ return "text/yaml; charset=utf-8"
case ".toml":
- return "text/toml; charset=utf-8", true
+ return "text/toml; charset=utf-8"
// Programming languages
case ".go":
- return "text/x-go; charset=utf-8", true
+ return "text/x-go; charset=utf-8"
case ".rs":
- return "text/x-rust; charset=utf-8", true
+ return "text/x-rust; charset=utf-8"
case ".py":
- return "text/x-python; charset=utf-8", true
+ return "text/x-python; charset=utf-8"
case ".rb":
- return "text/x-ruby; charset=utf-8", true
+ return "text/x-ruby; charset=utf-8"
case ".java":
- return "text/x-java; charset=utf-8", true
+ return "text/x-java; charset=utf-8"
case ".c", ".h":
- return "text/x-c; charset=utf-8", true
+ return "text/x-c; charset=utf-8"
case ".cpp", ".cc", ".cxx", ".hpp":
- return "text/x-c++; charset=utf-8", true
+ return "text/x-c++; charset=utf-8"
case ".ts":
- return "text/typescript; charset=utf-8", true
+ return "text/typescript; charset=utf-8"
case ".tsx":
- return "text/tsx; charset=utf-8", true
+ return "text/tsx; charset=utf-8"
case ".jsx":
- return "text/jsx; charset=utf-8", true
+ return "text/jsx; charset=utf-8"
case ".php":
- return "text/x-php; charset=utf-8", true
+ return "text/x-php; charset=utf-8"
// Config files
case ".conf", ".config", ".ini":
- return contentTypePlainText, true
+ return "text/plain; charset=utf-8"
case ".sh", ".bash":
- return "text/x-shellscript; charset=utf-8", true
+ return "text/x-shellscript; charset=utf-8"
case ".dockerfile":
- return "text/x-dockerfile; charset=utf-8", true
+ return "text/x-dockerfile; charset=utf-8"
// Images
case ".png":
- return "image/png", true
+ return "image/png"
case ".jpg", ".jpeg":
- return "image/jpeg", true
+ return "image/jpeg"
case ".gif":
- return "image/gif", true
+ return "image/gif"
case ".svg":
- return contentTypePlainText, true
+ return "image/svg+xml"
case ".ico":
- return "image/x-icon", true
+ return "image/x-icon"
// Archives
case ".zip", ".tar", ".gz", ".bz2", ".xz":
- return "application/octet-stream", true
+ return "application/octet-stream"
default:
+ // Try to detect if it looks like text
if isLikelyText(filename) {
- return contentTypePlainText, true
+ return "text/plain; charset=utf-8"
}
- return "", false
- }
-}
-
-func detectContentTypeFromPrefix(prefix []byte) string {
- result := magic.DetectPrefix(prefix)
- if result.Kind == magic.KindText {
- return contentTypePlainText
- }
-
- switch result.Format {
- case "png":
- return "image/png"
- case "jpeg":
- return "image/jpeg"
- case "gif":
- return "image/gif"
- case "pdf":
- return "application/pdf"
- default:
return "application/octet-stream"
}
}
@@ -503,57 +312,72 @@ func isLikelyText(filename string) bool {
}
// BrowseSourceData contains data for the browse source page.
-//
-// Version is the decoded version, for display. EscapedVersion is the same value
-// escaped as a single URL path segment and is what the links and the browse API
-// calls must use; see database.Version.EscapedVersion.
type BrowseSourceData struct {
- Layout
- Ecosystem string
- PackageName string
- Version string
- EscapedVersion string
+ Ecosystem string
+ PackageName string
+ Version string
}
-// handleBrowseSource is now showBrowseSource in server.go, dispatched via handlePackagePath.
+// handleBrowseSource renders the source code browser UI.
+// GET /package/{ecosystem}/{name}/{version}/browse
+func (s *Server) handleBrowseSource(w http.ResponseWriter, r *http.Request) {
+ ecosystem := chi.URLParam(r, "ecosystem")
+ name := chi.URLParam(r, "name")
+ version := chi.URLParam(r, "version")
+
+ data := BrowseSourceData{
+ Ecosystem: ecosystem,
+ PackageName: name,
+ Version: version,
+ }
+
+ if err := s.templates.Render(w, "browse_source", data); err != nil {
+ s.logger.Error("failed to render browse source page", "error", err)
+ http.Error(w, "internal server error", http.StatusInternalServerError)
+ }
+}
// handleCompareDiff compares two versions and returns a diff.
// GET /api/compare/{ecosystem}/{name}/{fromVersion}/{toVersion}
-// @Summary Compare two cached versions
-// @Description Returns a structured diff for two cached versions.
-// @Tags browse
-// @Produce json
-// @Param ecosystem path string true "Ecosystem"
-// @Param name path string true "Package name"
-// @Param fromVersion path string true "From version"
-// @Param toVersion path string true "To version"
-// @Success 200 {object} map[string]any
-// @Failure 404 {object} ErrorResponse
-// @Failure 500 {object} ErrorResponse
-// @Router /ui/api/compare/{ecosystem}/{name}/{fromVersion}/{toVersion} [get]
-func (s *Server) compareDiff(w http.ResponseWriter, r *http.Request, ecosystem, name, fromVersion, toVersion string) {
+func (s *Server) handleCompareDiff(w http.ResponseWriter, r *http.Request) {
+ ecosystem := chi.URLParam(r, "ecosystem")
+ name := chi.URLParam(r, "name")
+ fromVersion := chi.URLParam(r, "fromVersion")
+ toVersion := chi.URLParam(r, "toVersion")
+
// Get artifacts for both versions
- fromPURL := s.cachedVersionPURL(ecosystem, name, fromVersion)
- toPURL := s.cachedVersionPURL(ecosystem, name, toVersion)
+ fromPURL := purl.MakePURLString(ecosystem, name, fromVersion)
+ toPURL := purl.MakePURLString(ecosystem, name, toVersion)
fromArtifacts, err := s.db.GetArtifactsByVersionPURL(fromPURL)
if err != nil || len(fromArtifacts) == 0 {
- notFound(w, "from version not found or not cached")
+ http.Error(w, "from version not found or not cached", http.StatusNotFound)
return
}
toArtifacts, err := s.db.GetArtifactsByVersionPURL(toPURL)
if err != nil || len(toArtifacts) == 0 {
- notFound(w, "to version not found or not cached")
+ http.Error(w, "to version not found or not cached", http.StatusNotFound)
return
}
// Find cached artifacts
- fromArtifact := firstBrowsableArtifact(fromArtifacts)
- toArtifact := firstBrowsableArtifact(toArtifacts)
+ var fromArtifact, toArtifact *database.Artifact
+ for i := range fromArtifacts {
+ if fromArtifacts[i].StoragePath.Valid {
+ fromArtifact = &fromArtifacts[i]
+ break
+ }
+ }
+ for i := range toArtifacts {
+ if toArtifacts[i].StoragePath.Valid {
+ toArtifact = &toArtifacts[i]
+ break
+ }
+ }
if fromArtifact == nil || toArtifact == nil {
- notFound(w, "one or both versions not cached")
+ http.Error(w, "one or both versions not cached", http.StatusNotFound)
return
}
@@ -561,7 +385,7 @@ func (s *Server) compareDiff(w http.ResponseWriter, r *http.Request, ecosystem,
fromReader, err := s.storage.Open(r.Context(), fromArtifact.StoragePath.String)
if err != nil {
s.logger.Error("failed to open from artifact", "error", err)
- internalError(w, "failed to read from version")
+ http.Error(w, "failed to read from version", http.StatusInternalServerError)
return
}
defer func() { _ = fromReader.Close() }()
@@ -569,23 +393,25 @@ func (s *Server) compareDiff(w http.ResponseWriter, r *http.Request, ecosystem,
toReader, err := s.storage.Open(r.Context(), toArtifact.StoragePath.String)
if err != nil {
s.logger.Error("failed to open to artifact", "error", err)
- internalError(w, "failed to read to version")
+ http.Error(w, "failed to read to version", http.StatusInternalServerError)
return
}
defer func() { _ = toReader.Close() }()
- fromArchive, err := openArchive(fromArtifact.Filename, fromReader, ecosystem)
+ stripPrefix := getStripPrefix(ecosystem)
+
+ fromArchive, err := archives.OpenWithPrefix(fromArtifact.Filename, fromReader, stripPrefix)
if err != nil {
s.logger.Error("failed to open from archive", "error", err)
- internalError(w, "failed to open from archive")
+ http.Error(w, "failed to open from archive", http.StatusInternalServerError)
return
}
defer func() { _ = fromArchive.Close() }()
- toArchive, err := openArchive(toArtifact.Filename, toReader, ecosystem)
+ toArchive, err := archives.OpenWithPrefix(toArtifact.Filename, toReader, stripPrefix)
if err != nil {
s.logger.Error("failed to open to archive", "error", err)
- internalError(w, "failed to open to archive")
+ http.Error(w, "failed to open to archive", http.StatusInternalServerError)
return
}
defer func() { _ = toArchive.Close() }()
@@ -594,7 +420,7 @@ func (s *Server) compareDiff(w http.ResponseWriter, r *http.Request, ecosystem,
result, err := diff.Compare(fromArchive, toArchive)
if err != nil {
s.logger.Error("failed to generate diff", "error", err)
- internalError(w, "failed to generate diff")
+ http.Error(w, "failed to generate diff", http.StatusInternalServerError)
return
}
@@ -603,17 +429,40 @@ func (s *Server) compareDiff(w http.ResponseWriter, r *http.Request, ecosystem,
}
// ComparePageData contains data for the version comparison page.
-//
-// FromVersion and ToVersion are decoded, for display; the Escaped variants are
-// the path-segment form used to build the compare API URL.
type ComparePageData struct {
- Layout
- Ecosystem string
- PackageName string
- FromVersion string
- ToVersion string
- EscapedFromVersion string
- EscapedToVersion string
+ Ecosystem string
+ PackageName string
+ FromVersion string
+ ToVersion string
}
-// handleComparePage is now showComparePage in server.go, dispatched via handlePackagePath.
+// handleComparePage renders the version comparison UI.
+// GET /package/{ecosystem}/{name}/compare/{versions}
+// where {versions} is in format "fromVersion...toVersion"
+func (s *Server) handleComparePage(w http.ResponseWriter, r *http.Request) {
+ ecosystem := chi.URLParam(r, "ecosystem")
+ name := chi.URLParam(r, "name")
+ versions := chi.URLParam(r, "versions")
+
+ // Parse versions (format: "1.0.0...2.0.0")
+ parts := strings.Split(versions, "...")
+ if len(parts) != 2 {
+ http.Error(w, "invalid version format, use: version1...version2", http.StatusBadRequest)
+ return
+ }
+
+ fromVersion := parts[0]
+ toVersion := parts[1]
+
+ data := ComparePageData{
+ Ecosystem: ecosystem,
+ PackageName: name,
+ FromVersion: fromVersion,
+ ToVersion: toVersion,
+ }
+
+ if err := s.templates.Render(w, "compare_versions", data); err != nil {
+ s.logger.Error("failed to render compare page", "error", err)
+ http.Error(w, "internal server error", http.StatusInternalServerError)
+ }
+}
diff --git a/internal/server/browse_bench_test.go b/internal/server/browse_bench_test.go
deleted file mode 100644
index 840bc75..0000000
--- a/internal/server/browse_bench_test.go
+++ /dev/null
@@ -1,89 +0,0 @@
-package server
-
-import (
- "archive/tar"
- "bytes"
- "compress/gzip"
- "fmt"
- "math/rand"
- "testing"
-)
-
-func createBenchTarGz(prefix string, fileCount, fileSize int) []byte {
- rnd := rand.New(rand.NewSource(1)) //nolint:gosec
- buf := new(bytes.Buffer)
- gw := gzip.NewWriter(buf)
- tw := tar.NewWriter(gw)
-
- payload := make([]byte, fileSize)
- for i := range fileCount {
- rnd.Read(payload)
- _ = tw.WriteHeader(&tar.Header{
- Name: fmt.Sprintf("%sfile%04d.dat", prefix, i),
- Size: int64(fileSize),
- Mode: 0644,
- })
- _, _ = tw.Write(payload)
- }
- _ = tw.Close()
- _ = gw.Close()
- return buf.Bytes()
-}
-
-func BenchmarkOpenArchive(b *testing.B) {
- cases := []struct {
- name string
- ecosystem string
- filename string
- data []byte
- }{
- {"npm", "npm", "pkg.tgz", createBenchTarGz("package/", 64, 16*1024)},
- {"go", "go", "v1.2.3.tar.gz", createBenchTarGz("repo-abc123/", 64, 16*1024)},
- }
-
- for _, tc := range cases {
- b.Run(tc.name, func(b *testing.B) {
- b.SetBytes(int64(len(tc.data)))
- b.ReportAllocs()
- for b.Loop() {
- r, err := openArchive(tc.filename, bytes.NewReader(tc.data), tc.ecosystem)
- if err != nil {
- b.Fatal(err)
- }
- _ = r.Close()
- }
- })
- }
-}
-
-func BenchmarkDetectContentType(b *testing.B) {
- cases := []struct {
- name string
- filename string
- prefix []byte
- knownPath bool
- }{
- {"known-path", "README.md", nil, true},
- {"text-prefix", "artifact", bytes.Repeat([]byte("a"), browseSniffSize), false},
- {"png-prefix", "artifact", append([]byte("\x89PNG\r\n\x1a\n"), make([]byte, browseSniffSize-8)...), false},
- }
-
- for _, tc := range cases {
- b.Run(tc.name, func(b *testing.B) {
- b.ReportAllocs()
- var contentType string
- if tc.knownPath {
- for b.Loop() {
- contentType, _ = detectContentTypeFromPath(tc.filename)
- }
- } else {
- for b.Loop() {
- contentType = detectContentTypeFromPrefix(tc.prefix)
- }
- }
- if contentType == "" {
- b.Fatal("empty content type")
- }
- })
- }
-}
diff --git a/internal/server/browse_test.go b/internal/server/browse_test.go
index f4f2f9a..ed7af57 100644
--- a/internal/server/browse_test.go
+++ b/internal/server/browse_test.go
@@ -2,7 +2,6 @@ package server
import (
"archive/tar"
- "archive/zip"
"bytes"
"compress/gzip"
"database/sql"
@@ -17,8 +16,6 @@ import (
"github.com/git-pkgs/proxy/internal/database"
)
-const testArchiveName = "test.tar.gz"
-
func TestHandleBrowseList(t *testing.T) {
ts := newTestServer(t)
defer ts.close()
@@ -29,12 +26,12 @@ func TestHandleBrowseList(t *testing.T) {
if err := os.MkdirAll(artifactsDir, 0755); err != nil {
t.Fatalf("failed to create artifacts dir: %v", err)
}
- storagePath := filepath.Join(artifactsDir, testArchiveName)
+ storagePath := filepath.Join(artifactsDir, "test.tar.gz")
if err := os.WriteFile(storagePath, archiveData, 0644); err != nil {
t.Fatalf("failed to write test archive: %v", err)
}
// Storage path relative to artifacts directory
- relPath := testArchiveName
+ relPath := "test.tar.gz"
// Setup test package and artifact
pkg := &database.Package{
@@ -65,7 +62,7 @@ func TestHandleBrowseList(t *testing.T) {
}
// Test listing root directory
- req := httptest.NewRequest("GET", "/ui/api/browse/npm/test-browse/1.0.0", nil)
+ req := httptest.NewRequest("GET", "/api/browse/npm/test-browse/1.0.0", nil)
w := httptest.NewRecorder()
ts.handler.ServeHTTP(w, req)
@@ -83,7 +80,7 @@ func TestHandleBrowseList(t *testing.T) {
}
// Test listing subdirectory
- req = httptest.NewRequest("GET", "/ui/api/browse/npm/test-browse/1.0.0?path=lib", nil)
+ req = httptest.NewRequest("GET", "/api/browse/npm/test-browse/1.0.0?path=lib", nil)
w = httptest.NewRecorder()
ts.handler.ServeHTTP(w, req)
@@ -102,12 +99,12 @@ func TestHandleBrowseFile(t *testing.T) {
if err := os.MkdirAll(artifactsDir, 0755); err != nil {
t.Fatalf("failed to create artifacts dir: %v", err)
}
- storagePath := filepath.Join(artifactsDir, testArchiveName)
+ storagePath := filepath.Join(artifactsDir, "test.tar.gz")
if err := os.WriteFile(storagePath, archiveData, 0644); err != nil {
t.Fatalf("failed to write test archive: %v", err)
}
// Storage path relative to artifacts directory
- relPath := testArchiveName
+ relPath := "test.tar.gz"
// Setup test package and artifact
pkg := &database.Package{
@@ -137,44 +134,29 @@ func TestHandleBrowseFile(t *testing.T) {
t.Fatalf("failed to upsert artifact: %v", err)
}
- files := []struct {
- path string
- content string
- contentType string
- }{
- {"README.md", "# Test Package\n", contentTypePlainText},
- {"notes.data", "short text\n", contentTypePlainText},
- {"logo", "\x89PNG\r\n\x1a\nimage data", "image/png"},
- {"page", "", contentTypePlainText},
- {"misleading.txt", "\x89PNG\r\n\x1a\nimage data", contentTypePlainText},
- }
- for _, file := range files {
- t.Run(file.path, func(t *testing.T) {
- req := httptest.NewRequest("GET", "/ui/api/browse/npm/test-browse/1.0.0/file/"+file.path, nil)
- w := httptest.NewRecorder()
- ts.handler.ServeHTTP(w, req)
+ // Test fetching a file
+ req := httptest.NewRequest("GET", "/api/browse/npm/test-browse/1.0.0/file/README.md", nil)
+ w := httptest.NewRecorder()
+ ts.handler.ServeHTTP(w, req)
- if w.Code != http.StatusOK {
- t.Fatalf("expected status 200, got %d: %s", w.Code, w.Body.String())
- }
- if w.Body.String() != file.content {
- t.Errorf("unexpected file content: %q", w.Body.String())
- }
- if got := w.Header().Get("Content-Type"); got != file.contentType {
- t.Errorf("Content-Type = %q, want %q", got, file.contentType)
- }
- if got := w.Header().Get("Content-Security-Policy"); got != "sandbox" {
- t.Errorf("Content-Security-Policy = %q, want sandbox", got)
- }
- if got := w.Header().Get("X-Content-Type-Options"); got != "nosniff" {
- t.Errorf("X-Content-Type-Options = %q, want nosniff", got)
- }
- })
+ if w.Code != http.StatusOK {
+ t.Fatalf("expected status 200, got %d: %s", w.Code, w.Body.String())
+ }
+
+ body := w.Body.String()
+ if body != "# Test Package\n" {
+ t.Errorf("unexpected file content: %q", body)
+ }
+
+ // Check content type
+ contentType := w.Header().Get("Content-Type")
+ if contentType != "text/plain; charset=utf-8" {
+ t.Errorf("expected text/plain content type, got %q", contentType)
}
// Test fetching non-existent file
- req := httptest.NewRequest("GET", "/ui/api/browse/npm/test-browse/1.0.0/file/nonexistent.txt", nil)
- w := httptest.NewRecorder()
+ req = httptest.NewRequest("GET", "/api/browse/npm/test-browse/1.0.0/file/nonexistent.txt", nil)
+ w = httptest.NewRecorder()
ts.handler.ServeHTTP(w, req)
if w.Code != http.StatusNotFound {
@@ -182,70 +164,37 @@ func TestHandleBrowseFile(t *testing.T) {
}
}
-func TestBrowseContentTypePolicy(t *testing.T) {
+func TestDetectContentType(t *testing.T) {
tests := []struct {
- name string
- filename string
- prefix []byte
- expectedCT string
+ filename string
+ expectedCT string
}{
- {"text extension", "file.txt", nil, contentTypePlainText},
- {"markdown extension", "file.md", nil, contentTypePlainText},
- {"JSON extension", "file.json", nil, "application/json; charset=utf-8"},
- {"JavaScript extension", "file.js", nil, "application/javascript; charset=utf-8"},
- {"Go extension", "file.go", nil, "text/x-go; charset=utf-8"},
- {"Python extension", "file.py", nil, "text/x-python; charset=utf-8"},
- {"Rust extension", "file.rs", nil, "text/x-rust; charset=utf-8"},
- {"HTML extension", "file.html", nil, contentTypePlainText},
- {"HTM extension", "file.htm", nil, contentTypePlainText},
- {"XHTML extension", "file.xhtml", nil, contentTypePlainText},
- {"SVG extension", "file.svg", nil, contentTypePlainText},
- {"PNG extension", "file.png", nil, "image/png"},
- {"JPEG extension", "file.jpg", nil, "image/jpeg"},
- {"README", "README", nil, contentTypePlainText},
- {"LICENSE", "LICENSE", nil, contentTypePlainText},
- {"Makefile", "Makefile", nil, contentTypePlainText},
- {"gitignore", ".gitignore", nil, contentTypePlainText},
- {"unknown empty", "file.bin", nil, "application/octet-stream"},
- {"extensionless PNG", "asset", []byte("\x89PNG\r\n\x1a\n"), "image/png"},
- {"extensionless JPEG", "asset", []byte("\xff\xd8\xff"), "image/jpeg"},
- {"extensionless GIF", "asset", []byte("GIF89a"), "image/gif"},
- {"extensionless PDF", "asset", []byte("%PDF-1.7"), "application/pdf"},
- {"extensionless text", "asset", []byte("plain text\n"), contentTypePlainText},
- {"extensionless HTML", "asset", []byte(""), contentTypePlainText},
- {"extensionless XML", "asset", []byte(" "), contentTypePlainText},
- {"extensionless SVG", "asset", []byte(" "), contentTypePlainText},
- {"extensionless ZIP", "asset", []byte("PK\x03\x04"), "application/octet-stream"},
- {"extensionless binary", "asset", []byte{0, 1, 2}, "application/octet-stream"},
- {"known path wins", "file.txt", []byte("\x89PNG\r\n\x1a\n"), contentTypePlainText},
+ {"file.txt", "text/plain; charset=utf-8"},
+ {"file.md", "text/plain; charset=utf-8"},
+ {"file.json", "application/json; charset=utf-8"},
+ {"file.js", "application/javascript; charset=utf-8"},
+ {"file.go", "text/x-go; charset=utf-8"},
+ {"file.py", "text/x-python; charset=utf-8"},
+ {"file.rs", "text/x-rust; charset=utf-8"},
+ {"file.png", "image/png"},
+ {"file.jpg", "image/jpeg"},
+ {"README", "text/plain; charset=utf-8"},
+ {"LICENSE", "text/plain; charset=utf-8"},
+ {"Makefile", "text/plain; charset=utf-8"},
+ {".gitignore", "text/plain; charset=utf-8"},
+ {"file.bin", "application/octet-stream"},
}
for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- got, knownPath := detectContentTypeFromPath(tt.filename)
- if !knownPath {
- got = detectContentTypeFromPrefix(tt.prefix)
- }
+ t.Run(tt.filename, func(t *testing.T) {
+ got := detectContentType(tt.filename)
if got != tt.expectedCT {
- t.Errorf("content type for %q with prefix %q = %q, want %q", tt.filename, tt.prefix, got, tt.expectedCT)
+ t.Errorf("detectContentType(%q) = %q, want %q", tt.filename, got, tt.expectedCT)
}
})
}
}
-func TestOpenArchiveSizeLimit(t *testing.T) {
- huge := bytes.Repeat([]byte("x"), int(maxBrowseArchiveSize)+1)
- for _, eco := range []string{"npm", "go"} {
- _, err := openArchive("test.tar.gz", bytes.NewReader(huge), eco)
- if err == nil {
- t.Fatalf("%s: expected error for oversized archive, got nil", eco)
- }
- if !strings.Contains(err.Error(), "too large") {
- t.Fatalf("%s: expected 'too large' error, got: %v", eco, err)
- }
- }
-}
-
func TestIsLikelyText(t *testing.T) {
tests := []struct {
filename string
@@ -286,10 +235,6 @@ func createTestArchive(t *testing.T) []byte {
"package/lib/index.js": "module.exports = {};",
"package/lib/helper.js": "module.exports.help = () => {};",
"package/test/index.test.js": "// tests",
- "package/notes.data": "short text\n",
- "package/logo": "\x89PNG\r\n\x1a\nimage data",
- "package/page": "",
- "package/misleading.txt": "\x89PNG\r\n\x1a\nimage data",
}
for path, content := range files {
@@ -349,7 +294,7 @@ func TestBrowseNonCachedArtifact(t *testing.T) {
}
// Try to browse
- req := httptest.NewRequest("GET", "/ui/api/browse/npm/not-cached/1.0.0", nil)
+ req := httptest.NewRequest("GET", "/api/browse/npm/not-cached/1.0.0", nil)
w := httptest.NewRecorder()
ts.handler.ServeHTTP(w, req)
@@ -368,11 +313,11 @@ func TestHandleBrowseSourcePage(t *testing.T) {
if err := os.MkdirAll(artifactsDir, 0755); err != nil {
t.Fatalf("failed to create artifacts dir: %v", err)
}
- storagePath := filepath.Join(artifactsDir, testArchiveName)
+ storagePath := filepath.Join(artifactsDir, "test.tar.gz")
if err := os.WriteFile(storagePath, archiveData, 0644); err != nil {
t.Fatalf("failed to write test archive: %v", err)
}
- relPath := testArchiveName
+ relPath := "test.tar.gz"
// Setup test package and artifact
pkg := &database.Package{
@@ -403,7 +348,7 @@ func TestHandleBrowseSourcePage(t *testing.T) {
}
// Test the browse source page loads
- req := httptest.NewRequest("GET", "/ui/package/npm/test-browse/1.0.0/browse", nil)
+ req := httptest.NewRequest("GET", "/package/npm/test-browse/1.0.0/browse", nil)
w := httptest.NewRecorder()
ts.handler.ServeHTTP(w, req)
@@ -430,23 +375,6 @@ func TestHandleBrowseSourcePage(t *testing.T) {
}
}
- if !strings.Contains(body, "proxy test-version (test-commit)") {
- t.Error("browse source footer should contain proxy build information, not the package version")
- }
-
- // Check that the escapeHTML function is present for XSS protection
- if !strings.Contains(body, "function escapeHTML(str)") {
- t.Error("browse source page missing escapeHTML function for XSS protection")
- }
-
- // Check that onclick handlers use escapeHTML
- if strings.Contains(body, "onclick=\"loadFileTree('${file.path}')") {
- t.Error("browse source page has unescaped file.path in onclick handler")
- }
- if strings.Contains(body, "onclick=\"loadFile('${file.path}')") {
- t.Error("browse source page has unescaped file.path in onclick handler")
- }
-
// Check that ecosystem, package name, and version are set in JavaScript
if !strings.Contains(body, "const ecosystem = 'npm'") {
t.Error("browse source page missing ecosystem variable")
@@ -454,10 +382,8 @@ func TestHandleBrowseSourcePage(t *testing.T) {
if !strings.Contains(body, "const packageName = 'test-browse'") {
t.Error("browse source page missing packageName variable")
}
- // The version reaches the browse API as one path segment, so the page holds
- // its escaped form.
- if !strings.Contains(body, "const versionPath = '1.0.0'") {
- t.Error("browse source page missing versionPath variable")
+ if !strings.Contains(body, "const version = '1.0.0'") {
+ t.Error("browse source page missing version variable")
}
// Verify content type
@@ -542,7 +468,7 @@ func TestHandleCompareDiff(t *testing.T) {
}
// Test the compare endpoint
- req := httptest.NewRequest("GET", "/ui/api/compare/npm/test-compare/1.0.0/2.0.0", nil)
+ req := httptest.NewRequest("GET", "/api/compare/npm/test-compare/1.0.0/2.0.0", nil)
w := httptest.NewRecorder()
ts.handler.ServeHTTP(w, req)
@@ -613,7 +539,7 @@ func TestHandleComparePage(t *testing.T) {
defer ts.close()
// Test valid format with ... separator
- req := httptest.NewRequest("GET", "/ui/package/npm/test/compare/1.0.0...2.0.0", nil)
+ req := httptest.NewRequest("GET", "/package/npm/test/compare/1.0.0...2.0.0", nil)
w := httptest.NewRecorder()
ts.handler.ServeHTTP(w, req)
@@ -623,17 +549,16 @@ func TestHandleComparePage(t *testing.T) {
body := w.Body.String()
- // Check that versions are set correctly in JavaScript. The compare API takes
- // each version as a path segment, so the page holds their escaped forms.
- if !strings.Contains(body, "const fromVersionPath = '1.0.0'") {
- t.Error("page should set fromVersionPath")
+ // Check that versions are set correctly in JavaScript
+ if !strings.Contains(body, "const fromVersion = '1.0.0'") {
+ t.Error("page should set fromVersion")
}
- if !strings.Contains(body, "const toVersionPath = '2.0.0'") {
- t.Error("page should set toVersionPath")
+ if !strings.Contains(body, "const toVersion = '2.0.0'") {
+ t.Error("page should set toVersion")
}
// Test invalid format (missing separator)
- req = httptest.NewRequest("GET", "/ui/package/npm/test/compare/invalid", nil)
+ req = httptest.NewRequest("GET", "/package/npm/test/compare/invalid", nil)
w = httptest.NewRecorder()
ts.handler.ServeHTTP(w, req)
@@ -642,7 +567,7 @@ func TestHandleComparePage(t *testing.T) {
}
// Test with only one dot (should fail)
- req = httptest.NewRequest("GET", "/ui/package/npm/test/compare/1.0.0.2.0.0", nil)
+ req = httptest.NewRequest("GET", "/package/npm/test/compare/1.0.0.2.0.0", nil)
w = httptest.NewRecorder()
ts.handler.ServeHTTP(w, req)
@@ -650,256 +575,3 @@ func TestHandleComparePage(t *testing.T) {
t.Errorf("expected status 400 for invalid separator, got %d", w.Code)
}
}
-
-func TestOpenArchiveDetectsExtensionlessTarGz(t *testing.T) {
- reader, err := openArchive("artifact", bytes.NewReader(createTestArchive(t)), "npm")
- if err != nil {
- t.Fatalf("openArchive failed: %v", err)
- }
- defer func() { _ = reader.Close() }()
-
- files, err := reader.List()
- if err != nil {
- t.Fatalf("List failed: %v", err)
- }
- if len(files) == 0 {
- t.Fatal("expected files in extensionless archive")
- }
-}
-
-func TestOpenArchiveStripsSingleRootDir(t *testing.T) {
- data := createZipArchive(t, map[string]string{
- "repo-abc123/README.md": "hello",
- "repo-abc123/src/main.go": "package main",
- "repo-abc123/go.mod": "module test",
- })
- reader, err := openArchive("test.zip", bytes.NewReader(data), "composer")
- if err != nil {
- t.Fatalf("openArchive failed: %v", err)
- }
- defer func() { _ = reader.Close() }()
-
- files, err := reader.List()
- if err != nil {
- t.Fatalf("List failed: %v", err)
- }
- for _, f := range files {
- if strings.HasPrefix(f.Path, "repo-abc123/") {
- t.Errorf("file %q still has root prefix after stripping", f.Path)
- }
- }
-}
-
-func TestOpenArchiveMultipleRootDirs(t *testing.T) {
- data := createZipArchive(t, map[string]string{
- "src/main.go": "package main",
- "docs/README.md": "hello",
- })
- reader, err := openArchive("test.zip", bytes.NewReader(data), "composer")
- if err != nil {
- t.Fatalf("openArchive failed: %v", err)
- }
- defer func() { _ = reader.Close() }()
-
- files, err := reader.List()
- if err != nil {
- t.Fatalf("List failed: %v", err)
- }
- paths := make(map[string]bool)
- for _, f := range files {
- paths[f.Path] = true
- }
- if !paths["src/main.go"] {
- t.Error("expected src/main.go to remain unchanged")
- }
- if !paths["docs/README.md"] {
- t.Error("expected docs/README.md to remain unchanged")
- }
-}
-
-func TestOpenArchiveFlatNoSubdirs(t *testing.T) {
- data := createZipArchive(t, map[string]string{
- "README.md": "hello",
- "main.go": "package main",
- })
- reader, err := openArchive("test.zip", bytes.NewReader(data), "composer")
- if err != nil {
- t.Fatalf("openArchive failed: %v", err)
- }
- defer func() { _ = reader.Close() }()
-
- files, err := reader.List()
- if err != nil {
- t.Fatalf("List failed: %v", err)
- }
- paths := make(map[string]bool)
- for _, f := range files {
- paths[f.Path] = true
- }
- if !paths["README.md"] {
- t.Error("expected README.md at root")
- }
-}
-
-func TestOpenArchiveNpmUsesPackagePrefix(t *testing.T) {
- data := createTarGzArchive(t, map[string]string{
- "package/README.md": "hello",
- "package/index.js": "module.exports = {}",
- })
- reader, err := openArchive("pkg.tgz", bytes.NewReader(data), "npm")
- if err != nil {
- t.Fatalf("openArchive failed: %v", err)
- }
- defer func() { _ = reader.Close() }()
-
- files, err := reader.List()
- if err != nil {
- t.Fatalf("List failed: %v", err)
- }
- for _, f := range files {
- if strings.HasPrefix(f.Path, "package/") {
- t.Errorf("file %q still has package/ prefix", f.Path)
- }
- }
-}
-
-func TestOpenArchiveExtensionlessFilename(t *testing.T) {
- data := createZipArchive(t, map[string]string{
- "repo-hash/README.md": "hello",
- })
- reader, err := openArchive("d2e2f014ccd6ec9fae8dbe6336a4164346a2a856", bytes.NewReader(data), "composer")
- if err != nil {
- t.Fatalf("openArchive failed: %v", err)
- }
- defer func() { _ = reader.Close() }()
-
- files, err := reader.List()
- if err != nil {
- t.Fatalf("List failed: %v", err)
- }
- if len(files) == 0 {
- t.Fatal("expected files in archive")
- }
- for _, f := range files {
- if strings.HasPrefix(f.Path, "repo-hash/") {
- t.Errorf("file %q still has root prefix", f.Path)
- }
- }
-}
-
-func createZipArchive(t *testing.T, files map[string]string) []byte {
- t.Helper()
- buf := new(bytes.Buffer)
- w := zip.NewWriter(buf)
-
- for name, content := range files {
- f, err := w.Create(name)
- if err != nil {
- t.Fatalf("failed to create zip entry: %v", err)
- }
- if _, err := f.Write([]byte(content)); err != nil {
- t.Fatalf("failed to write zip content: %v", err)
- }
- }
-
- if err := w.Close(); err != nil {
- t.Fatalf("failed to close zip writer: %v", err)
- }
- return buf.Bytes()
-}
-
-func createTarGzArchive(t *testing.T, files map[string]string) []byte {
- t.Helper()
- buf := new(bytes.Buffer)
- gw := gzip.NewWriter(buf)
- tw := tar.NewWriter(gw)
-
- for name, content := range files {
- header := &tar.Header{
- Name: name,
- Size: int64(len(content)),
- Mode: 0644,
- }
- if err := tw.WriteHeader(header); err != nil {
- t.Fatalf("failed to write tar header: %v", err)
- }
- if _, err := tw.Write([]byte(content)); err != nil {
- t.Fatalf("failed to write tar content: %v", err)
- }
- }
-
- if err := tw.Close(); err != nil {
- t.Fatalf("failed to close tar writer: %v", err)
- }
- if err := gw.Close(); err != nil {
- t.Fatalf("failed to close gzip writer: %v", err)
- }
- return buf.Bytes()
-}
-
-// TestFirstBrowsableArtifact guards artifact selection against PEP 658
-// core-metadata sidecars. A sidecar resolves to the same version as the
-// distribution it describes, so it is cached under that version, but it is plain
-// text and openArchive cannot parse it.
-func TestFirstBrowsableArtifact(t *testing.T) {
- cached := func(filename string) database.Artifact {
- return database.Artifact{
- Filename: filename,
- StoragePath: sql.NullString{String: "pypi/" + filename, Valid: true},
- }
- }
- uncached := func(filename string) database.Artifact {
- return database.Artifact{Filename: filename}
- }
-
- tests := []struct {
- name string
- artifacts []database.Artifact
- want string
- }{
- {"no artifacts", nil, ""},
- {
- "sidecar only is not browsable",
- []database.Artifact{cached("foo-1.0-py3-none-any.whl.metadata")},
- "",
- },
- {
- // '-' (0x2D) sorts before '.' (0x2E), so the sidecar precedes the
- // sdist in the filename-ordered list the query returns.
- "sidecar sorting ahead of the sdist is skipped",
- []database.Artifact{cached("foo-1.0-py3-none-any.whl.metadata"), cached("foo-1.0.tar.gz")},
- "foo-1.0.tar.gz",
- },
- {
- "sidecar skipped in favour of its own wheel",
- []database.Artifact{cached("foo-1.0-py3-none-any.whl.metadata"), cached("foo-1.0-py3-none-any.whl")},
- "foo-1.0-py3-none-any.whl",
- },
- {
- "uncached archive is still not selected",
- []database.Artifact{cached("foo-1.0.tar.gz.metadata"), uncached("foo-1.0.tar.gz")},
- "",
- },
- {"plain sdist", []database.Artifact{cached("foo-1.0.tar.gz")}, "foo-1.0.tar.gz"},
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- got := firstBrowsableArtifact(tt.artifacts)
-
- if tt.want == "" {
- if got != nil {
- t.Fatalf("firstBrowsableArtifact() = %q, want nil", got.Filename)
- }
- return
- }
-
- if got == nil {
- t.Fatalf("firstBrowsableArtifact() = nil, want %q", tt.want)
- }
- if got.Filename != tt.want {
- t.Errorf("firstBrowsableArtifact() = %q, want %q", got.Filename, tt.want)
- }
- })
- }
-}
diff --git a/internal/server/dashboard.go b/internal/server/dashboard.go
index a3ac9eb..d6550bd 100644
--- a/internal/server/dashboard.go
+++ b/internal/server/dashboard.go
@@ -2,14 +2,12 @@ package server
import (
"html/template"
- "strings"
"github.com/git-pkgs/proxy/internal/database"
)
// DashboardData contains data for rendering the dashboard.
type DashboardData struct {
- Layout
Stats DashboardStats
EnrichmentStats EnrichmentStatsView
RecentPackages []PackageInfo
@@ -62,7 +60,6 @@ type RegistryConfig struct {
// PackageShowData contains data for rendering the package show page.
type PackageShowData struct {
- Layout
Package *database.Package
Versions []database.Version
Vulnerabilities []database.Vulnerability
@@ -71,7 +68,6 @@ type PackageShowData struct {
// VersionShowData contains data for rendering the version show page.
type VersionShowData struct {
- Layout
Package *database.Package
Version *database.Version
Artifacts []database.Artifact
@@ -83,7 +79,6 @@ type VersionShowData struct {
// SearchPageData contains data for rendering the search results page.
type SearchPageData struct {
- Layout
Query string
Ecosystem string
Results []SearchResultItem
@@ -109,7 +104,6 @@ type SearchResultItem struct {
// PackagesListPageData contains data for rendering the packages list page.
type PackagesListPageData struct {
- Layout
Ecosystem string
SortBy string
Results []SearchResultItem
@@ -119,96 +113,7 @@ type PackagesListPageData struct {
TotalPages int
}
-func supportedEcosystems() []string {
- // this list should be kept sorted in lexicographic order so
- // that the 'select' list in the UI will be in the expected
- // order
- return []string{
- "alpine",
- "cargo",
- "composer",
- "conan",
- "conda",
- "cran",
- "deb",
- "gem",
- "golang",
- "hex",
- "julia",
- "maven",
- "npm",
- "nuget",
- "oci",
- "pub",
- "pypi",
- "rpm",
- "swift",
- }
-}
-
-func ecosystemBadgeLabel(ecosystem string) string {
- switch ecosystem {
- case "oci":
- return "container"
- case "deb":
- return "debian"
- default:
- return ecosystem
- }
-}
-
-func ecosystemBadgeClasses(ecosystem string) string {
- base := "inline-flex items-center px-2 py-0.5 rounded text-xs font-medium"
-
- switch ecosystem {
- case "npm", "maven":
- return base + " bg-red-100 text-red-700 dark:bg-red-900/50 dark:text-red-300"
- case "cargo":
- return base + " bg-orange-100 text-orange-700 dark:bg-orange-900/50 dark:text-orange-300"
- case "gem":
- return base + " bg-pink-100 text-pink-700 dark:bg-pink-900/50 dark:text-pink-300"
- case "go":
- return base + " bg-cyan-100 text-cyan-700 dark:bg-cyan-900/50 dark:text-cyan-300"
- case "hex":
- return base + " bg-purple-100 text-purple-700 dark:bg-purple-900/50 dark:text-purple-300"
- case "pub":
- return base + " bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300"
- case "pypi":
- return base + " bg-yellow-100 text-yellow-700 dark:bg-yellow-900/50 dark:text-yellow-300"
- case "nuget":
- return base + " bg-indigo-100 text-indigo-700 dark:bg-indigo-900/50 dark:text-indigo-300"
- case "composer":
- return base + " bg-violet-100 text-violet-700 dark:bg-violet-900/50 dark:text-violet-300"
- case "conan":
- return base + " bg-teal-100 text-teal-700 dark:bg-teal-900/50 dark:text-teal-300"
- case "conda":
- return base + " bg-green-100 text-green-700 dark:bg-green-900/50 dark:text-green-300"
- case "cran":
- return base + " bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300"
- case "julia":
- return base + " bg-emerald-100 text-emerald-700 dark:bg-emerald-900/50 dark:text-emerald-300"
- case "swift":
- return base + " bg-orange-100 text-orange-700 dark:bg-orange-900/50 dark:text-orange-300"
- case "oci":
- return base + " bg-sky-100 text-sky-700 dark:bg-sky-900/50 dark:text-sky-300"
- case "deb":
- return base + " bg-red-100 text-red-800 dark:bg-red-900/50 dark:text-red-300"
- case "rpm":
- return base + " bg-amber-100 text-amber-800 dark:bg-amber-900/50 dark:text-amber-300"
- case "alpine":
- return base + " bg-lime-100 text-lime-800 dark:bg-lime-900/50 dark:text-lime-300"
- default:
- return base + " bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300"
- }
-}
-
func getRegistryConfigs(baseURL string) []RegistryConfig {
- swiftInsecureFlag := ""
- if strings.HasPrefix(strings.ToLower(baseURL), "http://") {
- swiftInsecureFlag = "--allow-insecure-http "
- }
- dockerHost := strings.TrimPrefix(strings.TrimPrefix(baseURL, "https://"), "http://")
-
return []RegistryConfig{
{
ID: "npm",
@@ -307,20 +212,6 @@ index-url = ` + baseURL + `/pypi/simple/`),
</mirror>
</mirrors>
</settings>`),
- },
- {
- ID: "gradle",
- Name: "Gradle Build Cache",
- Language: "Java/Kotlin",
- Endpoint: "/gradle/",
- Instructions: template.HTML(`Configure Gradle to use the proxy for HttpBuildCache:
-// In settings.gradle(.kts)
-buildCache {
- remote<HttpBuildCache> {
- url = uri("` + baseURL + `/gradle/")
- push = true
- }
-} `),
},
{
ID: "nuget",
@@ -398,26 +289,6 @@ local({
r["CRAN"] <- "` + baseURL + `/cran"
options(repos = r)
})`),
- },
- {
- ID: "julia",
- Name: "Julia",
- Language: "Julia",
- Endpoint: "/julia/",
- Instructions: template.HTML(`Set the Pkg server before starting Julia:
-export JULIA_PKG_SERVER=` + baseURL + `/julia
-Or inside a running session:
-ENV["JULIA_PKG_SERVER"] = "` + baseURL + `/julia"
-using Pkg; Pkg.update() `),
- },
- {
- ID: "swift",
- Name: "Swift Package Registry",
- Language: "Swift",
- Endpoint: "/swift/",
- Instructions: template.HTML(`Configure SwiftPM to use the proxy for this project:
-swift package-registry set ` + swiftInsecureFlag + baseURL + `/swift
-Use scoped package identifiers in Package.swift, for example apple.swift-argument-parser.
`),
},
{
ID: "oci",
@@ -434,7 +305,7 @@ using Pkg; Pkg.update()`),
sudo systemctl restart docker
# Or pull directly
-docker pull ` + dockerHost + `/library/nginx:latest`),
+docker pull ` + baseURL[8:] + `/library/nginx:latest`),
},
{
ID: "deb",
@@ -466,18 +337,5 @@ gpgcheck=0
sudo dnf clean all
sudo dnf update`),
},
- {
- ID: "apk",
- Name: "Alpine APK",
- Language: "Alpine Linux",
- Endpoint: "/apk/",
- Instructions: template.HTML(`Configure apk to use the proxy:
-# In /etc/apk/repositories
-` + baseURL + `/apk/alpine/v3.22/main
-` + baseURL + `/apk/alpine/v3.22/community
-
-# Then run:
-apk update `),
- },
}
}
diff --git a/internal/server/ecr_auth.go b/internal/server/ecr_auth.go
deleted file mode 100644
index ea46a32..0000000
--- a/internal/server/ecr_auth.go
+++ /dev/null
@@ -1,189 +0,0 @@
-package server
-
-import (
- "context"
- "log/slog"
- "net/url"
- "strings"
- "sync"
- "time"
-
- awsconfig "github.com/aws/aws-sdk-go-v2/config"
- "github.com/aws/aws-sdk-go-v2/service/ecr"
- "golang.org/x/sync/singleflight"
-)
-
-const (
- ecrTokenTimeout = 10 * time.Second
- ecrTokenSkew = 5 * time.Minute
- ecrTokenFailureBackoff = 30 * time.Second
- ecrDefaultTokenLifetime = 12 * time.Hour
-)
-
-// ecrTokens caches AWS ECR authorization tokens per region and refreshes them
-// on demand when they expire. Tokens are obtained via the AWS SDK default
-// credential chain, so IAM roles for service accounts, instance profiles, and
-// environment credentials all work without extra configuration.
-type ecrTokens struct {
- logger *slog.Logger
-
- mu sync.Mutex
- cache map[string]ecrToken
- sf singleflight.Group
-
- // getToken fetches a fresh authorization token for the given region and
- // returns the raw base64 "AWS:password" value plus its expiry. Overridable
- // in tests.
- getToken func(ctx context.Context, region string) (string, time.Time, error)
-}
-
-type ecrToken struct {
- value string
- refreshAt time.Time
- expiresAt time.Time
-}
-
-func newECRTokens(logger *slog.Logger) *ecrTokens {
- return &ecrTokens{
- logger: logger,
- cache: make(map[string]ecrToken),
- getToken: fetchECRToken,
- }
-}
-
-// header returns an Authorization header for the given region, fetching and
-// caching a token on first use and shortly before expiry. Concurrent refreshes
-// for the same region share a single GetAuthorizationToken call. If a refresh
-// fails, a cached token remains available until its actual expiry and another
-// refresh is delayed briefly.
-func (e *ecrTokens) header(region string) (name, value string) {
- if tok, ok := e.fresh(region); ok {
- return tok.header()
- }
-
- v, err, _ := e.sf.Do(region, func() (any, error) {
- if tok, ok := e.fresh(region); ok {
- return tok, nil
- }
-
- ctx, cancel := context.WithTimeout(context.Background(), ecrTokenTimeout)
- defer cancel()
-
- raw, expiresAt, err := e.getToken(ctx, region)
- if err != nil {
- e.logger.Error("fetching ECR authorization token", "region", region, "error", err)
- return e.cacheFailure(region), nil
- }
- if raw == "" {
- e.logger.Error("ECR authorization token response was empty", "region", region)
- return e.cacheFailure(region), nil
- }
-
- tok := ecrToken{
- value: "Basic " + raw,
- refreshAt: expiresAt.Add(-ecrTokenSkew),
- expiresAt: expiresAt,
- }
- e.store(region, tok)
- return tok, nil
- })
- if err != nil {
- return "", ""
- }
-
- return v.(ecrToken).header()
-}
-
-func (t ecrToken) header() (name, value string) {
- if t.value == "" {
- return "", ""
- }
- return "Authorization", t.value
-}
-
-func (e *ecrTokens) fresh(region string) (ecrToken, bool) {
- tok, ok := e.cached(region)
- return tok, ok && time.Now().Before(tok.refreshAt)
-}
-
-func (e *ecrTokens) cacheFailure(region string) ecrToken {
- now := time.Now()
- retryAt := now.Add(ecrTokenFailureBackoff)
- tok, ok := e.cached(region)
- if ok && now.Before(tok.expiresAt) {
- if retryAt.After(tok.expiresAt) {
- retryAt = tok.expiresAt
- }
- tok.refreshAt = retryAt
- } else {
- tok = ecrToken{refreshAt: retryAt, expiresAt: retryAt}
- }
- e.store(region, tok)
- return tok
-}
-
-func (e *ecrTokens) cached(region string) (ecrToken, bool) {
- e.mu.Lock()
- tok, ok := e.cache[region]
- e.mu.Unlock()
- return tok, ok
-}
-
-func (e *ecrTokens) store(region string, tok ecrToken) {
- e.mu.Lock()
- e.cache[region] = tok
- e.mu.Unlock()
-}
-
-func ecrRegion(rawURL string) string {
- parsed, err := url.Parse(rawURL)
- if err != nil {
- return ""
- }
-
- labels := strings.Split(strings.ToLower(parsed.Hostname()), ".")
- for i := 1; i < len(labels); i++ {
- if labels[i] == "dkr" && i+4 < len(labels) && (labels[i+1] == "ecr" || labels[i+1] == "ecr-fips") {
- region := labels[i+2]
- suffix := strings.Join(labels[i+3:], ".")
- if region != "" && (suffix == "amazonaws.com" || suffix == "amazonaws.com.cn") {
- return region
- }
- }
-
- if (labels[i] == "dkr-ecr" || labels[i] == "dkr-ecr-fips") && i+3 < len(labels) {
- region := labels[i+1]
- if region != "" && strings.Join(labels[i+2:], ".") == "on.aws" {
- return region
- }
- }
- }
-
- return ""
-}
-
-func fetchECRToken(ctx context.Context, region string) (string, time.Time, error) {
- var opts []func(*awsconfig.LoadOptions) error
- if region != "" {
- opts = append(opts, awsconfig.WithRegion(region))
- }
- cfg, err := awsconfig.LoadDefaultConfig(ctx, opts...)
- if err != nil {
- return "", time.Time{}, err
- }
-
- out, err := ecr.NewFromConfig(cfg).GetAuthorizationToken(ctx, &ecr.GetAuthorizationTokenInput{})
- if err != nil {
- return "", time.Time{}, err
- }
- if len(out.AuthorizationData) == 0 || out.AuthorizationData[0].AuthorizationToken == nil {
- return "", time.Time{}, nil
- }
-
- data := out.AuthorizationData[0]
- expiresAt := time.Now().Add(ecrDefaultTokenLifetime)
- if data.ExpiresAt != nil {
- expiresAt = *data.ExpiresAt
- }
- return *data.AuthorizationToken, expiresAt, nil
-}
diff --git a/internal/server/ecr_auth_test.go b/internal/server/ecr_auth_test.go
deleted file mode 100644
index 904792b..0000000
--- a/internal/server/ecr_auth_test.go
+++ /dev/null
@@ -1,330 +0,0 @@
-package server
-
-import (
- "context"
- "errors"
- "io"
- "log/slog"
- "net/http"
- "net/http/httptest"
- "sync"
- "sync/atomic"
- "testing"
- "time"
-
- "github.com/git-pkgs/proxy/internal/config"
- "github.com/git-pkgs/proxy/internal/httpclient"
-)
-
-func testECRTokens() *ecrTokens {
- return newECRTokens(slog.New(slog.NewTextHandler(io.Discard, nil)))
-}
-
-func TestECRTokensCachesUntilExpiry(t *testing.T) {
- e := testECRTokens()
- calls := 0
- e.getToken = func(_ context.Context, region string) (string, time.Time, error) {
- calls++
- if region != "eu-west-1" {
- t.Errorf("region = %q, want eu-west-1", region)
- }
- return "QVdTOnNlY3JldA==", time.Now().Add(12 * time.Hour), nil
- }
-
- name, value := e.header("eu-west-1")
- if name != "Authorization" || value != "Basic QVdTOnNlY3JldA==" {
- t.Fatalf("header() = %q, %q", name, value)
- }
-
- e.header("eu-west-1")
- e.header("eu-west-1")
- if calls != 1 {
- t.Fatalf("getToken called %d times, want 1", calls)
- }
-}
-
-func TestECRTokensRefreshesWithinSkew(t *testing.T) {
- e := testECRTokens()
- calls := 0
- e.getToken = func(_ context.Context, _ string) (string, time.Time, error) {
- calls++
- return "dG9rZW4=", time.Now().Add(time.Minute), nil
- }
-
- e.header("us-east-1")
- e.header("us-east-1")
- if calls != 2 {
- t.Fatalf("getToken called %d times, want 2 (token within skew window)", calls)
- }
-}
-
-func TestECRTokensUsesValidCachedTokenWhenRefreshFails(t *testing.T) {
- e := testECRTokens()
- calls := 0
- e.cache["eu-west-1"] = ecrToken{
- value: "Basic Y2FjaGVk",
- refreshAt: time.Now().Add(-time.Minute),
- expiresAt: time.Now().Add(time.Minute),
- }
- e.getToken = func(_ context.Context, _ string) (string, time.Time, error) {
- calls++
- return "", time.Time{}, errors.New("ECR unavailable")
- }
-
- name, value := e.header("eu-west-1")
- if name != "Authorization" || value != "Basic Y2FjaGVk" {
- t.Fatalf("header() = %q, %q; want cached token", name, value)
- }
- e.header("eu-west-1")
- if calls != 1 {
- t.Fatalf("getToken called %d times, want 1 during failure backoff", calls)
- }
-}
-
-func TestECRTokensRejectsExpiredCachedTokenWhenRefreshFails(t *testing.T) {
- e := testECRTokens()
- calls := 0
- e.cache["eu-west-1"] = ecrToken{
- value: "Basic ZXhwaXJlZA==",
- refreshAt: time.Now().Add(-2 * time.Minute),
- expiresAt: time.Now().Add(-time.Minute),
- }
- e.getToken = func(_ context.Context, _ string) (string, time.Time, error) {
- calls++
- return "", time.Time{}, errors.New("ECR unavailable")
- }
-
- name, value := e.header("eu-west-1")
- if name != "" || value != "" {
- t.Fatalf("header() = %q, %q; want empty for expired token", name, value)
- }
- e.header("eu-west-1")
- if calls != 1 {
- t.Fatalf("getToken called %d times, want 1 during failure backoff", calls)
- }
-}
-
-func TestECRTokensPerRegion(t *testing.T) {
- e := testECRTokens()
- seen := map[string]int{}
- e.getToken = func(_ context.Context, region string) (string, time.Time, error) {
- seen[region]++
- return region + "-token", time.Now().Add(time.Hour), nil
- }
-
- e.header("eu-west-1")
- e.header("us-east-1")
- e.header("eu-west-1")
-
- if seen["eu-west-1"] != 1 || seen["us-east-1"] != 1 {
- t.Fatalf("per-region calls = %v, want one each", seen)
- }
-}
-
-func TestECRTokensConcurrentMissesShareOneFetch(t *testing.T) {
- e := testECRTokens()
- var calls atomic.Int32
- started := make(chan struct{})
- release := make(chan struct{})
- var startedOnce sync.Once
- e.getToken = func(_ context.Context, _ string) (string, time.Time, error) {
- calls.Add(1)
- startedOnce.Do(func() { close(started) })
- <-release
- return "dG9rZW4=", time.Now().Add(time.Hour), nil
- }
-
- const n = 10
- var wg sync.WaitGroup
- wg.Add(n)
- for range n {
- go func() {
- defer wg.Done()
- name, value := e.header("eu-west-1")
- if name != "Authorization" || value != "Basic dG9rZW4=" {
- t.Errorf("header() = %q, %q", name, value)
- }
- }()
- }
-
- <-started
- time.Sleep(100 * time.Millisecond)
- close(release)
- wg.Wait()
-
- if got := calls.Load(); got != 1 {
- t.Fatalf("getToken called %d times, want 1", got)
- }
-}
-
-func TestECRTokensBacksOffAfterError(t *testing.T) {
- e := testECRTokens()
- calls := 0
- e.getToken = func(_ context.Context, _ string) (string, time.Time, error) {
- calls++
- return "", time.Time{}, errors.New("no credentials")
- }
-
- name, value := e.header("eu-west-1")
- if name != "" || value != "" {
- t.Fatalf("header() = %q, %q; want empty on error", name, value)
- }
- e.header("eu-west-1")
- if calls != 1 {
- t.Fatalf("getToken called %d times, want 1 during failure backoff", calls)
- }
-
- tok, ok := e.cached("eu-west-1")
- if !ok {
- t.Fatal("failure was not cached")
- }
- tok.refreshAt = time.Now().Add(-time.Second)
- tok.expiresAt = tok.refreshAt
- e.store("eu-west-1", tok)
- e.header("eu-west-1")
- if calls != 2 {
- t.Fatalf("getToken called %d times, want retry after failure backoff", calls)
- }
-}
-
-func TestECRAuthFailureReturnsBasicChallengeResponse(t *testing.T) {
- e := testECRTokens()
- var tokenRequests atomic.Int32
- e.getToken = func(_ context.Context, _ string) (string, time.Time, error) {
- tokenRequests.Add(1)
- return "", time.Time{}, errors.New("no credentials")
- }
-
- var upstreamRequests atomic.Int32
- upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- upstreamRequests.Add(1)
- if got := r.Header.Get("Authorization"); got != "" {
- t.Errorf("Authorization = %q, want empty", got)
- }
- w.Header().Set("WWW-Authenticate", `Basic realm="Amazon ECR"`)
- w.WriteHeader(http.StatusUnauthorized)
- }))
- defer upstream.Close()
-
- s := &Server{
- ecr: e,
- cfg: &config.Config{Upstream: config.UpstreamConfig{
- Auth: map[string]config.AuthConfig{
- upstream.URL: {Type: "ecr"},
- },
- }},
- }
- client := &http.Client{Transport: httpclient.NewTransport(http.DefaultTransport, s.authForURL)}
-
- resp, err := client.Get(upstream.URL + "/v2/repo/manifests/latest")
- if err != nil {
- t.Fatalf("GET upstream: %v", err)
- }
- defer func() { _ = resp.Body.Close() }()
- if resp.StatusCode != http.StatusUnauthorized {
- t.Errorf("status = %d, want %d", resp.StatusCode, http.StatusUnauthorized)
- }
- if got := resp.Header.Get("WWW-Authenticate"); got != `Basic realm="Amazon ECR"` {
- t.Errorf("WWW-Authenticate = %q, want Basic challenge", got)
- }
- if got := tokenRequests.Load(); got != 1 {
- t.Errorf("token requests = %d, want 1", got)
- }
- if got := upstreamRequests.Load(); got != 1 {
- t.Errorf("upstream requests = %d, want 1", got)
- }
-}
-
-func TestECRRegion(t *testing.T) {
- tests := []struct {
- name string
- url string
- want string
- }{
- {"commercial", "https://123456789012.dkr.ecr.eu-west-1.amazonaws.com/v2/repo", "eu-west-1"},
- {"China", "https://123456789012.dkr.ecr.cn-north-1.amazonaws.com.cn/v2/repo", "cn-north-1"},
- {"GovCloud", "https://123456789012.dkr.ecr.us-gov-west-1.amazonaws.com/v2/repo", "us-gov-west-1"},
- {"dual-stack", "https://123456789012.dkr-ecr.us-west-2.on.aws/v2/repo", "us-west-2"},
- {"FIPS", "https://123456789012.dkr.ecr-fips.us-east-1.amazonaws.com/v2/repo", "us-east-1"},
- {"FIPS dual-stack", "https://123456789012.dkr-ecr-fips.us-east-1.on.aws/v2/repo", "us-east-1"},
- {"case insensitive", "https://123456789012.DKR.ECR.EU-WEST-1.AMAZONAWS.COM/v2/repo", "eu-west-1"},
- {"lookalike suffix", "https://123456789012.dkr.ecr.eu-west-1.amazonaws.com.example/v2/repo", ""},
- {"not ECR", "https://registry.example.com/v2/repo", ""},
- {"invalid URL", "://invalid", ""},
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- if got := ecrRegion(tt.url); got != tt.want {
- t.Errorf("ecrRegion(%q) = %q, want %q", tt.url, got, tt.want)
- }
- })
- }
-}
-
-func TestAuthForURLInfersECRRegion(t *testing.T) {
- e := testECRTokens()
- e.getToken = func(_ context.Context, region string) (string, time.Time, error) {
- if region != "eu-west-1" {
- t.Errorf("region = %q, want eu-west-1", region)
- }
- return "QVdTOnNlY3JldA==", time.Now().Add(time.Hour), nil
- }
-
- s := &Server{
- ecr: e,
- cfg: &config.Config{Upstream: config.UpstreamConfig{
- Auth: map[string]config.AuthConfig{
- "https://123456789012.dkr.ecr.eu-west-1.amazonaws.com": {Type: "ecr"},
- },
- }},
- }
-
- name, value := s.authForURL("https://123456789012.dkr.ecr.eu-west-1.amazonaws.com/v2/repo")
- if name != "Authorization" || value != "Basic QVdTOnNlY3JldA==" {
- t.Fatalf("authForURL() = %q, %q", name, value)
- }
-}
-
-func TestAuthForURLRoutesECRType(t *testing.T) {
- e := testECRTokens()
- e.getToken = func(_ context.Context, region string) (string, time.Time, error) {
- if region != "eu-west-1" {
- t.Errorf("region = %q, want eu-west-1", region)
- }
- return "QVdTOnNlY3JldA==", time.Now().Add(time.Hour), nil
- }
-
- s := &Server{
- ecr: e,
- cfg: &config.Config{
- Upstream: config.UpstreamConfig{
- Auth: map[string]config.AuthConfig{
- "https://123456789012.dkr.ecr.eu-west-1.amazonaws.com": {
- Type: "ecr",
- Region: "eu-west-1",
- },
- "https://ghcr.io": {
- Type: "bearer",
- Token: "ghcr-token",
- },
- },
- },
- },
- }
-
- name, value := s.authForURL("https://123456789012.dkr.ecr.eu-west-1.amazonaws.com/v2/my/repo/manifests/latest")
- if name != "Authorization" || value != "Basic QVdTOnNlY3JldA==" {
- t.Fatalf("ecr authForURL() = %q, %q", name, value)
- }
-
- name, value = s.authForURL("https://ghcr.io/v2/owner/repo/blobs/sha256:abc")
- if name != "Authorization" || value != "Bearer ghcr-token" {
- t.Fatalf("bearer authForURL() = %q, %q", name, value)
- }
-
- name, value = s.authForURL("https://registry-1.docker.io/v2/")
- if name != "" || value != "" {
- t.Fatalf("unmatched authForURL() = %q, %q; want empty", name, value)
- }
-}
diff --git a/internal/server/errors.go b/internal/server/errors.go
deleted file mode 100644
index 474ecd7..0000000
--- a/internal/server/errors.go
+++ /dev/null
@@ -1,42 +0,0 @@
-package server
-
-import (
- "encoding/json"
- "net/http"
-)
-
-// Error codes returned in API error responses. These are stable identifiers
-// that clients can match on; the message text is for humans and may change.
-const (
- ErrCodeBadRequest = "BAD_REQUEST"
- ErrCodeNotFound = "NOT_FOUND"
- ErrCodeUpstream = "UPSTREAM_ERROR"
- ErrCodeInternal = "INTERNAL_ERROR"
-)
-
-// ErrorResponse is the JSON body returned for API errors.
-type ErrorResponse struct {
- Code string `json:"code"`
- Message string `json:"message"`
-}
-
-// writeError sends a JSON error response with the given status, code and
-// user-facing message. Internal error details should be logged separately
-// by the caller, never passed as the message.
-func writeError(w http.ResponseWriter, status int, code, message string) {
- w.Header().Set("Content-Type", "application/json")
- w.WriteHeader(status)
- _ = json.NewEncoder(w).Encode(ErrorResponse{Code: code, Message: message})
-}
-
-func badRequest(w http.ResponseWriter, message string) {
- writeError(w, http.StatusBadRequest, ErrCodeBadRequest, message)
-}
-
-func notFound(w http.ResponseWriter, message string) {
- writeError(w, http.StatusNotFound, ErrCodeNotFound, message)
-}
-
-func internalError(w http.ResponseWriter, message string) {
- writeError(w, http.StatusInternalServerError, ErrCodeInternal, message)
-}
diff --git a/internal/server/errors_test.go b/internal/server/errors_test.go
deleted file mode 100644
index c660ae2..0000000
--- a/internal/server/errors_test.go
+++ /dev/null
@@ -1,93 +0,0 @@
-package server
-
-import (
- "encoding/json"
- "net/http"
- "net/http/httptest"
- "testing"
-)
-
-func TestWriteError(t *testing.T) {
- tests := []struct {
- name string
- fn func(w http.ResponseWriter)
- status int
- code string
- message string
- }{
- {
- name: "badRequest",
- fn: func(w http.ResponseWriter) { badRequest(w, "missing field") },
- status: http.StatusBadRequest,
- code: ErrCodeBadRequest,
- message: "missing field",
- },
- {
- name: "notFound",
- fn: func(w http.ResponseWriter) { notFound(w, "package not found") },
- status: http.StatusNotFound,
- code: ErrCodeNotFound,
- message: "package not found",
- },
- {
- name: "internalError",
- fn: func(w http.ResponseWriter) { internalError(w, "boom") },
- status: http.StatusInternalServerError,
- code: ErrCodeInternal,
- message: "boom",
- },
- {
- name: "upstream",
- fn: func(w http.ResponseWriter) {
- writeError(w, http.StatusBadGateway, ErrCodeUpstream, "registry unreachable")
- },
- status: http.StatusBadGateway,
- code: ErrCodeUpstream,
- message: "registry unreachable",
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- w := httptest.NewRecorder()
- tt.fn(w)
-
- if w.Code != tt.status {
- t.Errorf("status = %d, want %d", w.Code, tt.status)
- }
- if ct := w.Header().Get("Content-Type"); ct != "application/json" {
- t.Errorf("Content-Type = %q, want application/json", ct)
- }
-
- var resp ErrorResponse
- if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
- t.Fatalf("response body is not valid JSON: %v (body: %q)", err, w.Body.String())
- }
- if resp.Code != tt.code {
- t.Errorf("code = %q, want %q", resp.Code, tt.code)
- }
- if resp.Message != tt.message {
- t.Errorf("message = %q, want %q", resp.Message, tt.message)
- }
- })
- }
-}
-
-func TestAPIErrorResponseShape(t *testing.T) {
- w := httptest.NewRecorder()
- badRequest(w, "x")
-
- var raw map[string]any
- if err := json.Unmarshal(w.Body.Bytes(), &raw); err != nil {
- t.Fatalf("invalid JSON: %v", err)
- }
- if _, ok := raw["code"]; !ok {
- t.Error("response missing 'code' field")
- }
- if _, ok := raw["message"]; !ok {
- t.Error("response missing 'message' field")
- }
- if len(raw) != 2 {
- t.Errorf("response has unexpected fields: %v", raw)
- }
-}
diff --git a/internal/server/eviction.go b/internal/server/eviction.go
deleted file mode 100644
index 4173bd5..0000000
--- a/internal/server/eviction.go
+++ /dev/null
@@ -1,105 +0,0 @@
-package server
-
-import (
- "context"
- "log/slog"
- "time"
-
- "github.com/git-pkgs/proxy/internal/database"
- "github.com/git-pkgs/proxy/internal/storage"
-)
-
-const (
- evictionInterval = 1 * time.Minute
- evictionBatch = 50
-)
-
-func (s *Server) startEvictionLoop(ctx context.Context) {
- maxSize := s.cfg.ParseMaxSize()
- if maxSize <= 0 {
- return
- }
-
- s.logger.Info("cache eviction enabled", "max_size", s.cfg.Storage.MaxSize)
-
- ticker := time.NewTicker(evictionInterval)
- defer ticker.Stop()
-
- s.runEviction(ctx, maxSize)
-
- for {
- select {
- case <-ctx.Done():
- return
- case <-ticker.C:
- s.runEviction(ctx, maxSize)
- }
- }
-}
-
-func (s *Server) runEviction(ctx context.Context, maxSize int64) {
- evictLRU(ctx, s.db, s.storage, s.logger, maxSize)
-}
-
-func evictLRU(ctx context.Context, db *database.DB, store storage.Storage, logger *slog.Logger, maxSize int64) {
- totalSize, err := db.GetTotalCacheSize()
- if err != nil {
- logger.Warn("eviction: failed to get cache size", "error", err)
- return
- }
-
- if totalSize <= maxSize {
- return
- }
-
- logger.Info("eviction: cache size exceeds limit, evicting",
- "current_size", totalSize, "max_size", maxSize)
-
- evicted := 0
- freedBytes := int64(0)
-
- for totalSize-freedBytes > maxSize {
- artifacts, err := db.GetLeastRecentlyUsedArtifacts(evictionBatch)
- if err != nil {
- logger.Warn("eviction: failed to get LRU artifacts", "error", err)
- return
- }
- if len(artifacts) == 0 {
- break
- }
-
- for _, art := range artifacts {
- if totalSize-freedBytes <= maxSize {
- break
- }
-
- if !art.StoragePath.Valid {
- continue
- }
-
- if err := store.Delete(ctx, art.StoragePath.String); err != nil {
- logger.Warn("eviction: failed to delete from storage",
- "path", art.StoragePath.String, "error", err)
- continue
- }
-
- if err := db.ClearArtifactCache(art.VersionPURL, art.Filename); err != nil {
- logger.Warn("eviction: failed to clear artifact record",
- "version_purl", art.VersionPURL, "filename", art.Filename, "error", err)
- continue
- }
-
- size := int64(0)
- if art.Size.Valid {
- size = art.Size.Int64
- }
- freedBytes += size
- evicted++
- }
- }
-
- if evicted > 0 {
- logger.Info("eviction: completed",
- "evicted", evicted, "freed_bytes", freedBytes)
- }
-}
diff --git a/internal/server/eviction_test.go b/internal/server/eviction_test.go
deleted file mode 100644
index bac3325..0000000
--- a/internal/server/eviction_test.go
+++ /dev/null
@@ -1,295 +0,0 @@
-package server
-
-import (
- "context"
- "database/sql"
- "io"
- "log/slog"
- "path/filepath"
- "strings"
- "testing"
- "time"
-
- "github.com/git-pkgs/proxy/internal/config"
- "github.com/git-pkgs/proxy/internal/database"
- "github.com/git-pkgs/proxy/internal/storage"
-)
-
-func setupEvictionTest(t *testing.T) (*database.DB, *storage.Blob) {
- t.Helper()
-
- tempDir := t.TempDir()
- dbPath := filepath.Join(tempDir, "test.db")
- storagePath := filepath.Join(tempDir, "artifacts")
-
- db, err := database.Create(dbPath)
- if err != nil {
- t.Fatalf("failed to create database: %v", err)
- }
-
- store, err := storage.OpenBucket(context.Background(), "file://"+storagePath)
- if err != nil {
- _ = db.Close()
- t.Fatalf("failed to create storage: %v", err)
- }
- blob, ok := store.(*storage.Blob)
- if !ok {
- _ = db.Close()
- t.Fatalf("OpenBucket returned %T, want *storage.Blob", store)
- }
-
- t.Cleanup(func() {
- _ = db.Close()
- })
-
- return db, blob
-}
-
-func seedArtifact(t *testing.T, ctx context.Context, db *database.DB, store storage.Storage, name string, dataSize int, accessedAt time.Time) {
- t.Helper()
-
- pkgPURL := "pkg:npm/" + name
- versionPURL := pkgPURL + "@1.0.0"
- filename := name + "-1.0.0.tgz"
-
- if err := db.UpsertPackage(&database.Package{
- PURL: pkgPURL,
- Ecosystem: "npm",
- Name: name,
- }); err != nil {
- t.Fatalf("failed to upsert package: %v", err)
- }
-
- if err := db.UpsertVersion(&database.Version{
- PURL: versionPURL,
- PackagePURL: pkgPURL,
- }); err != nil {
- t.Fatalf("failed to upsert version: %v", err)
- }
-
- storagePath := storage.ArtifactPath("npm", "", name, "1.0.0", filename)
- data := strings.NewReader(strings.Repeat("x", dataSize))
- size, hash, err := store.Store(ctx, storagePath, data)
- if err != nil {
- t.Fatalf("failed to store artifact: %v", err)
- }
-
- if err := db.UpsertArtifact(&database.Artifact{
- VersionPURL: versionPURL,
- Filename: filename,
- UpstreamURL: "https://example.com/" + filename,
- StoragePath: sql.NullString{String: storagePath, Valid: true},
- ContentHash: sql.NullString{String: hash, Valid: true},
- Size: sql.NullInt64{Int64: size, Valid: true},
- ContentType: sql.NullString{String: "application/gzip", Valid: true},
- FetchedAt: sql.NullTime{Time: time.Now(), Valid: true},
- LastAccessedAt: sql.NullTime{Time: accessedAt, Valid: true},
- }); err != nil {
- t.Fatalf("failed to upsert artifact: %v", err)
- }
-}
-
-func TestEvictLRU_NoEvictionWhenUnderLimit(t *testing.T) {
- db, store := setupEvictionTest(t)
- ctx := context.Background()
- logger := slog.New(slog.NewTextHandler(io.Discard, nil))
-
- seedArtifact(t, ctx, db, store, "pkg-a", 100, time.Now())
-
- evictLRU(ctx, db, store, logger, 1024)
-
- count, err := db.GetCachedArtifactCount()
- if err != nil {
- t.Fatalf("failed to get count: %v", err)
- }
- if count != 1 {
- t.Errorf("expected 1 cached artifact, got %d", count)
- }
-}
-
-func TestEvictLRU_EvictsOldestFirst(t *testing.T) {
- db, store := setupEvictionTest(t)
- ctx := context.Background()
- logger := slog.New(slog.NewTextHandler(io.Discard, nil))
-
- now := time.Now()
- seedArtifact(t, ctx, db, store, "old-pkg", 500, now.Add(-3*time.Hour))
- seedArtifact(t, ctx, db, store, "mid-pkg", 500, now.Add(-1*time.Hour))
- seedArtifact(t, ctx, db, store, "new-pkg", 500, now)
-
- // Total is 1500 bytes, limit to 1100 so only the oldest gets evicted
- evictLRU(ctx, db, store, logger, 1100)
-
- // old-pkg should be evicted
- art, err := db.GetArtifact("pkg:npm/old-pkg@1.0.0", "old-pkg-1.0.0.tgz")
- if err != nil {
- t.Fatalf("failed to get artifact: %v", err)
- }
- if art.StoragePath.Valid {
- t.Error("expected old-pkg to be evicted (storage_path should be NULL)")
- }
-
- // mid-pkg and new-pkg should remain
- art, err = db.GetArtifact("pkg:npm/mid-pkg@1.0.0", "mid-pkg-1.0.0.tgz")
- if err != nil {
- t.Fatalf("failed to get artifact: %v", err)
- }
- if !art.StoragePath.Valid {
- t.Error("expected mid-pkg to remain cached")
- }
-
- art, err = db.GetArtifact("pkg:npm/new-pkg@1.0.0", "new-pkg-1.0.0.tgz")
- if err != nil {
- t.Fatalf("failed to get artifact: %v", err)
- }
- if !art.StoragePath.Valid {
- t.Error("expected new-pkg to remain cached")
- }
-
- // Storage file should be removed for old-pkg
- storagePath := storage.ArtifactPath("npm", "", "old-pkg", "1.0.0", "old-pkg-1.0.0.tgz")
- exists, _ := store.Exists(ctx, storagePath)
- if exists {
- t.Error("expected old-pkg file to be deleted from storage")
- }
-}
-
-func TestEvictLRU_EvictsMultipleToGetUnderLimit(t *testing.T) {
- db, store := setupEvictionTest(t)
- ctx := context.Background()
- logger := slog.New(slog.NewTextHandler(io.Discard, nil))
-
- now := time.Now()
- seedArtifact(t, ctx, db, store, "pkg-1", 400, now.Add(-4*time.Hour))
- seedArtifact(t, ctx, db, store, "pkg-2", 400, now.Add(-3*time.Hour))
- seedArtifact(t, ctx, db, store, "pkg-3", 400, now.Add(-2*time.Hour))
- seedArtifact(t, ctx, db, store, "pkg-4", 400, now)
-
- // Total is 1600 bytes, limit to 900 so pkg-1 and pkg-2 get evicted
- evictLRU(ctx, db, store, logger, 900)
-
- count, err := db.GetCachedArtifactCount()
- if err != nil {
- t.Fatalf("failed to get count: %v", err)
- }
- if count != 2 {
- t.Errorf("expected 2 cached artifacts remaining, got %d", count)
- }
-
- // Verify the right ones remain
- for _, name := range []string{"pkg-3", "pkg-4"} {
- art, err := db.GetArtifact("pkg:npm/"+name+"@1.0.0", name+"-1.0.0.tgz")
- if err != nil {
- t.Fatalf("failed to get artifact %s: %v", name, err)
- }
- if !art.StoragePath.Valid {
- t.Errorf("expected %s to remain cached", name)
- }
- }
-}
-
-func TestEvictLRU_NothingToEvictWhenEmpty(t *testing.T) {
- db, store := setupEvictionTest(t)
- ctx := context.Background()
- logger := slog.New(slog.NewTextHandler(io.Discard, nil))
-
- // Should not panic or error with no artifacts
- evictLRU(ctx, db, store, logger, 1024)
-
- count, err := db.GetCachedArtifactCount()
- if err != nil {
- t.Fatalf("failed to get count: %v", err)
- }
- if count != 0 {
- t.Errorf("expected 0 cached artifacts, got %d", count)
- }
-}
-
-func TestEvictLRU_StorageFileDeleted(t *testing.T) {
- db, store := setupEvictionTest(t)
- ctx := context.Background()
- logger := slog.New(slog.NewTextHandler(io.Discard, nil))
-
- seedArtifact(t, ctx, db, store, "delete-me", 1000, time.Now().Add(-1*time.Hour))
-
- storagePath := storage.ArtifactPath("npm", "", "delete-me", "1.0.0", "delete-me-1.0.0.tgz")
- exists, _ := store.Exists(ctx, storagePath)
- if !exists {
- t.Fatal("expected artifact file to exist before eviction")
- }
-
- evictLRU(ctx, db, store, logger, 500)
-
- exists, _ = store.Exists(ctx, storagePath)
- if exists {
- t.Error("expected artifact file to be deleted after eviction")
- }
-
- art, err := db.GetArtifact("pkg:npm/delete-me@1.0.0", "delete-me-1.0.0.tgz")
- if err != nil {
- t.Fatalf("failed to get artifact: %v", err)
- }
- if art.StoragePath.Valid {
- t.Error("expected storage_path to be NULL after eviction")
- }
- if art.Size.Valid {
- t.Error("expected size to be NULL after eviction")
- }
-}
-
-func TestStartEvictionLoop_UnlimitedSkips(t *testing.T) {
- tempDir := t.TempDir()
- dbPath := filepath.Join(tempDir, "test.db")
- storagePath := filepath.Join(tempDir, "artifacts")
-
- db, err := database.Create(dbPath)
- if err != nil {
- t.Fatalf("failed to create database: %v", err)
- }
- defer func() { _ = db.Close() }()
-
- store, err := storage.OpenBucket(context.Background(), "file://"+storagePath)
- if err != nil {
- t.Fatalf("failed to create storage: %v", err)
- }
-
- cfg := defaultTestConfig(storagePath, dbPath)
-
- s := &Server{
- cfg: cfg,
- db: db,
- storage: store,
- logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
- }
-
- ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
- defer cancel()
-
- // Should return immediately since max_size is empty (unlimited)
- done := make(chan struct{})
- go func() {
- s.startEvictionLoop(ctx)
- close(done)
- }()
-
- select {
- case <-done:
- // Good, returned immediately
- case <-time.After(1 * time.Second):
- t.Error("startEvictionLoop should return immediately when max_size is unlimited")
- cancel()
- }
-}
-
-func defaultTestConfig(storagePath, dbPath string) *config.Config {
- return &config.Config{
- Listen: ":8080",
- BaseURL: "http://localhost:8080",
- Storage: config.StorageConfig{URL: "file://" + storagePath, MaxSize: ""},
- Database: config.DatabaseConfig{
- Driver: "sqlite",
- Path: dbPath,
- },
- Log: config.LogConfig{Level: "info", Format: "text"},
- }
-}
diff --git a/internal/server/gradle_cache_eviction.go b/internal/server/gradle_cache_eviction.go
deleted file mode 100644
index 7f546d1..0000000
--- a/internal/server/gradle_cache_eviction.go
+++ /dev/null
@@ -1,149 +0,0 @@
-package server
-
-import (
- "context"
- "fmt"
- "sort"
- "time"
-
- "github.com/git-pkgs/proxy/internal/storage"
-)
-
-const gradleBuildCacheStoragePrefix = "_gradle/http-build-cache/"
-
-type gradleBuildCacheLister interface {
- ListPrefix(ctx context.Context, prefix string) ([]storage.ObjectInfo, error)
-}
-
-func (s *Server) startGradleBuildCacheEviction(ctx context.Context) {
- maxAge := s.cfg.ParseGradleBuildCacheMaxAge()
- maxSize := s.cfg.ParseGradleBuildCacheMaxSize()
- if maxAge <= 0 && maxSize <= 0 {
- return
- }
-
- lister, ok := s.storage.(gradleBuildCacheLister)
- if !ok {
- s.logger.Warn("gradle cache eviction is enabled, but storage backend cannot list objects")
- return
- }
-
- interval := s.cfg.ParseGradleBuildCacheSweepInterval()
- s.logger.Info("gradle cache eviction enabled",
- "max_age", maxAge,
- "max_size_bytes", maxSize,
- "interval", interval)
-
- sweep := func() {
- deletedCount, freedBytes, err := sweepGradleBuildCache(ctx, s.storage, lister, maxAge, maxSize, time.Now())
- if err != nil {
- s.logger.Warn("gradle cache eviction sweep failed", "error", err)
- return
- }
- if deletedCount > 0 {
- s.logger.Info("gradle cache eviction sweep completed",
- "deleted_entries", deletedCount,
- "freed_bytes", freedBytes)
- }
- }
-
- sweep()
-
- go func() {
- ticker := time.NewTicker(interval)
- defer ticker.Stop()
-
- for {
- select {
- case <-ctx.Done():
- return
- case <-ticker.C:
- sweep()
- }
- }
- }()
-}
-
-func sweepGradleBuildCache(
- ctx context.Context,
- store storage.Storage,
- lister gradleBuildCacheLister,
- maxAge time.Duration,
- maxSize int64,
- now time.Time,
-) (int, int64, error) {
- entries, err := lister.ListPrefix(ctx, gradleBuildCacheStoragePrefix)
- if err != nil {
- return 0, 0, fmt.Errorf("listing gradle cache entries: %w", err)
- }
-
- if len(entries) == 0 {
- return 0, 0, nil
- }
-
- sortOldestFirst(entries)
-
- deletedCount := 0
- freedBytes := int64(0)
- var firstDeleteErr error
-
- deleteEntry := func(entry storage.ObjectInfo) bool {
- if err := store.Delete(ctx, entry.Path); err != nil {
- if firstDeleteErr == nil {
- firstDeleteErr = err
- }
- return false
- }
- deletedCount++
- freedBytes += entry.Size
- return true
- }
-
- remaining := entries
- if maxAge > 0 {
- cutoff := now.Add(-maxAge)
- kept := make([]storage.ObjectInfo, 0, len(entries))
-
- for _, entry := range entries {
- if !entry.ModTime.IsZero() && entry.ModTime.Before(cutoff) {
- if deleteEntry(entry) {
- continue
- }
- }
- kept = append(kept, entry)
- }
-
- remaining = kept
- }
-
- if maxSize > 0 {
- totalSize := int64(0)
- for _, entry := range remaining {
- totalSize += entry.Size
- }
-
- for _, entry := range remaining {
- if totalSize <= maxSize {
- break
- }
- if deleteEntry(entry) {
- totalSize -= entry.Size
- }
- }
- }
-
- if firstDeleteErr != nil {
- return deletedCount, freedBytes, fmt.Errorf("deleting gradle cache entries: %w", firstDeleteErr)
- }
-
- return deletedCount, freedBytes, nil
-}
-
-func sortOldestFirst(entries []storage.ObjectInfo) {
- sort.Slice(entries, func(i, j int) bool {
- if entries[i].ModTime.Equal(entries[j].ModTime) {
- return entries[i].Path < entries[j].Path
- }
- return entries[i].ModTime.Before(entries[j].ModTime)
- })
-}
diff --git a/internal/server/gradle_cache_eviction_test.go b/internal/server/gradle_cache_eviction_test.go
deleted file mode 100644
index 4e97507..0000000
--- a/internal/server/gradle_cache_eviction_test.go
+++ /dev/null
@@ -1,138 +0,0 @@
-package server
-
-import (
- "bytes"
- "context"
- "io"
- "strings"
- "testing"
- "time"
-
- "github.com/git-pkgs/proxy/internal/storage"
-)
-
-type fakeGradleCacheStore struct {
- objects map[string]storage.ObjectInfo
-}
-
-func newFakeGradleCacheStore(objects []storage.ObjectInfo) *fakeGradleCacheStore {
- m := make(map[string]storage.ObjectInfo, len(objects))
- for _, obj := range objects {
- m[obj.Path] = obj
- }
- return &fakeGradleCacheStore{objects: m}
-}
-
-func (s *fakeGradleCacheStore) Store(_ context.Context, path string, r io.Reader) (int64, string, error) {
- data, _ := io.ReadAll(r)
- s.objects[path] = storage.ObjectInfo{Path: path, Size: int64(len(data)), ModTime: time.Now()}
- return int64(len(data)), "", nil
-}
-
-func (s *fakeGradleCacheStore) Open(_ context.Context, path string) (io.ReadCloser, error) {
- obj, ok := s.objects[path]
- if !ok {
- return nil, storage.ErrNotFound
- }
- return io.NopCloser(bytes.NewReader(make([]byte, obj.Size))), nil
-}
-
-func (s *fakeGradleCacheStore) Exists(_ context.Context, path string) (bool, error) {
- _, ok := s.objects[path]
- return ok, nil
-}
-
-func (s *fakeGradleCacheStore) Delete(_ context.Context, path string) error {
- delete(s.objects, path)
- return nil
-}
-
-func (s *fakeGradleCacheStore) Size(_ context.Context, path string) (int64, error) {
- obj, ok := s.objects[path]
- if !ok {
- return 0, storage.ErrNotFound
- }
- return obj.Size, nil
-}
-
-func (s *fakeGradleCacheStore) SignedURL(_ context.Context, _ string, _ time.Duration) (string, error) {
- return "", storage.ErrSignedURLUnsupported
-}
-
-func (s *fakeGradleCacheStore) UsedSpace(_ context.Context) (int64, error) {
- var total int64
- for _, obj := range s.objects {
- total += obj.Size
- }
- return total, nil
-}
-
-func (s *fakeGradleCacheStore) URL() string { return "mem://" }
-
-func (s *fakeGradleCacheStore) Close() error { return nil }
-
-func (s *fakeGradleCacheStore) ListPrefix(_ context.Context, prefix string) ([]storage.ObjectInfo, error) {
- objects := make([]storage.ObjectInfo, 0)
- for _, obj := range s.objects {
- if strings.HasPrefix(obj.Path, prefix) {
- objects = append(objects, obj)
- }
- }
- return objects, nil
-}
-
-func TestSweepGradleBuildCache_MaxAge(t *testing.T) {
- now := time.Date(2026, 4, 27, 12, 0, 0, 0, time.UTC)
- store := newFakeGradleCacheStore([]storage.ObjectInfo{
- {Path: "_gradle/http-build-cache/old", Size: 10, ModTime: now.Add(-48 * time.Hour)},
- {Path: "_gradle/http-build-cache/new", Size: 10, ModTime: now.Add(-2 * time.Hour)},
- })
-
- deleted, freed, err := sweepGradleBuildCache(context.Background(), store, store, 24*time.Hour, 0, now)
- if err != nil {
- t.Fatalf("sweepGradleBuildCache() error = %v", err)
- }
- if deleted != 1 {
- t.Fatalf("deleted entries = %d, want 1", deleted)
- }
- if freed != 10 {
- t.Fatalf("freed bytes = %d, want 10", freed)
- }
-
- if _, ok := store.objects["_gradle/http-build-cache/old"]; ok {
- t.Fatal("old entry was not deleted")
- }
- if _, ok := store.objects["_gradle/http-build-cache/new"]; !ok {
- t.Fatal("new entry should remain")
- }
-}
-
-func TestSweepGradleBuildCache_MaxSizeOldestFirst(t *testing.T) {
- now := time.Date(2026, 4, 27, 12, 0, 0, 0, time.UTC)
- store := newFakeGradleCacheStore([]storage.ObjectInfo{
- {Path: "_gradle/http-build-cache/a", Size: 5, ModTime: now.Add(-3 * time.Hour)},
- {Path: "_gradle/http-build-cache/b", Size: 5, ModTime: now.Add(-2 * time.Hour)},
- {Path: "_gradle/http-build-cache/c", Size: 5, ModTime: now.Add(-1 * time.Hour)},
- })
-
- deleted, freed, err := sweepGradleBuildCache(context.Background(), store, store, 0, 10, now)
- if err != nil {
- t.Fatalf("sweepGradleBuildCache() error = %v", err)
- }
- if deleted != 1 {
- t.Fatalf("deleted entries = %d, want 1", deleted)
- }
- if freed != 5 {
- t.Fatalf("freed bytes = %d, want 5", freed)
- }
-
- if _, ok := store.objects["_gradle/http-build-cache/a"]; ok {
- t.Fatal("oldest entry was not deleted")
- }
- if _, ok := store.objects["_gradle/http-build-cache/b"]; !ok {
- t.Fatal("middle entry should remain")
- }
- if _, ok := store.objects["_gradle/http-build-cache/c"]; !ok {
- t.Fatal("newest entry should remain")
- }
-}
diff --git a/internal/server/health.go b/internal/server/health.go
deleted file mode 100644
index 9533bd5..0000000
--- a/internal/server/health.go
+++ /dev/null
@@ -1,190 +0,0 @@
-// Package server implements the proxy HTTP server.
-package server
-
-import (
- "bytes"
- "context"
- "crypto/rand"
- "encoding/hex"
- "errors"
- "fmt"
- "io"
- "log/slog"
- "strconv"
- "sync"
- "time"
-
- "github.com/git-pkgs/proxy/internal/metrics"
- "github.com/git-pkgs/proxy/internal/storage"
-)
-
-const (
- probePathPrefix = ".healthcheck/"
- probeMarker = "proxy-healthcheck:"
- probeSuffixBytes = 8
- defaultProbeTTL = 30 * time.Second
- defaultProbeTimeout = 10 * time.Second
-)
-
-// HealthResponse is the JSON payload returned by /health.
-type HealthResponse struct {
- Status string `json:"status"`
- Checks map[string]HealthCheck `json:"checks"`
- // CircuitBreakers reports the state ("open" or "closed") of each upstream
- // registry's artifact-fetch circuit breaker, keyed by the host fetched from
- // or, where the fetch URL has none to read, by an opaque placeholder
- // standing in for it. It is omitted when no breaker has been created yet.
- // An open breaker fails every artifact fetch it covers without contacting
- // the upstream, but says nothing about this proxy's own health, so it does
- // not change Status.
- CircuitBreakers map[string]string `json:"circuit_breakers,omitempty"`
-}
-
-// HealthCheck reports the status of a single subsystem check.
-type HealthCheck struct {
- Status string `json:"status"`
- Error string `json:"error,omitempty"`
- Step string `json:"step,omitempty"`
-}
-
-// probeError tags a storage probe failure with the step that failed.
-type probeError struct {
- step string
- err error
-}
-
-func (e *probeError) Error() string { return e.step + ": " + e.err.Error() }
-func (e *probeError) Unwrap() error { return e.err }
-
-// storageProbe runs a write → size-check → read → verify → delete round-trip
-// against the storage backend. Returns nil on success or a *probeError on failure.
-func storageProbe(ctx context.Context, s storage.Storage) (err error) {
- suffix, suffixErr := randomSuffix()
- if suffixErr != nil {
- return &probeError{step: "write", err: fmt.Errorf("generating random suffix: %w", suffixErr)}
- }
- path := probePathPrefix + strconv.FormatInt(time.Now().UnixNano(), 10) + "-" + suffix
- payload := []byte(probeMarker + suffix)
-
- // 1. Store
- size, _, storeErr := s.Store(ctx, path, bytes.NewReader(payload))
- if storeErr != nil {
- return &probeError{step: "write", err: storeErr}
- }
- // After Store succeeds, always attempt to delete on the way out so probe
- // objects don't accumulate when a later step (size/open/read/verify) fails.
- // Delete is reported as the primary error only if no earlier failure
- // already set one.
- defer func() {
- if delErr := s.Delete(ctx, path); delErr != nil && err == nil {
- err = &probeError{step: "delete", err: delErr}
- }
- }()
- // 2. Size check
- if size != int64(len(payload)) {
- return &probeError{step: "size", err: fmt.Errorf("wrote %d bytes, expected %d", size, len(payload))}
- }
- // 3. Open
- rc, openErr := s.Open(ctx, path)
- if openErr != nil {
- return &probeError{step: "read", err: openErr}
- }
- // 4. Read all (classify mid-stream errors as read, not verify).
- // Close explicitly (not deferred) so the file handle is released before
- // Delete — on Windows, an open handle prevents deletion.
- data, readErr := io.ReadAll(rc)
- _ = rc.Close()
- if readErr != nil {
- return &probeError{step: "read", err: readErr}
- }
- // 5. Verify
- if !bytes.Equal(data, payload) {
- return &probeError{step: "verify", err: fmt.Errorf("content mismatch")}
- }
- // 6. Delete is handled via the deferred cleanup above.
- return nil
-}
-
-// randomSuffix returns 8 cryptographically random bytes hex-encoded.
-func randomSuffix() (string, error) {
- b := make([]byte, probeSuffixBytes)
- if _, err := rand.Read(b); err != nil {
- return "", err
- }
- return hex.EncodeToString(b), nil
-}
-
-// healthCache memoizes the result of storageProbe for a configurable TTL.
-// It is safe for concurrent use.
-type healthCache struct {
- storage storage.Storage
- interval time.Duration
- probeTimeout time.Duration
- logger *slog.Logger
-
- mu sync.Mutex
- lastAt time.Time
- lastErr error
-}
-
-// newHealthCache builds a cache, parsing the interval from a duration string.
-// Empty interval string defaults to 30s. "0" or "0s" disables caching.
-func newHealthCache(s storage.Storage, intervalStr string, logger *slog.Logger) (*healthCache, error) {
- interval := defaultProbeTTL
- if intervalStr != "" {
- d, err := time.ParseDuration(intervalStr)
- if err != nil {
- return nil, fmt.Errorf("parsing storage_probe_interval %q: %w", intervalStr, err)
- }
- interval = d
- }
- return &healthCache{
- storage: s,
- interval: interval,
- probeTimeout: defaultProbeTimeout,
- logger: logger,
- }, nil
-}
-
-// Check returns the cached probe result if still fresh, otherwise runs a fresh probe.
-// The probe runs under a context derived from context.Background() with a fixed
-// timeout so that caller cancellation (e.g. client disconnect) cannot poison the
-// cache with context.Canceled.
-func (c *healthCache) Check() error {
- c.mu.Lock()
- defer c.mu.Unlock()
-
- // Cache hit
- if c.interval > 0 && !c.lastAt.IsZero() && time.Since(c.lastAt) < c.interval {
- return c.lastErr
- }
-
- // Fresh probe under a detached context
- probeCtx, cancel := context.WithTimeout(context.Background(), c.probeTimeout)
- defer cancel()
- err := storageProbe(probeCtx, c.storage)
-
- // Transition logging and metric increment happen only on the fresh-probe path.
- c.logTransition(c.lastErr, err)
- if err != nil {
- var pe *probeError
- if errors.As(err, &pe) {
- metrics.RecordHealthProbeFailure(pe.step)
- } else {
- metrics.RecordHealthProbeFailure("unknown")
- }
- }
-
- c.lastErr = err
- c.lastAt = time.Now()
- return err
-}
-
-func (c *healthCache) logTransition(prev, curr error) {
- switch {
- case prev != nil && curr == nil:
- c.logger.Info("storage probe recovered")
- case prev == nil && curr != nil:
- c.logger.Error("storage probe failed", "error", curr.Error())
- }
-}
diff --git a/internal/server/health_test.go b/internal/server/health_test.go
deleted file mode 100644
index 3b7eae8..0000000
--- a/internal/server/health_test.go
+++ /dev/null
@@ -1,448 +0,0 @@
-package server
-
-import (
- "bytes"
- "context"
- "errors"
- "io"
- "log/slog"
- "strings"
- "sync"
- "sync/atomic"
- "testing"
- "time"
-
- "github.com/git-pkgs/proxy/internal/metrics"
- "github.com/git-pkgs/proxy/internal/storage"
- "github.com/prometheus/client_golang/prometheus/testutil"
-)
-
-// fakeStorage is a minimal storage.Storage for probe tests with per-step failure injection.
-type fakeStorage struct {
- mu sync.Mutex
-
- storeCalls atomic.Int64
- openCalls atomic.Int64
- closeCalls atomic.Int64
- deleteCalls atomic.Int64
-
- paths []string
- payloads [][]byte
-
- // Failure injection.
- storeErr error
- openErr error
- readErr error // returned by the io.ReadCloser.Read after partial bytes
- deleteErr error
-
- // Misbehavior knobs.
- sizeDelta int64 // added to the reported size from Store
- readOverride []byte // if non-nil, Open returns a reader yielding these bytes instead of stored content
-
- // storeBlock, if non-nil, causes Store to block until the channel is closed or ctx is done.
- storeBlock chan struct{}
-
- stored map[string][]byte
-}
-
-func newFakeStorage() *fakeStorage { return &fakeStorage{stored: map[string][]byte{}} }
-
-func (f *fakeStorage) Store(ctx context.Context, path string, r io.Reader) (int64, string, error) {
- f.storeCalls.Add(1)
- if f.storeErr != nil {
- return 0, "", f.storeErr
- }
- if f.storeBlock != nil {
- select {
- case <-f.storeBlock:
- case <-ctx.Done():
- return 0, "", ctx.Err()
- }
- }
- data, err := io.ReadAll(r)
- if err != nil {
- return 0, "", err
- }
- f.mu.Lock()
- f.stored[path] = data
- f.paths = append(f.paths, path)
- f.payloads = append(f.payloads, data)
- f.mu.Unlock()
- return int64(len(data)) + f.sizeDelta, "fakehash", nil
-}
-
-type fakeReadCloser struct {
- data []byte
- pos int
- readErr error
- closed *atomic.Int64
-}
-
-func (rc *fakeReadCloser) Read(p []byte) (int, error) {
- if rc.pos >= len(rc.data) {
- if rc.readErr != nil {
- return 0, rc.readErr
- }
- return 0, io.EOF
- }
- n := copy(p, rc.data[rc.pos:])
- rc.pos += n
- if rc.pos >= len(rc.data) && rc.readErr != nil {
- return n, rc.readErr
- }
- return n, nil
-}
-
-func (rc *fakeReadCloser) Close() error { rc.closed.Add(1); return nil }
-
-func (f *fakeStorage) Open(ctx context.Context, path string) (io.ReadCloser, error) {
- f.openCalls.Add(1)
- if f.openErr != nil {
- return nil, f.openErr
- }
- f.mu.Lock()
- data := f.stored[path]
- f.mu.Unlock()
- if f.readOverride != nil {
- data = f.readOverride
- }
- return &fakeReadCloser{data: data, readErr: f.readErr, closed: &f.closeCalls}, nil
-}
-
-func (f *fakeStorage) Exists(ctx context.Context, path string) (bool, error) {
- f.mu.Lock()
- defer f.mu.Unlock()
- _, ok := f.stored[path]
- return ok, nil
-}
-
-func (f *fakeStorage) Delete(ctx context.Context, path string) error {
- f.deleteCalls.Add(1)
- if f.deleteErr != nil {
- return f.deleteErr
- }
- f.mu.Lock()
- delete(f.stored, path)
- f.mu.Unlock()
- return nil
-}
-
-func (f *fakeStorage) Size(ctx context.Context, path string) (int64, error) { return 0, nil }
-func (f *fakeStorage) SignedURL(ctx context.Context, path string, expiry time.Duration) (string, error) {
- return "", storage.ErrSignedURLUnsupported
-}
-func (f *fakeStorage) UsedSpace(ctx context.Context) (int64, error) { return 0, nil }
-func (f *fakeStorage) URL() string { return "fake://" }
-func (f *fakeStorage) Close() error { return nil }
-
-// --- Tests follow. First test: happy path ---
-
-func TestStorageProbe_HappyPath(t *testing.T) {
- fs := newFakeStorage()
- if err := storageProbe(context.Background(), fs); err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if got := fs.storeCalls.Load(); got != 1 {
- t.Errorf("Store calls = %d, want 1", got)
- }
- if got := fs.openCalls.Load(); got != 1 {
- t.Errorf("Open calls = %d, want 1", got)
- }
- if got := fs.closeCalls.Load(); got != 1 {
- t.Errorf("Close calls = %d, want 1", got)
- }
- if got := fs.deleteCalls.Load(); got != 1 {
- t.Errorf("Delete calls = %d, want 1", got)
- }
- if len(fs.paths) != 1 || !strings.HasPrefix(fs.paths[0], ".healthcheck/") {
- t.Errorf("unexpected probe path: %v", fs.paths)
- }
-}
-
-func TestStorageProbe_WriteFails(t *testing.T) {
- fs := newFakeStorage()
- fs.storeErr = errors.New("disk full")
- err := storageProbe(context.Background(), fs)
- var pe *probeError
- if !errors.As(err, &pe) {
- t.Fatalf("expected *probeError, got %T: %v", err, err)
- }
- if pe.step != "write" {
- t.Errorf("step = %q, want write", pe.step)
- }
- if fs.openCalls.Load() != 0 {
- t.Errorf("Open should not be called after write failure")
- }
-}
-
-func TestStorageProbe_SizeMismatch(t *testing.T) {
- fs := newFakeStorage()
- fs.sizeDelta = -1 // Report 1 byte fewer than actually written
- err := storageProbe(context.Background(), fs)
- var pe *probeError
- if !errors.As(err, &pe) || pe.step != "size" {
- t.Fatalf("step = %v, want size; err = %v", pe, err)
- }
- if fs.openCalls.Load() != 0 {
- t.Errorf("Open should not be called after size mismatch")
- }
-}
-
-func TestStorageProbe_OpenFails(t *testing.T) {
- fs := newFakeStorage()
- fs.openErr = errors.New("access denied")
- err := storageProbe(context.Background(), fs)
- var pe *probeError
- if !errors.As(err, &pe) || pe.step != "read" {
- t.Fatalf("step = %v, want read; err = %v", pe, err)
- }
-}
-
-func TestStorageProbe_ReadMidStreamFails(t *testing.T) {
- fs := newFakeStorage()
- fs.readErr = errors.New("connection reset")
- err := storageProbe(context.Background(), fs)
- var pe *probeError
- if !errors.As(err, &pe) || pe.step != "read" {
- t.Fatalf("step = %v, want read (NOT verify); err = %v", pe, err)
- }
-}
-
-func TestStorageProbe_ContentMismatch(t *testing.T) {
- fs := newFakeStorage()
- fs.readOverride = []byte("wrong content")
- err := storageProbe(context.Background(), fs)
- var pe *probeError
- if !errors.As(err, &pe) || pe.step != "verify" {
- t.Fatalf("step = %v, want verify; err = %v", pe, err)
- }
-}
-
-func TestStorageProbe_DeleteFails(t *testing.T) {
- fs := newFakeStorage()
- fs.deleteErr = errors.New("permission denied")
- err := storageProbe(context.Background(), fs)
- var pe *probeError
- if !errors.As(err, &pe) || pe.step != "delete" {
- t.Fatalf("step = %v, want delete; err = %v", pe, err)
- }
-}
-
-// TestStorageProbe_CleanupOnNonDeleteFailure asserts that the probe object is
-// deleted even when a step after Store (size/open/read/verify) fails, so
-// probe artifacts don't accumulate in the storage backend.
-func TestStorageProbe_CleanupOnNonDeleteFailure(t *testing.T) {
- cases := []struct {
- name string
- inject func(*fakeStorage)
- wantErr string
- }{
- {"size mismatch", func(fs *fakeStorage) { fs.sizeDelta = -1 }, "size"},
- {"open fails", func(fs *fakeStorage) { fs.openErr = errors.New("open boom") }, "read"},
- {"read mid-stream", func(fs *fakeStorage) { fs.readErr = errors.New("mid-stream boom") }, "read"},
- {"content mismatch", func(fs *fakeStorage) { fs.readOverride = []byte("wrong") }, "verify"},
- }
- for _, tc := range cases {
- t.Run(tc.name, func(t *testing.T) {
- fs := newFakeStorage()
- tc.inject(fs)
- err := storageProbe(context.Background(), fs)
- var pe *probeError
- if !errors.As(err, &pe) || pe.step != tc.wantErr {
- t.Fatalf("step = %v, want %q; err = %v", pe, tc.wantErr, err)
- }
- if got := fs.deleteCalls.Load(); got != 1 {
- t.Errorf("deleteCalls = %d, want 1 (cleanup should run on non-delete failures)", got)
- }
- })
- }
-}
-
-func TestStorageProbe_ReaderClosedOnReadFailure(t *testing.T) {
- fs := newFakeStorage()
- fs.readErr = errors.New("read error")
- _ = storageProbe(context.Background(), fs)
- if got := fs.closeCalls.Load(); got != fs.openCalls.Load() {
- t.Errorf("closeCalls = %d, openCalls = %d (should match)", got, fs.openCalls.Load())
- }
-}
-
-func TestStorageProbe_PathUniqueness(t *testing.T) {
- fs := newFakeStorage()
- for i := 0; i < 100; i++ {
- if err := storageProbe(context.Background(), fs); err != nil {
- t.Fatalf("probe %d: %v", i, err)
- }
- }
- seen := make(map[string]bool)
- for _, p := range fs.paths {
- if !strings.HasPrefix(p, ".healthcheck/") {
- t.Errorf("path missing prefix: %q", p)
- }
- if seen[p] {
- t.Errorf("duplicate path: %q", p)
- }
- seen[p] = true
- }
-}
-
-// helper: a healthCache wired to a fakeStorage and a discard logger.
-func newTestCache(fs *fakeStorage, interval time.Duration) *healthCache {
- return &healthCache{
- storage: fs,
- interval: interval,
- probeTimeout: 5 * time.Second,
- logger: discardLogger(),
- }
-}
-
-func discardLogger() *slog.Logger {
- return slog.New(slog.NewTextHandler(io.Discard, nil))
-}
-
-func TestHealthCache_CacheHit(t *testing.T) {
- fs := newFakeStorage()
- c := newTestCache(fs, 30*time.Second)
- if err := c.Check(); err != nil {
- t.Fatalf("first check: %v", err)
- }
- if err := c.Check(); err != nil {
- t.Fatalf("second check: %v", err)
- }
- if got := fs.storeCalls.Load(); got != 1 {
- t.Errorf("storeCalls = %d, want 1 (second call should be cached)", got)
- }
-}
-
-func TestHealthCache_MissAfterTTL(t *testing.T) {
- fs := newFakeStorage()
- c := newTestCache(fs, 10*time.Millisecond)
- _ = c.Check()
- time.Sleep(20 * time.Millisecond)
- _ = c.Check()
- if got := fs.storeCalls.Load(); got != 2 {
- t.Errorf("storeCalls = %d, want 2", got)
- }
-}
-
-func TestHealthCache_Disabled(t *testing.T) {
- fs := newFakeStorage()
- c := newTestCache(fs, 0) // interval = 0 means probe every call
- _ = c.Check()
- _ = c.Check()
- if got := fs.storeCalls.Load(); got != 2 {
- t.Errorf("storeCalls = %d, want 2", got)
- }
-}
-
-func TestHealthCache_LastAtNotAdvancedOnHit(t *testing.T) {
- fs := newFakeStorage()
- c := newTestCache(fs, 30*time.Second)
- for i := 0; i < 100; i++ {
- _ = c.Check()
- }
- if got := fs.storeCalls.Load(); got != 1 {
- t.Errorf("storeCalls = %d, want 1 across 100 hits", got)
- }
-}
-
-func TestHealthCache_ConcurrentSingleFlight(t *testing.T) {
- fs := newFakeStorage()
- c := newTestCache(fs, 30*time.Second)
- var wg sync.WaitGroup
- for i := 0; i < 20; i++ {
- wg.Add(1)
- go func() { defer wg.Done(); _ = c.Check() }()
- }
- wg.Wait()
- if got := fs.storeCalls.Load(); got != 1 {
- t.Errorf("storeCalls = %d, want 1 with 20 concurrent callers", got)
- }
-}
-
-func TestHealthCache_FailureCounterIncrement(t *testing.T) {
- fs := newFakeStorage()
- fs.storeErr = errors.New("boom")
- c := newTestCache(fs, 30*time.Second)
-
- before := testutil.ToFloat64(metrics.HealthProbeFailures.WithLabelValues("write"))
-
- // First call: fresh probe → counter +1
- _ = c.Check()
- afterFirst := testutil.ToFloat64(metrics.HealthProbeFailures.WithLabelValues("write"))
- if afterFirst-before != 1 {
- t.Errorf("counter delta after first call = %v, want 1", afterFirst-before)
- }
-
- // Second call: cache hit → counter NOT re-incremented
- _ = c.Check()
- afterSecond := testutil.ToFloat64(metrics.HealthProbeFailures.WithLabelValues("write"))
- if afterSecond != afterFirst {
- t.Errorf("counter changed on cache hit: %v → %v", afterFirst, afterSecond)
- }
-}
-
-func TestHealthCache_ProbeTimeout(t *testing.T) {
- fs := newFakeStorage()
- fs.storeBlock = make(chan struct{}) // Store will block until channel is closed (or never)
- t.Cleanup(func() { close(fs.storeBlock) })
-
- c := &healthCache{
- storage: fs,
- interval: 30 * time.Second,
- probeTimeout: 50 * time.Millisecond,
- logger: discardLogger(),
- }
- start := time.Now()
- err := c.Check()
- elapsed := time.Since(start)
-
- if err == nil {
- t.Fatal("expected timeout error, got nil")
- }
- if elapsed > 500*time.Millisecond {
- t.Errorf("probe took %v, expected ~50ms (timeout not respected)", elapsed)
- }
-}
-
-func TestHealthCache_TransitionLogging(t *testing.T) {
- fs := newFakeStorage()
- var buf bytes.Buffer
- logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelInfo}))
- c := &healthCache{
- storage: fs,
- interval: 0, // probe every call
- probeTimeout: 5 * time.Second,
- logger: logger,
- }
-
- // Steady ok state — should not log
- _ = c.Check()
- _ = c.Check()
- if got := strings.Count(buf.String(), "storage probe"); got != 0 {
- t.Errorf("steady-state logs = %d, want 0; output: %s", got, buf.String())
- }
-
- // ok → err transition: exactly one Error log
- buf.Reset()
- fs.storeErr = errors.New("boom")
- _ = c.Check()
- if !strings.Contains(buf.String(), "storage probe failed") {
- t.Errorf("missing failure log on transition; output: %s", buf.String())
- }
-
- // err steady state — should not log again
- buf.Reset()
- _ = c.Check()
- if buf.Len() != 0 {
- t.Errorf("steady-err logs = %q, want empty", buf.String())
- }
-
- // err → ok transition: exactly one Info log
- buf.Reset()
- fs.storeErr = nil
- _ = c.Check()
- if !strings.Contains(buf.String(), "storage probe recovered") {
- t.Errorf("missing recovery log on transition; output: %s", buf.String())
- }
-}
diff --git a/internal/server/layout.go b/internal/server/layout.go
deleted file mode 100644
index 2da9469..0000000
--- a/internal/server/layout.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package server
-
-import "net/http"
-
-// BuildInfo identifies the running proxy binary.
-type BuildInfo struct {
- Version string
- Commit string
-}
-
-// Layout carries shared fields consumed by the base template. It is embedded
-// in every page data struct so templates can access canonical URL and build
-// information alongside the page's own fields.
-type Layout struct {
- BuildInfo BuildInfo
- UIBaseURL string
- CanonicalPath string
-}
-
-func (s *Server) layoutFor(r *http.Request) Layout {
- return Layout{
- BuildInfo: s.buildInfo,
- UIBaseURL: s.cfg.UIBaseURL,
- CanonicalPath: r.URL.Path,
- }
-}
diff --git a/internal/server/middleware.go b/internal/server/middleware.go
index b6d483f..9b81254 100644
--- a/internal/server/middleware.go
+++ b/internal/server/middleware.go
@@ -3,15 +3,16 @@ package server
import (
"context"
"net/http"
- "strings"
"sync/atomic"
"time"
- "github.com/git-pkgs/proxy/internal/accesslog"
- "github.com/git-pkgs/proxy/internal/metrics"
"github.com/go-chi/chi/v5/middleware"
)
+type contextKey string
+
+const requestIDKey contextKey = "request_id"
+
var requestCounter atomic.Uint64
// RequestIDMiddleware adds a sequential request ID to the context and response headers.
@@ -22,7 +23,7 @@ func RequestIDMiddleware(next http.Handler) http.Handler {
requestID := middleware.GetReqID(r.Context())
// Store formatted ID in context
- ctx := accesslog.WithRequestID(r.Context(), requestID)
+ ctx := context.WithValue(r.Context(), requestIDKey, requestID)
// Add to response header for client tracking
w.Header().Set("X-Request-ID", requestID)
@@ -33,7 +34,10 @@ func RequestIDMiddleware(next http.Handler) http.Handler {
// GetRequestID retrieves the request ID from context.
func GetRequestID(ctx context.Context) string {
- return accesslog.RequestID(ctx)
+ if id, ok := ctx.Value(requestIDKey).(string); ok {
+ return id
+ }
+ return ""
}
// LoggerMiddleware logs HTTP requests with request ID correlation.
@@ -44,51 +48,27 @@ func (s *Server) LoggerMiddleware(next http.Handler) http.Handler {
rw := &responseWriter{ResponseWriter: w, status: http.StatusOK}
next.ServeHTTP(rw, r)
- duration := time.Since(start)
s.logger.Info("request",
"request_id", requestID,
"method", r.Method,
"path", r.URL.Path,
"status", rw.status,
- "duration", duration,
+ "duration", time.Since(start),
"remote", r.RemoteAddr)
-
- if r.URL.Path != "/metrics" {
- metrics.RecordRequest(requestEcosystem(r.URL.Path), rw.status, duration)
- }
-
- if s.accessLog != nil {
- if err := s.accessLog.Write(accesslog.Entry{
- Event: accesslog.EventRequest,
- RequestID: requestID,
- Method: r.Method,
- Path: r.URL.EscapedPath(),
- StatusCode: rw.status,
- DurationMS: duration.Milliseconds(),
- RemoteAddr: r.RemoteAddr,
- }); err != nil {
- s.logger.Error("failed to write access log", "error", err)
- }
- }
})
}
-func requestEcosystem(path string) string {
- segment, _, _ := strings.Cut(strings.TrimPrefix(path, "/"), "/")
- switch segment {
- case "npm", "cargo", "hex", "pub", "pypi", "maven", "gradle", "nuget",
- "conan", "conda", "cran", "julia", "debian", "rpm":
- return segment
- case "gem":
- return "rubygems"
- case "go":
- return "golang"
- case "composer":
- return "packagist"
- case "v2":
- return "oci"
- default:
- return "other"
- }
+// ActiveRequestsMiddleware tracks the number of active requests using Prometheus metrics.
+func ActiveRequestsMiddleware(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // Don't track metrics endpoint itself
+ if r.URL.Path == "/metrics" {
+ next.ServeHTTP(w, r)
+ return
+ }
+
+ // Implemented in server.go where metrics package is imported
+ next.ServeHTTP(w, r)
+ })
}
diff --git a/internal/server/middleware_test.go b/internal/server/middleware_test.go
index 38905b2..22a1167 100644
--- a/internal/server/middleware_test.go
+++ b/internal/server/middleware_test.go
@@ -2,21 +2,11 @@ package server
import (
"context"
- "encoding/json"
- "io"
- "log/slog"
"net/http"
"net/http/httptest"
- "os"
- "path/filepath"
"testing"
- "github.com/git-pkgs/proxy/internal/accesslog"
- "github.com/git-pkgs/proxy/internal/metrics"
"github.com/go-chi/chi/v5/middleware"
- "github.com/prometheus/client_golang/prometheus"
- "github.com/prometheus/client_golang/prometheus/testutil"
- dto "github.com/prometheus/client_model/go"
)
func TestRequestIDMiddleware(t *testing.T) {
@@ -53,7 +43,7 @@ func TestGetRequestID(t *testing.T) {
}{
{
name: "with request ID",
- ctx: accesslog.WithRequestID(context.Background(), "test-123"),
+ ctx: context.WithValue(context.Background(), requestIDKey, "test-123"),
expected: "test-123",
},
{
@@ -73,182 +63,32 @@ func TestGetRequestID(t *testing.T) {
}
}
-func TestLoggerMiddleware(t *testing.T) {
- logger := slog.New(slog.NewTextHandler(io.Discard, nil))
- s := &Server{logger: logger}
-
- called := false
- next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- called = true
- w.WriteHeader(http.StatusCreated)
- })
-
- handler := s.LoggerMiddleware(next)
-
- req := httptest.NewRequest(http.MethodGet, "/test-path", nil)
- rec := httptest.NewRecorder()
-
- handler.ServeHTTP(rec, req)
-
- if !called {
- t.Error("expected next handler to be called")
- }
-
- if rec.Code != http.StatusCreated {
- t.Errorf("expected status 201, got %d", rec.Code)
- }
-}
-
-func TestLoggerMiddlewareRecordsRequestMetrics(t *testing.T) {
- before := testutil.ToFloat64(metrics.RequestsTotal.WithLabelValues("rubygems", "404"))
- durationMetric := metrics.RequestDuration.WithLabelValues("rubygems", "404")
- beforeDurationCount := histogramSampleCount(t, durationMetric)
-
- logger := slog.New(slog.NewTextHandler(io.Discard, nil))
- s := &Server{logger: logger}
- handler := s.LoggerMiddleware(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
- w.WriteHeader(http.StatusNotFound)
+func TestActiveRequestsMiddleware(t *testing.T) {
+ handler := ActiveRequestsMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
}))
- req := httptest.NewRequest(http.MethodGet, "/gem/downloads/missing.gem", nil)
+ req := httptest.NewRequest(http.MethodGet, "/test", nil)
rec := httptest.NewRecorder()
+
handler.ServeHTTP(rec, req)
- after := testutil.ToFloat64(metrics.RequestsTotal.WithLabelValues("rubygems", "404"))
- if got := after - before; got != 1 {
- t.Errorf("request counter delta = %.0f, want 1", got)
- }
- afterDurationCount := histogramSampleCount(t, durationMetric)
- if got := afterDurationCount - beforeDurationCount; got != 1 {
- t.Errorf("request duration sample delta = %d, want 1", got)
+ if rec.Code != http.StatusOK {
+ t.Errorf("expected status 200, got %d", rec.Code)
}
}
-func histogramSampleCount(t *testing.T, observer prometheus.Observer) uint64 {
- t.Helper()
-
- metric, ok := observer.(prometheus.Metric)
- if !ok {
- t.Fatal("histogram observer does not implement prometheus.Metric")
- }
-
- var value dto.Metric
- if err := metric.Write(&value); err != nil {
- t.Fatalf("writing histogram metric: %v", err)
- }
- return value.GetHistogram().GetSampleCount()
-}
-
-func TestLoggerMiddlewareSkipsMetricsEndpointMetrics(t *testing.T) {
- before := testutil.ToFloat64(metrics.RequestsTotal.WithLabelValues("other", "200"))
-
- logger := slog.New(slog.NewTextHandler(io.Discard, nil))
- s := &Server{logger: logger}
- handler := s.LoggerMiddleware(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+func TestActiveRequestsMiddleware_SkipsMetricsEndpoint(t *testing.T) {
+ handler := ActiveRequestsMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
req := httptest.NewRequest(http.MethodGet, "/metrics", nil)
rec := httptest.NewRecorder()
+
handler.ServeHTTP(rec, req)
- after := testutil.ToFloat64(metrics.RequestsTotal.WithLabelValues("other", "200"))
- if got := after - before; got != 0 {
- t.Errorf("request counter delta = %.0f, want 0", got)
- }
-}
-
-func TestRequestEcosystem(t *testing.T) {
- tests := []struct {
- path string
- want string
- }{
- {path: "/npm/lodash", want: "npm"},
- {path: "/gem/downloads/rails.gem", want: "rubygems"},
- {path: "/go/example.com/module/@v/list", want: "golang"},
- {path: "/composer/vendor/package", want: "packagist"},
- {path: "/v2/library/alpine/manifests/latest", want: "oci"},
- {path: "/ui/", want: "other"},
- {path: "/api/package/npm/lodash", want: "other"},
- {path: "/", want: "other"},
- }
-
- for _, tt := range tests {
- t.Run(tt.path, func(t *testing.T) {
- if got := requestEcosystem(tt.path); got != tt.want {
- t.Errorf("requestEcosystem(%q) = %q, want %q", tt.path, got, tt.want)
- }
- })
- }
-}
-
-func TestLoggerMiddlewareWritesAccessLog(t *testing.T) {
- path := filepath.Join(t.TempDir(), "access.jsonl")
- activityLog, err := accesslog.Open(path)
- if err != nil {
- t.Fatal(err)
- }
-
- logger := slog.New(slog.NewTextHandler(io.Discard, nil))
- s := &Server{logger: logger, accessLog: activityLog}
- next := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
- w.WriteHeader(http.StatusNotFound)
- })
- handler := middleware.RequestID(RequestIDMiddleware(s.LoggerMiddleware(next)))
-
- req := httptest.NewRequest(http.MethodGet, "/packages/example?token=secret", nil)
- rec := httptest.NewRecorder()
- handler.ServeHTTP(rec, req)
-
- if err := activityLog.Close(); err != nil {
- t.Fatal(err)
- }
- data, err := os.ReadFile(path)
- if err != nil {
- t.Fatal(err)
- }
-
- var entry accesslog.Entry
- if err := json.Unmarshal(data, &entry); err != nil {
- t.Fatalf("decoding access log: %v", err)
- }
- if entry.Event != accesslog.EventRequest {
- t.Errorf("event = %q, want %q", entry.Event, accesslog.EventRequest)
- }
- if entry.RequestID == "" {
- t.Error("request_id is empty")
- }
- if entry.Path != "/packages/example" {
- t.Errorf("path = %q, want query string omitted", entry.Path)
- }
- if entry.StatusCode != http.StatusNotFound {
- t.Errorf("status_code = %d, want %d", entry.StatusCode, http.StatusNotFound)
- }
-}
-
-func TestResponseWriter_WriteHeader(t *testing.T) {
- tests := []struct {
- name string
- status int
- }{
- {"ok", http.StatusOK},
- {"not found", http.StatusNotFound},
- {"internal error", http.StatusInternalServerError},
- {"created", http.StatusCreated},
- }
-
- for _, tc := range tests {
- t.Run(tc.name, func(t *testing.T) {
- rec := httptest.NewRecorder()
- rw := &responseWriter{ResponseWriter: rec, status: http.StatusOK}
- rw.WriteHeader(tc.status)
-
- if rw.status != tc.status {
- t.Errorf("expected status %d, got %d", tc.status, rw.status)
- }
- if rec.Code != tc.status {
- t.Errorf("expected underlying recorder status %d, got %d", tc.status, rec.Code)
- }
- })
+ if rec.Code != http.StatusOK {
+ t.Errorf("expected status 200, got %d", rec.Code)
}
}
diff --git a/internal/server/mirror_api.go b/internal/server/mirror_api.go
deleted file mode 100644
index 028d4e0..0000000
--- a/internal/server/mirror_api.go
+++ /dev/null
@@ -1,61 +0,0 @@
-package server
-
-import (
- "encoding/json"
- "net/http"
-
- "github.com/git-pkgs/proxy/internal/mirror"
- "github.com/go-chi/chi/v5"
-)
-
-// MirrorAPIHandler handles mirror API requests.
-type MirrorAPIHandler struct {
- jobs *mirror.JobStore
-}
-
-// NewMirrorAPIHandler creates a new mirror API handler.
-func NewMirrorAPIHandler(jobs *mirror.JobStore) *MirrorAPIHandler {
- return &MirrorAPIHandler{jobs: jobs}
-}
-
-// HandleCreate starts a new mirror job.
-func (h *MirrorAPIHandler) HandleCreate(w http.ResponseWriter, r *http.Request) {
- r.Body = http.MaxBytesReader(w, r.Body, maxBodySize)
- var req mirror.JobRequest
- if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- badRequest(w, "invalid request body")
- return
- }
-
- id, err := h.jobs.Create(req)
- if err != nil {
- badRequest(w, "invalid mirror job request")
- return
- }
-
- w.Header().Set("Content-Type", "application/json")
- w.WriteHeader(http.StatusAccepted)
- writeJSON(w, map[string]string{"id": id})
-}
-
-// HandleGet returns the status of a mirror job.
-func (h *MirrorAPIHandler) HandleGet(w http.ResponseWriter, r *http.Request) {
- id := chi.URLParam(r, "id")
- job := h.jobs.Get(id)
- if job == nil {
- notFound(w, "job not found")
- return
- }
-
- writeJSON(w, job)
-}
-
-// HandleCancel cancels a running mirror job.
-func (h *MirrorAPIHandler) HandleCancel(w http.ResponseWriter, r *http.Request) {
- id := chi.URLParam(r, "id")
- if h.jobs.Cancel(id) {
- writeJSON(w, map[string]string{"status": "canceled"})
- } else {
- notFound(w, "job not found or not running")
- }
-}
diff --git a/internal/server/mirror_api_test.go b/internal/server/mirror_api_test.go
deleted file mode 100644
index c960348..0000000
--- a/internal/server/mirror_api_test.go
+++ /dev/null
@@ -1,232 +0,0 @@
-package server
-
-import (
- "bytes"
- "context"
- "encoding/json"
- "log/slog"
- "net/http"
- "net/http/httptest"
- "os"
- "testing"
-
- "github.com/git-pkgs/proxy/internal/database"
- "github.com/git-pkgs/proxy/internal/handler"
- "github.com/git-pkgs/proxy/internal/mirror"
- "github.com/git-pkgs/proxy/internal/storage"
- "github.com/git-pkgs/registries/fetch"
- "github.com/go-chi/chi/v5"
-)
-
-func setupMirrorAPI(t *testing.T) *MirrorAPIHandler {
- t.Helper()
-
- dbPath := t.TempDir() + "/test.db"
- db, err := database.Create(dbPath)
- if err != nil {
- t.Fatalf("creating database: %v", err)
- }
- if err := db.MigrateSchema(); err != nil {
- t.Fatalf("migrating schema: %v", err)
- }
- t.Cleanup(func() { _ = db.Close() })
-
- storeDir := t.TempDir()
- store, err := storage.OpenBucket(context.Background(), "file://"+storeDir)
- if err != nil {
- t.Fatalf("opening storage: %v", err)
- }
-
- logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelWarn}))
- fetcher := fetch.NewFetcher()
- resolver := fetch.NewResolver()
- proxy := handler.NewProxy(db, store, fetcher, resolver, logger)
-
- m := mirror.New(proxy, db, store, logger, 1)
- js := mirror.NewJobStore(context.Background(), m)
- return NewMirrorAPIHandler(js)
-}
-
-func TestMirrorAPICreateJob(t *testing.T) {
- h := setupMirrorAPI(t)
-
- body, _ := json.Marshal(mirror.JobRequest{
- PURLs: []string{"pkg:npm/lodash@4.17.21"},
- })
-
- req := httptest.NewRequest("POST", "/api/mirror", bytes.NewReader(body))
- w := httptest.NewRecorder()
- h.HandleCreate(w, req)
-
- if w.Code != http.StatusAccepted {
- t.Errorf("status = %d, want %d", w.Code, http.StatusAccepted)
- }
-
- var resp map[string]string
- if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
- t.Fatalf("decoding response: %v", err)
- }
- if resp["id"] == "" {
- t.Error("expected non-empty job ID")
- }
-}
-
-func TestMirrorAPICreateJobFromInlineSBOM(t *testing.T) {
- h := setupMirrorAPI(t)
-
- body, err := json.Marshal(mirror.JobRequest{
- SBOM: json.RawMessage(`{
- "bomFormat":"CycloneDX",
- "specVersion":"1.4",
- "components":[{
- "type":"library",
- "name":"lodash",
- "version":"4.17.21",
- "purl":"pkg:npm/lodash@4.17.21"
- }]
- }`),
- })
- if err != nil {
- t.Fatalf("marshaling request: %v", err)
- }
-
- req := httptest.NewRequest(http.MethodPost, "/api/mirror", bytes.NewReader(body))
- w := httptest.NewRecorder()
- h.HandleCreate(w, req)
-
- if w.Code != http.StatusAccepted {
- t.Fatalf("status = %d, want %d: %s", w.Code, http.StatusAccepted, w.Body.String())
- }
-
- var resp map[string]string
- if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
- t.Fatalf("decoding response: %v", err)
- }
- if resp["id"] == "" {
- t.Error("expected non-empty job ID")
- }
-}
-
-func TestMirrorAPICreateJobRejectsPURLsAndSBOM(t *testing.T) {
- h := setupMirrorAPI(t)
-
- body, err := json.Marshal(mirror.JobRequest{
- PURLs: []string{"pkg:npm/lodash@4.17.21"},
- SBOM: json.RawMessage(`{"bomFormat":"CycloneDX","components":[]}`),
- })
- if err != nil {
- t.Fatalf("marshaling request: %v", err)
- }
-
- req := httptest.NewRequest(http.MethodPost, "/api/mirror", bytes.NewReader(body))
- w := httptest.NewRecorder()
- h.HandleCreate(w, req)
-
- if w.Code != http.StatusBadRequest {
- t.Errorf("status = %d, want %d", w.Code, http.StatusBadRequest)
- }
-}
-
-func TestMirrorAPICreateOversizedBody(t *testing.T) {
- h := setupMirrorAPI(t)
-
- body := bytes.Repeat([]byte("x"), int(maxBodySize)+1)
- req := httptest.NewRequest("POST", "/api/mirror", bytes.NewReader(body))
- w := httptest.NewRecorder()
- h.HandleCreate(w, req)
-
- if w.Code != http.StatusBadRequest {
- t.Errorf("status = %d, want %d", w.Code, http.StatusBadRequest)
- }
-}
-
-func TestMirrorAPICreateInvalidBody(t *testing.T) {
- h := setupMirrorAPI(t)
-
- req := httptest.NewRequest("POST", "/api/mirror", bytes.NewReader([]byte("not json")))
- w := httptest.NewRecorder()
- h.HandleCreate(w, req)
-
- if w.Code != http.StatusBadRequest {
- t.Errorf("status = %d, want %d", w.Code, http.StatusBadRequest)
- }
-}
-
-func TestMirrorAPICreateEmptyRequest(t *testing.T) {
- h := setupMirrorAPI(t)
-
- body, _ := json.Marshal(mirror.JobRequest{})
- req := httptest.NewRequest("POST", "/api/mirror", bytes.NewReader(body))
- w := httptest.NewRecorder()
- h.HandleCreate(w, req)
-
- if w.Code != http.StatusBadRequest {
- t.Errorf("status = %d, want %d", w.Code, http.StatusBadRequest)
- }
-}
-
-func TestMirrorAPIGetNotFound(t *testing.T) {
- h := setupMirrorAPI(t)
-
- r := chi.NewRouter()
- r.Get("/api/mirror/{id}", h.HandleGet)
-
- req := httptest.NewRequest("GET", "/api/mirror/nonexistent", nil)
- w := httptest.NewRecorder()
- r.ServeHTTP(w, req)
-
- if w.Code != http.StatusNotFound {
- t.Errorf("status = %d, want %d", w.Code, http.StatusNotFound)
- }
-}
-
-func TestMirrorAPICancelNotFound(t *testing.T) {
- h := setupMirrorAPI(t)
-
- r := chi.NewRouter()
- r.Delete("/api/mirror/{id}", h.HandleCancel)
-
- req := httptest.NewRequest("DELETE", "/api/mirror/nonexistent", nil)
- w := httptest.NewRecorder()
- r.ServeHTTP(w, req)
-
- if w.Code != http.StatusNotFound {
- t.Errorf("status = %d, want %d", w.Code, http.StatusNotFound)
- }
-}
-
-func TestMirrorAPICreateAndGetJob(t *testing.T) {
- h := setupMirrorAPI(t)
-
- // Create a job
- body, _ := json.Marshal(mirror.JobRequest{
- PURLs: []string{"pkg:npm/lodash@4.17.21"},
- })
- createReq := httptest.NewRequest("POST", "/api/mirror", bytes.NewReader(body))
- createW := httptest.NewRecorder()
- h.HandleCreate(createW, createReq)
-
- var createResp map[string]string
- _ = json.NewDecoder(createW.Body).Decode(&createResp)
- jobID := createResp["id"]
-
- // Get the job
- r := chi.NewRouter()
- r.Get("/api/mirror/{id}", h.HandleGet)
-
- getReq := httptest.NewRequest("GET", "/api/mirror/"+jobID, nil)
- getW := httptest.NewRecorder()
- r.ServeHTTP(getW, getReq)
-
- if getW.Code != http.StatusOK {
- t.Errorf("status = %d, want %d", getW.Code, http.StatusOK)
- }
-
- var job mirror.Job
- if err := json.NewDecoder(getW.Body).Decode(&job); err != nil {
- t.Fatalf("decoding job: %v", err)
- }
- if job.ID != jobID {
- t.Errorf("job ID = %q, want %q", job.ID, jobID)
- }
-}
diff --git a/internal/server/packages_list_test.go b/internal/server/packages_list_test.go
index ac57d74..1b915ad 100644
--- a/internal/server/packages_list_test.go
+++ b/internal/server/packages_list_test.go
@@ -28,7 +28,7 @@ func TestHandlePackagesList(t *testing.T) {
// Create test data
pkg1 := &database.Package{
PURL: "pkg:npm/lodash",
- Ecosystem: testEcosystemNPM,
+ Ecosystem: "npm",
Name: "lodash",
LatestVersion: sql.NullString{String: "4.17.21", Valid: true},
License: sql.NullString{String: "MIT", Valid: true},
@@ -103,7 +103,7 @@ func TestHandlePackagesList(t *testing.T) {
if len(resp.Results) != 2 {
t.Errorf("expected 2 results, got %d", len(resp.Results))
}
- if resp.SortBy != defaultSortBy {
+ if resp.SortBy != "hits" {
t.Errorf("expected default sort to be hits, got %s", resp.SortBy)
}
})
@@ -123,7 +123,7 @@ func TestHandlePackagesList(t *testing.T) {
t.Fatal(err)
}
- if resp.Ecosystem != testEcosystemNPM {
+ if resp.Ecosystem != "npm" {
t.Errorf("expected ecosystem npm, got %s", resp.Ecosystem)
}
if resp.Count != 1 {
diff --git a/internal/server/resolve.go b/internal/server/resolve.go
deleted file mode 100644
index f7a1b23..0000000
--- a/internal/server/resolve.go
+++ /dev/null
@@ -1,146 +0,0 @@
-package server
-
-import (
- "fmt"
- "net/http"
- "net/url"
- "strings"
- "unicode"
-
- "github.com/git-pkgs/proxy/internal/database"
- "github.com/go-chi/chi/v5"
-)
-
-// maxPackagePathLen bounds the wildcard portion of package routes (name plus
-// version and any suffix). npm caps names at 214 and Maven coordinates can be
-// longer, so 512 leaves room without admitting pathological inputs.
-const maxPackagePathLen = 512
-
-// packagePathSegments validates the wildcard portion of a package route and
-// splits it into decoded path segments.
-func packagePathSegments(r *http.Request) ([]string, error) {
- wildcard := chi.URLParam(r, "*")
- encoded := wildcardIsEncoded(r)
- if err := validatePackagePath(wildcard, encoded); err != nil {
- return nil, err
- }
-
- return splitWildcardPath(wildcard, encoded), nil
-}
-
-// wildcardIsEncoded reports whether the chi wildcard for this request is still
-// percent-encoded.
-//
-// chi routes on r.URL.RawPath when it is set and on r.URL.Path otherwise, and
-// net/url only sets RawPath when the request's escaping differs from the
-// canonical encoding of the decoded path. A version such as "release%2F1" is
-// therefore routed raw, while "1.0%252B" (a version whose text contains a
-// literal "%2B") encodes canonically and arrives already decoded once. The
-// distinction decides whether the segments still need decoding: decoding the
-// second case again would turn it into "1.0+" and resolve a different version.
-func wildcardIsEncoded(r *http.Request) bool {
- return r.URL.RawPath != ""
-}
-
-// validatePackagePath rejects wildcard package paths that cannot be valid in
-// any supported ecosystem. It is a coarse filter applied before database or
-// enrichment lookups; ecosystem-specific name rules are layered on top.
-//
-// encoded has the meaning described on wildcardIsEncoded.
-func validatePackagePath(path string, encoded bool) error {
- if path == "" {
- return fmt.Errorf("package name required")
- }
- if len(path) > maxPackagePathLen {
- return fmt.Errorf("package path exceeds %d bytes", maxPackagePathLen)
- }
- // Validate the decoded segments: the handlers work with decoded values, so
- // an escape such as "%00" or "%2E%2E" must not slip past these checks.
- for _, seg := range splitWildcardPath(path, encoded) {
- // Each segment is checked both as the handlers see it and decoded once
- // more: a segment can reach a handler with escapes intact, and the
- // upstream registry is then the one that decodes them.
- for _, value := range []string{seg, decodePathSegment(seg)} {
- // A decoded segment can itself contain slashes (from "%2F"), and
- // the segments are later rejoined into a package name that
- // registries interpolate straight into an upstream URL. Check every
- // path element, not just the segment as a whole, or
- // "a%2F..%2F..%2Fb" traverses.
- for _, elem := range strings.Split(value, "/") {
- if elem == ".." {
- return fmt.Errorf("package path contains parent directory segment")
- }
- }
- for _, r := range value {
- if r == 0 {
- return fmt.Errorf("package path contains null byte")
- }
- if unicode.IsControl(r) {
- return fmt.Errorf("package path contains control character %#U", r)
- }
- }
- }
- }
- return nil
-}
-
-// resolvePackageName determines the package name from a wildcard path by
-// checking the database. This handles namespaced packages like Composer's
-// vendor/name format where the package name contains a slash.
-//
-// It tries the full path as a package name first. If not found, it splits
-// off the last segment as a non-name suffix (version, action, etc.) and
-// tries again, working backwards until a match is found or segments run out.
-//
-// Returns the package name and the remaining path segments after the name.
-// If no package is found, returns empty name and the original segments.
-func resolvePackageName(db *database.DB, ecosystem string, segments []string) (name string, rest []string) {
- // Try increasingly longer prefixes as the package name.
- // Start with the longest possible name (all segments) and work down.
- for i := len(segments); i >= 1; i-- {
- candidate := strings.Join(segments[:i], "/")
- pkg, err := db.GetPackageByEcosystemName(ecosystem, candidate)
- if err == nil && pkg != nil {
- return candidate, segments[i:]
- }
- }
-
- return "", segments
-}
-
-// splitWildcardPath splits a chi wildcard path value into segments,
-// trimming any leading/trailing slashes.
-//
-// When encoded is set the value is still percent-encoded (see
-// wildcardIsEncoded), so each segment is decoded after splitting. Splitting
-// first keeps an encoded "%2F" inside a name from being mistaken for a
-// separator. Decoding matters for versions such as "1.0%2Bbuild1", which must
-// reach the handlers as "1.0+build1" so that rebuilding the PURL yields the
-// value that was stored rather than a double-encoded one.
-func splitWildcardPath(path string, encoded bool) []string {
- path = strings.Trim(path, "/")
- if path == "" {
- return nil
- }
- segments := strings.Split(path, "/")
- if !encoded {
- return segments
- }
- for i, seg := range segments {
- segments[i] = decodePathSegment(seg)
- }
- return segments
-}
-
-// decodePathSegment percent-decodes a single URL path segment, returning it
-// unchanged if it is not valid percent-encoding.
-func decodePathSegment(seg string) string {
- if !strings.Contains(seg, "%") {
- return seg
- }
- decoded, err := url.PathUnescape(seg)
- if err != nil {
- return seg
- }
- return decoded
-}
diff --git a/internal/server/resolve_test.go b/internal/server/resolve_test.go
deleted file mode 100644
index 1867f46..0000000
--- a/internal/server/resolve_test.go
+++ /dev/null
@@ -1,241 +0,0 @@
-package server
-
-import (
- "net/http"
- "net/http/httptest"
- "os"
- "path/filepath"
- "strings"
- "testing"
-
- "github.com/git-pkgs/proxy/internal/database"
- "github.com/go-chi/chi/v5"
-)
-
-func newTestDB(t *testing.T) (*database.DB, func()) {
- t.Helper()
- dir, err := os.MkdirTemp("", "resolve-test-*")
- if err != nil {
- t.Fatal(err)
- }
- db, err := database.Create(filepath.Join(dir, "test.db"))
- if err != nil {
- _ = os.RemoveAll(dir)
- t.Fatal(err)
- }
- return db, func() { _ = db.Close(); _ = os.RemoveAll(dir) }
-}
-
-func seedPackage(t *testing.T, db *database.DB, ecosystem, name, purl string) {
- t.Helper()
- if err := db.UpsertPackage(&database.Package{
- PURL: purl, Ecosystem: ecosystem, Name: name,
- }); err != nil {
- t.Fatalf("failed to upsert package %s: %v", name, err)
- }
-}
-
-func TestResolvePackageName(t *testing.T) {
- db, cleanup := newTestDB(t)
- defer cleanup()
-
- seedPackage(t, db, "npm", "lodash", "pkg:npm/lodash")
- seedPackage(t, db, "composer", "monolog/monolog", "pkg:composer/monolog/monolog")
- seedPackage(t, db, "composer", "symfony/console", "pkg:composer/symfony/console")
-
- tests := []struct {
- name string
- ecosystem string
- segments []string
- wantName string
- wantRest []string
- }{
- {
- name: "simple package", ecosystem: "npm",
- segments: []string{"lodash"}, wantName: "lodash", wantRest: nil,
- },
- {
- name: "simple package with version", ecosystem: "npm",
- segments: []string{"lodash", "4.17.21"}, wantName: "lodash", wantRest: []string{"4.17.21"},
- },
- {
- name: "namespaced package", ecosystem: "composer",
- segments: []string{"monolog", "monolog"}, wantName: "monolog/monolog", wantRest: nil,
- },
- {
- name: "namespaced package with version", ecosystem: "composer",
- segments: []string{"symfony", "console", "6.0.0"}, wantName: "symfony/console", wantRest: []string{"6.0.0"},
- },
- {
- name: "namespaced with version and action", ecosystem: "composer",
- segments: []string{"symfony", "console", "6.0.0", "browse"},
- wantName: "symfony/console", wantRest: []string{"6.0.0", "browse"},
- },
- {
- name: "not found", ecosystem: "npm",
- segments: []string{"nonexistent"}, wantName: "", wantRest: []string{"nonexistent"},
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- name, rest := resolvePackageName(db, tt.ecosystem, tt.segments)
- if name != tt.wantName {
- t.Errorf("name = %q, want %q", name, tt.wantName)
- }
- if len(rest) != len(tt.wantRest) {
- t.Errorf("rest = %v, want %v", rest, tt.wantRest)
- } else {
- for i := range rest {
- if rest[i] != tt.wantRest[i] {
- t.Errorf("rest[%d] = %q, want %q", i, rest[i], tt.wantRest[i])
- }
- }
- }
- })
- }
-}
-
-func TestSplitWildcardPath(t *testing.T) {
- tests := []struct {
- input string
- encoded bool
- want []string
- }{
- {"lodash", false, []string{"lodash"}},
- {"lodash/4.17.21", false, []string{"lodash", "4.17.21"}},
- {"monolog/monolog", false, []string{"monolog", "monolog"}},
- {"symfony/console/6.0.0/browse", false, []string{"symfony", "console", "6.0.0", "browse"}},
- {"", false, nil},
- {"/", false, nil},
- // chi routes on the raw path when it differs from the canonical
- // encoding of the decoded path, so segments arrive percent-encoded and
- // must be decoded.
- {
- "nmap/7.91%2Bdfsg1%2Breally7.80%2Bdfsg1-2ubuntu0.1", true,
- []string{"nmap", "7.91+dfsg1+really7.80+dfsg1-2ubuntu0.1"},
- },
- {"%40babel/core/7.0.0", true, []string{"@babel", "core", "7.0.0"}},
- // An encoded separator stays inside its segment rather than splitting.
- {"vendor%2Fname/1.0.0", true, []string{"vendor/name", "1.0.0"}},
- // Invalid escapes are passed through untouched.
- {"lodash/1.0%zz", true, []string{"lodash", "1.0%zz"}},
- // When chi routed on the already-decoded path, an escape that survived
- // is part of the value: a version whose text is "1.0%2B" reaches here
- // as "1.0%2B" and decoding it again would yield "1.0+".
- {"nmap/1.0%2B", false, []string{"nmap", "1.0%2B"}},
- }
-
- for _, tt := range tests {
- got := splitWildcardPath(tt.input, tt.encoded)
- if len(got) != len(tt.want) {
- t.Errorf("splitWildcardPath(%q, %v) = %v, want %v", tt.input, tt.encoded, got, tt.want)
- continue
- }
- for i := range got {
- if got[i] != tt.want[i] {
- t.Errorf("splitWildcardPath(%q, %v)[%d] = %q, want %q",
- tt.input, tt.encoded, i, got[i], tt.want[i])
- }
- }
- }
-}
-
-func TestValidatePackagePath(t *testing.T) {
- tests := []struct {
- name string
- path string
- wantErr bool
- }{
- {"simple", "lodash", false},
- {"with version", "lodash/4.17.21", false},
- {"npm scoped", "@babel/core/7.0.0", false},
- {"composer namespaced", "symfony/console/6.0.0", false},
- {"maven coordinates", "org.apache.commons/commons-lang3/3.12.0", false},
- {"unicode", "café/1.0.0", false},
- {"encoded plus in version", "nmap/7.91%2Bdfsg1-2ubuntu0.1", false},
- {"empty", "", true},
- {"null byte", "lodash\x00/4.17.21", true},
- {"encoded null byte", "lodash/%00", true},
- {"encoded newline", "lodash/1.0%0A", true},
- {"parent segment", "lodash/../4.17.21", true},
- {"encoded parent segment", "lodash/%2E%2E/4.17.21", true},
- // A decoded segment can contain slashes, so traversal can hide inside
- // one segment. Registries interpolate the resolved name straight into
- // an upstream URL, and Go sends dot-segments verbatim.
- {"traversal inside one segment", "pkg%2F..%2F..%2Fadmin", true},
- {"traversal via encoded dots and slash", "pkg%2f%2e%2e%2fadmin", true},
- {"encoded slash alone is allowed", "vendor%2Fname/1.0.0", false},
- {"null byte suffix", "lodash\x00", true},
- {"newline", "lodash\n4.17.21", true},
- {"carriage return", "lodash\r", true},
- {"escape", "lodash\x1b[31m", true},
- {"delete", "lodash\x7f", true},
- {"too long", strings.Repeat("a", maxPackagePathLen+1), true},
- {"at limit", strings.Repeat("a", maxPackagePathLen), false},
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- // The verdict must not depend on whether chi routed on the raw or
- // on the already-decoded path: an escape that reaches a handler
- // undecoded is decoded by the upstream registry instead, so it is
- // rejected either way.
- for _, encoded := range []bool{false, true} {
- err := validatePackagePath(tt.path, encoded)
- if (err != nil) != tt.wantErr {
- t.Errorf("validatePackagePath(%q, %v) error = %v, wantErr %v",
- tt.path, encoded, err, tt.wantErr)
- }
- }
- })
- }
-}
-
-// TestPackagePathSegments drives the real router, which is what decides whether
-// the wildcard still carries percent-encoding. Go decodes the request path
-// itself unless the escaping is non-canonical, so the same version can arrive
-// either way and only one of the two forms may be decoded again.
-func TestPackagePathSegments(t *testing.T) {
- tests := []struct {
- name string
- target string
- want []string
- }{
- {"plain", "/pkg/npm/lodash/4.17.21", []string{"lodash", "4.17.21"}},
- {"encoded plus", "/pkg/deb/nmap/7.91%2Bdfsg1-2ubuntu0.1", []string{"nmap", "7.91+dfsg1-2ubuntu0.1"}},
- {"decoded plus", "/pkg/deb/nmap/7.91+dfsg1-2ubuntu0.1", []string{"nmap", "7.91+dfsg1-2ubuntu0.1"}},
- // An encoded slash is one segment, not a separator.
- {"encoded slash", "/pkg/composer/vendor%2Fname/1.0.0", []string{"vendor/name", "1.0.0"}},
- {"question mark", "/pkg/npm/example/v1%3Fbuild", []string{"example", "v1?build"}},
- // "1.0%252B" is the escaped form of the version "1.0%2B"; net/url
- // already decoded it once, so it must not be decoded again.
- {"literal percent escape", "/pkg/npm/example/1.0%252B", []string{"example", "1.0%2B"}},
- {"browse suffix", "/pkg/deb/nmap/7.91%2Bdfsg1/browse", []string{"nmap", "7.91+dfsg1", "browse"}},
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- var got []string
- var gotErr error
-
- router := chi.NewRouter()
- router.Get("/pkg/{ecosystem}/*", func(_ http.ResponseWriter, r *http.Request) {
- got, gotErr = packagePathSegments(r)
- })
- router.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("GET", tt.target, nil))
-
- if gotErr != nil {
- t.Fatalf("packagePathSegments(%q) failed: %v", tt.target, gotErr)
- }
- if len(got) != len(tt.want) {
- t.Fatalf("segments for %q = %v, want %v", tt.target, got, tt.want)
- }
- for i := range got {
- if got[i] != tt.want[i] {
- t.Errorf("segments for %q [%d] = %q, want %q", tt.target, i, got[i], tt.want[i])
- }
- }
- })
- }
-}
diff --git a/internal/server/server.go b/internal/server/server.go
index e82b626..957b1d1 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -9,34 +9,20 @@
// - /pub/* - pub.dev registry protocol
// - /pypi/* - PyPI registry protocol
// - /maven/* - Maven repository protocol
-// - /gradle/* - Gradle HttpBuildCache protocol
// - /nuget/* - NuGet V3 API protocol
// - /composer/* - Composer/Packagist protocol
// - /conan/* - Conan C/C++ protocol
// - /conda/* - Conda/Anaconda protocol
// - /cran/* - CRAN (R) protocol
-// - /julia/* - Julia Pkg server protocol
-// - /swift/* - Swift Package Registry protocol
// - /v2/* - OCI/Docker container registry protocol
-// - /apk/* - Alpine APK repository protocol
// - /debian/* - Debian/APT repository protocol
// - /rpm/* - RPM/Yum repository protocol
//
// Additional endpoints:
-// - /health - Health check endpoint
-// - /stats - Cache statistics (JSON)
-// - /openapi.json - OpenAPI spec (JSON)
-// - /metrics - Prometheus metrics
-//
-// Web UI (HTML), mounted under /ui so reverse proxies can gate it
-// separately from the package endpoints:
-// - /ui/ - Dashboard
-// - /ui/install - Client configuration guide
-// - /ui/packages - List all cached packages
-// - /ui/search - Search packages
-// - /ui/package/... - Package and version detail pages
-// - /ui/api/browse/... - Archive browsing (used by the UI)
-// - /ui/api/compare/... - Archive diffing (used by the UI)
+// - /health - Health check endpoint
+// - /stats - Cache statistics (JSON)
+// - /packages - List all cached packages (HTML)
+// - /search - Search packages (HTML)
//
// API endpoints for enrichment data:
// - GET /api/package/{ecosystem}/{name} - Package metadata
@@ -49,88 +35,40 @@
package server
import (
- "cmp"
"context"
"database/sql"
"encoding/json"
- "errors"
"fmt"
"log/slog"
- "net"
"net/http"
- "net/url"
"strconv"
- "strings"
"time"
- "github.com/git-pkgs/cooldown"
- swaggerdoc "github.com/git-pkgs/proxy/docs/swagger"
- "github.com/git-pkgs/proxy/internal/accesslog"
"github.com/git-pkgs/proxy/internal/config"
"github.com/git-pkgs/proxy/internal/database"
"github.com/git-pkgs/proxy/internal/enrichment"
"github.com/git-pkgs/proxy/internal/handler"
- upstreamhttp "github.com/git-pkgs/proxy/internal/httpclient"
"github.com/git-pkgs/proxy/internal/metrics"
- "github.com/git-pkgs/proxy/internal/mirror"
- "github.com/git-pkgs/proxy/internal/packageurl"
- "github.com/git-pkgs/proxy/internal/scanner"
"github.com/git-pkgs/proxy/internal/storage"
+ "github.com/git-pkgs/purl"
"github.com/git-pkgs/registries/fetch"
- "github.com/git-pkgs/registries/safehttp"
"github.com/git-pkgs/spdx"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
-const (
- serverReadTimeout = 30 * time.Second
- serverWriteTimeout = 5 * time.Minute
- serverIdleTimeout = 60 * time.Second
- dashboardTopN = 10
- hoursPerDay = 24
-
- // Upstream transport defaults, matching what fetch.NewFetcher would use
- // if we did not hand it our own client. Go's default transport keeps only
- // two idle connections per host and never times out waiting for response
- // headers.
- upstreamMaxIdleConnsPerHost = 10
- upstreamResponseHeaderTimeout = 60 * time.Second
-)
-
// Server is the main proxy server.
type Server struct {
- cfg *config.Config
- db *database.DB
- storage storage.Storage
- logger *slog.Logger
- buildInfo BuildInfo
- http *http.Server
- templates *Templates
- cancel context.CancelFunc
- healthCache *healthCache
- accessLog *accesslog.Logger
- ecr *ecrTokens
- breakers *breakerMonitor
+ cfg *config.Config
+ db *database.DB
+ storage storage.Storage
+ logger *slog.Logger
+ http *http.Server
+ templates *Templates
}
// New creates a new Server with the given configuration.
-func New(cfg *config.Config, logger *slog.Logger, buildInfo BuildInfo) (*Server, error) {
- var activityLog *accesslog.Logger
- if cfg.AccessLog.Path != "" {
- var err error
- activityLog, err = accesslog.Open(cfg.AccessLog.Path)
- if err != nil {
- return nil, fmt.Errorf("initializing access log: %w", err)
- }
- }
- closeAccessLog := true
- defer func() {
- if closeAccessLog && activityLog != nil {
- _ = activityLog.Close()
- }
- }()
-
+func New(cfg *config.Config, logger *slog.Logger) (*Server, error) {
// Initialize database
var db *database.DB
var err error
@@ -155,7 +93,7 @@ func New(cfg *config.Config, logger *slog.Logger, buildInfo BuildInfo) (*Server,
storageURL := cfg.Storage.URL
if storageURL == "" {
// Fall back to file:// with Path
- storageURL = "file://" + cfg.Storage.Path //nolint:staticcheck // backwards compat
+ storageURL = "file://" + cfg.Storage.Path
}
store, err := storage.OpenBucket(context.Background(), storageURL)
if err != nil {
@@ -163,94 +101,29 @@ func New(cfg *config.Config, logger *slog.Logger, buildInfo BuildInfo) (*Server,
return nil, fmt.Errorf("initializing storage: %w", err)
}
- // Verify storage is accessible (catches bad S3 credentials/endpoints early).
- // Exists returns (false, nil) for a missing key, so only real connectivity
- // or permission errors surface here.
- if _, err := store.Exists(context.Background(), ".health-check"); err != nil {
- _ = store.Close()
- _ = db.Close()
- return nil, fmt.Errorf("verifying storage connectivity: %w", err)
- }
-
- hc, err := newHealthCache(store, cfg.Health.StorageProbeInterval, logger)
+ // Load templates
+ templates, err := NewTemplates()
if err != nil {
- _ = store.Close()
_ = db.Close()
- return nil, fmt.Errorf("initializing health cache: %w", err)
+ return nil, fmt.Errorf("loading templates: %w", err)
}
- server := &Server{
- cfg: cfg,
- db: db,
- storage: store,
- logger: logger,
- buildInfo: buildInfo,
- templates: &Templates{},
- healthCache: hc,
- accessLog: activityLog,
- ecr: newECRTokens(logger),
- }
- closeAccessLog = false
- return server, nil
+ return &Server{
+ cfg: cfg,
+ db: db,
+ storage: store,
+ logger: logger,
+ templates: templates,
+ }, nil
}
// Start starts the HTTP server.
-func (s *Server) Start(listeners ...net.Listener) error {
- if len(listeners) > 1 {
- return errors.New("only one listener is supported")
- }
- var listener net.Listener
- if len(listeners) == 1 {
- listener = listeners[0]
- if listener == nil {
- return errors.New("listener is required")
- }
- }
- return s.serve(listener)
-}
-
-func (s *Server) serve(listener net.Listener) error {
- // Use one authentication-aware transport for metadata and artifacts so
- // configured credentials and cached OCI challenges apply consistently.
- safeClient := newUpstreamClient(s.cfg.Upstream)
- baseTransport := safeClient.Transport
- if s.accessLog != nil {
- baseTransport = upstreamhttp.NewAccessLogTransport(baseTransport, s.accessLog, s.logger)
- }
- authTransport := upstreamhttp.NewTransport(baseTransport, upstreamhttp.AuthFunc(s.authForURL))
- metadataClient := *safeClient
- metadataClient.Timeout = s.cfg.ParseHTTPTimeout()
- metadataClient.Transport = authTransport
- artifactClient := metadataClient
- artifactClient.Timeout = serverWriteTimeout
-
- // Create shared components with circuit breaker.
- baseFetcher := fetch.NewFetcher(fetch.WithHTTPClient(&artifactClient))
+func (s *Server) Start() error {
+ // Create shared components with circuit breaker
+ baseFetcher := fetch.NewFetcher(fetch.WithAuthFunc(s.authForURL))
fetcher := fetch.NewCircuitBreakerFetcher(baseFetcher)
- s.breakers = newBreakerMonitor(fetcher, s.logger)
resolver := fetch.NewResolver()
- cd := &cooldown.Config{
- Default: s.cfg.Cooldown.Default,
- Ecosystems: s.cfg.Cooldown.Ecosystems,
- Packages: s.cfg.Cooldown.NormalizedPackages(),
- }
proxy := handler.NewProxy(s.db, s.storage, fetcher, resolver, s.logger)
- proxy.HTTPClient = &metadataClient
- proxy.AuthForURL = s.authForURL
- proxy.Cooldown = cd
- scanGroup, err := configureScanning(proxy, s.cfg.Scanning, s.cfg.BaseURL, s.logger)
- if err != nil {
- return fmt.Errorf("configuring scanners: %w", err)
- }
- proxy.CacheMetadata = s.cfg.CacheMetadata
- proxy.MetadataTTL = s.cfg.ParseMetadataTTL()
- proxy.MetadataMaxSize = s.cfg.ParseMetadataMaxSize()
- proxy.GradleReadOnly = s.cfg.Gradle.BuildCache.ReadOnly
- proxy.NPMFullMetadata = s.cfg.Upstream.NPMFullMetadata
- proxy.GradleMaxUploadSize = s.cfg.ParseGradleBuildCacheMaxUploadSize()
- proxy.DirectServe = s.cfg.Storage.DirectServe
- proxy.DirectServeTTL = s.cfg.ParseDirectServeTTL()
- proxy.DirectServeBaseURL = s.cfg.Storage.DirectServeBaseURL
// Create router with Chi
r := chi.NewRouter()
@@ -258,6 +131,7 @@ func (s *Server) serve(listener net.Listener) error {
// Add middleware
r.Use(middleware.RequestID)
r.Use(RequestIDMiddleware)
+ r.Use(middleware.RealIP)
r.Use(s.LoggerMiddleware)
r.Use(middleware.Recoverer)
r.Use(func(next http.Handler) http.Handler {
@@ -271,156 +145,22 @@ func (s *Server) serve(listener net.Listener) error {
})
// Mount protocol handlers
- s.mountProtocolHandlers(r, proxy)
-
- // Health, stats, and metrics endpoints
- r.Get("/health", s.handleHealth)
- r.Get("/stats", s.handleStats)
- r.Get("/openapi.json", s.handleOpenAPIJSON)
- r.Get("/metrics", func(w http.ResponseWriter, r *http.Request) {
- // Breaker state is only held in the fetcher, so publish it on scrape.
- s.breakers.snapshot()
- metrics.Handler().ServeHTTP(w, r)
- })
-
- // Internal route used by external scanners to pull staged artifact
- // bytes before they're committed to the cache. Only wired up when
- // scanning is actually configured, so there's no unauthenticated path
- // to storage objects sitting in the router when the feature is unused.
- // Restrict this to internal-network access only at the
- // ingress/network-policy layer.
- if scanGroup.Enabled() && len(proxy.ScanSigningKey) > 0 {
- r.Get("/_internal/scan-fetch", proxy.ServeScanFetch)
- }
-
- // Web UI. Mounted under /ui so a reverse proxy can apply different
- // access rules to it than to the package endpoints above (#123).
- r.Route("/ui", func(ui chi.Router) {
- ui.Mount("/static", http.StripPrefix("/ui/static/", staticHandler()))
- ui.Get("/", s.handleRoot)
- ui.Get("/install", s.handleInstall)
- ui.Get("/search", s.handleSearch)
- ui.Get("/packages", s.handlePackagesList)
- ui.Get("/package/{ecosystem}/*", s.handlePackagePath)
- ui.Get("/api/browse/{ecosystem}/*", s.handleBrowsePath)
- ui.Get("/api/compare/{ecosystem}/*", s.handleComparePath)
- })
- r.Get("/", func(w http.ResponseWriter, r *http.Request) {
- http.Redirect(w, r, "/ui/", http.StatusFound)
- })
-
- // API endpoints for enrichment data
- enrichSvc := enrichment.New(s.logger)
- apiHandler := NewAPIHandler(enrichSvc, s.db)
-
- r.Get("/api/package/{ecosystem}/*", apiHandler.HandlePackagePath)
- r.Get("/api/vulns/{ecosystem}/*", apiHandler.HandleVulnsPath)
- r.Post("/api/outdated", apiHandler.HandleOutdated)
- r.Post("/api/bulk", apiHandler.HandleBulkLookup)
- r.Get("/api/search", apiHandler.HandleSearch)
- r.Get("/api/packages", apiHandler.HandlePackagesList)
-
- // Start background context (used by mirror jobs and cleanup)
- bgCtx, bgCancel := context.WithCancel(context.Background())
- s.cancel = bgCancel
- s.startGradleBuildCacheEviction(bgCtx)
-
- // Mirror API endpoints (opt-in via mirror_api config or PROXY_MIRROR_API env)
- if s.cfg.MirrorAPI {
- mirrorSvc := mirror.New(proxy, s.db, s.storage, s.logger, 4) //nolint:mnd // default concurrency
- jobStore := mirror.NewJobStore(bgCtx, mirrorSvc)
- mirrorAPI := NewMirrorAPIHandler(jobStore)
- r.Post("/api/mirror", mirrorAPI.HandleCreate)
- r.Get("/api/mirror/{id}", mirrorAPI.HandleGet)
- r.Delete("/api/mirror/{id}", mirrorAPI.HandleCancel)
- go jobStore.StartCleanup(bgCtx)
- }
-
- s.http = &http.Server{
- Addr: s.cfg.Listen,
- Handler: r,
- ReadTimeout: serverReadTimeout,
- WriteTimeout: serverWriteTimeout, // Large artifacts need time
- IdleTimeout: serverIdleTimeout,
- }
-
- s.logger.Info("starting server",
- "listen", s.cfg.Listen,
- "base_url", s.cfg.BaseURL,
- "ui_url", s.cfg.UIBaseURL,
- "storage", s.storage.URL(),
- "database", s.cfg.Database.String())
- go s.updateCacheStatsMetrics()
- go s.startEvictionLoop(bgCtx)
-
- if listener != nil {
- return s.http.Serve(listener)
- }
- return s.http.ListenAndServe()
-}
-
-// mountProtocolHandlers constructs every ecosystem handler and mounts it on
-// r under its protocol prefix.
-func (s *Server) mountProtocolHandlers(r chi.Router, proxy *handler.Proxy) {
- npmHandler := handler.NewNPMHandler(proxy, s.cfg.BaseURL, s.cfg.Upstream.NPM)
- cargoHandler := handler.NewCargoHandler(
- proxy,
- s.cfg.BaseURL,
- s.cfg.Upstream.Cargo,
- s.cfg.Upstream.CargoDownload,
- )
- gemHandler := handler.NewGemHandlerWithUpstream(proxy, s.cfg.BaseURL, s.cfg.Upstream.Gem)
- goHandler := handler.NewGoHandlerWithUpstream(proxy, s.cfg.BaseURL, s.cfg.Upstream.Go)
- hexHandler := handler.NewHexHandlerWithUpstreams(
- proxy,
- s.cfg.BaseURL,
- s.cfg.Upstream.Hex,
- s.cfg.Upstream.HexAPI,
- )
- pubHandler := handler.NewPubHandlerWithUpstream(proxy, s.cfg.BaseURL, s.cfg.Upstream.Pub)
- pypiHandler := handler.NewPyPIHandlerWithUpstreams(
- proxy,
- s.cfg.BaseURL,
- s.cfg.Upstream.PyPI,
- s.cfg.Upstream.PyPIDownload,
- )
- mavenHandler := handler.NewMavenHandler(
- proxy,
- s.cfg.BaseURL,
- s.cfg.Upstream.Maven,
- s.cfg.Upstream.GradlePluginPortal,
- )
- gradleHandler := handler.NewGradleBuildCacheHandler(proxy)
- nugetHandler := handler.NewNuGetHandlerWithUpstreams(
- proxy,
- s.cfg.BaseURL,
- s.cfg.Upstream.NuGet,
- s.cfg.Upstream.NuGetSearch,
- )
- composerHandler := handler.NewComposerHandlerWithUpstreams(
- proxy,
- s.cfg.BaseURL,
- s.cfg.Upstream.Composer,
- s.cfg.Upstream.ComposerRepository,
- )
- conanHandler := handler.NewConanHandlerWithUpstream(proxy, s.cfg.BaseURL, s.cfg.Upstream.Conan)
- condaHandler := handler.NewCondaHandlerWithUpstream(proxy, s.cfg.BaseURL, s.cfg.Upstream.Conda)
- cranHandler := handler.NewCRANHandlerWithUpstream(proxy, s.cfg.BaseURL, s.cfg.Upstream.CRAN)
- juliaHandler := handler.NewJuliaHandlerWithUpstream(proxy, s.cfg.Upstream.Julia)
- swiftHandler := handler.NewSwiftHandler(proxy, s.cfg.BaseURL, s.cfg.Upstream.Swift)
- homebrewHandler := handler.NewHomebrewHandler(proxy, s.cfg.Upstream.HomebrewAPI)
- containerHandler := handler.NewContainerHandlerWithRegistry(
- proxy,
- s.cfg.BaseURL,
- s.cfg.Upstream.OCIDefault,
- s.cfg.Upstream.OCI,
- )
- handler.RegisterHomebrewArtifacts(containerHandler, s.cfg.Upstream.HomebrewArtifact)
- helmHandler := handler.NewHelmHandler(proxy, s.cfg.BaseURL, s.cfg.Upstream.Helm)
- apkHandler := handler.NewAPKHandler(proxy, s.cfg.BaseURL, s.cfg.Upstream.APK)
- debianHandler := handler.NewDebianHandler(proxy, s.cfg.BaseURL, s.cfg.Upstream.Debian)
- rpmHandler := handler.NewRPMHandlerWithUpstream(proxy, s.cfg.BaseURL, s.cfg.Upstream.RPM)
- genericHandler := handler.NewGenericHandler(proxy, s.cfg.Upstream.Generic)
+ npmHandler := handler.NewNPMHandler(proxy, s.cfg.BaseURL)
+ cargoHandler := handler.NewCargoHandler(proxy, s.cfg.BaseURL)
+ gemHandler := handler.NewGemHandler(proxy, s.cfg.BaseURL)
+ goHandler := handler.NewGoHandler(proxy, s.cfg.BaseURL)
+ hexHandler := handler.NewHexHandler(proxy, s.cfg.BaseURL)
+ pubHandler := handler.NewPubHandler(proxy, s.cfg.BaseURL)
+ pypiHandler := handler.NewPyPIHandler(proxy, s.cfg.BaseURL)
+ mavenHandler := handler.NewMavenHandler(proxy, s.cfg.BaseURL)
+ nugetHandler := handler.NewNuGetHandler(proxy, s.cfg.BaseURL)
+ composerHandler := handler.NewComposerHandler(proxy, s.cfg.BaseURL)
+ conanHandler := handler.NewConanHandler(proxy, s.cfg.BaseURL)
+ condaHandler := handler.NewCondaHandler(proxy, s.cfg.BaseURL)
+ cranHandler := handler.NewCRANHandler(proxy, s.cfg.BaseURL)
+ containerHandler := handler.NewContainerHandler(proxy, s.cfg.BaseURL)
+ debianHandler := handler.NewDebianHandler(proxy, s.cfg.BaseURL)
+ rpmHandler := handler.NewRPMHandler(proxy, s.cfg.BaseURL)
r.Mount("/npm", http.StripPrefix("/npm", npmHandler.Routes()))
r.Mount("/cargo", http.StripPrefix("/cargo", cargoHandler.Routes()))
@@ -430,55 +170,69 @@ func (s *Server) mountProtocolHandlers(r chi.Router, proxy *handler.Proxy) {
r.Mount("/pub", http.StripPrefix("/pub", pubHandler.Routes()))
r.Mount("/pypi", http.StripPrefix("/pypi", pypiHandler.Routes()))
r.Mount("/maven", http.StripPrefix("/maven", mavenHandler.Routes()))
- r.Mount("/gradle", http.StripPrefix("/gradle", gradleHandler.Routes()))
r.Mount("/nuget", http.StripPrefix("/nuget", nugetHandler.Routes()))
r.Mount("/composer", http.StripPrefix("/composer", composerHandler.Routes()))
r.Mount("/conan", http.StripPrefix("/conan", conanHandler.Routes()))
r.Mount("/conda", http.StripPrefix("/conda", condaHandler.Routes()))
r.Mount("/cran", http.StripPrefix("/cran", cranHandler.Routes()))
- r.Mount("/julia", http.StripPrefix("/julia", juliaHandler.Routes()))
- r.Mount("/swift", http.StripPrefix("/swift", swiftHandler.Routes()))
- r.Mount("/homebrew", http.StripPrefix("/homebrew", homebrewHandler.Routes()))
r.Mount("/v2", http.StripPrefix("/v2", containerHandler.Routes()))
- r.Mount("/helm", http.StripPrefix("/helm", helmHandler.Routes()))
- r.Mount("/apk", http.StripPrefix("/apk", apkHandler.Routes()))
r.Mount("/debian", http.StripPrefix("/debian", debianHandler.Routes()))
r.Mount("/rpm", http.StripPrefix("/rpm", rpmHandler.Routes()))
- r.Mount("/generic", http.StripPrefix("/generic", genericHandler.Routes()))
-}
-// configureScanning builds the scanner group from cfg and wires it into
-// proxy, returning the group so the caller can decide whether to mount the
-// internal scan-fetch route.
-func configureScanning(proxy *handler.Proxy, cfg config.ScanningConfig, baseURL string, logger *slog.Logger) (*scanner.Group, error) {
- scanGroup, err := scanner.NewGroup(cfg, logger)
- if err != nil {
- return nil, err
- }
- proxy.Scanners = scanGroup
- proxy.ScanSigningKey = []byte(cfg.SigningKeyExpanded())
- proxy.ScanFetchBaseURL = cmp.Or(cfg.FetchBaseURL, baseURL)
- return scanGroup, nil
-}
+ // Health, stats, and static endpoints
+ r.Get("/health", s.handleHealth)
+ r.Get("/stats", s.handleStats)
+ r.Get("/metrics", func(w http.ResponseWriter, r *http.Request) {
+ metrics.Handler().ServeHTTP(w, r)
+ })
+ r.Mount("/static", http.StripPrefix("/static/", staticHandler()))
+ r.Get("/", s.handleRoot)
+ r.Get("/install", s.handleInstall)
+ r.Get("/search", s.handleSearch)
+ r.Get("/packages", s.handlePackagesList)
+ r.Get("/package/{ecosystem}/{name}", s.handlePackageShow)
+ r.Get("/package/{ecosystem}/{name}/{version}", s.handleVersionShow)
+ r.Get("/package/{ecosystem}/{name}/{version}/browse", s.handleBrowseSource)
-// newUpstreamClient builds the shared upstream client: a safehttp client whose
-// transport keeps upstreamMaxIdleConnsPerHost idle connections per host and
-// gives up after upstreamResponseHeaderTimeout when an upstream accepts a
-// request but stalls before sending headers.
-func newUpstreamClient(upstream config.UpstreamConfig) *http.Client {
- client := safehttp.New(nil, upstreamSafeHTTPOptions(upstream))
- if transport, ok := client.Transport.(*http.Transport); ok {
- transport.MaxIdleConnsPerHost = upstreamMaxIdleConnsPerHost
- transport.ResponseHeaderTimeout = upstreamResponseHeaderTimeout
- }
- return client
-}
+ // API endpoints for enrichment data
+ enrichSvc := enrichment.New(s.logger)
+ apiHandler := NewAPIHandler(enrichSvc, s.db)
-func upstreamSafeHTTPOptions(upstream config.UpstreamConfig) safehttp.Options {
- return safehttp.Options{
- AllowLoopback: upstream.AllowLoopback,
- AllowPrivateHosts: upstream.AllowPrivateHosts,
+ r.Get("/api/package/{ecosystem}/{name}", apiHandler.HandleGetPackage)
+ r.Get("/api/package/{ecosystem}/{name}/{version}", apiHandler.HandleGetVersion)
+ r.Get("/api/vulns/{ecosystem}/{name}", apiHandler.HandleGetVulns)
+ r.Get("/api/vulns/{ecosystem}/{name}/{version}", apiHandler.HandleGetVulns)
+ r.Post("/api/outdated", apiHandler.HandleOutdated)
+ r.Post("/api/bulk", apiHandler.HandleBulkLookup)
+ r.Get("/api/search", apiHandler.HandleSearch)
+ r.Get("/api/packages", apiHandler.HandlePackagesList)
+
+ // Archive browsing endpoints
+ r.Get("/api/browse/{ecosystem}/{name}/{version}", s.handleBrowseList)
+ r.Get("/api/browse/{ecosystem}/{name}/{version}/file/*", s.handleBrowseFile)
+
+ // Version comparison endpoints
+ r.Get("/api/compare/{ecosystem}/{name}/{fromVersion}/{toVersion}", s.handleCompareDiff)
+ r.Get("/package/{ecosystem}/{name}/compare/{versions}", s.handleComparePage)
+
+ s.http = &http.Server{
+ Addr: s.cfg.Listen,
+ Handler: r,
+ ReadTimeout: 30 * time.Second,
+ WriteTimeout: 5 * time.Minute, // Large artifacts need time
+ IdleTimeout: 60 * time.Second,
}
+
+ s.logger.Info("starting server",
+ "listen", s.cfg.Listen,
+ "base_url", s.cfg.BaseURL,
+ "storage", s.cfg.Storage.Path,
+ "database", s.cfg.Database.Path)
+
+ // Start background goroutine to update cache stats metrics
+ go s.updateCacheStatsMetrics()
+
+ return s.http.ListenAndServe()
}
// updateCacheStatsMetrics periodically updates cache statistics in Prometheus metrics.
@@ -507,10 +261,6 @@ func (s *Server) updateCacheStats() {
func (s *Server) Shutdown(ctx context.Context) error {
s.logger.Info("shutting down server")
- if s.cancel != nil {
- s.cancel()
- }
-
var errs []error
if s.http != nil {
@@ -519,18 +269,6 @@ func (s *Server) Shutdown(ctx context.Context) error {
}
}
- if s.storage != nil {
- if err := s.storage.Close(); err != nil {
- errs = append(errs, fmt.Errorf("storage close: %w", err))
- }
- }
-
- if s.accessLog != nil {
- if err := s.accessLog.Close(); err != nil {
- errs = append(errs, fmt.Errorf("access log close: %w", err))
- }
- }
-
if s.db != nil {
if err := s.db.Close(); err != nil {
errs = append(errs, fmt.Errorf("database close: %w", err))
@@ -549,13 +287,6 @@ func (s *Server) authForURL(url string) (headerName, headerValue string) {
if auth == nil {
return "", ""
}
- if strings.EqualFold(auth.Type, "ecr") {
- region := auth.Region
- if region == "" {
- region = ecrRegion(url)
- }
- return s.ecr.header(region)
- }
return auth.Header()
}
@@ -575,20 +306,19 @@ func (s *Server) handleRoot(w http.ResponseWriter, r *http.Request) {
}
// Get popular packages
- popular, err := s.db.GetMostPopularPackages(dashboardTopN)
+ popular, err := s.db.GetMostPopularPackages(10)
if err != nil {
s.logger.Error("failed to get popular packages", "error", err)
}
// Get recent packages
- recent, err := s.db.GetRecentlyCachedPackages(dashboardTopN)
+ recent, err := s.db.GetRecentlyCachedPackages(10)
if err != nil {
s.logger.Error("failed to get recent packages", "error", err)
}
// Build dashboard data
data := DashboardData{
- Layout: s.layoutFor(r),
Stats: DashboardStats{
CachedArtifacts: stats.TotalArtifacts,
TotalSize: formatSize(stats.TotalSize),
@@ -668,19 +398,10 @@ func (s *Server) handleRoot(w http.ResponseWriter, r *http.Request) {
}
}
-func (s *Server) handleOpenAPIJSON(w http.ResponseWriter, _ *http.Request) {
- w.Header().Set("Content-Type", "application/json; charset=utf-8")
- _, _ = w.Write([]byte(swaggerdoc.SwaggerInfo.ReadDoc()))
-}
-
func (s *Server) handleInstall(w http.ResponseWriter, r *http.Request) {
data := struct {
- Layout
- BaseURL string
Registries []RegistryConfig
}{
- Layout: s.layoutFor(r),
- BaseURL: s.cfg.BaseURL,
Registries: getRegistryConfigs(s.cfg.BaseURL),
}
@@ -694,7 +415,7 @@ func (s *Server) handleSearch(w http.ResponseWriter, r *http.Request) {
ecosystem := r.URL.Query().Get("ecosystem")
if query == "" {
- http.Redirect(w, r, "/ui/", http.StatusSeeOther)
+ http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
@@ -743,7 +464,6 @@ func (s *Server) handleSearch(w http.ResponseWriter, r *http.Request) {
totalPages := int((total + int64(limit) - 1) / int64(limit))
data := SearchPageData{
- Layout: s.layoutFor(r),
Query: query,
Ecosystem: ecosystem,
Results: items,
@@ -762,7 +482,7 @@ func (s *Server) handlePackagesList(w http.ResponseWriter, r *http.Request) {
ecosystem := r.URL.Query().Get("ecosystem")
sortBy := r.URL.Query().Get("sort")
if sortBy == "" {
- sortBy = defaultSortBy
+ sortBy = "hits"
}
page := 1
@@ -803,23 +523,22 @@ func (s *Server) handlePackagesList(w http.ResponseWriter, r *http.Request) {
}
}
items[i] = SearchResultItem{
- Ecosystem: pkg.Ecosystem,
- Name: pkg.Name,
- LatestVersion: latestVersion,
- License: license,
+ Ecosystem: pkg.Ecosystem,
+ Name: pkg.Name,
+ LatestVersion: latestVersion,
+ License: license,
LicenseCategory: categorizeLicenseCSS(license),
- Hits: pkg.Hits,
- Size: pkg.Size,
- SizeFormatted: formatSize(pkg.Size),
- CachedAt: cachedAt,
- VulnCount: pkg.VulnCount,
+ Hits: pkg.Hits,
+ Size: pkg.Size,
+ SizeFormatted: formatSize(pkg.Size),
+ CachedAt: cachedAt,
+ VulnCount: pkg.VulnCount,
}
}
totalPages := int((total + int64(limit) - 1) / int64(limit))
data := PackagesListPageData{
- Layout: s.layoutFor(r),
Ecosystem: ecosystem,
SortBy: sortBy,
Results: items,
@@ -834,74 +553,15 @@ func (s *Server) handlePackagesList(w http.ResponseWriter, r *http.Request) {
}
}
-// handlePackagePath dispatches wildcard package routes to the appropriate handler.
-// It resolves namespaced package names (e.g., Composer vendor/name) by consulting
-// the database to determine which path segments are part of the package name.
-//
-// Supported paths:
-//
-// {name} -> package show
-// {name}/{version} -> version show
-// {name}/{version}/browse -> browse source
-// {name}/compare/{v1}...{v2} -> compare versions
-func (s *Server) handlePackagePath(w http.ResponseWriter, r *http.Request) {
+func (s *Server) handlePackageShow(w http.ResponseWriter, r *http.Request) {
ecosystem := chi.URLParam(r, "ecosystem")
- segments, err := packagePathSegments(r)
- if err != nil {
- http.Error(w, err.Error(), http.StatusBadRequest)
+ name := chi.URLParam(r, "name")
+
+ if ecosystem == "" || name == "" {
+ http.Error(w, "ecosystem and name required", http.StatusBadRequest)
return
}
- if ecosystem == "" || len(segments) == 0 {
- http.Error(w, "ecosystem and package name required", http.StatusBadRequest)
- return
- }
-
- // Check for compare route: {name}/compare/{versions}
- for i, seg := range segments {
- if seg == "compare" && i > 0 && i < len(segments)-1 {
- name := strings.Join(segments[:i], "/")
- versions := strings.Join(segments[i+1:], "/")
- s.showComparePage(w, r, ecosystem, name, versions)
- return
- }
- }
-
- // Check for browse suffix
- browse := false
- if len(segments) > 1 && segments[len(segments)-1] == "browse" {
- browse = true
- segments = segments[:len(segments)-1]
- }
-
- // Resolve package name from the remaining segments using DB lookup.
- name, rest := resolvePackageName(s.db, ecosystem, segments)
-
- if name == "" {
- // No package found in DB. Fall back to heuristic: assume the last
- // segment is a version (if present) and everything else is the name.
- if len(segments) == 1 {
- // Single segment, no DB match: try package show (will 404).
- s.showPackage(w, r, ecosystem, segments[0])
- return
- }
- name = strings.Join(segments[:len(segments)-1], "/")
- rest = segments[len(segments)-1:]
- }
-
- switch {
- case len(rest) == 0 && !browse:
- s.showPackage(w, r, ecosystem, name)
- case len(rest) == 1 && browse:
- s.showBrowseSource(w, r, ecosystem, name, rest[0])
- case len(rest) == 1:
- s.showVersion(w, r, ecosystem, name, rest[0])
- default:
- http.Error(w, "not found", http.StatusNotFound)
- }
-}
-
-func (s *Server) showPackage(w http.ResponseWriter, r *http.Request, ecosystem, name string) {
pkg, err := s.db.GetPackageByEcosystemName(ecosystem, name)
if err != nil {
s.logger.Error("failed to get package", "error", err, "ecosystem", ecosystem, "name", name)
@@ -926,7 +586,6 @@ func (s *Server) showPackage(w http.ResponseWriter, r *http.Request, ecosystem,
}
data := PackageShowData{
- Layout: s.layoutFor(r),
Package: pkg,
Versions: versions,
Vulnerabilities: vulns,
@@ -938,7 +597,16 @@ func (s *Server) showPackage(w http.ResponseWriter, r *http.Request, ecosystem,
}
}
-func (s *Server) showVersion(w http.ResponseWriter, r *http.Request, ecosystem, name, version string) {
+func (s *Server) handleVersionShow(w http.ResponseWriter, r *http.Request) {
+ ecosystem := chi.URLParam(r, "ecosystem")
+ name := chi.URLParam(r, "name")
+ version := chi.URLParam(r, "version")
+
+ if ecosystem == "" || name == "" || version == "" {
+ http.Error(w, "ecosystem, name, and version required", http.StatusBadRequest)
+ return
+ }
+
pkg, err := s.db.GetPackageByEcosystemName(ecosystem, name)
if err != nil || pkg == nil {
s.logger.Error("failed to get package", "error", err)
@@ -946,7 +614,7 @@ func (s *Server) showVersion(w http.ResponseWriter, r *http.Request, ecosystem,
return
}
- versionPURL := packageurl.WithVersionString(pkg.PURL, version)
+ versionPURL := purl.MakePURLString(ecosystem, name, version)
ver, err := s.db.GetVersionByPURL(versionPURL)
if err != nil || ver == nil {
s.logger.Error("failed to get version", "error", err)
@@ -968,12 +636,16 @@ func (s *Server) showVersion(w http.ResponseWriter, r *http.Request, ecosystem,
isOutdated := pkg.LatestVersion.Valid && pkg.LatestVersion.String != version
- // A version whose only cached artifact is a metadata sidecar cannot be
- // browsed, so it must not be advertised as cached.
- hasCached := firstBrowsableArtifact(artifacts) != nil
+ // Check if any artifact is cached
+ hasCached := false
+ for _, art := range artifacts {
+ if art.StoragePath.Valid {
+ hasCached = true
+ break
+ }
+ }
data := VersionShowData{
- Layout: s.layoutFor(r),
Package: pkg,
Version: ver,
Artifacts: artifacts,
@@ -988,100 +660,16 @@ func (s *Server) showVersion(w http.ResponseWriter, r *http.Request, ecosystem,
}
}
-func (s *Server) cachedVersionPURL(ecosystem, name, version string) string {
- pkg, err := s.db.GetPackageByEcosystemName(ecosystem, name)
- if err != nil || pkg == nil {
- return ""
- }
- return packageurl.WithVersionString(pkg.PURL, version)
-}
-
-func (s *Server) showBrowseSource(w http.ResponseWriter, r *http.Request, ecosystem, name, version string) {
- data := BrowseSourceData{
- Layout: s.layoutFor(r),
- Ecosystem: ecosystem,
- PackageName: name,
- Version: version,
- EscapedVersion: url.PathEscape(version),
- }
-
- if err := s.templates.Render(w, "browse_source", data); err != nil {
- s.logger.Error("failed to render browse source page", "error", err)
- http.Error(w, "internal server error", http.StatusInternalServerError)
- }
-}
-
-func (s *Server) showComparePage(w http.ResponseWriter, r *http.Request, ecosystem, name, versions string) {
- const compareVersionParts = 2
- parts := strings.Split(versions, "...")
- if len(parts) != compareVersionParts {
- http.Error(w, "invalid version format, use: version1...version2", http.StatusBadRequest)
- return
- }
-
- data := ComparePageData{
- Layout: s.layoutFor(r),
- Ecosystem: ecosystem,
- PackageName: name,
- FromVersion: parts[0],
- ToVersion: parts[1],
- EscapedFromVersion: url.PathEscape(parts[0]),
- EscapedToVersion: url.PathEscape(parts[1]),
- }
-
- if err := s.templates.Render(w, "compare_versions", data); err != nil {
- s.logger.Error("failed to render compare page", "error", err)
- http.Error(w, "internal server error", http.StatusInternalServerError)
- }
-}
-
-// handleHealth responds with a structured JSON health report.
-//
-// @Summary Health check
-// @Tags meta
-// @Produce json
-// @Success 200 {object} HealthResponse
-// @Failure 503 {object} HealthResponse
-// @Router /health [get]
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Content-Type", "application/json")
-
- resp := HealthResponse{
- Status: "ok",
- Checks: map[string]HealthCheck{},
- CircuitBreakers: s.breakers.snapshot(),
- }
-
- // Database check (short-circuit; do not waste a storage probe call when DB is down).
- // On DB failure the storage entry reports "skipped" rather than being omitted so
- // the response always carries the same key set for monitors that expect it.
+ // Check database connectivity
if _, err := s.db.SchemaVersion(); err != nil {
- resp.Status = "error"
- resp.Checks["database"] = HealthCheck{Status: "error", Error: err.Error()}
- resp.Checks["storage"] = HealthCheck{Status: "skipped"}
w.WriteHeader(http.StatusServiceUnavailable)
- _ = json.NewEncoder(w).Encode(resp)
+ _, _ = fmt.Fprintf(w, "database error: %v", err)
return
}
- resp.Checks["database"] = HealthCheck{Status: "ok"}
-
- // Storage probe (via cache).
- if err := s.healthCache.Check(); err != nil {
- resp.Status = "error"
- sc := HealthCheck{Status: "error", Error: err.Error()}
- var pe *probeError
- if errors.As(err, &pe) {
- sc.Step = pe.step
- }
- resp.Checks["storage"] = sc
- w.WriteHeader(http.StatusServiceUnavailable)
- _ = json.NewEncoder(w).Encode(resp)
- return
- }
- resp.Checks["storage"] = HealthCheck{Status: "ok"}
w.WriteHeader(http.StatusOK)
- _ = json.NewEncoder(w).Encode(resp)
+ _, _ = fmt.Fprint(w, "ok")
}
// StatsResponse contains cache statistics.
@@ -1089,29 +677,22 @@ type StatsResponse struct {
CachedArtifacts int64 `json:"cached_artifacts"`
TotalSize int64 `json:"total_size_bytes"`
TotalSizeHuman string `json:"total_size"`
- StorageURL string `json:"storage_url"`
+ StoragePath string `json:"storage_path"`
DatabasePath string `json:"database_path"`
}
-// handleStats returns cache statistics.
-// @Summary Cache statistics
-// @Tags meta
-// @Produce json
-// @Success 200 {object} StatsResponse
-// @Failure 500 {object} ErrorResponse
-// @Router /stats [get]
func (s *Server) handleStats(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
count, err := s.db.GetCachedArtifactCount()
if err != nil {
- internalError(w, "failed to get artifact count")
+ http.Error(w, "failed to get artifact count", http.StatusInternalServerError)
return
}
size, err := s.db.GetTotalCacheSize()
if err != nil {
- internalError(w, "failed to get cache size")
+ http.Error(w, "failed to get cache size", http.StatusInternalServerError)
return
}
@@ -1121,8 +702,8 @@ func (s *Server) handleStats(w http.ResponseWriter, r *http.Request) {
CachedArtifacts: count,
TotalSize: size,
TotalSizeHuman: formatSize(size),
- StorageURL: s.storage.URL(),
- DatabasePath: s.cfg.Database.String(),
+ StoragePath: s.cfg.Storage.Path,
+ DatabasePath: s.cfg.Database.Path,
}
w.Header().Set("Content-Type", "application/json")
@@ -1156,14 +737,14 @@ func formatTimeAgo(t time.Time) string {
return "1 min ago"
}
return fmt.Sprintf("%d mins ago", m)
- case d < hoursPerDay*time.Hour:
+ case d < 24*time.Hour:
h := int(d.Hours())
if h == 1 {
return "1 hour ago"
}
return fmt.Sprintf("%d hours ago", h)
- case d < 7*hoursPerDay*time.Hour:
- days := int(d.Hours() / hoursPerDay)
+ case d < 7*24*time.Hour:
+ days := int(d.Hours() / 24)
if days == 1 {
return "1 day ago"
}
@@ -1176,7 +757,7 @@ func formatTimeAgo(t time.Time) string {
// categorizeLicenseCSS returns the CSS class suffix for a license category using the spdx module.
func categorizeLicenseCSS(license string) string {
if license == "" {
- return licenseCategoryUnknown
+ return "unknown"
}
if spdx.HasCopyleft(license) {
@@ -1187,13 +768,13 @@ func categorizeLicenseCSS(license string) string {
return "permissive"
}
- return licenseCategoryUnknown
+ return "unknown"
}
// categorizeLicense is a helper that handles sql.NullString.
func categorizeLicense(license sql.NullString) string {
if !license.Valid {
- return licenseCategoryUnknown
+ return "unknown"
}
return categorizeLicenseCSS(license.String)
}
@@ -1208,3 +789,4 @@ func (rw *responseWriter) WriteHeader(code int) {
rw.status = code
rw.ResponseWriter.WriteHeader(code)
}
+
diff --git a/internal/server/server_test.go b/internal/server/server_test.go
index d6bd20f..44498f9 100644
--- a/internal/server/server_test.go
+++ b/internal/server/server_test.go
@@ -1,40 +1,27 @@
package server
import (
- "context"
"database/sql"
"encoding/json"
- "errors"
- "fmt"
- "html"
"io"
"log/slog"
- "net"
"net/http"
"net/http/httptest"
- "net/url"
"os"
- "os/exec"
"path/filepath"
- "regexp"
- "strconv"
"strings"
"testing"
- "time"
"github.com/git-pkgs/proxy/internal/config"
"github.com/git-pkgs/proxy/internal/database"
"github.com/git-pkgs/proxy/internal/handler"
"github.com/git-pkgs/proxy/internal/storage"
- "github.com/git-pkgs/purl"
"github.com/git-pkgs/registries/fetch"
- "github.com/git-pkgs/registries/safehttp"
"github.com/go-chi/chi/v5"
)
type testServer struct {
handler http.Handler
- server *Server
db *database.DB
storage storage.Storage
tempDir string
@@ -57,7 +44,7 @@ func newTestServer(t *testing.T) *testServer {
t.Fatalf("failed to create database: %v", err)
}
- store, err := storage.OpenBucket(context.Background(), "file://"+storagePath)
+ store, err := storage.NewFilesystem(storagePath)
if err != nil {
_ = db.Close()
_ = os.RemoveAll(tempDir)
@@ -70,73 +57,58 @@ func newTestServer(t *testing.T) *testServer {
proxy := handler.NewProxy(db, store, fetcher, resolver, logger)
cfg := &config.Config{
- BaseURL: "http://localhost:8080",
- Storage: config.StorageConfig{URL: "file://" + storagePath},
+ BaseURL: "http://localhost:8080",
+ Storage: config.StorageConfig{Path: storagePath},
Database: config.DatabaseConfig{Path: dbPath},
}
r := chi.NewRouter()
// Mount handlers
- npmHandler := handler.NewNPMHandler(proxy, cfg.BaseURL, cfg.Upstream.NPM)
- cargoHandler := handler.NewCargoHandler(
- proxy,
- cfg.BaseURL,
- cfg.Upstream.Cargo,
- cfg.Upstream.CargoDownload,
- )
+ npmHandler := handler.NewNPMHandler(proxy, cfg.BaseURL)
+ cargoHandler := handler.NewCargoHandler(proxy, cfg.BaseURL)
gemHandler := handler.NewGemHandler(proxy, cfg.BaseURL)
goHandler := handler.NewGoHandler(proxy, cfg.BaseURL)
pypiHandler := handler.NewPyPIHandler(proxy, cfg.BaseURL)
- gradleHandler := handler.NewGradleBuildCacheHandler(proxy)
- swiftHandler := handler.NewSwiftHandler(proxy, cfg.BaseURL, cfg.Upstream.Swift)
r.Mount("/npm", http.StripPrefix("/npm", npmHandler.Routes()))
r.Mount("/cargo", http.StripPrefix("/cargo", cargoHandler.Routes()))
r.Mount("/gem", http.StripPrefix("/gem", gemHandler.Routes()))
r.Mount("/go", http.StripPrefix("/go", goHandler.Routes()))
r.Mount("/pypi", http.StripPrefix("/pypi", pypiHandler.Routes()))
- r.Mount("/gradle", http.StripPrefix("/gradle", gradleHandler.Routes()))
- r.Mount("/swift", http.StripPrefix("/swift", swiftHandler.Routes()))
- hc, err := newHealthCache(store, "30s", logger)
+ // Load templates
+ templates, err := NewTemplates()
if err != nil {
_ = db.Close()
_ = os.RemoveAll(tempDir)
- t.Fatalf("failed to create health cache: %v", err)
+ t.Fatalf("failed to load templates: %v", err)
}
// Create a minimal server struct for the handlers
s := &Server{
- cfg: cfg,
- db: db,
- storage: store,
- logger: logger,
- buildInfo: BuildInfo{Version: "test-version", Commit: "test-commit"},
- templates: &Templates{},
- healthCache: hc,
+ cfg: cfg,
+ db: db,
+ storage: store,
+ logger: logger,
+ templates: templates,
}
r.Get("/health", s.handleHealth)
r.Get("/stats", s.handleStats)
- r.Get("/openapi.json", s.handleOpenAPIJSON)
- r.Route("/ui", func(ui chi.Router) {
- ui.Mount("/static", http.StripPrefix("/ui/static/", staticHandler()))
- ui.Get("/", s.handleRoot)
- ui.Get("/install", s.handleInstall)
- ui.Get("/search", s.handleSearch)
- ui.Get("/packages", s.handlePackagesList)
- ui.Get("/package/{ecosystem}/*", s.handlePackagePath)
- ui.Get("/api/browse/{ecosystem}/*", s.handleBrowsePath)
- ui.Get("/api/compare/{ecosystem}/*", s.handleComparePath)
- })
- r.Get("/", func(w http.ResponseWriter, r *http.Request) {
- http.Redirect(w, r, "/ui/", http.StatusFound)
- })
+ r.Mount("/static", http.StripPrefix("/static/", staticHandler()))
+ r.Get("/search", s.handleSearch)
+ r.Get("/package/{ecosystem}/{name}", s.handlePackageShow)
+ r.Get("/package/{ecosystem}/{name}/{version}", s.handleVersionShow)
+ r.Get("/package/{ecosystem}/{name}/{version}/browse", s.handleBrowseSource)
+ r.Get("/api/browse/{ecosystem}/{name}/{version}", s.handleBrowseList)
+ r.Get("/api/browse/{ecosystem}/{name}/{version}/file/*", s.handleBrowseFile)
+ r.Get("/api/compare/{ecosystem}/{name}/{fromVersion}/{toVersion}", s.handleCompareDiff)
+ r.Get("/package/{ecosystem}/{name}/compare/{versions}", s.handleComparePage)
+ r.Get("/", s.handleRoot)
return &testServer{
handler: r,
- server: s,
db: db,
storage: store,
tempDir: tempDir,
@@ -148,262 +120,6 @@ func (ts *testServer) close() {
_ = os.RemoveAll(ts.tempDir)
}
-func TestUpstreamSafeHTTPOptions(t *testing.T) {
- opts := upstreamSafeHTTPOptions(config.UpstreamConfig{
- AllowPrivateHosts: []string{"registry.internal"},
- AllowLoopback: true,
- })
-
- if !opts.AllowLoopback {
- t.Fatal("AllowLoopback = false, want true")
- }
- privateIP := net.ParseIP("10.0.0.12")
- if err := safehttp.CheckHostIP("registry.internal", privateIP, opts); err != nil {
- t.Fatalf("listed private upstream rejected: %v", err)
- }
- if err := safehttp.CheckHostIP("other.internal", privateIP, opts); err == nil {
- t.Fatal("unlisted private upstream was allowed")
- }
-}
-
-func TestStartUsesConfiguredLoopbackUpstreams(t *testing.T) {
- if os.Getenv("PROXY_TEST_LOOPBACK_UPSTREAM") == "1" {
- testStartUsesConfiguredLoopbackUpstreams(t)
- return
- }
-
- cmd := exec.Command(os.Args[0], "-test.run=^TestStartUsesConfiguredLoopbackUpstreams$")
- cmd.Env = append(os.Environ(), "PROXY_TEST_LOOPBACK_UPSTREAM=1")
- if output, err := cmd.CombinedOutput(); err != nil {
- t.Fatalf("configured loopback upstream test failed: %v\n%s", err, output)
- }
-}
-
-func testStartUsesConfiguredLoopbackUpstreams(t *testing.T) {
- upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- switch r.URL.Path {
- case "/pypi/simple/ruff/":
- w.Header().Set("Content-Type", "application/vnd.pypi.simple.v1+json")
- _, _ = io.WriteString(w, `{"meta":{"api-version":"1.4"},"name":"ruff","files":[]}`)
- case "/v2/library/demo/manifests/latest":
- w.Header().Set("Content-Type", "application/vnd.oci.image.manifest.v1+json")
- _, _ = io.WriteString(w, `{"schemaVersion":2}`)
- default:
- t.Errorf("unexpected upstream path: %q", r.URL.Path)
- http.NotFound(w, r)
- }
- }))
- defer upstream.Close()
-
- listener, err := net.Listen("tcp", "127.0.0.1:0")
- if err != nil {
- t.Fatalf("reserving proxy address: %v", err)
- }
- t.Cleanup(func() { _ = listener.Close() })
- listenAddress := listener.Addr().String()
-
- tempDir := t.TempDir()
- cfg := config.Default()
- cfg.Listen = listenAddress
- cfg.BaseURL = "http://" + listenAddress
- cfg.Database.Path = filepath.Join(tempDir, "proxy.db")
- cfg.Storage.URL = "file://" + filepath.Join(tempDir, "artifacts")
- cfg.Upstream.PyPI = upstream.URL + "/pypi"
- cfg.Upstream.PyPIDownload = upstream.URL + "/pypi"
- cfg.Upstream.OCIDefault = upstream.URL
- cfg.Upstream.AllowLoopback = true
- if err := cfg.Validate(); err != nil {
- t.Fatalf("validating config: %v", err)
- }
-
- logger := slog.New(slog.NewTextHandler(io.Discard, nil))
- proxyServer, err := New(cfg, logger, BuildInfo{Version: "test", Commit: "test"})
- if err != nil {
- t.Fatalf("creating server: %v", err)
- }
- startErr := make(chan error, 1)
- go func() {
- startErr <- proxyServer.Start(listener)
- }()
-
- defer func() {
- ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
- defer cancel()
- if err := proxyServer.Shutdown(ctx); err != nil {
- t.Errorf("shutting down server: %v", err)
- }
- if err := <-startErr; !errors.Is(err, http.ErrServerClosed) {
- t.Errorf("Start() error = %v, want %v", err, http.ErrServerClosed)
- }
- }()
-
- probeClient := &http.Client{Timeout: 250 * time.Millisecond}
- deadline := time.Now().Add(5 * time.Second)
- for {
- req, err := http.NewRequest(http.MethodGet, cfg.BaseURL+"/pypi/simple/ruff/", nil)
- if err != nil {
- t.Fatalf("creating request: %v", err)
- }
- req.Header.Set("Accept", "application/vnd.pypi.simple.v1+json")
- resp, requestErr := probeClient.Do(req)
- if requestErr == nil {
- body, readErr := io.ReadAll(resp.Body)
- _ = resp.Body.Close()
- if readErr != nil {
- t.Fatalf("reading response: %v", readErr)
- }
- if resp.StatusCode != http.StatusOK {
- t.Fatalf("status = %d, want %d; body: %s", resp.StatusCode, http.StatusOK, body)
- }
- if !strings.Contains(string(body), `"name":"ruff"`) {
- t.Fatalf("response body = %s, want PyPI metadata", body)
- }
- break
- }
- if time.Now().After(deadline) {
- t.Fatalf("proxy did not start: %v", requestErr)
- }
- time.Sleep(10 * time.Millisecond)
- }
-
- // This checks upstream routing, not latency. Allow time for fetching and
- // cache I/O under -race on slower CI workers.
- client := &http.Client{Timeout: 5 * time.Second}
- resp, err := client.Get(cfg.BaseURL + "/v2/library/demo/manifests/latest")
- if err != nil {
- t.Fatalf("OCI request failed: %v", err)
- }
- body, readErr := io.ReadAll(resp.Body)
- _ = resp.Body.Close()
- if readErr != nil {
- t.Fatalf("reading OCI response: %v", readErr)
- }
- if resp.StatusCode != http.StatusOK {
- t.Fatalf("OCI status = %d, want %d; body: %s", resp.StatusCode, http.StatusOK, body)
- }
- if !strings.Contains(string(body), `"schemaVersion":2`) {
- t.Fatalf("OCI response body = %s, want manifest", body)
- }
-}
-
-// TestScanFetchRouteNotMountedWhenScanningDisabled verifies the internal
-// scan-fetch route is absent (404), not merely unauthenticated, when
-// scanning is disabled: mounting it unconditionally would expose an
-// unauthenticated way to pull arbitrary storage objects by path.
-func TestScanFetchRouteNotMountedWhenScanningDisabled(t *testing.T) {
- listener, err := net.Listen("tcp", "127.0.0.1:0")
- if err != nil {
- t.Fatalf("reserving proxy address: %v", err)
- }
- t.Cleanup(func() { _ = listener.Close() })
- listenAddress := listener.Addr().String()
-
- tempDir := t.TempDir()
- cfg := config.Default()
- cfg.Listen = listenAddress
- cfg.BaseURL = "http://" + listenAddress
- cfg.Database.Path = filepath.Join(tempDir, "proxy.db")
- cfg.Storage.URL = "file://" + filepath.Join(tempDir, "artifacts")
- if err := cfg.Validate(); err != nil {
- t.Fatalf("validating config: %v", err)
- }
-
- logger := slog.New(slog.NewTextHandler(io.Discard, nil))
- proxyServer, err := New(cfg, logger, BuildInfo{Version: "test", Commit: "test"})
- if err != nil {
- t.Fatalf("creating server: %v", err)
- }
- startErr := make(chan error, 1)
- go func() {
- startErr <- proxyServer.Start(listener)
- }()
- defer func() {
- ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
- defer cancel()
- if err := proxyServer.Shutdown(ctx); err != nil {
- t.Errorf("shutting down server: %v", err)
- }
- if err := <-startErr; !errors.Is(err, http.ErrServerClosed) {
- t.Errorf("Start() error = %v, want %v", err, http.ErrServerClosed)
- }
- }()
-
- client := &http.Client{Timeout: 250 * time.Millisecond}
- deadline := time.Now().Add(5 * time.Second)
- var resp *http.Response
- for {
- var requestErr error
- resp, requestErr = client.Get(cfg.BaseURL + "/_internal/scan-fetch")
- if requestErr == nil {
- break
- }
- if time.Now().After(deadline) {
- t.Fatalf("proxy did not start: %v", requestErr)
- }
- time.Sleep(10 * time.Millisecond)
- }
- defer func() { _ = resp.Body.Close() }()
-
- if resp.StatusCode != http.StatusNotFound {
- t.Errorf("scan-fetch status = %d, want 404 when scanning is disabled", resp.StatusCode)
- }
-}
-
-// seedTestPackage creates a package, version, and artifact in the database for testing
-// page rendering. The package is created under the npm ecosystem with version 1.0.0.
-func seedTestPackage(t *testing.T, db *database.DB, name string) {
- t.Helper()
-
- pkg := &database.Package{
- PURL: "pkg:npm/" + name,
- Ecosystem: "npm",
- Name: name,
- }
- if err := db.UpsertPackage(pkg); err != nil {
- t.Fatalf("failed to upsert package: %v", err)
- }
-
- ver := &database.Version{
- PURL: "pkg:npm/" + name + "@1.0.0",
- PackagePURL: pkg.PURL,
- }
- if err := db.UpsertVersion(ver); err != nil {
- t.Fatalf("failed to upsert version: %v", err)
- }
-
- artifact := &database.Artifact{
- VersionPURL: ver.PURL,
- Filename: name + "-1.0.0.tgz",
- UpstreamURL: "https://registry.npmjs.org/" + name + "/-/" + name + "-1.0.0.tgz",
- StoragePath: sql.NullString{String: "/tmp/test.tgz", Valid: true},
- }
- if err := db.UpsertArtifact(artifact); err != nil {
- t.Fatalf("failed to upsert artifact: %v", err)
- }
-}
-
-func TestHandleOpenAPIJSON(t *testing.T) {
- ts := newTestServer(t)
- defer ts.close()
-
- req := httptest.NewRequest(http.MethodGet, "/openapi.json", nil)
- w := httptest.NewRecorder()
- ts.handler.ServeHTTP(w, req)
-
- if w.Code != http.StatusOK {
- t.Fatalf("expected status 200, got %d", w.Code)
- }
-
- contentType := w.Header().Get("Content-Type")
- if !strings.Contains(contentType, "application/json") {
- t.Fatalf("expected JSON content type, got %q", contentType)
- }
-
- if !strings.Contains(w.Body.String(), `"swagger": "2.0"`) {
- t.Fatalf("expected swagger document, got %q", w.Body.String())
- }
-}
-
func TestHealthEndpoint(t *testing.T) {
ts := newTestServer(t)
defer ts.close()
@@ -413,55 +129,12 @@ func TestHealthEndpoint(t *testing.T) {
ts.handler.ServeHTTP(w, req)
if w.Code != http.StatusOK {
- t.Fatalf("status = %d, want 200; body: %s", w.Code, w.Body.String())
+ t.Errorf("expected status 200, got %d", w.Code)
}
- if got := w.Header().Get("Content-Type"); got != "application/json" {
- t.Errorf("Content-Type = %q, want application/json", got)
- }
- var resp HealthResponse
- if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
- t.Fatalf("decoding response: %v", err)
- }
- if resp.Status != "ok" {
- t.Errorf("status = %q, want ok", resp.Status)
- }
- if resp.Checks["database"].Status != "ok" {
- t.Errorf("database check = %+v, want ok", resp.Checks["database"])
- }
- if resp.Checks["storage"].Status != "ok" {
- t.Errorf("storage check = %+v, want ok", resp.Checks["storage"])
- }
-}
-func TestHealthEndpoint_DBFailureShortCircuits(t *testing.T) {
- ts := newTestServer(t)
- defer ts.close()
-
- // Force DB failure by closing the connection.
- _ = ts.db.Close()
-
- req := httptest.NewRequest("GET", "/health", nil)
- w := httptest.NewRecorder()
- ts.handler.ServeHTTP(w, req)
-
- if w.Code != http.StatusServiceUnavailable {
- t.Fatalf("status = %d, want 503; body: %s", w.Code, w.Body.String())
- }
- var resp HealthResponse
- if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
- t.Fatalf("decoding: %v", err)
- }
- if resp.Status != "error" {
- t.Errorf("status = %q, want error", resp.Status)
- }
- if resp.Checks["database"].Status != "error" {
- t.Errorf("database check = %+v, want error", resp.Checks["database"])
- }
- storage, present := resp.Checks["storage"]
- if !present {
- t.Error("storage key should be present (with status=skipped) on DB short-circuit")
- } else if storage.Status != "skipped" {
- t.Errorf("storage check = %+v, want status=skipped", storage)
+ body := w.Body.String()
+ if body != "ok" {
+ t.Errorf("expected body 'ok', got %q", body)
}
}
@@ -490,17 +163,13 @@ func TestStatsEndpoint(t *testing.T) {
if stats.CachedArtifacts != 0 {
t.Errorf("expected 0 cached artifacts, got %d", stats.CachedArtifacts)
}
-
- if !strings.HasPrefix(stats.StorageURL, "file://") {
- t.Errorf("expected storage_url to start with file://, got %q", stats.StorageURL)
- }
}
func TestDashboard(t *testing.T) {
ts := newTestServer(t)
defer ts.close()
- req := httptest.NewRequest("GET", "/ui/", nil)
+ req := httptest.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
ts.handler.ServeHTTP(w, req)
@@ -524,69 +193,9 @@ func TestDashboard(t *testing.T) {
if !strings.Contains(body, "Cached Artifacts") {
t.Error("dashboard should contain stats")
}
- if !strings.Contains(body, "proxy test-version (test-commit)") {
- t.Error("dashboard footer should contain build information")
- }
if !strings.Contains(body, "Popular Packages") {
t.Error("dashboard should contain popular packages section")
}
- if !strings.Contains(body, ">composer<") {
- t.Error("dashboard should show composer in supported ecosystems")
- }
- if !strings.Contains(body, ">conan<") {
- t.Error("dashboard should show conan in supported ecosystems")
- }
- if !strings.Contains(body, ">container<") {
- t.Error("dashboard should show container in supported ecosystems")
- }
- if !strings.Contains(body, ">debian<") {
- t.Error("dashboard should show debian in supported ecosystems")
- }
- if !strings.Contains(body, ">swift<") {
- t.Error("dashboard should show swift in supported ecosystems")
- }
- if !strings.Contains(body, "/openapi.json") {
- t.Error("page should link to the OpenAPI JSON spec")
- }
-}
-
-func TestSwiftHandlerMounted(t *testing.T) {
- ts := newTestServer(t)
- defer ts.close()
-
- req := httptest.NewRequest(http.MethodPut, "/swift/apple/example/1.2.3", strings.NewReader("ignored"))
- w := httptest.NewRecorder()
- ts.handler.ServeHTTP(w, req)
-
- if w.Code != http.StatusMethodNotAllowed {
- t.Fatalf("status = %d, want 405; body: %s", w.Code, w.Body.String())
- }
-}
-
-func TestSwiftCachedVersionPURLUsesStoredPackagePURL(t *testing.T) {
- ts := newTestServer(t)
- defer ts.close()
-
- packagePURL := "pkg:generic/swift-registry/apple.example?repository_url=https:%2F%2Fold.example%2Fswift"
- if err := ts.db.UpsertPackage(&database.Package{
- PURL: packagePURL,
- Ecosystem: "swift",
- Name: "apple/example",
- }); err != nil {
- t.Fatalf("failed to upsert package: %v", err)
- }
-
- s := &Server{
- cfg: &config.Config{
- Upstream: config.UpstreamConfig{Swift: "https://new.example/swift"},
- },
- db: ts.db,
- }
- got := s.cachedVersionPURL("swift", "apple/example", "1.2.3")
- want := "pkg:generic/swift-registry/apple.example@1.2.3?repository_url=https:%2F%2Fold.example%2Fswift"
- if got != want {
- t.Errorf("cachedVersionPURL() = %q, want %q", got, want)
- }
}
func min(a, b int) int {
@@ -644,14 +253,10 @@ func TestGoList(t *testing.T) {
w := httptest.NewRecorder()
ts.handler.ServeHTTP(w, req)
- // The handler is mounted if we get a response from the proxy (404 from upstream
- // or 502 from connection failure), not a chi router 404.
- // With metadata caching, upstream 404 is cleanly returned as our own 404.
- if w.Code == http.StatusNotFound {
- body := w.Body.String()
- if !strings.Contains(body, "not found") {
- t.Errorf("go handler should be mounted, got status %d, body: %s", w.Code, body)
- }
+ // The handler is mounted if we get a Go proxy error (not a generic 404)
+ body := w.Body.String()
+ if w.Code == http.StatusNotFound && !strings.Contains(body, "example.com") {
+ t.Errorf("go handler should be mounted, got status %d, body: %s", w.Code, body)
}
}
@@ -668,33 +273,6 @@ func TestPyPISimple(t *testing.T) {
}
}
-func TestGradleBuildCachePutGet(t *testing.T) {
- ts := newTestServer(t)
- defer ts.close()
-
- key := "abc123def456"
- body := "build-cache-bytes"
-
- putReq := httptest.NewRequest(http.MethodPut, "/gradle/"+key, strings.NewReader(body))
- putW := httptest.NewRecorder()
- ts.handler.ServeHTTP(putW, putReq)
-
- if putW.Code != http.StatusCreated {
- t.Fatalf("expected status 201, got %d: %s", putW.Code, putW.Body.String())
- }
-
- getReq := httptest.NewRequest(http.MethodGet, "/gradle/"+key, nil)
- getW := httptest.NewRecorder()
- ts.handler.ServeHTTP(getW, getReq)
-
- if getW.Code != http.StatusOK {
- t.Fatalf("expected status 200, got %d: %s", getW.Code, getW.Body.String())
- }
- if got := getW.Body.String(); got != body {
- t.Fatalf("expected body %q, got %q", body, got)
- }
-}
-
func TestGemSpecs(t *testing.T) {
ts := newTestServer(t)
defer ts.close()
@@ -716,9 +294,8 @@ func TestStaticFiles(t *testing.T) {
path string
contentTypes []string
}{
- {"/ui/static/vendor/tailwind.js", []string{"text/javascript", "application/javascript"}},
- {"/ui/static/vendor/lucide.min.js", []string{"text/javascript", "application/javascript"}},
- {"/ui/static/style.css", []string{"text/css"}},
+ {"/static/tailwind.js", []string{"text/javascript", "application/javascript"}},
+ {"/static/style.css", []string{"text/css"}},
}
for _, tc := range tests {
@@ -769,27 +346,11 @@ func TestCategorizeLicenseCSS(t *testing.T) {
}
}
-func TestRootRedirectsToUI(t *testing.T) {
- ts := newTestServer(t)
- defer ts.close()
-
- req := httptest.NewRequest("GET", "/", nil)
- w := httptest.NewRecorder()
- ts.handler.ServeHTTP(w, req)
-
- if w.Code != http.StatusFound {
- t.Errorf("expected status 302, got %d", w.Code)
- }
- if loc := w.Header().Get("Location"); loc != "/ui/" {
- t.Errorf("expected redirect to /ui/, got %q", loc)
- }
-}
-
func TestDashboardWithEnrichmentStats(t *testing.T) {
ts := newTestServer(t)
defer ts.close()
- req := httptest.NewRequest("GET", "/ui/", nil)
+ req := httptest.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
ts.handler.ServeHTTP(w, req)
@@ -800,7 +361,7 @@ func TestDashboardWithEnrichmentStats(t *testing.T) {
body := w.Body.String()
// Dashboard should link to Tailwind JS
- if !strings.Contains(body, "/ui/static/vendor/tailwind.js") {
+ if !strings.Contains(body, "/static/tailwind.js") {
t.Error("dashboard should link to Tailwind JS")
}
@@ -841,7 +402,7 @@ func TestVersionShowWithHitCount(t *testing.T) {
t.Fatalf("failed to upsert artifact: %v", err)
}
- req := httptest.NewRequest("GET", "/ui/package/npm/test/1.0.0", nil)
+ req := httptest.NewRequest("GET", "/package/npm/test/1.0.0", nil)
w := httptest.NewRecorder()
ts.handler.ServeHTTP(w, req)
@@ -854,9 +415,6 @@ func TestVersionShowWithHitCount(t *testing.T) {
if !strings.Contains(body, "42 cache hits") {
t.Error("expected page to show hit count")
}
- if !strings.Contains(body, "proxy test-version (test-commit)") {
- t.Error("version show footer should contain proxy build information, not the package version")
- }
}
func TestSearchWithNullValues(t *testing.T) {
@@ -896,7 +454,7 @@ func TestSearchWithNullValues(t *testing.T) {
t.Fatalf("failed to upsert artifact: %v", err)
}
- req := httptest.NewRequest("GET", "/ui/search?q=test", nil)
+ req := httptest.NewRequest("GET", "/search?q=test", nil)
w := httptest.NewRecorder()
ts.handler.ServeHTTP(w, req)
@@ -910,742 +468,3 @@ func TestSearchWithNullValues(t *testing.T) {
t.Error("expected search results to contain package name")
}
}
-
-func TestFormatTimeAgo_AllRanges(t *testing.T) {
- tests := []struct {
- name string
- input time.Time
- expected string
- }{
- {"zero time", time.Time{}, ""},
- {"now", time.Now(), "just now"},
- {"30 seconds ago", time.Now().Add(-30 * time.Second), "just now"},
- {"1 minute ago", time.Now().Add(-1 * time.Minute), "1 min ago"},
- {"5 minutes ago", time.Now().Add(-5 * time.Minute), "5 mins ago"},
- {"1 hour ago", time.Now().Add(-1 * time.Hour), "1 hour ago"},
- {"3 hours ago", time.Now().Add(-3 * time.Hour), "3 hours ago"},
- {"1 day ago", time.Now().Add(-24 * time.Hour), "1 day ago"},
- {"3 days ago", time.Now().Add(-3 * 24 * time.Hour), "3 days ago"},
- {"10 days ago", time.Now().Add(-10 * 24 * time.Hour), time.Now().Add(-10 * 24 * time.Hour).Format("Jan 2")},
- }
-
- for _, tc := range tests {
- t.Run(tc.name, func(t *testing.T) {
- got := formatTimeAgo(tc.input)
- if got != tc.expected {
- t.Errorf("formatTimeAgo() = %q, want %q", got, tc.expected)
- }
- })
- }
-}
-
-func TestFormatSize_AllUnits(t *testing.T) {
- tests := []struct {
- bytes int64
- expected string
- }{
- {0, "0 B"},
- {500, "500 B"},
- {1024, "1.0 KB"},
- {1536, "1.5 KB"},
- {1048576, "1.0 MB"},
- {1073741824, "1.0 GB"},
- }
-
- for _, tc := range tests {
- t.Run(tc.expected, func(t *testing.T) {
- got := formatSize(tc.bytes)
- if got != tc.expected {
- t.Errorf("formatSize(%d) = %q, want %q", tc.bytes, got, tc.expected)
- }
- })
- }
-}
-
-func TestCategorizeLicense_NullString(t *testing.T) {
- tests := []struct {
- name string
- license sql.NullString
- expected string
- }{
- {"invalid null string", sql.NullString{Valid: false}, "unknown"},
- {"MIT", sql.NullString{String: "MIT", Valid: true}, "permissive"},
- {"GPL-3.0", sql.NullString{String: "GPL-3.0", Valid: true}, "copyleft"},
- {"empty string", sql.NullString{String: "", Valid: true}, "unknown"},
- }
-
- for _, tc := range tests {
- t.Run(tc.name, func(t *testing.T) {
- got := categorizeLicense(tc.license)
- if got != tc.expected {
- t.Errorf("categorizeLicense(%v) = %q, want %q", tc.license, got, tc.expected)
- }
- })
- }
-}
-
-func TestSearchRedirectsWhenEmpty(t *testing.T) {
- ts := newTestServer(t)
- defer ts.close()
-
- req := httptest.NewRequest("GET", "/ui/search", nil)
- w := httptest.NewRecorder()
- ts.handler.ServeHTTP(w, req)
-
- if w.Code != http.StatusSeeOther {
- t.Errorf("expected status 303, got %d", w.Code)
- }
-
- loc := w.Header().Get("Location")
- if loc != "/ui/" {
- t.Errorf("expected redirect to /ui/, got %q", loc)
- }
-}
-
-func TestPackageShowPage_NotFoundServer(t *testing.T) {
- ts := newTestServer(t)
- defer ts.close()
-
- req := httptest.NewRequest("GET", "/ui/package/npm/nonexistent-srv", nil)
- w := httptest.NewRecorder()
- ts.handler.ServeHTTP(w, req)
-
- if w.Code != http.StatusNotFound {
- t.Errorf("expected status 404, got %d", w.Code)
- }
-}
-
-func TestVersionShowPage_NotFoundServer(t *testing.T) {
- ts := newTestServer(t)
- defer ts.close()
-
- req := httptest.NewRequest("GET", "/ui/package/npm/nonexistent-srv/1.0.0", nil)
- w := httptest.NewRecorder()
- ts.handler.ServeHTTP(w, req)
-
- if w.Code != http.StatusNotFound {
- t.Errorf("expected status 404, got %d", w.Code)
- }
-}
-
-// TestVersionShowPage_PlusInVersion covers Debian/Ubuntu style versions such as
-// nmap's "7.91+dfsg1+really7.80+dfsg1-2ubuntu0.1". PURL percent-encodes "+" as
-// "%2B", so the UI must show the decoded version and resolve both the decoded
-// and the still-encoded form of the URL back to the same version.
-func TestVersionShowPage_PlusInVersion(t *testing.T) {
- ts := newTestServer(t)
- defer ts.close()
-
- const version = "7.91+dfsg1+really7.80+dfsg1-2ubuntu0.1"
- const versionPURL = "pkg:deb/nmap@7.91%2Bdfsg1%2Breally7.80%2Bdfsg1-2ubuntu0.1"
-
- pkg := &database.Package{PURL: "pkg:deb/nmap", Ecosystem: "deb", Name: "nmap"}
- if err := ts.db.UpsertPackage(pkg); err != nil {
- t.Fatalf("failed to upsert package: %v", err)
- }
- if err := ts.db.UpsertVersion(&database.Version{
- PURL: versionPURL, PackagePURL: pkg.PURL,
- }); err != nil {
- t.Fatalf("failed to upsert version: %v", err)
- }
-
- // The package page must link to and display the decoded version.
- req := httptest.NewRequest("GET", "/ui/package/deb/nmap", nil)
- w := httptest.NewRecorder()
- ts.handler.ServeHTTP(w, req)
- if w.Code != http.StatusOK {
- t.Fatalf("package page: expected status 200, got %d", w.Code)
- }
- body := w.Body.String()
- if strings.Contains(body, "%2B") {
- t.Error("package page leaks PURL percent-encoding into the UI")
- }
- // html/template renders "+" as the "+" entity inside attributes and text.
- if !strings.Contains(body, "7.91+dfsg1+really7.80+dfsg1-2ubuntu0.1") {
- t.Error("expected package page to show the decoded version")
- }
-
- // Both the decoded and the encoded URL must reach the version page.
- for _, path := range []string{
- "/ui/package/deb/nmap/" + version,
- "/ui/package/deb/nmap/7.91%2Bdfsg1%2Breally7.80%2Bdfsg1-2ubuntu0.1",
- } {
- req := httptest.NewRequest("GET", path, nil)
- w := httptest.NewRecorder()
- ts.handler.ServeHTTP(w, req)
- if w.Code != http.StatusOK {
- t.Errorf("GET %s: expected status 200, got %d", path, w.Code)
- }
- }
-}
-
-// TestVersionURLEscaping covers versions whose characters are significant in a
-// URL path: "/" splits off another path segment, "?" starts a query string, and
-// a literal "%xx" is read back as the character it encodes. The pages show the
-// decoded version but must build every link from a separately escaped value,
-// and those links have to resolve back to the same version.
-func TestVersionURLEscaping(t *testing.T) {
- // A second version is needed for the compare controls to be rendered.
- const otherVersion = "1.0.0"
-
- tests := []struct {
- name string
- version string
- }{
- {"slash", "release/1"},
- {"question mark", "v1?build"},
- {"literal percent escape", "1.0%2B"},
- {"plus", "7.91+dfsg1-2ubuntu0.1"},
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- ts := newTestServer(t)
- defer ts.close()
- seedEscapingVersions(t, ts.db, tt.version, otherVersion)
-
- // The package page links to the escaped version.
- escaped := url.PathEscape(tt.version)
- versionPath := "/ui/package/deb/nmap/" + escaped
- packagePage := ts.getOK(t, "/ui/package/deb/nmap")
- if !containsValue(attrValues(packagePage, "href"), versionPath) {
- t.Fatalf("package page has no link to %q; hrefs: %v",
- versionPath, attrValues(packagePage, "href"))
- }
-
- ts.checkVersionAndBrowsePages(t, versionPath, tt.version, escaped)
- ts.checkComparePage(t, packagePage, tt.version, escaped, otherVersion)
- })
- }
-}
-
-// seedEscapingVersions stores a Debian package with the given versions, each
-// with a cached artifact so that the version page offers its browse link.
-func seedEscapingVersions(t *testing.T, db *database.DB, versions ...string) {
- t.Helper()
-
- pkg := &database.Package{PURL: "pkg:deb/nmap", Ecosystem: "deb", Name: "nmap"}
- if err := db.UpsertPackage(pkg); err != nil {
- t.Fatalf("failed to upsert package: %v", err)
- }
- for _, v := range versions {
- versionPURL := purl.MakePURLString("deb", "nmap", v)
- if err := db.UpsertVersion(&database.Version{
- PURL: versionPURL, PackagePURL: pkg.PURL,
- }); err != nil {
- t.Fatalf("failed to upsert version %q: %v", v, err)
- }
- if err := db.UpsertArtifact(&database.Artifact{
- VersionPURL: versionPURL,
- Filename: "nmap.deb",
- UpstreamURL: "http://archive.ubuntu.com/ubuntu/pool/universe/n/nmap/nmap.deb",
- StoragePath: sql.NullString{String: "/cache/nmap.deb", Valid: true},
- FetchedAt: sql.NullTime{Time: time.Now(), Valid: true},
- }); err != nil {
- t.Fatalf("failed to upsert artifact for %q: %v", v, err)
- }
- }
-}
-
-// checkVersionAndBrowsePages follows a version link from the package page and
-// then the browse link from the version page, checking that both resolve to the
-// stored version and display it decoded.
-func (ts *testServer) checkVersionAndBrowsePages(t *testing.T, versionPath, version, escaped string) {
- t.Helper()
-
- versionPage := ts.getOK(t, versionPath)
- wantPURL := "pkg:deb/nmap@" + version
- if !strings.Contains(html.UnescapeString(versionPage), wantPURL) {
- t.Errorf("version page does not show %q", wantPURL)
- }
-
- browsePath := versionPath + "/browse"
- if !containsValue(attrValues(versionPage, "href"), browsePath) {
- t.Fatalf("version page has no browse link to %q; hrefs: %v",
- browsePath, attrValues(versionPage, "href"))
- }
-
- browsePage := ts.getOK(t, browsePath)
- if !strings.Contains(html.UnescapeString(browsePage), "nmap@"+version) {
- t.Errorf("browse page does not show the decoded version %q", version)
- }
- // The browse API is called with the escaped version, not with the text shown
- // in the heading.
- if got := jsConstant(t, browsePage, "versionPath"); got != escaped {
- t.Errorf("browse page passes %q to the browse API, want %q", got, escaped)
- }
-}
-
-// checkComparePage builds the compare URL the way the package page's script
-// does, from the values its checkboxes carry, and checks the page it reaches.
-func (ts *testServer) checkComparePage(t *testing.T, packagePage, version, escaped, otherVersion string) {
- t.Helper()
-
- selectable := attrValues(packagePage, "data-version-path")
- if !containsValue(selectable, escaped) {
- t.Fatalf("package page compare data holds %v, want %q", selectable, escaped)
- }
-
- comparePage := ts.getOK(t, "/ui/package/deb/nmap/compare/"+escaped+"..."+otherVersion)
- decoded := html.UnescapeString(comparePage)
- for _, want := range []string{version, otherVersion} {
- if !strings.Contains(decoded, want) {
- t.Errorf("compare page does not show version %q", want)
- }
- }
- if got := jsConstant(t, comparePage, "fromVersionPath"); got != escaped {
- t.Errorf("compare page passes %q to the compare API, want %q", got, escaped)
- }
-}
-
-// getOK performs a GET against the server and fails the test unless it returns
-// 200, returning the response body.
-func (ts *testServer) getOK(t *testing.T, path string) string {
- t.Helper()
-
- req := httptest.NewRequest("GET", path, nil)
- w := httptest.NewRecorder()
- ts.handler.ServeHTTP(w, req)
- if w.Code != http.StatusOK {
- t.Fatalf("GET %s: expected status 200, got %d", path, w.Code)
- }
-
- return w.Body.String()
-}
-
-// attrValues returns the value of every occurrence of an HTML attribute in a
-// rendered page, with HTML entities resolved so that values can be compared
-// against the raw strings they were built from.
-func attrValues(body, attr string) []string {
- re := regexp.MustCompile(regexp.QuoteMeta(attr) + `="([^"]*)"`)
-
- var values []string
- for _, match := range re.FindAllStringSubmatch(body, -1) {
- values = append(values, html.UnescapeString(match[1]))
- }
-
- return values
-}
-
-// jsConstant returns the value of a single-quoted JavaScript string constant in
-// a rendered page. html/template escapes characters that are significant in
-// JavaScript, rendering "+" as "\\u002b" for instance, so the escapes are
-// resolved to recover the value the page actually uses.
-func jsConstant(t *testing.T, body, name string) string {
- t.Helper()
-
- re := regexp.MustCompile(`const ` + regexp.QuoteMeta(name) + ` = '([^']*)'`)
- match := re.FindStringSubmatch(body)
- if match == nil {
- t.Fatalf("page does not declare the constant %q", name)
- }
-
- unescaped, err := strconv.Unquote(`"` + match[1] + `"`)
- if err != nil {
- t.Fatalf("cannot unescape %q: %v", match[1], err)
- }
-
- return unescaped
-}
-
-func containsValue(values []string, want string) bool {
- for _, v := range values {
- if v == want {
- return true
- }
- }
-
- return false
-}
-
-func TestPackageShowPage_WithLicense(t *testing.T) {
- ts := newTestServer(t)
- defer ts.close()
-
- pkg := &database.Package{
- PURL: "pkg:npm/show-test-lic",
- Ecosystem: "npm",
- Name: "show-test-lic",
- License: sql.NullString{String: "MIT", Valid: true},
- }
- if err := ts.db.UpsertPackage(pkg); err != nil {
- t.Fatalf("failed to upsert package: %v", err)
- }
-
- ver := &database.Version{
- PURL: "pkg:npm/show-test-lic@1.0.0",
- PackagePURL: pkg.PURL,
- }
- if err := ts.db.UpsertVersion(ver); err != nil {
- t.Fatalf("failed to upsert version: %v", err)
- }
-
- req := httptest.NewRequest("GET", "/ui/package/npm/show-test-lic", nil)
- w := httptest.NewRecorder()
- ts.handler.ServeHTTP(w, req)
-
- if w.Code != http.StatusOK {
- t.Fatalf("expected status 200, got %d", w.Code)
- }
-
- body := w.Body.String()
- if !strings.Contains(body, "show-test-lic") {
- t.Error("expected page to contain the package name")
- }
-}
-
-func TestComposerNamespacedPackageRoutes(t *testing.T) {
- ts := newTestServer(t)
- defer ts.close()
-
- // Seed two Composer packages with vendor/name format.
- for _, p := range []struct {
- purl, name, versionPURL string
- }{
- {"pkg:composer/monolog/monolog", "monolog/monolog", "pkg:composer/monolog/monolog@3.0.0"},
- {"pkg:composer/symfony/console", "symfony/console", "pkg:composer/symfony/console@6.0.0"},
- } {
- if err := ts.db.UpsertPackage(&database.Package{
- PURL: p.purl, Ecosystem: "composer", Name: p.name,
- }); err != nil {
- t.Fatalf("failed to upsert package %s: %v", p.name, err)
- }
- if err := ts.db.UpsertVersion(&database.Version{
- PURL: p.versionPURL, PackagePURL: p.purl,
- }); err != nil {
- t.Fatalf("failed to upsert version for %s: %v", p.name, err)
- }
- }
-
- tests := []struct {
- name string
- url string
- want string
- }{
- {"package show", "/ui/package/composer/monolog/monolog", "monolog/monolog"},
- {"version show", "/ui/package/composer/symfony/console/6.0.0", "symfony/console"},
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- req := httptest.NewRequest("GET", tt.url, nil)
- w := httptest.NewRecorder()
- ts.handler.ServeHTTP(w, req)
-
- if w.Code != http.StatusOK {
- t.Errorf("GET %s: expected status 200, got %d", tt.url, w.Code)
- }
- if !strings.Contains(w.Body.String(), tt.want) {
- t.Errorf("GET %s: expected body to contain %q", tt.url, tt.want)
- }
- })
- }
-}
-
-func TestNamespacedPackageRoutes(t *testing.T) {
- ts := newTestServer(t)
- defer ts.close()
-
- // Seed packages from ecosystems that use slashes in package names.
- pkgs := []struct {
- purl, ecosystem, name, versionPURL string
- }{
- // npm scoped packages
- {"pkg:npm/%40babel/core", "npm", "@babel/core", "pkg:npm/%40babel/core@7.24.0"},
- // Go modules (multi-segment paths)
- {"pkg:golang/github.com/stretchr/testify", "golang", "github.com/stretchr/testify", "pkg:golang/github.com/stretchr/testify@1.9.0"},
- // OCI/container images
- {"pkg:oci/library/nginx", "oci", "library/nginx", "pkg:oci/library/nginx@sha256:abc123"},
- // Conda (channel/name)
- {"pkg:conda/conda-forge/numpy", "conda", "conda-forge/numpy", "pkg:conda/conda-forge/numpy@1.26.4"},
- // Conan (name/version@user/channel)
- {"pkg:conan/zlib/1.2.13@demo/stable", "conan", "zlib/1.2.13@demo/stable", "pkg:conan/zlib/1.2.13@demo/stable@rev1"},
- }
-
- for _, p := range pkgs {
- if err := ts.db.UpsertPackage(&database.Package{
- PURL: p.purl, Ecosystem: p.ecosystem, Name: p.name,
- }); err != nil {
- t.Fatalf("failed to upsert package %s: %v", p.name, err)
- }
- if err := ts.db.UpsertVersion(&database.Version{
- PURL: p.versionPURL, PackagePURL: p.purl,
- }); err != nil {
- t.Fatalf("failed to upsert version for %s: %v", p.name, err)
- }
- }
-
- tests := []struct {
- name string
- url string
- want int
- }{
- {"npm scoped package show", "/ui/package/npm/@babel/core", http.StatusOK},
- {"golang module show", "/ui/package/golang/github.com/stretchr/testify", http.StatusOK},
- {"oci image show", "/ui/package/oci/library/nginx", http.StatusOK},
- {"conda package show", "/ui/package/conda/conda-forge/numpy", http.StatusOK},
- {"conan package show", "/ui/package/conan/zlib/1.2.13@demo/stable", http.StatusOK},
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- req := httptest.NewRequest("GET", tt.url, nil)
- w := httptest.NewRecorder()
- ts.handler.ServeHTTP(w, req)
-
- if w.Code != tt.want {
- t.Errorf("GET %s: expected status %d, got %d (body: %s)",
- tt.url, tt.want, w.Code, w.Body.String())
- }
- })
- }
-}
-
-func TestSearchPage_WithSeededResults(t *testing.T) {
- ts := newTestServer(t)
- defer ts.close()
-
- seedTestPackage(t, ts.db, "searchable-pkg")
-
- req := httptest.NewRequest("GET", "/ui/search?q=searchable", nil)
- w := httptest.NewRecorder()
- ts.handler.ServeHTTP(w, req)
-
- if w.Code != http.StatusOK {
- t.Fatalf("expected status 200, got %d", w.Code)
- }
-
- body := w.Body.String()
- if !strings.Contains(body, "searchable-pkg") {
- t.Error("expected search results to contain package name")
- }
-}
-
-func TestSearchPage_PaginationMultiPage(t *testing.T) {
- ts := newTestServer(t)
- defer ts.close()
-
- // Seed 55 packages to exceed one page (limit=50)
- for i := 0; i < 55; i++ {
- name := fmt.Sprintf("page-test-%03d", i)
- pkg := &database.Package{
- PURL: fmt.Sprintf("pkg:npm/%s", name),
- Ecosystem: "npm",
- Name: name,
- }
- if err := ts.db.UpsertPackage(pkg); err != nil {
- t.Fatalf("failed to upsert package %d: %v", i, err)
- }
- ver := &database.Version{
- PURL: fmt.Sprintf("pkg:npm/%s@1.0.0", name),
- PackagePURL: pkg.PURL,
- }
- if err := ts.db.UpsertVersion(ver); err != nil {
- t.Fatalf("failed to upsert version %d: %v", i, err)
- }
- artifact := &database.Artifact{
- VersionPURL: ver.PURL,
- Filename: fmt.Sprintf("%s-1.0.0.tgz", name),
- UpstreamURL: fmt.Sprintf("https://registry.npmjs.org/%s/-/%s-1.0.0.tgz", name, name),
- StoragePath: sql.NullString{String: "/tmp/test.tgz", Valid: true},
- }
- if err := ts.db.UpsertArtifact(artifact); err != nil {
- t.Fatalf("failed to upsert artifact %d: %v", i, err)
- }
- }
-
- // First page
- req := httptest.NewRequest("GET", "/ui/search?q=page-test", nil)
- w := httptest.NewRecorder()
- ts.handler.ServeHTTP(w, req)
-
- if w.Code != http.StatusOK {
- t.Fatalf("expected status 200, got %d", w.Code)
- }
-
- body := w.Body.String()
- if !strings.Contains(body, "page-test-") {
- t.Error("expected first page to contain results")
- }
-
- // Second page
- req = httptest.NewRequest("GET", "/ui/search?q=page-test&page=2", nil)
- w = httptest.NewRecorder()
- ts.handler.ServeHTTP(w, req)
-
- if w.Code != http.StatusOK {
- t.Fatalf("expected status 200 for page 2, got %d", w.Code)
- }
-}
-
-func TestSearchPage_EcosystemFilterWithSeededData(t *testing.T) {
- ts := newTestServer(t)
- defer ts.close()
-
- // Seed npm package
- npmPkg := &database.Package{
- PURL: "pkg:npm/eco-filter-npm",
- Ecosystem: "npm",
- Name: "eco-filter-npm",
- }
- if err := ts.db.UpsertPackage(npmPkg); err != nil {
- t.Fatalf("failed to upsert npm package: %v", err)
- }
- npmVer := &database.Version{
- PURL: "pkg:npm/eco-filter-npm@1.0.0",
- PackagePURL: npmPkg.PURL,
- }
- if err := ts.db.UpsertVersion(npmVer); err != nil {
- t.Fatalf("failed to upsert npm version: %v", err)
- }
- npmArt := &database.Artifact{
- VersionPURL: npmVer.PURL,
- Filename: "eco-filter-npm-1.0.0.tgz",
- UpstreamURL: "https://registry.npmjs.org/eco-filter-npm/-/eco-filter-npm-1.0.0.tgz",
- StoragePath: sql.NullString{String: "/tmp/test.tgz", Valid: true},
- }
- if err := ts.db.UpsertArtifact(npmArt); err != nil {
- t.Fatalf("failed to upsert npm artifact: %v", err)
- }
-
- // Seed pypi package
- pypiPkg := &database.Package{
- PURL: "pkg:pypi/eco-filter-pypi",
- Ecosystem: "pypi",
- Name: "eco-filter-pypi",
- }
- if err := ts.db.UpsertPackage(pypiPkg); err != nil {
- t.Fatalf("failed to upsert pypi package: %v", err)
- }
- pypiVer := &database.Version{
- PURL: "pkg:pypi/eco-filter-pypi@1.0.0",
- PackagePURL: pypiPkg.PURL,
- }
- if err := ts.db.UpsertVersion(pypiVer); err != nil {
- t.Fatalf("failed to upsert pypi version: %v", err)
- }
- pypiArt := &database.Artifact{
- VersionPURL: pypiVer.PURL,
- Filename: "eco-filter-pypi-1.0.0.tar.gz",
- UpstreamURL: "https://files.pythonhosted.org/eco-filter-pypi-1.0.0.tar.gz",
- StoragePath: sql.NullString{String: "/tmp/test.tar.gz", Valid: true},
- }
- if err := ts.db.UpsertArtifact(pypiArt); err != nil {
- t.Fatalf("failed to upsert pypi artifact: %v", err)
- }
-
- // Search with ecosystem filter for npm only
- req := httptest.NewRequest("GET", "/ui/search?q=eco-filter&ecosystem=npm", nil)
- w := httptest.NewRecorder()
- ts.handler.ServeHTTP(w, req)
-
- if w.Code != http.StatusOK {
- t.Fatalf("expected status 200, got %d", w.Code)
- }
-
- body := w.Body.String()
- if !strings.Contains(body, "eco-filter-npm") {
- t.Error("expected npm package in filtered results")
- }
- if strings.Contains(body, "eco-filter-pypi") {
- t.Error("did not expect pypi package in npm-filtered results")
- }
-}
-
-func TestHandlePackagesListPage(t *testing.T) {
- ts := newTestServer(t)
- defer ts.close()
-
- seedTestPackage(t, ts.db, "list-test")
-
- req := httptest.NewRequest("GET", "/ui/packages", nil)
- w := httptest.NewRecorder()
- ts.handler.ServeHTTP(w, req)
-
- if w.Code != http.StatusOK {
- t.Fatalf("expected status 200, got %d", w.Code)
- }
-
- body := w.Body.String()
- if !strings.Contains(body, "list-test") {
- t.Error("expected packages list to contain seeded package")
- }
-}
-
-func TestNewServer_StorageConnectivityCheck(t *testing.T) {
- tempDir := t.TempDir()
- dbPath := filepath.Join(tempDir, "test.db")
- storagePath := filepath.Join(tempDir, "artifacts")
-
- cfg := &config.Config{
- Listen: ":0",
- BaseURL: "http://localhost:8080",
- Storage: config.StorageConfig{URL: "file://" + storagePath},
- Database: config.DatabaseConfig{Path: dbPath},
- }
-
- logger := slog.New(slog.NewTextHandler(io.Discard, nil))
-
- buildInfo := BuildInfo{Version: "test-version", Commit: "test-commit"}
- srv, err := New(cfg, logger, buildInfo)
- if err != nil {
- t.Fatalf("New() failed: %v", err)
- }
- if srv.buildInfo != buildInfo {
- t.Errorf("build info = %#v, want %#v", srv.buildInfo, buildInfo)
- }
-
- // On Windows, OpenBucket normalises to file:///C:/path; on Unix the
- // absolute path already starts with /, so file:// + /path == file:///path.
- wantPrefix := "file://"
- wantPath := filepath.ToSlash(storagePath)
- got := srv.storage.URL()
- if !strings.HasPrefix(got, wantPrefix) || !strings.Contains(got, wantPath) {
- t.Errorf("expected storage URL containing %s, got %s", wantPath, got)
- }
-
- _ = srv.db.Close()
-}
-
-func TestNewServer_InvalidAccessLogFailsBeforeDatabaseInit(t *testing.T) {
- tempDir := t.TempDir()
- dbPath := filepath.Join(tempDir, "test.db")
- cfg := &config.Config{
- Storage: config.StorageConfig{URL: "file://" + filepath.Join(tempDir, "artifacts")},
- Database: config.DatabaseConfig{Path: dbPath},
- AccessLog: config.AccessLogConfig{Path: filepath.Join(tempDir, "missing", "access.jsonl")},
- }
-
- logger := slog.New(slog.NewTextHandler(io.Discard, nil))
- if _, err := New(cfg, logger, BuildInfo{}); err == nil {
- t.Fatal("New() succeeded with invalid access log path")
- } else if !strings.Contains(err.Error(), "initializing access log") {
- t.Fatalf("New() error = %v, want access log initialization error", err)
- }
-
- if _, err := os.Stat(dbPath); !os.IsNotExist(err) {
- t.Errorf("database initialized before access log validation: %v", err)
- }
-}
-
-func TestStatsEndpoint_StorageURL(t *testing.T) {
- ts := newTestServer(t)
- defer ts.close()
-
- req := httptest.NewRequest("GET", "/stats", nil)
- w := httptest.NewRecorder()
- ts.handler.ServeHTTP(w, req)
-
- if w.Code != http.StatusOK {
- t.Fatalf("expected status 200, got %d", w.Code)
- }
-
- // Verify the JSON response uses storage_url (not storage_path)
- body := w.Body.String()
- if !strings.Contains(body, `"storage_url"`) {
- t.Errorf("expected JSON key storage_url in response, got: %s", body)
- }
- if strings.Contains(body, `"storage_path"`) {
- t.Errorf("unexpected JSON key storage_path in response (should be storage_url)")
- }
-}
diff --git a/internal/server/static/vendor/tailwind.js b/internal/server/static/tailwind.js
similarity index 100%
rename from internal/server/static/vendor/tailwind.js
rename to internal/server/static/tailwind.js
diff --git a/internal/server/static/vendor/lucide.min.js b/internal/server/static/vendor/lucide.min.js
deleted file mode 100644
index 956ea96..0000000
--- a/internal/server/static/vendor/lucide.min.js
+++ /dev/null
@@ -1,12 +0,0 @@
-/**
- * @license lucide v0.545.0 - ISC
- *
- * This source code is licensed under the ISC license.
- * See the LICENSE file in the root directory of this source tree.
- */
-
-(function(a,n){typeof exports=="object"&&typeof module<"u"?n(exports):typeof define=="function"&&define.amd?define(["exports"],n):(a=typeof globalThis<"u"?globalThis:a||self,n(a.lucide={}))})(this,(function(a){"use strict";const n={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":2,"stroke-linecap":"round","stroke-linejoin":"round"},la=([t,h,d])=>{const c=document.createElementNS("http://www.w3.org/2000/svg",t);return Object.keys(h).forEach(M=>{c.setAttribute(M,String(h[M]))}),d?.length&&d.forEach(M=>{const i=la(M);c.appendChild(i)}),c},ea=(t,h={})=>{const d={...n,...h};return la(["svg",d,t])},Ru=t=>Array.from(t.attributes).reduce((h,d)=>(h[d.name]=d.value,h),{}),Tu=t=>typeof t=="string"?t:!t||!t.class?"":t.class&&typeof t.class=="string"?t.class.split(" "):t.class&&Array.isArray(t.class)?t.class:"",qu=t=>t.flatMap(Tu).map(h=>h.trim()).filter(Boolean).filter((h,d,c)=>c.indexOf(h)===d).join(" "),Uu=t=>t.replace(/(\w)(\w*)(_|-|\s*)/g,(h,d,c)=>d.toUpperCase()+c.toLowerCase()),ra=(t,{nameAttr:h,icons:d,attrs:c})=>{const M=t.getAttribute(h);if(M==null)return;const i=Uu(M),na=d[i];if(!na)return console.warn(`${t.outerHTML} icon name was not found in the provided icons object.`);const Fu=Ru(t),zu={...n,"data-lucide":M,...c,...Fu},bu=qu(["lucide",`lucide-${M}`,Fu,c]);bu&&Object.assign(zu,{class:bu});const Zu=ea(na,zu);return t.parentNode?.replaceChild(Zu,t)},oa=[["path",{d:"m14 12 4 4 4-4"}],["path",{d:"M18 16V7"}],["path",{d:"m2 16 4.039-9.69a.5.5 0 0 1 .923 0L11 16"}],["path",{d:"M3.304 13h6.392"}]],va=[["path",{d:"m14 11 4-4 4 4"}],["path",{d:"M18 16V7"}],["path",{d:"m2 16 4.039-9.69a.5.5 0 0 1 .923 0L11 16"}],["path",{d:"M3.304 13h6.392"}]],$a=[["path",{d:"m15 16 2.536-7.328a1.02 1.02 1 0 1 1.928 0L22 16"}],["path",{d:"M15.697 14h5.606"}],["path",{d:"m2 16 4.039-9.69a.5.5 0 0 1 .923 0L11 16"}],["path",{d:"M3.304 13h6.392"}]],ma=[["circle",{cx:"16",cy:"4",r:"1"}],["path",{d:"m18 19 1-7-6 1"}],["path",{d:"m5 8 3-3 5.5 3-2.36 3.5"}],["path",{d:"M4.24 14.5a5 5 0 0 0 6.88 6"}],["path",{d:"M13.76 17.5a5 5 0 0 0-6.88-6"}]],ya=[["path",{d:"M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2"}]],sa=[["path",{d:"M18 17.5a2.5 2.5 0 1 1-4 2.03V12"}],["path",{d:"M6 12H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"}],["path",{d:"M6 8h12"}],["path",{d:"M6.6 15.572A2 2 0 1 0 10 17v-5"}]],ga=[["path",{d:"M5 17H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2h-1"}],["path",{d:"m12 15 5 6H7Z"}]],y=[["circle",{cx:"12",cy:"13",r:"8"}],["path",{d:"M5 3 2 6"}],["path",{d:"m22 6-3-3"}],["path",{d:"M6.38 18.7 4 21"}],["path",{d:"M17.64 18.67 20 21"}],["path",{d:"m9 13 2 2 4-4"}]],s=[["circle",{cx:"12",cy:"13",r:"8"}],["path",{d:"M5 3 2 6"}],["path",{d:"m22 6-3-3"}],["path",{d:"M6.38 18.7 4 21"}],["path",{d:"M17.64 18.67 20 21"}],["path",{d:"M9 13h6"}]],ua=[["path",{d:"M6.87 6.87a8 8 0 1 0 11.26 11.26"}],["path",{d:"M19.9 14.25a8 8 0 0 0-9.15-9.15"}],["path",{d:"m22 6-3-3"}],["path",{d:"M6.26 18.67 4 21"}],["path",{d:"m2 2 20 20"}],["path",{d:"M4 4 2 6"}]],g=[["circle",{cx:"12",cy:"13",r:"8"}],["path",{d:"M5 3 2 6"}],["path",{d:"m22 6-3-3"}],["path",{d:"M6.38 18.7 4 21"}],["path",{d:"M17.64 18.67 20 21"}],["path",{d:"M12 10v6"}],["path",{d:"M9 13h6"}]],Ca=[["circle",{cx:"12",cy:"13",r:"8"}],["path",{d:"M12 9v4l2 2"}],["path",{d:"M5 3 2 6"}],["path",{d:"m22 6-3-3"}],["path",{d:"M6.38 18.7 4 21"}],["path",{d:"M17.64 18.67 20 21"}]],Ha=[["path",{d:"M11 21c0-2.5 2-2.5 2-5"}],["path",{d:"M16 21c0-2.5 2-2.5 2-5"}],["path",{d:"m19 8-.8 3a1.25 1.25 0 0 1-1.2 1H7a1.25 1.25 0 0 1-1.2-1L5 8"}],["path",{d:"M21 3a1 1 0 0 1 1 1v2a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V4a1 1 0 0 1 1-1z"}],["path",{d:"M6 21c0-2.5 2-2.5 2-5"}]],Aa=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2"}],["polyline",{points:"11 3 11 11 14 8 17 11 17 3"}]],wa=[["path",{d:"M2 12h20"}],["path",{d:"M10 16v4a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-4"}],["path",{d:"M10 8V4a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v4"}],["path",{d:"M20 16v1a2 2 0 0 1-2 2h-2a2 2 0 0 1-2-2v-1"}],["path",{d:"M14 8V7c0-1.1.9-2 2-2h2a2 2 0 0 1 2 2v1"}]],Va=[["path",{d:"M12 2v20"}],["path",{d:"M8 10H4a2 2 0 0 1-2-2V6c0-1.1.9-2 2-2h4"}],["path",{d:"M16 10h4a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2h-4"}],["path",{d:"M8 20H7a2 2 0 0 1-2-2v-2c0-1.1.9-2 2-2h1"}],["path",{d:"M16 14h1a2 2 0 0 1 2 2v2a2 2 0 0 1-2 2h-1"}]],Sa=[["rect",{width:"6",height:"16",x:"4",y:"2",rx:"2"}],["rect",{width:"6",height:"9",x:"14",y:"9",rx:"2"}],["path",{d:"M22 22H2"}]],La=[["rect",{width:"16",height:"6",x:"2",y:"4",rx:"2"}],["rect",{width:"9",height:"6",x:"9",y:"14",rx:"2"}],["path",{d:"M22 22V2"}]],fa=[["rect",{width:"6",height:"14",x:"4",y:"5",rx:"2"}],["rect",{width:"6",height:"10",x:"14",y:"7",rx:"2"}],["path",{d:"M17 22v-5"}],["path",{d:"M17 7V2"}],["path",{d:"M7 22v-3"}],["path",{d:"M7 5V2"}]],ka=[["rect",{width:"6",height:"14",x:"4",y:"5",rx:"2"}],["rect",{width:"6",height:"10",x:"14",y:"7",rx:"2"}],["path",{d:"M10 2v20"}],["path",{d:"M20 2v20"}]],Pa=[["rect",{width:"6",height:"14",x:"4",y:"5",rx:"2"}],["rect",{width:"6",height:"10",x:"14",y:"7",rx:"2"}],["path",{d:"M4 2v20"}],["path",{d:"M14 2v20"}]],Ba=[["rect",{width:"6",height:"14",x:"2",y:"5",rx:"2"}],["rect",{width:"6",height:"10",x:"16",y:"7",rx:"2"}],["path",{d:"M12 2v20"}]],Da=[["rect",{width:"6",height:"14",x:"2",y:"5",rx:"2"}],["rect",{width:"6",height:"10",x:"12",y:"7",rx:"2"}],["path",{d:"M22 2v20"}]],Fa=[["rect",{width:"6",height:"14",x:"6",y:"5",rx:"2"}],["rect",{width:"6",height:"10",x:"16",y:"7",rx:"2"}],["path",{d:"M2 2v20"}]],za=[["rect",{width:"6",height:"10",x:"9",y:"7",rx:"2"}],["path",{d:"M4 22V2"}],["path",{d:"M20 22V2"}]],ba=[["rect",{width:"6",height:"14",x:"3",y:"5",rx:"2"}],["rect",{width:"6",height:"10",x:"15",y:"7",rx:"2"}],["path",{d:"M3 2v20"}],["path",{d:"M21 2v20"}]],Ra=[["rect",{width:"6",height:"16",x:"4",y:"6",rx:"2"}],["rect",{width:"6",height:"9",x:"14",y:"6",rx:"2"}],["path",{d:"M22 2H2"}]],Ta=[["rect",{width:"9",height:"6",x:"6",y:"14",rx:"2"}],["rect",{width:"16",height:"6",x:"6",y:"4",rx:"2"}],["path",{d:"M2 2v20"}]],qa=[["path",{d:"M22 17h-3"}],["path",{d:"M22 7h-5"}],["path",{d:"M5 17H2"}],["path",{d:"M7 7H2"}],["rect",{x:"5",y:"14",width:"14",height:"6",rx:"2"}],["rect",{x:"7",y:"4",width:"10",height:"6",rx:"2"}]],Ua=[["rect",{width:"14",height:"6",x:"5",y:"14",rx:"2"}],["rect",{width:"10",height:"6",x:"7",y:"4",rx:"2"}],["path",{d:"M2 20h20"}],["path",{d:"M2 10h20"}]],Oa=[["rect",{width:"14",height:"6",x:"5",y:"14",rx:"2"}],["rect",{width:"10",height:"6",x:"7",y:"4",rx:"2"}],["path",{d:"M2 14h20"}],["path",{d:"M2 4h20"}]],Za=[["rect",{width:"14",height:"6",x:"5",y:"16",rx:"2"}],["rect",{width:"10",height:"6",x:"7",y:"2",rx:"2"}],["path",{d:"M2 12h20"}]],Ga=[["rect",{width:"14",height:"6",x:"5",y:"12",rx:"2"}],["rect",{width:"10",height:"6",x:"7",y:"2",rx:"2"}],["path",{d:"M2 22h20"}]],Ia=[["rect",{width:"14",height:"6",x:"5",y:"16",rx:"2"}],["rect",{width:"10",height:"6",x:"7",y:"6",rx:"2"}],["path",{d:"M2 2h20"}]],Wa=[["rect",{width:"10",height:"6",x:"7",y:"9",rx:"2"}],["path",{d:"M22 20H2"}],["path",{d:"M22 4H2"}]],Ea=[["rect",{width:"14",height:"6",x:"5",y:"15",rx:"2"}],["rect",{width:"10",height:"6",x:"7",y:"3",rx:"2"}],["path",{d:"M2 21h20"}],["path",{d:"M2 3h20"}]],Xa=[["path",{d:"M17.5 12c0 4.4-3.6 8-8 8A4.5 4.5 0 0 1 5 15.5c0-6 8-4 8-8.5a3 3 0 1 0-6 0c0 3 2.5 8.5 12 13"}],["path",{d:"M16 12h3"}]],ja=[["path",{d:"M10 10H6"}],["path",{d:"M14 18V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v11a1 1 0 0 0 1 1h2"}],["path",{d:"M19 18h2a1 1 0 0 0 1-1v-3.28a1 1 0 0 0-.684-.948l-1.923-.641a1 1 0 0 1-.578-.502l-1.539-3.076A1 1 0 0 0 16.382 8H14"}],["path",{d:"M8 8v4"}],["path",{d:"M9 18h6"}],["circle",{cx:"17",cy:"18",r:"2"}],["circle",{cx:"7",cy:"18",r:"2"}]],Na=[["path",{d:"M10 17c-5-3-7-7-7-9a2 2 0 0 1 4 0c0 2.5-5 2.5-5 6 0 1.7 1.3 3 3 3 2.8 0 5-2.2 5-5"}],["path",{d:"M22 17c-5-3-7-7-7-9a2 2 0 0 1 4 0c0 2.5-5 2.5-5 6 0 1.7 1.3 3 3 3 2.8 0 5-2.2 5-5"}]],Ka=[["path",{d:"M10 2v5.632c0 .424-.272.795-.653.982A6 6 0 0 0 6 14c.006 4 3 7 5 8"}],["path",{d:"M10 5H8a2 2 0 0 0 0 4h.68"}],["path",{d:"M14 2v5.632c0 .424.272.795.652.982A6 6 0 0 1 18 14c0 4-3 7-5 8"}],["path",{d:"M14 5h2a2 2 0 0 1 0 4h-.68"}],["path",{d:"M18 22H6"}],["path",{d:"M9 2h6"}]],Qa=[["path",{d:"M12 22V8"}],["path",{d:"M5 12H2a10 10 0 0 0 20 0h-3"}],["circle",{cx:"12",cy:"5",r:"3"}]],Ja=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M16 16s-1.5-2-4-2-4 2-4 2"}],["path",{d:"M7.5 8 10 9"}],["path",{d:"m14 9 2.5-1"}],["path",{d:"M9 10h.01"}],["path",{d:"M15 10h.01"}]],Ya=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M8 15h8"}],["path",{d:"M8 9h2"}],["path",{d:"M14 9h2"}]],_a=[["path",{d:"M2 12 7 2"}],["path",{d:"m7 12 5-10"}],["path",{d:"m12 12 5-10"}],["path",{d:"m17 12 5-10"}],["path",{d:"M4.5 7h15"}],["path",{d:"M12 16v6"}]],xa=[["path",{d:"M7 10H6a4 4 0 0 1-4-4 1 1 0 0 1 1-1h4"}],["path",{d:"M7 5a1 1 0 0 1 1-1h13a1 1 0 0 1 1 1 7 7 0 0 1-7 7H8a1 1 0 0 1-1-1z"}],["path",{d:"M9 12v5"}],["path",{d:"M15 12v5"}],["path",{d:"M5 20a3 3 0 0 1 3-3h8a3 3 0 0 1 3 3 1 1 0 0 1-1 1H6a1 1 0 0 1-1-1"}]],at=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"m14.31 8 5.74 9.94"}],["path",{d:"M9.69 8h11.48"}],["path",{d:"m7.38 12 5.74-9.94"}],["path",{d:"M9.69 16 3.95 6.06"}],["path",{d:"M14.31 16H2.83"}],["path",{d:"m16.62 12-5.74 9.94"}]],tt=[["rect",{width:"20",height:"16",x:"2",y:"4",rx:"2"}],["path",{d:"M6 8h.01"}],["path",{d:"M10 8h.01"}],["path",{d:"M14 8h.01"}]],ht=[["rect",{x:"2",y:"4",width:"20",height:"16",rx:"2"}],["path",{d:"M10 4v4"}],["path",{d:"M2 8h20"}],["path",{d:"M6 4v4"}]],dt=[["path",{d:"M12 6.528V3a1 1 0 0 1 1-1h0"}],["path",{d:"M18.237 21A15 15 0 0 0 22 11a6 6 0 0 0-10-4.472A6 6 0 0 0 2 11a15.1 15.1 0 0 0 3.763 10 3 3 0 0 0 3.648.648 5.5 5.5 0 0 1 5.178 0A3 3 0 0 0 18.237 21"}]],ct=[["rect",{width:"20",height:"5",x:"2",y:"3",rx:"1"}],["path",{d:"M4 8v11a2 2 0 0 0 2 2h2"}],["path",{d:"M20 8v11a2 2 0 0 1-2 2h-2"}],["path",{d:"m9 15 3-3 3 3"}],["path",{d:"M12 12v9"}]],Mt=[["rect",{width:"20",height:"5",x:"2",y:"3",rx:"1"}],["path",{d:"M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8"}],["path",{d:"m9.5 17 5-5"}],["path",{d:"m9.5 12 5 5"}]],pt=[["rect",{width:"20",height:"5",x:"2",y:"3",rx:"1"}],["path",{d:"M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8"}],["path",{d:"M10 12h4"}]],it=[["path",{d:"M19 9V6a2 2 0 0 0-2-2H7a2 2 0 0 0-2 2v3"}],["path",{d:"M3 16a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-5a2 2 0 0 0-4 0v1.5a.5.5 0 0 1-.5.5h-9a.5.5 0 0 1-.5-.5V11a2 2 0 0 0-4 0z"}],["path",{d:"M5 18v2"}],["path",{d:"M19 18v2"}]],nt=[["path",{d:"M15 11a1 1 0 0 0 1 1h2.939a1 1 0 0 1 .75 1.811l-6.835 6.836a1.207 1.207 0 0 1-1.707 0L4.31 13.81a1 1 0 0 1 .75-1.811H8a1 1 0 0 0 1-1V9a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1z"}],["path",{d:"M9 4h6"}]],lt=[["path",{d:"M15 11a1 1 0 0 0 1 1h2.939a1 1 0 0 1 .75 1.811l-6.835 6.836a1.207 1.207 0 0 1-1.707 0L4.31 13.81a1 1 0 0 1 .75-1.811H8a1 1 0 0 0 1-1V5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1z"}]],et=[["path",{d:"M13 9a1 1 0 0 1-1-1V5.061a1 1 0 0 0-1.811-.75l-6.835 6.836a1.207 1.207 0 0 0 0 1.707l6.835 6.835a1 1 0 0 0 1.811-.75V16a1 1 0 0 1 1-1h2a1 1 0 0 0 1-1v-4a1 1 0 0 0-1-1z"}],["path",{d:"M20 9v6"}]],rt=[["path",{d:"M13 9a1 1 0 0 1-1-1V5.061a1 1 0 0 0-1.811-.75l-6.835 6.836a1.207 1.207 0 0 0 0 1.707l6.835 6.835a1 1 0 0 0 1.811-.75V16a1 1 0 0 1 1-1h6a1 1 0 0 0 1-1v-4a1 1 0 0 0-1-1z"}]],ot=[["path",{d:"M11 9a1 1 0 0 0 1-1V5.061a1 1 0 0 1 1.811-.75l6.836 6.836a1.207 1.207 0 0 1 0 1.707l-6.836 6.835a1 1 0 0 1-1.811-.75V16a1 1 0 0 0-1-1H9a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1z"}],["path",{d:"M4 9v6"}]],vt=[["path",{d:"M11 9a1 1 0 0 0 1-1V5.061a1 1 0 0 1 1.811-.75l6.836 6.836a1.207 1.207 0 0 1 0 1.707l-6.836 6.835a1 1 0 0 1-1.811-.75V16a1 1 0 0 0-1-1H5a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1z"}]],$t=[["path",{d:"M9 13a1 1 0 0 0-1-1H5.061a1 1 0 0 1-.75-1.811l6.836-6.835a1.207 1.207 0 0 1 1.707 0l6.835 6.835a1 1 0 0 1-.75 1.811H16a1 1 0 0 0-1 1v2a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1z"}],["path",{d:"M9 20h6"}]],mt=[["path",{d:"m3 16 4 4 4-4"}],["path",{d:"M7 20V4"}],["rect",{x:"15",y:"4",width:"4",height:"6",ry:"2"}],["path",{d:"M17 20v-6h-2"}],["path",{d:"M15 20h4"}]],yt=[["path",{d:"m3 16 4 4 4-4"}],["path",{d:"M7 20V4"}],["path",{d:"M17 10V4h-2"}],["path",{d:"M15 10h4"}],["rect",{x:"15",y:"14",width:"4",height:"6",ry:"2"}]],st=[["path",{d:"M9 13a1 1 0 0 0-1-1H5.061a1 1 0 0 1-.75-1.811l6.836-6.835a1.207 1.207 0 0 1 1.707 0l6.835 6.835a1 1 0 0 1-.75 1.811H16a1 1 0 0 0-1 1v6a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1z"}]],u=[["path",{d:"m3 16 4 4 4-4"}],["path",{d:"M7 20V4"}],["path",{d:"M20 8h-5"}],["path",{d:"M15 10V6.5a2.5 2.5 0 0 1 5 0V10"}],["path",{d:"M15 14h5l-5 6h5"}]],gt=[["path",{d:"M19 3H5"}],["path",{d:"M12 21V7"}],["path",{d:"m6 15 6 6 6-6"}]],ut=[["path",{d:"m3 16 4 4 4-4"}],["path",{d:"M7 20V4"}],["path",{d:"M11 4h4"}],["path",{d:"M11 8h7"}],["path",{d:"M11 12h10"}]],Ct=[["path",{d:"M17 7 7 17"}],["path",{d:"M17 17H7V7"}]],Ht=[["path",{d:"M12 2v14"}],["path",{d:"m19 9-7 7-7-7"}],["circle",{cx:"12",cy:"21",r:"1"}]],At=[["path",{d:"m7 7 10 10"}],["path",{d:"M17 7v10H7"}]],wt=[["path",{d:"M12 17V3"}],["path",{d:"m6 11 6 6 6-6"}],["path",{d:"M19 21H5"}]],Vt=[["path",{d:"m3 16 4 4 4-4"}],["path",{d:"M7 20V4"}],["path",{d:"m21 8-4-4-4 4"}],["path",{d:"M17 4v16"}]],C=[["path",{d:"m3 16 4 4 4-4"}],["path",{d:"M7 20V4"}],["path",{d:"M11 4h10"}],["path",{d:"M11 8h7"}],["path",{d:"M11 12h4"}]],H=[["path",{d:"m3 16 4 4 4-4"}],["path",{d:"M7 4v16"}],["path",{d:"M15 4h5l-5 6h5"}],["path",{d:"M15 20v-3.5a2.5 2.5 0 0 1 5 0V20"}],["path",{d:"M20 18h-5"}]],St=[["path",{d:"M12 5v14"}],["path",{d:"m19 12-7 7-7-7"}]],Lt=[["path",{d:"m9 6-6 6 6 6"}],["path",{d:"M3 12h14"}],["path",{d:"M21 19V5"}]],ft=[["path",{d:"M8 3 4 7l4 4"}],["path",{d:"M4 7h16"}],["path",{d:"m16 21 4-4-4-4"}],["path",{d:"M20 17H4"}]],kt=[["path",{d:"M3 19V5"}],["path",{d:"m13 6-6 6 6 6"}],["path",{d:"M7 12h14"}]],Pt=[["path",{d:"m12 19-7-7 7-7"}],["path",{d:"M19 12H5"}]],Bt=[["path",{d:"M3 5v14"}],["path",{d:"M21 12H7"}],["path",{d:"m15 18 6-6-6-6"}]],Dt=[["path",{d:"M17 12H3"}],["path",{d:"m11 18 6-6-6-6"}],["path",{d:"M21 5v14"}]],Ft=[["path",{d:"m16 3 4 4-4 4"}],["path",{d:"M20 7H4"}],["path",{d:"m8 21-4-4 4-4"}],["path",{d:"M4 17h16"}]],zt=[["path",{d:"M5 12h14"}],["path",{d:"m12 5 7 7-7 7"}]],bt=[["path",{d:"m3 8 4-4 4 4"}],["path",{d:"M7 4v16"}],["rect",{x:"15",y:"4",width:"4",height:"6",ry:"2"}],["path",{d:"M17 20v-6h-2"}],["path",{d:"M15 20h4"}]],Rt=[["path",{d:"m3 8 4-4 4 4"}],["path",{d:"M7 4v16"}],["path",{d:"M17 10V4h-2"}],["path",{d:"M15 10h4"}],["rect",{x:"15",y:"14",width:"4",height:"6",ry:"2"}]],A=[["path",{d:"m3 8 4-4 4 4"}],["path",{d:"M7 4v16"}],["path",{d:"M20 8h-5"}],["path",{d:"M15 10V6.5a2.5 2.5 0 0 1 5 0V10"}],["path",{d:"M15 14h5l-5 6h5"}]],Tt=[["path",{d:"m21 16-4 4-4-4"}],["path",{d:"M17 20V4"}],["path",{d:"m3 8 4-4 4 4"}],["path",{d:"M7 4v16"}]],qt=[["path",{d:"m18 9-6-6-6 6"}],["path",{d:"M12 3v14"}],["path",{d:"M5 21h14"}]],Ut=[["path",{d:"m5 9 7-7 7 7"}],["path",{d:"M12 16V2"}],["circle",{cx:"12",cy:"21",r:"1"}]],Ot=[["path",{d:"M7 17V7h10"}],["path",{d:"M17 17 7 7"}]],w=[["path",{d:"m3 8 4-4 4 4"}],["path",{d:"M7 4v16"}],["path",{d:"M11 12h4"}],["path",{d:"M11 16h7"}],["path",{d:"M11 20h10"}]],Zt=[["path",{d:"M7 7h10v10"}],["path",{d:"M7 17 17 7"}]],Gt=[["path",{d:"m3 8 4-4 4 4"}],["path",{d:"M7 4v16"}],["path",{d:"M11 12h10"}],["path",{d:"M11 16h7"}],["path",{d:"M11 20h4"}]],V=[["path",{d:"m3 8 4-4 4 4"}],["path",{d:"M7 4v16"}],["path",{d:"M15 4h5l-5 6h5"}],["path",{d:"M15 20v-3.5a2.5 2.5 0 0 1 5 0V20"}],["path",{d:"M20 18h-5"}]],It=[["path",{d:"M5 3h14"}],["path",{d:"m18 13-6-6-6 6"}],["path",{d:"M12 7v14"}]],Wt=[["path",{d:"m5 12 7-7 7 7"}],["path",{d:"M12 19V5"}]],Et=[["path",{d:"m4 6 3-3 3 3"}],["path",{d:"M7 17V3"}],["path",{d:"m14 6 3-3 3 3"}],["path",{d:"M17 17V3"}],["path",{d:"M4 21h16"}]],Xt=[["path",{d:"M12 6v12"}],["path",{d:"M17.196 9 6.804 15"}],["path",{d:"m6.804 9 10.392 6"}]],jt=[["circle",{cx:"12",cy:"12",r:"4"}],["path",{d:"M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-4 8"}]],Nt=[["path",{d:"M2 10v3"}],["path",{d:"M6 6v11"}],["path",{d:"M10 3v18"}],["path",{d:"M14 8v7"}],["path",{d:"M18 5v13"}],["path",{d:"M22 10v3"}]],Kt=[["path",{d:"M2 13a2 2 0 0 0 2-2V7a2 2 0 0 1 4 0v13a2 2 0 0 0 4 0V4a2 2 0 0 1 4 0v13a2 2 0 0 0 4 0v-4a2 2 0 0 1 2-2"}]],Qt=[["circle",{cx:"12",cy:"12",r:"1"}],["path",{d:"M20.2 20.2c2.04-2.03.02-7.36-4.5-11.9-4.54-4.52-9.87-6.54-11.9-4.5-2.04 2.03-.02 7.36 4.5 11.9 4.54 4.52 9.87 6.54 11.9 4.5Z"}],["path",{d:"M15.7 15.7c4.52-4.54 6.54-9.87 4.5-11.9-2.03-2.04-7.36-.02-11.9 4.5-4.52 4.54-6.54 9.87-4.5 11.9 2.03 2.04 7.36.02 11.9-4.5Z"}]],Jt=[["path",{d:"m15.477 12.89 1.515 8.526a.5.5 0 0 1-.81.47l-3.58-2.687a1 1 0 0 0-1.197 0l-3.586 2.686a.5.5 0 0 1-.81-.469l1.514-8.526"}],["circle",{cx:"12",cy:"8",r:"6"}]],Yt=[["path",{d:"m14 12-8.381 8.38a1 1 0 0 1-3.001-3L11 9"}],["path",{d:"M15 15.5a.5.5 0 0 0 .5.5A6.5 6.5 0 0 0 22 9.5a.5.5 0 0 0-.5-.5h-1.672a2 2 0 0 1-1.414-.586l-5.062-5.062a1.205 1.205 0 0 0-1.704 0L9.352 5.648a1.205 1.205 0 0 0 0 1.704l5.062 5.062A2 2 0 0 1 15 13.828z"}]],S=[["path",{d:"M13.5 10.5 15 9"}],["path",{d:"M4 4v15a1 1 0 0 0 1 1h15"}],["path",{d:"M4.293 19.707 6 18"}],["path",{d:"m9 15 1.5-1.5"}]],_t=[["path",{d:"M10 16c.5.3 1.2.5 2 .5s1.5-.2 2-.5"}],["path",{d:"M15 12h.01"}],["path",{d:"M19.38 6.813A9 9 0 0 1 20.8 10.2a2 2 0 0 1 0 3.6 9 9 0 0 1-17.6 0 2 2 0 0 1 0-3.6A9 9 0 0 1 12 3c2 0 3.5 1.1 3.5 2.5s-.9 2.5-2 2.5c-.8 0-1.5-.4-1.5-1"}],["path",{d:"M9 12h.01"}]],xt=[["path",{d:"M4 10a4 4 0 0 1 4-4h8a4 4 0 0 1 4 4v10a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2z"}],["path",{d:"M8 10h8"}],["path",{d:"M8 18h8"}],["path",{d:"M8 22v-6a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v6"}],["path",{d:"M9 6V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2"}]],ah=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}],["line",{x1:"12",x2:"12",y1:"8",y2:"12"}],["line",{x1:"12",x2:"12.01",y1:"16",y2:"16"}]],th=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}],["path",{d:"M12 7v10"}],["path",{d:"M15.4 10a4 4 0 1 0 0 4"}]],L=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}],["path",{d:"m9 12 2 2 4-4"}]],hh=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}],["path",{d:"M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8"}],["path",{d:"M12 18V6"}]],dh=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}],["path",{d:"M7 12h5"}],["path",{d:"M15 9.4a4 4 0 1 0 0 5.2"}]],ch=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}],["path",{d:"M8 8h8"}],["path",{d:"M8 12h8"}],["path",{d:"m13 17-5-1h1a4 4 0 0 0 0-8"}]],Mh=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}],["line",{x1:"12",x2:"12",y1:"16",y2:"12"}],["line",{x1:"12",x2:"12.01",y1:"8",y2:"8"}]],ph=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}],["path",{d:"m9 8 3 3v7"}],["path",{d:"m12 11 3-3"}],["path",{d:"M9 12h6"}],["path",{d:"M9 16h6"}]],ih=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}],["line",{x1:"8",x2:"16",y1:"12",y2:"12"}]],nh=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}],["path",{d:"m15 9-6 6"}],["path",{d:"M9 9h.01"}],["path",{d:"M15 15h.01"}]],lh=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}],["line",{x1:"12",x2:"12",y1:"8",y2:"16"}],["line",{x1:"8",x2:"16",y1:"12",y2:"12"}]],eh=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}],["path",{d:"M8 12h4"}],["path",{d:"M10 16V9.5a2.5 2.5 0 0 1 5 0"}],["path",{d:"M8 16h7"}]],f=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}],["path",{d:"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"}],["line",{x1:"12",x2:"12.01",y1:"17",y2:"17"}]],rh=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}],["path",{d:"M9 16h5"}],["path",{d:"M9 12h5a2 2 0 1 0 0-4h-3v9"}]],oh=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}],["path",{d:"M11 17V8h4"}],["path",{d:"M11 12h3"}],["path",{d:"M9 16h4"}]],vh=[["path",{d:"M11 7v10a5 5 0 0 0 5-5"}],["path",{d:"m15 8-6 3"}],["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76"}]],$h=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}],["line",{x1:"15",x2:"9",y1:"9",y2:"15"}],["line",{x1:"9",x2:"15",y1:"9",y2:"15"}]],mh=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}]],yh=[["path",{d:"M22 18H6a2 2 0 0 1-2-2V7a2 2 0 0 0-2-2"}],["path",{d:"M17 14V4a2 2 0 0 0-2-2h-1a2 2 0 0 0-2 2v10"}],["rect",{width:"13",height:"8",x:"8",y:"6",rx:"1"}],["circle",{cx:"18",cy:"20",r:"2"}],["circle",{cx:"9",cy:"20",r:"2"}]],sh=[["path",{d:"M4.929 4.929 19.07 19.071"}],["circle",{cx:"12",cy:"12",r:"10"}]],gh=[["path",{d:"M4 13c3.5-2 8-2 10 2a5.5 5.5 0 0 1 8 5"}],["path",{d:"M5.15 17.89c5.52-1.52 8.65-6.89 7-12C11.55 4 11.5 2 13 2c3.22 0 5 5.5 5 8 0 6.5-4.2 12-10.49 12C5.11 22 2 22 2 20c0-1.5 1.14-1.55 3.15-2.11Z"}]],uh=[["path",{d:"M10 10.01h.01"}],["path",{d:"M10 14.01h.01"}],["path",{d:"M14 10.01h.01"}],["path",{d:"M14 14.01h.01"}],["path",{d:"M18 6v11.5"}],["path",{d:"M6 6v12"}],["rect",{x:"2",y:"6",width:"20",height:"12",rx:"2"}]],Ch=[["path",{d:"M12 18H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5"}],["path",{d:"m16 19 3 3 3-3"}],["path",{d:"M18 12h.01"}],["path",{d:"M19 16v6"}],["path",{d:"M6 12h.01"}],["circle",{cx:"12",cy:"12",r:"2"}]],Hh=[["path",{d:"M12 18H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5"}],["path",{d:"M18 12h.01"}],["path",{d:"M19 22v-6"}],["path",{d:"m22 19-3-3-3 3"}],["path",{d:"M6 12h.01"}],["circle",{cx:"12",cy:"12",r:"2"}]],Ah=[["path",{d:"M13 18H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5"}],["path",{d:"m17 17 5 5"}],["path",{d:"M18 12h.01"}],["path",{d:"m22 17-5 5"}],["path",{d:"M6 12h.01"}],["circle",{cx:"12",cy:"12",r:"2"}]],wh=[["rect",{width:"20",height:"12",x:"2",y:"6",rx:"2"}],["circle",{cx:"12",cy:"12",r:"2"}],["path",{d:"M6 12h.01M18 12h.01"}]],Vh=[["path",{d:"M3 5v14"}],["path",{d:"M8 5v14"}],["path",{d:"M12 5v14"}],["path",{d:"M17 5v14"}],["path",{d:"M21 5v14"}]],Sh=[["path",{d:"M10 3a41 41 0 0 0 0 18"}],["path",{d:"M14 3a41 41 0 0 1 0 18"}],["path",{d:"M17 3a2 2 0 0 1 1.68.92 15.25 15.25 0 0 1 0 16.16A2 2 0 0 1 17 21H7a2 2 0 0 1-1.68-.92 15.25 15.25 0 0 1 0-16.16A2 2 0 0 1 7 3z"}],["path",{d:"M3.84 17h16.32"}],["path",{d:"M3.84 7h16.32"}]],Lh=[["path",{d:"M4 20h16"}],["path",{d:"m6 16 6-12 6 12"}],["path",{d:"M8 12h8"}]],fh=[["path",{d:"M10 4 8 6"}],["path",{d:"M17 19v2"}],["path",{d:"M2 12h20"}],["path",{d:"M7 19v2"}],["path",{d:"M9 5 7.621 3.621A2.121 2.121 0 0 0 4 5v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-5"}]],kh=[["path",{d:"m11 7-3 5h4l-3 5"}],["path",{d:"M14.856 6H16a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.935"}],["path",{d:"M22 14v-4"}],["path",{d:"M5.14 18H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h2.936"}]],Ph=[["path",{d:"M10 10v4"}],["path",{d:"M14 10v4"}],["path",{d:"M22 14v-4"}],["path",{d:"M6 10v4"}],["rect",{x:"2",y:"6",width:"16",height:"12",rx:"2"}]],Bh=[["path",{d:"M22 14v-4"}],["path",{d:"M6 14v-4"}],["rect",{x:"2",y:"6",width:"16",height:"12",rx:"2"}]],Dh=[["path",{d:"M10 14v-4"}],["path",{d:"M22 14v-4"}],["path",{d:"M6 14v-4"}],["rect",{x:"2",y:"6",width:"16",height:"12",rx:"2"}]],Fh=[["path",{d:"M10 9v6"}],["path",{d:"M12.543 6H16a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-3.605"}],["path",{d:"M22 14v-4"}],["path",{d:"M7 12h6"}],["path",{d:"M7.606 18H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h3.606"}]],zh=[["path",{d:"M10 17h.01"}],["path",{d:"M10 7v6"}],["path",{d:"M14 6h2a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2"}],["path",{d:"M22 14v-4"}],["path",{d:"M6 18H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h2"}]],bh=[["path",{d:"M 22 14 L 22 10"}],["rect",{x:"2",y:"6",width:"16",height:"12",rx:"2"}]],Rh=[["path",{d:"M4.5 3h15"}],["path",{d:"M6 3v16a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V3"}],["path",{d:"M6 14h12"}]],Th=[["path",{d:"M9 9c-.64.64-1.521.954-2.402 1.165A6 6 0 0 0 8 22a13.96 13.96 0 0 0 9.9-4.1"}],["path",{d:"M10.75 5.093A6 6 0 0 1 22 8c0 2.411-.61 4.68-1.683 6.66"}],["path",{d:"M5.341 10.62a4 4 0 0 0 6.487 1.208M10.62 5.341a4.015 4.015 0 0 1 2.039 2.04"}],["line",{x1:"2",x2:"22",y1:"2",y2:"22"}]],qh=[["path",{d:"M10.165 6.598C9.954 7.478 9.64 8.36 9 9c-.64.64-1.521.954-2.402 1.165A6 6 0 0 0 8 22c7.732 0 14-6.268 14-14a6 6 0 0 0-11.835-1.402Z"}],["path",{d:"M5.341 10.62a4 4 0 1 0 5.279-5.28"}]],Uh=[["path",{d:"M2 20v-8a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v8"}],["path",{d:"M4 10V6a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v4"}],["path",{d:"M12 4v6"}],["path",{d:"M2 18h20"}]],Oh=[["path",{d:"M3 20v-8a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v8"}],["path",{d:"M5 10V6a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v4"}],["path",{d:"M3 18h18"}]],Zh=[["path",{d:"M2 4v16"}],["path",{d:"M2 8h18a2 2 0 0 1 2 2v10"}],["path",{d:"M2 17h20"}],["path",{d:"M6 8v9"}]],Gh=[["path",{d:"M16.4 13.7A6.5 6.5 0 1 0 6.28 6.6c-1.1 3.13-.78 3.9-3.18 6.08A3 3 0 0 0 5 18c4 0 8.4-1.8 11.4-4.3"}],["path",{d:"m18.5 6 2.19 4.5a6.48 6.48 0 0 1-2.29 7.2C15.4 20.2 11 22 7 22a3 3 0 0 1-2.68-1.66L2.4 16.5"}],["circle",{cx:"12.5",cy:"8.5",r:"2.5"}]],Ih=[["path",{d:"M13 13v5"}],["path",{d:"M17 11.47V8"}],["path",{d:"M17 11h1a3 3 0 0 1 2.745 4.211"}],["path",{d:"m2 2 20 20"}],["path",{d:"M5 8v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-3"}],["path",{d:"M7.536 7.535C6.766 7.649 6.154 8 5.5 8a2.5 2.5 0 0 1-1.768-4.268"}],["path",{d:"M8.727 3.204C9.306 2.767 9.885 2 11 2c1.56 0 2 1.5 3 1.5s1.72-.5 2.5-.5a1 1 0 1 1 0 5c-.78 0-1.5-.5-2.5-.5a3.149 3.149 0 0 0-.842.12"}],["path",{d:"M9 14.6V18"}]],Wh=[["path",{d:"M17 11h1a3 3 0 0 1 0 6h-1"}],["path",{d:"M9 12v6"}],["path",{d:"M13 12v6"}],["path",{d:"M14 7.5c-1 0-1.44.5-3 .5s-2-.5-3-.5-1.72.5-2.5.5a2.5 2.5 0 0 1 0-5c.78 0 1.57.5 2.5.5S9.44 2 11 2s2 1.5 3 1.5 1.72-.5 2.5-.5a2.5 2.5 0 0 1 0 5c-.78 0-1.5-.5-2.5-.5Z"}],["path",{d:"M5 8v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V8"}]],Eh=[["path",{d:"M10.268 21a2 2 0 0 0 3.464 0"}],["path",{d:"M13.916 2.314A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.74 7.327A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673 9 9 0 0 1-.585-.665"}],["circle",{cx:"18",cy:"8",r:"3"}]],Xh=[["path",{d:"M18.518 17.347A7 7 0 0 1 14 19"}],["path",{d:"M18.8 4A11 11 0 0 1 20 9"}],["path",{d:"M9 9h.01"}],["circle",{cx:"20",cy:"16",r:"2"}],["circle",{cx:"9",cy:"9",r:"7"}],["rect",{x:"4",y:"16",width:"10",height:"6",rx:"2"}]],jh=[["path",{d:"M10.268 21a2 2 0 0 0 3.464 0"}],["path",{d:"M15 8h6"}],["path",{d:"M16.243 3.757A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673A9.4 9.4 0 0 1 18.667 12"}]],Nh=[["path",{d:"M10.268 21a2 2 0 0 0 3.464 0"}],["path",{d:"M17 17H4a1 1 0 0 1-.74-1.673C4.59 13.956 6 12.499 6 8a6 6 0 0 1 .258-1.742"}],["path",{d:"m2 2 20 20"}],["path",{d:"M8.668 3.01A6 6 0 0 1 18 8c0 2.687.77 4.653 1.707 6.05"}]],Kh=[["path",{d:"M10.268 21a2 2 0 0 0 3.464 0"}],["path",{d:"M15 8h6"}],["path",{d:"M18 5v6"}],["path",{d:"M20.002 14.464a9 9 0 0 0 .738.863A1 1 0 0 1 20 17H4a1 1 0 0 1-.74-1.673C4.59 13.956 6 12.499 6 8a6 6 0 0 1 8.75-5.332"}]],Qh=[["path",{d:"M10.268 21a2 2 0 0 0 3.464 0"}],["path",{d:"M22 8c0-2.3-.8-4.3-2-6"}],["path",{d:"M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326"}],["path",{d:"M4 2C2.8 3.7 2 5.7 2 8"}]],Jh=[["path",{d:"M10.268 21a2 2 0 0 0 3.464 0"}],["path",{d:"M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326"}]],k=[["rect",{width:"13",height:"7",x:"3",y:"3",rx:"1"}],["path",{d:"m22 15-3-3 3-3"}],["rect",{width:"13",height:"7",x:"3",y:"14",rx:"1"}]],P=[["rect",{width:"13",height:"7",x:"8",y:"3",rx:"1"}],["path",{d:"m2 9 3 3-3 3"}],["rect",{width:"13",height:"7",x:"8",y:"14",rx:"1"}]],Yh=[["rect",{width:"7",height:"13",x:"3",y:"3",rx:"1"}],["path",{d:"m9 22 3-3 3 3"}],["rect",{width:"7",height:"13",x:"14",y:"3",rx:"1"}]],_h=[["rect",{width:"7",height:"13",x:"3",y:"8",rx:"1"}],["path",{d:"m15 2-3 3-3-3"}],["rect",{width:"7",height:"13",x:"14",y:"8",rx:"1"}]],xh=[["path",{d:"M12.409 13.017A5 5 0 0 1 22 15c0 3.866-4 7-9 7-4.077 0-8.153-.82-10.371-2.462-.426-.316-.631-.832-.62-1.362C2.118 12.723 2.627 2 10 2a3 3 0 0 1 3 3 2 2 0 0 1-2 2c-1.105 0-1.64-.444-2-1"}],["path",{d:"M15 14a5 5 0 0 0-7.584 2"}],["path",{d:"M9.964 6.825C8.019 7.977 9.5 13 8 15"}]],a4=[["circle",{cx:"18.5",cy:"17.5",r:"3.5"}],["circle",{cx:"5.5",cy:"17.5",r:"3.5"}],["circle",{cx:"15",cy:"5",r:"1"}],["path",{d:"M12 17.5V14l-3-3 4-3 2 3h2"}]],t4=[["rect",{x:"14",y:"14",width:"4",height:"6",rx:"2"}],["rect",{x:"6",y:"4",width:"4",height:"6",rx:"2"}],["path",{d:"M6 20h4"}],["path",{d:"M14 10h4"}],["path",{d:"M6 14h2v6"}],["path",{d:"M14 4h2v6"}]],h4=[["path",{d:"M10 10h4"}],["path",{d:"M19 7V4a1 1 0 0 0-1-1h-2a1 1 0 0 0-1 1v3"}],["path",{d:"M20 21a2 2 0 0 0 2-2v-3.851c0-1.39-2-2.962-2-4.829V8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v11a2 2 0 0 0 2 2z"}],["path",{d:"M 22 16 L 2 16"}],["path",{d:"M4 21a2 2 0 0 1-2-2v-3.851c0-1.39 2-2.962 2-4.829V8a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v11a2 2 0 0 1-2 2z"}],["path",{d:"M9 7V4a1 1 0 0 0-1-1H6a1 1 0 0 0-1 1v3"}]],d4=[["circle",{cx:"12",cy:"11.9",r:"2"}],["path",{d:"M6.7 3.4c-.9 2.5 0 5.2 2.2 6.7C6.5 9 3.7 9.6 2 11.6"}],["path",{d:"m8.9 10.1 1.4.8"}],["path",{d:"M17.3 3.4c.9 2.5 0 5.2-2.2 6.7 2.4-1.2 5.2-.6 6.9 1.5"}],["path",{d:"m15.1 10.1-1.4.8"}],["path",{d:"M16.7 20.8c-2.6-.4-4.6-2.6-4.7-5.3-.2 2.6-2.1 4.8-4.7 5.2"}],["path",{d:"M12 13.9v1.6"}],["path",{d:"M13.5 5.4c-1-.2-2-.2-3 0"}],["path",{d:"M17 16.4c.7-.7 1.2-1.6 1.5-2.5"}],["path",{d:"M5.5 13.9c.3.9.8 1.8 1.5 2.5"}]],c4=[["path",{d:"M16 7h.01"}],["path",{d:"M3.4 18H12a8 8 0 0 0 8-8V7a4 4 0 0 0-7.28-2.3L2 20"}],["path",{d:"m20 7 2 .5-2 .5"}],["path",{d:"M10 18v3"}],["path",{d:"M14 17.75V21"}],["path",{d:"M7 18a6 6 0 0 0 3.84-10.61"}]],M4=[["circle",{cx:"9",cy:"9",r:"7"}],["circle",{cx:"15",cy:"15",r:"7"}]],p4=[["path",{d:"M11.767 19.089c4.924.868 6.14-6.025 1.216-6.894m-1.216 6.894L5.86 18.047m5.908 1.042-.347 1.97m1.563-8.864c4.924.869 6.14-6.025 1.215-6.893m-1.215 6.893-3.94-.694m5.155-6.2L8.29 4.26m5.908 1.042.348-1.97M7.48 20.364l3.126-17.727"}]],i4=[["path",{d:"M10 22V7a1 1 0 0 0-1-1H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-5a1 1 0 0 0-1-1H2"}],["rect",{x:"14",y:"2",width:"8",height:"8",rx:"1"}]],n4=[["path",{d:"M3 3h18"}],["path",{d:"M20 7H8"}],["path",{d:"M20 11H8"}],["path",{d:"M10 19h10"}],["path",{d:"M8 15h12"}],["path",{d:"M4 3v14"}],["circle",{cx:"4",cy:"19",r:"2"}]],l4=[["path",{d:"m7 7 10 10-5 5V2l5 5L7 17"}],["line",{x1:"18",x2:"21",y1:"12",y2:"12"}],["line",{x1:"3",x2:"6",y1:"12",y2:"12"}]],e4=[["path",{d:"m7 7 10 10-5 5V2l5 5L7 17"}],["path",{d:"M20.83 14.83a4 4 0 0 0 0-5.66"}],["path",{d:"M18 12h.01"}]],r4=[["path",{d:"m7 7 10 10-5 5V2l5 5L7 17"}]],o4=[["path",{d:"m17 17-5 5V12l-5 5"}],["path",{d:"m2 2 20 20"}],["path",{d:"M14.5 9.5 17 7l-5-5v4.5"}]],v4=[["path",{d:"M6 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8"}]],$4=[["path",{d:"M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"}],["circle",{cx:"12",cy:"12",r:"4"}]],m4=[["circle",{cx:"11",cy:"13",r:"9"}],["path",{d:"M14.35 4.65 16.3 2.7a2.41 2.41 0 0 1 3.4 0l1.6 1.6a2.4 2.4 0 0 1 0 3.4l-1.95 1.95"}],["path",{d:"m22 2-1.5 1.5"}]],y4=[["path",{d:"M17 10c.7-.7 1.69 0 2.5 0a2.5 2.5 0 1 0 0-5 .5.5 0 0 1-.5-.5 2.5 2.5 0 1 0-5 0c0 .81.7 1.8 0 2.5l-7 7c-.7.7-1.69 0-2.5 0a2.5 2.5 0 0 0 0 5c.28 0 .5.22.5.5a2.5 2.5 0 1 0 5 0c0-.81-.7-1.8 0-2.5Z"}]],s4=[["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}],["path",{d:"m8 13 4-7 4 7"}],["path",{d:"M9.1 11h5.7"}]],g4=[["path",{d:"M12 13h.01"}],["path",{d:"M12 6v3"}],["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}]],u4=[["path",{d:"M12 6v7"}],["path",{d:"M16 8v3"}],["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}],["path",{d:"M8 8v3"}]],C4=[["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}],["path",{d:"m9 9.5 2 2 4-4"}]],H4=[["path",{d:"M5 7a2 2 0 0 0-2 2v11"}],["path",{d:"M5.803 18H5a2 2 0 0 0 0 4h9.5a.5.5 0 0 0 .5-.5V21"}],["path",{d:"M9 15V4a2 2 0 0 1 2-2h9.5a.5.5 0 0 1 .5.5v14a.5.5 0 0 1-.5.5H11a2 2 0 0 1 0-4h10"}]],B=[["path",{d:"M12 17h1.5"}],["path",{d:"M12 22h1.5"}],["path",{d:"M12 2h1.5"}],["path",{d:"M17.5 22H19a1 1 0 0 0 1-1"}],["path",{d:"M17.5 2H19a1 1 0 0 1 1 1v1.5"}],["path",{d:"M20 14v3h-2.5"}],["path",{d:"M20 8.5V10"}],["path",{d:"M4 10V8.5"}],["path",{d:"M4 19.5V14"}],["path",{d:"M4 4.5A2.5 2.5 0 0 1 6.5 2H8"}],["path",{d:"M8 22H6.5a1 1 0 0 1 0-5H8"}]],A4=[["path",{d:"M12 13V7"}],["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}],["path",{d:"m9 10 3 3 3-3"}]],w4=[["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}],["path",{d:"M8 12v-2a4 4 0 0 1 8 0v2"}],["circle",{cx:"15",cy:"12",r:"1"}],["circle",{cx:"9",cy:"12",r:"1"}]],V4=[["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}],["path",{d:"M8.62 9.8A2.25 2.25 0 1 1 12 6.836a2.25 2.25 0 1 1 3.38 2.966l-2.626 2.856a.998.998 0 0 1-1.507 0z"}]],S4=[["path",{d:"m20 13.7-2.1-2.1a2 2 0 0 0-2.8 0L9.7 17"}],["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}],["circle",{cx:"10",cy:"8",r:"2"}]],L4=[["path",{d:"m19 3 1 1"}],["path",{d:"m20 2-4.5 4.5"}],["path",{d:"M20 7.898V21a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}],["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2h7.844"}],["circle",{cx:"14",cy:"8",r:"2"}]],f4=[["path",{d:"M18 6V4a2 2 0 1 0-4 0v2"}],["path",{d:"M20 15v6a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}],["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H10"}],["rect",{x:"12",y:"6",width:"8",height:"5",rx:"1"}]],k4=[["path",{d:"M10 2v8l3-3 3 3V2"}],["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}]],P4=[["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}],["path",{d:"M9 10h6"}]],B4=[["path",{d:"M12 21V7"}],["path",{d:"m16 12 2 2 4-4"}],["path",{d:"M22 6V4a1 1 0 0 0-1-1h-5a4 4 0 0 0-4 4 4 4 0 0 0-4-4H3a1 1 0 0 0-1 1v13a1 1 0 0 0 1 1h6a3 3 0 0 1 3 3 3 3 0 0 1 3-3h6a1 1 0 0 0 1-1v-1.3"}]],D4=[["path",{d:"M12 7v14"}],["path",{d:"M16 12h2"}],["path",{d:"M16 8h2"}],["path",{d:"M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"}],["path",{d:"M6 12h2"}],["path",{d:"M6 8h2"}]],F4=[["path",{d:"M12 7v14"}],["path",{d:"M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"}]],z4=[["path",{d:"M12 7v6"}],["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}],["path",{d:"M9 10h6"}]],b4=[["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}],["path",{d:"M8 11h8"}],["path",{d:"M8 7h6"}]],R4=[["path",{d:"M12 13V7"}],["path",{d:"M18 2h1a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}],["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2"}],["path",{d:"m9 10 3-3 3 3"}],["path",{d:"m9 5 3-3 3 3"}]],T4=[["path",{d:"M10 13h4"}],["path",{d:"M12 6v7"}],["path",{d:"M16 8V6H8v2"}],["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}]],q4=[["path",{d:"M12 13V7"}],["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}],["path",{d:"m9 10 3-3 3 3"}]],U4=[["path",{d:"M15 13a3 3 0 1 0-6 0"}],["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}],["circle",{cx:"12",cy:"8",r:"2"}]],O4=[["path",{d:"m14.5 7-5 5"}],["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}],["path",{d:"m9.5 7 5 5"}]],Z4=[["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}]],G4=[["path",{d:"m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2Z"}],["path",{d:"m9 10 2 2 4-4"}]],I4=[["path",{d:"m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z"}],["line",{x1:"15",x2:"9",y1:"10",y2:"10"}]],W4=[["path",{d:"m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z"}],["line",{x1:"12",x2:"12",y1:"7",y2:"13"}],["line",{x1:"15",x2:"9",y1:"10",y2:"10"}]],E4=[["path",{d:"m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2Z"}],["path",{d:"m14.5 7.5-5 5"}],["path",{d:"m9.5 7.5 5 5"}]],X4=[["path",{d:"m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z"}]],j4=[["path",{d:"M4 9V5a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v4"}],["path",{d:"M8 8v1"}],["path",{d:"M12 8v1"}],["path",{d:"M16 8v1"}],["rect",{width:"20",height:"12",x:"2",y:"9",rx:"2"}],["circle",{cx:"8",cy:"15",r:"2"}],["circle",{cx:"16",cy:"15",r:"2"}]],N4=[["path",{d:"M13.67 8H18a2 2 0 0 1 2 2v4.33"}],["path",{d:"M2 14h2"}],["path",{d:"M20 14h2"}],["path",{d:"M22 22 2 2"}],["path",{d:"M8 8H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h12a2 2 0 0 0 1.414-.586"}],["path",{d:"M9 13v2"}],["path",{d:"M9.67 4H12v2.33"}]],K4=[["path",{d:"M12 6V2H8"}],["path",{d:"M15 11v2"}],["path",{d:"M2 12h2"}],["path",{d:"M20 12h2"}],["path",{d:"M20 16a2 2 0 0 1-2 2H8.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 4 20.286V8a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2z"}],["path",{d:"M9 11v2"}]],Q4=[["path",{d:"M12 8V4H8"}],["rect",{width:"16",height:"12",x:"4",y:"8",rx:"2"}],["path",{d:"M2 14h2"}],["path",{d:"M20 14h2"}],["path",{d:"M15 13v2"}],["path",{d:"M9 13v2"}]],J4=[["path",{d:"M10 3a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v2a6 6 0 0 0 1.2 3.6l.6.8A6 6 0 0 1 17 13v8a1 1 0 0 1-1 1H8a1 1 0 0 1-1-1v-8a6 6 0 0 1 1.2-3.6l.6-.8A6 6 0 0 0 10 5z"}],["path",{d:"M17 13h-4a1 1 0 0 0-1 1v3a1 1 0 0 0 1 1h4"}]],Y4=[["path",{d:"M17 3h4v4"}],["path",{d:"M18.575 11.082a13 13 0 0 1 1.048 9.027 1.17 1.17 0 0 1-1.914.597L14 17"}],["path",{d:"M7 10 3.29 6.29a1.17 1.17 0 0 1 .6-1.91 13 13 0 0 1 9.03 1.05"}],["path",{d:"M7 14a1.7 1.7 0 0 0-1.207.5l-2.646 2.646A.5.5 0 0 0 3.5 18H5a1 1 0 0 1 1 1v1.5a.5.5 0 0 0 .854.354L9.5 18.207A1.7 1.7 0 0 0 10 17v-2a1 1 0 0 0-1-1z"}],["path",{d:"M9.707 14.293 21 3"}]],_4=[["path",{d:"M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"}],["path",{d:"m3.3 7 8.7 5 8.7-5"}],["path",{d:"M12 22V12"}]],x4=[["path",{d:"M2.97 12.92A2 2 0 0 0 2 14.63v3.24a2 2 0 0 0 .97 1.71l3 1.8a2 2 0 0 0 2.06 0L12 19v-5.5l-5-3-4.03 2.42Z"}],["path",{d:"m7 16.5-4.74-2.85"}],["path",{d:"m7 16.5 5-3"}],["path",{d:"M7 16.5v5.17"}],["path",{d:"M12 13.5V19l3.97 2.38a2 2 0 0 0 2.06 0l3-1.8a2 2 0 0 0 .97-1.71v-3.24a2 2 0 0 0-.97-1.71L17 10.5l-5 3Z"}],["path",{d:"m17 16.5-5-3"}],["path",{d:"m17 16.5 4.74-2.85"}],["path",{d:"M17 16.5v5.17"}],["path",{d:"M7.97 4.42A2 2 0 0 0 7 6.13v4.37l5 3 5-3V6.13a2 2 0 0 0-.97-1.71l-3-1.8a2 2 0 0 0-2.06 0l-3 1.8Z"}],["path",{d:"M12 8 7.26 5.15"}],["path",{d:"m12 8 4.74-2.85"}],["path",{d:"M12 13.5V8"}]],D=[["path",{d:"M8 3H7a2 2 0 0 0-2 2v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5c0 1.1.9 2 2 2h1"}],["path",{d:"M16 21h1a2 2 0 0 0 2-2v-5c0-1.1.9-2 2-2a2 2 0 0 1-2-2V5a2 2 0 0 0-2-2h-1"}]],a5=[["path",{d:"M16 3h3a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1h-3"}],["path",{d:"M8 21H5a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h3"}]],t5=[["path",{d:"M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z"}],["path",{d:"M9 13a4.5 4.5 0 0 0 3-4"}],["path",{d:"M6.003 5.125A3 3 0 0 0 6.401 6.5"}],["path",{d:"M3.477 10.896a4 4 0 0 1 .585-.396"}],["path",{d:"M6 18a4 4 0 0 1-1.967-.516"}],["path",{d:"M12 13h4"}],["path",{d:"M12 18h6a2 2 0 0 1 2 2v1"}],["path",{d:"M12 8h8"}],["path",{d:"M16 8V5a2 2 0 0 1 2-2"}],["circle",{cx:"16",cy:"13",r:".5"}],["circle",{cx:"18",cy:"3",r:".5"}],["circle",{cx:"20",cy:"21",r:".5"}],["circle",{cx:"20",cy:"8",r:".5"}]],h5=[["path",{d:"m10.852 14.772-.383.923"}],["path",{d:"m10.852 9.228-.383-.923"}],["path",{d:"m13.148 14.772.382.924"}],["path",{d:"m13.531 8.305-.383.923"}],["path",{d:"m14.772 10.852.923-.383"}],["path",{d:"m14.772 13.148.923.383"}],["path",{d:"M17.598 6.5A3 3 0 1 0 12 5a3 3 0 0 0-5.63-1.446 3 3 0 0 0-.368 1.571 4 4 0 0 0-2.525 5.771"}],["path",{d:"M17.998 5.125a4 4 0 0 1 2.525 5.771"}],["path",{d:"M19.505 10.294a4 4 0 0 1-1.5 7.706"}],["path",{d:"M4.032 17.483A4 4 0 0 0 11.464 20c.18-.311.892-.311 1.072 0a4 4 0 0 0 7.432-2.516"}],["path",{d:"M4.5 10.291A4 4 0 0 0 6 18"}],["path",{d:"M6.002 5.125a3 3 0 0 0 .4 1.375"}],["path",{d:"m9.228 10.852-.923-.383"}],["path",{d:"m9.228 13.148-.923.383"}],["circle",{cx:"12",cy:"12",r:"3"}]],d5=[["path",{d:"M12 18V5"}],["path",{d:"M15 13a4.17 4.17 0 0 1-3-4 4.17 4.17 0 0 1-3 4"}],["path",{d:"M17.598 6.5A3 3 0 1 0 12 5a3 3 0 1 0-5.598 1.5"}],["path",{d:"M17.997 5.125a4 4 0 0 1 2.526 5.77"}],["path",{d:"M18 18a4 4 0 0 0 2-7.464"}],["path",{d:"M19.967 17.483A4 4 0 1 1 12 18a4 4 0 1 1-7.967-.517"}],["path",{d:"M6 18a4 4 0 0 1-2-7.464"}],["path",{d:"M6.003 5.125a4 4 0 0 0-2.526 5.77"}]],c5=[["path",{d:"M16 3v2.107"}],["path",{d:"M17 9c1 3 2.5 3.5 3.5 4.5A5 5 0 0 1 22 17a5 5 0 0 1-10 0c0-.3 0-.6.1-.9a2 2 0 1 0 3.3-2C13 11.5 16 9 17 9"}],["path",{d:"M21 8.274V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h3.938"}],["path",{d:"M3 15h5.253"}],["path",{d:"M3 9h8.228"}],["path",{d:"M8 15v6"}],["path",{d:"M8 3v6"}]],M5=[["path",{d:"M12 9v1.258"}],["path",{d:"M16 3v5.46"}],["path",{d:"M21 9.118V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h5.75"}],["path",{d:"M22 17.5c0 2.499-1.75 3.749-3.83 4.474a.5.5 0 0 1-.335-.005c-2.085-.72-3.835-1.97-3.835-4.47V14a.5.5 0 0 1 .5-.499c1 0 2.25-.6 3.12-1.36a.6.6 0 0 1 .76-.001c.875.765 2.12 1.36 3.12 1.36a.5.5 0 0 1 .5.5z"}],["path",{d:"M3 15h7"}],["path",{d:"M3 9h12.142"}],["path",{d:"M8 15v6"}],["path",{d:"M8 3v6"}]],p5=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M12 9v6"}],["path",{d:"M16 15v6"}],["path",{d:"M16 3v6"}],["path",{d:"M3 15h18"}],["path",{d:"M3 9h18"}],["path",{d:"M8 15v6"}],["path",{d:"M8 3v6"}]],i5=[["path",{d:"M12 12h.01"}],["path",{d:"M16 6V4a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2"}],["path",{d:"M22 13a18.15 18.15 0 0 1-20 0"}],["rect",{width:"20",height:"14",x:"2",y:"6",rx:"2"}]],n5=[["path",{d:"M10 20v2"}],["path",{d:"M14 20v2"}],["path",{d:"M18 20v2"}],["path",{d:"M21 20H3"}],["path",{d:"M6 20v2"}],["path",{d:"M8 16V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v12"}],["rect",{x:"4",y:"6",width:"16",height:"10",rx:"2"}]],l5=[["path",{d:"M12 11v4"}],["path",{d:"M14 13h-4"}],["path",{d:"M16 6V4a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2"}],["path",{d:"M18 6v14"}],["path",{d:"M6 6v14"}],["rect",{width:"20",height:"14",x:"2",y:"6",rx:"2"}]],e5=[["path",{d:"M16 20V4a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"}],["rect",{width:"20",height:"14",x:"2",y:"6",rx:"2"}]],r5=[["rect",{x:"8",y:"8",width:"8",height:"8",rx:"2"}],["path",{d:"M4 10a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2"}],["path",{d:"M14 20a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2"}]],o5=[["path",{d:"m16 22-1-4"}],["path",{d:"M19 13.99a1 1 0 0 0 1-1V12a2 2 0 0 0-2-2h-3a1 1 0 0 1-1-1V4a2 2 0 0 0-4 0v5a1 1 0 0 1-1 1H6a2 2 0 0 0-2 2v.99a1 1 0 0 0 1 1"}],["path",{d:"M5 14h14l1.973 6.767A1 1 0 0 1 20 22H4a1 1 0 0 1-.973-1.233z"}],["path",{d:"m8 22 1-4"}]],v5=[["path",{d:"m11 10 3 3"}],["path",{d:"M6.5 21A3.5 3.5 0 1 0 3 17.5a2.62 2.62 0 0 1-.708 1.792A1 1 0 0 0 3 21z"}],["path",{d:"M9.969 17.031 21.378 5.624a1 1 0 0 0-3.002-3.002L6.967 14.031"}]],$5=[["path",{d:"M7.2 14.8a2 2 0 0 1 2 2"}],["circle",{cx:"18.5",cy:"8.5",r:"3.5"}],["circle",{cx:"7.5",cy:"16.5",r:"5.5"}],["circle",{cx:"7.5",cy:"4.5",r:"2.5"}]],m5=[["path",{d:"M12 20v-8"}],["path",{d:"M14.12 3.88 16 2"}],["path",{d:"M15 7.13V6a3 3 0 0 0-5.14-2.1L8 2"}],["path",{d:"M18 12.34V11a4 4 0 0 0-4-4h-1.3"}],["path",{d:"m2 2 20 20"}],["path",{d:"M21 5a4 4 0 0 1-3.55 3.97"}],["path",{d:"M22 13h-3.34"}],["path",{d:"M3 21a4 4 0 0 1 3.81-4"}],["path",{d:"M6 13H2"}],["path",{d:"M7.7 7.7A4 4 0 0 0 6 11v3a6 6 0 0 0 11.13 3.13"}]],y5=[["path",{d:"M10 19.655A6 6 0 0 1 6 14v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 3.97"}],["path",{d:"M14 15.003a1 1 0 0 1 1.517-.859l4.997 2.997a1 1 0 0 1 0 1.718l-4.997 2.997a1 1 0 0 1-1.517-.86z"}],["path",{d:"M14.12 3.88 16 2"}],["path",{d:"M21 5a4 4 0 0 1-3.55 3.97"}],["path",{d:"M3 21a4 4 0 0 1 3.81-4"}],["path",{d:"M3 5a4 4 0 0 0 3.55 3.97"}],["path",{d:"M6 13H2"}],["path",{d:"m8 2 1.88 1.88"}],["path",{d:"M9 7.13V6a3 3 0 1 1 6 0v1.13"}]],s5=[["path",{d:"M12 20v-9"}],["path",{d:"M14 7a4 4 0 0 1 4 4v3a6 6 0 0 1-12 0v-3a4 4 0 0 1 4-4z"}],["path",{d:"M14.12 3.88 16 2"}],["path",{d:"M21 21a4 4 0 0 0-3.81-4"}],["path",{d:"M21 5a4 4 0 0 1-3.55 3.97"}],["path",{d:"M22 13h-4"}],["path",{d:"M3 21a4 4 0 0 1 3.81-4"}],["path",{d:"M3 5a4 4 0 0 0 3.55 3.97"}],["path",{d:"M6 13H2"}],["path",{d:"m8 2 1.88 1.88"}],["path",{d:"M9 7.13V6a3 3 0 1 1 6 0v1.13"}]],g5=[["path",{d:"M10 12h4"}],["path",{d:"M10 8h4"}],["path",{d:"M14 21v-3a2 2 0 0 0-4 0v3"}],["path",{d:"M6 10H4a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-2"}],["path",{d:"M6 21V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v16"}]],u5=[["path",{d:"M12 10h.01"}],["path",{d:"M12 14h.01"}],["path",{d:"M12 6h.01"}],["path",{d:"M16 10h.01"}],["path",{d:"M16 14h.01"}],["path",{d:"M16 6h.01"}],["path",{d:"M8 10h.01"}],["path",{d:"M8 14h.01"}],["path",{d:"M8 6h.01"}],["path",{d:"M9 22v-3a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v3"}],["rect",{x:"4",y:"2",width:"16",height:"20",rx:"2"}]],C5=[["path",{d:"M4 6 2 7"}],["path",{d:"M10 6h4"}],["path",{d:"m22 7-2-1"}],["rect",{width:"16",height:"16",x:"4",y:"3",rx:"2"}],["path",{d:"M4 11h16"}],["path",{d:"M8 15h.01"}],["path",{d:"M16 15h.01"}],["path",{d:"M6 19v2"}],["path",{d:"M18 21v-2"}]],H5=[["path",{d:"M8 6v6"}],["path",{d:"M15 6v6"}],["path",{d:"M2 12h19.6"}],["path",{d:"M18 18h3s.5-1.7.8-2.8c.1-.4.2-.8.2-1.2 0-.4-.1-.8-.2-1.2l-1.4-5C20.1 6.8 19.1 6 18 6H4a2 2 0 0 0-2 2v10h3"}],["circle",{cx:"7",cy:"18",r:"2"}],["path",{d:"M9 18h5"}],["circle",{cx:"16",cy:"18",r:"2"}]],A5=[["path",{d:"M10 3h.01"}],["path",{d:"M14 2h.01"}],["path",{d:"m2 9 20-5"}],["path",{d:"M12 12V6.5"}],["rect",{width:"16",height:"10",x:"4",y:"12",rx:"3"}],["path",{d:"M9 12v5"}],["path",{d:"M15 12v5"}],["path",{d:"M4 17h16"}]],w5=[["path",{d:"M17 19a1 1 0 0 1-1-1v-2a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2a1 1 0 0 1-1 1z"}],["path",{d:"M17 21v-2"}],["path",{d:"M19 14V6.5a1 1 0 0 0-7 0v11a1 1 0 0 1-7 0V10"}],["path",{d:"M21 21v-2"}],["path",{d:"M3 5V3"}],["path",{d:"M4 10a2 2 0 0 1-2-2V6a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2a2 2 0 0 1-2 2z"}],["path",{d:"M7 5V3"}]],V5=[["path",{d:"M16 13H3"}],["path",{d:"M16 17H3"}],["path",{d:"m7.2 7.9-3.388 2.5A2 2 0 0 0 3 12.01V20a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1v-8.654c0-2-2.44-6.026-6.44-8.026a1 1 0 0 0-1.082.057L10.4 5.6"}],["circle",{cx:"9",cy:"7",r:"2"}]],S5=[["path",{d:"M20 21v-8a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8"}],["path",{d:"M4 16s.5-1 2-1 2.5 2 4 2 2.5-2 4-2 2.5 2 4 2 2-1 2-1"}],["path",{d:"M2 21h20"}],["path",{d:"M7 8v3"}],["path",{d:"M12 8v3"}],["path",{d:"M17 8v3"}],["path",{d:"M7 4h.01"}],["path",{d:"M12 4h.01"}],["path",{d:"M17 4h.01"}]],L5=[["rect",{width:"16",height:"20",x:"4",y:"2",rx:"2"}],["line",{x1:"8",x2:"16",y1:"6",y2:"6"}],["line",{x1:"16",x2:"16",y1:"14",y2:"18"}],["path",{d:"M16 10h.01"}],["path",{d:"M12 10h.01"}],["path",{d:"M8 10h.01"}],["path",{d:"M12 14h.01"}],["path",{d:"M8 14h.01"}],["path",{d:"M12 18h.01"}],["path",{d:"M8 18h.01"}]],f5=[["path",{d:"M11 14h1v4"}],["path",{d:"M16 2v4"}],["path",{d:"M3 10h18"}],["path",{d:"M8 2v4"}],["rect",{x:"3",y:"4",width:"18",height:"18",rx:"2"}]],k5=[["path",{d:"m14 18 4 4 4-4"}],["path",{d:"M16 2v4"}],["path",{d:"M18 14v8"}],["path",{d:"M21 11.354V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h7.343"}],["path",{d:"M3 10h18"}],["path",{d:"M8 2v4"}]],P5=[["path",{d:"m14 18 4-4 4 4"}],["path",{d:"M16 2v4"}],["path",{d:"M18 22v-8"}],["path",{d:"M21 11.343V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h9"}],["path",{d:"M3 10h18"}],["path",{d:"M8 2v4"}]],B5=[["path",{d:"M8 2v4"}],["path",{d:"M16 2v4"}],["path",{d:"M21 14V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h8"}],["path",{d:"M3 10h18"}],["path",{d:"m16 20 2 2 4-4"}]],D5=[["path",{d:"M8 2v4"}],["path",{d:"M16 2v4"}],["rect",{width:"18",height:"18",x:"3",y:"4",rx:"2"}],["path",{d:"M3 10h18"}],["path",{d:"m9 16 2 2 4-4"}]],F5=[["path",{d:"M16 14v2.2l1.6 1"}],["path",{d:"M16 2v4"}],["path",{d:"M21 7.5V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h3.5"}],["path",{d:"M3 10h5"}],["path",{d:"M8 2v4"}],["circle",{cx:"16",cy:"16",r:"6"}]],z5=[["path",{d:"M8 2v4"}],["path",{d:"M16 2v4"}],["rect",{width:"18",height:"18",x:"3",y:"4",rx:"2"}],["path",{d:"M3 10h18"}],["path",{d:"M8 14h.01"}],["path",{d:"M12 14h.01"}],["path",{d:"M16 14h.01"}],["path",{d:"M8 18h.01"}],["path",{d:"M12 18h.01"}],["path",{d:"M16 18h.01"}]],b5=[["path",{d:"M8 2v4"}],["path",{d:"M16 2v4"}],["path",{d:"M21 17V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h11Z"}],["path",{d:"M3 10h18"}],["path",{d:"M15 22v-4a2 2 0 0 1 2-2h4"}]],R5=[["path",{d:"m15.228 16.852-.923-.383"}],["path",{d:"m15.228 19.148-.923.383"}],["path",{d:"M16 2v4"}],["path",{d:"m16.47 14.305.382.923"}],["path",{d:"m16.852 20.772-.383.924"}],["path",{d:"m19.148 15.228.383-.923"}],["path",{d:"m19.53 21.696-.382-.924"}],["path",{d:"m20.772 16.852.924-.383"}],["path",{d:"m20.772 19.148.924.383"}],["path",{d:"M21 10.592V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h6"}],["path",{d:"M3 10h18"}],["path",{d:"M8 2v4"}],["circle",{cx:"18",cy:"18",r:"3"}]],T5=[["path",{d:"M12.127 22H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v5.125"}],["path",{d:"M14.62 18.8A2.25 2.25 0 1 1 18 15.836a2.25 2.25 0 1 1 3.38 2.966l-2.626 2.856a.998.998 0 0 1-1.507 0z"}],["path",{d:"M16 2v4"}],["path",{d:"M3 10h18"}],["path",{d:"M8 2v4"}]],q5=[["path",{d:"M8 2v4"}],["path",{d:"M16 2v4"}],["rect",{width:"18",height:"18",x:"3",y:"4",rx:"2"}],["path",{d:"M3 10h18"}],["path",{d:"M10 16h4"}]],U5=[["path",{d:"M16 19h6"}],["path",{d:"M16 2v4"}],["path",{d:"M21 15V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h8.5"}],["path",{d:"M3 10h18"}],["path",{d:"M8 2v4"}]],O5=[["path",{d:"M4.2 4.2A2 2 0 0 0 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 1.82-1.18"}],["path",{d:"M21 15.5V6a2 2 0 0 0-2-2H9.5"}],["path",{d:"M16 2v4"}],["path",{d:"M3 10h7"}],["path",{d:"M21 10h-5.5"}],["path",{d:"m2 2 20 20"}]],Z5=[["path",{d:"M16 19h6"}],["path",{d:"M16 2v4"}],["path",{d:"M19 16v6"}],["path",{d:"M21 12.598V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h8.5"}],["path",{d:"M3 10h18"}],["path",{d:"M8 2v4"}]],G5=[["rect",{width:"18",height:"18",x:"3",y:"4",rx:"2"}],["path",{d:"M16 2v4"}],["path",{d:"M3 10h18"}],["path",{d:"M8 2v4"}],["path",{d:"M17 14h-6"}],["path",{d:"M13 18H7"}],["path",{d:"M7 14h.01"}],["path",{d:"M17 18h.01"}]],I5=[["path",{d:"M8 2v4"}],["path",{d:"M16 2v4"}],["rect",{width:"18",height:"18",x:"3",y:"4",rx:"2"}],["path",{d:"M3 10h18"}],["path",{d:"M10 16h4"}],["path",{d:"M12 14v4"}]],W5=[["path",{d:"M11 10v4h4"}],["path",{d:"m11 14 1.535-1.605a5 5 0 0 1 8 1.5"}],["path",{d:"M16 2v4"}],["path",{d:"m21 18-1.535 1.605a5 5 0 0 1-8-1.5"}],["path",{d:"M21 22v-4h-4"}],["path",{d:"M21 8.5V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h4.3"}],["path",{d:"M3 10h4"}],["path",{d:"M8 2v4"}]],E5=[["path",{d:"M16 2v4"}],["path",{d:"M21 11.75V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h7.25"}],["path",{d:"m22 22-1.875-1.875"}],["path",{d:"M3 10h18"}],["path",{d:"M8 2v4"}],["circle",{cx:"18",cy:"18",r:"3"}]],X5=[["path",{d:"M8 2v4"}],["path",{d:"M16 2v4"}],["path",{d:"M21 13V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h8"}],["path",{d:"M3 10h18"}],["path",{d:"m17 22 5-5"}],["path",{d:"m17 17 5 5"}]],j5=[["path",{d:"M8 2v4"}],["path",{d:"M16 2v4"}],["rect",{width:"18",height:"18",x:"3",y:"4",rx:"2"}],["path",{d:"M3 10h18"}],["path",{d:"m14 14-4 4"}],["path",{d:"m10 14 4 4"}]],N5=[["path",{d:"M8 2v4"}],["path",{d:"M16 2v4"}],["rect",{width:"18",height:"18",x:"3",y:"4",rx:"2"}],["path",{d:"M3 10h18"}]],K5=[["path",{d:"M14.564 14.558a3 3 0 1 1-4.122-4.121"}],["path",{d:"m2 2 20 20"}],["path",{d:"M20 20H4a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h1.997a2 2 0 0 0 .819-.175"}],["path",{d:"M9.695 4.024A2 2 0 0 1 10.004 4h3.993a2 2 0 0 1 1.76 1.05l.486.9A2 2 0 0 0 18.003 7H20a2 2 0 0 1 2 2v7.344"}]],Q5=[["path",{d:"M13.997 4a2 2 0 0 1 1.76 1.05l.486.9A2 2 0 0 0 18.003 7H20a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h1.997a2 2 0 0 0 1.759-1.048l.489-.904A2 2 0 0 1 10.004 4z"}],["circle",{cx:"12",cy:"13",r:"3"}]],J5=[["path",{d:"M5.7 21a2 2 0 0 1-3.5-2l8.6-14a6 6 0 0 1 10.4 6 2 2 0 1 1-3.464-2 2 2 0 1 0-3.464-2Z"}],["path",{d:"M17.75 7 15 2.1"}],["path",{d:"M10.9 4.8 13 9"}],["path",{d:"m7.9 9.7 2 4.4"}],["path",{d:"M4.9 14.7 7 18.9"}]],Y5=[["path",{d:"M10 10v7.9"}],["path",{d:"M11.802 6.145a5 5 0 0 1 6.053 6.053"}],["path",{d:"M14 6.1v2.243"}],["path",{d:"m15.5 15.571-.964.964a5 5 0 0 1-7.071 0 5 5 0 0 1 0-7.07l.964-.965"}],["path",{d:"M16 7V3a1 1 0 0 1 1.707-.707 2.5 2.5 0 0 0 2.152.717 1 1 0 0 1 1.131 1.131 2.5 2.5 0 0 0 .717 2.152A1 1 0 0 1 21 8h-4"}],["path",{d:"m2 2 20 20"}],["path",{d:"M8 17v4a1 1 0 0 1-1.707.707 2.5 2.5 0 0 0-2.152-.717 1 1 0 0 1-1.131-1.131 2.5 2.5 0 0 0-.717-2.152A1 1 0 0 1 3 16h4"}]],_5=[["path",{d:"M10 7v10.9"}],["path",{d:"M14 6.1V17"}],["path",{d:"M16 7V3a1 1 0 0 1 1.707-.707 2.5 2.5 0 0 0 2.152.717 1 1 0 0 1 1.131 1.131 2.5 2.5 0 0 0 .717 2.152A1 1 0 0 1 21 8h-4"}],["path",{d:"M16.536 7.465a5 5 0 0 0-7.072 0l-2 2a5 5 0 0 0 0 7.07 5 5 0 0 0 7.072 0l2-2a5 5 0 0 0 0-7.07"}],["path",{d:"M8 17v4a1 1 0 0 1-1.707.707 2.5 2.5 0 0 0-2.152-.717 1 1 0 0 1-1.131-1.131 2.5 2.5 0 0 0-.717-2.152A1 1 0 0 1 3 16h4"}]],x5=[["path",{d:"M12 22v-4"}],["path",{d:"M7 12c-1.5 0-4.5 1.5-5 3 3.5 1.5 6 1 6 1-1.5 1.5-2 3.5-2 5 2.5 0 4.5-1.5 6-3 1.5 1.5 3.5 3 6 3 0-1.5-.5-3.5-2-5 0 0 2.5.5 6-1-.5-1.5-3.5-3-5-3 1.5-1 4-4 4-6-2.5 0-5.5 1.5-7 3 0-2.5-.5-5-2-7-1.5 2-2 4.5-2 7-1.5-1.5-4.5-3-7-3 0 2 2.5 5 4 6"}]],a3=[["path",{d:"M10.5 5H19a2 2 0 0 1 2 2v8.5"}],["path",{d:"M17 11h-.5"}],["path",{d:"M19 19H5a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2"}],["path",{d:"m2 2 20 20"}],["path",{d:"M7 11h4"}],["path",{d:"M7 15h2.5"}]],F=[["rect",{width:"18",height:"14",x:"3",y:"5",rx:"2",ry:"2"}],["path",{d:"M7 15h4M15 15h2M7 11h2M13 11h4"}]],t3=[["path",{d:"m21 8-2 2-1.5-3.7A2 2 0 0 0 15.646 5H8.4a2 2 0 0 0-1.903 1.257L5 10 3 8"}],["path",{d:"M7 14h.01"}],["path",{d:"M17 14h.01"}],["rect",{width:"18",height:"8",x:"3",y:"10",rx:"2"}],["path",{d:"M5 18v2"}],["path",{d:"M19 18v2"}]],h3=[["path",{d:"M10 2h4"}],["path",{d:"m21 8-2 2-1.5-3.7A2 2 0 0 0 15.646 5H8.4a2 2 0 0 0-1.903 1.257L5 10 3 8"}],["path",{d:"M7 14h.01"}],["path",{d:"M17 14h.01"}],["rect",{width:"18",height:"8",x:"3",y:"10",rx:"2"}],["path",{d:"M5 18v2"}],["path",{d:"M19 18v2"}]],d3=[["path",{d:"M19 17h2c.6 0 1-.4 1-1v-3c0-.9-.7-1.7-1.5-1.9C18.7 10.6 16 10 16 10s-1.3-1.4-2.2-2.3c-.5-.4-1.1-.7-1.8-.7H5c-.6 0-1.1.4-1.4.9l-1.4 2.9A3.7 3.7 0 0 0 2 12v4c0 .6.4 1 1 1h2"}],["circle",{cx:"7",cy:"17",r:"2"}],["path",{d:"M9 17h6"}],["circle",{cx:"17",cy:"17",r:"2"}]],c3=[["path",{d:"M18 19V9a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v8a2 2 0 0 0 2 2h2"}],["path",{d:"M2 9h3a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H2"}],["path",{d:"M22 17v1a1 1 0 0 1-1 1H10v-9a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v9"}],["circle",{cx:"8",cy:"19",r:"2"}]],M3=[["path",{d:"M12 14v4"}],["path",{d:"M14.172 2a2 2 0 0 1 1.414.586l3.828 3.828A2 2 0 0 1 20 7.828V20a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2z"}],["path",{d:"M8 14h8"}],["rect",{x:"8",y:"10",width:"8",height:"8",rx:"1"}]],p3=[["path",{d:"M2.27 21.7s9.87-3.5 12.73-6.36a4.5 4.5 0 0 0-6.36-6.37C5.77 11.84 2.27 21.7 2.27 21.7zM8.64 14l-2.05-2.04M15.34 15l-2.46-2.46"}],["path",{d:"M22 9s-1.33-2-3.5-2C16.86 7 15 9 15 9s1.33 2 3.5 2S22 9 22 9z"}],["path",{d:"M15 2s-2 1.33-2 3.5S15 9 15 9s2-1.84 2-3.5C17 3.33 15 2 15 2z"}]],i3=[["path",{d:"M10 9v7"}],["path",{d:"M14 6v10"}],["circle",{cx:"17.5",cy:"12.5",r:"3.5"}],["circle",{cx:"6.5",cy:"12.5",r:"3.5"}]],n3=[["path",{d:"m2 16 4.039-9.69a.5.5 0 0 1 .923 0L11 16"}],["path",{d:"M22 9v7"}],["path",{d:"M3.304 13h6.392"}],["circle",{cx:"18.5",cy:"12.5",r:"3.5"}]],l3=[["path",{d:"M15 11h4.5a1 1 0 0 1 0 5h-4a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5h3a1 1 0 0 1 0 5"}],["path",{d:"m2 16 4.039-9.69a.5.5 0 0 1 .923 0L11 16"}],["path",{d:"M3.304 13h6.392"}]],e3=[["rect",{width:"20",height:"16",x:"2",y:"4",rx:"2"}],["circle",{cx:"8",cy:"10",r:"2"}],["path",{d:"M8 12h8"}],["circle",{cx:"16",cy:"10",r:"2"}],["path",{d:"m6 20 .7-2.9A1.4 1.4 0 0 1 8.1 16h7.8a1.4 1.4 0 0 1 1.4 1l.7 3"}]],r3=[["path",{d:"M2 8V6a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-6"}],["path",{d:"M2 12a9 9 0 0 1 8 8"}],["path",{d:"M2 16a5 5 0 0 1 4 4"}],["line",{x1:"2",x2:"2.01",y1:"20",y2:"20"}]],o3=[["path",{d:"M10 5V3"}],["path",{d:"M14 5V3"}],["path",{d:"M15 21v-3a3 3 0 0 0-6 0v3"}],["path",{d:"M18 3v8"}],["path",{d:"M18 5H6"}],["path",{d:"M22 11H2"}],["path",{d:"M22 9v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V9"}],["path",{d:"M6 3v8"}]],v3=[["path",{d:"M12 5c.67 0 1.35.09 2 .26 1.78-2 5.03-2.84 6.42-2.26 1.4.58-.42 7-.42 7 .57 1.07 1 2.24 1 3.44C21 17.9 16.97 21 12 21s-9-3-9-7.56c0-1.25.5-2.4 1-3.44 0 0-1.89-6.42-.5-7 1.39-.58 4.72.23 6.5 2.23A9.04 9.04 0 0 1 12 5Z"}],["path",{d:"M8 14v.5"}],["path",{d:"M16 14v.5"}],["path",{d:"M11.25 16.25h1.5L12 17l-.75-.75Z"}]],$3=[["path",{d:"M16.75 12h3.632a1 1 0 0 1 .894 1.447l-2.034 4.069a1 1 0 0 1-1.708.134l-2.124-2.97"}],["path",{d:"M17.106 9.053a1 1 0 0 1 .447 1.341l-3.106 6.211a1 1 0 0 1-1.342.447L3.61 12.3a2.92 2.92 0 0 1-1.3-3.91L3.69 5.6a2.92 2.92 0 0 1 3.92-1.3z"}],["path",{d:"M2 19h3.76a2 2 0 0 0 1.8-1.1L9 15"}],["path",{d:"M2 21v-4"}],["path",{d:"M7 9h.01"}]],z=[["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}],["path",{d:"M7 11.207a.5.5 0 0 1 .146-.353l2-2a.5.5 0 0 1 .708 0l3.292 3.292a.5.5 0 0 0 .708 0l4.292-4.292a.5.5 0 0 1 .854.353V16a1 1 0 0 1-1 1H8a1 1 0 0 1-1-1z"}]],b=[["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}],["rect",{x:"7",y:"13",width:"9",height:"4",rx:"1"}],["rect",{x:"7",y:"5",width:"12",height:"4",rx:"1"}]],m3=[["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}],["path",{d:"M7 11h8"}],["path",{d:"M7 16h3"}],["path",{d:"M7 6h12"}]],y3=[["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}],["path",{d:"M7 11h8"}],["path",{d:"M7 16h12"}],["path",{d:"M7 6h3"}]],s3=[["path",{d:"M11 13v4"}],["path",{d:"M15 5v4"}],["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}],["rect",{x:"7",y:"13",width:"9",height:"4",rx:"1"}],["rect",{x:"7",y:"5",width:"12",height:"4",rx:"1"}]],R=[["path",{d:"M9 5v4"}],["rect",{width:"4",height:"6",x:"7",y:"9",rx:"1"}],["path",{d:"M9 15v2"}],["path",{d:"M17 3v2"}],["rect",{width:"4",height:"8",x:"15",y:"5",rx:"1"}],["path",{d:"M17 13v3"}],["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}]],T=[["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}],["path",{d:"M7 16h8"}],["path",{d:"M7 11h12"}],["path",{d:"M7 6h3"}]],g3=[["path",{d:"M13 17V9"}],["path",{d:"M18 17v-3"}],["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}],["path",{d:"M8 17V5"}]],q=[["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}],["rect",{x:"15",y:"5",width:"4",height:"12",rx:"1"}],["rect",{x:"7",y:"8",width:"4",height:"9",rx:"1"}]],U=[["path",{d:"M13 17V9"}],["path",{d:"M18 17V5"}],["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}],["path",{d:"M8 17v-3"}]],u3=[["path",{d:"M11 13H7"}],["path",{d:"M19 9h-4"}],["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}],["rect",{x:"15",y:"5",width:"4",height:"12",rx:"1"}],["rect",{x:"7",y:"8",width:"4",height:"9",rx:"1"}]],O=[["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}],["path",{d:"M18 17V9"}],["path",{d:"M13 17V5"}],["path",{d:"M8 17v-3"}]],C3=[["path",{d:"M10 6h8"}],["path",{d:"M12 16h6"}],["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}],["path",{d:"M8 11h7"}]],H3=[["path",{d:"m13.11 7.664 1.78 2.672"}],["path",{d:"m14.162 12.788-3.324 1.424"}],["path",{d:"m20 4-6.06 1.515"}],["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}],["circle",{cx:"12",cy:"6",r:"2"}],["circle",{cx:"16",cy:"12",r:"2"}],["circle",{cx:"9",cy:"15",r:"2"}]],Z=[["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}],["path",{d:"m19 9-5 5-4-4-3 3"}]],A3=[["path",{d:"M5 21V3"}],["path",{d:"M12 21V9"}],["path",{d:"M19 21v-6"}]],G=[["path",{d:"M5 21v-6"}],["path",{d:"M12 21V9"}],["path",{d:"M19 21V3"}]],I=[["path",{d:"M5 21v-6"}],["path",{d:"M12 21V3"}],["path",{d:"M19 21V9"}]],w3=[["path",{d:"M12 16v5"}],["path",{d:"M16 14v7"}],["path",{d:"M20 10v11"}],["path",{d:"m22 3-8.646 8.646a.5.5 0 0 1-.708 0L9.354 8.354a.5.5 0 0 0-.707 0L2 15"}],["path",{d:"M4 18v3"}],["path",{d:"M8 14v7"}]],W=[["path",{d:"M6 5h12"}],["path",{d:"M4 12h10"}],["path",{d:"M12 19h8"}]],E=[["circle",{cx:"7.5",cy:"7.5",r:".5",fill:"currentColor"}],["circle",{cx:"18.5",cy:"5.5",r:".5",fill:"currentColor"}],["circle",{cx:"11.5",cy:"11.5",r:".5",fill:"currentColor"}],["circle",{cx:"7.5",cy:"16.5",r:".5",fill:"currentColor"}],["circle",{cx:"17.5",cy:"14.5",r:".5",fill:"currentColor"}],["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}]],X=[["path",{d:"M21 12c.552 0 1.005-.449.95-.998a10 10 0 0 0-8.953-8.951c-.55-.055-.998.398-.998.95v8a1 1 0 0 0 1 1z"}],["path",{d:"M21.21 15.89A10 10 0 1 1 8 2.83"}]],V3=[["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}],["path",{d:"M7 16c.5-2 1.5-7 4-7 2 0 2 3 4 3 2.5 0 4.5-5 5-7"}]],S3=[["path",{d:"M18 6 7 17l-5-5"}],["path",{d:"m22 10-7.5 7.5L13 16"}]],L3=[["path",{d:"M20 4L9 15"}],["path",{d:"M21 19L3 19"}],["path",{d:"M9 15L4 10"}]],f3=[["path",{d:"M20 6 9 17l-5-5"}]],k3=[["path",{d:"M17 21a1 1 0 0 0 1-1v-5.35c0-.457.316-.844.727-1.041a4 4 0 0 0-2.134-7.589 5 5 0 0 0-9.186 0 4 4 0 0 0-2.134 7.588c.411.198.727.585.727 1.041V20a1 1 0 0 0 1 1Z"}],["path",{d:"M6 17h12"}]],P3=[["path",{d:"m6 9 6 6 6-6"}]],B3=[["path",{d:"M2 17a5 5 0 0 0 10 0c0-2.76-2.5-5-5-3-2.5-2-5 .24-5 3Z"}],["path",{d:"M12 17a5 5 0 0 0 10 0c0-2.76-2.5-5-5-3-2.5-2-5 .24-5 3Z"}],["path",{d:"M7 14c3.22-2.91 4.29-8.75 5-12 1.66 2.38 4.94 9 5 12"}],["path",{d:"M22 9c-4.29 0-7.14-2.33-10-7 5.71 0 10 4.67 10 7Z"}]],D3=[["path",{d:"m17 18-6-6 6-6"}],["path",{d:"M7 6v12"}]],F3=[["path",{d:"m7 18 6-6-6-6"}],["path",{d:"M17 6v12"}]],z3=[["path",{d:"m15 18-6-6 6-6"}]],b3=[["path",{d:"m9 18 6-6-6-6"}]],R3=[["path",{d:"m18 15-6-6-6 6"}]],T3=[["path",{d:"m7 20 5-5 5 5"}],["path",{d:"m7 4 5 5 5-5"}]],q3=[["path",{d:"m7 6 5 5 5-5"}],["path",{d:"m7 13 5 5 5-5"}]],U3=[["path",{d:"M12 12h.01"}],["path",{d:"M16 12h.01"}],["path",{d:"m17 7 5 5-5 5"}],["path",{d:"m7 7-5 5 5 5"}],["path",{d:"M8 12h.01"}]],O3=[["path",{d:"m9 7-5 5 5 5"}],["path",{d:"m15 7 5 5-5 5"}]],Z3=[["path",{d:"m11 17-5-5 5-5"}],["path",{d:"m18 17-5-5 5-5"}]],G3=[["path",{d:"m20 17-5-5 5-5"}],["path",{d:"m4 17 5-5-5-5"}]],I3=[["path",{d:"m6 17 5-5-5-5"}],["path",{d:"m13 17 5-5-5-5"}]],W3=[["path",{d:"m7 15 5 5 5-5"}],["path",{d:"m7 9 5-5 5 5"}]],E3=[["path",{d:"m17 11-5-5-5 5"}],["path",{d:"m17 18-5-5-5 5"}]],j=[["path",{d:"M10.88 21.94 15.46 14"}],["path",{d:"M21.17 8H12"}],["path",{d:"M3.95 6.06 8.54 14"}],["circle",{cx:"12",cy:"12",r:"10"}],["circle",{cx:"12",cy:"12",r:"4"}]],X3=[["path",{d:"M10 9h4"}],["path",{d:"M12 7v5"}],["path",{d:"M14 21v-3a2 2 0 0 0-4 0v3"}],["path",{d:"m18 9 3.52 2.147a1 1 0 0 1 .48.854V19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-6.999a1 1 0 0 1 .48-.854L6 9"}],["path",{d:"M6 21V7a1 1 0 0 1 .376-.782l5-3.999a1 1 0 0 1 1.249.001l5 4A1 1 0 0 1 18 7v14"}]],j3=[["path",{d:"M12 12H3a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h13"}],["path",{d:"M18 8c0-2.5-2-2.5-2-5"}],["path",{d:"m2 2 20 20"}],["path",{d:"M21 12a1 1 0 0 1 1 1v2a1 1 0 0 1-.5.866"}],["path",{d:"M22 8c0-2.5-2-2.5-2-5"}],["path",{d:"M7 12v4"}]],N3=[["path",{d:"M17 12H3a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h14"}],["path",{d:"M18 8c0-2.5-2-2.5-2-5"}],["path",{d:"M21 16a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1"}],["path",{d:"M22 8c0-2.5-2-2.5-2-5"}],["path",{d:"M7 12v4"}]],N=[["circle",{cx:"12",cy:"12",r:"10"}],["line",{x1:"12",x2:"12",y1:"8",y2:"12"}],["line",{x1:"12",x2:"12.01",y1:"16",y2:"16"}]],K=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M12 8v8"}],["path",{d:"m8 12 4 4 4-4"}]],Q=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"m12 8-4 4 4 4"}],["path",{d:"M16 12H8"}]],J=[["path",{d:"M2 12a10 10 0 1 1 10 10"}],["path",{d:"m2 22 10-10"}],["path",{d:"M8 22H2v-6"}]],Y=[["path",{d:"M2 8V2h6"}],["path",{d:"m2 2 10 10"}],["path",{d:"M12 2A10 10 0 1 1 2 12"}]],_=[["path",{d:"M12 22a10 10 0 1 1 10-10"}],["path",{d:"M22 22 12 12"}],["path",{d:"M22 16v6h-6"}]],x=[["path",{d:"M22 12A10 10 0 1 1 12 2"}],["path",{d:"M22 2 12 12"}],["path",{d:"M16 2h6v6"}]],a1=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"m12 16 4-4-4-4"}],["path",{d:"M8 12h8"}]],t1=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"m16 12-4-4-4 4"}],["path",{d:"M12 16V8"}]],h1=[["path",{d:"M21.801 10A10 10 0 1 1 17 3.335"}],["path",{d:"m9 11 3 3L22 4"}]],d1=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"m9 12 2 2 4-4"}]],c1=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"m16 10-4 4-4-4"}]],M1=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"m14 16-4-4 4-4"}]],p1=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"m10 8 4 4-4 4"}]],i1=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"m8 14 4-4 4 4"}]],K3=[["path",{d:"M10.1 2.182a10 10 0 0 1 3.8 0"}],["path",{d:"M13.9 21.818a10 10 0 0 1-3.8 0"}],["path",{d:"M17.609 3.721a10 10 0 0 1 2.69 2.7"}],["path",{d:"M2.182 13.9a10 10 0 0 1 0-3.8"}],["path",{d:"M20.279 17.609a10 10 0 0 1-2.7 2.69"}],["path",{d:"M21.818 10.1a10 10 0 0 1 0 3.8"}],["path",{d:"M3.721 6.391a10 10 0 0 1 2.7-2.69"}],["path",{d:"M6.391 20.279a10 10 0 0 1-2.69-2.7"}]],n1=[["line",{x1:"8",x2:"16",y1:"12",y2:"12"}],["line",{x1:"12",x2:"12",y1:"16",y2:"16"}],["line",{x1:"12",x2:"12",y1:"8",y2:"8"}],["circle",{cx:"12",cy:"12",r:"10"}]],Q3=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8"}],["path",{d:"M12 18V6"}]],J3=[["path",{d:"M10.1 2.18a9.93 9.93 0 0 1 3.8 0"}],["path",{d:"M17.6 3.71a9.95 9.95 0 0 1 2.69 2.7"}],["path",{d:"M21.82 10.1a9.93 9.93 0 0 1 0 3.8"}],["path",{d:"M20.29 17.6a9.95 9.95 0 0 1-2.7 2.69"}],["path",{d:"M13.9 21.82a9.94 9.94 0 0 1-3.8 0"}],["path",{d:"M6.4 20.29a9.95 9.95 0 0 1-2.69-2.7"}],["path",{d:"M2.18 13.9a9.93 9.93 0 0 1 0-3.8"}],["path",{d:"M3.71 6.4a9.95 9.95 0 0 1 2.7-2.69"}],["circle",{cx:"12",cy:"12",r:"1"}]],Y3=[["circle",{cx:"12",cy:"12",r:"10"}],["circle",{cx:"12",cy:"12",r:"1"}]],_3=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M17 12h.01"}],["path",{d:"M12 12h.01"}],["path",{d:"M7 12h.01"}]],x3=[["path",{d:"M7 10h10"}],["path",{d:"M7 14h10"}],["circle",{cx:"12",cy:"12",r:"10"}]],ad=[["path",{d:"M12 2a10 10 0 0 1 7.38 16.75"}],["path",{d:"m16 12-4-4-4 4"}],["path",{d:"M12 16V8"}],["path",{d:"M2.5 8.875a10 10 0 0 0-.5 3"}],["path",{d:"M2.83 16a10 10 0 0 0 2.43 3.4"}],["path",{d:"M4.636 5.235a10 10 0 0 1 .891-.857"}],["path",{d:"M8.644 21.42a10 10 0 0 0 7.631-.38"}]],l1=[["path",{d:"M15.6 2.7a10 10 0 1 0 5.7 5.7"}],["circle",{cx:"12",cy:"12",r:"2"}],["path",{d:"M13.4 10.6 19 5"}]],td=[["path",{d:"M12 2a10 10 0 0 1 7.38 16.75"}],["path",{d:"M12 8v8"}],["path",{d:"M16 12H8"}],["path",{d:"M2.5 8.875a10 10 0 0 0-.5 3"}],["path",{d:"M2.83 16a10 10 0 0 0 2.43 3.4"}],["path",{d:"M4.636 5.235a10 10 0 0 1 .891-.857"}],["path",{d:"M8.644 21.42a10 10 0 0 0 7.631-.38"}]],e1=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M8 12h8"}]],hd=[["path",{d:"m2 2 20 20"}],["path",{d:"M8.35 2.69A10 10 0 0 1 21.3 15.65"}],["path",{d:"M19.08 19.08A10 10 0 1 1 4.92 4.92"}]],r1=[["path",{d:"M12.656 7H13a3 3 0 0 1 2.984 3.307"}],["path",{d:"M13 13H9"}],["path",{d:"M19.071 19.071A1 1 0 0 1 4.93 4.93"}],["path",{d:"m2 2 20 20"}],["path",{d:"M8.357 2.687a10 10 0 0 1 12.956 12.956"}],["path",{d:"M9 17V9"}]],o1=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M9 17V7h4a3 3 0 0 1 0 6H9"}]],v1=[["circle",{cx:"12",cy:"12",r:"10"}],["line",{x1:"10",x2:"10",y1:"15",y2:"9"}],["line",{x1:"14",x2:"14",y1:"15",y2:"9"}]],$1=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"m15 9-6 6"}],["path",{d:"M9 9h.01"}],["path",{d:"M15 15h.01"}]],m1=[["path",{d:"M9 9.003a1 1 0 0 1 1.517-.859l4.997 2.997a1 1 0 0 1 0 1.718l-4.997 2.997A1 1 0 0 1 9 14.996z"}],["circle",{cx:"12",cy:"12",r:"10"}]],y1=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M8 12h8"}],["path",{d:"M12 8v8"}]],dd=[["path",{d:"M10 16V9.5a1 1 0 0 1 5 0"}],["path",{d:"M8 12h4"}],["path",{d:"M8 16h7"}],["circle",{cx:"12",cy:"12",r:"10"}]],s1=[["path",{d:"M12 7v4"}],["path",{d:"M7.998 9.003a5 5 0 1 0 8-.005"}],["circle",{cx:"12",cy:"12",r:"10"}]],l=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"}],["path",{d:"M12 17h.01"}]],g1=[["path",{d:"M22 2 2 22"}],["circle",{cx:"12",cy:"12",r:"10"}]],cd=[["circle",{cx:"12",cy:"12",r:"10"}],["line",{x1:"9",x2:"15",y1:"15",y2:"9"}]],Md=[["circle",{cx:"12",cy:"12",r:"6"}]],pd=[["path",{d:"M11.051 7.616a1 1 0 0 1 1.909.024l.737 1.452a1 1 0 0 0 .737.535l1.634.256a1 1 0 0 1 .588 1.806l-1.172 1.168a1 1 0 0 0-.282.866l.259 1.613a1 1 0 0 1-1.541 1.134l-1.465-.75a1 1 0 0 0-.912 0l-1.465.75a1 1 0 0 1-1.539-1.133l.258-1.613a1 1 0 0 0-.282-.867l-1.156-1.152a1 1 0 0 1 .572-1.822l1.633-.256a1 1 0 0 0 .737-.535z"}],["circle",{cx:"12",cy:"12",r:"10"}]],u1=[["circle",{cx:"12",cy:"12",r:"10"}],["rect",{x:"9",y:"9",width:"6",height:"6",rx:"1"}]],C1=[["path",{d:"M18 20a6 6 0 0 0-12 0"}],["circle",{cx:"12",cy:"10",r:"4"}],["circle",{cx:"12",cy:"12",r:"10"}]],H1=[["circle",{cx:"12",cy:"12",r:"10"}],["circle",{cx:"12",cy:"10",r:"3"}],["path",{d:"M7 20.662V19a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v1.662"}]],A1=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"m15 9-6 6"}],["path",{d:"m9 9 6 6"}]],id=[["circle",{cx:"12",cy:"12",r:"10"}]],nd=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M11 9h4a2 2 0 0 0 2-2V3"}],["circle",{cx:"9",cy:"9",r:"2"}],["path",{d:"M7 21v-4a2 2 0 0 1 2-2h4"}],["circle",{cx:"15",cy:"15",r:"2"}]],ld=[["path",{d:"M21.66 17.67a1.08 1.08 0 0 1-.04 1.6A12 12 0 0 1 4.73 2.38a1.1 1.1 0 0 1 1.61-.04z"}],["path",{d:"M19.65 15.66A8 8 0 0 1 8.35 4.34"}],["path",{d:"m14 10-5.5 5.5"}],["path",{d:"M14 17.85V10H6.15"}]],ed=[["path",{d:"M20.2 6 3 11l-.9-2.4c-.3-1.1.3-2.2 1.3-2.5l13.5-4c1.1-.3 2.2.3 2.5 1.3Z"}],["path",{d:"m6.2 5.3 3.1 3.9"}],["path",{d:"m12.4 3.4 3.1 4"}],["path",{d:"M3 11h18v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"}]],rd=[["rect",{width:"8",height:"4",x:"8",y:"2",rx:"1",ry:"1"}],["path",{d:"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"}],["path",{d:"m9 14 2 2 4-4"}]],od=[["path",{d:"M16 14v2.2l1.6 1"}],["path",{d:"M16 4h2a2 2 0 0 1 2 2v.832"}],["path",{d:"M8 4H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h2"}],["circle",{cx:"16",cy:"16",r:"6"}],["rect",{x:"8",y:"2",width:"8",height:"4",rx:"1"}]],vd=[["rect",{width:"8",height:"4",x:"8",y:"2",rx:"1",ry:"1"}],["path",{d:"M8 4H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"}],["path",{d:"M16 4h2a2 2 0 0 1 2 2v4"}],["path",{d:"M21 14H11"}],["path",{d:"m15 10-4 4 4 4"}]],$d=[["rect",{width:"8",height:"4",x:"8",y:"2",rx:"1",ry:"1"}],["path",{d:"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"}],["path",{d:"M12 11h4"}],["path",{d:"M12 16h4"}],["path",{d:"M8 11h.01"}],["path",{d:"M8 16h.01"}]],md=[["rect",{width:"8",height:"4",x:"8",y:"2",rx:"1",ry:"1"}],["path",{d:"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"}],["path",{d:"M9 14h6"}]],yd=[["path",{d:"M11 14h10"}],["path",{d:"M16 4h2a2 2 0 0 1 2 2v1.344"}],["path",{d:"m17 18 4-4-4-4"}],["path",{d:"M8 4H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 1.793-1.113"}],["rect",{x:"8",y:"2",width:"8",height:"4",rx:"1"}]],w1=[["rect",{width:"8",height:"4",x:"8",y:"2",rx:"1"}],["path",{d:"M8 4H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-.5"}],["path",{d:"M16 4h2a2 2 0 0 1 1.73 1"}],["path",{d:"M8 18h1"}],["path",{d:"M21.378 12.626a1 1 0 0 0-3.004-3.004l-4.01 4.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"}]],V1=[["rect",{width:"8",height:"4",x:"8",y:"2",rx:"1"}],["path",{d:"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-5.5"}],["path",{d:"M4 13.5V6a2 2 0 0 1 2-2h2"}],["path",{d:"M13.378 15.626a1 1 0 1 0-3.004-3.004l-5.01 5.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"}]],sd=[["rect",{width:"8",height:"4",x:"8",y:"2",rx:"1",ry:"1"}],["path",{d:"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"}],["path",{d:"M9 14h6"}],["path",{d:"M12 17v-6"}]],gd=[["rect",{width:"8",height:"4",x:"8",y:"2",rx:"1",ry:"1"}],["path",{d:"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"}],["path",{d:"M9 12v-1h6v1"}],["path",{d:"M11 17h2"}],["path",{d:"M12 11v6"}]],ud=[["rect",{width:"8",height:"4",x:"8",y:"2",rx:"1",ry:"1"}],["path",{d:"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"}],["path",{d:"m15 11-6 6"}],["path",{d:"m9 11 6 6"}]],Cd=[["rect",{width:"8",height:"4",x:"8",y:"2",rx:"1",ry:"1"}],["path",{d:"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"}]],Hd=[["path",{d:"M12 6v6l2-4"}],["circle",{cx:"12",cy:"12",r:"10"}]],Ad=[["path",{d:"M12 6v6l-4-2"}],["circle",{cx:"12",cy:"12",r:"10"}]],wd=[["path",{d:"M12 6v6l-2-4"}],["circle",{cx:"12",cy:"12",r:"10"}]],Vd=[["path",{d:"M12 6v6"}],["circle",{cx:"12",cy:"12",r:"10"}]],Sd=[["path",{d:"M12 6v6l4-2"}],["circle",{cx:"12",cy:"12",r:"10"}]],Ld=[["path",{d:"M12 6v6h4"}],["circle",{cx:"12",cy:"12",r:"10"}]],fd=[["path",{d:"M12 6v6l4 2"}],["circle",{cx:"12",cy:"12",r:"10"}]],kd=[["path",{d:"M12 6v6l2 4"}],["circle",{cx:"12",cy:"12",r:"10"}]],Pd=[["path",{d:"M12 6v10"}],["circle",{cx:"12",cy:"12",r:"10"}]],Bd=[["path",{d:"M12 6v6l-2 4"}],["circle",{cx:"12",cy:"12",r:"10"}]],Dd=[["path",{d:"M12 6v6l-4 2"}],["circle",{cx:"12",cy:"12",r:"10"}]],Fd=[["path",{d:"M12 6v6H8"}],["circle",{cx:"12",cy:"12",r:"10"}]],zd=[["path",{d:"M12 6v6l4 2"}],["path",{d:"M20 12v5"}],["path",{d:"M20 21h.01"}],["path",{d:"M21.25 8.2A10 10 0 1 0 16 21.16"}]],bd=[["path",{d:"M12 6v6l2 1"}],["path",{d:"M12.337 21.994a10 10 0 1 1 9.588-8.767"}],["path",{d:"m14 18 4 4 4-4"}],["path",{d:"M18 14v8"}]],Rd=[["path",{d:"M12 6v6l1.56.78"}],["path",{d:"M13.227 21.925a10 10 0 1 1 8.767-9.588"}],["path",{d:"m14 18 4-4 4 4"}],["path",{d:"M18 22v-8"}]],Td=[["path",{d:"M12 2a10 10 0 0 1 7.38 16.75"}],["path",{d:"M12 6v6l4 2"}],["path",{d:"M2.5 8.875a10 10 0 0 0-.5 3"}],["path",{d:"M2.83 16a10 10 0 0 0 2.43 3.4"}],["path",{d:"M4.636 5.235a10 10 0 0 1 .891-.857"}],["path",{d:"M8.644 21.42a10 10 0 0 0 7.631-.38"}]],qd=[["path",{d:"M12 6v6l3.644 1.822"}],["path",{d:"M16 19h6"}],["path",{d:"M19 16v6"}],["path",{d:"M21.92 13.267a10 10 0 1 0-8.653 8.653"}]],Ud=[["path",{d:"M12 6v6l4 2"}],["circle",{cx:"12",cy:"12",r:"10"}]],Od=[["path",{d:"M10 9.17a3 3 0 1 0 0 5.66"}],["path",{d:"M17 9.17a3 3 0 1 0 0 5.66"}],["rect",{x:"2",y:"5",width:"20",height:"14",rx:"2"}]],Zd=[["path",{d:"M12 12v4"}],["path",{d:"M12 20h.01"}],["path",{d:"M17 18h.5a1 1 0 0 0 0-9h-1.79A7 7 0 1 0 7 17.708"}]],Gd=[["path",{d:"m17 15-5.5 5.5L9 18"}],["path",{d:"M5 17.743A7 7 0 1 1 15.71 10h1.79a4.5 4.5 0 0 1 1.5 8.742"}]],Id=[["path",{d:"m10.852 19.772-.383.924"}],["path",{d:"m13.148 14.228.383-.923"}],["path",{d:"M13.148 19.772a3 3 0 1 0-2.296-5.544l-.383-.923"}],["path",{d:"m13.53 20.696-.382-.924a3 3 0 1 1-2.296-5.544"}],["path",{d:"m14.772 15.852.923-.383"}],["path",{d:"m14.772 18.148.923.383"}],["path",{d:"M4.2 15.1a7 7 0 1 1 9.93-9.858A7 7 0 0 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.2"}],["path",{d:"m9.228 15.852-.923-.383"}],["path",{d:"m9.228 18.148-.923.383"}]],S1=[["path",{d:"M12 13v8l-4-4"}],["path",{d:"m12 21 4-4"}],["path",{d:"M4.393 15.269A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.436 8.284"}]],Wd=[["path",{d:"M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242"}],["path",{d:"M8 19v1"}],["path",{d:"M8 14v1"}],["path",{d:"M16 19v1"}],["path",{d:"M16 14v1"}],["path",{d:"M12 21v1"}],["path",{d:"M12 16v1"}]],Ed=[["path",{d:"M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242"}],["path",{d:"M16 17H7"}],["path",{d:"M17 21H9"}]],Xd=[["path",{d:"M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242"}],["path",{d:"M16 14v2"}],["path",{d:"M8 14v2"}],["path",{d:"M16 20h.01"}],["path",{d:"M8 20h.01"}],["path",{d:"M12 16v2"}],["path",{d:"M12 22h.01"}]],jd=[["path",{d:"M11 20v2"}],["path",{d:"M18.376 14.512a6 6 0 0 0 3.461-4.127c.148-.625-.659-.97-1.248-.714a4 4 0 0 1-5.259-5.26c.255-.589-.09-1.395-.716-1.248a6 6 0 0 0-4.594 5.36"}],["path",{d:"M3 20a5 5 0 1 1 8.9-4H13a3 3 0 0 1 2 5.24"}],["path",{d:"M7 19v2"}]],Nd=[["path",{d:"M6 16.326A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 .5 8.973"}],["path",{d:"m13 12-3 5h4l-3 5"}]],Kd=[["path",{d:"M13 16a3 3 0 0 1 0 6H7a5 5 0 1 1 4.9-6z"}],["path",{d:"M18.376 14.512a6 6 0 0 0 3.461-4.127c.148-.625-.659-.97-1.248-.714a4 4 0 0 1-5.259-5.26c.255-.589-.09-1.395-.716-1.248a6 6 0 0 0-4.594 5.36"}]],Qd=[["path",{d:"m2 2 20 20"}],["path",{d:"M5.782 5.782A7 7 0 0 0 9 19h8.5a4.5 4.5 0 0 0 1.307-.193"}],["path",{d:"M21.532 16.5A4.5 4.5 0 0 0 17.5 10h-1.79A7.008 7.008 0 0 0 10 5.07"}]],Jd=[["path",{d:"M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242"}],["path",{d:"m9.2 22 3-7"}],["path",{d:"m9 13-3 7"}],["path",{d:"m17 13-3 7"}]],Yd=[["path",{d:"M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242"}],["path",{d:"M16 14v6"}],["path",{d:"M8 14v6"}],["path",{d:"M12 16v6"}]],_d=[["path",{d:"M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242"}],["path",{d:"M8 15h.01"}],["path",{d:"M8 19h.01"}],["path",{d:"M12 17h.01"}],["path",{d:"M12 21h.01"}],["path",{d:"M16 15h.01"}],["path",{d:"M16 19h.01"}]],xd=[["path",{d:"M12 2v2"}],["path",{d:"m4.93 4.93 1.41 1.41"}],["path",{d:"M20 12h2"}],["path",{d:"m19.07 4.93-1.41 1.41"}],["path",{d:"M15.947 12.65a4 4 0 0 0-5.925-4.128"}],["path",{d:"M3 20a5 5 0 1 1 8.9-4H13a3 3 0 0 1 2 5.24"}],["path",{d:"M11 20v2"}],["path",{d:"M7 19v2"}]],a6=[["path",{d:"M12 2v2"}],["path",{d:"m4.93 4.93 1.41 1.41"}],["path",{d:"M20 12h2"}],["path",{d:"m19.07 4.93-1.41 1.41"}],["path",{d:"M15.947 12.65a4 4 0 0 0-5.925-4.128"}],["path",{d:"M13 22H7a5 5 0 1 1 4.9-6H13a3 3 0 0 1 0 6Z"}]],L1=[["path",{d:"M12 13v8"}],["path",{d:"M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242"}],["path",{d:"m8 17 4-4 4 4"}]],t6=[["path",{d:"M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z"}]],h6=[["path",{d:"M17.5 21H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z"}],["path",{d:"M22 10a3 3 0 0 0-3-3h-2.207a5.502 5.502 0 0 0-10.702.5"}]],d6=[["path",{d:"M16.17 7.83 2 22"}],["path",{d:"M4.02 12a2.827 2.827 0 1 1 3.81-4.17A2.827 2.827 0 1 1 12 4.02a2.827 2.827 0 1 1 4.17 3.81A2.827 2.827 0 1 1 19.98 12a2.827 2.827 0 1 1-3.81 4.17A2.827 2.827 0 1 1 12 19.98a2.827 2.827 0 1 1-4.17-3.81A1 1 0 1 1 4 12"}],["path",{d:"m7.83 7.83 8.34 8.34"}]],c6=[["path",{d:"M17.28 9.05a5.5 5.5 0 1 0-10.56 0A5.5 5.5 0 1 0 12 17.66a5.5 5.5 0 1 0 5.28-8.6Z"}],["path",{d:"M12 17.66L12 22"}]],f1=[["path",{d:"m18 16 4-4-4-4"}],["path",{d:"m6 8-4 4 4 4"}],["path",{d:"m14.5 4-5 16"}]],M6=[["polygon",{points:"12 2 22 8.5 22 15.5 12 22 2 15.5 2 8.5 12 2"}],["line",{x1:"12",x2:"12",y1:"22",y2:"15.5"}],["polyline",{points:"22 8.5 12 15.5 2 8.5"}],["polyline",{points:"2 15.5 12 8.5 22 15.5"}],["line",{x1:"12",x2:"12",y1:"2",y2:"8.5"}]],p6=[["path",{d:"m16 18 6-6-6-6"}],["path",{d:"m8 6-6 6 6 6"}]],i6=[["path",{d:"M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"}],["polyline",{points:"7.5 4.21 12 6.81 16.5 4.21"}],["polyline",{points:"7.5 19.79 7.5 14.6 3 12"}],["polyline",{points:"21 12 16.5 14.6 16.5 19.79"}],["polyline",{points:"3.27 6.96 12 12.01 20.73 6.96"}],["line",{x1:"12",x2:"12",y1:"22.08",y2:"12"}]],n6=[["path",{d:"M10 2v2"}],["path",{d:"M14 2v2"}],["path",{d:"M16 8a1 1 0 0 1 1 1v8a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4V9a1 1 0 0 1 1-1h14a4 4 0 1 1 0 8h-1"}],["path",{d:"M6 2v2"}]],l6=[["path",{d:"M11 10.27 7 3.34"}],["path",{d:"m11 13.73-4 6.93"}],["path",{d:"M12 22v-2"}],["path",{d:"M12 2v2"}],["path",{d:"M14 12h8"}],["path",{d:"m17 20.66-1-1.73"}],["path",{d:"m17 3.34-1 1.73"}],["path",{d:"M2 12h2"}],["path",{d:"m20.66 17-1.73-1"}],["path",{d:"m20.66 7-1.73 1"}],["path",{d:"m3.34 17 1.73-1"}],["path",{d:"m3.34 7 1.73 1"}],["circle",{cx:"12",cy:"12",r:"2"}],["circle",{cx:"12",cy:"12",r:"8"}]],e6=[["circle",{cx:"8",cy:"8",r:"6"}],["path",{d:"M18.09 10.37A6 6 0 1 1 10.34 18"}],["path",{d:"M7 6h1v4"}],["path",{d:"m16.71 13.88.7.71-2.82 2.82"}]],k1=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M12 3v18"}]],e=[["path",{d:"M10.5 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v5.5"}],["path",{d:"m14.3 19.6 1-.4"}],["path",{d:"M15 3v7.5"}],["path",{d:"m15.2 16.9-.9-.3"}],["path",{d:"m16.6 21.7.3-.9"}],["path",{d:"m16.8 15.3-.4-1"}],["path",{d:"m19.1 15.2.3-.9"}],["path",{d:"m19.6 21.7-.4-1"}],["path",{d:"m20.7 16.8 1-.4"}],["path",{d:"m21.7 19.4-.9-.3"}],["path",{d:"M9 3v18"}],["circle",{cx:"18",cy:"18",r:"3"}]],r6=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M7.5 3v18"}],["path",{d:"M12 3v18"}],["path",{d:"M16.5 3v18"}]],P1=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M9 3v18"}],["path",{d:"M15 3v18"}]],o6=[["path",{d:"M14 3a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1"}],["path",{d:"M19 3a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1"}],["path",{d:"m7 15 3 3"}],["path",{d:"m7 21 3-3H5a2 2 0 0 1-2-2v-2"}],["rect",{x:"14",y:"14",width:"7",height:"7",rx:"1"}],["rect",{x:"3",y:"3",width:"7",height:"7",rx:"1"}]],v6=[["path",{d:"M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3"}]],$6=[["path",{d:"m16.24 7.76-1.804 5.411a2 2 0 0 1-1.265 1.265L7.76 16.24l1.804-5.411a2 2 0 0 1 1.265-1.265z"}],["circle",{cx:"12",cy:"12",r:"10"}]],m6=[["path",{d:"M15.536 11.293a1 1 0 0 0 0 1.414l2.376 2.377a1 1 0 0 0 1.414 0l2.377-2.377a1 1 0 0 0 0-1.414l-2.377-2.377a1 1 0 0 0-1.414 0z"}],["path",{d:"M2.297 11.293a1 1 0 0 0 0 1.414l2.377 2.377a1 1 0 0 0 1.414 0l2.377-2.377a1 1 0 0 0 0-1.414L6.088 8.916a1 1 0 0 0-1.414 0z"}],["path",{d:"M8.916 17.912a1 1 0 0 0 0 1.415l2.377 2.376a1 1 0 0 0 1.414 0l2.377-2.376a1 1 0 0 0 0-1.415l-2.377-2.376a1 1 0 0 0-1.414 0z"}],["path",{d:"M8.916 4.674a1 1 0 0 0 0 1.414l2.377 2.376a1 1 0 0 0 1.414 0l2.377-2.376a1 1 0 0 0 0-1.414l-2.377-2.377a1 1 0 0 0-1.414 0z"}]],y6=[["rect",{width:"14",height:"8",x:"5",y:"2",rx:"2"}],["rect",{width:"20",height:"8",x:"2",y:"14",rx:"2"}],["path",{d:"M6 18h2"}],["path",{d:"M12 18h6"}]],s6=[["path",{d:"M3 20a1 1 0 0 1-1-1v-1a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1Z"}],["path",{d:"M20 16a8 8 0 1 0-16 0"}],["path",{d:"M12 4v4"}],["path",{d:"M10 4h4"}]],g6=[["path",{d:"m20.9 18.55-8-15.98a1 1 0 0 0-1.8 0l-8 15.98"}],["ellipse",{cx:"12",cy:"19",rx:"9",ry:"3"}]],u6=[["rect",{x:"2",y:"6",width:"20",height:"8",rx:"1"}],["path",{d:"M17 14v7"}],["path",{d:"M7 14v7"}],["path",{d:"M17 3v3"}],["path",{d:"M7 3v3"}],["path",{d:"M10 14 2.3 6.3"}],["path",{d:"m14 6 7.7 7.7"}],["path",{d:"m8 6 8 8"}]],B1=[["path",{d:"M16 2v2"}],["path",{d:"M17.915 22a6 6 0 0 0-12 0"}],["path",{d:"M8 2v2"}],["circle",{cx:"12",cy:"12",r:"4"}],["rect",{x:"3",y:"4",width:"18",height:"18",rx:"2"}]],C6=[["path",{d:"M16 2v2"}],["path",{d:"M7 22v-2a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v2"}],["path",{d:"M8 2v2"}],["circle",{cx:"12",cy:"11",r:"3"}],["rect",{x:"3",y:"4",width:"18",height:"18",rx:"2"}]],H6=[["path",{d:"M22 7.7c0-.6-.4-1.2-.8-1.5l-6.3-3.9a1.72 1.72 0 0 0-1.7 0l-10.3 6c-.5.2-.9.8-.9 1.4v6.6c0 .5.4 1.2.8 1.5l6.3 3.9a1.72 1.72 0 0 0 1.7 0l10.3-6c.5-.3.9-1 .9-1.5Z"}],["path",{d:"M10 21.9V14L2.1 9.1"}],["path",{d:"m10 14 11.9-6.9"}],["path",{d:"M14 19.8v-8.1"}],["path",{d:"M18 17.5V9.4"}]],A6=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M12 18a6 6 0 0 0 0-12v12z"}]],w6=[["path",{d:"M12 2a10 10 0 1 0 10 10 4 4 0 0 1-5-5 4 4 0 0 1-5-5"}],["path",{d:"M8.5 8.5v.01"}],["path",{d:"M16 15.5v.01"}],["path",{d:"M12 12v.01"}],["path",{d:"M11 17v.01"}],["path",{d:"M7 14v.01"}]],V6=[["path",{d:"M2 12h20"}],["path",{d:"M20 12v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-8"}],["path",{d:"m4 8 16-4"}],["path",{d:"m8.86 6.78-.45-1.81a2 2 0 0 1 1.45-2.43l1.94-.48a2 2 0 0 1 2.43 1.46l.45 1.8"}]],S6=[["path",{d:"m12 15 2 2 4-4"}],["rect",{width:"14",height:"14",x:"8",y:"8",rx:"2",ry:"2"}],["path",{d:"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"}]],L6=[["line",{x1:"12",x2:"18",y1:"15",y2:"15"}],["rect",{width:"14",height:"14",x:"8",y:"8",rx:"2",ry:"2"}],["path",{d:"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"}]],f6=[["line",{x1:"15",x2:"15",y1:"12",y2:"18"}],["line",{x1:"12",x2:"18",y1:"15",y2:"15"}],["rect",{width:"14",height:"14",x:"8",y:"8",rx:"2",ry:"2"}],["path",{d:"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"}]],k6=[["line",{x1:"12",x2:"18",y1:"18",y2:"12"}],["rect",{width:"14",height:"14",x:"8",y:"8",rx:"2",ry:"2"}],["path",{d:"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"}]],P6=[["line",{x1:"12",x2:"18",y1:"12",y2:"18"}],["line",{x1:"12",x2:"18",y1:"18",y2:"12"}],["rect",{width:"14",height:"14",x:"8",y:"8",rx:"2",ry:"2"}],["path",{d:"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"}]],B6=[["rect",{width:"14",height:"14",x:"8",y:"8",rx:"2",ry:"2"}],["path",{d:"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"}]],D6=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M9.17 14.83a4 4 0 1 0 0-5.66"}]],F6=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M14.83 14.83a4 4 0 1 1 0-5.66"}]],z6=[["path",{d:"m15 10 5 5-5 5"}],["path",{d:"M4 4v7a4 4 0 0 0 4 4h12"}]],b6=[["path",{d:"M20 4v7a4 4 0 0 1-4 4H4"}],["path",{d:"m9 10-5 5 5 5"}]],R6=[["path",{d:"m14 15-5 5-5-5"}],["path",{d:"M20 4h-7a4 4 0 0 0-4 4v12"}]],T6=[["path",{d:"M14 9 9 4 4 9"}],["path",{d:"M20 20h-7a4 4 0 0 1-4-4V4"}]],q6=[["path",{d:"m10 15 5 5 5-5"}],["path",{d:"M4 4h7a4 4 0 0 1 4 4v12"}]],U6=[["path",{d:"m10 9 5-5 5 5"}],["path",{d:"M4 20h7a4 4 0 0 0 4-4V4"}]],O6=[["path",{d:"m15 14 5-5-5-5"}],["path",{d:"M4 20v-7a4 4 0 0 1 4-4h12"}]],Z6=[["path",{d:"M12 20v2"}],["path",{d:"M12 2v2"}],["path",{d:"M17 20v2"}],["path",{d:"M17 2v2"}],["path",{d:"M2 12h2"}],["path",{d:"M2 17h2"}],["path",{d:"M2 7h2"}],["path",{d:"M20 12h2"}],["path",{d:"M20 17h2"}],["path",{d:"M20 7h2"}],["path",{d:"M7 20v2"}],["path",{d:"M7 2v2"}],["rect",{x:"4",y:"4",width:"16",height:"16",rx:"2"}],["rect",{x:"8",y:"8",width:"8",height:"8",rx:"1"}]],G6=[["path",{d:"M20 20v-7a4 4 0 0 0-4-4H4"}],["path",{d:"M9 14 4 9l5-5"}]],I6=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M10 9.3a2.8 2.8 0 0 0-3.5 1 3.1 3.1 0 0 0 0 3.4 2.7 2.7 0 0 0 3.5 1"}],["path",{d:"M17 9.3a2.8 2.8 0 0 0-3.5 1 3.1 3.1 0 0 0 0 3.4 2.7 2.7 0 0 0 3.5 1"}]],W6=[["rect",{width:"20",height:"14",x:"2",y:"5",rx:"2"}],["line",{x1:"2",x2:"22",y1:"10",y2:"10"}]],E6=[["path",{d:"M10.2 18H4.774a1.5 1.5 0 0 1-1.352-.97 11 11 0 0 1 .132-6.487"}],["path",{d:"M18 10.2V4.774a1.5 1.5 0 0 0-.97-1.352 11 11 0 0 0-6.486.132"}],["path",{d:"M18 5a4 3 0 0 1 4 3 2 2 0 0 1-2 2 10 10 0 0 0-5.139 1.42"}],["path",{d:"M5 18a3 4 0 0 0 3 4 2 2 0 0 0 2-2 10 10 0 0 1 1.42-5.14"}],["path",{d:"M8.709 2.554a10 10 0 0 0-6.155 6.155 1.5 1.5 0 0 0 .676 1.626l9.807 5.42a2 2 0 0 0 2.718-2.718l-5.42-9.807a1.5 1.5 0 0 0-1.626-.676"}]],X6=[["path",{d:"M6 2v14a2 2 0 0 0 2 2h14"}],["path",{d:"M18 22V8a2 2 0 0 0-2-2H2"}]],j6=[["path",{d:"M4 9a2 2 0 0 0-2 2v2a2 2 0 0 0 2 2h4a1 1 0 0 1 1 1v4a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2v-4a1 1 0 0 1 1-1h4a2 2 0 0 0 2-2v-2a2 2 0 0 0-2-2h-4a1 1 0 0 1-1-1V4a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2v4a1 1 0 0 1-1 1z"}]],N6=[["circle",{cx:"12",cy:"12",r:"10"}],["line",{x1:"22",x2:"18",y1:"12",y2:"12"}],["line",{x1:"6",x2:"2",y1:"12",y2:"12"}],["line",{x1:"12",x2:"12",y1:"6",y2:"2"}],["line",{x1:"12",x2:"12",y1:"22",y2:"18"}]],K6=[["path",{d:"M11.562 3.266a.5.5 0 0 1 .876 0L15.39 8.87a1 1 0 0 0 1.516.294L21.183 5.5a.5.5 0 0 1 .798.519l-2.834 10.246a1 1 0 0 1-.956.734H5.81a1 1 0 0 1-.957-.734L2.02 6.02a.5.5 0 0 1 .798-.519l4.276 3.664a1 1 0 0 0 1.516-.294z"}],["path",{d:"M5 21h14"}]],Q6=[["path",{d:"m21.12 6.4-6.05-4.06a2 2 0 0 0-2.17-.05L2.95 8.41a2 2 0 0 0-.95 1.7v5.82a2 2 0 0 0 .88 1.66l6.05 4.07a2 2 0 0 0 2.17.05l9.95-6.12a2 2 0 0 0 .95-1.7V8.06a2 2 0 0 0-.88-1.66Z"}],["path",{d:"M10 22v-8L2.25 9.15"}],["path",{d:"m10 14 11.77-6.87"}]],J6=[["path",{d:"m6 8 1.75 12.28a2 2 0 0 0 2 1.72h4.54a2 2 0 0 0 2-1.72L18 8"}],["path",{d:"M5 8h14"}],["path",{d:"M7 15a6.47 6.47 0 0 1 5 0 6.47 6.47 0 0 0 5 0"}],["path",{d:"m12 8 1-6h2"}]],Y6=[["circle",{cx:"12",cy:"12",r:"8"}],["line",{x1:"3",x2:"6",y1:"3",y2:"6"}],["line",{x1:"21",x2:"18",y1:"3",y2:"6"}],["line",{x1:"3",x2:"6",y1:"21",y2:"18"}],["line",{x1:"21",x2:"18",y1:"21",y2:"18"}]],_6=[["ellipse",{cx:"12",cy:"5",rx:"9",ry:"3"}],["path",{d:"M3 5v14a9 3 0 0 0 18 0V5"}]],x6=[["path",{d:"M11 11.31c1.17.56 1.54 1.69 3.5 1.69 2.5 0 2.5-2 5-2 1.3 0 1.9.5 2.5 1"}],["path",{d:"M11.75 18c.35.5 1.45 1 2.75 1 2.5 0 2.5-2 5-2 1.3 0 1.9.5 2.5 1"}],["path",{d:"M2 10h4"}],["path",{d:"M2 14h4"}],["path",{d:"M2 18h4"}],["path",{d:"M2 6h4"}],["path",{d:"M7 3a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1L10 4a1 1 0 0 0-1-1z"}]],a8=[["ellipse",{cx:"12",cy:"5",rx:"9",ry:"3"}],["path",{d:"M3 12a9 3 0 0 0 5 2.69"}],["path",{d:"M21 9.3V5"}],["path",{d:"M3 5v14a9 3 0 0 0 6.47 2.88"}],["path",{d:"M12 12v4h4"}],["path",{d:"M13 20a5 5 0 0 0 9-3 4.5 4.5 0 0 0-4.5-4.5c-1.33 0-2.54.54-3.41 1.41L12 16"}]],t8=[["ellipse",{cx:"12",cy:"5",rx:"9",ry:"3"}],["path",{d:"M3 5V19A9 3 0 0 0 15 21.84"}],["path",{d:"M21 5V8"}],["path",{d:"M21 12L18 17H22L19 22"}],["path",{d:"M3 12A9 3 0 0 0 14.59 14.87"}]],h8=[["ellipse",{cx:"12",cy:"5",rx:"9",ry:"3"}],["path",{d:"M3 5V19A9 3 0 0 0 21 19V5"}],["path",{d:"M3 12A9 3 0 0 0 21 12"}]],d8=[["path",{d:"m13 21-3-3 3-3"}],["path",{d:"M20 18H10"}],["path",{d:"M3 11h.01"}],["rect",{x:"6",y:"3",width:"5",height:"8",rx:"2.5"}]],c8=[["path",{d:"M10 18h10"}],["path",{d:"m17 21 3-3-3-3"}],["path",{d:"M3 11h.01"}],["rect",{x:"15",y:"3",width:"5",height:"8",rx:"2.5"}],["rect",{x:"6",y:"3",width:"5",height:"8",rx:"2.5"}]],M8=[["path",{d:"M10 5a2 2 0 0 0-1.344.519l-6.328 5.74a1 1 0 0 0 0 1.481l6.328 5.741A2 2 0 0 0 10 19h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2z"}],["path",{d:"m12 9 6 6"}],["path",{d:"m18 9-6 6"}]],p8=[["path",{d:"M10.162 3.167A10 10 0 0 0 2 13a2 2 0 0 0 4 0v-1a2 2 0 0 1 4 0v4a2 2 0 0 0 4 0v-4a2 2 0 0 1 4 0v1a2 2 0 0 0 4-.006 10 10 0 0 0-8.161-9.826"}],["path",{d:"M20.804 14.869a9 9 0 0 1-17.608 0"}],["circle",{cx:"12",cy:"4",r:"2"}]],i8=[["circle",{cx:"19",cy:"19",r:"2"}],["circle",{cx:"5",cy:"5",r:"2"}],["path",{d:"M6.48 3.66a10 10 0 0 1 13.86 13.86"}],["path",{d:"m6.41 6.41 11.18 11.18"}],["path",{d:"M3.66 6.48a10 10 0 0 0 13.86 13.86"}]],n8=[["path",{d:"M2.7 10.3a2.41 2.41 0 0 0 0 3.41l7.59 7.59a2.41 2.41 0 0 0 3.41 0l7.59-7.59a2.41 2.41 0 0 0 0-3.41L13.7 2.71a2.41 2.41 0 0 0-3.41 0z"}],["path",{d:"M8 12h8"}]],D1=[["path",{d:"M2.7 10.3a2.41 2.41 0 0 0 0 3.41l7.59 7.59a2.41 2.41 0 0 0 3.41 0l7.59-7.59a2.41 2.41 0 0 0 0-3.41L13.7 2.71a2.41 2.41 0 0 0-3.41 0Z"}],["path",{d:"M9.2 9.2h.01"}],["path",{d:"m14.5 9.5-5 5"}],["path",{d:"M14.7 14.8h.01"}]],l8=[["path",{d:"M12 8v8"}],["path",{d:"M2.7 10.3a2.41 2.41 0 0 0 0 3.41l7.59 7.59a2.41 2.41 0 0 0 3.41 0l7.59-7.59a2.41 2.41 0 0 0 0-3.41L13.7 2.71a2.41 2.41 0 0 0-3.41 0z"}],["path",{d:"M8 12h8"}]],e8=[["path",{d:"M2.7 10.3a2.41 2.41 0 0 0 0 3.41l7.59 7.59a2.41 2.41 0 0 0 3.41 0l7.59-7.59a2.41 2.41 0 0 0 0-3.41l-7.59-7.59a2.41 2.41 0 0 0-3.41 0Z"}]],r8=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2"}],["path",{d:"M12 12h.01"}]],o8=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2"}],["path",{d:"M15 9h.01"}],["path",{d:"M9 15h.01"}]],v8=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2"}],["path",{d:"M16 8h.01"}],["path",{d:"M12 12h.01"}],["path",{d:"M8 16h.01"}]],$8=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2"}],["path",{d:"M16 8h.01"}],["path",{d:"M8 8h.01"}],["path",{d:"M8 16h.01"}],["path",{d:"M16 16h.01"}]],m8=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2"}],["path",{d:"M16 8h.01"}],["path",{d:"M8 8h.01"}],["path",{d:"M8 16h.01"}],["path",{d:"M16 16h.01"}],["path",{d:"M12 12h.01"}]],y8=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2"}],["path",{d:"M16 8h.01"}],["path",{d:"M16 12h.01"}],["path",{d:"M16 16h.01"}],["path",{d:"M8 8h.01"}],["path",{d:"M8 12h.01"}],["path",{d:"M8 16h.01"}]],s8=[["rect",{width:"12",height:"12",x:"2",y:"10",rx:"2",ry:"2"}],["path",{d:"m17.92 14 3.5-3.5a2.24 2.24 0 0 0 0-3l-5-4.92a2.24 2.24 0 0 0-3 0L10 6"}],["path",{d:"M6 18h.01"}],["path",{d:"M10 14h.01"}],["path",{d:"M15 6h.01"}],["path",{d:"M18 9h.01"}]],g8=[["path",{d:"M12 3v14"}],["path",{d:"M5 10h14"}],["path",{d:"M5 21h14"}]],u8=[["circle",{cx:"12",cy:"12",r:"10"}],["circle",{cx:"12",cy:"12",r:"4"}],["path",{d:"M12 12h.01"}]],C8=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M6 12c0-1.7.7-3.2 1.8-4.2"}],["circle",{cx:"12",cy:"12",r:"2"}],["path",{d:"M18 12c0 1.7-.7 3.2-1.8 4.2"}]],H8=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["circle",{cx:"12",cy:"12",r:"5"}],["path",{d:"M12 12h.01"}]],A8=[["circle",{cx:"12",cy:"12",r:"10"}],["circle",{cx:"12",cy:"12",r:"2"}]],w8=[["circle",{cx:"12",cy:"6",r:"1"}],["line",{x1:"5",x2:"19",y1:"12",y2:"12"}],["circle",{cx:"12",cy:"18",r:"1"}]],V8=[["path",{d:"M15 2c-1.35 1.5-2.092 3-2.5 4.5L14 8"}],["path",{d:"m17 6-2.891-2.891"}],["path",{d:"M2 15c3.333-3 6.667-3 10-3"}],["path",{d:"m2 2 20 20"}],["path",{d:"m20 9 .891.891"}],["path",{d:"M22 9c-1.5 1.35-3 2.092-4.5 2.5l-1-1"}],["path",{d:"M3.109 14.109 4 15"}],["path",{d:"m6.5 12.5 1 1"}],["path",{d:"m7 18 2.891 2.891"}],["path",{d:"M9 22c1.35-1.5 2.092-3 2.5-4.5L10 16"}]],S8=[["path",{d:"m10 16 1.5 1.5"}],["path",{d:"m14 8-1.5-1.5"}],["path",{d:"M15 2c-1.798 1.998-2.518 3.995-2.807 5.993"}],["path",{d:"m16.5 10.5 1 1"}],["path",{d:"m17 6-2.891-2.891"}],["path",{d:"M2 15c6.667-6 13.333 0 20-6"}],["path",{d:"m20 9 .891.891"}],["path",{d:"M3.109 14.109 4 15"}],["path",{d:"m6.5 12.5 1 1"}],["path",{d:"m7 18 2.891 2.891"}],["path",{d:"M9 22c1.798-1.998 2.518-3.995 2.807-5.993"}]],L8=[["path",{d:"M2 8h20"}],["rect",{width:"20",height:"16",x:"2",y:"4",rx:"2"}],["path",{d:"M6 16h12"}]],f8=[["path",{d:"M11.25 16.25h1.5L12 17z"}],["path",{d:"M16 14v.5"}],["path",{d:"M4.42 11.247A13.152 13.152 0 0 0 4 14.556C4 18.728 7.582 21 12 21s8-2.272 8-6.444a11.702 11.702 0 0 0-.493-3.309"}],["path",{d:"M8 14v.5"}],["path",{d:"M8.5 8.5c-.384 1.05-1.083 2.028-2.344 2.5-1.931.722-3.576-.297-3.656-1-.113-.994 1.177-6.53 4-7 1.923-.321 3.651.845 3.651 2.235A7.497 7.497 0 0 1 14 5.277c0-1.39 1.844-2.598 3.767-2.277 2.823.47 4.113 6.006 4 7-.08.703-1.725 1.722-3.656 1-1.261-.472-1.855-1.45-2.239-2.5"}]],k8=[["line",{x1:"12",x2:"12",y1:"2",y2:"22"}],["path",{d:"M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"}]],P8=[["path",{d:"M20.5 10a2.5 2.5 0 0 1-2.4-3H18a2.95 2.95 0 0 1-2.6-4.4 10 10 0 1 0 6.3 7.1c-.3.2-.8.3-1.2.3"}],["circle",{cx:"12",cy:"12",r:"3"}]],B8=[["path",{d:"M10 12h.01"}],["path",{d:"M18 9V6a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v14"}],["path",{d:"M2 20h8"}],["path",{d:"M20 17v-2a2 2 0 1 0-4 0v2"}],["rect",{x:"14",y:"17",width:"8",height:"5",rx:"1"}]],D8=[["path",{d:"M10 12h.01"}],["path",{d:"M18 20V6a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v14"}],["path",{d:"M2 20h20"}]],F8=[["path",{d:"M11 20H2"}],["path",{d:"M11 4.562v16.157a1 1 0 0 0 1.242.97L19 20V5.562a2 2 0 0 0-1.515-1.94l-4-1A2 2 0 0 0 11 4.561z"}],["path",{d:"M11 4H8a2 2 0 0 0-2 2v14"}],["path",{d:"M14 12h.01"}],["path",{d:"M22 20h-3"}]],z8=[["circle",{cx:"12.1",cy:"12.1",r:"1"}]],b8=[["path",{d:"M12 15V3"}],["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"}],["path",{d:"m7 10 5 5 5-5"}]],R8=[["path",{d:"m12.99 6.74 1.93 3.44"}],["path",{d:"M19.136 12a10 10 0 0 1-14.271 0"}],["path",{d:"m21 21-2.16-3.84"}],["path",{d:"m3 21 8.02-14.26"}],["circle",{cx:"12",cy:"5",r:"2"}]],T8=[["path",{d:"M10 11h.01"}],["path",{d:"M14 6h.01"}],["path",{d:"M18 6h.01"}],["path",{d:"M6.5 13.1h.01"}],["path",{d:"M22 5c0 9-4 12-6 12s-6-3-6-12c0-2 2-3 6-3s6 1 6 3"}],["path",{d:"M17.4 9.9c-.8.8-2 .8-2.8 0"}],["path",{d:"M10.1 7.1C9 7.2 7.7 7.7 6 8.6c-3.5 2-4.7 3.9-3.7 5.6 4.5 7.8 9.5 8.4 11.2 7.4.9-.5 1.9-2.1 1.9-4.7"}],["path",{d:"M9.1 16.5c.3-1.1 1.4-1.7 2.4-1.4"}]],q8=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M19.13 5.09C15.22 9.14 10 10.44 2.25 10.94"}],["path",{d:"M21.75 12.84c-6.62-1.41-12.14 1-16.38 6.32"}],["path",{d:"M8.56 2.75c4.37 6 6 9.42 8 17.72"}]],U8=[["path",{d:"M10 18a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H5a3 3 0 0 1-3-3 1 1 0 0 1 1-1z"}],["path",{d:"M13 10H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1l-.81 3.242a1 1 0 0 1-.97.758H8"}],["path",{d:"M14 4h3a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-3"}],["path",{d:"M18 6h4"}],["path",{d:"m5 10-2 8"}],["path",{d:"m7 18 2-8"}]],O8=[["path",{d:"M10 10 7 7"}],["path",{d:"m10 14-3 3"}],["path",{d:"m14 10 3-3"}],["path",{d:"m14 14 3 3"}],["path",{d:"M14.205 4.139a4 4 0 1 1 5.439 5.863"}],["path",{d:"M19.637 14a4 4 0 1 1-5.432 5.868"}],["path",{d:"M4.367 10a4 4 0 1 1 5.438-5.862"}],["path",{d:"M9.795 19.862a4 4 0 1 1-5.429-5.873"}],["rect",{x:"10",y:"8",width:"4",height:"8",rx:"1"}]],Z8=[["path",{d:"M18.715 13.186C18.29 11.858 17.384 10.607 16 9.5c-2-1.6-3.5-4-4-6.5a10.7 10.7 0 0 1-.884 2.586"}],["path",{d:"m2 2 20 20"}],["path",{d:"M8.795 8.797A11 11 0 0 1 8 9.5C6 11.1 5 13 5 15a7 7 0 0 0 13.222 3.208"}]],G8=[["path",{d:"M12 22a7 7 0 0 0 7-7c0-2-1-3.9-3-5.5s-3.5-4-4-6.5c-.5 2.5-2 4.9-4 6.5C6 11.1 5 13 5 15a7 7 0 0 0 7 7z"}]],I8=[["path",{d:"M7 16.3c2.2 0 4-1.83 4-4.05 0-1.16-.57-2.26-1.71-3.19S7.29 6.75 7 5.3c-.29 1.45-1.14 2.84-2.29 3.76S3 11.1 3 12.25c0 2.22 1.8 4.05 4 4.05z"}],["path",{d:"M12.56 6.6A10.97 10.97 0 0 0 14 3.02c.5 2.5 2 4.9 4 6.5s3 3.5 3 5.5a6.98 6.98 0 0 1-11.91 4.97"}]],W8=[["path",{d:"m2 2 8 8"}],["path",{d:"m22 2-8 8"}],["ellipse",{cx:"12",cy:"9",rx:"10",ry:"5"}],["path",{d:"M7 13.4v7.9"}],["path",{d:"M12 14v8"}],["path",{d:"M17 13.4v7.9"}],["path",{d:"M2 9v8a10 5 0 0 0 20 0V9"}]],E8=[["path",{d:"M15.4 15.63a7.875 6 135 1 1 6.23-6.23 4.5 3.43 135 0 0-6.23 6.23"}],["path",{d:"m8.29 12.71-2.6 2.6a2.5 2.5 0 1 0-1.65 4.65A2.5 2.5 0 1 0 8.7 18.3l2.59-2.59"}]],X8=[["path",{d:"M17.596 12.768a2 2 0 1 0 2.829-2.829l-1.768-1.767a2 2 0 0 0 2.828-2.829l-2.828-2.828a2 2 0 0 0-2.829 2.828l-1.767-1.768a2 2 0 1 0-2.829 2.829z"}],["path",{d:"m2.5 21.5 1.4-1.4"}],["path",{d:"m20.1 3.9 1.4-1.4"}],["path",{d:"M5.343 21.485a2 2 0 1 0 2.829-2.828l1.767 1.768a2 2 0 1 0 2.829-2.829l-6.364-6.364a2 2 0 1 0-2.829 2.829l1.768 1.767a2 2 0 0 0-2.828 2.829z"}],["path",{d:"m9.6 14.4 4.8-4.8"}]],j8=[["path",{d:"M6 8.5a6.5 6.5 0 1 1 13 0c0 6-6 6-6 10a3.5 3.5 0 1 1-7 0"}],["path",{d:"M15 8.5a2.5 2.5 0 0 0-5 0v1a2 2 0 1 1 0 4"}]],N8=[["path",{d:"M6 18.5a3.5 3.5 0 1 0 7 0c0-1.57.92-2.52 2.04-3.46"}],["path",{d:"M6 8.5c0-.75.13-1.47.36-2.14"}],["path",{d:"M8.8 3.15A6.5 6.5 0 0 1 19 8.5c0 1.63-.44 2.81-1.09 3.76"}],["path",{d:"M12.5 6A2.5 2.5 0 0 1 15 8.5M10 13a2 2 0 0 0 1.82-1.18"}],["line",{x1:"2",x2:"22",y1:"2",y2:"22"}]],K8=[["path",{d:"M7 3.34V5a3 3 0 0 0 3 3"}],["path",{d:"M11 21.95V18a2 2 0 0 0-2-2 2 2 0 0 1-2-2v-1a2 2 0 0 0-2-2H2.05"}],["path",{d:"M21.54 15H17a2 2 0 0 0-2 2v4.54"}],["path",{d:"M12 2a10 10 0 1 0 9.54 13"}],["path",{d:"M20 6V4a2 2 0 1 0-4 0v2"}],["rect",{width:"8",height:"5",x:"14",y:"6",rx:"1"}]],F1=[["path",{d:"M21.54 15H17a2 2 0 0 0-2 2v4.54"}],["path",{d:"M7 3.34V5a3 3 0 0 0 3 3a2 2 0 0 1 2 2c0 1.1.9 2 2 2a2 2 0 0 0 2-2c0-1.1.9-2 2-2h3.17"}],["path",{d:"M11 21.95V18a2 2 0 0 0-2-2a2 2 0 0 1-2-2v-1a2 2 0 0 0-2-2H2.05"}],["circle",{cx:"12",cy:"12",r:"10"}]],Q8=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M12 2a7 7 0 1 0 10 10"}]],J8=[["circle",{cx:"11.5",cy:"12.5",r:"3.5"}],["path",{d:"M3 8c0-3.5 2.5-6 6.5-6 5 0 4.83 3 7.5 5s5 2 5 6c0 4.5-2.5 6.5-7 6.5-2.5 0-2.5 2.5-6 2.5s-7-2-7-5.5c0-3 1.5-3 1.5-5C3.5 10 3 9 3 8Z"}]],Y8=[["path",{d:"M12 2C8 2 4 8 4 14a8 8 0 0 0 16 0c0-6-4-12-8-12"}]],_8=[["path",{d:"m2 2 20 20"}],["path",{d:"M20 14.347V14c0-6-4-12-8-12-1.078 0-2.157.436-3.157 1.19"}],["path",{d:"M6.206 6.21C4.871 8.4 4 11.2 4 14a8 8 0 0 0 14.568 4.568"}]],z1=[["circle",{cx:"12",cy:"12",r:"1"}],["circle",{cx:"12",cy:"5",r:"1"}],["circle",{cx:"12",cy:"19",r:"1"}]],b1=[["circle",{cx:"12",cy:"12",r:"1"}],["circle",{cx:"19",cy:"12",r:"1"}],["circle",{cx:"5",cy:"12",r:"1"}]],x8=[["path",{d:"M5 15a6.5 6.5 0 0 1 7 0 6.5 6.5 0 0 0 7 0"}],["path",{d:"M5 9a6.5 6.5 0 0 1 7 0 6.5 6.5 0 0 0 7 0"}]],ac=[["line",{x1:"5",x2:"19",y1:"9",y2:"9"}],["line",{x1:"5",x2:"19",y1:"15",y2:"15"}],["line",{x1:"19",x2:"5",y1:"5",y2:"19"}]],tc=[["line",{x1:"5",x2:"19",y1:"9",y2:"9"}],["line",{x1:"5",x2:"19",y1:"15",y2:"15"}]],hc=[["path",{d:"m15 20 3-3h2a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h2l3 3z"}],["path",{d:"M6 8v1"}],["path",{d:"M10 8v1"}],["path",{d:"M14 8v1"}],["path",{d:"M18 8v1"}]],dc=[["path",{d:"M21 21H8a2 2 0 0 1-1.42-.587l-3.994-3.999a2 2 0 0 1 0-2.828l10-10a2 2 0 0 1 2.829 0l5.999 6a2 2 0 0 1 0 2.828L12.834 21"}],["path",{d:"m5.082 11.09 8.828 8.828"}]],cc=[["path",{d:"M4 10h12"}],["path",{d:"M4 14h9"}],["path",{d:"M19 6a7.7 7.7 0 0 0-5.2-2A7.9 7.9 0 0 0 6 12c0 4.4 3.5 8 7.8 8 2 0 3.8-.8 5.2-2"}]],Mc=[["path",{d:"M14 13h2a2 2 0 0 1 2 2v2a2 2 0 0 0 4 0v-6.998a2 2 0 0 0-.59-1.42L18 5"}],["path",{d:"M14 21V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v16"}],["path",{d:"M2 21h13"}],["path",{d:"M3 7h11"}],["path",{d:"m9 11-2 3h3l-2 3"}]],pc=[["path",{d:"m15 15 6 6"}],["path",{d:"m15 9 6-6"}],["path",{d:"M21 16v5h-5"}],["path",{d:"M21 8V3h-5"}],["path",{d:"M3 16v5h5"}],["path",{d:"m3 21 6-6"}],["path",{d:"M3 8V3h5"}],["path",{d:"M9 9 3 3"}]],ic=[["path",{d:"M15 3h6v6"}],["path",{d:"M10 14 21 3"}],["path",{d:"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"}]],nc=[["path",{d:"m15 18-.722-3.25"}],["path",{d:"M2 8a10.645 10.645 0 0 0 20 0"}],["path",{d:"m20 15-1.726-2.05"}],["path",{d:"m4 15 1.726-2.05"}],["path",{d:"m9 18 .722-3.25"}]],lc=[["path",{d:"M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49"}],["path",{d:"M14.084 14.158a3 3 0 0 1-4.242-4.242"}],["path",{d:"M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143"}],["path",{d:"m2 2 20 20"}]],ec=[["path",{d:"M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"}],["circle",{cx:"12",cy:"12",r:"3"}]],rc=[["path",{d:"M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"}]],oc=[["path",{d:"M12 16h.01"}],["path",{d:"M16 16h.01"}],["path",{d:"M3 19a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V8.5a.5.5 0 0 0-.769-.422l-4.462 2.844A.5.5 0 0 1 15 10.5v-2a.5.5 0 0 0-.769-.422L9.77 10.922A.5.5 0 0 1 9 10.5V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2z"}],["path",{d:"M8 16h.01"}]],vc=[["path",{d:"M10.827 16.379a6.082 6.082 0 0 1-8.618-7.002l5.412 1.45a6.082 6.082 0 0 1 7.002-8.618l-1.45 5.412a6.082 6.082 0 0 1 8.618 7.002l-5.412-1.45a6.082 6.082 0 0 1-7.002 8.618l1.45-5.412Z"}],["path",{d:"M12 12v.01"}]],$c=[["path",{d:"M12 6a2 2 0 0 1 3.414-1.414l6 6a2 2 0 0 1 0 2.828l-6 6A2 2 0 0 1 12 18z"}],["path",{d:"M2 6a2 2 0 0 1 3.414-1.414l6 6a2 2 0 0 1 0 2.828l-6 6A2 2 0 0 1 2 18z"}]],mc=[["path",{d:"M12.67 19a2 2 0 0 0 1.416-.588l6.154-6.172a6 6 0 0 0-8.49-8.49L5.586 9.914A2 2 0 0 0 5 11.328V18a1 1 0 0 0 1 1z"}],["path",{d:"M16 8 2 22"}],["path",{d:"M17.5 15H9"}]],yc=[["path",{d:"M4 3 2 5v15c0 .6.4 1 1 1h2c.6 0 1-.4 1-1V5Z"}],["path",{d:"M6 8h4"}],["path",{d:"M6 18h4"}],["path",{d:"m12 3-2 2v15c0 .6.4 1 1 1h2c.6 0 1-.4 1-1V5Z"}],["path",{d:"M14 8h4"}],["path",{d:"M14 18h4"}],["path",{d:"m20 3-2 2v15c0 .6.4 1 1 1h2c.6 0 1-.4 1-1V5Z"}]],sc=[["circle",{cx:"12",cy:"12",r:"2"}],["path",{d:"M12 2v4"}],["path",{d:"m6.8 15-3.5 2"}],["path",{d:"m20.7 7-3.5 2"}],["path",{d:"M6.8 9 3.3 7"}],["path",{d:"m20.7 17-3.5-2"}],["path",{d:"m9 22 3-8 3 8"}],["path",{d:"M8 22h8"}],["path",{d:"M18 18.7a9 9 0 1 0-12 0"}]],gc=[["path",{d:"M5 5.5A3.5 3.5 0 0 1 8.5 2H12v7H8.5A3.5 3.5 0 0 1 5 5.5z"}],["path",{d:"M12 2h3.5a3.5 3.5 0 1 1 0 7H12V2z"}],["path",{d:"M12 12.5a3.5 3.5 0 1 1 7 0 3.5 3.5 0 1 1-7 0z"}],["path",{d:"M5 19.5A3.5 3.5 0 0 1 8.5 16H12v3.5a3.5 3.5 0 1 1-7 0z"}],["path",{d:"M5 12.5A3.5 3.5 0 0 1 8.5 9H12v7H8.5A3.5 3.5 0 0 1 5 12.5z"}]],uc=[["path",{d:"M10 12v-1"}],["path",{d:"M10 18v-2"}],["path",{d:"M10 7V6"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["path",{d:"M15.5 22H18a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v16a2 2 0 0 0 .274 1.01"}],["circle",{cx:"10",cy:"20",r:"2"}]],Cc=[["path",{d:"M4 22h14a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v2"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["circle",{cx:"3",cy:"17",r:"1"}],["path",{d:"M2 17v-3a4 4 0 0 1 8 0v3"}],["circle",{cx:"9",cy:"17",r:"1"}]],R1=[["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["path",{d:"m8 18 4-4"}],["path",{d:"M8 10v8h8"}]],Hc=[["path",{d:"M17.5 22h.5a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v3"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["path",{d:"M2 19a2 2 0 1 1 4 0v1a2 2 0 1 1-4 0v-4a6 6 0 0 1 12 0v4a2 2 0 1 1-4 0v-1a2 2 0 1 1 4 0"}]],Ac=[["path",{d:"m13.69 12.479 1.29 4.88a.5.5 0 0 1-.697.591l-1.844-.849a1 1 0 0 0-.88.001l-1.846.85a.5.5 0 0 1-.693-.593l1.29-4.88"}],["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z"}],["circle",{cx:"12",cy:"10",r:"3"}]],wc=[["path",{d:"M12 22h6a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v3.072"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["path",{d:"m6.69 16.479 1.29 4.88a.5.5 0 0 1-.698.591l-1.843-.849a1 1 0 0 0-.88.001l-1.846.85a.5.5 0 0 1-.693-.593l1.29-4.88"}],["circle",{cx:"5",cy:"14",r:"3"}]],Vc=[["path",{d:"M14.5 22H18a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v4"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["path",{d:"M3 13.1a2 2 0 0 0-1 1.76v3.24a2 2 0 0 0 .97 1.78L6 21.7a2 2 0 0 0 2.03.01L11 19.9a2 2 0 0 0 1-1.76V14.9a2 2 0 0 0-.97-1.78L8 11.3a2 2 0 0 0-2.03-.01Z"}],["path",{d:"M7 17v5"}],["path",{d:"M11.7 14.2 7 17l-4.7-2.8"}]],T1=[["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["path",{d:"M8 18v-2"}],["path",{d:"M12 18v-4"}],["path",{d:"M16 18v-6"}]],q1=[["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["path",{d:"M8 18v-1"}],["path",{d:"M12 18v-6"}],["path",{d:"M16 18v-3"}]],U1=[["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["path",{d:"m16 13-3.5 3.5-2-2L8 17"}]],O1=[["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["path",{d:"M16 22h2a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v3.5"}],["path",{d:"M4.017 11.512a6 6 0 1 0 8.466 8.475"}],["path",{d:"M9 16a1 1 0 0 1-1-1v-4c0-.552.45-1.008.995-.917a6 6 0 0 1 4.922 4.922c.091.544-.365.995-.917.995z"}]],Sc=[["path",{d:"M4 22h14a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v4"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["path",{d:"m3 15 2 2 4-4"}]],Lc=[["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["path",{d:"m9 15 2 2 4-4"}]],fc=[["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["path",{d:"M16 22h2a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v3"}],["path",{d:"M8 14v2.2l1.6 1"}],["circle",{cx:"8",cy:"16",r:"6"}]],kc=[["path",{d:"M4 22h14a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v4"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["path",{d:"m5 12-3 3 3 3"}],["path",{d:"m9 18 3-3-3-3"}]],Pc=[["path",{d:"M10 12.5 8 15l2 2.5"}],["path",{d:"m14 12.5 2 2.5-2 2.5"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z"}]],Z1=[["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["path",{d:"m2.305 15.53.923-.382"}],["path",{d:"m3.228 12.852-.924-.383"}],["path",{d:"M4.677 21.5a2 2 0 0 0 1.313.5H18a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v2.5"}],["path",{d:"m4.852 11.228-.383-.923"}],["path",{d:"m4.852 16.772-.383.924"}],["path",{d:"m7.148 11.228.383-.923"}],["path",{d:"m7.53 17.696-.382-.924"}],["path",{d:"m8.772 12.852.923-.383"}],["path",{d:"m8.772 15.148.923.383"}],["circle",{cx:"6",cy:"14",r:"3"}]],Bc=[["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"}],["path",{d:"M9 10h6"}],["path",{d:"M12 13V7"}],["path",{d:"M9 17h6"}]],Dc=[["path",{d:"M4 22h14a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v4"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["rect",{width:"4",height:"6",x:"2",y:"12",rx:"2"}],["path",{d:"M10 12h2v6"}],["path",{d:"M10 18h4"}]],Fc=[["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["path",{d:"M12 18v-6"}],["path",{d:"m9 15 3 3 3-3"}]],zc=[["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["path",{d:"M2.62 13.8A2.25 2.25 0 1 1 6 10.836a2.25 2.25 0 1 1 3.38 2.966l-2.626 2.856a.998.998 0 0 1-1.507 0z"}],["path",{d:"M4 6.005V4a2 2 0 0 1 2-2h9l5 5v13a2 2 0 0 1-2 2H6a2 2 0 0 1-1.9-1.376"}]],bc=[["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["circle",{cx:"10",cy:"12",r:"2"}],["path",{d:"m20 17-1.296-1.296a2.41 2.41 0 0 0-3.408 0L9 22"}]],Rc=[["path",{d:"M4 22h14a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v4"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["path",{d:"M2 15h10"}],["path",{d:"m9 18 3-3-3-3"}]],Tc=[["path",{d:"M4 22h14a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v4"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["path",{d:"M4 12a1 1 0 0 0-1 1v1a1 1 0 0 1-1 1 1 1 0 0 1 1 1v1a1 1 0 0 0 1 1"}],["path",{d:"M8 18a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1 1 1 0 0 1-1-1v-1a1 1 0 0 0-1-1"}]],qc=[["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["path",{d:"M10 12a1 1 0 0 0-1 1v1a1 1 0 0 1-1 1 1 1 0 0 1 1 1v1a1 1 0 0 0 1 1"}],["path",{d:"M14 18a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1 1 1 0 0 1-1-1v-1a1 1 0 0 0-1-1"}]],Uc=[["path",{d:"M4 22h14a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v6"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["circle",{cx:"4",cy:"16",r:"2"}],["path",{d:"m10 10-4.5 4.5"}],["path",{d:"m9 11 1 1"}]],Oc=[["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"}],["circle",{cx:"10",cy:"16",r:"2"}],["path",{d:"m16 10-4.5 4.5"}],["path",{d:"m15 11 1 1"}]],Zc=[["path",{d:"M4 22h14a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v1"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["rect",{width:"8",height:"5",x:"2",y:"13",rx:"1"}],["path",{d:"M8 13v-2a2 2 0 1 0-4 0v2"}]],Gc=[["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"}],["rect",{width:"8",height:"6",x:"8",y:"12",rx:"1"}],["path",{d:"M10 12v-2a2 2 0 1 1 4 0v2"}]],Ic=[["path",{d:"M4 22h14a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v4"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["path",{d:"M3 15h6"}]],Wc=[["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["path",{d:"M9 15h6"}]],Ec=[["path",{d:"M10.5 22H18a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v8.4"}],["path",{d:"M8 18v-7.7L16 9v7"}],["circle",{cx:"14",cy:"16",r:"2"}],["circle",{cx:"6",cy:"18",r:"2"}]],Xc=[["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["path",{d:"M4 7V4a2 2 0 0 1 2-2 2 2 0 0 0-2 2"}],["path",{d:"M4.063 20.999a2 2 0 0 0 2 1L18 22a2 2 0 0 0 2-2V7l-5-5H6"}],["path",{d:"m5 11-3 3"}],["path",{d:"m5 17-3-3h10"}]],G1=[["path",{d:"m18 5-2.414-2.414A2 2 0 0 0 14.172 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2"}],["path",{d:"M21.378 12.626a1 1 0 0 0-3.004-3.004l-4.01 4.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"}],["path",{d:"M8 18h1"}]],I1=[["path",{d:"M12.5 22H18a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v9.5"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["path",{d:"M13.378 15.626a1 1 0 1 0-3.004-3.004l-5.01 5.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"}]],W1=[["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z"}],["path",{d:"M15.033 13.44a.647.647 0 0 1 0 1.12l-4.065 2.352a.645.645 0 0 1-.968-.56v-4.704a.645.645 0 0 1 .967-.56z"}]],jc=[["path",{d:"M4 22h14a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v4"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["path",{d:"M3 15h6"}],["path",{d:"M6 12v6"}]],Nc=[["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["path",{d:"M9 15h6"}],["path",{d:"M12 18v-6"}]],E1=[["path",{d:"M12 17h.01"}],["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z"}],["path",{d:"M9.1 9a3 3 0 0 1 5.82 1c0 2-3 3-3 3"}]],Kc=[["path",{d:"M20 10V7l-5-5H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h4"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["path",{d:"M16 14a2 2 0 0 0-2 2"}],["path",{d:"M20 14a2 2 0 0 1 2 2"}],["path",{d:"M20 22a2 2 0 0 0 2-2"}],["path",{d:"M16 22a2 2 0 0 1-2-2"}]],Qc=[["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["circle",{cx:"11.5",cy:"14.5",r:"2.5"}],["path",{d:"M13.3 16.3 15 18"}]],Jc=[["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["path",{d:"M4.268 21a2 2 0 0 0 1.727 1H18a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v3"}],["path",{d:"m9 18-1.5-1.5"}],["circle",{cx:"5",cy:"14",r:"3"}]],Yc=[["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["path",{d:"M8 12h8"}],["path",{d:"M10 11v2"}],["path",{d:"M8 17h8"}],["path",{d:"M14 16v2"}]],_c=[["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["path",{d:"M8 13h2"}],["path",{d:"M14 13h2"}],["path",{d:"M8 17h2"}],["path",{d:"M14 17h2"}]],xc=[["path",{d:"M11 21a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-8a1 1 0 0 1 1-1"}],["path",{d:"M16 16a1 1 0 0 1-1 1H9a1 1 0 0 1-1-1V8a1 1 0 0 1 1-1"}],["path",{d:"M21 6a2 2 0 0 0-.586-1.414l-2-2A2 2 0 0 0 17 2h-3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1z"}]],a7=[["path",{d:"m10 18 3-3-3-3"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["path",{d:"M4 11V4a2 2 0 0 1 2-2h9l5 5v13a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h7"}]],t7=[["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["path",{d:"m8 16 2-2-2-2"}],["path",{d:"M12 18h4"}]],h7=[["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["path",{d:"M10 9H8"}],["path",{d:"M16 13H8"}],["path",{d:"M16 17H8"}]],d7=[["path",{d:"M4 22h14a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v4"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["path",{d:"M2 13v-1h6v1"}],["path",{d:"M5 12v6"}],["path",{d:"M4 18h2"}]],c7=[["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["path",{d:"M9 13v-1h6v1"}],["path",{d:"M12 12v6"}],["path",{d:"M11 18h2"}]],M7=[["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["path",{d:"M12 12v6"}],["path",{d:"m15 15-3-3-3 3"}]],p7=[["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["path",{d:"M15 18a3 3 0 1 0-6 0"}],["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z"}],["circle",{cx:"12",cy:"13",r:"2"}]],X1=[["path",{d:"M4 22h14a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v4"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["rect",{width:"8",height:"6",x:"2",y:"12",rx:"1"}],["path",{d:"m10 13.843 3.033-1.755a.645.645 0 0 1 .967.56v4.704a.645.645 0 0 1-.967.56L10 16.157"}]],i7=[["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["path",{d:"M8 15h.01"}],["path",{d:"M11.5 13.5a2.5 2.5 0 0 1 0 3"}],["path",{d:"M15 12a5 5 0 0 1 0 6"}]],n7=[["path",{d:"M11 11a5 5 0 0 1 0 6"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["path",{d:"M4 6.765V4a2 2 0 0 1 2-2h9l5 5v13a2 2 0 0 1-2 2H6a2 2 0 0 1-.93-.23"}],["path",{d:"M7 10.51a.5.5 0 0 0-.826-.38l-1.893 1.628A1 1 0 0 1 3.63 12H2.5a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5h1.129a1 1 0 0 1 .652.242l1.893 1.63a.5.5 0 0 0 .826-.38z"}]],l7=[["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"}],["path",{d:"M12 9v4"}],["path",{d:"M12 17h.01"}]],e7=[["path",{d:"M4 22h14a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v4"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["path",{d:"m8 12.5-5 5"}],["path",{d:"m3 12.5 5 5"}]],r7=[["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["path",{d:"m14.5 12.5-5 5"}],["path",{d:"m9.5 12.5 5 5"}]],o7=[["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}]],v7=[["path",{d:"M15 2a2 2 0 0 1 1.414.586l4 4A2 2 0 0 1 21 8v7a2 2 0 0 1-2 2h-8a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2z"}],["path",{d:"M15 2v4a2 2 0 0 0 2 2h4"}],["path",{d:"M5 7a2 2 0 0 0-2 2v11a2 2 0 0 0 2 2h8a2 2 0 0 0 1.732-1"}]],$7=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M7 3v18"}],["path",{d:"M3 7.5h4"}],["path",{d:"M3 12h18"}],["path",{d:"M3 16.5h4"}],["path",{d:"M17 3v18"}],["path",{d:"M17 7.5h4"}],["path",{d:"M17 16.5h4"}]],m7=[["path",{d:"M15 6.5V3a1 1 0 0 0-1-1h-2a1 1 0 0 0-1 1v3.5"}],["path",{d:"M9 18h8"}],["path",{d:"M18 3h-3"}],["path",{d:"M11 3a6 6 0 0 0-6 6v11"}],["path",{d:"M5 13h4"}],["path",{d:"M17 10a4 4 0 0 0-8 0v10a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2Z"}]],y7=[["path",{d:"M12 10a2 2 0 0 0-2 2c0 1.02-.1 2.51-.26 4"}],["path",{d:"M14 13.12c0 2.38 0 6.38-1 8.88"}],["path",{d:"M17.29 21.02c.12-.6.43-2.3.5-3.02"}],["path",{d:"M2 12a10 10 0 0 1 18-6"}],["path",{d:"M2 16h.01"}],["path",{d:"M21.8 16c.2-2 .131-5.354 0-6"}],["path",{d:"M5 19.5C5.5 18 6 15 6 12a6 6 0 0 1 .34-2"}],["path",{d:"M8.65 22c.21-.66.45-1.32.57-2"}],["path",{d:"M9 6.8a6 6 0 0 1 9 5.2v2"}]],s7=[["path",{d:"M18 12.47v.03m0-.5v.47m-.475 5.056A6.744 6.744 0 0 1 15 18c-3.56 0-7.56-2.53-8.5-6 .348-1.28 1.114-2.433 2.121-3.38m3.444-2.088A8.802 8.802 0 0 1 15 6c3.56 0 6.06 2.54 7 6-.309 1.14-.786 2.177-1.413 3.058"}],["path",{d:"M7 10.67C7 8 5.58 5.97 2.73 5.5c-1 1.5-1 5 .23 6.5-1.24 1.5-1.24 5-.23 6.5C5.58 18.03 7 16 7 13.33m7.48-4.372A9.77 9.77 0 0 1 16 6.07m0 11.86a9.77 9.77 0 0 1-1.728-3.618"}],["path",{d:"m16.01 17.93-.23 1.4A2 2 0 0 1 13.8 21H9.5a5.96 5.96 0 0 0 1.49-3.98M8.53 3h5.27a2 2 0 0 1 1.98 1.67l.23 1.4M2 2l20 20"}]],g7=[["path",{d:"M2 16s9-15 20-4C11 23 2 8 2 8"}]],u7=[["path",{d:"M6.5 12c.94-3.46 4.94-6 8.5-6 3.56 0 6.06 2.54 7 6-.94 3.47-3.44 6-7 6s-7.56-2.53-8.5-6Z"}],["path",{d:"M18 12v.5"}],["path",{d:"M16 17.93a9.77 9.77 0 0 1 0-11.86"}],["path",{d:"M7 10.67C7 8 5.58 5.97 2.73 5.5c-1 1.5-1 5 .23 6.5-1.24 1.5-1.24 5-.23 6.5C5.58 18.03 7 16 7 13.33"}],["path",{d:"M10.46 7.26C10.2 5.88 9.17 4.24 8 3h5.8a2 2 0 0 1 1.98 1.67l.23 1.4"}],["path",{d:"m16.01 17.93-.23 1.4A2 2 0 0 1 13.8 21H9.5a5.96 5.96 0 0 0 1.49-3.98"}]],C7=[["path",{d:"M16 16c-3 0-5-2-8-2a6 6 0 0 0-4 1.528"}],["path",{d:"m2 2 20 20"}],["path",{d:"M4 22V4"}],["path",{d:"M7.656 2H8c3 0 5 2 7.333 2q2 0 3.067-.8A1 1 0 0 1 20 4v10.347"}]],H7=[["path",{d:"M18 22V2.8a.8.8 0 0 0-1.17-.71L5.45 7.78a.8.8 0 0 0 0 1.44L18 15.5"}]],A7=[["path",{d:"M6 22V2.8a.8.8 0 0 1 1.17-.71l11.38 5.69a.8.8 0 0 1 0 1.44L6 15.5"}]],w7=[["path",{d:"M4 22V4a1 1 0 0 1 .4-.8A6 6 0 0 1 8 2c3 0 5 2 7.333 2q2 0 3.067-.8A1 1 0 0 1 20 4v10a1 1 0 0 1-.4.8A6 6 0 0 1 16 16c-3 0-5-2-8-2a6 6 0 0 0-4 1.528"}]],V7=[["path",{d:"M12 2c1 3 2.5 3.5 3.5 4.5A5 5 0 0 1 17 10a5 5 0 1 1-10 0c0-.3 0-.6.1-.9a2 2 0 1 0 3.3-2C8 4.5 11 2 12 2Z"}],["path",{d:"m5 22 14-4"}],["path",{d:"m5 18 14 4"}]],S7=[["path",{d:"M12 3q1 4 4 6.5t3 5.5a1 1 0 0 1-14 0 5 5 0 0 1 1-3 1 1 0 0 0 5 0c0-2-1.5-3-1.5-5q0-2 2.5-4"}]],L7=[["path",{d:"M16 16v4a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2V10c0-2-2-2-2-4"}],["path",{d:"M7 2h11v4c0 2-2 2-2 4v1"}],["line",{x1:"11",x2:"18",y1:"6",y2:"6"}],["line",{x1:"2",x2:"22",y1:"2",y2:"22"}]],f7=[["path",{d:"M18 6c0 2-2 2-2 4v10a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2V10c0-2-2-2-2-4V2h12z"}],["line",{x1:"6",x2:"18",y1:"6",y2:"6"}],["line",{x1:"12",x2:"12",y1:"12",y2:"12"}]],k7=[["path",{d:"M10 2v2.343"}],["path",{d:"M14 2v6.343"}],["path",{d:"m2 2 20 20"}],["path",{d:"M20 20a2 2 0 0 1-2 2H6a2 2 0 0 1-1.755-2.96l5.227-9.563"}],["path",{d:"M6.453 15H15"}],["path",{d:"M8.5 2h7"}]],P7=[["path",{d:"M14 2v6a2 2 0 0 0 .245.96l5.51 10.08A2 2 0 0 1 18 22H6a2 2 0 0 1-1.755-2.96l5.51-10.08A2 2 0 0 0 10 8V2"}],["path",{d:"M6.453 15h11.094"}],["path",{d:"M8.5 2h7"}]],B7=[["path",{d:"M10 2v6.292a7 7 0 1 0 4 0V2"}],["path",{d:"M5 15h14"}],["path",{d:"M8.5 2h7"}]],D7=[["path",{d:"m3 7 5 5-5 5V7"}],["path",{d:"m21 7-5 5 5 5V7"}],["path",{d:"M12 20v2"}],["path",{d:"M12 14v2"}],["path",{d:"M12 8v2"}],["path",{d:"M12 2v2"}]],F7=[["path",{d:"M8 3H5a2 2 0 0 0-2 2v14c0 1.1.9 2 2 2h3"}],["path",{d:"M16 3h3a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-3"}],["path",{d:"M12 20v2"}],["path",{d:"M12 14v2"}],["path",{d:"M12 8v2"}],["path",{d:"M12 2v2"}]],z7=[["path",{d:"m17 3-5 5-5-5h10"}],["path",{d:"m17 21-5-5-5 5h10"}],["path",{d:"M4 12H2"}],["path",{d:"M10 12H8"}],["path",{d:"M16 12h-2"}],["path",{d:"M22 12h-2"}]],b7=[["path",{d:"M21 8V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v3"}],["path",{d:"M21 16v3a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-3"}],["path",{d:"M4 12H2"}],["path",{d:"M10 12H8"}],["path",{d:"M16 12h-2"}],["path",{d:"M22 12h-2"}]],R7=[["path",{d:"M12 5a3 3 0 1 1 3 3m-3-3a3 3 0 1 0-3 3m3-3v1M9 8a3 3 0 1 0 3 3M9 8h1m5 0a3 3 0 1 1-3 3m3-3h-1m-2 3v-1"}],["circle",{cx:"12",cy:"8",r:"2"}],["path",{d:"M12 10v12"}],["path",{d:"M12 22c4.2 0 7-1.667 7-5-4.2 0-7 1.667-7 5Z"}],["path",{d:"M12 22c-4.2 0-7-1.667-7-5 4.2 0 7 1.667 7 5Z"}]],T7=[["circle",{cx:"12",cy:"12",r:"3"}],["path",{d:"M12 16.5A4.5 4.5 0 1 1 7.5 12 4.5 4.5 0 1 1 12 7.5a4.5 4.5 0 1 1 4.5 4.5 4.5 4.5 0 1 1-4.5 4.5"}],["path",{d:"M12 7.5V9"}],["path",{d:"M7.5 12H9"}],["path",{d:"M16.5 12H15"}],["path",{d:"M12 16.5V15"}],["path",{d:"m8 8 1.88 1.88"}],["path",{d:"M14.12 9.88 16 8"}],["path",{d:"m8 16 1.88-1.88"}],["path",{d:"M14.12 14.12 16 16"}]],q7=[["circle",{cx:"12",cy:"12",r:"3"}],["path",{d:"M3 7V5a2 2 0 0 1 2-2h2"}],["path",{d:"M17 3h2a2 2 0 0 1 2 2v2"}],["path",{d:"M21 17v2a2 2 0 0 1-2 2h-2"}],["path",{d:"M7 21H5a2 2 0 0 1-2-2v-2"}]],U7=[["path",{d:"M2 12h6"}],["path",{d:"M22 12h-6"}],["path",{d:"M12 2v2"}],["path",{d:"M12 8v2"}],["path",{d:"M12 14v2"}],["path",{d:"M12 20v2"}],["path",{d:"m19 9-3 3 3 3"}],["path",{d:"m5 15 3-3-3-3"}]],O7=[["path",{d:"M12 22v-6"}],["path",{d:"M12 8V2"}],["path",{d:"M4 12H2"}],["path",{d:"M10 12H8"}],["path",{d:"M16 12h-2"}],["path",{d:"M22 12h-2"}],["path",{d:"m15 19-3-3-3 3"}],["path",{d:"m15 5-3 3-3-3"}]],Z7=[["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"}],["path",{d:"m9 13 2 2 4-4"}]],G7=[["circle",{cx:"15",cy:"19",r:"2"}],["path",{d:"M20.9 19.8A2 2 0 0 0 22 18V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2h5.1"}],["path",{d:"M15 11v-1"}],["path",{d:"M15 17v-2"}]],I7=[["path",{d:"M16 14v2.2l1.6 1"}],["path",{d:"M7 20H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2"}],["circle",{cx:"16",cy:"16",r:"6"}]],W7=[["path",{d:"M10 10.5 8 13l2 2.5"}],["path",{d:"m14 10.5 2 2.5-2 2.5"}],["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2z"}]],E7=[["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"}],["path",{d:"M2 10h20"}]],j1=[["path",{d:"M10.3 20H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.98a2 2 0 0 1 1.69.9l.66 1.2A2 2 0 0 0 12 6h8a2 2 0 0 1 2 2v3.3"}],["path",{d:"m14.305 19.53.923-.382"}],["path",{d:"m15.228 16.852-.923-.383"}],["path",{d:"m16.852 15.228-.383-.923"}],["path",{d:"m16.852 20.772-.383.924"}],["path",{d:"m19.148 15.228.383-.923"}],["path",{d:"m19.53 21.696-.382-.924"}],["path",{d:"m20.772 16.852.924-.383"}],["path",{d:"m20.772 19.148.924.383"}],["circle",{cx:"18",cy:"18",r:"3"}]],X7=[["path",{d:"M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.93a2 2 0 0 1-1.66-.9l-.82-1.2A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13c0 1.1.9 2 2 2Z"}],["circle",{cx:"12",cy:"13",r:"1"}]],j7=[["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"}],["path",{d:"M12 10v6"}],["path",{d:"m15 13-3 3-3-3"}]],N7=[["path",{d:"M9 20H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v5"}],["circle",{cx:"13",cy:"12",r:"2"}],["path",{d:"M18 19c-2.8 0-5-2.2-5-5v8"}],["circle",{cx:"20",cy:"19",r:"2"}]],K7=[["circle",{cx:"12",cy:"13",r:"2"}],["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"}],["path",{d:"M14 13h3"}],["path",{d:"M7 13h3"}]],Q7=[["path",{d:"M10.638 20H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v3.417"}],["path",{d:"M14.62 18.8A2.25 2.25 0 1 1 18 15.836a2.25 2.25 0 1 1 3.38 2.966l-2.626 2.856a.998.998 0 0 1-1.507 0z"}]],J7=[["path",{d:"M2 9V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-1"}],["path",{d:"M2 13h10"}],["path",{d:"m9 16 3-3-3-3"}]],Y7=[["circle",{cx:"16",cy:"20",r:"2"}],["path",{d:"M10 20H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v2"}],["path",{d:"m22 14-4.5 4.5"}],["path",{d:"m21 15 1 1"}]],_7=[["path",{d:"M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.93a2 2 0 0 1-1.66-.9l-.82-1.2A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13c0 1.1.9 2 2 2Z"}],["path",{d:"M8 10v4"}],["path",{d:"M12 10v2"}],["path",{d:"M16 10v6"}]],x7=[["path",{d:"M9 13h6"}],["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"}]],aM=[["rect",{width:"8",height:"5",x:"14",y:"17",rx:"1"}],["path",{d:"M10 20H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v2.5"}],["path",{d:"M20 17v-2a2 2 0 1 0-4 0v2"}]],tM=[["path",{d:"m6 14 1.45-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.55 6a2 2 0 0 1-1.94 1.5H4a2 2 0 0 1-2-2V5c0-1.1.9-2 2-2h3.93a2 2 0 0 1 1.66.9l.82 1.2a2 2 0 0 0 1.66.9H18a2 2 0 0 1 2 2v2"}],["circle",{cx:"14",cy:"15",r:"1"}]],hM=[["path",{d:"m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2"}]],dM=[["path",{d:"M2 7.5V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-1.5"}],["path",{d:"M2 13h10"}],["path",{d:"m5 10-3 3 3 3"}]],N1=[["path",{d:"M2 11.5V5a2 2 0 0 1 2-2h3.9c.7 0 1.3.3 1.7.9l.8 1.2c.4.6 1 .9 1.7.9H20a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2h-9.5"}],["path",{d:"M11.378 13.626a1 1 0 1 0-3.004-3.004l-5.01 5.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"}]],cM=[["path",{d:"M12 10v6"}],["path",{d:"M9 13h6"}],["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"}]],MM=[["path",{d:"M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.93a2 2 0 0 1-1.66-.9l-.82-1.2A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13c0 1.1.9 2 2 2Z"}],["circle",{cx:"12",cy:"13",r:"2"}],["path",{d:"M12 15v5"}]],pM=[["circle",{cx:"11.5",cy:"12.5",r:"2.5"}],["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"}],["path",{d:"M13.3 14.3 15 16"}]],iM=[["path",{d:"M10.7 20H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v4.1"}],["path",{d:"m21 21-1.9-1.9"}],["circle",{cx:"17",cy:"17",r:"3"}]],nM=[["path",{d:"M2 9.35V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h7"}],["path",{d:"m8 16 3-3-3-3"}]],lM=[["path",{d:"M9 20H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v.5"}],["path",{d:"M12 10v4h4"}],["path",{d:"m12 14 1.535-1.605a5 5 0 0 1 8 1.5"}],["path",{d:"M22 22v-4h-4"}],["path",{d:"m22 18-1.535 1.605a5 5 0 0 1-8-1.5"}]],eM=[["path",{d:"M20 10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1h-2.5a1 1 0 0 1-.8-.4l-.9-1.2A1 1 0 0 0 15 3h-2a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1Z"}],["path",{d:"M20 21a1 1 0 0 0 1-1v-3a1 1 0 0 0-1-1h-2.9a1 1 0 0 1-.88-.55l-.42-.85a1 1 0 0 0-.92-.6H13a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1Z"}],["path",{d:"M3 5a2 2 0 0 0 2 2h3"}],["path",{d:"M3 3v13a2 2 0 0 0 2 2h3"}]],rM=[["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"}],["path",{d:"M12 10v6"}],["path",{d:"m9 13 3-3 3 3"}]],oM=[["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"}],["path",{d:"m9.5 10.5 5 5"}],["path",{d:"m14.5 10.5-5 5"}]],vM=[["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"}]],$M=[["path",{d:"M20 5a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H9a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h2.5a1.5 1.5 0 0 1 1.2.6l.6.8a1.5 1.5 0 0 0 1.2.6z"}],["path",{d:"M3 8.268a2 2 0 0 0-1 1.738V19a2 2 0 0 0 2 2h11a2 2 0 0 0 1.732-1"}]],mM=[["path",{d:"M4 16v-2.38C4 11.5 2.97 10.5 3 8c.03-2.72 1.49-6 4.5-6C9.37 2 10 3.8 10 5.5c0 3.11-2 5.66-2 8.68V16a2 2 0 1 1-4 0Z"}],["path",{d:"M20 20v-2.38c0-2.12 1.03-3.12 1-5.62-.03-2.72-1.49-6-4.5-6C14.63 6 14 7.8 14 9.5c0 3.11 2 5.66 2 8.68V20a2 2 0 1 0 4 0Z"}],["path",{d:"M16 17h4"}],["path",{d:"M4 13h4"}]],yM=[["path",{d:"M12 12H5a2 2 0 0 0-2 2v5"}],["circle",{cx:"13",cy:"19",r:"2"}],["circle",{cx:"5",cy:"19",r:"2"}],["path",{d:"M8 19h3m5-17v17h6M6 12V7c0-1.1.9-2 2-2h3l5 5"}]],sM=[["path",{d:"m15 17 5-5-5-5"}],["path",{d:"M4 18v-2a4 4 0 0 1 4-4h12"}]],gM=[["line",{x1:"22",x2:"2",y1:"6",y2:"6"}],["line",{x1:"22",x2:"2",y1:"18",y2:"18"}],["line",{x1:"6",x2:"6",y1:"2",y2:"22"}],["line",{x1:"18",x2:"18",y1:"2",y2:"22"}]],uM=[["path",{d:"M5 16V9h14V2H5l14 14h-7m-7 0 7 7v-7m-7 0h7"}]],CM=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M16 16s-1.5-2-4-2-4 2-4 2"}],["line",{x1:"9",x2:"9.01",y1:"9",y2:"9"}],["line",{x1:"15",x2:"15.01",y1:"9",y2:"9"}]],HM=[["path",{d:"M14 13h2a2 2 0 0 1 2 2v2a2 2 0 0 0 4 0v-6.998a2 2 0 0 0-.59-1.42L18 5"}],["path",{d:"M14 21V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v16"}],["path",{d:"M2 21h13"}],["path",{d:"M3 9h11"}]],AM=[["path",{d:"M3 7V5a2 2 0 0 1 2-2h2"}],["path",{d:"M17 3h2a2 2 0 0 1 2 2v2"}],["path",{d:"M21 17v2a2 2 0 0 1-2 2h-2"}],["path",{d:"M7 21H5a2 2 0 0 1-2-2v-2"}],["rect",{width:"10",height:"8",x:"7",y:"8",rx:"1"}]],wM=[["path",{d:"M13.354 3H3a1 1 0 0 0-.742 1.67l7.225 7.989A2 2 0 0 1 10 14v6a1 1 0 0 0 .553.895l2 1A1 1 0 0 0 14 21v-7a2 2 0 0 1 .517-1.341l1.218-1.348"}],["path",{d:"M16 6h6"}],["path",{d:"M19 3v6"}]],K1=[["path",{d:"M10 20a1 1 0 0 0 .553.895l2 1A1 1 0 0 0 14 21v-7a2 2 0 0 1 .517-1.341L21.74 4.67A1 1 0 0 0 21 3H3a1 1 0 0 0-.742 1.67l7.225 7.989A2 2 0 0 1 10 14z"}]],Q1=[["path",{d:"M12.531 3H3a1 1 0 0 0-.742 1.67l7.225 7.989A2 2 0 0 1 10 14v6a1 1 0 0 0 .553.895l2 1A1 1 0 0 0 14 21v-7a2 2 0 0 1 .517-1.341l.427-.473"}],["path",{d:"m16.5 3.5 5 5"}],["path",{d:"m21.5 3.5-5 5"}]],VM=[["path",{d:"M2 7v10"}],["path",{d:"M6 5v14"}],["rect",{width:"12",height:"18",x:"10",y:"3",rx:"2"}]],SM=[["path",{d:"M2 3v18"}],["rect",{width:"12",height:"18",x:"6",y:"3",rx:"2"}],["path",{d:"M22 3v18"}]],LM=[["rect",{width:"18",height:"14",x:"3",y:"3",rx:"2"}],["path",{d:"M4 21h1"}],["path",{d:"M9 21h1"}],["path",{d:"M14 21h1"}],["path",{d:"M19 21h1"}]],fM=[["path",{d:"M7 2h10"}],["path",{d:"M5 6h14"}],["rect",{width:"18",height:"12",x:"3",y:"10",rx:"2"}]],kM=[["path",{d:"M3 2h18"}],["rect",{width:"18",height:"12",x:"3",y:"6",rx:"2"}],["path",{d:"M3 22h18"}]],PM=[["line",{x1:"6",x2:"10",y1:"11",y2:"11"}],["line",{x1:"8",x2:"8",y1:"9",y2:"13"}],["line",{x1:"15",x2:"15.01",y1:"12",y2:"12"}],["line",{x1:"18",x2:"18.01",y1:"10",y2:"10"}],["path",{d:"M17.32 5H6.68a4 4 0 0 0-3.978 3.59c-.006.052-.01.101-.017.152C2.604 9.416 2 14.456 2 16a3 3 0 0 0 3 3c1 0 1.5-.5 2-1l1.414-1.414A2 2 0 0 1 9.828 16h4.344a2 2 0 0 1 1.414.586L17 18c.5.5 1 1 2 1a3 3 0 0 0 3-3c0-1.545-.604-6.584-.685-7.258-.007-.05-.011-.1-.017-.151A4 4 0 0 0 17.32 5z"}]],BM=[["line",{x1:"6",x2:"10",y1:"12",y2:"12"}],["line",{x1:"8",x2:"8",y1:"10",y2:"14"}],["line",{x1:"15",x2:"15.01",y1:"13",y2:"13"}],["line",{x1:"18",x2:"18.01",y1:"11",y2:"11"}],["rect",{width:"20",height:"12",x:"2",y:"6",rx:"2"}]],DM=[["path",{d:"m12 14 4-4"}],["path",{d:"M3.34 19a10 10 0 1 1 17.32 0"}]],FM=[["path",{d:"m14 13-8.381 8.38a1 1 0 0 1-3.001-3l8.384-8.381"}],["path",{d:"m16 16 6-6"}],["path",{d:"m21.5 10.5-8-8"}],["path",{d:"m8 8 6-6"}],["path",{d:"m8.5 7.5 8 8"}]],zM=[["path",{d:"M10.5 3 8 9l4 13 4-13-2.5-6"}],["path",{d:"M17 3a2 2 0 0 1 1.6.8l3 4a2 2 0 0 1 .013 2.382l-7.99 10.986a2 2 0 0 1-3.247 0l-7.99-10.986A2 2 0 0 1 2.4 7.8l2.998-3.997A2 2 0 0 1 7 3z"}],["path",{d:"M2 9h20"}]],bM=[["path",{d:"M11.5 21a7.5 7.5 0 1 1 7.35-9"}],["path",{d:"M13 12V3"}],["path",{d:"M4 21h16"}],["path",{d:"M9 12V3"}]],RM=[["rect",{x:"3",y:"8",width:"18",height:"4",rx:"1"}],["path",{d:"M12 8v13"}],["path",{d:"M19 12v7a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-7"}],["path",{d:"M7.5 8a2.5 2.5 0 0 1 0-5A4.8 8 0 0 1 12 8a4.8 8 0 0 1 4.5-5 2.5 2.5 0 0 1 0 5"}]],TM=[["path",{d:"M6 3v12"}],["path",{d:"M18 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"}],["path",{d:"M6 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"}],["path",{d:"M15 6a9 9 0 0 0-9 9"}],["path",{d:"M18 15v6"}],["path",{d:"M21 18h-6"}]],qM=[["path",{d:"M9 10h.01"}],["path",{d:"M15 10h.01"}],["path",{d:"M12 2a8 8 0 0 0-8 8v12l3-3 2.5 2.5L12 19l2.5 2.5L17 19l3 3V10a8 8 0 0 0-8-8z"}]],UM=[["line",{x1:"6",x2:"6",y1:"3",y2:"15"}],["circle",{cx:"18",cy:"6",r:"3"}],["circle",{cx:"6",cy:"18",r:"3"}],["path",{d:"M18 9a9 9 0 0 1-9 9"}]],J1=[["circle",{cx:"12",cy:"12",r:"3"}],["line",{x1:"3",x2:"9",y1:"12",y2:"12"}],["line",{x1:"15",x2:"21",y1:"12",y2:"12"}]],OM=[["path",{d:"M12 3v6"}],["circle",{cx:"12",cy:"12",r:"3"}],["path",{d:"M12 15v6"}]],ZM=[["circle",{cx:"5",cy:"6",r:"3"}],["path",{d:"M12 6h5a2 2 0 0 1 2 2v7"}],["path",{d:"m15 9-3-3 3-3"}],["circle",{cx:"19",cy:"18",r:"3"}],["path",{d:"M12 18H7a2 2 0 0 1-2-2V9"}],["path",{d:"m9 15 3 3-3 3"}]],GM=[["circle",{cx:"18",cy:"18",r:"3"}],["circle",{cx:"6",cy:"6",r:"3"}],["path",{d:"M13 6h3a2 2 0 0 1 2 2v7"}],["path",{d:"M11 18H8a2 2 0 0 1-2-2V9"}]],IM=[["circle",{cx:"12",cy:"18",r:"3"}],["circle",{cx:"6",cy:"6",r:"3"}],["circle",{cx:"18",cy:"6",r:"3"}],["path",{d:"M18 9v2c0 .6-.4 1-1 1H7c-.6 0-1-.4-1-1V9"}],["path",{d:"M12 12v3"}]],WM=[["circle",{cx:"5",cy:"6",r:"3"}],["path",{d:"M5 9v6"}],["circle",{cx:"5",cy:"18",r:"3"}],["path",{d:"M12 3v18"}],["circle",{cx:"19",cy:"6",r:"3"}],["path",{d:"M16 15.7A9 9 0 0 0 19 9"}]],EM=[["circle",{cx:"18",cy:"18",r:"3"}],["circle",{cx:"6",cy:"6",r:"3"}],["path",{d:"M6 21V9a9 9 0 0 0 9 9"}]],XM=[["circle",{cx:"5",cy:"6",r:"3"}],["path",{d:"M5 9v12"}],["circle",{cx:"19",cy:"18",r:"3"}],["path",{d:"m15 9-3-3 3-3"}],["path",{d:"M12 6h5a2 2 0 0 1 2 2v7"}]],jM=[["circle",{cx:"6",cy:"6",r:"3"}],["path",{d:"M6 9v12"}],["path",{d:"m21 3-6 6"}],["path",{d:"m21 9-6-6"}],["path",{d:"M18 11.5V15"}],["circle",{cx:"18",cy:"18",r:"3"}]],NM=[["circle",{cx:"5",cy:"6",r:"3"}],["path",{d:"M5 9v12"}],["path",{d:"m15 9-3-3 3-3"}],["path",{d:"M12 6h5a2 2 0 0 1 2 2v3"}],["path",{d:"M19 15v6"}],["path",{d:"M22 18h-6"}]],KM=[["circle",{cx:"6",cy:"6",r:"3"}],["path",{d:"M6 9v12"}],["path",{d:"M13 6h3a2 2 0 0 1 2 2v3"}],["path",{d:"M18 15v6"}],["path",{d:"M21 18h-6"}]],QM=[["circle",{cx:"18",cy:"18",r:"3"}],["circle",{cx:"6",cy:"6",r:"3"}],["path",{d:"M18 6V5"}],["path",{d:"M18 11v-1"}],["line",{x1:"6",x2:"6",y1:"9",y2:"21"}]],JM=[["circle",{cx:"18",cy:"18",r:"3"}],["circle",{cx:"6",cy:"6",r:"3"}],["path",{d:"M13 6h3a2 2 0 0 1 2 2v7"}],["line",{x1:"6",x2:"6",y1:"9",y2:"21"}]],YM=[["path",{d:"M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"}],["path",{d:"M9 18c-4.51 2-5-2-7-2"}]],_M=[["path",{d:"m22 13.29-3.33-10a.42.42 0 0 0-.14-.18.38.38 0 0 0-.22-.11.39.39 0 0 0-.23.07.42.42 0 0 0-.14.18l-2.26 6.67H8.32L6.1 3.26a.42.42 0 0 0-.1-.18.38.38 0 0 0-.26-.08.39.39 0 0 0-.23.07.42.42 0 0 0-.14.18L2 13.29a.74.74 0 0 0 .27.83L12 21l9.69-6.88a.71.71 0 0 0 .31-.83Z"}]],xM=[["path",{d:"M5.116 4.104A1 1 0 0 1 6.11 3h11.78a1 1 0 0 1 .994 1.105L17.19 20.21A2 2 0 0 1 15.2 22H8.8a2 2 0 0 1-2-1.79z"}],["path",{d:"M6 12a5 5 0 0 1 6 0 5 5 0 0 0 6 0"}]],a9=[["circle",{cx:"6",cy:"15",r:"4"}],["circle",{cx:"18",cy:"15",r:"4"}],["path",{d:"M14 15a2 2 0 0 0-2-2 2 2 0 0 0-2 2"}],["path",{d:"M2.5 13 5 7c.7-1.3 1.4-2 3-2"}],["path",{d:"M21.5 13 19 7c-.7-1.3-1.5-2-3-2"}]],t9=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"}],["path",{d:"M2 12h20"}]],h9=[["path",{d:"M15.686 15A14.5 14.5 0 0 1 12 22a14.5 14.5 0 0 1 0-20 10 10 0 1 0 9.542 13"}],["path",{d:"M2 12h8.5"}],["path",{d:"M20 6V4a2 2 0 1 0-4 0v2"}],["rect",{width:"8",height:"5",x:"14",y:"6",rx:"1"}]],d9=[["path",{d:"M12 13V2l8 4-8 4"}],["path",{d:"M20.561 10.222a9 9 0 1 1-12.55-5.29"}],["path",{d:"M8.002 9.997a5 5 0 1 0 8.9 2.02"}]],c9=[["path",{d:"M2 21V3"}],["path",{d:"M2 5h18a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H2.26"}],["path",{d:"M7 17v3a1 1 0 0 0 1 1h5a1 1 0 0 0 1-1v-3"}],["circle",{cx:"16",cy:"11",r:"2"}],["circle",{cx:"8",cy:"11",r:"2"}]],M9=[["path",{d:"M21.42 10.922a1 1 0 0 0-.019-1.838L12.83 5.18a2 2 0 0 0-1.66 0L2.6 9.08a1 1 0 0 0 0 1.832l8.57 3.908a2 2 0 0 0 1.66 0z"}],["path",{d:"M22 10v6"}],["path",{d:"M6 12.5V16a6 3 0 0 0 12 0v-3.5"}]],p9=[["path",{d:"M22 5V2l-5.89 5.89"}],["circle",{cx:"16.6",cy:"15.89",r:"3"}],["circle",{cx:"8.11",cy:"7.4",r:"3"}],["circle",{cx:"12.35",cy:"11.65",r:"3"}],["circle",{cx:"13.91",cy:"5.85",r:"3"}],["circle",{cx:"18.15",cy:"10.09",r:"3"}],["circle",{cx:"6.56",cy:"13.2",r:"3"}],["circle",{cx:"10.8",cy:"17.44",r:"3"}],["circle",{cx:"5",cy:"19",r:"3"}]],Y1=[["path",{d:"M12 3v17a1 1 0 0 1-1 1H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v6a1 1 0 0 1-1 1H3"}],["path",{d:"M16 19h6"}],["path",{d:"M19 22v-6"}]],_1=[["path",{d:"M12 3v17a1 1 0 0 1-1 1H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v6a1 1 0 0 1-1 1H3"}],["path",{d:"m16 19 2 2 4-4"}]],x1=[["path",{d:"M12 3v17a1 1 0 0 1-1 1H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v6a1 1 0 0 1-1 1H3"}],["path",{d:"m16 16 5 5"}],["path",{d:"m16 21 5-5"}]],i9=[["path",{d:"M15 3v18"}],["path",{d:"M3 12h18"}],["path",{d:"M9 3v18"}],["rect",{x:"3",y:"3",width:"18",height:"18",rx:"2"}]],a2=[["path",{d:"M12 3v18"}],["path",{d:"M3 12h18"}],["rect",{x:"3",y:"3",width:"18",height:"18",rx:"2"}]],r=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M3 9h18"}],["path",{d:"M3 15h18"}],["path",{d:"M9 3v18"}],["path",{d:"M15 3v18"}]],n9=[["circle",{cx:"12",cy:"9",r:"1"}],["circle",{cx:"19",cy:"9",r:"1"}],["circle",{cx:"5",cy:"9",r:"1"}],["circle",{cx:"12",cy:"15",r:"1"}],["circle",{cx:"19",cy:"15",r:"1"}],["circle",{cx:"5",cy:"15",r:"1"}]],l9=[["circle",{cx:"9",cy:"12",r:"1"}],["circle",{cx:"9",cy:"5",r:"1"}],["circle",{cx:"9",cy:"19",r:"1"}],["circle",{cx:"15",cy:"12",r:"1"}],["circle",{cx:"15",cy:"5",r:"1"}],["circle",{cx:"15",cy:"19",r:"1"}]],e9=[["circle",{cx:"12",cy:"5",r:"1"}],["circle",{cx:"19",cy:"5",r:"1"}],["circle",{cx:"5",cy:"5",r:"1"}],["circle",{cx:"12",cy:"12",r:"1"}],["circle",{cx:"19",cy:"12",r:"1"}],["circle",{cx:"5",cy:"12",r:"1"}],["circle",{cx:"12",cy:"19",r:"1"}],["circle",{cx:"19",cy:"19",r:"1"}],["circle",{cx:"5",cy:"19",r:"1"}]],r9=[["path",{d:"M3 7V5c0-1.1.9-2 2-2h2"}],["path",{d:"M17 3h2c1.1 0 2 .9 2 2v2"}],["path",{d:"M21 17v2c0 1.1-.9 2-2 2h-2"}],["path",{d:"M7 21H5c-1.1 0-2-.9-2-2v-2"}],["rect",{width:"7",height:"5",x:"7",y:"7",rx:"1"}],["rect",{width:"7",height:"5",x:"10",y:"12",rx:"1"}]],o9=[["path",{d:"M13.144 21.144A7.274 10.445 45 1 0 2.856 10.856"}],["path",{d:"M13.144 21.144A7.274 4.365 45 0 0 2.856 10.856a7.274 4.365 45 0 0 10.288 10.288"}],["path",{d:"M16.565 10.435 18.6 8.4a2.501 2.501 0 1 0 1.65-4.65 2.5 2.5 0 1 0-4.66 1.66l-2.024 2.025"}],["path",{d:"m8.5 16.5-1-1"}]],v9=[["path",{d:"m11.9 12.1 4.514-4.514"}],["path",{d:"M20.1 2.3a1 1 0 0 0-1.4 0l-1.114 1.114A2 2 0 0 0 17 4.828v1.344a2 2 0 0 1-.586 1.414A2 2 0 0 1 17.828 7h1.344a2 2 0 0 0 1.414-.586L21.7 5.3a1 1 0 0 0 0-1.4z"}],["path",{d:"m6 16 2 2"}],["path",{d:"M8.23 9.85A3 3 0 0 1 11 8a5 5 0 0 1 5 5 3 3 0 0 1-1.85 2.77l-.92.38A2 2 0 0 0 12 18a4 4 0 0 1-4 4 6 6 0 0 1-6-6 4 4 0 0 1 4-4 2 2 0 0 0 1.85-1.23z"}]],$9=[["path",{d:"M12 16H4a2 2 0 1 1 0-4h16a2 2 0 1 1 0 4h-4.25"}],["path",{d:"M5 12a2 2 0 0 1-2-2 9 7 0 0 1 18 0 2 2 0 0 1-2 2"}],["path",{d:"M5 16a2 2 0 0 0-2 2 3 3 0 0 0 3 3h12a3 3 0 0 0 3-3 2 2 0 0 0-2-2q0 0 0 0"}],["path",{d:"m6.67 12 6.13 4.6a2 2 0 0 0 2.8-.4l3.15-4.2"}]],m9=[["path",{d:"m15 12-9.373 9.373a1 1 0 0 1-3.001-3L12 9"}],["path",{d:"m18 15 4-4"}],["path",{d:"m21.5 11.5-1.914-1.914A2 2 0 0 1 19 8.172v-.344a2 2 0 0 0-.586-1.414l-1.657-1.657A6 6 0 0 0 12.516 3H9l1.243 1.243A6 6 0 0 1 12 8.485V10l2 2h1.172a2 2 0 0 1 1.414.586L18.5 14.5"}]],y9=[["path",{d:"M11 15h2a2 2 0 1 0 0-4h-3c-.6 0-1.1.2-1.4.6L3 17"}],["path",{d:"m7 21 1.6-1.4c.3-.4.8-.6 1.4-.6h4c1.1 0 2.1-.4 2.8-1.2l4.6-4.4a2 2 0 0 0-2.75-2.91l-4.2 3.9"}],["path",{d:"m2 16 6 6"}],["circle",{cx:"16",cy:"9",r:"2.9"}],["circle",{cx:"6",cy:"5",r:"3"}]],s9=[["path",{d:"M12.035 17.012a3 3 0 0 0-3-3l-.311-.002a.72.72 0 0 1-.505-1.229l1.195-1.195A2 2 0 0 1 10.828 11H12a2 2 0 0 0 0-4H9.243a3 3 0 0 0-2.122.879l-2.707 2.707A4.83 4.83 0 0 0 3 14a8 8 0 0 0 8 8h2a8 8 0 0 0 8-8V7a2 2 0 1 0-4 0v2a2 2 0 1 0 4 0"}],["path",{d:"M13.888 9.662A2 2 0 0 0 17 8V5A2 2 0 1 0 13 5"}],["path",{d:"M9 5A2 2 0 1 0 5 5V10"}],["path",{d:"M9 7V4A2 2 0 1 1 13 4V7.268"}]],t2=[["path",{d:"M18 11.5V9a2 2 0 0 0-2-2a2 2 0 0 0-2 2v1.4"}],["path",{d:"M14 10V8a2 2 0 0 0-2-2a2 2 0 0 0-2 2v2"}],["path",{d:"M10 9.9V9a2 2 0 0 0-2-2a2 2 0 0 0-2 2v5"}],["path",{d:"M6 14a2 2 0 0 0-2-2a2 2 0 0 0-2 2"}],["path",{d:"M18 11a2 2 0 1 1 4 0v3a8 8 0 0 1-8 8h-4a8 8 0 0 1-8-8 2 2 0 1 1 4 0"}]],h2=[["path",{d:"M11 12h2a2 2 0 1 0 0-4h-3c-.6 0-1.1.2-1.4.6L3 14"}],["path",{d:"m7 18 1.6-1.4c.3-.4.8-.6 1.4-.6h4c1.1 0 2.1-.4 2.8-1.2l4.6-4.4a2 2 0 0 0-2.75-2.91l-4.2 3.9"}],["path",{d:"m2 13 6 6"}]],g9=[["path",{d:"M11 14h2a2 2 0 0 0 0-4h-3c-.6 0-1.1.2-1.4.6L3 16"}],["path",{d:"m14.45 13.39 5.05-4.694C20.196 8 21 6.85 21 5.75a2.75 2.75 0 0 0-4.797-1.837.276.276 0 0 1-.406 0A2.75 2.75 0 0 0 11 5.75c0 1.2.802 2.248 1.5 2.946L16 11.95"}],["path",{d:"m2 15 6 6"}],["path",{d:"m7 20 1.6-1.4c.3-.4.8-.6 1.4-.6h4c1.1 0 2.1-.4 2.8-1.2l4.6-4.4a1 1 0 0 0-2.75-2.91"}]],u9=[["path",{d:"M18 12.5V10a2 2 0 0 0-2-2a2 2 0 0 0-2 2v1.4"}],["path",{d:"M14 11V9a2 2 0 1 0-4 0v2"}],["path",{d:"M10 10.5V5a2 2 0 1 0-4 0v9"}],["path",{d:"m7 15-1.76-1.76a2 2 0 0 0-2.83 2.82l3.6 3.6C7.5 21.14 9.2 22 12 22h2a8 8 0 0 0 8-8V7a2 2 0 1 0-4 0v5"}]],C9=[["path",{d:"M12 3V2"}],["path",{d:"m15.4 17.4 3.2-2.8a2 2 0 1 1 2.8 2.9l-3.6 3.3c-.7.8-1.7 1.2-2.8 1.2h-4c-1.1 0-2.1-.4-2.8-1.2l-1.302-1.464A1 1 0 0 0 6.151 19H5"}],["path",{d:"M2 14h12a2 2 0 0 1 0 4h-2"}],["path",{d:"M4 10h16"}],["path",{d:"M5 10a7 7 0 0 1 14 0"}],["path",{d:"M5 14v6a1 1 0 0 1-1 1H2"}]],H9=[["path",{d:"M2.048 18.566A2 2 0 0 0 4 21h16a2 2 0 0 0 1.952-2.434l-2-9A2 2 0 0 0 18 8H6a2 2 0 0 0-1.952 1.566z"}],["path",{d:"M8 11V6a4 4 0 0 1 8 0v5"}]],A9=[["path",{d:"M18 11V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2"}],["path",{d:"M14 10V4a2 2 0 0 0-2-2a2 2 0 0 0-2 2v2"}],["path",{d:"M10 10.5V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2v8"}],["path",{d:"M18 8a2 2 0 1 1 4 0v6a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.86-5.99-2.34l-3.6-3.6a2 2 0 0 1 2.83-2.82L7 15"}]],w9=[["path",{d:"m11 17 2 2a1 1 0 1 0 3-3"}],["path",{d:"m14 14 2.5 2.5a1 1 0 1 0 3-3l-3.88-3.88a3 3 0 0 0-4.24 0l-.88.88a1 1 0 1 1-3-3l2.81-2.81a5.79 5.79 0 0 1 7.06-.87l.47.28a2 2 0 0 0 1.42.25L21 4"}],["path",{d:"m21 3 1 11h-2"}],["path",{d:"M3 3 2 14l6.5 6.5a1 1 0 1 0 3-3"}],["path",{d:"M3 4h8"}]],V9=[["path",{d:"M12 2v8"}],["path",{d:"m16 6-4 4-4-4"}],["rect",{width:"20",height:"8",x:"2",y:"14",rx:"2"}],["path",{d:"M6 18h.01"}],["path",{d:"M10 18h.01"}]],S9=[["path",{d:"m16 6-4-4-4 4"}],["path",{d:"M12 2v8"}],["rect",{width:"20",height:"8",x:"2",y:"14",rx:"2"}],["path",{d:"M6 18h.01"}],["path",{d:"M10 18h.01"}]],L9=[["path",{d:"M10 10V5a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v5"}],["path",{d:"M14 6a6 6 0 0 1 6 6v3"}],["path",{d:"M4 15v-3a6 6 0 0 1 6-6"}],["rect",{x:"2",y:"15",width:"20",height:"4",rx:"1"}]],f9=[["line",{x1:"22",x2:"2",y1:"12",y2:"12"}],["path",{d:"M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"}],["line",{x1:"6",x2:"6.01",y1:"16",y2:"16"}],["line",{x1:"10",x2:"10.01",y1:"16",y2:"16"}]],k9=[["line",{x1:"4",x2:"20",y1:"9",y2:"9"}],["line",{x1:"4",x2:"20",y1:"15",y2:"15"}],["line",{x1:"10",x2:"8",y1:"3",y2:"21"}],["line",{x1:"16",x2:"14",y1:"3",y2:"21"}]],P9=[["path",{d:"M14 18a2 2 0 0 0-4 0"}],["path",{d:"m19 11-2.11-6.657a2 2 0 0 0-2.752-1.148l-1.276.61A2 2 0 0 1 12 4H8.5a2 2 0 0 0-1.925 1.456L5 11"}],["path",{d:"M2 11h20"}],["circle",{cx:"17",cy:"18",r:"3"}],["circle",{cx:"7",cy:"18",r:"3"}]],B9=[["path",{d:"M22 9a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v4a1 1 0 0 0 1 1h1l2 2h12l2-2h1a1 1 0 0 0 1-1Z"}],["path",{d:"M7.5 12h9"}]],D9=[["path",{d:"m5.2 6.2 1.4 1.4"}],["path",{d:"M2 13h2"}],["path",{d:"M20 13h2"}],["path",{d:"m17.4 7.6 1.4-1.4"}],["path",{d:"M22 17H2"}],["path",{d:"M22 21H2"}],["path",{d:"M16 13a4 4 0 0 0-8 0"}],["path",{d:"M12 5V2.5"}]],F9=[["path",{d:"M4 12h8"}],["path",{d:"M4 18V6"}],["path",{d:"M12 18V6"}],["path",{d:"m17 12 3-2v8"}]],z9=[["path",{d:"M4 12h8"}],["path",{d:"M4 18V6"}],["path",{d:"M12 18V6"}],["path",{d:"M21 18h-4c0-4 4-3 4-6 0-1.5-2-2.5-4-1"}]],b9=[["path",{d:"M12 18V6"}],["path",{d:"M17 10v3a1 1 0 0 0 1 1h3"}],["path",{d:"M21 10v8"}],["path",{d:"M4 12h8"}],["path",{d:"M4 18V6"}]],R9=[["path",{d:"M4 12h8"}],["path",{d:"M4 18V6"}],["path",{d:"M12 18V6"}],["path",{d:"M17.5 10.5c1.7-1 3.5 0 3.5 1.5a2 2 0 0 1-2 2"}],["path",{d:"M17 17.5c2 1.5 4 .3 4-1.5a2 2 0 0 0-2-2"}]],T9=[["path",{d:"M4 12h8"}],["path",{d:"M4 18V6"}],["path",{d:"M12 18V6"}],["path",{d:"M17 13v-3h4"}],["path",{d:"M17 17.7c.4.2.8.3 1.3.3 1.5 0 2.7-1.1 2.7-2.5S19.8 13 18.3 13H17"}]],q9=[["path",{d:"M4 12h8"}],["path",{d:"M4 18V6"}],["path",{d:"M12 18V6"}],["circle",{cx:"19",cy:"16",r:"2"}],["path",{d:"M20 10c-2 2-3 3.5-3 6"}]],U9=[["path",{d:"M6 12h12"}],["path",{d:"M6 20V4"}],["path",{d:"M18 20V4"}]],O9=[["path",{d:"M21 14h-1.343"}],["path",{d:"M9.128 3.47A9 9 0 0 1 21 12v3.343"}],["path",{d:"m2 2 20 20"}],["path",{d:"M20.414 20.414A2 2 0 0 1 19 21h-1a2 2 0 0 1-2-2v-3"}],["path",{d:"M3 14h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-7a9 9 0 0 1 2.636-6.364"}]],Z9=[["path",{d:"M3 14h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-7a9 9 0 0 1 18 0v7a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3"}]],G9=[["path",{d:"M3 11h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-5Zm0 0a9 9 0 1 1 18 0m0 0v5a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3Z"}],["path",{d:"M21 16v2a4 4 0 0 1-4 4h-5"}]],I9=[["path",{d:"M12.409 5.824c-.702.792-1.15 1.496-1.415 2.166l2.153 2.156a.5.5 0 0 1 0 .707l-2.293 2.293a.5.5 0 0 0 0 .707L12 15"}],["path",{d:"M13.508 20.313a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5a5.5 5.5 0 0 1 9.591-3.677.6.6 0 0 0 .818.001A5.5 5.5 0 0 1 22 9.5c0 2.29-1.5 4-3 5.5z"}]],W9=[["path",{d:"M19.414 14.414C21 12.828 22 11.5 22 9.5a5.5 5.5 0 0 0-9.591-3.676.6.6 0 0 1-.818.001A5.5 5.5 0 0 0 2 9.5c0 2.3 1.5 4 3 5.5l5.535 5.362a2 2 0 0 0 2.879.052 2.12 2.12 0 0 0-.004-3 2.124 2.124 0 1 0 3-3 2.124 2.124 0 0 0 3.004 0 2 2 0 0 0 0-2.828l-1.881-1.882a2.41 2.41 0 0 0-3.409 0l-1.71 1.71a2 2 0 0 1-2.828 0 2 2 0 0 1 0-2.828l2.823-2.762"}]],E9=[["path",{d:"m14.876 18.99-1.368 1.323a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5a5.5 5.5 0 0 1 9.591-3.676.56.56 0 0 0 .818 0A5.49 5.49 0 0 1 22 9.5a5.2 5.2 0 0 1-.244 1.572"}],["path",{d:"M15 15h6"}]],X9=[["path",{d:"M10.5 4.893a5.5 5.5 0 0 1 1.091.931.56.56 0 0 0 .818 0A5.49 5.49 0 0 1 22 9.5c0 1.872-1.002 3.356-2.187 4.655"}],["path",{d:"m16.967 16.967-3.459 3.346a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5a5.5 5.5 0 0 1 2.747-4.761"}],["path",{d:"m2 2 20 20"}]],j9=[["path",{d:"m14.479 19.374-.971.939a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5a5.5 5.5 0 0 1 9.591-3.676.56.56 0 0 0 .818 0A5.49 5.49 0 0 1 22 9.5a5.2 5.2 0 0 1-.219 1.49"}],["path",{d:"M15 15h6"}],["path",{d:"M18 12v6"}]],N9=[["path",{d:"M2 9.5a5.5 5.5 0 0 1 9.591-3.676.56.56 0 0 0 .818 0A5.49 5.49 0 0 1 22 9.5c0 2.29-1.5 4-3 5.5l-5.492 5.313a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5"}],["path",{d:"M3.22 13H9.5l.5-1 2 4.5 2-7 1.5 3.5h5.27"}]],K9=[["path",{d:"M2 9.5a5.5 5.5 0 0 1 9.591-3.676.56.56 0 0 0 .818 0A5.49 5.49 0 0 1 22 9.5c0 2.29-1.5 4-3 5.5l-5.492 5.313a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5"}]],Q9=[["path",{d:"M11 8c2-3-2-3 0-6"}],["path",{d:"M15.5 8c2-3-2-3 0-6"}],["path",{d:"M6 10h.01"}],["path",{d:"M6 14h.01"}],["path",{d:"M10 16v-4"}],["path",{d:"M14 16v-4"}],["path",{d:"M18 16v-4"}],["path",{d:"M20 6a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h3"}],["path",{d:"M5 20v2"}],["path",{d:"M19 20v2"}]],J9=[["path",{d:"M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"}]],Y9=[["path",{d:"m9 11-6 6v3h9l3-3"}],["path",{d:"m22 12-4.6 4.6a2 2 0 0 1-2.8 0l-5.2-5.2a2 2 0 0 1 0-2.8L14 4"}]],_9=[["path",{d:"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"}],["path",{d:"M3 3v5h5"}],["path",{d:"M12 7v5l4 2"}]],x9=[["path",{d:"M10.82 16.12c1.69.6 3.91.79 5.18.85.28.01.53-.09.7-.27"}],["path",{d:"M11.14 20.57c.52.24 2.44 1.12 4.08 1.37.46.06.86-.25.9-.71.12-1.52-.3-3.43-.5-4.28"}],["path",{d:"M16.13 21.05c1.65.63 3.68.84 4.87.91a.9.9 0 0 0 .7-.26"}],["path",{d:"M17.99 5.52a20.83 20.83 0 0 1 3.15 4.5.8.8 0 0 1-.68 1.13c-1.17.1-2.5.02-3.9-.25"}],["path",{d:"M20.57 11.14c.24.52 1.12 2.44 1.37 4.08.04.3-.08.59-.31.75"}],["path",{d:"M4.93 4.93a10 10 0 0 0-.67 13.4c.35.43.96.4 1.17-.12.69-1.71 1.07-5.07 1.07-6.71 1.34.45 3.1.9 4.88.62a.85.85 0 0 0 .48-.24"}],["path",{d:"M5.52 17.99c1.05.95 2.91 2.42 4.5 3.15a.8.8 0 0 0 1.13-.68c.2-2.34-.33-5.3-1.57-8.28"}],["path",{d:"M8.35 2.68a10 10 0 0 1 9.98 1.58c.43.35.4.96-.12 1.17-1.5.6-4.3.98-6.07 1.05"}],["path",{d:"m2 2 20 20"}]],ap=[["path",{d:"M10.82 16.12c1.69.6 3.91.79 5.18.85.55.03 1-.42.97-.97-.06-1.27-.26-3.5-.85-5.18"}],["path",{d:"M11.5 6.5c1.64 0 5-.38 6.71-1.07.52-.2.55-.82.12-1.17A10 10 0 0 0 4.26 18.33c.35.43.96.4 1.17-.12.69-1.71 1.07-5.07 1.07-6.71 1.34.45 3.1.9 4.88.62a.88.88 0 0 0 .73-.74c.3-2.14-.15-3.5-.61-4.88"}],["path",{d:"M15.62 16.95c.2.85.62 2.76.5 4.28a.77.77 0 0 1-.9.7 16.64 16.64 0 0 1-4.08-1.36"}],["path",{d:"M16.13 21.05c1.65.63 3.68.84 4.87.91a.9.9 0 0 0 .96-.96 17.68 17.68 0 0 0-.9-4.87"}],["path",{d:"M16.94 15.62c.86.2 2.77.62 4.29.5a.77.77 0 0 0 .7-.9 16.64 16.64 0 0 0-1.36-4.08"}],["path",{d:"M17.99 5.52a20.82 20.82 0 0 1 3.15 4.5.8.8 0 0 1-.68 1.13c-2.33.2-5.3-.32-8.27-1.57"}],["path",{d:"M4.93 4.93 3 3a.7.7 0 0 1 0-1"}],["path",{d:"M9.58 12.18c1.24 2.98 1.77 5.95 1.57 8.28a.8.8 0 0 1-1.13.68 20.82 20.82 0 0 1-4.5-3.15"}]],tp=[["path",{d:"M12 7v4"}],["path",{d:"M14 21v-3a2 2 0 0 0-4 0v3"}],["path",{d:"M14 9h-4"}],["path",{d:"M18 11h2a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-9a2 2 0 0 1 2-2h2"}],["path",{d:"M18 21V5a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16"}]],hp=[["path",{d:"M10 22v-6.57"}],["path",{d:"M12 11h.01"}],["path",{d:"M12 7h.01"}],["path",{d:"M14 15.43V22"}],["path",{d:"M15 16a5 5 0 0 0-6 0"}],["path",{d:"M16 11h.01"}],["path",{d:"M16 7h.01"}],["path",{d:"M8 11h.01"}],["path",{d:"M8 7h.01"}],["rect",{x:"4",y:"2",width:"16",height:"20",rx:"2"}]],dp=[["path",{d:"M5 22h14"}],["path",{d:"M5 2h14"}],["path",{d:"M17 22v-4.172a2 2 0 0 0-.586-1.414L12 12l-4.414 4.414A2 2 0 0 0 7 17.828V22"}],["path",{d:"M7 2v4.172a2 2 0 0 0 .586 1.414L12 12l4.414-4.414A2 2 0 0 0 17 6.172V2"}]],cp=[["path",{d:"M8.62 13.8A2.25 2.25 0 1 1 12 10.836a2.25 2.25 0 1 1 3.38 2.966l-2.626 2.856a.998.998 0 0 1-1.507 0z"}],["path",{d:"M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"}]],Mp=[["path",{d:"M10 12V8.964"}],["path",{d:"M14 12V8.964"}],["path",{d:"M15 12a1 1 0 0 1 1 1v2a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-2a1 1 0 0 1 1-1z"}],["path",{d:"M8.5 21H5a2 2 0 0 1-2-2v-9a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2h-5a2 2 0 0 1-2-2v-2"}]],pp=[["path",{d:"M12.35 21H5a2 2 0 0 1-2-2v-9a2 2 0 0 1 .71-1.53l7-6a2 2 0 0 1 2.58 0l7 6A2 2 0 0 1 21 10v2.35"}],["path",{d:"M14.8 12.4A1 1 0 0 0 14 12h-4a1 1 0 0 0-1 1v8"}],["path",{d:"M15 18h6"}],["path",{d:"M18 15v6"}]],ip=[["path",{d:"M9.5 13.866a4 4 0 0 1 5 .01"}],["path",{d:"M12 17h.01"}],["path",{d:"M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"}],["path",{d:"M7 10.754a8 8 0 0 1 10 0"}]],d2=[["path",{d:"M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8"}],["path",{d:"M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"}]],c2=[["path",{d:"m7 11 4.08 10.35a1 1 0 0 0 1.84 0L17 11"}],["path",{d:"M17 7A5 5 0 0 0 7 7"}],["path",{d:"M17 7a2 2 0 0 1 0 4H7a2 2 0 0 1 0-4"}]],M2=[["path",{d:"M12 17c5 0 8-2.69 8-6H4c0 3.31 3 6 8 6m-4 4h8m-4-3v3M5.14 11a3.5 3.5 0 1 1 6.71 0"}],["path",{d:"M12.14 11a3.5 3.5 0 1 1 6.71 0"}],["path",{d:"M15.5 6.5a3.5 3.5 0 1 0-7 0"}]],np=[["path",{d:"M13.5 8h-3"}],["path",{d:"m15 2-1 2h3a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h3"}],["path",{d:"M16.899 22A5 5 0 0 0 7.1 22"}],["path",{d:"m9 2 3 6"}],["circle",{cx:"12",cy:"15",r:"3"}]],lp=[["path",{d:"M16 10h2"}],["path",{d:"M16 14h2"}],["path",{d:"M6.17 15a3 3 0 0 1 5.66 0"}],["circle",{cx:"9",cy:"11",r:"2"}],["rect",{x:"2",y:"5",width:"20",height:"14",rx:"2"}]],ep=[["path",{d:"M10.3 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v10l-3.1-3.1a2 2 0 0 0-2.814.014L6 21"}],["path",{d:"m14 19 3 3v-5.5"}],["path",{d:"m17 22 3-3"}],["circle",{cx:"9",cy:"9",r:"2"}]],rp=[["path",{d:"M21 9v10a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7"}],["line",{x1:"16",x2:"22",y1:"5",y2:"5"}],["circle",{cx:"9",cy:"9",r:"2"}],["path",{d:"m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"}]],op=[["line",{x1:"2",x2:"22",y1:"2",y2:"22"}],["path",{d:"M10.41 10.41a2 2 0 1 1-2.83-2.83"}],["line",{x1:"13.5",x2:"6",y1:"13.5",y2:"21"}],["line",{x1:"18",x2:"21",y1:"12",y2:"15"}],["path",{d:"M3.59 3.59A1.99 1.99 0 0 0 3 5v14a2 2 0 0 0 2 2h14c.55 0 1.052-.22 1.41-.59"}],["path",{d:"M21 15V5a2 2 0 0 0-2-2H9"}]],vp=[["path",{d:"M15 15.003a1 1 0 0 1 1.517-.859l4.997 2.997a1 1 0 0 1 0 1.718l-4.997 2.997a1 1 0 0 1-1.517-.86z"}],["path",{d:"M21 12.17V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h6"}],["path",{d:"m6 21 5-5"}],["circle",{cx:"9",cy:"9",r:"2"}]],$p=[["path",{d:"M16 5h6"}],["path",{d:"M19 2v6"}],["path",{d:"M21 11.5V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7.5"}],["path",{d:"m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"}],["circle",{cx:"9",cy:"9",r:"2"}]],mp=[["path",{d:"M10.3 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v10l-3.1-3.1a2 2 0 0 0-2.814.014L6 21"}],["path",{d:"m14 19.5 3-3 3 3"}],["path",{d:"M17 22v-5.5"}],["circle",{cx:"9",cy:"9",r:"2"}]],yp=[["path",{d:"M16 3h5v5"}],["path",{d:"M17 21h2a2 2 0 0 0 2-2"}],["path",{d:"M21 12v3"}],["path",{d:"m21 3-5 5"}],["path",{d:"M3 7V5a2 2 0 0 1 2-2"}],["path",{d:"m5 21 4.144-4.144a1.21 1.21 0 0 1 1.712 0L13 19"}],["path",{d:"M9 3h3"}],["rect",{x:"3",y:"11",width:"10",height:"10",rx:"1"}]],sp=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2"}],["circle",{cx:"9",cy:"9",r:"2"}],["path",{d:"m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"}]],gp=[["path",{d:"m22 11-1.296-1.296a2.4 2.4 0 0 0-3.408 0L11 16"}],["path",{d:"M4 8a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2"}],["circle",{cx:"13",cy:"7",r:"1",fill:"currentColor"}],["rect",{x:"8",y:"2",width:"14",height:"14",rx:"2"}]],up=[["path",{d:"M12 3v12"}],["path",{d:"m8 11 4 4 4-4"}],["path",{d:"M8 5H4a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-4"}]],Cp=[["path",{d:"M6 3h12"}],["path",{d:"M6 8h12"}],["path",{d:"m6 13 8.5 8"}],["path",{d:"M6 13h3"}],["path",{d:"M9 13c6.667 0 6.667-10 0-10"}]],Hp=[["polyline",{points:"22 12 16 12 14 15 10 15 8 12 2 12"}],["path",{d:"M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"}]],Ap=[["path",{d:"M6 16c5 0 7-8 12-8a4 4 0 0 1 0 8c-5 0-7-8-12-8a4 4 0 1 0 0 8"}]],wp=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M12 16v-4"}],["path",{d:"M12 8h.01"}]],Vp=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M7 7h.01"}],["path",{d:"M17 7h.01"}],["path",{d:"M7 17h.01"}],["path",{d:"M17 17h.01"}]],Sp=[["rect",{width:"20",height:"20",x:"2",y:"2",rx:"5",ry:"5"}],["path",{d:"M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"}],["line",{x1:"17.5",x2:"17.51",y1:"6.5",y2:"6.5"}]],Lp=[["line",{x1:"19",x2:"10",y1:"4",y2:"4"}],["line",{x1:"14",x2:"5",y1:"20",y2:"20"}],["line",{x1:"15",x2:"9",y1:"4",y2:"20"}]],fp=[["path",{d:"m16 14 4 4-4 4"}],["path",{d:"M20 10a8 8 0 1 0-8 8h8"}]],kp=[["path",{d:"M4 10a8 8 0 1 1 8 8H4"}],["path",{d:"m8 22-4-4 4-4"}]],Pp=[["path",{d:"M12 9.5V21m0-11.5L6 3m6 6.5L18 3"}],["path",{d:"M6 15h12"}],["path",{d:"M6 11h12"}]],Bp=[["path",{d:"M21 17a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v2a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-2Z"}],["path",{d:"M6 15v-2"}],["path",{d:"M12 15V9"}],["circle",{cx:"12",cy:"6",r:"3"}]],Dp=[["path",{d:"M5 3v14"}],["path",{d:"M12 3v8"}],["path",{d:"M19 3v18"}]],Fp=[["path",{d:"M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z"}],["circle",{cx:"16.5",cy:"7.5",r:".5",fill:"currentColor"}]],zp=[["path",{d:"M18 17a1 1 0 0 0-1 1v1a2 2 0 1 0 2-2z"}],["path",{d:"M20.97 3.61a.45.45 0 0 0-.58-.58C10.2 6.6 6.6 10.2 3.03 20.39a.45.45 0 0 0 .58.58C13.8 17.4 17.4 13.8 20.97 3.61"}],["path",{d:"m6.707 6.707 10.586 10.586"}],["path",{d:"M7 5a2 2 0 1 0-2 2h1a1 1 0 0 0 1-1z"}]],bp=[["path",{d:"M12.4 2.7a2.5 2.5 0 0 1 3.4 0l5.5 5.5a2.5 2.5 0 0 1 0 3.4l-3.7 3.7a2.5 2.5 0 0 1-3.4 0L8.7 9.8a2.5 2.5 0 0 1 0-3.4z"}],["path",{d:"m14 7 3 3"}],["path",{d:"m9.4 10.6-6.814 6.814A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814"}]],Rp=[["path",{d:"m15.5 7.5 2.3 2.3a1 1 0 0 0 1.4 0l2.1-2.1a1 1 0 0 0 0-1.4L19 4"}],["path",{d:"m21 2-9.6 9.6"}],["circle",{cx:"7.5",cy:"15.5",r:"5.5"}]],Tp=[["rect",{width:"20",height:"16",x:"2",y:"4",rx:"2"}],["path",{d:"M6 8h4"}],["path",{d:"M14 8h.01"}],["path",{d:"M18 8h.01"}],["path",{d:"M2 12h20"}],["path",{d:"M6 12v4"}],["path",{d:"M10 12v4"}],["path",{d:"M14 12v4"}],["path",{d:"M18 12v4"}]],qp=[["path",{d:"M 20 4 A2 2 0 0 1 22 6"}],["path",{d:"M 22 6 L 22 16.41"}],["path",{d:"M 7 16 L 16 16"}],["path",{d:"M 9.69 4 L 20 4"}],["path",{d:"M14 8h.01"}],["path",{d:"M18 8h.01"}],["path",{d:"m2 2 20 20"}],["path",{d:"M20 20H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2"}],["path",{d:"M6 8h.01"}],["path",{d:"M8 12h.01"}]],Up=[["path",{d:"M10 8h.01"}],["path",{d:"M12 12h.01"}],["path",{d:"M14 8h.01"}],["path",{d:"M16 12h.01"}],["path",{d:"M18 8h.01"}],["path",{d:"M6 8h.01"}],["path",{d:"M7 16h10"}],["path",{d:"M8 12h.01"}],["rect",{width:"20",height:"16",x:"2",y:"4",rx:"2"}]],Op=[["path",{d:"M12 2v5"}],["path",{d:"M14.829 15.998a3 3 0 1 1-5.658 0"}],["path",{d:"M20.92 14.606A1 1 0 0 1 20 16H4a1 1 0 0 1-.92-1.394l3-7A1 1 0 0 1 7 7h10a1 1 0 0 1 .92.606z"}]],Zp=[["path",{d:"M10.293 2.293a1 1 0 0 1 1.414 0l2.5 2.5 5.994 1.227a1 1 0 0 1 .506 1.687l-7 7a1 1 0 0 1-1.687-.506l-1.227-5.994-2.5-2.5a1 1 0 0 1 0-1.414z"}],["path",{d:"m14.207 4.793-3.414 3.414"}],["path",{d:"M3 20a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1z"}],["path",{d:"m9.086 6.5-4.793 4.793a1 1 0 0 0-.18 1.17L7 18"}]],Gp=[["path",{d:"M12 10v12"}],["path",{d:"M17.929 7.629A1 1 0 0 1 17 9H7a1 1 0 0 1-.928-1.371l2-5A1 1 0 0 1 9 2h6a1 1 0 0 1 .928.629z"}],["path",{d:"M9 22h6"}]],Ip=[["path",{d:"M19.929 18.629A1 1 0 0 1 19 20H9a1 1 0 0 1-.928-1.371l2-5A1 1 0 0 1 11 13h6a1 1 0 0 1 .928.629z"}],["path",{d:"M6 3a2 2 0 0 1 2 2v2a2 2 0 0 1-2 2H5a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z"}],["path",{d:"M8 6h4a2 2 0 0 1 2 2v5"}]],Wp=[["path",{d:"M19.929 9.629A1 1 0 0 1 19 11H9a1 1 0 0 1-.928-1.371l2-5A1 1 0 0 1 11 4h6a1 1 0 0 1 .928.629z"}],["path",{d:"M6 15a2 2 0 0 1 2 2v2a2 2 0 0 1-2 2H5a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1z"}],["path",{d:"M8 18h4a2 2 0 0 0 2-2v-5"}]],Ep=[["path",{d:"M12 12v6"}],["path",{d:"M4.077 10.615A1 1 0 0 0 5 12h14a1 1 0 0 0 .923-1.385l-3.077-7.384A2 2 0 0 0 15 2H9a2 2 0 0 0-1.846 1.23Z"}],["path",{d:"M8 20a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1H9a1 1 0 0 1-1-1z"}]],Xp=[["path",{d:"m12 8 6-3-6-3v10"}],["path",{d:"m8 11.99-5.5 3.14a1 1 0 0 0 0 1.74l8.5 4.86a2 2 0 0 0 2 0l8.5-4.86a1 1 0 0 0 0-1.74L16 12"}],["path",{d:"m6.49 12.85 11.02 6.3"}],["path",{d:"M17.51 12.85 6.5 19.15"}]],jp=[["path",{d:"M10 18v-7"}],["path",{d:"M11.12 2.198a2 2 0 0 1 1.76.006l7.866 3.847c.476.233.31.949-.22.949H3.474c-.53 0-.695-.716-.22-.949z"}],["path",{d:"M14 18v-7"}],["path",{d:"M18 18v-7"}],["path",{d:"M3 22h18"}],["path",{d:"M6 18v-7"}]],Np=[["path",{d:"m5 8 6 6"}],["path",{d:"m4 14 6-6 2-3"}],["path",{d:"M2 5h12"}],["path",{d:"M7 2h1"}],["path",{d:"m22 22-5-10-5 10"}],["path",{d:"M14 18h6"}]],Kp=[["path",{d:"M2 20h20"}],["path",{d:"m9 10 2 2 4-4"}],["rect",{x:"3",y:"4",width:"18",height:"12",rx:"2"}]],p2=[["rect",{width:"18",height:"12",x:"3",y:"4",rx:"2",ry:"2"}],["line",{x1:"2",x2:"22",y1:"20",y2:"20"}]],Qp=[["path",{d:"M18 5a2 2 0 0 1 2 2v8.526a2 2 0 0 0 .212.897l1.068 2.127a1 1 0 0 1-.9 1.45H3.62a1 1 0 0 1-.9-1.45l1.068-2.127A2 2 0 0 0 4 15.526V7a2 2 0 0 1 2-2z"}],["path",{d:"M20.054 15.987H3.946"}]],Jp=[["path",{d:"M7 22a5 5 0 0 1-2-4"}],["path",{d:"M7 16.93c.96.43 1.96.74 2.99.91"}],["path",{d:"M3.34 14A6.8 6.8 0 0 1 2 10c0-4.42 4.48-8 10-8s10 3.58 10 8a7.19 7.19 0 0 1-.33 2"}],["path",{d:"M5 18a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"}],["path",{d:"M14.33 22h-.09a.35.35 0 0 1-.24-.32v-10a.34.34 0 0 1 .33-.34c.08 0 .15.03.21.08l7.34 6a.33.33 0 0 1-.21.59h-4.49l-2.57 3.85a.35.35 0 0 1-.28.14z"}]],Yp=[["path",{d:"M3.704 14.467A10 8 0 0 1 2 10a10 8 0 0 1 20 0 10 8 0 0 1-10 8 10 8 0 0 1-5.181-1.158"}],["path",{d:"M7 22a5 5 0 0 1-2-3.994"}],["circle",{cx:"5",cy:"16",r:"2"}]],_p=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M18 13a6 6 0 0 1-6 5 6 6 0 0 1-6-5h12Z"}],["line",{x1:"9",x2:"9.01",y1:"9",y2:"9"}],["line",{x1:"15",x2:"15.01",y1:"9",y2:"9"}]],xp=[["path",{d:"M13 13.74a2 2 0 0 1-2 0L2.5 8.87a1 1 0 0 1 0-1.74L11 2.26a2 2 0 0 1 2 0l8.5 4.87a1 1 0 0 1 0 1.74z"}],["path",{d:"m20 14.285 1.5.845a1 1 0 0 1 0 1.74L13 21.74a2 2 0 0 1-2 0l-8.5-4.87a1 1 0 0 1 0-1.74l1.5-.845"}]],ai=[["rect",{width:"7",height:"9",x:"3",y:"3",rx:"1"}],["rect",{width:"7",height:"5",x:"14",y:"3",rx:"1"}],["rect",{width:"7",height:"9",x:"14",y:"12",rx:"1"}],["rect",{width:"7",height:"5",x:"3",y:"16",rx:"1"}]],ti=[["rect",{width:"7",height:"7",x:"3",y:"3",rx:"1"}],["rect",{width:"7",height:"7",x:"14",y:"3",rx:"1"}],["rect",{width:"7",height:"7",x:"14",y:"14",rx:"1"}],["rect",{width:"7",height:"7",x:"3",y:"14",rx:"1"}]],i2=[["path",{d:"M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z"}],["path",{d:"M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12"}],["path",{d:"M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17"}]],hi=[["rect",{width:"7",height:"7",x:"3",y:"3",rx:"1"}],["rect",{width:"7",height:"7",x:"3",y:"14",rx:"1"}],["path",{d:"M14 4h7"}],["path",{d:"M14 9h7"}],["path",{d:"M14 15h7"}],["path",{d:"M14 20h7"}]],di=[["rect",{width:"7",height:"18",x:"3",y:"3",rx:"1"}],["rect",{width:"7",height:"7",x:"14",y:"3",rx:"1"}],["rect",{width:"7",height:"7",x:"14",y:"14",rx:"1"}]],ci=[["rect",{width:"18",height:"7",x:"3",y:"3",rx:"1"}],["rect",{width:"9",height:"7",x:"3",y:"14",rx:"1"}],["rect",{width:"5",height:"7",x:"16",y:"14",rx:"1"}]],Mi=[["rect",{width:"18",height:"7",x:"3",y:"3",rx:"1"}],["rect",{width:"7",height:"7",x:"3",y:"14",rx:"1"}],["rect",{width:"7",height:"7",x:"14",y:"14",rx:"1"}]],pi=[["path",{d:"M11 20A7 7 0 0 1 9.8 6.1C15.5 5 17 4.48 19 2c1 2 2 4.18 2 8 0 5.5-4.78 10-10 10Z"}],["path",{d:"M2 21c0-3 1.85-5.36 5.08-6C9.5 14.52 12 13 13 12"}]],ii=[["path",{d:"M2 22c1.25-.987 2.27-1.975 3.9-2.2a5.56 5.56 0 0 1 3.8 1.5 4 4 0 0 0 6.187-2.353 3.5 3.5 0 0 0 3.69-5.116A3.5 3.5 0 0 0 20.95 8 3.5 3.5 0 1 0 16 3.05a3.5 3.5 0 0 0-5.831 1.373 3.5 3.5 0 0 0-5.116 3.69 4 4 0 0 0-2.348 6.155C3.499 15.42 4.409 16.712 4.2 18.1 3.926 19.743 3.014 20.732 2 22"}],["path",{d:"M2 22 17 7"}]],ni=[["path",{d:"M16 12h3a2 2 0 0 0 1.902-1.38l1.056-3.333A1 1 0 0 0 21 6H3a1 1 0 0 0-.958 1.287l1.056 3.334A2 2 0 0 0 5 12h3"}],["path",{d:"M18 6V3a1 1 0 0 0-1-1h-3"}],["rect",{width:"8",height:"12",x:"8",y:"10",rx:"1"}]],li=[["rect",{width:"8",height:"18",x:"3",y:"3",rx:"1"}],["path",{d:"M7 3v18"}],["path",{d:"M20.4 18.9c.2.5-.1 1.1-.6 1.3l-1.9.7c-.5.2-1.1-.1-1.3-.6L11.1 5.1c-.2-.5.1-1.1.6-1.3l1.9-.7c.5-.2 1.1.1 1.3.6Z"}]],ei=[["path",{d:"m16 6 4 14"}],["path",{d:"M12 6v14"}],["path",{d:"M8 8v12"}],["path",{d:"M4 4v16"}]],ri=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"m4.93 4.93 4.24 4.24"}],["path",{d:"m14.83 9.17 4.24-4.24"}],["path",{d:"m14.83 14.83 4.24 4.24"}],["path",{d:"m9.17 14.83-4.24 4.24"}],["circle",{cx:"12",cy:"12",r:"4"}]],oi=[["path",{d:"M16.8 11.2c.8-.9 1.2-2 1.2-3.2a6 6 0 0 0-9.3-5"}],["path",{d:"m2 2 20 20"}],["path",{d:"M6.3 6.3a4.67 4.67 0 0 0 1.2 5.2c.7.7 1.3 1.5 1.5 2.5"}],["path",{d:"M9 18h6"}],["path",{d:"M10 22h4"}]],vi=[["path",{d:"M14 12h2v8"}],["path",{d:"M14 20h4"}],["path",{d:"M6 12h4"}],["path",{d:"M6 20h4"}],["path",{d:"M8 20V8a4 4 0 0 1 7.464-2"}]],$i=[["path",{d:"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"}],["path",{d:"M9 18h6"}],["path",{d:"M10 22h4"}]],mi=[["path",{d:"M7 3.5c5-2 7 2.5 3 4C1.5 10 2 15 5 16c5 2 9-10 14-7s.5 13.5-4 12c-5-2.5.5-11 6-2"}]],yi=[["path",{d:"M9 17H7A5 5 0 0 1 7 7"}],["path",{d:"M15 7h2a5 5 0 0 1 4 8"}],["line",{x1:"8",x2:"12",y1:"12",y2:"12"}],["line",{x1:"2",x2:"22",y1:"2",y2:"22"}]],si=[["path",{d:"M9 17H7A5 5 0 0 1 7 7h2"}],["path",{d:"M15 7h2a5 5 0 1 1 0 10h-2"}],["line",{x1:"8",x2:"16",y1:"12",y2:"12"}]],gi=[["path",{d:"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"}],["path",{d:"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"}]],ui=[["path",{d:"M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"}],["rect",{width:"4",height:"12",x:"2",y:"9"}],["circle",{cx:"4",cy:"4",r:"2"}]],Ci=[["path",{d:"M16 5H3"}],["path",{d:"M16 12H3"}],["path",{d:"M11 19H3"}],["path",{d:"m15 18 2 2 4-4"}]],Hi=[["path",{d:"M13 5h8"}],["path",{d:"M13 12h8"}],["path",{d:"M13 19h8"}],["path",{d:"m3 17 2 2 4-4"}],["path",{d:"m3 7 2 2 4-4"}]],Ai=[["path",{d:"M3 5h8"}],["path",{d:"M3 12h8"}],["path",{d:"M3 19h8"}],["path",{d:"m15 5 3 3 3-3"}],["path",{d:"m15 19 3-3 3 3"}]],wi=[["path",{d:"M3 5h8"}],["path",{d:"M3 12h8"}],["path",{d:"M3 19h8"}],["path",{d:"m15 8 3-3 3 3"}],["path",{d:"m15 16 3 3 3-3"}]],Vi=[["path",{d:"M10 5h11"}],["path",{d:"M10 12h11"}],["path",{d:"M10 19h11"}],["path",{d:"m3 10 3-3-3-3"}],["path",{d:"m3 20 3-3-3-3"}]],Si=[["path",{d:"M16 5H3"}],["path",{d:"M16 12H3"}],["path",{d:"M9 19H3"}],["path",{d:"m16 16-3 3 3 3"}],["path",{d:"M21 5v12a2 2 0 0 1-2 2h-6"}]],Li=[["path",{d:"M12 5H2"}],["path",{d:"M6 12h12"}],["path",{d:"M9 19h6"}],["path",{d:"M16 5h6"}],["path",{d:"M19 8V2"}]],fi=[["path",{d:"M2 5h20"}],["path",{d:"M6 12h12"}],["path",{d:"M9 19h6"}]],o=[["path",{d:"M21 5H11"}],["path",{d:"M21 12H11"}],["path",{d:"M21 19H11"}],["path",{d:"m7 8-4 4 4 4"}]],v=[["path",{d:"M21 5H11"}],["path",{d:"M21 12H11"}],["path",{d:"M21 19H11"}],["path",{d:"m3 8 4 4-4 4"}]],ki=[["path",{d:"M16 5H3"}],["path",{d:"M11 12H3"}],["path",{d:"M16 19H3"}],["path",{d:"M21 12h-6"}]],Pi=[["path",{d:"M16 5H3"}],["path",{d:"M11 12H3"}],["path",{d:"M11 19H3"}],["path",{d:"M21 16V5"}],["circle",{cx:"18",cy:"16",r:"3"}]],Bi=[["path",{d:"M11 5h10"}],["path",{d:"M11 12h10"}],["path",{d:"M11 19h10"}],["path",{d:"M4 4h1v5"}],["path",{d:"M4 9h2"}],["path",{d:"M6.5 20H3.4c0-1 2.6-1.925 2.6-3.5a1.5 1.5 0 0 0-2.6-1.02"}]],Di=[["path",{d:"M16 5H3"}],["path",{d:"M11 12H3"}],["path",{d:"M16 19H3"}],["path",{d:"M18 9v6"}],["path",{d:"M21 12h-6"}]],Fi=[["path",{d:"M21 5H3"}],["path",{d:"M7 12H3"}],["path",{d:"M7 19H3"}],["path",{d:"M12 18a5 5 0 0 0 9-3 4.5 4.5 0 0 0-4.5-4.5c-1.33 0-2.54.54-3.41 1.41L11 14"}],["path",{d:"M11 10v4h4"}]],zi=[["path",{d:"M3 5h6"}],["path",{d:"M3 12h13"}],["path",{d:"M3 19h13"}],["path",{d:"m16 8-3-3 3-3"}],["path",{d:"M21 19V7a2 2 0 0 0-2-2h-6"}]],bi=[["path",{d:"M13 5h8"}],["path",{d:"M13 12h8"}],["path",{d:"M13 19h8"}],["path",{d:"m3 17 2 2 4-4"}],["rect",{x:"3",y:"4",width:"6",height:"6",rx:"1"}]],Ri=[["path",{d:"M21 5H3"}],["path",{d:"M10 12H3"}],["path",{d:"M10 19H3"}],["path",{d:"M15 12.003a1 1 0 0 1 1.517-.859l4.997 2.997a1 1 0 0 1 0 1.718l-4.997 2.997a1 1 0 0 1-1.517-.86z"}]],Ti=[["path",{d:"M8 5h13"}],["path",{d:"M13 12h8"}],["path",{d:"M13 19h8"}],["path",{d:"M3 10a2 2 0 0 0 2 2h3"}],["path",{d:"M3 5v12a2 2 0 0 0 2 2h3"}]],qi=[["path",{d:"M3 5h.01"}],["path",{d:"M3 12h.01"}],["path",{d:"M3 19h.01"}],["path",{d:"M8 5h13"}],["path",{d:"M8 12h13"}],["path",{d:"M8 19h13"}]],Ui=[["path",{d:"M16 5H3"}],["path",{d:"M11 12H3"}],["path",{d:"M16 19H3"}],["path",{d:"m15.5 9.5 5 5"}],["path",{d:"m20.5 9.5-5 5"}]],n2=[["path",{d:"M21 12a9 9 0 1 1-6.219-8.56"}]],Oi=[["path",{d:"M22 12a1 1 0 0 1-10 0 1 1 0 0 0-10 0"}],["path",{d:"M7 20.7a1 1 0 1 1 5-8.7 1 1 0 1 0 5-8.6"}],["path",{d:"M7 3.3a1 1 0 1 1 5 8.6 1 1 0 1 0 5 8.6"}],["circle",{cx:"12",cy:"12",r:"10"}]],Zi=[["path",{d:"M12 2v4"}],["path",{d:"m16.2 7.8 2.9-2.9"}],["path",{d:"M18 12h4"}],["path",{d:"m16.2 16.2 2.9 2.9"}],["path",{d:"M12 18v4"}],["path",{d:"m4.9 19.1 2.9-2.9"}],["path",{d:"M2 12h4"}],["path",{d:"m4.9 4.9 2.9 2.9"}]],Gi=[["path",{d:"M12 19v3"}],["path",{d:"M12 2v3"}],["path",{d:"M18.89 13.24a7 7 0 0 0-8.13-8.13"}],["path",{d:"M19 12h3"}],["path",{d:"M2 12h3"}],["path",{d:"m2 2 20 20"}],["path",{d:"M7.05 7.05a7 7 0 0 0 9.9 9.9"}]],Ii=[["line",{x1:"2",x2:"5",y1:"12",y2:"12"}],["line",{x1:"19",x2:"22",y1:"12",y2:"12"}],["line",{x1:"12",x2:"12",y1:"2",y2:"5"}],["line",{x1:"12",x2:"12",y1:"19",y2:"22"}],["circle",{cx:"12",cy:"12",r:"7"}],["circle",{cx:"12",cy:"12",r:"3"}]],Wi=[["line",{x1:"2",x2:"5",y1:"12",y2:"12"}],["line",{x1:"19",x2:"22",y1:"12",y2:"12"}],["line",{x1:"12",x2:"12",y1:"2",y2:"5"}],["line",{x1:"12",x2:"12",y1:"19",y2:"22"}],["circle",{cx:"12",cy:"12",r:"7"}]],l2=[["circle",{cx:"12",cy:"16",r:"1"}],["rect",{width:"18",height:"12",x:"3",y:"10",rx:"2"}],["path",{d:"M7 10V7a5 5 0 0 1 9.33-2.5"}]],Ei=[["circle",{cx:"12",cy:"16",r:"1"}],["rect",{x:"3",y:"10",width:"18",height:"12",rx:"2"}],["path",{d:"M7 10V7a5 5 0 0 1 10 0v3"}]],e2=[["rect",{width:"18",height:"11",x:"3",y:"11",rx:"2",ry:"2"}],["path",{d:"M7 11V7a5 5 0 0 1 9.9-1"}]],Xi=[["rect",{width:"18",height:"11",x:"3",y:"11",rx:"2",ry:"2"}],["path",{d:"M7 11V7a5 5 0 0 1 10 0v4"}]],ji=[["path",{d:"m16 17 5-5-5-5"}],["path",{d:"M21 12H9"}],["path",{d:"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"}]],Ni=[["path",{d:"m10 17 5-5-5-5"}],["path",{d:"M15 12H3"}],["path",{d:"M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"}]],Ki=[["path",{d:"M3 5h1"}],["path",{d:"M3 12h1"}],["path",{d:"M3 19h1"}],["path",{d:"M8 5h1"}],["path",{d:"M8 12h1"}],["path",{d:"M8 19h1"}],["path",{d:"M13 5h8"}],["path",{d:"M13 12h8"}],["path",{d:"M13 19h8"}]],Qi=[["circle",{cx:"11",cy:"11",r:"8"}],["path",{d:"m21 21-4.3-4.3"}],["path",{d:"M11 11a2 2 0 0 0 4 0 4 4 0 0 0-8 0 6 6 0 0 0 12 0"}]],Ji=[["path",{d:"M6 20a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2"}],["path",{d:"M8 18V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v14"}],["path",{d:"M10 20h4"}],["circle",{cx:"16",cy:"20",r:"2"}],["circle",{cx:"8",cy:"20",r:"2"}]],Yi=[["path",{d:"m12 15 4 4"}],["path",{d:"M2.352 10.648a1.205 1.205 0 0 0 0 1.704l2.296 2.296a1.205 1.205 0 0 0 1.704 0l6.029-6.029a1 1 0 1 1 3 3l-6.029 6.029a1.205 1.205 0 0 0 0 1.704l2.296 2.296a1.205 1.205 0 0 0 1.704 0l6.365-6.367A1 1 0 0 0 8.716 4.282z"}],["path",{d:"m5 8 4 4"}]],_i=[["path",{d:"M22 13V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v12c0 1.1.9 2 2 2h8"}],["path",{d:"m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"}],["path",{d:"m16 19 2 2 4-4"}]],xi=[["path",{d:"M22 15V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v12c0 1.1.9 2 2 2h8"}],["path",{d:"m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"}],["path",{d:"M16 19h6"}]],an=[["path",{d:"M21.2 8.4c.5.38.8.97.8 1.6v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V10a2 2 0 0 1 .8-1.6l8-6a2 2 0 0 1 2.4 0l8 6Z"}],["path",{d:"m22 10-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 10"}]],tn=[["path",{d:"M22 13V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v12c0 1.1.9 2 2 2h8"}],["path",{d:"m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"}],["path",{d:"M19 16v6"}],["path",{d:"M16 19h6"}]],r2=[["path",{d:"M22 10.5V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v12c0 1.1.9 2 2 2h12.5"}],["path",{d:"m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"}],["path",{d:"M18 15.28c.2-.4.5-.8.9-1a2.1 2.1 0 0 1 2.6.4c.3.4.5.8.5 1.3 0 1.3-2 2-2 2"}],["path",{d:"M20 22v.01"}]],hn=[["path",{d:"M22 12.5V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v12c0 1.1.9 2 2 2h7.5"}],["path",{d:"m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"}],["path",{d:"M18 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z"}],["circle",{cx:"18",cy:"18",r:"3"}],["path",{d:"m22 22-1.5-1.5"}]],dn=[["path",{d:"M22 10.5V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v12c0 1.1.9 2 2 2h12.5"}],["path",{d:"m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"}],["path",{d:"M20 14v4"}],["path",{d:"M20 22v.01"}]],cn=[["path",{d:"M22 13V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v12c0 1.1.9 2 2 2h9"}],["path",{d:"m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"}],["path",{d:"m17 17 4 4"}],["path",{d:"m21 17-4 4"}]],Mn=[["path",{d:"m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7"}],["rect",{x:"2",y:"4",width:"20",height:"16",rx:"2"}]],pn=[["path",{d:"M22 17a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V9.5C2 7 4 5 6.5 5H18c2.2 0 4 1.8 4 4v8Z"}],["polyline",{points:"15,9 18,9 18,11"}],["path",{d:"M6.5 5C9 5 11 7 11 9.5V17a2 2 0 0 1-2 2"}],["line",{x1:"6",x2:"7",y1:"10",y2:"10"}]],nn=[["path",{d:"M17 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 1-1.732"}],["path",{d:"m22 5.5-6.419 4.179a2 2 0 0 1-2.162 0L7 5.5"}],["rect",{x:"7",y:"3",width:"15",height:"12",rx:"2"}]],ln=[["path",{d:"m11 19-1.106-.552a2 2 0 0 0-1.788 0l-3.659 1.83A1 1 0 0 1 3 19.381V6.618a1 1 0 0 1 .553-.894l4.553-2.277a2 2 0 0 1 1.788 0l4.212 2.106a2 2 0 0 0 1.788 0l3.659-1.83A1 1 0 0 1 21 4.619V14"}],["path",{d:"M15 5.764V14"}],["path",{d:"M21 18h-6"}],["path",{d:"M9 3.236v15"}]],en=[["path",{d:"M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"}],["path",{d:"m9 10 2 2 4-4"}]],rn=[["path",{d:"M19.43 12.935c.357-.967.57-1.955.57-2.935a8 8 0 0 0-16 0c0 4.993 5.539 10.193 7.399 11.799a1 1 0 0 0 1.202 0 32.197 32.197 0 0 0 .813-.728"}],["circle",{cx:"12",cy:"10",r:"3"}],["path",{d:"m16 18 2 2 4-4"}]],on=[["path",{d:"M15 22a1 1 0 0 1-1-1v-4a1 1 0 0 1 .445-.832l3-2a1 1 0 0 1 1.11 0l3 2A1 1 0 0 1 22 17v4a1 1 0 0 1-1 1z"}],["path",{d:"M18 10a8 8 0 0 0-16 0c0 4.993 5.539 10.193 7.399 11.799a1 1 0 0 0 .601.2"}],["path",{d:"M18 22v-3"}],["circle",{cx:"10",cy:"10",r:"3"}]],vn=[["path",{d:"M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"}],["path",{d:"M9 10h6"}]],$n=[["path",{d:"M18.977 14C19.6 12.701 20 11.343 20 10a8 8 0 0 0-16 0c0 4.993 5.539 10.193 7.399 11.799a1 1 0 0 0 1.202 0 32 32 0 0 0 .824-.738"}],["circle",{cx:"12",cy:"10",r:"3"}],["path",{d:"M16 18h6"}]],mn=[["path",{d:"M12.75 7.09a3 3 0 0 1 2.16 2.16"}],["path",{d:"M17.072 17.072c-1.634 2.17-3.527 3.912-4.471 4.727a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 1.432-4.568"}],["path",{d:"m2 2 20 20"}],["path",{d:"M8.475 2.818A8 8 0 0 1 20 10c0 1.183-.31 2.377-.81 3.533"}],["path",{d:"M9.13 9.13a3 3 0 0 0 3.74 3.74"}]],o2=[["path",{d:"M17.97 9.304A8 8 0 0 0 2 10c0 4.69 4.887 9.562 7.022 11.468"}],["path",{d:"M21.378 16.626a1 1 0 0 0-3.004-3.004l-4.01 4.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"}],["circle",{cx:"10",cy:"10",r:"3"}]],yn=[["path",{d:"M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"}],["path",{d:"M12 7v6"}],["path",{d:"M9 10h6"}]],sn=[["path",{d:"M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"}],["path",{d:"m14.5 7.5-5 5"}],["path",{d:"m9.5 7.5 5 5"}]],gn=[["path",{d:"M19.914 11.105A7.298 7.298 0 0 0 20 10a8 8 0 0 0-16 0c0 4.993 5.539 10.193 7.399 11.799a1 1 0 0 0 1.202 0 32 32 0 0 0 .824-.738"}],["circle",{cx:"12",cy:"10",r:"3"}],["path",{d:"M16 18h6"}],["path",{d:"M19 15v6"}]],un=[["path",{d:"M19.752 11.901A7.78 7.78 0 0 0 20 10a8 8 0 0 0-16 0c0 4.993 5.539 10.193 7.399 11.799a1 1 0 0 0 1.202 0 19 19 0 0 0 .09-.077"}],["circle",{cx:"12",cy:"10",r:"3"}],["path",{d:"m21.5 15.5-5 5"}],["path",{d:"m21.5 20.5-5-5"}]],Cn=[["path",{d:"M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"}],["circle",{cx:"12",cy:"10",r:"3"}]],Hn=[["path",{d:"M18 8c0 3.613-3.869 7.429-5.393 8.795a1 1 0 0 1-1.214 0C9.87 15.429 6 11.613 6 8a6 6 0 0 1 12 0"}],["circle",{cx:"12",cy:"8",r:"2"}],["path",{d:"M8.714 14h-3.71a1 1 0 0 0-.948.683l-2.004 6A1 1 0 0 0 3 22h18a1 1 0 0 0 .948-1.316l-2-6a1 1 0 0 0-.949-.684h-3.712"}]],An=[["path",{d:"m11 19-1.106-.552a2 2 0 0 0-1.788 0l-3.659 1.83A1 1 0 0 1 3 19.381V6.618a1 1 0 0 1 .553-.894l4.553-2.277a2 2 0 0 1 1.788 0l4.212 2.106a2 2 0 0 0 1.788 0l3.659-1.83A1 1 0 0 1 21 4.619V12"}],["path",{d:"M15 5.764V12"}],["path",{d:"M18 15v6"}],["path",{d:"M21 18h-6"}],["path",{d:"M9 3.236v15"}]],wn=[["path",{d:"M14.106 5.553a2 2 0 0 0 1.788 0l3.659-1.83A1 1 0 0 1 21 4.619v12.764a1 1 0 0 1-.553.894l-4.553 2.277a2 2 0 0 1-1.788 0l-4.212-2.106a2 2 0 0 0-1.788 0l-3.659 1.83A1 1 0 0 1 3 19.381V6.618a1 1 0 0 1 .553-.894l4.553-2.277a2 2 0 0 1 1.788 0z"}],["path",{d:"M15 5.764v15"}],["path",{d:"M9 3.236v15"}]],Vn=[["path",{d:"m14 6 4 4"}],["path",{d:"M17 3h4v4"}],["path",{d:"m21 3-7.75 7.75"}],["circle",{cx:"9",cy:"15",r:"6"}]],Sn=[["path",{d:"M16 3h5v5"}],["path",{d:"m21 3-6.75 6.75"}],["circle",{cx:"10",cy:"14",r:"6"}]],Ln=[["path",{d:"M8 22h8"}],["path",{d:"M12 11v11"}],["path",{d:"m19 3-7 8-7-8Z"}]],fn=[["path",{d:"M15 3h6v6"}],["path",{d:"m21 3-7 7"}],["path",{d:"m3 21 7-7"}],["path",{d:"M9 21H3v-6"}]],kn=[["path",{d:"M8 3H5a2 2 0 0 0-2 2v3"}],["path",{d:"M21 8V5a2 2 0 0 0-2-2h-3"}],["path",{d:"M3 16v3a2 2 0 0 0 2 2h3"}],["path",{d:"M16 21h3a2 2 0 0 0 2-2v-3"}]],Pn=[["path",{d:"M7.21 15 2.66 7.14a2 2 0 0 1 .13-2.2L4.4 2.8A2 2 0 0 1 6 2h12a2 2 0 0 1 1.6.8l1.6 2.14a2 2 0 0 1 .14 2.2L16.79 15"}],["path",{d:"M11 12 5.12 2.2"}],["path",{d:"m13 12 5.88-9.8"}],["path",{d:"M8 7h8"}],["circle",{cx:"12",cy:"17",r:"5"}],["path",{d:"M12 18v-2h-.5"}]],Bn=[["path",{d:"M11.636 6A13 13 0 0 0 19.4 3.2 1 1 0 0 1 21 4v11.344"}],["path",{d:"M14.378 14.357A13 13 0 0 0 11 14H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h1"}],["path",{d:"m2 2 20 20"}],["path",{d:"M6 14a12 12 0 0 0 2.4 7.2 2 2 0 0 0 3.2-2.4A8 8 0 0 1 10 14"}],["path",{d:"M8 8v6"}]],Dn=[["path",{d:"M11 6a13 13 0 0 0 8.4-2.8A1 1 0 0 1 21 4v12a1 1 0 0 1-1.6.8A13 13 0 0 0 11 14H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z"}],["path",{d:"M6 14a12 12 0 0 0 2.4 7.2 2 2 0 0 0 3.2-2.4A8 8 0 0 1 10 14"}],["path",{d:"M8 6v8"}]],Fn=[["circle",{cx:"12",cy:"12",r:"10"}],["line",{x1:"8",x2:"16",y1:"15",y2:"15"}],["line",{x1:"9",x2:"9.01",y1:"9",y2:"9"}],["line",{x1:"15",x2:"15.01",y1:"9",y2:"9"}]],zn=[["path",{d:"M6 19v-3"}],["path",{d:"M10 19v-3"}],["path",{d:"M14 19v-3"}],["path",{d:"M18 19v-3"}],["path",{d:"M8 11V9"}],["path",{d:"M16 11V9"}],["path",{d:"M12 11V9"}],["path",{d:"M2 15h20"}],["path",{d:"M2 7a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v1.1a2 2 0 0 0 0 3.837V17a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-5.1a2 2 0 0 0 0-3.837Z"}]],bn=[["path",{d:"M4 5h16"}],["path",{d:"M4 12h16"}],["path",{d:"M4 19h16"}]],Rn=[["path",{d:"m8 6 4-4 4 4"}],["path",{d:"M12 2v10.3a4 4 0 0 1-1.172 2.872L4 22"}],["path",{d:"m20 22-5-5"}]],Tn=[["path",{d:"m10 9-3 3 3 3"}],["path",{d:"m14 15 3-3-3-3"}],["path",{d:"M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"}]],qn=[["path",{d:"M10.1 2.182a10 10 0 0 1 3.8 0"}],["path",{d:"M13.9 21.818a10 10 0 0 1-3.8 0"}],["path",{d:"M17.609 3.72a10 10 0 0 1 2.69 2.7"}],["path",{d:"M2.182 13.9a10 10 0 0 1 0-3.8"}],["path",{d:"M20.28 17.61a10 10 0 0 1-2.7 2.69"}],["path",{d:"M21.818 10.1a10 10 0 0 1 0 3.8"}],["path",{d:"M3.721 6.391a10 10 0 0 1 2.7-2.69"}],["path",{d:"m6.163 21.117-2.906.85a1 1 0 0 1-1.236-1.169l.965-2.98"}]],Un=[["path",{d:"M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"}],["path",{d:"M7.828 13.07A3 3 0 0 1 12 8.764a3 3 0 0 1 5.004 2.224 3 3 0 0 1-.832 2.083l-3.447 3.62a1 1 0 0 1-1.45-.001z"}]],On=[["path",{d:"M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"}],["path",{d:"M8 12h.01"}],["path",{d:"M12 12h.01"}],["path",{d:"M16 12h.01"}]],Zn=[["path",{d:"m2 2 20 20"}],["path",{d:"M4.93 4.929a10 10 0 0 0-1.938 11.412 2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 0 0 11.302-1.989"}],["path",{d:"M8.35 2.69A10 10 0 0 1 21.3 15.65"}]],Gn=[["path",{d:"M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"}],["path",{d:"M8 12h8"}],["path",{d:"M12 8v8"}]],v2=[["path",{d:"M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"}],["path",{d:"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"}],["path",{d:"M12 17h.01"}]],In=[["path",{d:"M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"}],["path",{d:"m10 15-3-3 3-3"}],["path",{d:"M7 12h8a2 2 0 0 1 2 2v1"}]],Wn=[["path",{d:"M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"}],["path",{d:"M12 8v4"}],["path",{d:"M12 16h.01"}]],En=[["path",{d:"M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"}],["path",{d:"m15 9-6 6"}],["path",{d:"m9 9 6 6"}]],Xn=[["path",{d:"M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"}]],jn=[["path",{d:"M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"}],["path",{d:"m10 8-3 3 3 3"}],["path",{d:"m14 14 3-3-3-3"}]],Nn=[["path",{d:"M12 19h.01"}],["path",{d:"M12 3h.01"}],["path",{d:"M16 19h.01"}],["path",{d:"M16 3h.01"}],["path",{d:"M2 13h.01"}],["path",{d:"M2 17v4.286a.71.71 0 0 0 1.212.502l2.202-2.202A2 2 0 0 1 6.828 19H8"}],["path",{d:"M2 5a2 2 0 0 1 2-2"}],["path",{d:"M2 9h.01"}],["path",{d:"M20 3a2 2 0 0 1 2 2"}],["path",{d:"M22 13h.01"}],["path",{d:"M22 17a2 2 0 0 1-2 2"}],["path",{d:"M22 9h.01"}],["path",{d:"M8 3h.01"}]],Kn=[["path",{d:"M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"}],["path",{d:"M10 15h4"}],["path",{d:"M10 9h4"}],["path",{d:"M12 7v4"}]],Qn=[["path",{d:"M12.7 3H4a2 2 0 0 0-2 2v16.286a.71.71 0 0 0 1.212.502l2.202-2.202A2 2 0 0 1 6.828 19H20a2 2 0 0 0 2-2v-4.7"}],["circle",{cx:"19",cy:"6",r:"3"}]],Jn=[["path",{d:"M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"}],["path",{d:"M7.5 9.5c0 .687.265 1.383.697 1.844l3.009 3.264a1.14 1.14 0 0 0 .407.314 1 1 0 0 0 .783-.004 1.14 1.14 0 0 0 .398-.31l3.008-3.264A2.77 2.77 0 0 0 16.5 9.5 2.5 2.5 0 0 0 12 8a2.5 2.5 0 0 0-4.5 1.5"}]],Yn=[["path",{d:"M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"}],["path",{d:"M12 11h.01"}],["path",{d:"M16 11h.01"}],["path",{d:"M8 11h.01"}]],_n=[["path",{d:"M22 8.5V5a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v16.286a.71.71 0 0 0 1.212.502l2.202-2.202A2 2 0 0 1 6.828 19H10"}],["path",{d:"M20 15v-2a2 2 0 0 0-4 0v2"}],["rect",{x:"14",y:"15",width:"8",height:"5",rx:"1"}]],xn=[["path",{d:"M19 19H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.7.7 0 0 1 2 21.286V5a2 2 0 0 1 1.184-1.826"}],["path",{d:"m2 2 20 20"}],["path",{d:"M8.656 3H20a2 2 0 0 1 2 2v11.344"}]],al=[["path",{d:"M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"}],["path",{d:"M12 8v6"}],["path",{d:"M9 11h6"}]],tl=[["path",{d:"M14 14a2 2 0 0 0 2-2V8h-2"}],["path",{d:"M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"}],["path",{d:"M8 14a2 2 0 0 0 2-2V8H8"}]],hl=[["path",{d:"M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"}],["path",{d:"m10 8-3 3 3 3"}],["path",{d:"M17 14v-1a2 2 0 0 0-2-2H7"}]],dl=[["path",{d:"M12 3H4a2 2 0 0 0-2 2v16.286a.71.71 0 0 0 1.212.502l2.202-2.202A2 2 0 0 1 6.828 19H20a2 2 0 0 0 2-2v-4"}],["path",{d:"M16 3h6v6"}],["path",{d:"m16 9 6-6"}]],cl=[["path",{d:"M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"}],["path",{d:"M7 11h10"}],["path",{d:"M7 15h6"}],["path",{d:"M7 7h8"}]],Ml=[["path",{d:"M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"}],["path",{d:"M12 15h.01"}],["path",{d:"M12 7v4"}]],pl=[["path",{d:"M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"}],["path",{d:"m14.5 8.5-5 5"}],["path",{d:"m9.5 8.5 5 5"}]],il=[["path",{d:"M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"}]],nl=[["path",{d:"M16 10a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 14.286V4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"}],["path",{d:"M20 9a2 2 0 0 1 2 2v10.286a.71.71 0 0 1-1.212.502l-2.202-2.202A2 2 0 0 0 17.172 19H10a2 2 0 0 1-2-2v-1"}]],$2=[["path",{d:"m11 7.601-5.994 8.19a1 1 0 0 0 .1 1.298l.817.818a1 1 0 0 0 1.314.087L15.09 12"}],["path",{d:"M16.5 21.174C15.5 20.5 14.372 20 13 20c-2.058 0-3.928 2.356-6 2-2.072-.356-2.775-3.369-1.5-4.5"}],["circle",{cx:"16",cy:"7",r:"5"}]],ll=[["path",{d:"M12 19v3"}],["path",{d:"M15 9.34V5a3 3 0 0 0-5.68-1.33"}],["path",{d:"M16.95 16.95A7 7 0 0 1 5 12v-2"}],["path",{d:"M18.89 13.23A7 7 0 0 0 19 12v-2"}],["path",{d:"m2 2 20 20"}],["path",{d:"M9 9v3a3 3 0 0 0 5.12 2.12"}]],el=[["path",{d:"M12 19v3"}],["path",{d:"M19 10v2a7 7 0 0 1-14 0v-2"}],["rect",{x:"9",y:"2",width:"6",height:"13",rx:"3"}]],rl=[["path",{d:"M18 12h2"}],["path",{d:"M18 16h2"}],["path",{d:"M18 20h2"}],["path",{d:"M18 4h2"}],["path",{d:"M18 8h2"}],["path",{d:"M4 12h2"}],["path",{d:"M4 16h2"}],["path",{d:"M4 20h2"}],["path",{d:"M4 4h2"}],["path",{d:"M4 8h2"}],["path",{d:"M8 2a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2h-1.5c-.276 0-.494.227-.562.495a2 2 0 0 1-3.876 0C9.994 2.227 9.776 2 9.5 2z"}]],ol=[["path",{d:"M6 18h8"}],["path",{d:"M3 22h18"}],["path",{d:"M14 22a7 7 0 1 0 0-14h-1"}],["path",{d:"M9 14h2"}],["path",{d:"M9 12a2 2 0 0 1-2-2V6h6v4a2 2 0 0 1-2 2Z"}],["path",{d:"M12 6V3a1 1 0 0 0-1-1H9a1 1 0 0 0-1 1v3"}]],vl=[["rect",{width:"20",height:"15",x:"2",y:"4",rx:"2"}],["rect",{width:"8",height:"7",x:"6",y:"8",rx:"1"}],["path",{d:"M18 8v7"}],["path",{d:"M6 19v2"}],["path",{d:"M18 19v2"}]],$l=[["path",{d:"M12 13v8"}],["path",{d:"M12 3v3"}],["path",{d:"M4 6a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h13a2 2 0 0 0 1.152-.365l3.424-2.317a1 1 0 0 0 0-1.635l-3.424-2.318A2 2 0 0 0 17 6z"}]],ml=[["path",{d:"M8 2h8"}],["path",{d:"M9 2v1.343M15 2v2.789a4 4 0 0 0 .672 2.219l.656.984a4 4 0 0 1 .672 2.22v1.131M7.8 7.8l-.128.192A4 4 0 0 0 7 10.212V20a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2v-3"}],["path",{d:"M7 15a6.47 6.47 0 0 1 5 0 6.472 6.472 0 0 0 3.435.435"}],["line",{x1:"2",x2:"22",y1:"2",y2:"22"}]],yl=[["path",{d:"M8 2h8"}],["path",{d:"M9 2v2.789a4 4 0 0 1-.672 2.219l-.656.984A4 4 0 0 0 7 10.212V20a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2v-9.789a4 4 0 0 0-.672-2.219l-.656-.984A4 4 0 0 1 15 4.788V2"}],["path",{d:"M7 15a6.472 6.472 0 0 1 5 0 6.47 6.47 0 0 0 5 0"}]],sl=[["path",{d:"m14 10 7-7"}],["path",{d:"M20 10h-6V4"}],["path",{d:"m3 21 7-7"}],["path",{d:"M4 14h6v6"}]],gl=[["path",{d:"M8 3v3a2 2 0 0 1-2 2H3"}],["path",{d:"M21 8h-3a2 2 0 0 1-2-2V3"}],["path",{d:"M3 16h3a2 2 0 0 1 2 2v3"}],["path",{d:"M16 21v-3a2 2 0 0 1 2-2h3"}]],ul=[["path",{d:"M5 12h14"}]],Cl=[["path",{d:"m9 10 2 2 4-4"}],["rect",{width:"20",height:"14",x:"2",y:"3",rx:"2"}],["path",{d:"M12 17v4"}],["path",{d:"M8 21h8"}]],Hl=[["path",{d:"M12 17v4"}],["path",{d:"m14.305 7.53.923-.382"}],["path",{d:"m15.228 4.852-.923-.383"}],["path",{d:"m16.852 3.228-.383-.924"}],["path",{d:"m16.852 8.772-.383.923"}],["path",{d:"m19.148 3.228.383-.924"}],["path",{d:"m19.53 9.696-.382-.924"}],["path",{d:"m20.772 4.852.924-.383"}],["path",{d:"m20.772 7.148.924.383"}],["path",{d:"M22 13v2a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7"}],["path",{d:"M8 21h8"}],["circle",{cx:"18",cy:"6",r:"3"}]],Al=[["path",{d:"M12 17v4"}],["path",{d:"M22 12.307V15a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h8.693"}],["path",{d:"M8 21h8"}],["circle",{cx:"19",cy:"6",r:"3"}]],wl=[["path",{d:"M12 13V7"}],["path",{d:"m15 10-3 3-3-3"}],["rect",{width:"20",height:"14",x:"2",y:"3",rx:"2"}],["path",{d:"M12 17v4"}],["path",{d:"M8 21h8"}]],Vl=[["path",{d:"M17 17H4a2 2 0 0 1-2-2V5c0-1.5 1-2 1-2"}],["path",{d:"M22 15V5a2 2 0 0 0-2-2H9"}],["path",{d:"M8 21h8"}],["path",{d:"M12 17v4"}],["path",{d:"m2 2 20 20"}]],Sl=[["path",{d:"M10 13V7"}],["path",{d:"M14 13V7"}],["rect",{width:"20",height:"14",x:"2",y:"3",rx:"2"}],["path",{d:"M12 17v4"}],["path",{d:"M8 21h8"}]],Ll=[["path",{d:"M15.033 9.44a.647.647 0 0 1 0 1.12l-4.065 2.352a.645.645 0 0 1-.968-.56V7.648a.645.645 0 0 1 .967-.56z"}],["path",{d:"M12 17v4"}],["path",{d:"M8 21h8"}],["rect",{x:"2",y:"3",width:"20",height:"14",rx:"2"}]],fl=[["path",{d:"M18 8V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h8"}],["path",{d:"M10 19v-3.96 3.15"}],["path",{d:"M7 19h5"}],["rect",{width:"6",height:"10",x:"16",y:"12",rx:"2"}]],kl=[["path",{d:"M5.5 20H8"}],["path",{d:"M17 9h.01"}],["rect",{width:"10",height:"16",x:"12",y:"4",rx:"2"}],["path",{d:"M8 6H4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h4"}],["circle",{cx:"17",cy:"15",r:"1"}]],Pl=[["path",{d:"M12 17v4"}],["path",{d:"M8 21h8"}],["rect",{x:"2",y:"3",width:"20",height:"14",rx:"2"}],["rect",{x:"9",y:"7",width:"6",height:"6",rx:"1"}]],Bl=[["path",{d:"m9 10 3-3 3 3"}],["path",{d:"M12 13V7"}],["rect",{width:"20",height:"14",x:"2",y:"3",rx:"2"}],["path",{d:"M12 17v4"}],["path",{d:"M8 21h8"}]],Dl=[["path",{d:"m14.5 12.5-5-5"}],["path",{d:"m9.5 12.5 5-5"}],["rect",{width:"20",height:"14",x:"2",y:"3",rx:"2"}],["path",{d:"M12 17v4"}],["path",{d:"M8 21h8"}]],Fl=[["path",{d:"M18 5h4"}],["path",{d:"M20 3v4"}],["path",{d:"M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401"}]],zl=[["rect",{width:"20",height:"14",x:"2",y:"3",rx:"2"}],["line",{x1:"8",x2:"16",y1:"21",y2:"21"}],["line",{x1:"12",x2:"12",y1:"17",y2:"21"}]],bl=[["path",{d:"M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401"}]],Rl=[["path",{d:"m18 14-1-3"}],["path",{d:"m3 9 6 2a2 2 0 0 1 2-2h2a2 2 0 0 1 1.99 1.81"}],["path",{d:"M8 17h3a1 1 0 0 0 1-1 6 6 0 0 1 6-6 1 1 0 0 0 1-1v-.75A5 5 0 0 0 17 5"}],["circle",{cx:"19",cy:"17",r:"3"}],["circle",{cx:"5",cy:"17",r:"3"}]],Tl=[["path",{d:"m8 3 4 8 5-5 5 15H2L8 3z"}],["path",{d:"M4.14 15.08c2.62-1.57 5.24-1.43 7.86.42 2.74 1.94 5.49 2 8.23.19"}]],ql=[["path",{d:"m8 3 4 8 5-5 5 15H2L8 3z"}]],Ul=[["path",{d:"M12 6v.343"}],["path",{d:"M18.218 18.218A7 7 0 0 1 5 15V9a7 7 0 0 1 .782-3.218"}],["path",{d:"M19 13.343V9A7 7 0 0 0 8.56 2.902"}],["path",{d:"M22 22 2 2"}]],Ol=[["path",{d:"M4.037 4.688a.495.495 0 0 1 .651-.651l16 6.5a.5.5 0 0 1-.063.947l-6.124 1.58a2 2 0 0 0-1.438 1.435l-1.579 6.126a.5.5 0 0 1-.947.063z"}]],Zl=[["path",{d:"M2.034 2.681a.498.498 0 0 1 .647-.647l9 3.5a.5.5 0 0 1-.033.944L8.204 7.545a1 1 0 0 0-.66.66l-1.066 3.443a.5.5 0 0 1-.944.033z"}],["circle",{cx:"16",cy:"16",r:"6"}],["path",{d:"m11.8 11.8 8.4 8.4"}]],Gl=[["path",{d:"M14 4.1 12 6"}],["path",{d:"m5.1 8-2.9-.8"}],["path",{d:"m6 12-1.9 2"}],["path",{d:"M7.2 2.2 8 5.1"}],["path",{d:"M9.037 9.69a.498.498 0 0 1 .653-.653l11 4.5a.5.5 0 0 1-.074.949l-4.349 1.041a1 1 0 0 0-.74.739l-1.04 4.35a.5.5 0 0 1-.95.074z"}]],Il=[["path",{d:"M12.586 12.586 19 19"}],["path",{d:"M3.688 3.037a.497.497 0 0 0-.651.651l6.5 15.999a.501.501 0 0 0 .947-.062l1.569-6.083a2 2 0 0 1 1.448-1.479l6.124-1.579a.5.5 0 0 0 .063-.947z"}]],Wl=[["rect",{x:"5",y:"2",width:"14",height:"20",rx:"7"}],["path",{d:"M12 6v4"}]],m2=[["path",{d:"M5 3v16h16"}],["path",{d:"m5 19 6-6"}],["path",{d:"m2 6 3-3 3 3"}],["path",{d:"m18 16 3 3-3 3"}]],El=[["path",{d:"M19 13v6h-6"}],["path",{d:"M5 11V5h6"}],["path",{d:"m5 5 14 14"}]],Xl=[["path",{d:"M11 19H5v-6"}],["path",{d:"M13 5h6v6"}],["path",{d:"M19 5 5 19"}]],jl=[["path",{d:"M11 19H5V13"}],["path",{d:"M19 5L5 19"}]],Nl=[["path",{d:"M19 13V19H13"}],["path",{d:"M5 5L19 19"}]],Kl=[["path",{d:"M8 18L12 22L16 18"}],["path",{d:"M12 2V22"}]],Ql=[["path",{d:"m18 8 4 4-4 4"}],["path",{d:"M2 12h20"}],["path",{d:"m6 8-4 4 4 4"}]],Jl=[["path",{d:"M6 8L2 12L6 16"}],["path",{d:"M2 12H22"}]],Yl=[["path",{d:"M18 8L22 12L18 16"}],["path",{d:"M2 12H22"}]],_l=[["path",{d:"M13 5H19V11"}],["path",{d:"M19 5L5 19"}]],xl=[["path",{d:"M5 11V5H11"}],["path",{d:"M5 5L19 19"}]],ae=[["path",{d:"M8 6L12 2L16 6"}],["path",{d:"M12 2V22"}]],te=[["path",{d:"M12 2v20"}],["path",{d:"m8 18 4 4 4-4"}],["path",{d:"m8 6 4-4 4 4"}]],he=[["path",{d:"M12 2v20"}],["path",{d:"m15 19-3 3-3-3"}],["path",{d:"m19 9 3 3-3 3"}],["path",{d:"M2 12h20"}],["path",{d:"m5 9-3 3 3 3"}],["path",{d:"m9 5 3-3 3 3"}]],de=[["circle",{cx:"8",cy:"18",r:"4"}],["path",{d:"M12 18V2l7 4"}]],ce=[["circle",{cx:"12",cy:"18",r:"4"}],["path",{d:"M16 18V2"}]],Me=[["path",{d:"M9 18V5l12-2v13"}],["path",{d:"m9 9 12-2"}],["circle",{cx:"6",cy:"18",r:"3"}],["circle",{cx:"18",cy:"16",r:"3"}]],pe=[["path",{d:"M9 18V5l12-2v13"}],["circle",{cx:"6",cy:"18",r:"3"}],["circle",{cx:"18",cy:"16",r:"3"}]],ie=[["path",{d:"M9.31 9.31 5 21l7-4 7 4-1.17-3.17"}],["path",{d:"M14.53 8.88 12 2l-1.17 3.17"}],["line",{x1:"2",x2:"22",y1:"2",y2:"22"}]],ne=[["polygon",{points:"12 2 19 21 12 17 5 21 12 2"}]],le=[["path",{d:"M8.43 8.43 3 11l8 2 2 8 2.57-5.43"}],["path",{d:"M17.39 11.73 22 2l-9.73 4.61"}],["line",{x1:"2",x2:"22",y1:"2",y2:"22"}]],ee=[["polygon",{points:"3 11 22 2 13 21 11 13 3 11"}]],re=[["rect",{x:"16",y:"16",width:"6",height:"6",rx:"1"}],["rect",{x:"2",y:"16",width:"6",height:"6",rx:"1"}],["rect",{x:"9",y:"2",width:"6",height:"6",rx:"1"}],["path",{d:"M5 16v-3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3"}],["path",{d:"M12 12V8"}]],oe=[["path",{d:"M15 18h-5"}],["path",{d:"M18 14h-8"}],["path",{d:"M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-4 0v-9a2 2 0 0 1 2-2h2"}],["rect",{width:"8",height:"4",x:"10",y:"6",rx:"1"}]],ve=[["path",{d:"M6 8.32a7.43 7.43 0 0 1 0 7.36"}],["path",{d:"M9.46 6.21a11.76 11.76 0 0 1 0 11.58"}],["path",{d:"M12.91 4.1a15.91 15.91 0 0 1 .01 15.8"}],["path",{d:"M16.37 2a20.16 20.16 0 0 1 0 20"}]],$e=[["path",{d:"M12 2v10"}],["path",{d:"m8.5 4 7 4"}],["path",{d:"m8.5 8 7-4"}],["circle",{cx:"12",cy:"17",r:"5"}]],me=[["path",{d:"M13.4 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-7.4"}],["path",{d:"M2 6h4"}],["path",{d:"M2 10h4"}],["path",{d:"M2 14h4"}],["path",{d:"M2 18h4"}],["path",{d:"M21.378 5.626a1 1 0 1 0-3.004-3.004l-5.01 5.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"}]],ye=[["path",{d:"M2 6h4"}],["path",{d:"M2 10h4"}],["path",{d:"M2 14h4"}],["path",{d:"M2 18h4"}],["rect",{width:"16",height:"20",x:"4",y:"2",rx:"2"}],["path",{d:"M15 2v20"}],["path",{d:"M15 7h5"}],["path",{d:"M15 12h5"}],["path",{d:"M15 17h5"}]],se=[["path",{d:"M2 6h4"}],["path",{d:"M2 10h4"}],["path",{d:"M2 14h4"}],["path",{d:"M2 18h4"}],["rect",{width:"16",height:"20",x:"4",y:"2",rx:"2"}],["path",{d:"M9.5 8h5"}],["path",{d:"M9.5 12H16"}],["path",{d:"M9.5 16H14"}]],ge=[["path",{d:"M2 6h4"}],["path",{d:"M2 10h4"}],["path",{d:"M2 14h4"}],["path",{d:"M2 18h4"}],["rect",{width:"16",height:"20",x:"4",y:"2",rx:"2"}],["path",{d:"M16 2v20"}]],ue=[["path",{d:"M8 2v4"}],["path",{d:"M12 2v4"}],["path",{d:"M16 2v4"}],["path",{d:"M16 4h2a2 2 0 0 1 2 2v2"}],["path",{d:"M20 12v2"}],["path",{d:"M20 18v2a2 2 0 0 1-2 2h-1"}],["path",{d:"M13 22h-2"}],["path",{d:"M7 22H6a2 2 0 0 1-2-2v-2"}],["path",{d:"M4 14v-2"}],["path",{d:"M4 8V6a2 2 0 0 1 2-2h2"}],["path",{d:"M8 10h6"}],["path",{d:"M8 14h8"}],["path",{d:"M8 18h5"}]],Ce=[["path",{d:"M8 2v4"}],["path",{d:"M12 2v4"}],["path",{d:"M16 2v4"}],["rect",{width:"16",height:"18",x:"4",y:"4",rx:"2"}],["path",{d:"M8 10h6"}],["path",{d:"M8 14h8"}],["path",{d:"M8 18h5"}]],He=[["path",{d:"M12 4V2"}],["path",{d:"M5 10v4a7.004 7.004 0 0 0 5.277 6.787c.412.104.802.292 1.102.592L12 22l.621-.621c.3-.3.69-.488 1.102-.592a7.01 7.01 0 0 0 4.125-2.939"}],["path",{d:"M19 10v3.343"}],["path",{d:"M12 12c-1.349-.573-1.905-1.005-2.5-2-.546.902-1.048 1.353-2.5 2-1.018-.644-1.46-1.08-2-2-1.028.71-1.69.918-3 1 1.081-1.048 1.757-2.03 2-3 .194-.776.84-1.551 1.79-2.21m11.654 5.997c.887-.457 1.28-.891 1.556-1.787 1.032.916 1.683 1.157 3 1-1.297-1.036-1.758-2.03-2-3-.5-2-4-4-8-4-.74 0-1.461.068-2.15.192"}],["line",{x1:"2",x2:"22",y1:"2",y2:"22"}]],Ae=[["path",{d:"M12 4V2"}],["path",{d:"M5 10v4a7.004 7.004 0 0 0 5.277 6.787c.412.104.802.292 1.102.592L12 22l.621-.621c.3-.3.69-.488 1.102-.592A7.003 7.003 0 0 0 19 14v-4"}],["path",{d:"M12 4C8 4 4.5 6 4 8c-.243.97-.919 1.952-2 3 1.31-.082 1.972-.29 3-1 .54.92.982 1.356 2 2 1.452-.647 1.954-1.098 2.5-2 .595.995 1.151 1.427 2.5 2 1.31-.621 1.862-1.058 2.5-2 .629.977 1.162 1.423 2.5 2 1.209-.548 1.68-.967 2-2 1.032.916 1.683 1.157 3 1-1.297-1.036-1.758-2.03-2-3-.5-2-4-4-8-4Z"}]],y2=[["path",{d:"M12 16h.01"}],["path",{d:"M12 8v4"}],["path",{d:"M15.312 2a2 2 0 0 1 1.414.586l4.688 4.688A2 2 0 0 1 22 8.688v6.624a2 2 0 0 1-.586 1.414l-4.688 4.688a2 2 0 0 1-1.414.586H8.688a2 2 0 0 1-1.414-.586l-4.688-4.688A2 2 0 0 1 2 15.312V8.688a2 2 0 0 1 .586-1.414l4.688-4.688A2 2 0 0 1 8.688 2z"}]],we=[["path",{d:"M2.586 16.726A2 2 0 0 1 2 15.312V8.688a2 2 0 0 1 .586-1.414l4.688-4.688A2 2 0 0 1 8.688 2h6.624a2 2 0 0 1 1.414.586l4.688 4.688A2 2 0 0 1 22 8.688v6.624a2 2 0 0 1-.586 1.414l-4.688 4.688a2 2 0 0 1-1.414.586H8.688a2 2 0 0 1-1.414-.586z"}],["path",{d:"M8 12h8"}]],s2=[["path",{d:"M10 15V9"}],["path",{d:"M14 15V9"}],["path",{d:"M2.586 16.726A2 2 0 0 1 2 15.312V8.688a2 2 0 0 1 .586-1.414l4.688-4.688A2 2 0 0 1 8.688 2h6.624a2 2 0 0 1 1.414.586l4.688 4.688A2 2 0 0 1 22 8.688v6.624a2 2 0 0 1-.586 1.414l-4.688 4.688a2 2 0 0 1-1.414.586H8.688a2 2 0 0 1-1.414-.586z"}]],g2=[["path",{d:"m15 9-6 6"}],["path",{d:"M2.586 16.726A2 2 0 0 1 2 15.312V8.688a2 2 0 0 1 .586-1.414l4.688-4.688A2 2 0 0 1 8.688 2h6.624a2 2 0 0 1 1.414.586l4.688 4.688A2 2 0 0 1 22 8.688v6.624a2 2 0 0 1-.586 1.414l-4.688 4.688a2 2 0 0 1-1.414.586H8.688a2 2 0 0 1-1.414-.586z"}],["path",{d:"m9 9 6 6"}]],Ve=[["path",{d:"M2.586 16.726A2 2 0 0 1 2 15.312V8.688a2 2 0 0 1 .586-1.414l4.688-4.688A2 2 0 0 1 8.688 2h6.624a2 2 0 0 1 1.414.586l4.688 4.688A2 2 0 0 1 22 8.688v6.624a2 2 0 0 1-.586 1.414l-4.688 4.688a2 2 0 0 1-1.414.586H8.688a2 2 0 0 1-1.414-.586z"}]],Se=[["path",{d:"M3 20h4.5a.5.5 0 0 0 .5-.5v-.282a.52.52 0 0 0-.247-.437 8 8 0 1 1 8.494-.001.52.52 0 0 0-.247.438v.282a.5.5 0 0 0 .5.5H21"}]],Le=[["path",{d:"M20.341 6.484A10 10 0 0 1 10.266 21.85"}],["path",{d:"M3.659 17.516A10 10 0 0 1 13.74 2.152"}],["circle",{cx:"12",cy:"12",r:"3"}],["circle",{cx:"19",cy:"5",r:"2"}],["circle",{cx:"5",cy:"19",r:"2"}]],fe=[["path",{d:"M3 3h6l6 18h6"}],["path",{d:"M14 3h7"}]],ke=[["path",{d:"M12 12V4a1 1 0 0 1 1-1h6.297a1 1 0 0 1 .651 1.759l-4.696 4.025"}],["path",{d:"m12 21-7.414-7.414A2 2 0 0 1 4 12.172V6.415a1.002 1.002 0 0 1 1.707-.707L20 20.009"}],["path",{d:"m12.214 3.381 8.414 14.966a1 1 0 0 1-.167 1.199l-1.168 1.163a1 1 0 0 1-.706.291H6.351a1 1 0 0 1-.625-.219L3.25 18.8a1 1 0 0 1 .631-1.781l4.165.027"}]],Pe=[["path",{d:"M12 3v6"}],["path",{d:"M16.76 3a2 2 0 0 1 1.8 1.1l2.23 4.479a2 2 0 0 1 .21.891V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V9.472a2 2 0 0 1 .211-.894L5.45 4.1A2 2 0 0 1 7.24 3z"}],["path",{d:"M3.054 9.013h17.893"}]],Be=[["path",{d:"m16 16 2 2 4-4"}],["path",{d:"M21 10V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l2-1.14"}],["path",{d:"m7.5 4.27 9 5.15"}],["polyline",{points:"3.29 7 12 12 20.71 7"}],["line",{x1:"12",x2:"12",y1:"22",y2:"12"}]],De=[["path",{d:"M16 16h6"}],["path",{d:"M21 10V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l2-1.14"}],["path",{d:"m7.5 4.27 9 5.15"}],["polyline",{points:"3.29 7 12 12 20.71 7"}],["line",{x1:"12",x2:"12",y1:"22",y2:"12"}]],Fe=[["path",{d:"M16 16h6"}],["path",{d:"M19 13v6"}],["path",{d:"M21 10V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l2-1.14"}],["path",{d:"m7.5 4.27 9 5.15"}],["polyline",{points:"3.29 7 12 12 20.71 7"}],["line",{x1:"12",x2:"12",y1:"22",y2:"12"}]],ze=[["path",{d:"M12 22v-9"}],["path",{d:"M15.17 2.21a1.67 1.67 0 0 1 1.63 0L21 4.57a1.93 1.93 0 0 1 0 3.36L8.82 14.79a1.655 1.655 0 0 1-1.64 0L3 12.43a1.93 1.93 0 0 1 0-3.36z"}],["path",{d:"M20 13v3.87a2.06 2.06 0 0 1-1.11 1.83l-6 3.08a1.93 1.93 0 0 1-1.78 0l-6-3.08A2.06 2.06 0 0 1 4 16.87V13"}],["path",{d:"M21 12.43a1.93 1.93 0 0 0 0-3.36L8.83 2.2a1.64 1.64 0 0 0-1.63 0L3 4.57a1.93 1.93 0 0 0 0 3.36l12.18 6.86a1.636 1.636 0 0 0 1.63 0z"}]],be=[["path",{d:"M21 10V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l2-1.14"}],["path",{d:"m7.5 4.27 9 5.15"}],["polyline",{points:"3.29 7 12 12 20.71 7"}],["line",{x1:"12",x2:"12",y1:"22",y2:"12"}],["circle",{cx:"18.5",cy:"15.5",r:"2.5"}],["path",{d:"M20.27 17.27 22 19"}]],Re=[["path",{d:"M21 10V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l2-1.14"}],["path",{d:"m7.5 4.27 9 5.15"}],["polyline",{points:"3.29 7 12 12 20.71 7"}],["line",{x1:"12",x2:"12",y1:"22",y2:"12"}],["path",{d:"m17 13 5 5m-5 0 5-5"}]],Te=[["path",{d:"M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z"}],["path",{d:"M12 22V12"}],["polyline",{points:"3.29 7 12 12 20.71 7"}],["path",{d:"m7.5 4.27 9 5.15"}]],qe=[["path",{d:"m19 11-8-8-8.6 8.6a2 2 0 0 0 0 2.8l5.2 5.2c.8.8 2 .8 2.8 0L19 11Z"}],["path",{d:"m5 2 5 5"}],["path",{d:"M2 13h15"}],["path",{d:"M22 20a2 2 0 1 1-4 0c0-1.6 1.7-2.4 2-4 .3 1.6 2 2.4 2 4Z"}]],Ue=[["rect",{width:"16",height:"6",x:"2",y:"2",rx:"2"}],["path",{d:"M10 16v-2a2 2 0 0 1 2-2h8a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2"}],["rect",{width:"4",height:"6",x:"8",y:"16",rx:"1"}]],u2=[["path",{d:"M10 2v2"}],["path",{d:"M14 2v4"}],["path",{d:"M17 2a1 1 0 0 1 1 1v9H6V3a1 1 0 0 1 1-1z"}],["path",{d:"M6 12a1 1 0 0 0-1 1v1a2 2 0 0 0 2 2h2a1 1 0 0 1 1 1v2.9a2 2 0 1 0 4 0V17a1 1 0 0 1 1-1h2a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1"}]],Oe=[["path",{d:"m14.622 17.897-10.68-2.913"}],["path",{d:"M18.376 2.622a1 1 0 1 1 3.002 3.002L17.36 9.643a.5.5 0 0 0 0 .707l.944.944a2.41 2.41 0 0 1 0 3.408l-.944.944a.5.5 0 0 1-.707 0L8.354 7.348a.5.5 0 0 1 0-.707l.944-.944a2.41 2.41 0 0 1 3.408 0l.944.944a.5.5 0 0 0 .707 0z"}],["path",{d:"M9 8c-1.804 2.71-3.97 3.46-6.583 3.948a.507.507 0 0 0-.302.819l7.32 8.883a1 1 0 0 0 1.185.204C12.735 20.405 16 16.792 16 15"}]],Ze=[["path",{d:"M12 22a1 1 0 0 1 0-20 10 9 0 0 1 10 9 5 5 0 0 1-5 5h-2.25a1.75 1.75 0 0 0-1.4 2.8l.3.4a1.75 1.75 0 0 1-1.4 2.8z"}],["circle",{cx:"13.5",cy:"6.5",r:".5",fill:"currentColor"}],["circle",{cx:"17.5",cy:"10.5",r:".5",fill:"currentColor"}],["circle",{cx:"6.5",cy:"12.5",r:".5",fill:"currentColor"}],["circle",{cx:"8.5",cy:"7.5",r:".5",fill:"currentColor"}]],Ge=[["path",{d:"M11.25 17.25h1.5L12 18z"}],["path",{d:"m15 12 2 2"}],["path",{d:"M18 6.5a.5.5 0 0 0-.5-.5"}],["path",{d:"M20.69 9.67a4.5 4.5 0 1 0-7.04-5.5 8.35 8.35 0 0 0-3.3 0 4.5 4.5 0 1 0-7.04 5.5C2.49 11.2 2 12.88 2 14.5 2 19.47 6.48 22 12 22s10-2.53 10-7.5c0-1.62-.48-3.3-1.3-4.83"}],["path",{d:"M6 6.5a.495.495 0 0 1 .5-.5"}],["path",{d:"m9 12-2 2"}]],C2=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M14 15h1"}],["path",{d:"M19 15h2"}],["path",{d:"M3 15h2"}],["path",{d:"M9 15h1"}]],Ie=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M3 15h18"}],["path",{d:"m15 8-3 3-3-3"}]],We=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M3 15h18"}],["path",{d:"m9 10 3-3 3 3"}]],Ee=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M3 15h18"}]],H2=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M9 3v18"}],["path",{d:"m16 15-3-3 3-3"}]],A2=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M9 14v1"}],["path",{d:"M9 19v2"}],["path",{d:"M9 3v2"}],["path",{d:"M9 9v1"}]],w2=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M9 3v18"}],["path",{d:"m14 9 3 3-3 3"}]],Xe=[["path",{d:"M15 10V9"}],["path",{d:"M15 15v-1"}],["path",{d:"M15 21v-2"}],["path",{d:"M15 5V3"}],["path",{d:"M9 10V9"}],["path",{d:"M9 15v-1"}],["path",{d:"M9 21v-2"}],["path",{d:"M9 5V3"}],["rect",{x:"3",y:"3",width:"18",height:"18",rx:"2"}]],V2=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M9 3v18"}]],je=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M15 3v18"}],["path",{d:"m8 9 3 3-3 3"}]],S2=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M15 14v1"}],["path",{d:"M15 19v2"}],["path",{d:"M15 3v2"}],["path",{d:"M15 9v1"}]],Ne=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M15 3v18"}],["path",{d:"m10 15-3-3 3-3"}]],Ke=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M15 3v18"}]],Qe=[["path",{d:"M14 15h1"}],["path",{d:"M14 9h1"}],["path",{d:"M19 15h2"}],["path",{d:"M19 9h2"}],["path",{d:"M3 15h2"}],["path",{d:"M3 9h2"}],["path",{d:"M9 15h1"}],["path",{d:"M9 9h1"}],["rect",{x:"3",y:"3",width:"18",height:"18",rx:"2"}]],Je=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M3 9h18"}],["path",{d:"m9 16 3-3 3 3"}]],L2=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M14 9h1"}],["path",{d:"M19 9h2"}],["path",{d:"M3 9h2"}],["path",{d:"M9 9h1"}]],Ye=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M3 9h18"}],["path",{d:"m15 14-3 3-3-3"}]],_e=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M3 9h18"}]],xe=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M9 3v18"}],["path",{d:"M9 15h12"}]],ar=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M3 15h12"}],["path",{d:"M15 3v18"}]],f2=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M3 9h18"}],["path",{d:"M9 21V9"}]],tr=[["path",{d:"M8 21s-4-3-4-9 4-9 4-9"}],["path",{d:"M16 3s4 3 4 9-4 9-4 9"}]],hr=[["path",{d:"m16 6-8.414 8.586a2 2 0 0 0 2.829 2.829l8.414-8.586a4 4 0 1 0-5.657-5.657l-8.379 8.551a6 6 0 1 0 8.485 8.485l8.379-8.551"}]],dr=[["path",{d:"M11 15h2"}],["path",{d:"M12 12v3"}],["path",{d:"M12 19v3"}],["path",{d:"M15.282 19a1 1 0 0 0 .948-.68l2.37-6.988a7 7 0 1 0-13.2 0l2.37 6.988a1 1 0 0 0 .948.68z"}],["path",{d:"M9 9a3 3 0 1 1 6 0"}]],cr=[["path",{d:"M5.8 11.3 2 22l10.7-3.79"}],["path",{d:"M4 3h.01"}],["path",{d:"M22 8h.01"}],["path",{d:"M15 2h.01"}],["path",{d:"M22 20h.01"}],["path",{d:"m22 2-2.24.75a2.9 2.9 0 0 0-1.96 3.12c.1.86-.57 1.63-1.45 1.63h-.38c-.86 0-1.6.6-1.76 1.44L14 10"}],["path",{d:"m22 13-.82-.33c-.86-.34-1.82.2-1.98 1.11c-.11.7-.72 1.22-1.43 1.22H17"}],["path",{d:"m11 2 .33.82c.34.86-.2 1.82-1.11 1.98C9.52 4.9 9 5.52 9 6.23V7"}],["path",{d:"M11 13c1.93 1.93 2.83 4.17 2 5-.83.83-3.07-.07-5-2-1.93-1.93-2.83-4.17-2-5 .83-.83 3.07.07 5 2Z"}]],Mr=[["rect",{x:"14",y:"3",width:"5",height:"18",rx:"1"}],["rect",{x:"5",y:"3",width:"5",height:"18",rx:"1"}]],pr=[["circle",{cx:"11",cy:"4",r:"2"}],["circle",{cx:"18",cy:"8",r:"2"}],["circle",{cx:"20",cy:"16",r:"2"}],["path",{d:"M9 10a5 5 0 0 1 5 5v3.5a3.5 3.5 0 0 1-6.84 1.045Q6.52 17.48 4.46 16.84A3.5 3.5 0 0 1 5.5 10Z"}]],k2=[["path",{d:"M13 21h8"}],["path",{d:"M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"}]],ir=[["path",{d:"m10 10-6.157 6.162a2 2 0 0 0-.5.833l-1.322 4.36a.5.5 0 0 0 .622.624l4.358-1.323a2 2 0 0 0 .83-.5L14 13.982"}],["path",{d:"m12.829 7.172 4.359-4.346a1 1 0 1 1 3.986 3.986l-4.353 4.353"}],["path",{d:"m2 2 20 20"}]],nr=[["rect",{width:"14",height:"20",x:"5",y:"2",rx:"2"}],["path",{d:"M15 14h.01"}],["path",{d:"M9 6h6"}],["path",{d:"M9 10h6"}]],lr=[["path",{d:"M15.707 21.293a1 1 0 0 1-1.414 0l-1.586-1.586a1 1 0 0 1 0-1.414l5.586-5.586a1 1 0 0 1 1.414 0l1.586 1.586a1 1 0 0 1 0 1.414z"}],["path",{d:"m18 13-1.375-6.874a1 1 0 0 0-.746-.776L3.235 2.028a1 1 0 0 0-1.207 1.207L5.35 15.879a1 1 0 0 0 .776.746L13 18"}],["path",{d:"m2.3 2.3 7.286 7.286"}],["circle",{cx:"11",cy:"11",r:"2"}]],er=[["path",{d:"M13 21h8"}],["path",{d:"m15 5 4 4"}],["path",{d:"M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"}]],rr=[["path",{d:"m10 10-6.157 6.162a2 2 0 0 0-.5.833l-1.322 4.36a.5.5 0 0 0 .622.624l4.358-1.323a2 2 0 0 0 .83-.5L14 13.982"}],["path",{d:"m12.829 7.172 4.359-4.346a1 1 0 1 1 3.986 3.986l-4.353 4.353"}],["path",{d:"m15 5 4 4"}],["path",{d:"m2 2 20 20"}]],P2=[["path",{d:"M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"}]],or=[["path",{d:"M13 7 8.7 2.7a2.41 2.41 0 0 0-3.4 0L2.7 5.3a2.41 2.41 0 0 0 0 3.4L7 13"}],["path",{d:"m8 6 2-2"}],["path",{d:"m18 16 2-2"}],["path",{d:"m17 11 4.3 4.3c.94.94.94 2.46 0 3.4l-2.6 2.6c-.94.94-2.46.94-3.4 0L11 17"}],["path",{d:"M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"}],["path",{d:"m15 5 4 4"}]],vr=[["path",{d:"M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"}],["path",{d:"m15 5 4 4"}]],$r=[["path",{d:"M10.83 2.38a2 2 0 0 1 2.34 0l8 5.74a2 2 0 0 1 .73 2.25l-3.04 9.26a2 2 0 0 1-1.9 1.37H7.04a2 2 0 0 1-1.9-1.37L2.1 10.37a2 2 0 0 1 .73-2.25z"}]],mr=[["line",{x1:"19",x2:"5",y1:"5",y2:"19"}],["circle",{cx:"6.5",cy:"6.5",r:"2.5"}],["circle",{cx:"17.5",cy:"17.5",r:"2.5"}]],yr=[["circle",{cx:"12",cy:"5",r:"1"}],["path",{d:"m9 20 3-6 3 6"}],["path",{d:"m6 8 6 2 6-2"}],["path",{d:"M12 10v4"}]],sr=[["path",{d:"M20 11H4"}],["path",{d:"M20 7H4"}],["path",{d:"M7 21V4a1 1 0 0 1 1-1h4a1 1 0 0 1 0 12H7"}]],gr=[["path",{d:"M13 2a9 9 0 0 1 9 9"}],["path",{d:"M13 6a5 5 0 0 1 5 5"}],["path",{d:"M13.832 16.568a1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 6.392 6.384"}]],ur=[["path",{d:"M14 6h8"}],["path",{d:"m18 2 4 4-4 4"}],["path",{d:"M13.832 16.568a1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 6.392 6.384"}]],Cr=[["path",{d:"M16 2v6h6"}],["path",{d:"m22 2-6 6"}],["path",{d:"M13.832 16.568a1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 6.392 6.384"}]],Hr=[["path",{d:"m16 2 6 6"}],["path",{d:"m22 2-6 6"}],["path",{d:"M13.832 16.568a1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 6.392 6.384"}]],Ar=[["path",{d:"M10.1 13.9a14 14 0 0 0 3.732 2.668 1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2 18 18 0 0 1-12.728-5.272"}],["path",{d:"M22 2 2 22"}],["path",{d:"M4.76 13.582A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 .244.473"}]],wr=[["path",{d:"m16 8 6-6"}],["path",{d:"M22 8V2h-6"}],["path",{d:"M13.832 16.568a1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 6.392 6.384"}]],Vr=[["path",{d:"M13.832 16.568a1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 6.392 6.384"}]],Sr=[["line",{x1:"9",x2:"9",y1:"4",y2:"20"}],["path",{d:"M4 7c0-1.7 1.3-3 3-3h13"}],["path",{d:"M18 20c-1.7 0-3-1.3-3-3V4"}]],Lr=[["path",{d:"M18.5 8c-1.4 0-2.6-.8-3.2-2A6.87 6.87 0 0 0 2 9v11a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-8.5C22 9.6 20.4 8 18.5 8"}],["path",{d:"M2 14h20"}],["path",{d:"M6 14v4"}],["path",{d:"M10 14v4"}],["path",{d:"M14 14v4"}],["path",{d:"M18 14v4"}]],fr=[["path",{d:"m14 13-8.381 8.38a1 1 0 0 1-3.001-3L11 9.999"}],["path",{d:"M15.973 4.027A13 13 0 0 0 5.902 2.373c-1.398.342-1.092 2.158.277 2.601a19.9 19.9 0 0 1 5.822 3.024"}],["path",{d:"M16.001 11.999a19.9 19.9 0 0 1 3.024 5.824c.444 1.369 2.26 1.676 2.603.278A13 13 0 0 0 20 8.069"}],["path",{d:"M18.352 3.352a1.205 1.205 0 0 0-1.704 0l-5.296 5.296a1.205 1.205 0 0 0 0 1.704l2.296 2.296a1.205 1.205 0 0 0 1.704 0l5.296-5.296a1.205 1.205 0 0 0 0-1.704z"}]],kr=[["path",{d:"M21 9V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v10c0 1.1.9 2 2 2h4"}],["rect",{width:"10",height:"7",x:"12",y:"13",rx:"2"}]],Pr=[["path",{d:"M2 10h6V4"}],["path",{d:"m2 4 6 6"}],["path",{d:"M21 10V7a2 2 0 0 0-2-2h-7"}],["path",{d:"M3 14v2a2 2 0 0 0 2 2h3"}],["rect",{x:"12",y:"14",width:"10",height:"7",rx:"1"}]],Br=[["path",{d:"M11 17h3v2a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1v-3a3.16 3.16 0 0 0 2-2h1a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1h-1a5 5 0 0 0-2-4V3a4 4 0 0 0-3.2 1.6l-.3.4H11a6 6 0 0 0-6 6v1a5 5 0 0 0 2 4v3a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1z"}],["path",{d:"M16 10h.01"}],["path",{d:"M2 8v1a2 2 0 0 0 2 2h1"}]],Dr=[["path",{d:"M14 3v11"}],["path",{d:"M14 9h-3a3 3 0 0 1 0-6h9"}],["path",{d:"M18 3v11"}],["path",{d:"M22 18H2l4-4"}],["path",{d:"m6 22-4-4"}]],Fr=[["path",{d:"M10 3v11"}],["path",{d:"M10 9H7a1 1 0 0 1 0-6h8"}],["path",{d:"M14 3v11"}],["path",{d:"m18 14 4 4H2"}],["path",{d:"m22 18-4 4"}]],zr=[["path",{d:"M13 4v16"}],["path",{d:"M17 4v16"}],["path",{d:"M19 4H9.5a4.5 4.5 0 0 0 0 9H13"}]],br=[["path",{d:"M18 11h-4a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h4"}],["path",{d:"M6 7v13a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V7"}],["rect",{width:"16",height:"5",x:"4",y:"2",rx:"1"}]],Rr=[["path",{d:"m10.5 20.5 10-10a4.95 4.95 0 1 0-7-7l-10 10a4.95 4.95 0 1 0 7 7Z"}],["path",{d:"m8.5 8.5 7 7"}]],Tr=[["path",{d:"M12 17v5"}],["path",{d:"M15 9.34V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H7.89"}],["path",{d:"m2 2 20 20"}],["path",{d:"M9 9v1.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h11"}]],qr=[["path",{d:"M12 17v5"}],["path",{d:"M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z"}]],Ur=[["path",{d:"m12 9-8.414 8.414A2 2 0 0 0 3 18.828v1.344a2 2 0 0 1-.586 1.414A2 2 0 0 1 3.828 21h1.344a2 2 0 0 0 1.414-.586L15 12"}],["path",{d:"m18 9 .4.4a1 1 0 1 1-3 3l-3.8-3.8a1 1 0 1 1 3-3l.4.4 3.4-3.4a1 1 0 1 1 3 3z"}],["path",{d:"m2 22 .414-.414"}]],Or=[["path",{d:"m12 14-1 1"}],["path",{d:"m13.75 18.25-1.25 1.42"}],["path",{d:"M17.775 5.654a15.68 15.68 0 0 0-12.121 12.12"}],["path",{d:"M18.8 9.3a1 1 0 0 0 2.1 7.7"}],["path",{d:"M21.964 20.732a1 1 0 0 1-1.232 1.232l-18-5a1 1 0 0 1-.695-1.232A19.68 19.68 0 0 1 15.732 2.037a1 1 0 0 1 1.232.695z"}]],Zr=[["path",{d:"M2 22h20"}],["path",{d:"M3.77 10.77 2 9l2-4.5 1.1.55c.55.28.9.84.9 1.45s.35 1.17.9 1.45L8 8.5l3-6 1.05.53a2 2 0 0 1 1.09 1.52l.72 5.4a2 2 0 0 0 1.09 1.52l4.4 2.2c.42.22.78.55 1.01.96l.6 1.03c.49.88-.06 1.98-1.06 2.1l-1.18.15c-.47.06-.95-.02-1.37-.24L4.29 11.15a2 2 0 0 1-.52-.38Z"}]],Gr=[["path",{d:"M2 22h20"}],["path",{d:"M6.36 17.4 4 17l-2-4 1.1-.55a2 2 0 0 1 1.8 0l.17.1a2 2 0 0 0 1.8 0L8 12 5 6l.9-.45a2 2 0 0 1 2.09.2l4.02 3a2 2 0 0 0 2.1.2l4.19-2.06a2.41 2.41 0 0 1 1.73-.17L21 7a1.4 1.4 0 0 1 .87 1.99l-.38.76c-.23.46-.6.84-1.07 1.08L7.58 17.2a2 2 0 0 1-1.22.18Z"}]],Ir=[["path",{d:"M17.8 19.2 16 11l3.5-3.5C21 6 21.5 4 21 3c-1-.5-3 0-4.5 1.5L13 8 4.8 6.2c-.5-.1-.9.1-1.1.5l-.3.5c-.2.5-.1 1 .3 1.3L9 12l-2 3H4l-1 1 3 2 2 3 1-1v-3l3-2 3.5 5.3c.3.4.8.5 1.3.3l.5-.2c.4-.3.6-.7.5-1.2z"}]],Wr=[["path",{d:"M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z"}]],B2=[["path",{d:"M6.3 20.3a2.4 2.4 0 0 0 3.4 0L12 18l-6-6-2.3 2.3a2.4 2.4 0 0 0 0 3.4Z"}],["path",{d:"m2 22 3-3"}],["path",{d:"M7.5 13.5 10 11"}],["path",{d:"M10.5 16.5 13 14"}],["path",{d:"m18 3-4 4h6l-4 4"}]],Er=[["path",{d:"M9 2v6"}],["path",{d:"M15 2v6"}],["path",{d:"M12 17v5"}],["path",{d:"M5 8h14"}],["path",{d:"M6 11V8h12v3a6 6 0 1 1-12 0Z"}]],Xr=[["path",{d:"M12 22v-5"}],["path",{d:"M9 8V2"}],["path",{d:"M15 8V2"}],["path",{d:"M18 8v5a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V8Z"}]],jr=[["path",{d:"M5 12h14"}],["path",{d:"M12 5v14"}]],Nr=[["path",{d:"M3 2v1c0 1 2 1 2 2S3 6 3 7s2 1 2 2-2 1-2 2 2 1 2 2"}],["path",{d:"M18 6h.01"}],["path",{d:"M6 18h.01"}],["path",{d:"M20.83 8.83a4 4 0 0 0-5.66-5.66l-12 12a4 4 0 1 0 5.66 5.66Z"}],["path",{d:"M18 11.66V22a4 4 0 0 0 4-4V6"}]],Kr=[["path",{d:"M20 3a2 2 0 0 1 2 2v6a1 1 0 0 1-20 0V5a2 2 0 0 1 2-2z"}],["path",{d:"m8 10 4 4 4-4"}]],Qr=[["path",{d:"M10 4.5V4a2 2 0 0 0-2.41-1.957"}],["path",{d:"M13.9 8.4a2 2 0 0 0-1.26-1.295"}],["path",{d:"M21.7 16.2A8 8 0 0 0 22 14v-3a2 2 0 1 0-4 0v-1a2 2 0 0 0-3.63-1.158"}],["path",{d:"m7 15-1.8-1.8a2 2 0 0 0-2.79 2.86L6 19.7a7.74 7.74 0 0 0 6 2.3h2a8 8 0 0 0 5.657-2.343"}],["path",{d:"M6 6v8"}],["path",{d:"m2 2 20 20"}]],Jr=[["path",{d:"M22 14a8 8 0 0 1-8 8"}],["path",{d:"M18 11v-1a2 2 0 0 0-2-2a2 2 0 0 0-2 2"}],["path",{d:"M14 10V9a2 2 0 0 0-2-2a2 2 0 0 0-2 2v1"}],["path",{d:"M10 9.5V4a2 2 0 0 0-2-2a2 2 0 0 0-2 2v10"}],["path",{d:"M18 11a2 2 0 1 1 4 0v3a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.86-5.99-2.34l-3.6-3.6a2 2 0 0 1 2.83-2.82L7 15"}]],Yr=[["path",{d:"M13 17a1 1 0 1 0-2 0l.5 4.5a0.5 0.5 0 0 0 1 0z",fill:"currentColor"}],["path",{d:"M16.85 18.58a9 9 0 1 0-9.7 0"}],["path",{d:"M8 14a5 5 0 1 1 8 0"}],["circle",{cx:"12",cy:"11",r:"1",fill:"currentColor"}]],_r=[["path",{d:"M18 8a2 2 0 0 0 0-4 2 2 0 0 0-4 0 2 2 0 0 0-4 0 2 2 0 0 0-4 0 2 2 0 0 0 0 4"}],["path",{d:"M10 22 9 8"}],["path",{d:"m14 22 1-14"}],["path",{d:"M20 8c.5 0 .9.4.8 1l-2.6 12c-.1.5-.7 1-1.2 1H7c-.6 0-1.1-.4-1.2-1L3.2 9c-.1-.6.3-1 .8-1Z"}]],xr=[["path",{d:"M18.6 14.4c.8-.8.8-2 0-2.8l-8.1-8.1a4.95 4.95 0 1 0-7.1 7.1l8.1 8.1c.9.7 2.1.7 2.9-.1Z"}],["path",{d:"m22 22-5.5-5.5"}]],ao=[["path",{d:"M18 7c0-5.333-8-5.333-8 0"}],["path",{d:"M10 7v14"}],["path",{d:"M6 21h12"}],["path",{d:"M6 13h10"}]],to=[["path",{d:"M18.36 6.64A9 9 0 0 1 20.77 15"}],["path",{d:"M6.16 6.16a9 9 0 1 0 12.68 12.68"}],["path",{d:"M12 2v4"}],["path",{d:"m2 2 20 20"}]],ho=[["path",{d:"M12 2v10"}],["path",{d:"M18.4 6.6a9 9 0 1 1-12.77.04"}]],co=[["path",{d:"M2 3h20"}],["path",{d:"M21 3v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V3"}],["path",{d:"m7 21 5-5 5 5"}]],Mo=[["path",{d:"M13.5 22H7a1 1 0 0 1-1-1v-6a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v.5"}],["path",{d:"m16 19 2 2 4-4"}],["path",{d:"M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v2"}],["path",{d:"M6 9V3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v6"}]],po=[["path",{d:"M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"}],["path",{d:"M6 9V3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v6"}],["rect",{x:"6",y:"14",width:"12",height:"8",rx:"1"}]],io=[["path",{d:"M5 7 3 5"}],["path",{d:"M9 6V3"}],["path",{d:"m13 7 2-2"}],["circle",{cx:"9",cy:"13",r:"3"}],["path",{d:"M11.83 12H20a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2h2.17"}],["path",{d:"M16 16h2"}]],no=[["rect",{width:"20",height:"16",x:"2",y:"4",rx:"2"}],["path",{d:"M12 9v11"}],["path",{d:"M2 9h13a2 2 0 0 1 2 2v9"}]],lo=[["path",{d:"M15.39 4.39a1 1 0 0 0 1.68-.474 2.5 2.5 0 1 1 3.014 3.015 1 1 0 0 0-.474 1.68l1.683 1.682a2.414 2.414 0 0 1 0 3.414L19.61 15.39a1 1 0 0 1-1.68-.474 2.5 2.5 0 1 0-3.014 3.015 1 1 0 0 1 .474 1.68l-1.683 1.682a2.414 2.414 0 0 1-3.414 0L8.61 19.61a1 1 0 0 0-1.68.474 2.5 2.5 0 1 1-3.014-3.015 1 1 0 0 0 .474-1.68l-1.683-1.682a2.414 2.414 0 0 1 0-3.414L4.39 8.61a1 1 0 0 1 1.68.474 2.5 2.5 0 1 0 3.014-3.015 1 1 0 0 1-.474-1.68l1.683-1.682a2.414 2.414 0 0 1 3.414 0z"}]],eo=[["path",{d:"M2.5 16.88a1 1 0 0 1-.32-1.43l9-13.02a1 1 0 0 1 1.64 0l9 13.01a1 1 0 0 1-.32 1.44l-8.51 4.86a2 2 0 0 1-1.98 0Z"}],["path",{d:"M12 2v20"}]],ro=[["rect",{width:"5",height:"5",x:"3",y:"3",rx:"1"}],["rect",{width:"5",height:"5",x:"16",y:"3",rx:"1"}],["rect",{width:"5",height:"5",x:"3",y:"16",rx:"1"}],["path",{d:"M21 16h-3a2 2 0 0 0-2 2v3"}],["path",{d:"M21 21v.01"}],["path",{d:"M12 7v3a2 2 0 0 1-2 2H7"}],["path",{d:"M3 12h.01"}],["path",{d:"M12 3h.01"}],["path",{d:"M12 16v.01"}],["path",{d:"M16 12h1"}],["path",{d:"M21 12v.01"}],["path",{d:"M12 21v-1"}]],oo=[["path",{d:"M16 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1v2a1 1 0 0 0 1 1 6 6 0 0 0 6-6V5a2 2 0 0 0-2-2z"}],["path",{d:"M5 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1v2a1 1 0 0 0 1 1 6 6 0 0 0 6-6V5a2 2 0 0 0-2-2z"}]],vo=[["path",{d:"M13 16a3 3 0 0 1 2.24 5"}],["path",{d:"M18 12h.01"}],["path",{d:"M18 21h-8a4 4 0 0 1-4-4 7 7 0 0 1 7-7h.2L9.6 6.4a1 1 0 1 1 2.8-2.8L15.8 7h.2c3.3 0 6 2.7 6 6v1a2 2 0 0 1-2 2h-1a3 3 0 0 0-3 3"}],["path",{d:"M20 8.54V4a2 2 0 1 0-4 0v3"}],["path",{d:"M7.612 12.524a3 3 0 1 0-1.6 4.3"}]],$o=[["path",{d:"M19.07 4.93A10 10 0 0 0 6.99 3.34"}],["path",{d:"M4 6h.01"}],["path",{d:"M2.29 9.62A10 10 0 1 0 21.31 8.35"}],["path",{d:"M16.24 7.76A6 6 0 1 0 8.23 16.67"}],["path",{d:"M12 18h.01"}],["path",{d:"M17.99 11.66A6 6 0 0 1 15.77 16.67"}],["circle",{cx:"12",cy:"12",r:"2"}],["path",{d:"m13.41 10.59 5.66-5.66"}]],mo=[["path",{d:"M12 12h.01"}],["path",{d:"M14 15.4641a4 4 0 0 1-4 0L7.52786 19.74597 A 1 1 0 0 0 7.99303 21.16211 10 10 0 0 0 16.00697 21.16211 1 1 0 0 0 16.47214 19.74597z"}],["path",{d:"M16 12a4 4 0 0 0-2-3.464l2.472-4.282a1 1 0 0 1 1.46-.305 10 10 0 0 1 4.006 6.94A1 1 0 0 1 21 12z"}],["path",{d:"M8 12a4 4 0 0 1 2-3.464L7.528 4.254a1 1 0 0 0-1.46-.305 10 10 0 0 0-4.006 6.94A1 1 0 0 0 3 12z"}]],yo=[["path",{d:"M3 12h3.28a1 1 0 0 1 .948.684l2.298 7.934a.5.5 0 0 0 .96-.044L13.82 4.771A1 1 0 0 1 14.792 4H21"}]],so=[["path",{d:"M5 16v2"}],["path",{d:"M19 16v2"}],["rect",{width:"20",height:"8",x:"2",y:"8",rx:"2"}],["path",{d:"M18 12h.01"}]],go=[["path",{d:"M4.9 16.1C1 12.2 1 5.8 4.9 1.9"}],["path",{d:"M7.8 4.7a6.14 6.14 0 0 0-.8 7.5"}],["circle",{cx:"12",cy:"9",r:"2"}],["path",{d:"M16.2 4.8c2 2 2.26 5.11.8 7.47"}],["path",{d:"M19.1 1.9a9.96 9.96 0 0 1 0 14.1"}],["path",{d:"M9.5 18h5"}],["path",{d:"m8 22 4-11 4 11"}]],uo=[["path",{d:"M16.247 7.761a6 6 0 0 1 0 8.478"}],["path",{d:"M19.075 4.933a10 10 0 0 1 0 14.134"}],["path",{d:"M4.925 19.067a10 10 0 0 1 0-14.134"}],["path",{d:"M7.753 16.239a6 6 0 0 1 0-8.478"}],["circle",{cx:"12",cy:"12",r:"2"}]],Co=[["path",{d:"M5 15h14"}],["path",{d:"M5 9h14"}],["path",{d:"m14 20-5-5 6-6-5-5"}]],Ho=[["path",{d:"M20.34 17.52a10 10 0 1 0-2.82 2.82"}],["circle",{cx:"19",cy:"19",r:"2"}],["path",{d:"m13.41 13.41 4.18 4.18"}],["circle",{cx:"12",cy:"12",r:"2"}]],Ao=[["path",{d:"M22 17a10 10 0 0 0-20 0"}],["path",{d:"M6 17a6 6 0 0 1 12 0"}],["path",{d:"M10 17a2 2 0 0 1 4 0"}]],wo=[["path",{d:"M13 22H4a2 2 0 0 1 0-4h12"}],["path",{d:"M13.236 18a3 3 0 0 0-2.2-5"}],["path",{d:"M16 9h.01"}],["path",{d:"M16.82 3.94a3 3 0 1 1 3.237 4.868l1.815 2.587a1.5 1.5 0 0 1-1.5 2.1l-2.872-.453a3 3 0 0 0-3.5 3"}],["path",{d:"M17 4.988a3 3 0 1 0-5.2 2.052A7 7 0 0 0 4 14.015 4 4 0 0 0 8 18"}]],Vo=[["rect",{width:"12",height:"20",x:"6",y:"2",rx:"2"}],["rect",{width:"20",height:"12",x:"2",y:"6",rx:"2"}]],So=[["path",{d:"M4 2v20l2-1 2 1 2-1 2 1 2-1 2 1 2-1 2 1V2l-2 1-2-1-2 1-2-1-2 1-2-1-2 1Z"}],["path",{d:"M12 6.5v11"}],["path",{d:"M15 9.4a4 4 0 1 0 0 5.2"}]],Lo=[["path",{d:"M4 2v20l2-1 2 1 2-1 2 1 2-1 2 1 2-1 2 1V2l-2 1-2-1-2 1-2-1-2 1-2-1-2 1Z"}],["path",{d:"M8 12h5"}],["path",{d:"M16 9.5a4 4 0 1 0 0 5.2"}]],fo=[["path",{d:"M4 2v20l2-1 2 1 2-1 2 1 2-1 2 1 2-1 2 1V2l-2 1-2-1-2 1-2-1-2 1-2-1-2 1Z"}],["path",{d:"M8 7h8"}],["path",{d:"M12 17.5 8 15h1a4 4 0 0 0 0-8"}],["path",{d:"M8 11h8"}]],ko=[["path",{d:"M4 2v20l2-1 2 1 2-1 2 1 2-1 2 1 2-1 2 1V2l-2 1-2-1-2 1-2-1-2 1-2-1-2 1Z"}],["path",{d:"m12 10 3-3"}],["path",{d:"m9 7 3 3v7.5"}],["path",{d:"M9 11h6"}],["path",{d:"M9 15h6"}]],Po=[["path",{d:"M4 2v20l2-1 2 1 2-1 2 1 2-1 2 1 2-1 2 1V2l-2 1-2-1-2 1-2-1-2 1-2-1-2 1Z"}],["path",{d:"M8 13h5"}],["path",{d:"M10 17V9.5a2.5 2.5 0 0 1 5 0"}],["path",{d:"M8 17h7"}]],Bo=[["path",{d:"M4 2v20l2-1 2 1 2-1 2 1 2-1 2 1 2-1 2 1V2l-2 1-2-1-2 1-2-1-2 1-2-1-2 1Z"}],["path",{d:"M8 15h5"}],["path",{d:"M8 11h5a2 2 0 1 0 0-4h-3v10"}]],Do=[["path",{d:"M4 2v20l2-1 2 1 2-1 2 1 2-1 2 1 2-1 2 1V2l-2 1-2-1-2 1-2-1-2 1-2-1-2 1Z"}],["path",{d:"M10 17V7h5"}],["path",{d:"M10 11h4"}],["path",{d:"M8 15h5"}]],Fo=[["path",{d:"M4 2v20l2-1 2 1 2-1 2 1 2-1 2 1 2-1 2 1V2l-2 1-2-1-2 1-2-1-2 1-2-1-2 1Z"}],["path",{d:"M14 8H8"}],["path",{d:"M16 12H8"}],["path",{d:"M13 16H8"}]],zo=[["path",{d:"M4 2v20l2-1 2 1 2-1 2 1 2-1 2 1 2-1 2 1V2l-2 1-2-1-2 1-2-1-2 1-2-1-2 1Z"}],["path",{d:"M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8"}],["path",{d:"M12 17.5v-11"}]],bo=[["path",{d:"M10 6.5v11a5.5 5.5 0 0 0 5.5-5.5"}],["path",{d:"m14 8-6 3"}],["path",{d:"M4 2v20l2-1 2 1 2-1 2 1 2-1 2 1 2-1 2 1V2l-2 1-2-1-2 1-2-1-2 1-2-1-2 1z"}]],Ro=[["path",{d:"M14 4v16H3a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1z"}],["circle",{cx:"14",cy:"12",r:"8"}]],D2=[["rect",{width:"20",height:"12",x:"2",y:"6",rx:"2"}],["path",{d:"M12 12h.01"}],["path",{d:"M17 12h.01"}],["path",{d:"M7 12h.01"}]],To=[["path",{d:"M20 6a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-4a2 2 0 0 1-1.6-.8l-1.6-2.13a1 1 0 0 0-1.6 0L9.6 17.2A2 2 0 0 1 8 18H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z"}]],qo=[["rect",{width:"20",height:"12",x:"2",y:"6",rx:"2"}]],Uo=[["path",{d:"M7 19H4.815a1.83 1.83 0 0 1-1.57-.881 1.785 1.785 0 0 1-.004-1.784L7.196 9.5"}],["path",{d:"M11 19h8.203a1.83 1.83 0 0 0 1.556-.89 1.784 1.784 0 0 0 0-1.775l-1.226-2.12"}],["path",{d:"m14 16-3 3 3 3"}],["path",{d:"M8.293 13.596 7.196 9.5 3.1 10.598"}],["path",{d:"m9.344 5.811 1.093-1.892A1.83 1.83 0 0 1 11.985 3a1.784 1.784 0 0 1 1.546.888l3.943 6.843"}],["path",{d:"m13.378 9.633 4.096 1.098 1.097-4.096"}]],Oo=[["rect",{width:"12",height:"20",x:"6",y:"2",rx:"2"}]],Zo=[["path",{d:"m15 14 5-5-5-5"}],["path",{d:"M20 9H9.5A5.5 5.5 0 0 0 4 14.5A5.5 5.5 0 0 0 9.5 20H13"}]],Go=[["circle",{cx:"12",cy:"17",r:"1"}],["path",{d:"M21 7v6h-6"}],["path",{d:"M3 17a9 9 0 0 1 9-9 9 9 0 0 1 6 2.3l3 2.7"}]],Io=[["path",{d:"M21 7v6h-6"}],["path",{d:"M3 17a9 9 0 0 1 9-9 9 9 0 0 1 6 2.3l3 2.7"}]],Wo=[["path",{d:"M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"}],["path",{d:"M3 3v5h5"}],["path",{d:"M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"}],["path",{d:"M16 16h5v5"}]],Eo=[["path",{d:"M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"}],["path",{d:"M3 3v5h5"}],["path",{d:"M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"}],["path",{d:"M16 16h5v5"}],["circle",{cx:"12",cy:"12",r:"1"}]],Xo=[["path",{d:"M21 8L18.74 5.74A9.75 9.75 0 0 0 12 3C11 3 10.03 3.16 9.13 3.47"}],["path",{d:"M8 16H3v5"}],["path",{d:"M3 12C3 9.51 4 7.26 5.64 5.64"}],["path",{d:"m3 16 2.26 2.26A9.75 9.75 0 0 0 12 21c2.49 0 4.74-1 6.36-2.64"}],["path",{d:"M21 12c0 1-.16 1.97-.47 2.87"}],["path",{d:"M21 3v5h-5"}],["path",{d:"M22 22 2 2"}]],jo=[["path",{d:"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"}],["path",{d:"M21 3v5h-5"}],["path",{d:"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"}],["path",{d:"M8 16H3v5"}]],No=[["path",{d:"M5 6a4 4 0 0 1 4-4h6a4 4 0 0 1 4 4v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6Z"}],["path",{d:"M5 10h14"}],["path",{d:"M15 7v6"}]],Ko=[["path",{d:"M17 3v10"}],["path",{d:"m12.67 5.5 8.66 5"}],["path",{d:"m12.67 10.5 8.66-5"}],["path",{d:"M9 17a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v2a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2v-2z"}]],Qo=[["path",{d:"M4 7V4h16v3"}],["path",{d:"M5 20h6"}],["path",{d:"M13 4 8 20"}],["path",{d:"m15 15 5 5"}],["path",{d:"m20 15-5 5"}]],Jo=[["path",{d:"m2 9 3-3 3 3"}],["path",{d:"M13 18H7a2 2 0 0 1-2-2V6"}],["path",{d:"m22 15-3 3-3-3"}],["path",{d:"M11 6h6a2 2 0 0 1 2 2v10"}]],Yo=[["path",{d:"m17 2 4 4-4 4"}],["path",{d:"M3 11v-1a4 4 0 0 1 4-4h14"}],["path",{d:"m7 22-4-4 4-4"}],["path",{d:"M21 13v1a4 4 0 0 1-4 4H3"}],["path",{d:"M11 10h1v4"}]],_o=[["path",{d:"m17 2 4 4-4 4"}],["path",{d:"M3 11v-1a4 4 0 0 1 4-4h14"}],["path",{d:"m7 22-4-4 4-4"}],["path",{d:"M21 13v1a4 4 0 0 1-4 4H3"}]],xo=[["path",{d:"M14 14a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1"}],["path",{d:"M14 4a1 1 0 0 1 1-1"}],["path",{d:"M15 10a1 1 0 0 1-1-1"}],["path",{d:"M19 14a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1"}],["path",{d:"M21 4a1 1 0 0 0-1-1"}],["path",{d:"M21 9a1 1 0 0 1-1 1"}],["path",{d:"m3 7 3 3 3-3"}],["path",{d:"M6 10V5a2 2 0 0 1 2-2h2"}],["rect",{x:"3",y:"14",width:"7",height:"7",rx:"1"}]],av=[["path",{d:"M14 4a1 1 0 0 1 1-1"}],["path",{d:"M15 10a1 1 0 0 1-1-1"}],["path",{d:"M21 4a1 1 0 0 0-1-1"}],["path",{d:"M21 9a1 1 0 0 1-1 1"}],["path",{d:"m3 7 3 3 3-3"}],["path",{d:"M6 10V5a2 2 0 0 1 2-2h2"}],["rect",{x:"3",y:"14",width:"7",height:"7",rx:"1"}]],tv=[["path",{d:"m12 17-5-5 5-5"}],["path",{d:"M22 18v-2a4 4 0 0 0-4-4H7"}],["path",{d:"m7 17-5-5 5-5"}]],hv=[["path",{d:"M20 18v-2a4 4 0 0 0-4-4H4"}],["path",{d:"m9 17-5-5 5-5"}]],dv=[["path",{d:"M12 6a2 2 0 0 0-3.414-1.414l-6 6a2 2 0 0 0 0 2.828l6 6A2 2 0 0 0 12 18z"}],["path",{d:"M22 6a2 2 0 0 0-3.414-1.414l-6 6a2 2 0 0 0 0 2.828l6 6A2 2 0 0 0 22 18z"}]],cv=[["path",{d:"M12 11.22C11 9.997 10 9 10 8a2 2 0 0 1 4 0c0 1-.998 2.002-2.01 3.22"}],["path",{d:"m12 18 2.57-3.5"}],["path",{d:"M6.243 9.016a7 7 0 0 1 11.507-.009"}],["path",{d:"M9.35 14.53 12 11.22"}],["path",{d:"M9.35 14.53C7.728 12.246 6 10.221 6 7a6 5 0 0 1 12 0c-.005 3.22-1.778 5.235-3.43 7.5l3.557 4.527a1 1 0 0 1-.203 1.43l-1.894 1.36a1 1 0 0 1-1.384-.215L12 18l-2.679 3.593a1 1 0 0 1-1.39.213l-1.865-1.353a1 1 0 0 1-.203-1.422z"}]],Mv=[["path",{d:"M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"}],["path",{d:"m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"}],["path",{d:"M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0"}],["path",{d:"M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"}]],pv=[["polyline",{points:"3.5 2 6.5 12.5 18 12.5"}],["line",{x1:"9.5",x2:"5.5",y1:"12.5",y2:"20"}],["line",{x1:"15",x2:"18.5",y1:"12.5",y2:"20"}],["path",{d:"M2.75 18a13 13 0 0 0 18.5 0"}]],iv=[["path",{d:"M6 19V5"}],["path",{d:"M10 19V6.8"}],["path",{d:"M14 19v-7.8"}],["path",{d:"M18 5v4"}],["path",{d:"M18 19v-6"}],["path",{d:"M22 19V9"}],["path",{d:"M2 19V9a4 4 0 0 1 4-4c2 0 4 1.33 6 4s4 4 6 4a4 4 0 1 0-3-6.65"}]],nv=[["path",{d:"M17 10h-1a4 4 0 1 1 4-4v.534"}],["path",{d:"M17 6h1a4 4 0 0 1 1.42 7.74l-2.29.87a6 6 0 0 1-5.339-10.68l2.069-1.31"}],["path",{d:"M4.5 17c2.8-.5 4.4 0 5.5.8s1.8 2.2 2.3 3.7c-2 .4-3.5.4-4.8-.3-1.2-.6-2.3-1.9-3-4.2"}],["path",{d:"M9.77 12C4 15 2 22 2 22"}],["circle",{cx:"17",cy:"8",r:"2"}]],F2=[["path",{d:"M16.466 7.5C15.643 4.237 13.952 2 12 2 9.239 2 7 6.477 7 12s2.239 10 5 10c.342 0 .677-.069 1-.2"}],["path",{d:"m15.194 13.707 3.814 1.86-1.86 3.814"}],["path",{d:"M19 15.57c-1.804.885-4.274 1.43-7 1.43-5.523 0-10-2.239-10-5s4.477-5 10-5c4.838 0 8.873 1.718 9.8 4"}]],lv=[["path",{d:"m14.5 9.5 1 1"}],["path",{d:"m15.5 8.5-4 4"}],["path",{d:"M3 12a9 9 0 1 0 9-9 9.74 9.74 0 0 0-6.74 2.74L3 8"}],["path",{d:"M3 3v5h5"}],["circle",{cx:"10",cy:"14",r:"2"}]],ev=[["path",{d:"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"}],["path",{d:"M3 3v5h5"}]],rv=[["path",{d:"M12 5H6a2 2 0 0 0-2 2v3"}],["path",{d:"m9 8 3-3-3-3"}],["path",{d:"M4 14v4a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2"}]],ov=[["path",{d:"M20 9V7a2 2 0 0 0-2-2h-6"}],["path",{d:"m15 2-3 3 3 3"}],["path",{d:"M20 13v5a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h2"}]],vv=[["circle",{cx:"6",cy:"19",r:"3"}],["path",{d:"M9 19h8.5c.4 0 .9-.1 1.3-.2"}],["path",{d:"M5.2 5.2A3.5 3.53 0 0 0 6.5 12H12"}],["path",{d:"m2 2 20 20"}],["path",{d:"M21 15.3a3.5 3.5 0 0 0-3.3-3.3"}],["path",{d:"M15 5h-4.3"}],["circle",{cx:"18",cy:"5",r:"3"}]],$v=[["path",{d:"M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"}],["path",{d:"M21 3v5h-5"}]],mv=[["circle",{cx:"6",cy:"19",r:"3"}],["path",{d:"M9 19h8.5a3.5 3.5 0 0 0 0-7h-11a3.5 3.5 0 0 1 0-7H15"}],["circle",{cx:"18",cy:"5",r:"3"}]],yv=[["rect",{width:"20",height:"8",x:"2",y:"14",rx:"2"}],["path",{d:"M6.01 18H6"}],["path",{d:"M10.01 18H10"}],["path",{d:"M15 10v4"}],["path",{d:"M17.84 7.17a4 4 0 0 0-5.66 0"}],["path",{d:"M20.66 4.34a8 8 0 0 0-11.31 0"}]],z2=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M3 12h18"}]],b2=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M21 9H3"}],["path",{d:"M21 15H3"}]],sv=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M21 7.5H3"}],["path",{d:"M21 12H3"}],["path",{d:"M21 16.5H3"}]],gv=[["path",{d:"M4 11a9 9 0 0 1 9 9"}],["path",{d:"M4 4a16 16 0 0 1 16 16"}],["circle",{cx:"5",cy:"19",r:"1"}]],uv=[["path",{d:"M12 15v-3.014"}],["path",{d:"M16 15v-3.014"}],["path",{d:"M20 6H4"}],["path",{d:"M20 8V4"}],["path",{d:"M4 8V4"}],["path",{d:"M8 15v-3.014"}],["rect",{x:"3",y:"12",width:"18",height:"7",rx:"1"}]],Cv=[["path",{d:"M21.3 15.3a2.4 2.4 0 0 1 0 3.4l-2.6 2.6a2.4 2.4 0 0 1-3.4 0L2.7 8.7a2.41 2.41 0 0 1 0-3.4l2.6-2.6a2.41 2.41 0 0 1 3.4 0Z"}],["path",{d:"m14.5 12.5 2-2"}],["path",{d:"m11.5 9.5 2-2"}],["path",{d:"m8.5 6.5 2-2"}],["path",{d:"m17.5 15.5 2-2"}]],Hv=[["path",{d:"M6 11h8a4 4 0 0 0 0-8H9v18"}],["path",{d:"M6 15h8"}]],Av=[["path",{d:"M10 2v15"}],["path",{d:"M7 22a4 4 0 0 1-4-4 1 1 0 0 1 1-1h16a1 1 0 0 1 1 1 4 4 0 0 1-4 4z"}],["path",{d:"M9.159 2.46a1 1 0 0 1 1.521-.193l9.977 8.98A1 1 0 0 1 20 13H4a1 1 0 0 1-.824-1.567z"}]],wv=[["path",{d:"M7 21h10"}],["path",{d:"M12 21a9 9 0 0 0 9-9H3a9 9 0 0 0 9 9Z"}],["path",{d:"M11.38 12a2.4 2.4 0 0 1-.4-4.77 2.4 2.4 0 0 1 3.2-2.77 2.4 2.4 0 0 1 3.47-.63 2.4 2.4 0 0 1 3.37 3.37 2.4 2.4 0 0 1-1.1 3.7 2.51 2.51 0 0 1 .03 1.1"}],["path",{d:"m13 12 4-4"}],["path",{d:"M10.9 7.25A3.99 3.99 0 0 0 4 10c0 .73.2 1.41.54 2"}]],Vv=[["path",{d:"m2.37 11.223 8.372-6.777a2 2 0 0 1 2.516 0l8.371 6.777"}],["path",{d:"M21 15a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-5.25"}],["path",{d:"M3 15a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h9"}],["path",{d:"m6.67 15 6.13 4.6a2 2 0 0 0 2.8-.4l3.15-4.2"}],["rect",{width:"20",height:"4",x:"2",y:"11",rx:"1"}]],Sv=[["path",{d:"M4 10a7.31 7.31 0 0 0 10 10Z"}],["path",{d:"m9 15 3-3"}],["path",{d:"M17 13a6 6 0 0 0-6-6"}],["path",{d:"M21 13A10 10 0 0 0 11 3"}]],Lv=[["path",{d:"m13.5 6.5-3.148-3.148a1.205 1.205 0 0 0-1.704 0L6.352 5.648a1.205 1.205 0 0 0 0 1.704L9.5 10.5"}],["path",{d:"M16.5 7.5 19 5"}],["path",{d:"m17.5 10.5 3.148 3.148a1.205 1.205 0 0 1 0 1.704l-2.296 2.296a1.205 1.205 0 0 1-1.704 0L13.5 14.5"}],["path",{d:"M9 21a6 6 0 0 0-6-6"}],["path",{d:"M9.352 10.648a1.205 1.205 0 0 0 0 1.704l2.296 2.296a1.205 1.205 0 0 0 1.704 0l4.296-4.296a1.205 1.205 0 0 0 0-1.704l-2.296-2.296a1.205 1.205 0 0 0-1.704 0z"}]],fv=[["path",{d:"m20 19.5-5.5 1.2"}],["path",{d:"M14.5 4v11.22a1 1 0 0 0 1.242.97L20 15.2"}],["path",{d:"m2.978 19.351 5.549-1.363A2 2 0 0 0 10 16V2"}],["path",{d:"M20 10 4 13.5"}]],kv=[["path",{d:"M10 2v3a1 1 0 0 0 1 1h5"}],["path",{d:"M18 18v-6a1 1 0 0 0-1-1h-6a1 1 0 0 0-1 1v6"}],["path",{d:"M18 22H4a2 2 0 0 1-2-2V6"}],["path",{d:"M8 18a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9.172a2 2 0 0 1 1.414.586l2.828 2.828A2 2 0 0 1 22 6.828V16a2 2 0 0 1-2.01 2z"}]],Pv=[["path",{d:"M13 13H8a1 1 0 0 0-1 1v7"}],["path",{d:"M14 8h1"}],["path",{d:"M17 21v-4"}],["path",{d:"m2 2 20 20"}],["path",{d:"M20.41 20.41A2 2 0 0 1 19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 .59-1.41"}],["path",{d:"M29.5 11.5s5 5 4 5"}],["path",{d:"M9 3h6.2a2 2 0 0 1 1.4.6l3.8 3.8a2 2 0 0 1 .6 1.4V15"}]],Bv=[["path",{d:"M15.2 3a2 2 0 0 1 1.4.6l3.8 3.8a2 2 0 0 1 .6 1.4V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z"}],["path",{d:"M17 21v-7a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v7"}],["path",{d:"M7 3v4a1 1 0 0 0 1 1h7"}]],R2=[["path",{d:"M5 7v11a1 1 0 0 0 1 1h11"}],["path",{d:"M5.293 18.707 11 13"}],["circle",{cx:"19",cy:"19",r:"2"}],["circle",{cx:"5",cy:"5",r:"2"}]],Dv=[["path",{d:"m16 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z"}],["path",{d:"m2 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z"}],["path",{d:"M7 21h10"}],["path",{d:"M12 3v18"}],["path",{d:"M3 7h2c2 0 5-1 7-2 2 1 5 2 7 2h2"}]],Fv=[["path",{d:"M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"}],["path",{d:"M14 15H9v-5"}],["path",{d:"M16 3h5v5"}],["path",{d:"M21 3 9 15"}]],zv=[["path",{d:"M3 7V5a2 2 0 0 1 2-2h2"}],["path",{d:"M17 3h2a2 2 0 0 1 2 2v2"}],["path",{d:"M21 17v2a2 2 0 0 1-2 2h-2"}],["path",{d:"M7 21H5a2 2 0 0 1-2-2v-2"}],["path",{d:"M8 7v10"}],["path",{d:"M12 7v10"}],["path",{d:"M17 7v10"}]],bv=[["path",{d:"M3 7V5a2 2 0 0 1 2-2h2"}],["path",{d:"M17 3h2a2 2 0 0 1 2 2v2"}],["path",{d:"M21 17v2a2 2 0 0 1-2 2h-2"}],["path",{d:"M7 21H5a2 2 0 0 1-2-2v-2"}],["circle",{cx:"12",cy:"12",r:"1"}],["path",{d:"M18.944 12.33a1 1 0 0 0 0-.66 7.5 7.5 0 0 0-13.888 0 1 1 0 0 0 0 .66 7.5 7.5 0 0 0 13.888 0"}]],Rv=[["path",{d:"M3 7V5a2 2 0 0 1 2-2h2"}],["path",{d:"M17 3h2a2 2 0 0 1 2 2v2"}],["path",{d:"M21 17v2a2 2 0 0 1-2 2h-2"}],["path",{d:"M7 21H5a2 2 0 0 1-2-2v-2"}],["path",{d:"M8 14s1.5 2 4 2 4-2 4-2"}],["path",{d:"M9 9h.01"}],["path",{d:"M15 9h.01"}]],Tv=[["path",{d:"M17 3h2a2 2 0 0 1 2 2v2"}],["path",{d:"M21 17v2a2 2 0 0 1-2 2h-2"}],["path",{d:"M3 7V5a2 2 0 0 1 2-2h2"}],["path",{d:"M7 21H5a2 2 0 0 1-2-2v-2"}],["path",{d:"M7.828 13.07A3 3 0 0 1 12 8.764a3 3 0 0 1 4.172 4.306l-3.447 3.62a1 1 0 0 1-1.449 0z"}]],qv=[["path",{d:"M3 7V5a2 2 0 0 1 2-2h2"}],["path",{d:"M17 3h2a2 2 0 0 1 2 2v2"}],["path",{d:"M21 17v2a2 2 0 0 1-2 2h-2"}],["path",{d:"M7 21H5a2 2 0 0 1-2-2v-2"}],["path",{d:"M7 12h10"}]],Uv=[["path",{d:"M17 12v4a1 1 0 0 1-1 1h-4"}],["path",{d:"M17 3h2a2 2 0 0 1 2 2v2"}],["path",{d:"M17 8V7"}],["path",{d:"M21 17v2a2 2 0 0 1-2 2h-2"}],["path",{d:"M3 7V5a2 2 0 0 1 2-2h2"}],["path",{d:"M7 17h.01"}],["path",{d:"M7 21H5a2 2 0 0 1-2-2v-2"}],["rect",{x:"7",y:"7",width:"5",height:"5",rx:"1"}]],Ov=[["path",{d:"M3 7V5a2 2 0 0 1 2-2h2"}],["path",{d:"M17 3h2a2 2 0 0 1 2 2v2"}],["path",{d:"M21 17v2a2 2 0 0 1-2 2h-2"}],["path",{d:"M7 21H5a2 2 0 0 1-2-2v-2"}],["circle",{cx:"12",cy:"12",r:"3"}],["path",{d:"m16 16-1.9-1.9"}]],Zv=[["path",{d:"M3 7V5a2 2 0 0 1 2-2h2"}],["path",{d:"M17 3h2a2 2 0 0 1 2 2v2"}],["path",{d:"M21 17v2a2 2 0 0 1-2 2h-2"}],["path",{d:"M7 21H5a2 2 0 0 1-2-2v-2"}],["path",{d:"M7 8h8"}],["path",{d:"M7 12h10"}],["path",{d:"M7 16h6"}]],Gv=[["path",{d:"M3 7V5a2 2 0 0 1 2-2h2"}],["path",{d:"M17 3h2a2 2 0 0 1 2 2v2"}],["path",{d:"M21 17v2a2 2 0 0 1-2 2h-2"}],["path",{d:"M7 21H5a2 2 0 0 1-2-2v-2"}]],Iv=[["path",{d:"M14 21v-3a2 2 0 0 0-4 0v3"}],["path",{d:"M18 5v16"}],["path",{d:"m4 6 7.106-3.79a2 2 0 0 1 1.788 0L20 6"}],["path",{d:"m6 11-3.52 2.147a1 1 0 0 0-.48.854V19a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5a1 1 0 0 0-.48-.853L18 11"}],["path",{d:"M6 5v16"}],["circle",{cx:"12",cy:"9",r:"2"}]],Wv=[["path",{d:"M5.42 9.42 8 12"}],["circle",{cx:"4",cy:"8",r:"2"}],["path",{d:"m14 6-8.58 8.58"}],["circle",{cx:"4",cy:"16",r:"2"}],["path",{d:"M10.8 14.8 14 18"}],["path",{d:"M16 12h-2"}],["path",{d:"M22 12h-2"}]],Ev=[["circle",{cx:"6",cy:"6",r:"3"}],["path",{d:"M8.12 8.12 12 12"}],["path",{d:"M20 4 8.12 15.88"}],["circle",{cx:"6",cy:"18",r:"3"}],["path",{d:"M14.8 14.8 20 20"}]],Xv=[["path",{d:"M13 3H4a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-3"}],["path",{d:"M8 21h8"}],["path",{d:"M12 17v4"}],["path",{d:"m22 3-5 5"}],["path",{d:"m17 3 5 5"}]],jv=[["path",{d:"M13 3H4a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-3"}],["path",{d:"M8 21h8"}],["path",{d:"M12 17v4"}],["path",{d:"m17 8 5-5"}],["path",{d:"M17 3h5v5"}]],Nv=[["path",{d:"M15 12h-5"}],["path",{d:"M15 8h-5"}],["path",{d:"M19 17V5a2 2 0 0 0-2-2H4"}],["path",{d:"M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3"}]],Kv=[["path",{d:"M19 17V5a2 2 0 0 0-2-2H4"}],["path",{d:"M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3"}]],Qv=[["path",{d:"m8 11 2 2 4-4"}],["circle",{cx:"11",cy:"11",r:"8"}],["path",{d:"m21 21-4.3-4.3"}]],Jv=[["path",{d:"m13 13.5 2-2.5-2-2.5"}],["path",{d:"m21 21-4.3-4.3"}],["path",{d:"M9 8.5 7 11l2 2.5"}],["circle",{cx:"11",cy:"11",r:"8"}]],Yv=[["path",{d:"m13.5 8.5-5 5"}],["circle",{cx:"11",cy:"11",r:"8"}],["path",{d:"m21 21-4.3-4.3"}]],_v=[["path",{d:"m13.5 8.5-5 5"}],["path",{d:"m8.5 8.5 5 5"}],["circle",{cx:"11",cy:"11",r:"8"}],["path",{d:"m21 21-4.3-4.3"}]],xv=[["path",{d:"m21 21-4.34-4.34"}],["circle",{cx:"11",cy:"11",r:"8"}]],a$=[["path",{d:"M16 5a4 3 0 0 0-8 0c0 4 8 3 8 7a4 3 0 0 1-8 0"}],["path",{d:"M8 19a4 3 0 0 0 8 0c0-4-8-3-8-7a4 3 0 0 1 8 0"}]],T2=[["path",{d:"M3.714 3.048a.498.498 0 0 0-.683.627l2.843 7.627a2 2 0 0 1 0 1.396l-2.842 7.627a.498.498 0 0 0 .682.627l18-8.5a.5.5 0 0 0 0-.904z"}],["path",{d:"M6 12h16"}]],t$=[["rect",{x:"14",y:"14",width:"8",height:"8",rx:"2"}],["rect",{x:"2",y:"2",width:"8",height:"8",rx:"2"}],["path",{d:"M7 14v1a2 2 0 0 0 2 2h1"}],["path",{d:"M14 7h1a2 2 0 0 1 2 2v1"}]],h$=[["path",{d:"M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z"}],["path",{d:"m21.854 2.147-10.94 10.939"}]],d$=[["path",{d:"m16 16-4 4-4-4"}],["path",{d:"M3 12h18"}],["path",{d:"m8 8 4-4 4 4"}]],c$=[["path",{d:"M12 3v18"}],["path",{d:"m16 16 4-4-4-4"}],["path",{d:"m8 8-4 4 4 4"}]],M$=[["path",{d:"m10.852 14.772-.383.923"}],["path",{d:"M13.148 14.772a3 3 0 1 0-2.296-5.544l-.383-.923"}],["path",{d:"m13.148 9.228.383-.923"}],["path",{d:"m13.53 15.696-.382-.924a3 3 0 1 1-2.296-5.544"}],["path",{d:"m14.772 10.852.923-.383"}],["path",{d:"m14.772 13.148.923.383"}],["path",{d:"M4.5 10H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-.5"}],["path",{d:"M4.5 14H4a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2h-.5"}],["path",{d:"M6 18h.01"}],["path",{d:"M6 6h.01"}],["path",{d:"m9.228 10.852-.923-.383"}],["path",{d:"m9.228 13.148-.923.383"}]],p$=[["path",{d:"M6 10H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-2"}],["path",{d:"M6 14H4a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2h-2"}],["path",{d:"M6 6h.01"}],["path",{d:"M6 18h.01"}],["path",{d:"m13 6-4 6h6l-4 6"}]],i$=[["path",{d:"M7 2h13a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-5"}],["path",{d:"M10 10 2.5 2.5C2 2 2 2.5 2 5v3a2 2 0 0 0 2 2h6z"}],["path",{d:"M22 17v-1a2 2 0 0 0-2-2h-1"}],["path",{d:"M4 14a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h16.5l1-.5.5.5-8-8H4z"}],["path",{d:"M6 18h.01"}],["path",{d:"m2 2 20 20"}]],n$=[["rect",{width:"20",height:"8",x:"2",y:"2",rx:"2",ry:"2"}],["rect",{width:"20",height:"8",x:"2",y:"14",rx:"2",ry:"2"}],["line",{x1:"6",x2:"6.01",y1:"6",y2:"6"}],["line",{x1:"6",x2:"6.01",y1:"18",y2:"18"}]],l$=[["path",{d:"M14 17H5"}],["path",{d:"M19 7h-9"}],["circle",{cx:"17",cy:"17",r:"3"}],["circle",{cx:"7",cy:"7",r:"3"}]],e$=[["path",{d:"M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915"}],["circle",{cx:"12",cy:"12",r:"3"}]],r$=[["circle",{cx:"18",cy:"5",r:"3"}],["circle",{cx:"6",cy:"12",r:"3"}],["circle",{cx:"18",cy:"19",r:"3"}],["line",{x1:"8.59",x2:"15.42",y1:"13.51",y2:"17.49"}],["line",{x1:"15.41",x2:"8.59",y1:"6.51",y2:"10.49"}]],o$=[["path",{d:"M8.3 10a.7.7 0 0 1-.626-1.079L11.4 3a.7.7 0 0 1 1.198-.043L16.3 8.9a.7.7 0 0 1-.572 1.1Z"}],["rect",{x:"3",y:"14",width:"7",height:"7",rx:"1"}],["circle",{cx:"17.5",cy:"17.5",r:"3.5"}]],v$=[["path",{d:"M12 2v13"}],["path",{d:"m16 6-4-4-4 4"}],["path",{d:"M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"}]],$$=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2"}],["line",{x1:"3",x2:"21",y1:"9",y2:"9"}],["line",{x1:"3",x2:"21",y1:"15",y2:"15"}],["line",{x1:"9",x2:"9",y1:"9",y2:"21"}],["line",{x1:"15",x2:"15",y1:"9",y2:"21"}]],m$=[["path",{d:"M14 11a2 2 0 1 1-4 0 4 4 0 0 1 8 0 6 6 0 0 1-12 0 8 8 0 0 1 16 0 10 10 0 1 1-20 0 11.93 11.93 0 0 1 2.42-7.22 2 2 0 1 1 3.16 2.44"}]],y$=[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"}],["path",{d:"M12 8v4"}],["path",{d:"M12 16h.01"}]],s$=[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"}],["path",{d:"m4.243 5.21 14.39 12.472"}]],g$=[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"}],["path",{d:"m9 12 2 2 4-4"}]],u$=[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"}],["path",{d:"M8 12h.01"}],["path",{d:"M12 12h.01"}],["path",{d:"M16 12h.01"}]],C$=[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"}],["path",{d:"M12 22V2"}]],H$=[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"}],["path",{d:"M9 12h6"}]],A$=[["path",{d:"m2 2 20 20"}],["path",{d:"M5 5a1 1 0 0 0-1 1v7c0 5 3.5 7.5 7.67 8.94a1 1 0 0 0 .67.01c2.35-.82 4.48-1.97 5.9-3.71"}],["path",{d:"M9.309 3.652A12.252 12.252 0 0 0 11.24 2.28a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1v7a9.784 9.784 0 0 1-.08 1.264"}]],w$=[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"}],["path",{d:"M9 12h6"}],["path",{d:"M12 9v6"}]],q2=[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"}],["path",{d:"M9.1 9a3 3 0 0 1 5.82 1c0 2-3 3-3 3"}],["path",{d:"M12 17h.01"}]],V$=[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"}],["path",{d:"M6.376 18.91a6 6 0 0 1 11.249.003"}],["circle",{cx:"12",cy:"11",r:"4"}]],U2=[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"}],["path",{d:"m14.5 9.5-5 5"}],["path",{d:"m9.5 9.5 5 5"}]],S$=[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"}]],L$=[["circle",{cx:"12",cy:"12",r:"8"}],["path",{d:"M12 2v7.5"}],["path",{d:"m19 5-5.23 5.23"}],["path",{d:"M22 12h-7.5"}],["path",{d:"m19 19-5.23-5.23"}],["path",{d:"M12 14.5V22"}],["path",{d:"M10.23 13.77 5 19"}],["path",{d:"M9.5 12H2"}],["path",{d:"M10.23 10.23 5 5"}],["circle",{cx:"12",cy:"12",r:"2.5"}]],f$=[["path",{d:"M12 10.189V14"}],["path",{d:"M12 2v3"}],["path",{d:"M19 13V7a2 2 0 0 0-2-2H7a2 2 0 0 0-2 2v6"}],["path",{d:"M19.38 20A11.6 11.6 0 0 0 21 14l-8.188-3.639a2 2 0 0 0-1.624 0L3 14a11.6 11.6 0 0 0 2.81 7.76"}],["path",{d:"M2 21c.6.5 1.2 1 2.5 1 2.5 0 2.5-2 5-2 1.3 0 1.9.5 2.5 1s1.2 1 2.5 1c2.5 0 2.5-2 5-2 1.3 0 1.9.5 2.5 1"}]],k$=[["path",{d:"M20.38 3.46 16 2a4 4 0 0 1-8 0L3.62 3.46a2 2 0 0 0-1.34 2.23l.58 3.47a1 1 0 0 0 .99.84H6v10c0 1.1.9 2 2 2h8a2 2 0 0 0 2-2V10h2.15a1 1 0 0 0 .99-.84l.58-3.47a2 2 0 0 0-1.34-2.23z"}]],P$=[["path",{d:"M16 10a4 4 0 0 1-8 0"}],["path",{d:"M3.103 6.034h17.794"}],["path",{d:"M3.4 5.467a2 2 0 0 0-.4 1.2V20a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6.667a2 2 0 0 0-.4-1.2l-2-2.667A2 2 0 0 0 17 2H7a2 2 0 0 0-1.6.8z"}]],B$=[["path",{d:"m15 11-1 9"}],["path",{d:"m19 11-4-7"}],["path",{d:"M2 11h20"}],["path",{d:"m3.5 11 1.6 7.4a2 2 0 0 0 2 1.6h9.8a2 2 0 0 0 2-1.6l1.7-7.4"}],["path",{d:"M4.5 15.5h15"}],["path",{d:"m5 11 4-7"}],["path",{d:"m9 11 1 9"}]],D$=[["circle",{cx:"8",cy:"21",r:"1"}],["circle",{cx:"19",cy:"21",r:"1"}],["path",{d:"M2.05 2.05h2l2.66 12.42a2 2 0 0 0 2 1.58h9.78a2 2 0 0 0 1.95-1.57l1.65-7.43H5.12"}]],F$=[["path",{d:"M21.56 4.56a1.5 1.5 0 0 1 0 2.122l-.47.47a3 3 0 0 1-4.212-.03 3 3 0 0 1 0-4.243l.44-.44a1.5 1.5 0 0 1 2.121 0z"}],["path",{d:"M3 22a1 1 0 0 1-1-1v-3.586a1 1 0 0 1 .293-.707l3.355-3.355a1.205 1.205 0 0 1 1.704 0l3.296 3.296a1.205 1.205 0 0 1 0 1.704l-3.355 3.355a1 1 0 0 1-.707.293z"}],["path",{d:"m9 15 7.879-7.878"}]],z$=[["path",{d:"m4 4 2.5 2.5"}],["path",{d:"M13.5 6.5a4.95 4.95 0 0 0-7 7"}],["path",{d:"M15 5 5 15"}],["path",{d:"M14 17v.01"}],["path",{d:"M10 16v.01"}],["path",{d:"M13 13v.01"}],["path",{d:"M16 10v.01"}],["path",{d:"M11 20v.01"}],["path",{d:"M17 14v.01"}],["path",{d:"M20 11v.01"}]],b$=[["path",{d:"M10 22v-5"}],["path",{d:"M14 19v-2"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}],["path",{d:"M18 20v-3"}],["path",{d:"M2 13h20"}],["path",{d:"M20 13V7l-5-5H6a2 2 0 0 0-2 2v9"}],["path",{d:"M6 20v-3"}]],R$=[["path",{d:"M11 12h.01"}],["path",{d:"M13 22c.5-.5 1.12-1 2.5-1-1.38 0-2-.5-2.5-1"}],["path",{d:"M14 2a3.28 3.28 0 0 1-3.227 1.798l-6.17-.561A2.387 2.387 0 1 0 4.387 8H15.5a1 1 0 0 1 0 13 1 1 0 0 0 0-5H12a7 7 0 0 1-7-7V8"}],["path",{d:"M14 8a8.5 8.5 0 0 1 0 8"}],["path",{d:"M16 16c2 0 4.5-4 4-6"}]],T$=[["path",{d:"m15 15 6 6m-6-6v4.8m0-4.8h4.8"}],["path",{d:"M9 19.8V15m0 0H4.2M9 15l-6 6"}],["path",{d:"M15 4.2V9m0 0h4.8M15 9l6-6"}],["path",{d:"M9 4.2V9m0 0H4.2M9 9 3 3"}]],q$=[["path",{d:"M12 22v-5.172a2 2 0 0 0-.586-1.414L9.5 13.5"}],["path",{d:"M14.5 14.5 12 17"}],["path",{d:"M17 8.8A6 6 0 0 1 13.8 20H10A6.5 6.5 0 0 1 7 8a5 5 0 0 1 10 0z"}]],U$=[["path",{d:"m18 14 4 4-4 4"}],["path",{d:"m18 2 4 4-4 4"}],["path",{d:"M2 18h1.973a4 4 0 0 0 3.3-1.7l5.454-8.6a4 4 0 0 1 3.3-1.7H22"}],["path",{d:"M2 6h1.972a4 4 0 0 1 3.6 2.2"}],["path",{d:"M22 18h-6.041a4 4 0 0 1-3.3-1.8l-.359-.45"}]],O$=[["path",{d:"M18 7V5a1 1 0 0 0-1-1H6.5a.5.5 0 0 0-.4.8l4.5 6a2 2 0 0 1 0 2.4l-4.5 6a.5.5 0 0 0 .4.8H17a1 1 0 0 0 1-1v-2"}]],Z$=[["path",{d:"M2 20h.01"}],["path",{d:"M7 20v-4"}],["path",{d:"M12 20v-8"}],["path",{d:"M17 20V8"}]],G$=[["path",{d:"M2 20h.01"}],["path",{d:"M7 20v-4"}]],I$=[["path",{d:"M2 20h.01"}],["path",{d:"M7 20v-4"}],["path",{d:"M12 20v-8"}]],W$=[["path",{d:"M2 20h.01"}]],E$=[["path",{d:"M2 20h.01"}],["path",{d:"M7 20v-4"}],["path",{d:"M12 20v-8"}],["path",{d:"M17 20V8"}],["path",{d:"M22 4v16"}]],X$=[["path",{d:"m21 17-2.156-1.868A.5.5 0 0 0 18 15.5v.5a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1c0-2.545-3.991-3.97-8.5-4a1 1 0 0 0 0 5c4.153 0 4.745-11.295 5.708-13.5a2.5 2.5 0 1 1 3.31 3.284"}],["path",{d:"M3 21h18"}]],j$=[["path",{d:"M10 9H4L2 7l2-2h6"}],["path",{d:"M14 5h6l2 2-2 2h-6"}],["path",{d:"M10 22V4a2 2 0 1 1 4 0v18"}],["path",{d:"M8 22h8"}]],N$=[["path",{d:"M12 13v8"}],["path",{d:"M12 3v3"}],["path",{d:"M18 6a2 2 0 0 1 1.387.56l2.307 2.22a1 1 0 0 1 0 1.44l-2.307 2.22A2 2 0 0 1 18 13H6a2 2 0 0 1-1.387-.56l-2.306-2.22a1 1 0 0 1 0-1.44l2.306-2.22A2 2 0 0 1 6 6z"}]],K$=[["path",{d:"M7 18v-6a5 5 0 1 1 10 0v6"}],["path",{d:"M5 21a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-1a2 2 0 0 0-2-2H7a2 2 0 0 0-2 2z"}],["path",{d:"M21 12h1"}],["path",{d:"M18.5 4.5 18 5"}],["path",{d:"M2 12h1"}],["path",{d:"M12 2v1"}],["path",{d:"m4.929 4.929.707.707"}],["path",{d:"M12 12v6"}]],Q$=[["path",{d:"M17.971 4.285A2 2 0 0 1 21 6v12a2 2 0 0 1-3.029 1.715l-9.997-5.998a2 2 0 0 1-.003-3.432z"}],["path",{d:"M3 20V4"}]],J$=[["path",{d:"M21 4v16"}],["path",{d:"M6.029 4.285A2 2 0 0 0 3 6v12a2 2 0 0 0 3.029 1.715l9.997-5.998a2 2 0 0 0 .003-3.432z"}]],Y$=[["path",{d:"m12.5 17-.5-1-.5 1h1z"}],["path",{d:"M15 22a1 1 0 0 0 1-1v-1a2 2 0 0 0 1.56-3.25 8 8 0 1 0-11.12 0A2 2 0 0 0 8 20v1a1 1 0 0 0 1 1z"}],["circle",{cx:"15",cy:"12",r:"1"}],["circle",{cx:"9",cy:"12",r:"1"}]],_$=[["rect",{width:"3",height:"8",x:"13",y:"2",rx:"1.5"}],["path",{d:"M19 8.5V10h1.5A1.5 1.5 0 1 0 19 8.5"}],["rect",{width:"3",height:"8",x:"8",y:"14",rx:"1.5"}],["path",{d:"M5 15.5V14H3.5A1.5 1.5 0 1 0 5 15.5"}],["rect",{width:"8",height:"3",x:"14",y:"13",rx:"1.5"}],["path",{d:"M15.5 19H14v1.5a1.5 1.5 0 1 0 1.5-1.5"}],["rect",{width:"8",height:"3",x:"2",y:"8",rx:"1.5"}],["path",{d:"M8.5 5H10V3.5A1.5 1.5 0 1 0 8.5 5"}]],x$=[["path",{d:"M22 2 2 22"}]],am=[["path",{d:"M11 16.586V19a1 1 0 0 1-1 1H2L18.37 3.63a1 1 0 1 1 3 3l-9.663 9.663a1 1 0 0 1-1.414 0L8 14"}]],tm=[["path",{d:"M10 5H3"}],["path",{d:"M12 19H3"}],["path",{d:"M14 3v4"}],["path",{d:"M16 17v4"}],["path",{d:"M21 12h-9"}],["path",{d:"M21 19h-5"}],["path",{d:"M21 5h-7"}],["path",{d:"M8 10v4"}],["path",{d:"M8 12H3"}]],O2=[["path",{d:"M10 8h4"}],["path",{d:"M12 21v-9"}],["path",{d:"M12 8V3"}],["path",{d:"M17 16h4"}],["path",{d:"M19 12V3"}],["path",{d:"M19 21v-5"}],["path",{d:"M3 14h4"}],["path",{d:"M5 10V3"}],["path",{d:"M5 21v-7"}]],hm=[["rect",{width:"14",height:"20",x:"5",y:"2",rx:"2",ry:"2"}],["path",{d:"M12.667 8 10 12h4l-2.667 4"}]],dm=[["rect",{width:"7",height:"12",x:"2",y:"6",rx:"1"}],["path",{d:"M13 8.32a7.43 7.43 0 0 1 0 7.36"}],["path",{d:"M16.46 6.21a11.76 11.76 0 0 1 0 11.58"}],["path",{d:"M19.91 4.1a15.91 15.91 0 0 1 .01 15.8"}]],cm=[["rect",{width:"14",height:"20",x:"5",y:"2",rx:"2",ry:"2"}],["path",{d:"M12 18h.01"}]],Mm=[["path",{d:"M22 11v1a10 10 0 1 1-9-10"}],["path",{d:"M8 14s1.5 2 4 2 4-2 4-2"}],["line",{x1:"9",x2:"9.01",y1:"9",y2:"9"}],["line",{x1:"15",x2:"15.01",y1:"9",y2:"9"}],["path",{d:"M16 5h6"}],["path",{d:"M19 2v6"}]],pm=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M8 14s1.5 2 4 2 4-2 4-2"}],["line",{x1:"9",x2:"9.01",y1:"9",y2:"9"}],["line",{x1:"15",x2:"15.01",y1:"9",y2:"9"}]],im=[["path",{d:"M2 13a6 6 0 1 0 12 0 4 4 0 1 0-8 0 2 2 0 0 0 4 0"}],["circle",{cx:"10",cy:"13",r:"8"}],["path",{d:"M2 21h12c4.4 0 8-3.6 8-8V7a2 2 0 1 0-4 0v6"}],["path",{d:"M18 3 19.1 5.2"}],["path",{d:"M22 3 20.9 5.2"}]],nm=[["path",{d:"m10 20-1.25-2.5L6 18"}],["path",{d:"M10 4 8.75 6.5 6 6"}],["path",{d:"m14 20 1.25-2.5L18 18"}],["path",{d:"m14 4 1.25 2.5L18 6"}],["path",{d:"m17 21-3-6h-4"}],["path",{d:"m17 3-3 6 1.5 3"}],["path",{d:"M2 12h6.5L10 9"}],["path",{d:"m20 10-1.5 2 1.5 2"}],["path",{d:"M22 12h-6.5L14 15"}],["path",{d:"m4 10 1.5 2L4 14"}],["path",{d:"m7 21 3-6-1.5-3"}],["path",{d:"m7 3 3 6h4"}]],lm=[["path",{d:"M20 9V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v3"}],["path",{d:"M2 16a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5a2 2 0 0 0-4 0v1.5a.5.5 0 0 1-.5.5h-11a.5.5 0 0 1-.5-.5V11a2 2 0 0 0-4 0z"}],["path",{d:"M4 18v2"}],["path",{d:"M20 18v2"}],["path",{d:"M12 4v9"}]],em=[["path",{d:"M10.5 2v4"}],["path",{d:"M14 2H7a2 2 0 0 0-2 2"}],["path",{d:"M19.29 14.76A6.67 6.67 0 0 1 17 11a6.6 6.6 0 0 1-2.29 3.76c-1.15.92-1.71 2.04-1.71 3.19 0 2.22 1.8 4.05 4 4.05s4-1.83 4-4.05c0-1.16-.57-2.26-1.71-3.19"}],["path",{d:"M9.607 21H6a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h7V7a1 1 0 0 0-1-1H9a1 1 0 0 0-1 1v3"}]],rm=[["path",{d:"M12 21a9 9 0 0 0 9-9H3a9 9 0 0 0 9 9Z"}],["path",{d:"M7 21h10"}],["path",{d:"M19.5 12 22 6"}],["path",{d:"M16.25 3c.27.1.8.53.75 1.36-.06.83-.93 1.2-1 2.02-.05.78.34 1.24.73 1.62"}],["path",{d:"M11.25 3c.27.1.8.53.74 1.36-.05.83-.93 1.2-.98 2.02-.06.78.33 1.24.72 1.62"}],["path",{d:"M6.25 3c.27.1.8.53.75 1.36-.06.83-.93 1.2-1 2.02-.05.78.34 1.24.74 1.62"}]],om=[["path",{d:"M22 17v1c0 .5-.5 1-1 1H3c-.5 0-1-.5-1-1v-1"}]],vm=[["path",{d:"M12 18v4"}],["path",{d:"M2 14.499a5.5 5.5 0 0 0 9.591 3.675.6.6 0 0 1 .818.001A5.5 5.5 0 0 0 22 14.5c0-2.29-1.5-4-3-5.5l-5.492-5.312a2 2 0 0 0-3-.02L5 8.999c-1.5 1.5-3 3.2-3 5.5"}]],$m=[["path",{d:"M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z"}]],Z2=[["path",{d:"M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z"}],["path",{d:"M20 2v4"}],["path",{d:"M22 4h-4"}],["circle",{cx:"4",cy:"20",r:"2"}]],mm=[["rect",{width:"16",height:"20",x:"4",y:"2",rx:"2"}],["path",{d:"M12 6h.01"}],["circle",{cx:"12",cy:"14",r:"4"}],["path",{d:"M12 14h.01"}]],ym=[["path",{d:"M8.8 20v-4.1l1.9.2a2.3 2.3 0 0 0 2.164-2.1V8.3A5.37 5.37 0 0 0 2 8.25c0 2.8.656 3.054 1 4.55a5.77 5.77 0 0 1 .029 2.758L2 20"}],["path",{d:"M19.8 17.8a7.5 7.5 0 0 0 .003-10.603"}],["path",{d:"M17 15a3.5 3.5 0 0 0-.025-4.975"}]],sm=[["path",{d:"m6 16 6-12 6 12"}],["path",{d:"M8 12h8"}],["path",{d:"M4 21c1.1 0 1.1-1 2.3-1s1.1 1 2.3 1c1.1 0 1.1-1 2.3-1 1.1 0 1.1 1 2.3 1 1.1 0 1.1-1 2.3-1 1.1 0 1.1 1 2.3 1 1.1 0 1.1-1 2.3-1"}]],gm=[["path",{d:"m6 16 6-12 6 12"}],["path",{d:"M8 12h8"}],["path",{d:"m16 20 2 2 4-4"}]],um=[["path",{d:"M12.034 12.681a.498.498 0 0 1 .647-.647l9 3.5a.5.5 0 0 1-.033.943l-3.444 1.068a1 1 0 0 0-.66.66l-1.067 3.443a.5.5 0 0 1-.943.033z"}],["path",{d:"M5 17A12 12 0 0 1 17 5"}],["circle",{cx:"19",cy:"5",r:"2"}],["circle",{cx:"5",cy:"19",r:"2"}]],Cm=[["circle",{cx:"19",cy:"5",r:"2"}],["circle",{cx:"5",cy:"19",r:"2"}],["path",{d:"M5 17A12 12 0 0 1 17 5"}]],Hm=[["path",{d:"M16 3h5v5"}],["path",{d:"M8 3H3v5"}],["path",{d:"M12 22v-8.3a4 4 0 0 0-1.172-2.872L3 3"}],["path",{d:"m15 9 6-6"}]],Am=[["path",{d:"M17 13.44 4.442 17.082A2 2 0 0 0 4.982 21H19a2 2 0 0 0 .558-3.921l-1.115-.32A2 2 0 0 1 17 14.837V7.66"}],["path",{d:"m7 10.56 12.558-3.642A2 2 0 0 0 19.018 3H5a2 2 0 0 0-.558 3.921l1.115.32A2 2 0 0 1 7 9.163v7.178"}]],wm=[["path",{d:"M15.295 19.562 16 22"}],["path",{d:"m17 16 3.758 2.098"}],["path",{d:"m19 12.5 3.026-.598"}],["path",{d:"M7.61 6.3a3 3 0 0 0-3.92 1.3l-1.38 2.79a3 3 0 0 0 1.3 3.91l6.89 3.597a1 1 0 0 0 1.342-.447l3.106-6.211a1 1 0 0 0-.447-1.341z"}],["path",{d:"M8 9V2"}]],Vm=[["path",{d:"M3 3h.01"}],["path",{d:"M7 5h.01"}],["path",{d:"M11 7h.01"}],["path",{d:"M3 7h.01"}],["path",{d:"M7 9h.01"}],["path",{d:"M3 11h.01"}],["rect",{width:"4",height:"4",x:"15",y:"5"}],["path",{d:"m19 9 2 2v10c0 .6-.4 1-1 1h-6c-.6 0-1-.4-1-1V11l2-2"}],["path",{d:"m13 14 8-2"}],["path",{d:"m13 19 8-2"}]],Sm=[["path",{d:"M14 9.536V7a4 4 0 0 1 4-4h1.5a.5.5 0 0 1 .5.5V5a4 4 0 0 1-4 4 4 4 0 0 0-4 4c0 2 1 3 1 5a5 5 0 0 1-1 3"}],["path",{d:"M4 9a5 5 0 0 1 8 4 5 5 0 0 1-8-4"}],["path",{d:"M5 21h14"}]],G2=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M17 12h-2l-2 5-2-10-2 5H7"}]],I2=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"m16 8-8 8"}],["path",{d:"M16 16H8V8"}]],W2=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"m8 8 8 8"}],["path",{d:"M16 8v8H8"}]],E2=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M12 8v8"}],["path",{d:"m8 12 4 4 4-4"}]],X2=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"m12 8-4 4 4 4"}],["path",{d:"M16 12H8"}]],j2=[["path",{d:"M13 21h6a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v6"}],["path",{d:"m3 21 9-9"}],["path",{d:"M9 21H3v-6"}]],N2=[["path",{d:"M21 11V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h6"}],["path",{d:"m21 21-9-9"}],["path",{d:"M21 15v6h-6"}]],K2=[["path",{d:"M13 3h6a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-6"}],["path",{d:"m3 3 9 9"}],["path",{d:"M3 9V3h6"}]],Q2=[["path",{d:"M21 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h6"}],["path",{d:"m21 3-9 9"}],["path",{d:"M15 3h6v6"}]],J2=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M8 12h8"}],["path",{d:"m12 16 4-4-4-4"}]],Y2=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M8 16V8h8"}],["path",{d:"M16 16 8 8"}]],_2=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M8 8h8v8"}],["path",{d:"m8 16 8-8"}]],x2=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"m16 12-4-4-4 4"}],["path",{d:"M12 16V8"}]],a0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M12 8v8"}],["path",{d:"m8.5 14 7-4"}],["path",{d:"m8.5 10 7 4"}]],t0=[["path",{d:"M4 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v16a2 2 0 0 1-2 2"}],["path",{d:"M10 22H8"}],["path",{d:"M16 22h-2"}],["circle",{cx:"8",cy:"8",r:"2"}],["path",{d:"M9.414 9.414 12 12"}],["path",{d:"M14.8 14.8 18 18"}],["circle",{cx:"8",cy:"16",r:"2"}],["path",{d:"m18 6-8.586 8.586"}]],$=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M9 8h7"}],["path",{d:"M8 12h6"}],["path",{d:"M11 16h5"}]],h0=[["path",{d:"M21 10.656V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h12.344"}],["path",{d:"m9 11 3 3L22 4"}]],d0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"m9 12 2 2 4-4"}]],c0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"m16 10-4 4-4-4"}]],M0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"m14 16-4-4 4-4"}]],p0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"m10 8 4 4-4 4"}]],i0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"m8 14 4-4 4 4"}]],n0=[["path",{d:"m10 9-3 3 3 3"}],["path",{d:"m14 15 3-3-3-3"}],["rect",{x:"3",y:"3",width:"18",height:"18",rx:"2"}]],Lm=[["path",{d:"M10 9.5 8 12l2 2.5"}],["path",{d:"M14 21h1"}],["path",{d:"m14 9.5 2 2.5-2 2.5"}],["path",{d:"M5 21a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2"}],["path",{d:"M9 21h1"}]],fm=[["path",{d:"M5 21a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2"}],["path",{d:"M9 21h1"}],["path",{d:"M14 21h1"}]],l0=[["path",{d:"M8 7v7"}],["path",{d:"M12 7v4"}],["path",{d:"M16 7v9"}],["path",{d:"M5 3a2 2 0 0 0-2 2"}],["path",{d:"M9 3h1"}],["path",{d:"M14 3h1"}],["path",{d:"M19 3a2 2 0 0 1 2 2"}],["path",{d:"M21 9v1"}],["path",{d:"M21 14v1"}],["path",{d:"M21 19a2 2 0 0 1-2 2"}],["path",{d:"M14 21h1"}],["path",{d:"M9 21h1"}],["path",{d:"M5 21a2 2 0 0 1-2-2"}],["path",{d:"M3 14v1"}],["path",{d:"M3 9v1"}]],e0=[["path",{d:"M12.034 12.681a.498.498 0 0 1 .647-.647l9 3.5a.5.5 0 0 1-.033.943l-3.444 1.068a1 1 0 0 0-.66.66l-1.067 3.443a.5.5 0 0 1-.943.033z"}],["path",{d:"M5 3a2 2 0 0 0-2 2"}],["path",{d:"M19 3a2 2 0 0 1 2 2"}],["path",{d:"M5 21a2 2 0 0 1-2-2"}],["path",{d:"M9 3h1"}],["path",{d:"M9 21h2"}],["path",{d:"M14 3h1"}],["path",{d:"M3 9v1"}],["path",{d:"M21 9v2"}],["path",{d:"M3 14v1"}]],km=[["path",{d:"M14 21h1"}],["path",{d:"M21 14v1"}],["path",{d:"M21 19a2 2 0 0 1-2 2"}],["path",{d:"M21 9v1"}],["path",{d:"M3 14v1"}],["path",{d:"M3 5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2"}],["path",{d:"M3 9v1"}],["path",{d:"M5 21a2 2 0 0 1-2-2"}],["path",{d:"M9 21h1"}]],r0=[["path",{d:"M5 3a2 2 0 0 0-2 2"}],["path",{d:"M19 3a2 2 0 0 1 2 2"}],["path",{d:"M21 19a2 2 0 0 1-2 2"}],["path",{d:"M5 21a2 2 0 0 1-2-2"}],["path",{d:"M9 3h1"}],["path",{d:"M9 21h1"}],["path",{d:"M14 3h1"}],["path",{d:"M14 21h1"}],["path",{d:"M3 9v1"}],["path",{d:"M21 9v1"}],["path",{d:"M3 14v1"}],["path",{d:"M21 14v1"}]],o0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2"}],["line",{x1:"8",x2:"16",y1:"12",y2:"12"}],["line",{x1:"12",x2:"12",y1:"16",y2:"16"}],["line",{x1:"12",x2:"12",y1:"8",y2:"8"}]],v0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["circle",{cx:"12",cy:"12",r:"1"}]],$0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M7 10h10"}],["path",{d:"M7 14h10"}]],m0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2"}],["path",{d:"M9 17c2 0 2.8-1 2.8-2.8V10c0-2 1-3.3 3.2-3"}],["path",{d:"M9 11.2h5.7"}]],y0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M8 7v7"}],["path",{d:"M12 7v4"}],["path",{d:"M16 7v9"}]],s0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M7 7v10"}],["path",{d:"M11 7v10"}],["path",{d:"m15 7 2 10"}]],g0=[["path",{d:"M8 16V8.5a.5.5 0 0 1 .9-.3l2.7 3.599a.5.5 0 0 0 .8 0l2.7-3.6a.5.5 0 0 1 .9.3V16"}],["rect",{x:"3",y:"3",width:"18",height:"18",rx:"2"}]],u0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M7 8h10"}],["path",{d:"M7 12h10"}],["path",{d:"M7 16h10"}]],C0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M8 12h8"}]],H0=[["path",{d:"M12.034 12.681a.498.498 0 0 1 .647-.647l9 3.5a.5.5 0 0 1-.033.943l-3.444 1.068a1 1 0 0 0-.66.66l-1.067 3.443a.5.5 0 0 1-.943.033z"}],["path",{d:"M21 11V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h6"}]],A0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M9 17V7h4a3 3 0 0 1 0 6H9"}]],w0=[["path",{d:"M3.6 3.6A2 2 0 0 1 5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-.59 1.41"}],["path",{d:"M3 8.7V19a2 2 0 0 0 2 2h10.3"}],["path",{d:"m2 2 20 20"}],["path",{d:"M13 13a3 3 0 1 0 0-6H9v2"}],["path",{d:"M9 17v-2.3"}]],Pm=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["line",{x1:"10",x2:"10",y1:"15",y2:"9"}],["line",{x1:"14",x2:"14",y1:"15",y2:"9"}]],p=[["path",{d:"M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"}],["path",{d:"M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z"}]],V0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"m15 9-6 6"}],["path",{d:"M9 9h.01"}],["path",{d:"M15 15h.01"}]],S0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M7 7h10"}],["path",{d:"M10 7v10"}],["path",{d:"M16 17a2 2 0 0 1-2-2V7"}]],L0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M12 12H9.5a2.5 2.5 0 0 1 0-5H17"}],["path",{d:"M12 7v10"}],["path",{d:"M16 7v10"}]],f0=[["rect",{x:"3",y:"3",width:"18",height:"18",rx:"2"}],["path",{d:"M9 9.003a1 1 0 0 1 1.517-.859l4.997 2.997a1 1 0 0 1 0 1.718l-4.997 2.997A1 1 0 0 1 9 14.996z"}]],k0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M8 12h8"}],["path",{d:"M12 8v8"}]],P0=[["path",{d:"M12 7v4"}],["path",{d:"M7.998 9.003a5 5 0 1 0 8-.005"}],["rect",{x:"3",y:"3",width:"18",height:"18",rx:"2"}]],Bm=[["path",{d:"M7 12h2l2 5 2-10h4"}],["rect",{x:"3",y:"3",width:"18",height:"18",rx:"2"}]],Dm=[["path",{d:"M21 11a8 8 0 0 0-8-8"}],["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"}]],B0=[["rect",{width:"20",height:"20",x:"2",y:"2",rx:"2"}],["circle",{cx:"8",cy:"8",r:"2"}],["path",{d:"M9.414 9.414 12 12"}],["path",{d:"M14.8 14.8 18 18"}],["circle",{cx:"8",cy:"16",r:"2"}],["path",{d:"m18 6-8.586 8.586"}]],D0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M16 8.9V7H8l4 5-4 5h8v-1.9"}]],F0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["line",{x1:"9",x2:"15",y1:"15",y2:"9"}]],z0=[["path",{d:"M8 19H5c-1 0-2-1-2-2V7c0-1 1-2 2-2h3"}],["path",{d:"M16 5h3c1 0 2 1 2 2v10c0 1-1 2-2 2h-3"}],["line",{x1:"12",x2:"12",y1:"4",y2:"20"}]],b0=[["path",{d:"M5 8V5c0-1 1-2 2-2h10c1 0 2 1 2 2v3"}],["path",{d:"M19 16v3c0 1-1 2-2 2H7c-1 0-2-1-2-2v-3"}],["line",{x1:"4",x2:"20",y1:"12",y2:"12"}]],Fm=[["rect",{x:"3",y:"3",width:"18",height:"18",rx:"2"}],["rect",{x:"8",y:"8",width:"8",height:"8",rx:"1"}]],zm=[["path",{d:"M4 10c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h4c1.1 0 2 .9 2 2"}],["path",{d:"M10 16c-1.1 0-2-.9-2-2v-4c0-1.1.9-2 2-2h4c1.1 0 2 .9 2 2"}],["rect",{width:"8",height:"8",x:"14",y:"14",rx:"2"}]],bm=[["path",{d:"M11.035 7.69a1 1 0 0 1 1.909.024l.737 1.452a1 1 0 0 0 .737.535l1.634.256a1 1 0 0 1 .588 1.806l-1.172 1.168a1 1 0 0 0-.282.866l.259 1.613a1 1 0 0 1-1.541 1.134l-1.465-.75a1 1 0 0 0-.912 0l-1.465.75a1 1 0 0 1-1.539-1.133l.258-1.613a1 1 0 0 0-.282-.866l-1.156-1.153a1 1 0 0 1 .572-1.822l1.633-.256a1 1 0 0 0 .737-.535z"}],["rect",{x:"3",y:"3",width:"18",height:"18",rx:"2"}]],Rm=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["rect",{x:"9",y:"9",width:"6",height:"6",rx:"1"}]],R0=[["path",{d:"m7 11 2-2-2-2"}],["path",{d:"M11 13h4"}],["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2"}]],T0=[["path",{d:"M18 21a6 6 0 0 0-12 0"}],["circle",{cx:"12",cy:"11",r:"4"}],["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}]],q0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["circle",{cx:"12",cy:"10",r:"3"}],["path",{d:"M7 21v-2a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v2"}]],U0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2"}],["path",{d:"m15 9-6 6"}],["path",{d:"m9 9 6 6"}]],Tm=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}]],qm=[["path",{d:"M16 12v2a2 2 0 0 1-2 2H9a1 1 0 0 0-1 1v3a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2h0"}],["path",{d:"M4 16a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v3a1 1 0 0 1-1 1h-5a2 2 0 0 0-2 2v2"}]],Um=[["path",{d:"M10 22a2 2 0 0 1-2-2"}],["path",{d:"M14 2a2 2 0 0 1 2 2"}],["path",{d:"M16 22h-2"}],["path",{d:"M2 10V8"}],["path",{d:"M2 4a2 2 0 0 1 2-2"}],["path",{d:"M20 8a2 2 0 0 1 2 2"}],["path",{d:"M22 14v2"}],["path",{d:"M22 20a2 2 0 0 1-2 2"}],["path",{d:"M4 16a2 2 0 0 1-2-2"}],["path",{d:"M8 10a2 2 0 0 1 2-2h5a1 1 0 0 1 1 1v5a2 2 0 0 1-2 2H9a1 1 0 0 1-1-1z"}],["path",{d:"M8 2h2"}]],Om=[["path",{d:"M10 22a2 2 0 0 1-2-2"}],["path",{d:"M16 22h-2"}],["path",{d:"M16 4a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h3a1 1 0 0 0 1-1v-5a2 2 0 0 1 2-2h5a1 1 0 0 0 1-1z"}],["path",{d:"M20 8a2 2 0 0 1 2 2"}],["path",{d:"M22 14v2"}],["path",{d:"M22 20a2 2 0 0 1-2 2"}]],Zm=[["path",{d:"M4 16a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v3a1 1 0 0 0 1 1h3a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H10a2 2 0 0 1-2-2v-3a1 1 0 0 0-1-1z"}]],Gm=[["path",{d:"M12 3c7.2 0 9 1.8 9 9s-1.8 9-9 9-9-1.8-9-9 1.8-9 9-9"}]],Im=[["path",{d:"M13.77 3.043a34 34 0 0 0-3.54 0"}],["path",{d:"M13.771 20.956a33 33 0 0 1-3.541.001"}],["path",{d:"M20.18 17.74c-.51 1.15-1.29 1.93-2.439 2.44"}],["path",{d:"M20.18 6.259c-.51-1.148-1.291-1.929-2.44-2.438"}],["path",{d:"M20.957 10.23a33 33 0 0 1 0 3.54"}],["path",{d:"M3.043 10.23a34 34 0 0 0 .001 3.541"}],["path",{d:"M6.26 20.179c-1.15-.508-1.93-1.29-2.44-2.438"}],["path",{d:"M6.26 3.82c-1.149.51-1.93 1.291-2.44 2.44"}]],Wm=[["path",{d:"M15.236 22a3 3 0 0 0-2.2-5"}],["path",{d:"M16 20a3 3 0 0 1 3-3h1a2 2 0 0 0 2-2v-2a4 4 0 0 0-4-4V4"}],["path",{d:"M18 13h.01"}],["path",{d:"M18 6a4 4 0 0 0-4 4 7 7 0 0 0-7 7c0-5 4-5 4-10.5a4.5 4.5 0 1 0-9 0 2.5 2.5 0 0 0 5 0C7 10 3 11 3 17c0 2.8 2.2 5 5 5h10"}]],Em=[["path",{d:"M14 13V8.5C14 7 15 7 15 5a3 3 0 0 0-6 0c0 2 1 2 1 3.5V13"}],["path",{d:"M20 15.5a2.5 2.5 0 0 0-2.5-2.5h-11A2.5 2.5 0 0 0 4 15.5V17a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1z"}],["path",{d:"M5 22h14"}]],Xm=[["path",{d:"M12 18.338a2.1 2.1 0 0 0-.987.244L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.12 2.12 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.12 2.12 0 0 0 1.597-1.16l2.309-4.679A.53.53 0 0 1 12 2"}]],jm=[["path",{d:"M8.34 8.34 2 9.27l5 4.87L5.82 21 12 17.77 18.18 21l-.59-3.43"}],["path",{d:"M18.42 12.76 22 9.27l-6.91-1L12 2l-1.44 2.91"}],["line",{x1:"2",x2:"22",y1:"2",y2:"22"}]],Nm=[["path",{d:"M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"}]],Km=[["path",{d:"M13.971 4.285A2 2 0 0 1 17 6v12a2 2 0 0 1-3.029 1.715l-9.997-5.998a2 2 0 0 1-.003-3.432z"}],["path",{d:"M21 20V4"}]],Qm=[["path",{d:"M11 2v2"}],["path",{d:"M5 2v2"}],["path",{d:"M5 3H4a2 2 0 0 0-2 2v4a6 6 0 0 0 12 0V5a2 2 0 0 0-2-2h-1"}],["path",{d:"M8 15a6 6 0 0 0 12 0v-3"}],["circle",{cx:"20",cy:"10",r:"2"}]],Jm=[["path",{d:"M10.029 4.285A2 2 0 0 0 7 6v12a2 2 0 0 0 3.029 1.715l9.997-5.998a2 2 0 0 0 .003-3.432z"}],["path",{d:"M3 4v16"}]],Ym=[["path",{d:"M15.5 3H5a2 2 0 0 0-2 2v14c0 1.1.9 2 2 2h14a2 2 0 0 0 2-2V8.5L15.5 3Z"}],["path",{d:"M14 3v4a2 2 0 0 0 2 2h4"}],["path",{d:"M8 13h.01"}],["path",{d:"M16 13h.01"}],["path",{d:"M10 16s.8 1 2 1c1.3 0 2-1 2-1"}]],_m=[["path",{d:"M16 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V8Z"}],["path",{d:"M15 3v4a2 2 0 0 0 2 2h4"}]],xm=[["path",{d:"M15 21v-5a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v5"}],["path",{d:"M17.774 10.31a1.12 1.12 0 0 0-1.549 0 2.5 2.5 0 0 1-3.451 0 1.12 1.12 0 0 0-1.548 0 2.5 2.5 0 0 1-3.452 0 1.12 1.12 0 0 0-1.549 0 2.5 2.5 0 0 1-3.77-3.248l2.889-4.184A2 2 0 0 1 7 2h10a2 2 0 0 1 1.653.873l2.895 4.192a2.5 2.5 0 0 1-3.774 3.244"}],["path",{d:"M4 10.95V19a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8.05"}]],ay=[["rect",{width:"20",height:"6",x:"2",y:"4",rx:"2"}],["rect",{width:"20",height:"6",x:"2",y:"14",rx:"2"}]],ty=[["rect",{width:"6",height:"20",x:"4",y:"2",rx:"2"}],["rect",{width:"6",height:"20",x:"14",y:"2",rx:"2"}]],hy=[["path",{d:"M16 4H9a3 3 0 0 0-2.83 4"}],["path",{d:"M14 12a4 4 0 0 1 0 8H6"}],["line",{x1:"4",x2:"20",y1:"12",y2:"12"}]],dy=[["path",{d:"m4 5 8 8"}],["path",{d:"m12 5-8 8"}],["path",{d:"M20 19h-4c0-1.5.44-2 1.5-2.5S20 15.33 20 14c0-.47-.17-.93-.48-1.29a2.11 2.11 0 0 0-2.62-.44c-.42.24-.74.62-.9 1.07"}]],cy=[["circle",{cx:"12",cy:"12",r:"4"}],["path",{d:"M12 4h.01"}],["path",{d:"M20 12h.01"}],["path",{d:"M12 20h.01"}],["path",{d:"M4 12h.01"}],["path",{d:"M17.657 6.343h.01"}],["path",{d:"M17.657 17.657h.01"}],["path",{d:"M6.343 17.657h.01"}],["path",{d:"M6.343 6.343h.01"}]],My=[["circle",{cx:"12",cy:"12",r:"4"}],["path",{d:"M12 3v1"}],["path",{d:"M12 20v1"}],["path",{d:"M3 12h1"}],["path",{d:"M20 12h1"}],["path",{d:"m18.364 5.636-.707.707"}],["path",{d:"m6.343 17.657-.707.707"}],["path",{d:"m5.636 5.636.707.707"}],["path",{d:"m17.657 17.657.707.707"}]],py=[["path",{d:"M12 2v2"}],["path",{d:"M14.837 16.385a6 6 0 1 1-7.223-7.222c.624-.147.97.66.715 1.248a4 4 0 0 0 5.26 5.259c.589-.255 1.396.09 1.248.715"}],["path",{d:"M16 12a4 4 0 0 0-4-4"}],["path",{d:"m19 5-1.256 1.256"}],["path",{d:"M20 12h2"}]],iy=[["path",{d:"M10 21v-1"}],["path",{d:"M10 4V3"}],["path",{d:"M10 9a3 3 0 0 0 0 6"}],["path",{d:"m14 20 1.25-2.5L18 18"}],["path",{d:"m14 4 1.25 2.5L18 6"}],["path",{d:"m17 21-3-6 1.5-3H22"}],["path",{d:"m17 3-3 6 1.5 3"}],["path",{d:"M2 12h1"}],["path",{d:"m20 10-1.5 2 1.5 2"}],["path",{d:"m3.64 18.36.7-.7"}],["path",{d:"m4.34 6.34-.7-.7"}]],ny=[["circle",{cx:"12",cy:"12",r:"4"}],["path",{d:"M12 2v2"}],["path",{d:"M12 20v2"}],["path",{d:"m4.93 4.93 1.41 1.41"}],["path",{d:"m17.66 17.66 1.41 1.41"}],["path",{d:"M2 12h2"}],["path",{d:"M20 12h2"}],["path",{d:"m6.34 17.66-1.41 1.41"}],["path",{d:"m19.07 4.93-1.41 1.41"}]],ly=[["path",{d:"M12 2v8"}],["path",{d:"m4.93 10.93 1.41 1.41"}],["path",{d:"M2 18h2"}],["path",{d:"M20 18h2"}],["path",{d:"m19.07 10.93-1.41 1.41"}],["path",{d:"M22 22H2"}],["path",{d:"m8 6 4-4 4 4"}],["path",{d:"M16 18a4 4 0 0 0-8 0"}]],ey=[["path",{d:"M12 10V2"}],["path",{d:"m4.93 10.93 1.41 1.41"}],["path",{d:"M2 18h2"}],["path",{d:"M20 18h2"}],["path",{d:"m19.07 10.93-1.41 1.41"}],["path",{d:"M22 22H2"}],["path",{d:"m16 6-4 4-4-4"}],["path",{d:"M16 18a4 4 0 0 0-8 0"}]],ry=[["path",{d:"M11 17a4 4 0 0 1-8 0V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2Z"}],["path",{d:"M16.7 13H19a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H7"}],["path",{d:"M 7 17h.01"}],["path",{d:"m11 8 2.3-2.3a2.4 2.4 0 0 1 3.404.004L18.6 7.6a2.4 2.4 0 0 1 .026 3.434L9.9 19.8"}]],oy=[["path",{d:"m4 19 8-8"}],["path",{d:"m12 19-8-8"}],["path",{d:"M20 12h-4c0-1.5.442-2 1.5-2.5S20 8.334 20 7.002c0-.472-.17-.93-.484-1.29a2.105 2.105 0 0 0-2.617-.436c-.42.239-.738.614-.899 1.06"}]],vy=[["path",{d:"M10 21V3h8"}],["path",{d:"M6 16h9"}],["path",{d:"M10 9.5h7"}]],$y=[["path",{d:"M11 19H4a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h5"}],["path",{d:"M13 5h7a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2h-5"}],["circle",{cx:"12",cy:"12",r:"3"}],["path",{d:"m18 22-3-3 3-3"}],["path",{d:"m6 2 3 3-3 3"}]],my=[["polyline",{points:"14.5 17.5 3 6 3 3 6 3 17.5 14.5"}],["line",{x1:"13",x2:"19",y1:"19",y2:"13"}],["line",{x1:"16",x2:"20",y1:"16",y2:"20"}],["line",{x1:"19",x2:"21",y1:"21",y2:"19"}]],yy=[["polyline",{points:"14.5 17.5 3 6 3 3 6 3 17.5 14.5"}],["line",{x1:"13",x2:"19",y1:"19",y2:"13"}],["line",{x1:"16",x2:"20",y1:"16",y2:"20"}],["line",{x1:"19",x2:"21",y1:"21",y2:"19"}],["polyline",{points:"14.5 6.5 18 3 21 3 21 6 17.5 9.5"}],["line",{x1:"5",x2:"9",y1:"14",y2:"18"}],["line",{x1:"7",x2:"4",y1:"17",y2:"20"}],["line",{x1:"3",x2:"5",y1:"19",y2:"21"}]],sy=[["path",{d:"m18 2 4 4"}],["path",{d:"m17 7 3-3"}],["path",{d:"M19 9 8.7 19.3c-1 1-2.5 1-3.4 0l-.6-.6c-1-1-1-2.5 0-3.4L15 5"}],["path",{d:"m9 11 4 4"}],["path",{d:"m5 19-3 3"}],["path",{d:"m14 4 6 6"}]],gy=[["path",{d:"M9 3H5a2 2 0 0 0-2 2v4m6-6h10a2 2 0 0 1 2 2v4M9 3v18m0 0h10a2 2 0 0 0 2-2V9M9 21H5a2 2 0 0 1-2-2V9m0 0h18"}]],uy=[["path",{d:"M12 21v-6"}],["path",{d:"M12 9V3"}],["path",{d:"M3 15h18"}],["path",{d:"M3 9h18"}],["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}]],Cy=[["path",{d:"M14 14v2"}],["path",{d:"M14 20v2"}],["path",{d:"M14 2v2"}],["path",{d:"M14 8v2"}],["path",{d:"M2 15h8"}],["path",{d:"M2 3h6a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H2"}],["path",{d:"M2 9h8"}],["path",{d:"M22 15h-4"}],["path",{d:"M22 3h-2a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h2"}],["path",{d:"M22 9h-4"}],["path",{d:"M5 3v18"}]],Hy=[["path",{d:"M12 15V9"}],["path",{d:"M3 15h18"}],["path",{d:"M3 9h18"}],["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}]],Ay=[["path",{d:"M15 3v18"}],["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M21 9H3"}],["path",{d:"M21 15H3"}]],wy=[["path",{d:"M16 5H3"}],["path",{d:"M16 12H3"}],["path",{d:"M16 19H3"}],["path",{d:"M21 5h.01"}],["path",{d:"M21 12h.01"}],["path",{d:"M21 19h.01"}]],Vy=[["path",{d:"M14 10h2"}],["path",{d:"M15 22v-8"}],["path",{d:"M15 2v4"}],["path",{d:"M2 10h2"}],["path",{d:"M20 10h2"}],["path",{d:"M3 19h18"}],["path",{d:"M3 22v-6a2 2 135 0 1 2-2h14a2 2 45 0 1 2 2v6"}],["path",{d:"M3 2v2a2 2 45 0 0 2 2h14a2 2 135 0 0 2-2V2"}],["path",{d:"M8 10h2"}],["path",{d:"M9 22v-8"}],["path",{d:"M9 2v4"}]],Sy=[["path",{d:"M12 3v18"}],["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M3 9h18"}],["path",{d:"M3 15h18"}]],Ly=[["rect",{width:"10",height:"14",x:"3",y:"8",rx:"2"}],["path",{d:"M5 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v16a2 2 0 0 1-2 2h-2.4"}],["path",{d:"M8 18h.01"}]],fy=[["rect",{width:"16",height:"20",x:"4",y:"2",rx:"2",ry:"2"}],["line",{x1:"12",x2:"12.01",y1:"18",y2:"18"}]],ky=[["circle",{cx:"7",cy:"7",r:"5"}],["circle",{cx:"17",cy:"17",r:"5"}],["path",{d:"M12 17h10"}],["path",{d:"m3.46 10.54 7.08-7.08"}]],Py=[["path",{d:"M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z"}],["circle",{cx:"7.5",cy:"7.5",r:".5",fill:"currentColor"}]],By=[["path",{d:"M4 4v16"}]],Dy=[["path",{d:"M13.172 2a2 2 0 0 1 1.414.586l6.71 6.71a2.4 2.4 0 0 1 0 3.408l-4.592 4.592a2.4 2.4 0 0 1-3.408 0l-6.71-6.71A2 2 0 0 1 6 9.172V3a1 1 0 0 1 1-1z"}],["path",{d:"M2 7v6.172a2 2 0 0 0 .586 1.414l6.71 6.71a2.4 2.4 0 0 0 3.191.193"}],["circle",{cx:"10.5",cy:"6.5",r:".5",fill:"currentColor"}]],Fy=[["path",{d:"M4 4v16"}],["path",{d:"M9 4v16"}]],zy=[["path",{d:"M4 4v16"}],["path",{d:"M9 4v16"}],["path",{d:"M14 4v16"}]],by=[["path",{d:"M4 4v16"}],["path",{d:"M9 4v16"}],["path",{d:"M14 4v16"}],["path",{d:"M19 4v16"}]],Ry=[["path",{d:"M4 4v16"}],["path",{d:"M9 4v16"}],["path",{d:"M14 4v16"}],["path",{d:"M19 4v16"}],["path",{d:"M22 6 2 18"}]],Ty=[["circle",{cx:"17",cy:"4",r:"2"}],["path",{d:"M15.59 5.41 5.41 15.59"}],["circle",{cx:"4",cy:"17",r:"2"}],["path",{d:"M12 22s-4-9-1.5-11.5S22 12 22 12"}]],qy=[["circle",{cx:"12",cy:"12",r:"10"}],["circle",{cx:"12",cy:"12",r:"6"}],["circle",{cx:"12",cy:"12",r:"2"}]],Uy=[["path",{d:"m10.065 12.493-6.18 1.318a.934.934 0 0 1-1.108-.702l-.537-2.15a1.07 1.07 0 0 1 .691-1.265l13.504-4.44"}],["path",{d:"m13.56 11.747 4.332-.924"}],["path",{d:"m16 21-3.105-6.21"}],["path",{d:"M16.485 5.94a2 2 0 0 1 1.455-2.425l1.09-.272a1 1 0 0 1 1.212.727l1.515 6.06a1 1 0 0 1-.727 1.213l-1.09.272a2 2 0 0 1-2.425-1.455z"}],["path",{d:"m6.158 8.633 1.114 4.456"}],["path",{d:"m8 21 3.105-6.21"}],["circle",{cx:"12",cy:"13",r:"2"}]],Oy=[["circle",{cx:"4",cy:"4",r:"2"}],["path",{d:"m14 5 3-3 3 3"}],["path",{d:"m14 10 3-3 3 3"}],["path",{d:"M17 14V2"}],["path",{d:"M17 14H7l-5 8h20Z"}],["path",{d:"M8 14v8"}],["path",{d:"m9 14 5 8"}]],Zy=[["path",{d:"M3.5 21 14 3"}],["path",{d:"M20.5 21 10 3"}],["path",{d:"M15.5 21 12 15l-3.5 6"}],["path",{d:"M2 21h20"}]],Gy=[["path",{d:"M12 19h8"}],["path",{d:"m4 17 6-6-6-6"}]],O0=[["path",{d:"M21 7 6.82 21.18a2.83 2.83 0 0 1-3.99-.01a2.83 2.83 0 0 1 0-4L17 3"}],["path",{d:"m16 2 6 6"}],["path",{d:"M12 16H4"}]],Iy=[["path",{d:"M14.5 2v17.5c0 1.4-1.1 2.5-2.5 2.5c-1.4 0-2.5-1.1-2.5-2.5V2"}],["path",{d:"M8.5 2h7"}],["path",{d:"M14.5 16h-5"}]],Wy=[["path",{d:"M9 2v17.5A2.5 2.5 0 0 1 6.5 22A2.5 2.5 0 0 1 4 19.5V2"}],["path",{d:"M20 2v17.5a2.5 2.5 0 0 1-2.5 2.5a2.5 2.5 0 0 1-2.5-2.5V2"}],["path",{d:"M3 2h7"}],["path",{d:"M14 2h7"}],["path",{d:"M9 16H4"}],["path",{d:"M20 16h-5"}]],Z0=[["path",{d:"M21 5H3"}],["path",{d:"M17 12H7"}],["path",{d:"M19 19H5"}]],G0=[["path",{d:"M21 5H3"}],["path",{d:"M21 12H9"}],["path",{d:"M21 19H7"}]],I0=[["path",{d:"M3 5h18"}],["path",{d:"M3 12h18"}],["path",{d:"M3 19h18"}]],m=[["path",{d:"M21 5H3"}],["path",{d:"M15 12H3"}],["path",{d:"M17 19H3"}]],Ey=[["path",{d:"M12 20h-1a2 2 0 0 1-2-2 2 2 0 0 1-2 2H6"}],["path",{d:"M13 8h7a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-7"}],["path",{d:"M5 16H4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2h1"}],["path",{d:"M6 4h1a2 2 0 0 1 2 2 2 2 0 0 1 2-2h1"}],["path",{d:"M9 6v12"}]],Xy=[["path",{d:"M17 22h-1a4 4 0 0 1-4-4V6a4 4 0 0 1 4-4h1"}],["path",{d:"M7 22h1a4 4 0 0 0 4-4v-1"}],["path",{d:"M7 2h1a4 4 0 0 1 4 4v1"}]],W0=[["path",{d:"M15 5h6"}],["path",{d:"M15 12h6"}],["path",{d:"M3 19h18"}],["path",{d:"m3 12 3.553-7.724a.5.5 0 0 1 .894 0L11 12"}],["path",{d:"M3.92 10h6.16"}]],jy=[["path",{d:"M17 5H3"}],["path",{d:"M21 12H8"}],["path",{d:"M21 19H8"}],["path",{d:"M3 12v7"}]],Ny=[["path",{d:"M21 5H3"}],["path",{d:"M10 12H3"}],["path",{d:"M10 19H3"}],["circle",{cx:"17",cy:"15",r:"3"}],["path",{d:"m21 19-1.9-1.9"}]],E0=[["path",{d:"M14 21h1"}],["path",{d:"M14 3h1"}],["path",{d:"M19 3a2 2 0 0 1 2 2"}],["path",{d:"M21 14v1"}],["path",{d:"M21 19a2 2 0 0 1-2 2"}],["path",{d:"M21 9v1"}],["path",{d:"M3 14v1"}],["path",{d:"M3 9v1"}],["path",{d:"M5 21a2 2 0 0 1-2-2"}],["path",{d:"M5 3a2 2 0 0 0-2 2"}],["path",{d:"M7 12h10"}],["path",{d:"M7 16h6"}],["path",{d:"M7 8h8"}],["path",{d:"M9 21h1"}],["path",{d:"M9 3h1"}]],X0=[["path",{d:"m16 16-3 3 3 3"}],["path",{d:"M3 12h14.5a1 1 0 0 1 0 7H13"}],["path",{d:"M3 19h6"}],["path",{d:"M3 5h18"}]],Ky=[["path",{d:"M2 10s3-3 3-8"}],["path",{d:"M22 10s-3-3-3-8"}],["path",{d:"M10 2c0 4.4-3.6 8-8 8"}],["path",{d:"M14 2c0 4.4 3.6 8 8 8"}],["path",{d:"M2 10s2 2 2 5"}],["path",{d:"M22 10s-2 2-2 5"}],["path",{d:"M8 15h8"}],["path",{d:"M2 22v-1a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1"}],["path",{d:"M14 22v-1a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1"}]],Qy=[["path",{d:"m10 20-1.25-2.5L6 18"}],["path",{d:"M10 4 8.75 6.5 6 6"}],["path",{d:"M10.585 15H10"}],["path",{d:"M2 12h6.5L10 9"}],["path",{d:"M20 14.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0z"}],["path",{d:"m4 10 1.5 2L4 14"}],["path",{d:"m7 21 3-6-1.5-3"}],["path",{d:"m7 3 3 6h2"}]],Jy=[["path",{d:"M12 9a4 4 0 0 0-2 7.5"}],["path",{d:"M12 3v2"}],["path",{d:"m6.6 18.4-1.4 1.4"}],["path",{d:"M20 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z"}],["path",{d:"M4 13H2"}],["path",{d:"M6.34 7.34 4.93 5.93"}]],Yy=[["path",{d:"M14 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z"}]],_y=[["path",{d:"M17 14V2"}],["path",{d:"M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H20a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.76a2 2 0 0 0-1.79 1.11L12 22a3.13 3.13 0 0 1-3-3.88Z"}]],xy=[["path",{d:"M7 10v12"}],["path",{d:"M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z"}]],as=[["path",{d:"M2 9a3 3 0 0 1 0 6v2a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-2a3 3 0 0 1 0-6V7a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2Z"}],["path",{d:"m9 12 2 2 4-4"}]],ts=[["path",{d:"M2 9a3 3 0 0 1 0 6v2a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-2a3 3 0 0 1 0-6V7a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2Z"}],["path",{d:"M9 12h6"}]],hs=[["path",{d:"M2 9a3 3 0 1 1 0 6v2a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-2a3 3 0 1 1 0-6V7a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2Z"}],["path",{d:"M9 9h.01"}],["path",{d:"m15 9-6 6"}],["path",{d:"M15 15h.01"}]],ds=[["path",{d:"M2 9a3 3 0 0 1 0 6v2a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-2a3 3 0 0 1 0-6V7a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2Z"}],["path",{d:"M9 12h6"}],["path",{d:"M12 9v6"}]],cs=[["path",{d:"M2 9a3 3 0 0 1 0 6v2a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-2a3 3 0 0 1 0-6V7a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2Z"}],["path",{d:"m9.5 14.5 5-5"}],["path",{d:"m9.5 9.5 5 5"}]],Ms=[["path",{d:"M2 9a3 3 0 0 1 0 6v2a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-2a3 3 0 0 1 0-6V7a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2Z"}],["path",{d:"M13 5v2"}],["path",{d:"M13 17v2"}],["path",{d:"M13 11v2"}]],ps=[["path",{d:"M2 9a3 3 0 0 1 0 6v2a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-2a3 3 0 0 1 0-6V7a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2Z"}],["path",{d:"m9.5 14.5 5-5"}]],is=[["path",{d:"M10.5 17h1.227a2 2 0 0 0 1.345-.52L18 12"}],["path",{d:"m12 13.5 3.75.5"}],["path",{d:"m4.5 8 10.58-5.06a1 1 0 0 1 1.342.488L18.5 8"}],["path",{d:"M6 10V8"}],["path",{d:"M6 14v1"}],["path",{d:"M6 19v2"}],["rect",{x:"2",y:"8",width:"20",height:"13",rx:"2"}]],ns=[["path",{d:"m4.5 8 10.58-5.06a1 1 0 0 1 1.342.488L18.5 8"}],["path",{d:"M6 10V8"}],["path",{d:"M6 14v1"}],["path",{d:"M6 19v2"}],["rect",{x:"2",y:"8",width:"20",height:"13",rx:"2"}]],ls=[["path",{d:"M10 2h4"}],["path",{d:"M4.6 11a8 8 0 0 0 1.7 8.7 8 8 0 0 0 8.7 1.7"}],["path",{d:"M7.4 7.4a8 8 0 0 1 10.3 1 8 8 0 0 1 .9 10.2"}],["path",{d:"m2 2 20 20"}],["path",{d:"M12 12v-2"}]],es=[["path",{d:"M10 2h4"}],["path",{d:"M12 14v-4"}],["path",{d:"M4 13a8 8 0 0 1 8-7 8 8 0 1 1-5.3 14L4 17.6"}],["path",{d:"M9 17H4v5"}]],rs=[["line",{x1:"10",x2:"14",y1:"2",y2:"2"}],["line",{x1:"12",x2:"15",y1:"14",y2:"11"}],["circle",{cx:"12",cy:"14",r:"8"}]],os=[["circle",{cx:"9",cy:"12",r:"3"}],["rect",{width:"20",height:"14",x:"2",y:"5",rx:"7"}]],vs=[["circle",{cx:"15",cy:"12",r:"3"}],["rect",{width:"20",height:"14",x:"2",y:"5",rx:"7"}]],$s=[["path",{d:"M7 12h13a1 1 0 0 1 1 1 5 5 0 0 1-5 5h-.598a.5.5 0 0 0-.424.765l1.544 2.47a.5.5 0 0 1-.424.765H5.402a.5.5 0 0 1-.424-.765L7 18"}],["path",{d:"M8 18a5 5 0 0 1-5-5V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8"}]],ms=[["path",{d:"M21 4H3"}],["path",{d:"M18 8H6"}],["path",{d:"M19 12H9"}],["path",{d:"M16 16h-6"}],["path",{d:"M11 20H9"}]],ys=[["path",{d:"M10 15h4"}],["path",{d:"m14.817 10.995-.971-1.45 1.034-1.232a2 2 0 0 0-2.025-3.238l-1.82.364L9.91 3.885a2 2 0 0 0-3.625.748L6.141 6.55l-1.725.426a2 2 0 0 0-.19 3.756l.657.27"}],["path",{d:"m18.822 10.995 2.26-5.38a1 1 0 0 0-.557-1.318L16.954 2.9a1 1 0 0 0-1.281.533l-.924 2.122"}],["path",{d:"M4 12.006A1 1 0 0 1 4.994 11H19a1 1 0 0 1 1 1v7a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2z"}]],ss=[["ellipse",{cx:"12",cy:"11",rx:"3",ry:"2"}],["ellipse",{cx:"12",cy:"12.5",rx:"10",ry:"8.5"}]],gs=[["path",{d:"M12 20v-6"}],["path",{d:"M19.656 14H22"}],["path",{d:"M2 14h12"}],["path",{d:"m2 2 20 20"}],["path",{d:"M20 20H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2"}],["path",{d:"M9.656 4H20a2 2 0 0 1 2 2v10.344"}]],us=[["rect",{width:"20",height:"16",x:"2",y:"4",rx:"2"}],["path",{d:"M2 14h20"}],["path",{d:"M12 20v-6"}]],Cs=[["rect",{width:"18",height:"12",x:"3",y:"8",rx:"1"}],["path",{d:"M10 8V5c0-.6-.4-1-1-1H6a1 1 0 0 0-1 1v3"}],["path",{d:"M19 8V5c0-.6-.4-1-1-1h-3a1 1 0 0 0-1 1v3"}]],Hs=[["path",{d:"M18.2 12.27 20 6H4l1.8 6.27a1 1 0 0 0 .95.73h10.5a1 1 0 0 0 .96-.73Z"}],["path",{d:"M8 13v9"}],["path",{d:"M16 22v-9"}],["path",{d:"m9 6 1 7"}],["path",{d:"m15 6-1 7"}],["path",{d:"M12 6V2"}],["path",{d:"M13 2h-2"}]],As=[["path",{d:"m10 11 11 .9a1 1 0 0 1 .8 1.1l-.665 4.158a1 1 0 0 1-.988.842H20"}],["path",{d:"M16 18h-5"}],["path",{d:"M18 5a1 1 0 0 0-1 1v5.573"}],["path",{d:"M3 4h8.129a1 1 0 0 1 .99.863L13 11.246"}],["path",{d:"M4 11V4"}],["path",{d:"M7 15h.01"}],["path",{d:"M8 10.1V4"}],["circle",{cx:"18",cy:"18",r:"2"}],["circle",{cx:"7",cy:"15",r:"5"}]],ws=[["path",{d:"M16.05 10.966a5 2.5 0 0 1-8.1 0"}],["path",{d:"m16.923 14.049 4.48 2.04a1 1 0 0 1 .001 1.831l-8.574 3.9a2 2 0 0 1-1.66 0l-8.574-3.91a1 1 0 0 1 0-1.83l4.484-2.04"}],["path",{d:"M16.949 14.14a5 2.5 0 1 1-9.9 0L10.063 3.5a2 2 0 0 1 3.874 0z"}],["path",{d:"M9.194 6.57a5 2.5 0 0 0 5.61 0"}]],Vs=[["path",{d:"M2 22V12a10 10 0 1 1 20 0v10"}],["path",{d:"M15 6.8v1.4a3 2.8 0 1 1-6 0V6.8"}],["path",{d:"M10 15h.01"}],["path",{d:"M14 15h.01"}],["path",{d:"M10 19a4 4 0 0 1-4-4v-3a6 6 0 1 1 12 0v3a4 4 0 0 1-4 4Z"}],["path",{d:"m9 19-2 3"}],["path",{d:"m15 19 2 3"}]],Ss=[["path",{d:"M8 3.1V7a4 4 0 0 0 8 0V3.1"}],["path",{d:"m9 15-1-1"}],["path",{d:"m15 15 1-1"}],["path",{d:"M9 19c-2.8 0-5-2.2-5-5v-4a8 8 0 0 1 16 0v4c0 2.8-2.2 5-5 5Z"}],["path",{d:"m8 19-2 3"}],["path",{d:"m16 19 2 3"}]],Ls=[["path",{d:"M2 17 17 2"}],["path",{d:"m2 14 8 8"}],["path",{d:"m5 11 8 8"}],["path",{d:"m8 8 8 8"}],["path",{d:"m11 5 8 8"}],["path",{d:"m14 2 8 8"}],["path",{d:"M7 22 22 7"}]],j0=[["rect",{width:"16",height:"16",x:"4",y:"3",rx:"2"}],["path",{d:"M4 11h16"}],["path",{d:"M12 3v8"}],["path",{d:"m8 19-2 3"}],["path",{d:"m18 22-2-3"}],["path",{d:"M8 15h.01"}],["path",{d:"M16 15h.01"}]],fs=[["path",{d:"M12 16v6"}],["path",{d:"M14 20h-4"}],["path",{d:"M18 2h4v4"}],["path",{d:"m2 2 7.17 7.17"}],["path",{d:"M2 5.355V2h3.357"}],["path",{d:"m22 2-7.17 7.17"}],["path",{d:"M8 5 5 8"}],["circle",{cx:"12",cy:"12",r:"4"}]],ks=[["path",{d:"M10 11v6"}],["path",{d:"M14 11v6"}],["path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"}],["path",{d:"M3 6h18"}],["path",{d:"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"}]],Ps=[["path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"}],["path",{d:"M3 6h18"}],["path",{d:"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"}]],Bs=[["path",{d:"M8 19a4 4 0 0 1-2.24-7.32A3.5 3.5 0 0 1 9 6.03V6a3 3 0 1 1 6 0v.04a3.5 3.5 0 0 1 3.24 5.65A4 4 0 0 1 16 19Z"}],["path",{d:"M12 19v3"}]],N0=[["path",{d:"M13 8c0-2.76-2.46-5-5.5-5S2 5.24 2 8h2l1-1 1 1h4"}],["path",{d:"M13 7.14A5.82 5.82 0 0 1 16.5 6c3.04 0 5.5 2.24 5.5 5h-3l-1-1-1 1h-3"}],["path",{d:"M5.89 9.71c-2.15 2.15-2.3 5.47-.35 7.43l4.24-4.25.7-.7.71-.71 2.12-2.12c-1.95-1.96-5.27-1.8-7.42.35"}],["path",{d:"M11 15.5c.5 2.5-.17 4.5-1 6.5h4c2-5.5-.5-12-1-14"}]],Ds=[["path",{d:"m17 14 3 3.3a1 1 0 0 1-.7 1.7H4.7a1 1 0 0 1-.7-1.7L7 14h-.3a1 1 0 0 1-.7-1.7L9 9h-.2A1 1 0 0 1 8 7.3L12 3l4 4.3a1 1 0 0 1-.8 1.7H15l3 3.3a1 1 0 0 1-.7 1.7H17Z"}],["path",{d:"M12 22v-3"}]],Fs=[["path",{d:"M10 10v.2A3 3 0 0 1 8.9 16H5a3 3 0 0 1-1-5.8V10a3 3 0 0 1 6 0Z"}],["path",{d:"M7 16v6"}],["path",{d:"M13 19v3"}],["path",{d:"M12 19h8.3a1 1 0 0 0 .7-1.7L18 14h.3a1 1 0 0 0 .7-1.7L16 9h.2a1 1 0 0 0 .8-1.7L13 3l-1.4 1.5"}]],zs=[["path",{d:"M16 17h6v-6"}],["path",{d:"m22 17-8.5-8.5-5 5L2 7"}]],bs=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2"}],["rect",{width:"3",height:"9",x:"7",y:"7"}],["rect",{width:"3",height:"5",x:"14",y:"7"}]],Rs=[["path",{d:"M14.828 14.828 21 21"}],["path",{d:"M21 16v5h-5"}],["path",{d:"m21 3-9 9-4-4-6 6"}],["path",{d:"M21 8V3h-5"}]],Ts=[["path",{d:"M16 7h6v6"}],["path",{d:"m22 7-8.5 8.5-5-5L2 17"}]],qs=[["path",{d:"M10.17 4.193a2 2 0 0 1 3.666.013"}],["path",{d:"M14 21h2"}],["path",{d:"m15.874 7.743 1 1.732"}],["path",{d:"m18.849 12.952 1 1.732"}],["path",{d:"M21.824 18.18a2 2 0 0 1-1.835 2.824"}],["path",{d:"M4.024 21a2 2 0 0 1-1.839-2.839"}],["path",{d:"m5.136 12.952-1 1.732"}],["path",{d:"M8 21h2"}],["path",{d:"m8.102 7.743-1 1.732"}]],K0=[["path",{d:"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"}],["path",{d:"M12 9v4"}],["path",{d:"M12 17h.01"}]],Us=[["path",{d:"M22 18a2 2 0 0 1-2 2H3c-1.1 0-1.3-.6-.4-1.3L20.4 4.3c.9-.7 1.6-.4 1.6.7Z"}]],Os=[["path",{d:"M13.73 4a2 2 0 0 0-3.46 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"}]],Zs=[["path",{d:"M10 14.66v1.626a2 2 0 0 1-.976 1.696A5 5 0 0 0 7 21.978"}],["path",{d:"M14 14.66v1.626a2 2 0 0 0 .976 1.696A5 5 0 0 1 17 21.978"}],["path",{d:"M18 9h1.5a1 1 0 0 0 0-5H18"}],["path",{d:"M4 22h16"}],["path",{d:"M6 9a6 6 0 0 0 12 0V3a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1z"}],["path",{d:"M6 9H4.5a1 1 0 0 1 0-5H6"}]],Gs=[["path",{d:"M14 19V7a2 2 0 0 0-2-2H9"}],["path",{d:"M15 19H9"}],["path",{d:"M19 19h2a1 1 0 0 0 1-1v-3.65a1 1 0 0 0-.22-.62L18.3 9.38a1 1 0 0 0-.78-.38H14"}],["path",{d:"M2 13v5a1 1 0 0 0 1 1h2"}],["path",{d:"M4 3 2.15 5.15a.495.495 0 0 0 .35.86h2.15a.47.47 0 0 1 .35.86L3 9.02"}],["circle",{cx:"17",cy:"19",r:"2"}],["circle",{cx:"7",cy:"19",r:"2"}]],Is=[["path",{d:"M14 18V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v11a1 1 0 0 0 1 1h2"}],["path",{d:"M15 18H9"}],["path",{d:"M19 18h2a1 1 0 0 0 1-1v-3.65a1 1 0 0 0-.22-.624l-3.48-4.35A1 1 0 0 0 17.52 8H14"}],["circle",{cx:"17",cy:"18",r:"2"}],["circle",{cx:"7",cy:"18",r:"2"}]],Ws=[["path",{d:"M10 12.01h.01"}],["path",{d:"M18 8v4a8 8 0 0 1-1.07 4"}],["circle",{cx:"10",cy:"12",r:"4"}],["rect",{x:"2",y:"4",width:"20",height:"16",rx:"2"}]],Es=[["path",{d:"M15 4 5 9"}],["path",{d:"m15 8.5-10 5"}],["path",{d:"M18 12a9 9 0 0 1-9 9V3"}]],Xs=[["path",{d:"m12 10 2 4v3a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1v-3a8 8 0 1 0-16 0v3a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1v-3l2-4h4Z"}],["path",{d:"M4.82 7.9 8 10"}],["path",{d:"M15.18 7.9 12 10"}],["path",{d:"M16.93 10H20a2 2 0 0 1 0 4H2"}]],js=[["path",{d:"M15.033 9.44a.647.647 0 0 1 0 1.12l-4.065 2.352a.645.645 0 0 1-.968-.56V7.648a.645.645 0 0 1 .967-.56z"}],["path",{d:"M7 21h10"}],["rect",{width:"20",height:"14",x:"2",y:"3",rx:"2"}]],Q0=[["path",{d:"M7 21h10"}],["rect",{width:"20",height:"14",x:"2",y:"3",rx:"2"}]],Ns=[["path",{d:"m17 2-5 5-5-5"}],["rect",{width:"20",height:"15",x:"2",y:"7",rx:"2"}]],Ks=[["path",{d:"M21 2H3v16h5v4l4-4h5l4-4V2zm-10 9V7m5 4V7"}]],Qs=[["path",{d:"M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6 2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4-.9-4.2 4-6.6 7-3.8 1.1 0 3-1.2 3-1.2z"}]],Js=[["path",{d:"M14 16.5a.5.5 0 0 0 .5.5h.5a2 2 0 0 1 0 4H9a2 2 0 0 1 0-4h.5a.5.5 0 0 0 .5-.5v-9a.5.5 0 0 0-.5-.5h-3a.5.5 0 0 0-.5.5V8a2 2 0 0 1-4 0V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v3a2 2 0 0 1-4 0v-.5a.5.5 0 0 0-.5-.5h-3a.5.5 0 0 0-.5.5Z"}]],Ys=[["path",{d:"M12 4v16"}],["path",{d:"M4 7V5a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2"}],["path",{d:"M9 20h6"}]],_s=[["path",{d:"M12 13v7a2 2 0 0 0 4 0"}],["path",{d:"M12 2v2"}],["path",{d:"M18.656 13h2.336a1 1 0 0 0 .97-1.274 10.284 10.284 0 0 0-12.07-7.51"}],["path",{d:"m2 2 20 20"}],["path",{d:"M5.961 5.957a10.28 10.28 0 0 0-3.922 5.769A1 1 0 0 0 3 13h10"}]],xs=[["path",{d:"M12 13v7a2 2 0 0 0 4 0"}],["path",{d:"M12 2v2"}],["path",{d:"M20.992 13a1 1 0 0 0 .97-1.274 10.284 10.284 0 0 0-19.923 0A1 1 0 0 0 3 13z"}]],ag=[["path",{d:"M6 4v6a6 6 0 0 0 12 0V4"}],["line",{x1:"4",x2:"20",y1:"20",y2:"20"}]],tg=[["path",{d:"M9 14 4 9l5-5"}],["path",{d:"M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11"}]],hg=[["path",{d:"M21 17a9 9 0 0 0-15-6.7L3 13"}],["path",{d:"M3 7v6h6"}],["circle",{cx:"12",cy:"17",r:"1"}]],dg=[["path",{d:"M3 7v6h6"}],["path",{d:"M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13"}]],cg=[["path",{d:"M16 12h6"}],["path",{d:"M8 12H2"}],["path",{d:"M12 2v2"}],["path",{d:"M12 8v2"}],["path",{d:"M12 14v2"}],["path",{d:"M12 20v2"}],["path",{d:"m19 15 3-3-3-3"}],["path",{d:"m5 9-3 3 3 3"}]],Mg=[["path",{d:"M12 22v-6"}],["path",{d:"M12 8V2"}],["path",{d:"M4 12H2"}],["path",{d:"M10 12H8"}],["path",{d:"M16 12h-2"}],["path",{d:"M22 12h-2"}],["path",{d:"m15 19-3 3-3-3"}],["path",{d:"m15 5-3-3-3 3"}]],pg=[["rect",{width:"8",height:"6",x:"5",y:"4",rx:"1"}],["rect",{width:"8",height:"6",x:"11",y:"14",rx:"1"}]],J0=[["path",{d:"M14 21v-3a2 2 0 0 0-4 0v3"}],["path",{d:"M18 12h.01"}],["path",{d:"M18 16h.01"}],["path",{d:"M22 7a1 1 0 0 0-1-1h-2a2 2 0 0 1-1.143-.359L13.143 2.36a2 2 0 0 0-2.286-.001L6.143 5.64A2 2 0 0 1 5 6H3a1 1 0 0 0-1 1v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2z"}],["path",{d:"M6 12h.01"}],["path",{d:"M6 16h.01"}],["circle",{cx:"12",cy:"10",r:"2"}]],ig=[["path",{d:"M15 7h2a5 5 0 0 1 0 10h-2m-6 0H7A5 5 0 0 1 7 7h2"}]],ng=[["path",{d:"m19 5 3-3"}],["path",{d:"m2 22 3-3"}],["path",{d:"M6.3 20.3a2.4 2.4 0 0 0 3.4 0L12 18l-6-6-2.3 2.3a2.4 2.4 0 0 0 0 3.4Z"}],["path",{d:"M7.5 13.5 10 11"}],["path",{d:"M10.5 16.5 13 14"}],["path",{d:"m12 6 6 6 2.3-2.3a2.4 2.4 0 0 0 0-3.4l-2.6-2.6a2.4 2.4 0 0 0-3.4 0Z"}]],lg=[["path",{d:"m18.84 12.25 1.72-1.71h-.02a5.004 5.004 0 0 0-.12-7.07 5.006 5.006 0 0 0-6.95 0l-1.72 1.71"}],["path",{d:"m5.17 11.75-1.71 1.71a5.004 5.004 0 0 0 .12 7.07 5.006 5.006 0 0 0 6.95 0l1.71-1.71"}],["line",{x1:"8",x2:"8",y1:"2",y2:"5"}],["line",{x1:"2",x2:"5",y1:"8",y2:"8"}],["line",{x1:"16",x2:"16",y1:"19",y2:"22"}],["line",{x1:"19",x2:"22",y1:"16",y2:"16"}]],eg=[["path",{d:"M12 3v12"}],["path",{d:"m17 8-5-5-5 5"}],["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"}]],rg=[["circle",{cx:"10",cy:"7",r:"1"}],["circle",{cx:"4",cy:"20",r:"1"}],["path",{d:"M4.7 19.3 19 5"}],["path",{d:"m21 3-3 1 2 2Z"}],["path",{d:"M9.26 7.68 5 12l2 5"}],["path",{d:"m10 14 5 2 3.5-3.5"}],["path",{d:"m18 12 1-1 1 1-1 1Z"}]],og=[["path",{d:"m16 11 2 2 4-4"}],["path",{d:"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"}],["circle",{cx:"9",cy:"7",r:"4"}]],vg=[["path",{d:"M10 15H6a4 4 0 0 0-4 4v2"}],["path",{d:"m14.305 16.53.923-.382"}],["path",{d:"m15.228 13.852-.923-.383"}],["path",{d:"m16.852 12.228-.383-.923"}],["path",{d:"m16.852 17.772-.383.924"}],["path",{d:"m19.148 12.228.383-.923"}],["path",{d:"m19.53 18.696-.382-.924"}],["path",{d:"m20.772 13.852.924-.383"}],["path",{d:"m20.772 16.148.924.383"}],["circle",{cx:"18",cy:"15",r:"3"}],["circle",{cx:"9",cy:"7",r:"4"}]],$g=[["circle",{cx:"10",cy:"7",r:"4"}],["path",{d:"M10.3 15H7a4 4 0 0 0-4 4v2"}],["path",{d:"M15 15.5V14a2 2 0 0 1 4 0v1.5"}],["rect",{width:"8",height:"5",x:"13",y:"16",rx:".899"}]],mg=[["path",{d:"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"}],["circle",{cx:"9",cy:"7",r:"4"}],["line",{x1:"22",x2:"16",y1:"11",y2:"11"}]],yg=[["path",{d:"M11.5 15H7a4 4 0 0 0-4 4v2"}],["path",{d:"M21.378 16.626a1 1 0 0 0-3.004-3.004l-4.01 4.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"}],["circle",{cx:"10",cy:"7",r:"4"}]],sg=[["path",{d:"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"}],["circle",{cx:"9",cy:"7",r:"4"}],["line",{x1:"19",x2:"19",y1:"8",y2:"14"}],["line",{x1:"22",x2:"16",y1:"11",y2:"11"}]],Y0=[["path",{d:"m14.305 19.53.923-.382"}],["path",{d:"m15.228 16.852-.923-.383"}],["path",{d:"m16.852 15.228-.383-.923"}],["path",{d:"m16.852 20.772-.383.924"}],["path",{d:"m19.148 15.228.383-.923"}],["path",{d:"m19.53 21.696-.382-.924"}],["path",{d:"M2 21a8 8 0 0 1 10.434-7.62"}],["path",{d:"m20.772 16.852.924-.383"}],["path",{d:"m20.772 19.148.924.383"}],["circle",{cx:"10",cy:"8",r:"5"}],["circle",{cx:"18",cy:"18",r:"3"}]],_0=[["path",{d:"M2 21a8 8 0 0 1 13.292-6"}],["circle",{cx:"10",cy:"8",r:"5"}],["path",{d:"m16 19 2 2 4-4"}]],x0=[["path",{d:"M2 21a8 8 0 0 1 13.292-6"}],["circle",{cx:"10",cy:"8",r:"5"}],["path",{d:"M22 19h-6"}]],gg=[["path",{d:"M2 21a8 8 0 0 1 10.821-7.487"}],["path",{d:"M21.378 16.626a1 1 0 0 0-3.004-3.004l-4.01 4.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"}],["circle",{cx:"10",cy:"8",r:"5"}]],aa=[["path",{d:"M2 21a8 8 0 0 1 13.292-6"}],["circle",{cx:"10",cy:"8",r:"5"}],["path",{d:"M19 16v6"}],["path",{d:"M22 19h-6"}]],ug=[["circle",{cx:"10",cy:"8",r:"5"}],["path",{d:"M2 21a8 8 0 0 1 10.434-7.62"}],["circle",{cx:"18",cy:"18",r:"3"}],["path",{d:"m22 22-1.9-1.9"}]],ta=[["path",{d:"M2 21a8 8 0 0 1 11.873-7"}],["circle",{cx:"10",cy:"8",r:"5"}],["path",{d:"m17 17 5 5"}],["path",{d:"m22 17-5 5"}]],ha=[["circle",{cx:"12",cy:"8",r:"5"}],["path",{d:"M20 21a8 8 0 0 0-16 0"}]],Cg=[["circle",{cx:"10",cy:"7",r:"4"}],["path",{d:"M10.3 15H7a4 4 0 0 0-4 4v2"}],["circle",{cx:"17",cy:"17",r:"3"}],["path",{d:"m21 21-1.9-1.9"}]],Hg=[["path",{d:"M16.051 12.616a1 1 0 0 1 1.909.024l.737 1.452a1 1 0 0 0 .737.535l1.634.256a1 1 0 0 1 .588 1.806l-1.172 1.168a1 1 0 0 0-.282.866l.259 1.613a1 1 0 0 1-1.541 1.134l-1.465-.75a1 1 0 0 0-.912 0l-1.465.75a1 1 0 0 1-1.539-1.133l.258-1.613a1 1 0 0 0-.282-.866l-1.156-1.153a1 1 0 0 1 .572-1.822l1.633-.256a1 1 0 0 0 .737-.535z"}],["path",{d:"M8 15H7a4 4 0 0 0-4 4v2"}],["circle",{cx:"10",cy:"7",r:"4"}]],Ag=[["path",{d:"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"}],["circle",{cx:"9",cy:"7",r:"4"}],["line",{x1:"17",x2:"22",y1:"8",y2:"13"}],["line",{x1:"22",x2:"17",y1:"8",y2:"13"}]],wg=[["path",{d:"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"}],["circle",{cx:"12",cy:"7",r:"4"}]],da=[["path",{d:"M18 21a8 8 0 0 0-16 0"}],["circle",{cx:"10",cy:"8",r:"5"}],["path",{d:"M22 20c0-3.37-2-6.5-4-8a5 5 0 0 0-.45-8.3"}]],Vg=[["path",{d:"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"}],["path",{d:"M16 3.128a4 4 0 0 1 0 7.744"}],["path",{d:"M22 21v-2a4 4 0 0 0-3-3.87"}],["circle",{cx:"9",cy:"7",r:"4"}]],ca=[["path",{d:"m16 2-2.3 2.3a3 3 0 0 0 0 4.2l1.8 1.8a3 3 0 0 0 4.2 0L22 8"}],["path",{d:"M15 15 3.3 3.3a4.2 4.2 0 0 0 0 6l7.3 7.3c.7.7 2 .7 2.8 0L15 15Zm0 0 7 7"}],["path",{d:"m2.1 21.8 6.4-6.3"}],["path",{d:"m19 5-7 7"}]],Ma=[["path",{d:"M3 2v7c0 1.1.9 2 2 2h4a2 2 0 0 0 2-2V2"}],["path",{d:"M7 2v20"}],["path",{d:"M21 15V2a5 5 0 0 0-5 5v6c0 1.1.9 2 2 2h3Zm0 0v7"}]],Sg=[["path",{d:"M12 2v20"}],["path",{d:"M2 5h20"}],["path",{d:"M3 3v2"}],["path",{d:"M7 3v2"}],["path",{d:"M17 3v2"}],["path",{d:"M21 3v2"}],["path",{d:"m19 5-7 7-7-7"}]],Lg=[["path",{d:"M8 21s-4-3-4-9 4-9 4-9"}],["path",{d:"M16 3s4 3 4 9-4 9-4 9"}],["line",{x1:"15",x2:"9",y1:"9",y2:"15"}],["line",{x1:"9",x2:"15",y1:"9",y2:"15"}]],fg=[["path",{d:"M19.5 7a24 24 0 0 1 0 10"}],["path",{d:"M4.5 7a24 24 0 0 0 0 10"}],["path",{d:"M7 19.5a24 24 0 0 0 10 0"}],["path",{d:"M7 4.5a24 24 0 0 1 10 0"}],["rect",{x:"17",y:"17",width:"5",height:"5",rx:"1"}],["rect",{x:"17",y:"2",width:"5",height:"5",rx:"1"}],["rect",{x:"2",y:"17",width:"5",height:"5",rx:"1"}],["rect",{x:"2",y:"2",width:"5",height:"5",rx:"1"}]],kg=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["circle",{cx:"7.5",cy:"7.5",r:".5",fill:"currentColor"}],["path",{d:"m7.9 7.9 2.7 2.7"}],["circle",{cx:"16.5",cy:"7.5",r:".5",fill:"currentColor"}],["path",{d:"m13.4 10.6 2.7-2.7"}],["circle",{cx:"7.5",cy:"16.5",r:".5",fill:"currentColor"}],["path",{d:"m7.9 16.1 2.7-2.7"}],["circle",{cx:"16.5",cy:"16.5",r:".5",fill:"currentColor"}],["path",{d:"m13.4 13.4 2.7 2.7"}],["circle",{cx:"12",cy:"12",r:"2"}]],Pg=[["path",{d:"M16 8q6 0 6-6-6 0-6 6"}],["path",{d:"M17.41 3.59a10 10 0 1 0 3 3"}],["path",{d:"M2 2a26.6 26.6 0 0 1 10 20c.9-6.82 1.5-9.5 4-14"}]],Bg=[["path",{d:"M18 11c-1.5 0-2.5.5-3 2"}],["path",{d:"M4 6a2 2 0 0 0-2 2v4a5 5 0 0 0 5 5 8 8 0 0 1 5 2 8 8 0 0 1 5-2 5 5 0 0 0 5-5V8a2 2 0 0 0-2-2h-3a8 8 0 0 0-5 2 8 8 0 0 0-5-2z"}],["path",{d:"M6 11c1.5 0 2.5.5 3 2"}]],Dg=[["path",{d:"M10 20h4"}],["path",{d:"M12 16v6"}],["path",{d:"M17 2h4v4"}],["path",{d:"m21 2-5.46 5.46"}],["circle",{cx:"12",cy:"11",r:"5"}]],Fg=[["path",{d:"M12 15v7"}],["path",{d:"M9 19h6"}],["circle",{cx:"12",cy:"9",r:"6"}]],zg=[["path",{d:"m2 8 2 2-2 2 2 2-2 2"}],["path",{d:"m22 8-2 2 2 2-2 2 2 2"}],["path",{d:"M8 8v10c0 .55.45 1 1 1h6c.55 0 1-.45 1-1v-2"}],["path",{d:"M16 10.34V6c0-.55-.45-1-1-1h-4.34"}],["line",{x1:"2",x2:"22",y1:"2",y2:"22"}]],bg=[["path",{d:"m2 8 2 2-2 2 2 2-2 2"}],["path",{d:"m22 8-2 2 2 2-2 2 2 2"}],["rect",{width:"8",height:"14",x:"8",y:"5",rx:"1"}]],Rg=[["path",{d:"M10.66 6H14a2 2 0 0 1 2 2v2.5l5.248-3.062A.5.5 0 0 1 22 7.87v8.196"}],["path",{d:"M16 16a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h2"}],["path",{d:"m2 2 20 20"}]],Tg=[["path",{d:"m16 13 5.223 3.482a.5.5 0 0 0 .777-.416V7.87a.5.5 0 0 0-.752-.432L16 10.5"}],["rect",{x:"2",y:"6",width:"14",height:"12",rx:"2"}]],qg=[["rect",{width:"20",height:"16",x:"2",y:"4",rx:"2"}],["path",{d:"M2 8h20"}],["circle",{cx:"8",cy:"14",r:"2"}],["path",{d:"M8 12h8"}],["circle",{cx:"16",cy:"14",r:"2"}]],Ug=[["path",{d:"M21 17v2a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-2"}],["path",{d:"M21 7V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v2"}],["circle",{cx:"12",cy:"12",r:"1"}],["path",{d:"M18.944 12.33a1 1 0 0 0 0-.66 7.5 7.5 0 0 0-13.888 0 1 1 0 0 0 0 .66 7.5 7.5 0 0 0 13.888 0"}]],Og=[["circle",{cx:"6",cy:"12",r:"4"}],["circle",{cx:"18",cy:"12",r:"4"}],["line",{x1:"6",x2:"18",y1:"16",y2:"16"}]],Zg=[["path",{d:"M11.1 7.1a16.55 16.55 0 0 1 10.9 4"}],["path",{d:"M12 12a12.6 12.6 0 0 1-8.7 5"}],["path",{d:"M16.8 13.6a16.55 16.55 0 0 1-9 7.5"}],["path",{d:"M20.7 17a12.8 12.8 0 0 0-8.7-5 13.3 13.3 0 0 1 0-10"}],["path",{d:"M6.3 3.8a16.55 16.55 0 0 0 1.9 11.5"}],["circle",{cx:"12",cy:"12",r:"10"}]],Gg=[["path",{d:"M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z"}],["path",{d:"M16 9a5 5 0 0 1 0 6"}]],Ig=[["path",{d:"M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z"}],["path",{d:"M16 9a5 5 0 0 1 0 6"}],["path",{d:"M19.364 18.364a9 9 0 0 0 0-12.728"}]],Wg=[["path",{d:"M16 9a5 5 0 0 1 .95 2.293"}],["path",{d:"M19.364 5.636a9 9 0 0 1 1.889 9.96"}],["path",{d:"m2 2 20 20"}],["path",{d:"m7 7-.587.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298V11"}],["path",{d:"M9.828 4.172A.686.686 0 0 1 11 4.657v.686"}]],Eg=[["path",{d:"M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z"}],["line",{x1:"22",x2:"16",y1:"9",y2:"15"}],["line",{x1:"16",x2:"22",y1:"9",y2:"15"}]],Xg=[["path",{d:"M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z"}]],jg=[["path",{d:"m9 12 2 2 4-4"}],["path",{d:"M5 7c0-1.1.9-2 2-2h10a2 2 0 0 1 2 2v12H5V7Z"}],["path",{d:"M22 19H2"}]],Ng=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M3 9a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2"}],["path",{d:"M3 11h3c.8 0 1.6.3 2.1.9l1.1.9c1.6 1.6 4.1 1.6 5.7 0l1.1-.9c.5-.5 1.3-.9 2.1-.9H21"}]],pa=[["path",{d:"M17 14h.01"}],["path",{d:"M7 7h12a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14"}]],Kg=[["path",{d:"M19 7V4a1 1 0 0 0-1-1H5a2 2 0 0 0 0 4h15a1 1 0 0 1 1 1v4h-3a2 2 0 0 0 0 4h3a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1"}],["path",{d:"M3 5v14a2 2 0 0 0 2 2h15a1 1 0 0 0 1-1v-4"}]],Qg=[["path",{d:"M12 17v4"}],["path",{d:"M8 21h8"}],["path",{d:"m9 17 6.1-6.1a2 2 0 0 1 2.81.01L22 15"}],["circle",{cx:"8",cy:"9",r:"2"}],["rect",{x:"2",y:"3",width:"20",height:"14",rx:"2"}]],ia=[["path",{d:"m21.64 3.64-1.28-1.28a1.21 1.21 0 0 0-1.72 0L2.36 18.64a1.21 1.21 0 0 0 0 1.72l1.28 1.28a1.2 1.2 0 0 0 1.72 0L21.64 5.36a1.2 1.2 0 0 0 0-1.72"}],["path",{d:"m14 7 3 3"}],["path",{d:"M5 6v4"}],["path",{d:"M19 14v4"}],["path",{d:"M10 2v2"}],["path",{d:"M7 8H3"}],["path",{d:"M21 16h-4"}],["path",{d:"M11 3H9"}]],Jg=[["path",{d:"M15 4V2"}],["path",{d:"M15 16v-2"}],["path",{d:"M8 9h2"}],["path",{d:"M20 9h2"}],["path",{d:"M17.8 11.8 19 13"}],["path",{d:"M15 9h.01"}],["path",{d:"M17.8 6.2 19 5"}],["path",{d:"m3 21 9-9"}],["path",{d:"M12.2 6.2 11 5"}]],Yg=[["path",{d:"M18 21V10a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1v11"}],["path",{d:"M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8a2 2 0 0 1 1.132-1.803l7.95-3.974a2 2 0 0 1 1.837 0l7.948 3.974A2 2 0 0 1 22 8z"}],["path",{d:"M6 13h12"}],["path",{d:"M6 17h12"}]],_g=[["path",{d:"M3 6h3"}],["path",{d:"M17 6h.01"}],["rect",{width:"18",height:"20",x:"3",y:"2",rx:"2"}],["circle",{cx:"12",cy:"13",r:"5"}],["path",{d:"M12 18a2.5 2.5 0 0 0 0-5 2.5 2.5 0 0 1 0-5"}]],xg=[["path",{d:"M12 10v2.2l1.6 1"}],["path",{d:"m16.13 7.66-.81-4.05a2 2 0 0 0-2-1.61h-2.68a2 2 0 0 0-2 1.61l-.78 4.05"}],["path",{d:"m7.88 16.36.8 4a2 2 0 0 0 2 1.61h2.72a2 2 0 0 0 2-1.61l.81-4.05"}],["circle",{cx:"12",cy:"12",r:"6"}]],au=[["path",{d:"M19 5a2 2 0 0 0-2 2v11"}],["path",{d:"M2 18c.6.5 1.2 1 2.5 1 2.5 0 2.5-2 5-2 2.6 0 2.4 2 5 2 2.5 0 2.5-2 5-2 1.3 0 1.9.5 2.5 1"}],["path",{d:"M7 13h10"}],["path",{d:"M7 9h10"}],["path",{d:"M9 5a2 2 0 0 0-2 2v11"}]],tu=[["path",{d:"M2 6c.6.5 1.2 1 2.5 1C7 7 7 5 9.5 5c2.6 0 2.4 2 5 2 2.5 0 2.5-2 5-2 1.3 0 1.9.5 2.5 1"}],["path",{d:"M2 12c.6.5 1.2 1 2.5 1 2.5 0 2.5-2 5-2 2.6 0 2.4 2 5 2 2.5 0 2.5-2 5-2 1.3 0 1.9.5 2.5 1"}],["path",{d:"M2 18c.6.5 1.2 1 2.5 1 2.5 0 2.5-2 5-2 2.6 0 2.4 2 5 2 2.5 0 2.5-2 5-2 1.3 0 1.9.5 2.5 1"}]],hu=[["circle",{cx:"12",cy:"4.5",r:"2.5"}],["path",{d:"m10.2 6.3-3.9 3.9"}],["circle",{cx:"4.5",cy:"12",r:"2.5"}],["path",{d:"M7 12h10"}],["circle",{cx:"19.5",cy:"12",r:"2.5"}],["path",{d:"m13.8 17.7 3.9-3.9"}],["circle",{cx:"12",cy:"19.5",r:"2.5"}]],du=[["circle",{cx:"12",cy:"10",r:"8"}],["circle",{cx:"12",cy:"10",r:"3"}],["path",{d:"M7 22h10"}],["path",{d:"M12 22v-4"}]],cu=[["circle",{cx:"12",cy:"5",r:"3"}],["path",{d:"M6.5 8a2 2 0 0 0-1.905 1.46L2.1 18.5A2 2 0 0 0 4 21h16a2 2 0 0 0 1.925-2.54L19.4 9.5A2 2 0 0 0 17.48 8Z"}]],Mu=[["path",{d:"M17 17h-5c-1.09-.02-1.94.92-2.5 1.9A3 3 0 1 1 2.57 15"}],["path",{d:"M9 3.4a4 4 0 0 1 6.52.66"}],["path",{d:"m6 17 3.1-5.8a2.5 2.5 0 0 0 .057-2.05"}],["path",{d:"M20.3 20.3a4 4 0 0 1-2.3.7"}],["path",{d:"M18.6 13a4 4 0 0 1 3.357 3.414"}],["path",{d:"m12 6 .6 1"}],["path",{d:"m2 2 20 20"}]],pu=[["path",{d:"M18 16.98h-5.99c-1.1 0-1.95.94-2.48 1.9A4 4 0 0 1 2 17c.01-.7.2-1.4.57-2"}],["path",{d:"m6 17 3.13-5.78c.53-.97.1-2.18-.5-3.1a4 4 0 1 1 6.89-4.06"}],["path",{d:"m12 6 3.13 5.73C15.66 12.7 16.9 13 18 13a4 4 0 0 1 0 8"}]],iu=[["path",{d:"m2 22 10-10"}],["path",{d:"m16 8-1.17 1.17"}],["path",{d:"M3.47 12.53 5 11l1.53 1.53a3.5 3.5 0 0 1 0 4.94L5 19l-1.53-1.53a3.5 3.5 0 0 1 0-4.94Z"}],["path",{d:"m8 8-.53.53a3.5 3.5 0 0 0 0 4.94L9 15l1.53-1.53c.55-.55.88-1.25.98-1.97"}],["path",{d:"M10.91 5.26c.15-.26.34-.51.56-.73L13 3l1.53 1.53a3.5 3.5 0 0 1 .28 4.62"}],["path",{d:"M20 2h2v2a4 4 0 0 1-4 4h-2V6a4 4 0 0 1 4-4Z"}],["path",{d:"M11.47 17.47 13 19l-1.53 1.53a3.5 3.5 0 0 1-4.94 0L5 19l1.53-1.53a3.5 3.5 0 0 1 4.94 0Z"}],["path",{d:"m16 16-.53.53a3.5 3.5 0 0 1-4.94 0L9 15l1.53-1.53a3.49 3.49 0 0 1 1.97-.98"}],["path",{d:"M18.74 13.09c.26-.15.51-.34.73-.56L21 11l-1.53-1.53a3.5 3.5 0 0 0-4.62-.28"}],["line",{x1:"2",x2:"22",y1:"2",y2:"22"}]],nu=[["path",{d:"M2 22 16 8"}],["path",{d:"M3.47 12.53 5 11l1.53 1.53a3.5 3.5 0 0 1 0 4.94L5 19l-1.53-1.53a3.5 3.5 0 0 1 0-4.94Z"}],["path",{d:"M7.47 8.53 9 7l1.53 1.53a3.5 3.5 0 0 1 0 4.94L9 15l-1.53-1.53a3.5 3.5 0 0 1 0-4.94Z"}],["path",{d:"M11.47 4.53 13 3l1.53 1.53a3.5 3.5 0 0 1 0 4.94L13 11l-1.53-1.53a3.5 3.5 0 0 1 0-4.94Z"}],["path",{d:"M20 2h2v2a4 4 0 0 1-4 4h-2V6a4 4 0 0 1 4-4Z"}],["path",{d:"M11.47 17.47 13 19l-1.53 1.53a3.5 3.5 0 0 1-4.94 0L5 19l1.53-1.53a3.5 3.5 0 0 1 4.94 0Z"}],["path",{d:"M15.47 13.47 17 15l-1.53 1.53a3.5 3.5 0 0 1-4.94 0L9 15l1.53-1.53a3.5 3.5 0 0 1 4.94 0Z"}],["path",{d:"M19.47 9.47 21 11l-1.53 1.53a3.5 3.5 0 0 1-4.94 0L13 11l1.53-1.53a3.5 3.5 0 0 1 4.94 0Z"}]],lu=[["circle",{cx:"7",cy:"12",r:"3"}],["path",{d:"M10 9v6"}],["circle",{cx:"17",cy:"12",r:"3"}],["path",{d:"M14 7v8"}],["path",{d:"M22 17v1c0 .5-.5 1-1 1H3c-.5 0-1-.5-1-1v-1"}]],eu=[["path",{d:"m14.305 19.53.923-.382"}],["path",{d:"m15.228 16.852-.923-.383"}],["path",{d:"m16.852 15.228-.383-.923"}],["path",{d:"m16.852 20.772-.383.924"}],["path",{d:"m19.148 15.228.383-.923"}],["path",{d:"m19.53 21.696-.382-.924"}],["path",{d:"M2 7.82a15 15 0 0 1 20 0"}],["path",{d:"m20.772 16.852.924-.383"}],["path",{d:"m20.772 19.148.924.383"}],["path",{d:"M5 11.858a10 10 0 0 1 11.5-1.785"}],["path",{d:"M8.5 15.429a5 5 0 0 1 2.413-1.31"}],["circle",{cx:"18",cy:"18",r:"3"}]],ru=[["path",{d:"M12 20h.01"}],["path",{d:"M5 12.859a10 10 0 0 1 14 0"}],["path",{d:"M8.5 16.429a5 5 0 0 1 7 0"}]],ou=[["path",{d:"M12 20h.01"}],["path",{d:"M8.5 16.429a5 5 0 0 1 7 0"}]],vu=[["path",{d:"M2 8.82a15 15 0 0 1 20 0"}],["path",{d:"M21.378 16.626a1 1 0 0 0-3.004-3.004l-4.01 4.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"}],["path",{d:"M5 12.859a10 10 0 0 1 10.5-2.222"}],["path",{d:"M8.5 16.429a5 5 0 0 1 3-1.406"}]],$u=[["path",{d:"M11.965 10.105v4L13.5 12.5a5 5 0 0 1 8 1.5"}],["path",{d:"M11.965 14.105h4"}],["path",{d:"M17.965 18.105h4L20.43 19.71a5 5 0 0 1-8-1.5"}],["path",{d:"M2 8.82a15 15 0 0 1 20 0"}],["path",{d:"M21.965 22.105v-4"}],["path",{d:"M5 12.86a10 10 0 0 1 3-2.032"}],["path",{d:"M8.5 16.429h.01"}]],mu=[["path",{d:"M12 20h.01"}],["path",{d:"M8.5 16.429a5 5 0 0 1 7 0"}],["path",{d:"M5 12.859a10 10 0 0 1 5.17-2.69"}],["path",{d:"M19 12.859a10 10 0 0 0-2.007-1.523"}],["path",{d:"M2 8.82a15 15 0 0 1 4.177-2.643"}],["path",{d:"M22 8.82a15 15 0 0 0-11.288-3.764"}],["path",{d:"m2 2 20 20"}]],yu=[["path",{d:"M12 20h.01"}]],su=[["path",{d:"M12 20h.01"}],["path",{d:"M2 8.82a15 15 0 0 1 20 0"}],["path",{d:"M5 12.859a10 10 0 0 1 14 0"}],["path",{d:"M8.5 16.429a5 5 0 0 1 7 0"}]],gu=[["path",{d:"M10 2v8"}],["path",{d:"M12.8 21.6A2 2 0 1 0 14 18H2"}],["path",{d:"M17.5 10a2.5 2.5 0 1 1 2 4H2"}],["path",{d:"m6 6 4 4 4-4"}]],uu=[["path",{d:"M12.8 19.6A2 2 0 1 0 14 16H2"}],["path",{d:"M17.5 8a2.5 2.5 0 1 1 2 4H2"}],["path",{d:"M9.8 4.4A2 2 0 1 1 11 8H2"}]],Cu=[["path",{d:"M8 22h8"}],["path",{d:"M7 10h3m7 0h-1.343"}],["path",{d:"M12 15v7"}],["path",{d:"M7.307 7.307A12.33 12.33 0 0 0 7 10a5 5 0 0 0 7.391 4.391M8.638 2.981C8.75 2.668 8.872 2.34 9 2h6c1.5 4 2 6 2 8 0 .407-.05.809-.145 1.198"}],["line",{x1:"2",x2:"22",y1:"2",y2:"22"}]],Hu=[["path",{d:"M8 22h8"}],["path",{d:"M7 10h10"}],["path",{d:"M12 15v7"}],["path",{d:"M12 15a5 5 0 0 0 5-5c0-2-.5-4-2-8H9c-1.5 4-2 6-2 8a5 5 0 0 0 5 5Z"}]],Au=[["rect",{width:"8",height:"8",x:"3",y:"3",rx:"2"}],["path",{d:"M7 11v4a2 2 0 0 0 2 2h4"}],["rect",{width:"8",height:"8",x:"13",y:"13",rx:"2"}]],wu=[["path",{d:"m19 12-1.5 3"}],["path",{d:"M19.63 18.81 22 20"}],["path",{d:"M6.47 8.23a1.68 1.68 0 0 1 2.44 1.93l-.64 2.08a6.76 6.76 0 0 0 10.16 7.67l.42-.27a1 1 0 1 0-2.73-4.21l-.42.27a1.76 1.76 0 0 1-2.63-1.99l.64-2.08A6.66 6.66 0 0 0 3.94 3.9l-.7.4a1 1 0 1 0 2.55 4.34z"}]],Vu=[["path",{d:"M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z"}]],Su=[["path",{d:"M18 6 6 18"}],["path",{d:"m6 6 12 12"}]],Lu=[["path",{d:"M2.5 17a24.12 24.12 0 0 1 0-10 2 2 0 0 1 1.4-1.4 49.56 49.56 0 0 1 16.2 0A2 2 0 0 1 21.5 7a24.12 24.12 0 0 1 0 10 2 2 0 0 1-1.4 1.4 49.55 49.55 0 0 1-16.2 0A2 2 0 0 1 2.5 17"}],["path",{d:"m10 15 5-3-5-3z"}]],fu=[["path",{d:"M10.513 4.856 13.12 2.17a.5.5 0 0 1 .86.46l-1.377 4.317"}],["path",{d:"M15.656 10H20a1 1 0 0 1 .78 1.63l-1.72 1.773"}],["path",{d:"M16.273 16.273 10.88 21.83a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14H4a1 1 0 0 1-.78-1.63l4.507-4.643"}],["path",{d:"m2 2 20 20"}]],ku=[["path",{d:"M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"}]],Pu=[["circle",{cx:"11",cy:"11",r:"8"}],["line",{x1:"21",x2:"16.65",y1:"21",y2:"16.65"}],["line",{x1:"11",x2:"11",y1:"8",y2:"14"}],["line",{x1:"8",x2:"14",y1:"11",y2:"11"}]],Bu=[["circle",{cx:"11",cy:"11",r:"8"}],["line",{x1:"21",x2:"16.65",y1:"21",y2:"16.65"}],["line",{x1:"8",x2:"14",y1:"11",y2:"11"}]];var Du=Object.freeze({__proto__:null,AArrowDown:oa,AArrowUp:va,ALargeSmall:$a,Accessibility:ma,Activity:ya,ActivitySquare:G2,AirVent:sa,Airplay:ga,AlarmCheck:y,AlarmClock:Ca,AlarmClockCheck:y,AlarmClockMinus:s,AlarmClockOff:ua,AlarmClockPlus:g,AlarmMinus:s,AlarmPlus:g,AlarmSmoke:Ha,Album:Aa,AlertCircle:N,AlertOctagon:y2,AlertTriangle:K0,AlignCenter:Z0,AlignCenterHorizontal:wa,AlignCenterVertical:Va,AlignEndHorizontal:Sa,AlignEndVertical:La,AlignHorizontalDistributeCenter:fa,AlignHorizontalDistributeEnd:ka,AlignHorizontalDistributeStart:Pa,AlignHorizontalJustifyCenter:Ba,AlignHorizontalJustifyEnd:Da,AlignHorizontalJustifyStart:Fa,AlignHorizontalSpaceAround:za,AlignHorizontalSpaceBetween:ba,AlignJustify:I0,AlignLeft:m,AlignRight:G0,AlignStartHorizontal:Ra,AlignStartVertical:Ta,AlignVerticalDistributeCenter:qa,AlignVerticalDistributeEnd:Ua,AlignVerticalDistributeStart:Oa,AlignVerticalJustifyCenter:Za,AlignVerticalJustifyEnd:Ga,AlignVerticalJustifyStart:Ia,AlignVerticalSpaceAround:Wa,AlignVerticalSpaceBetween:Ea,Ambulance:ja,Ampersand:Xa,Ampersands:Na,Amphora:Ka,Anchor:Qa,Angry:Ja,Annoyed:Ya,Antenna:_a,Anvil:xa,Aperture:at,AppWindow:ht,AppWindowMac:tt,Apple:dt,Archive:pt,ArchiveRestore:ct,ArchiveX:Mt,AreaChart:z,Armchair:it,ArrowBigDown:lt,ArrowBigDownDash:nt,ArrowBigLeft:rt,ArrowBigLeftDash:et,ArrowBigRight:vt,ArrowBigRightDash:ot,ArrowBigUp:st,ArrowBigUpDash:$t,ArrowDown:St,ArrowDown01:mt,ArrowDown10:yt,ArrowDownAZ:u,ArrowDownAz:u,ArrowDownCircle:K,ArrowDownFromLine:gt,ArrowDownLeft:Ct,ArrowDownLeftFromCircle:J,ArrowDownLeftFromSquare:j2,ArrowDownLeftSquare:I2,ArrowDownNarrowWide:ut,ArrowDownRight:At,ArrowDownRightFromCircle:_,ArrowDownRightFromSquare:N2,ArrowDownRightSquare:W2,ArrowDownSquare:E2,ArrowDownToDot:Ht,ArrowDownToLine:wt,ArrowDownUp:Vt,ArrowDownWideNarrow:C,ArrowDownZA:H,ArrowDownZa:H,ArrowLeft:Pt,ArrowLeftCircle:Q,ArrowLeftFromLine:Lt,ArrowLeftRight:ft,ArrowLeftSquare:X2,ArrowLeftToLine:kt,ArrowRight:zt,ArrowRightCircle:a1,ArrowRightFromLine:Bt,ArrowRightLeft:Ft,ArrowRightSquare:J2,ArrowRightToLine:Dt,ArrowUp:Wt,ArrowUp01:bt,ArrowUp10:Rt,ArrowUpAZ:A,ArrowUpAz:A,ArrowUpCircle:t1,ArrowUpDown:Tt,ArrowUpFromDot:Ut,ArrowUpFromLine:qt,ArrowUpLeft:Ot,ArrowUpLeftFromCircle:Y,ArrowUpLeftFromSquare:K2,ArrowUpLeftSquare:Y2,ArrowUpNarrowWide:w,ArrowUpRight:Zt,ArrowUpRightFromCircle:x,ArrowUpRightFromSquare:Q2,ArrowUpRightSquare:_2,ArrowUpSquare:x2,ArrowUpToLine:It,ArrowUpWideNarrow:Gt,ArrowUpZA:V,ArrowUpZa:V,ArrowsUpFromLine:Et,Asterisk:Xt,AsteriskSquare:a0,AtSign:jt,Atom:Qt,AudioLines:Nt,AudioWaveform:Kt,Award:Jt,Axe:Yt,Axis3D:S,Axis3d:S,Baby:_t,Backpack:xt,Badge:mh,BadgeAlert:ah,BadgeCent:th,BadgeCheck:L,BadgeDollarSign:hh,BadgeEuro:dh,BadgeHelp:f,BadgeIndianRupee:ch,BadgeInfo:Mh,BadgeJapaneseYen:ph,BadgeMinus:ih,BadgePercent:nh,BadgePlus:lh,BadgePoundSterling:eh,BadgeQuestionMark:f,BadgeRussianRuble:rh,BadgeSwissFranc:oh,BadgeTurkishLira:vh,BadgeX:$h,BaggageClaim:yh,Ban:sh,Banana:gh,Bandage:uh,Banknote:wh,BanknoteArrowDown:Ch,BanknoteArrowUp:Hh,BanknoteX:Ah,BarChart:G,BarChart2:I,BarChart3:O,BarChart4:U,BarChartBig:q,BarChartHorizontal:T,BarChartHorizontalBig:b,Barcode:Vh,Barrel:Sh,Baseline:Lh,Bath:fh,Battery:bh,BatteryCharging:kh,BatteryFull:Ph,BatteryLow:Bh,BatteryMedium:Dh,BatteryPlus:Fh,BatteryWarning:zh,Beaker:Rh,Bean:qh,BeanOff:Th,Bed:Zh,BedDouble:Uh,BedSingle:Oh,Beef:Gh,Beer:Wh,BeerOff:Ih,Bell:Jh,BellDot:Eh,BellElectric:Xh,BellMinus:jh,BellOff:Nh,BellPlus:Kh,BellRing:Qh,BetweenHorizonalEnd:k,BetweenHorizonalStart:P,BetweenHorizontalEnd:k,BetweenHorizontalStart:P,BetweenVerticalEnd:Yh,BetweenVerticalStart:_h,BicepsFlexed:xh,Bike:a4,Binary:t4,Binoculars:h4,Biohazard:d4,Bird:c4,Bitcoin:p4,Blend:M4,Blinds:n4,Blocks:i4,Bluetooth:r4,BluetoothConnected:l4,BluetoothOff:o4,BluetoothSearching:e4,Bold:v4,Bolt:$4,Bomb:m4,Bone:y4,Book:Z4,BookA:s4,BookAlert:g4,BookAudio:u4,BookCheck:C4,BookCopy:H4,BookDashed:B,BookDown:A4,BookHeadphones:w4,BookHeart:V4,BookImage:S4,BookKey:L4,BookLock:f4,BookMarked:k4,BookMinus:P4,BookOpen:F4,BookOpenCheck:B4,BookOpenText:D4,BookPlus:z4,BookTemplate:B,BookText:b4,BookType:T4,BookUp:q4,BookUp2:R4,BookUser:U4,BookX:O4,Bookmark:X4,BookmarkCheck:G4,BookmarkMinus:I4,BookmarkPlus:W4,BookmarkX:E4,BoomBox:j4,Bot:Q4,BotMessageSquare:K4,BotOff:N4,BottleWine:J4,BowArrow:Y4,Box:_4,BoxSelect:r0,Boxes:x4,Braces:D,Brackets:a5,Brain:d5,BrainCircuit:t5,BrainCog:h5,BrickWall:p5,BrickWallFire:c5,BrickWallShield:M5,Briefcase:e5,BriefcaseBusiness:i5,BriefcaseConveyorBelt:n5,BriefcaseMedical:l5,BringToFront:r5,Brush:v5,BrushCleaning:o5,Bubbles:$5,Bug:s5,BugOff:m5,BugPlay:y5,Building:u5,Building2:g5,Bus:H5,BusFront:C5,Cable:w5,CableCar:A5,Cake:S5,CakeSlice:V5,Calculator:L5,Calendar:N5,Calendar1:f5,CalendarArrowDown:k5,CalendarArrowUp:P5,CalendarCheck:D5,CalendarCheck2:B5,CalendarClock:F5,CalendarCog:R5,CalendarDays:z5,CalendarFold:b5,CalendarHeart:T5,CalendarMinus:U5,CalendarMinus2:q5,CalendarOff:O5,CalendarPlus:Z5,CalendarPlus2:I5,CalendarRange:G5,CalendarSearch:E5,CalendarSync:W5,CalendarX:j5,CalendarX2:X5,Camera:Q5,CameraOff:K5,CandlestickChart:R,Candy:_5,CandyCane:J5,CandyOff:Y5,Cannabis:x5,Captions:F,CaptionsOff:a3,Car:d3,CarFront:t3,CarTaxiFront:h3,Caravan:c3,CardSim:M3,Carrot:p3,CaseLower:i3,CaseSensitive:n3,CaseUpper:l3,CassetteTape:e3,Cast:r3,Castle:o3,Cat:v3,Cctv:$3,ChartArea:z,ChartBar:T,ChartBarBig:b,ChartBarDecreasing:m3,ChartBarIncreasing:y3,ChartBarStacked:s3,ChartCandlestick:R,ChartColumn:O,ChartColumnBig:q,ChartColumnDecreasing:g3,ChartColumnIncreasing:U,ChartColumnStacked:u3,ChartGantt:C3,ChartLine:Z,ChartNetwork:H3,ChartNoAxesColumn:I,ChartNoAxesColumnDecreasing:A3,ChartNoAxesColumnIncreasing:G,ChartNoAxesCombined:w3,ChartNoAxesGantt:W,ChartPie:X,ChartScatter:E,ChartSpline:V3,Check:f3,CheckCheck:S3,CheckCircle:h1,CheckCircle2:d1,CheckLine:L3,CheckSquare:h0,CheckSquare2:d0,ChefHat:k3,Cherry:B3,ChevronDown:P3,ChevronDownCircle:c1,ChevronDownSquare:c0,ChevronFirst:D3,ChevronLast:F3,ChevronLeft:z3,ChevronLeftCircle:M1,ChevronLeftSquare:M0,ChevronRight:b3,ChevronRightCircle:p1,ChevronRightSquare:p0,ChevronUp:R3,ChevronUpCircle:i1,ChevronUpSquare:i0,ChevronsDown:q3,ChevronsDownUp:T3,ChevronsLeft:Z3,ChevronsLeftRight:O3,ChevronsLeftRightEllipsis:U3,ChevronsRight:I3,ChevronsRightLeft:G3,ChevronsUp:E3,ChevronsUpDown:W3,Chrome:j,Chromium:j,Church:X3,Cigarette:N3,CigaretteOff:j3,Circle:id,CircleAlert:N,CircleArrowDown:K,CircleArrowLeft:Q,CircleArrowOutDownLeft:J,CircleArrowOutDownRight:_,CircleArrowOutUpLeft:Y,CircleArrowOutUpRight:x,CircleArrowRight:a1,CircleArrowUp:t1,CircleCheck:d1,CircleCheckBig:h1,CircleChevronDown:c1,CircleChevronLeft:M1,CircleChevronRight:p1,CircleChevronUp:i1,CircleDashed:K3,CircleDivide:n1,CircleDollarSign:Q3,CircleDot:Y3,CircleDotDashed:J3,CircleEllipsis:_3,CircleEqual:x3,CircleFadingArrowUp:ad,CircleFadingPlus:td,CircleGauge:l1,CircleHelp:l,CircleMinus:e1,CircleOff:hd,CircleParking:o1,CircleParkingOff:r1,CirclePause:v1,CirclePercent:$1,CirclePlay:m1,CirclePlus:y1,CirclePoundSterling:dd,CirclePower:s1,CircleQuestionMark:l,CircleSlash:cd,CircleSlash2:g1,CircleSlashed:g1,CircleSmall:Md,CircleStar:pd,CircleStop:u1,CircleUser:H1,CircleUserRound:C1,CircleX:A1,CircuitBoard:nd,Citrus:ld,Clapperboard:ed,Clipboard:Cd,ClipboardCheck:rd,ClipboardClock:od,ClipboardCopy:vd,ClipboardEdit:V1,ClipboardList:$d,ClipboardMinus:md,ClipboardPaste:yd,ClipboardPen:V1,ClipboardPenLine:w1,ClipboardPlus:sd,ClipboardSignature:w1,ClipboardType:gd,ClipboardX:ud,Clock:Ud,Clock1:Hd,Clock10:Ad,Clock11:wd,Clock12:Vd,Clock2:Sd,Clock3:Ld,Clock4:fd,Clock5:kd,Clock6:Pd,Clock7:Bd,Clock8:Dd,Clock9:Fd,ClockAlert:zd,ClockArrowDown:bd,ClockArrowUp:Rd,ClockFading:Td,ClockPlus:qd,ClosedCaption:Od,Cloud:t6,CloudAlert:Zd,CloudCheck:Gd,CloudCog:Id,CloudDownload:S1,CloudDrizzle:Wd,CloudFog:Ed,CloudHail:Xd,CloudLightning:Nd,CloudMoon:Kd,CloudMoonRain:jd,CloudOff:Qd,CloudRain:Yd,CloudRainWind:Jd,CloudSnow:_d,CloudSun:a6,CloudSunRain:xd,CloudUpload:L1,Cloudy:h6,Clover:d6,Club:c6,Code:p6,Code2:f1,CodeSquare:n0,CodeXml:f1,Codepen:M6,Codesandbox:i6,Coffee:n6,Cog:l6,Coins:e6,Columns:k1,Columns2:k1,Columns3:P1,Columns3Cog:e,Columns4:r6,ColumnsSettings:e,Combine:o6,Command:v6,Compass:$6,Component:m6,Computer:y6,ConciergeBell:s6,Cone:g6,Construction:u6,Contact:C6,Contact2:B1,ContactRound:B1,Container:H6,Contrast:A6,Cookie:w6,CookingPot:V6,Copy:B6,CopyCheck:S6,CopyMinus:L6,CopyPlus:f6,CopySlash:k6,CopyX:P6,Copyleft:D6,Copyright:F6,CornerDownLeft:b6,CornerDownRight:z6,CornerLeftDown:R6,CornerLeftUp:T6,CornerRightDown:q6,CornerRightUp:U6,CornerUpLeft:G6,CornerUpRight:O6,Cpu:Z6,CreativeCommons:I6,CreditCard:W6,Croissant:E6,Crop:X6,Cross:j6,Crosshair:N6,Crown:K6,Cuboid:Q6,CupSoda:J6,CurlyBraces:D,Currency:Y6,Cylinder:_6,Dam:x6,Database:h8,DatabaseBackup:a8,DatabaseZap:t8,DecimalsArrowLeft:d8,DecimalsArrowRight:c8,Delete:M8,Dessert:p8,Diameter:i8,Diamond:e8,DiamondMinus:n8,DiamondPercent:D1,DiamondPlus:l8,Dice1:r8,Dice2:o8,Dice3:v8,Dice4:$8,Dice5:m8,Dice6:y8,Dices:s8,Diff:g8,Disc:A8,Disc2:u8,Disc3:C8,DiscAlbum:H8,Divide:w8,DivideCircle:n1,DivideSquare:o0,Dna:S8,DnaOff:V8,Dock:L8,Dog:f8,DollarSign:k8,Donut:P8,DoorClosed:D8,DoorClosedLocked:B8,DoorOpen:F8,Dot:z8,DotSquare:v0,Download:b8,DownloadCloud:S1,DraftingCompass:R8,Drama:T8,Dribbble:q8,Drill:U8,Drone:O8,Droplet:G8,DropletOff:Z8,Droplets:I8,Drum:W8,Drumstick:E8,Dumbbell:X8,Ear:j8,EarOff:N8,Earth:F1,EarthLock:K8,Eclipse:Q8,Edit:p,Edit2:P2,Edit3:k2,Egg:Y8,EggFried:J8,EggOff:_8,Ellipsis:b1,EllipsisVertical:z1,Equal:tc,EqualApproximately:x8,EqualNot:ac,EqualSquare:$0,Eraser:dc,EthernetPort:hc,Euro:cc,EvCharger:Mc,Expand:pc,ExternalLink:ic,Eye:ec,EyeClosed:nc,EyeOff:lc,Facebook:rc,Factory:oc,Fan:vc,FastForward:$c,Feather:mc,Fence:yc,FerrisWheel:sc,Figma:gc,File:o7,FileArchive:uc,FileAudio:Hc,FileAudio2:Cc,FileAxis3D:R1,FileAxis3d:R1,FileBadge:wc,FileBadge2:Ac,FileBarChart:T1,FileBarChart2:q1,FileBox:Vc,FileChartColumn:q1,FileChartColumnIncreasing:T1,FileChartLine:U1,FileChartPie:O1,FileCheck:Lc,FileCheck2:Sc,FileClock:fc,FileCode:Pc,FileCode2:kc,FileCog:Z1,FileCog2:Z1,FileDiff:Bc,FileDigit:Dc,FileDown:Fc,FileEdit:I1,FileHeart:zc,FileImage:bc,FileInput:Rc,FileJson:qc,FileJson2:Tc,FileKey:Oc,FileKey2:Uc,FileLineChart:U1,FileLock:Gc,FileLock2:Zc,FileMinus:Wc,FileMinus2:Ic,FileMusic:Ec,FileOutput:Xc,FilePen:I1,FilePenLine:G1,FilePieChart:O1,FilePlay:W1,FilePlus:Nc,FilePlus2:jc,FileQuestion:E1,FileQuestionMark:E1,FileScan:Kc,FileSearch:Jc,FileSearch2:Qc,FileSignature:G1,FileSliders:Yc,FileSpreadsheet:_c,FileStack:xc,FileSymlink:a7,FileTerminal:t7,FileText:h7,FileType:c7,FileType2:d7,FileUp:M7,FileUser:p7,FileVideo:W1,FileVideo2:X1,FileVideoCamera:X1,FileVolume:n7,FileVolume2:i7,FileWarning:l7,FileX:r7,FileX2:e7,Files:v7,Film:$7,Filter:K1,FilterX:Q1,Fingerprint:y7,FireExtinguisher:m7,Fish:u7,FishOff:s7,FishSymbol:g7,Flag:w7,FlagOff:C7,FlagTriangleLeft:H7,FlagTriangleRight:A7,Flame:S7,FlameKindling:V7,Flashlight:f7,FlashlightOff:L7,FlaskConical:P7,FlaskConicalOff:k7,FlaskRound:B7,FlipHorizontal:F7,FlipHorizontal2:D7,FlipVertical:b7,FlipVertical2:z7,Flower:T7,Flower2:R7,Focus:q7,FoldHorizontal:U7,FoldVertical:O7,Folder:vM,FolderArchive:G7,FolderCheck:Z7,FolderClock:I7,FolderClosed:E7,FolderCode:W7,FolderCog:j1,FolderCog2:j1,FolderDot:X7,FolderDown:j7,FolderEdit:N1,FolderGit:K7,FolderGit2:N7,FolderHeart:Q7,FolderInput:J7,FolderKanban:_7,FolderKey:Y7,FolderLock:aM,FolderMinus:x7,FolderOpen:hM,FolderOpenDot:tM,FolderOutput:dM,FolderPen:N1,FolderPlus:cM,FolderRoot:MM,FolderSearch:iM,FolderSearch2:pM,FolderSymlink:nM,FolderSync:lM,FolderTree:eM,FolderUp:rM,FolderX:oM,Folders:$M,Footprints:mM,ForkKnife:Ma,ForkKnifeCrossed:ca,Forklift:yM,FormInput:D2,Forward:sM,Frame:gM,Framer:uM,Frown:CM,Fuel:HM,Fullscreen:AM,FunctionSquare:m0,Funnel:K1,FunnelPlus:wM,FunnelX:Q1,GalleryHorizontal:SM,GalleryHorizontalEnd:VM,GalleryThumbnails:LM,GalleryVertical:kM,GalleryVerticalEnd:fM,Gamepad:BM,Gamepad2:PM,GanttChart:W,GanttChartSquare:$,Gauge:DM,GaugeCircle:l1,Gavel:FM,Gem:zM,GeorgianLari:bM,Ghost:qM,Gift:RM,GitBranch:UM,GitBranchPlus:TM,GitCommit:J1,GitCommitHorizontal:J1,GitCommitVertical:OM,GitCompare:GM,GitCompareArrows:ZM,GitFork:IM,GitGraph:WM,GitMerge:EM,GitPullRequest:JM,GitPullRequestArrow:XM,GitPullRequestClosed:jM,GitPullRequestCreate:KM,GitPullRequestCreateArrow:NM,GitPullRequestDraft:QM,Github:YM,Gitlab:_M,GlassWater:xM,Glasses:a9,Globe:t9,Globe2:F1,GlobeLock:h9,Goal:d9,Gpu:c9,Grab:t2,GraduationCap:M9,Grape:p9,Grid:r,Grid2X2:a2,Grid2X2Check:_1,Grid2X2Plus:Y1,Grid2X2X:x1,Grid2x2:a2,Grid2x2Check:_1,Grid2x2Plus:Y1,Grid2x2X:x1,Grid3X3:r,Grid3x2:i9,Grid3x3:r,Grip:e9,GripHorizontal:n9,GripVertical:l9,Group:r9,Guitar:v9,Ham:o9,Hamburger:$9,Hammer:m9,Hand:A9,HandCoins:y9,HandFist:s9,HandGrab:t2,HandHeart:g9,HandHelping:h2,HandMetal:u9,HandPlatter:C9,Handbag:H9,Handshake:w9,HardDrive:f9,HardDriveDownload:V9,HardDriveUpload:S9,HardHat:L9,Hash:k9,HatGlasses:P9,Haze:D9,HdmiPort:B9,Heading:U9,Heading1:F9,Heading2:z9,Heading3:R9,Heading4:b9,Heading5:T9,Heading6:q9,HeadphoneOff:O9,Headphones:Z9,Headset:G9,Heart:K9,HeartCrack:I9,HeartHandshake:W9,HeartMinus:E9,HeartOff:X9,HeartPlus:j9,HeartPulse:N9,Heater:Q9,HelpCircle:l,HelpingHand:h2,Hexagon:J9,Highlighter:Y9,History:_9,Home:d2,Hop:ap,HopOff:x9,Hospital:tp,Hotel:hp,Hourglass:dp,House:d2,HouseHeart:cp,HousePlug:Mp,HousePlus:pp,HouseWifi:ip,IceCream:c2,IceCream2:M2,IceCreamBowl:M2,IceCreamCone:c2,IdCard:lp,IdCardLanyard:np,Image:sp,ImageDown:ep,ImageMinus:rp,ImageOff:op,ImagePlay:vp,ImagePlus:$p,ImageUp:mp,ImageUpscale:yp,Images:gp,Import:up,Inbox:Hp,Indent:v,IndentDecrease:o,IndentIncrease:v,IndianRupee:Cp,Infinity:Ap,Info:wp,Inspect:H0,InspectionPanel:Vp,Instagram:Sp,Italic:Lp,IterationCcw:fp,IterationCw:kp,JapaneseYen:Pp,Joystick:Bp,Kanban:Dp,KanbanSquare:y0,KanbanSquareDashed:l0,Kayak:zp,Key:Rp,KeyRound:Fp,KeySquare:bp,Keyboard:Up,KeyboardMusic:Tp,KeyboardOff:qp,Lamp:Ep,LampCeiling:Op,LampDesk:Zp,LampFloor:Gp,LampWallDown:Ip,LampWallUp:Wp,LandPlot:Xp,Landmark:jp,Languages:Np,Laptop:Qp,Laptop2:p2,LaptopMinimal:p2,LaptopMinimalCheck:Kp,Lasso:Yp,LassoSelect:Jp,Laugh:_p,Layers:i2,Layers2:xp,Layers3:i2,Layout:f2,LayoutDashboard:ai,LayoutGrid:ti,LayoutList:hi,LayoutPanelLeft:di,LayoutPanelTop:Mi,LayoutTemplate:ci,Leaf:pi,LeafyGreen:ii,Lectern:ni,LetterText:W0,Library:ei,LibraryBig:li,LibrarySquare:s0,LifeBuoy:ri,Ligature:vi,Lightbulb:$i,LightbulbOff:oi,LineChart:Z,LineSquiggle:mi,Link:gi,Link2:si,Link2Off:yi,Linkedin:ui,List:qi,ListCheck:Ci,ListChecks:Hi,ListChevronsDownUp:Ai,ListChevronsUpDown:wi,ListCollapse:Vi,ListEnd:Si,ListFilter:fi,ListFilterPlus:Li,ListIndentDecrease:o,ListIndentIncrease:v,ListMinus:ki,ListMusic:Pi,ListOrdered:Bi,ListPlus:Di,ListRestart:Fi,ListStart:zi,ListTodo:bi,ListTree:Ti,ListVideo:Ri,ListX:Ui,Loader:Zi,Loader2:n2,LoaderCircle:n2,LoaderPinwheel:Oi,Locate:Wi,LocateFixed:Ii,LocateOff:Gi,LocationEdit:o2,Lock:Xi,LockKeyhole:Ei,LockKeyholeOpen:l2,LockOpen:e2,LogIn:Ni,LogOut:ji,Logs:Ki,Lollipop:Qi,Luggage:Ji,MSquare:g0,Magnet:Yi,Mail:Mn,MailCheck:_i,MailMinus:xi,MailOpen:an,MailPlus:tn,MailQuestion:r2,MailQuestionMark:r2,MailSearch:hn,MailWarning:dn,MailX:cn,Mailbox:pn,Mails:nn,Map:wn,MapMinus:ln,MapPin:Cn,MapPinCheck:rn,MapPinCheckInside:en,MapPinHouse:on,MapPinMinus:$n,MapPinMinusInside:vn,MapPinOff:mn,MapPinPen:o2,MapPinPlus:gn,MapPinPlusInside:yn,MapPinX:un,MapPinXInside:sn,MapPinned:Hn,MapPlus:An,Mars:Sn,MarsStroke:Vn,Martini:Ln,Maximize:kn,Maximize2:fn,Medal:Pn,Megaphone:Dn,MegaphoneOff:Bn,Meh:Fn,MemoryStick:zn,Menu:bn,MenuSquare:u0,Merge:Rn,MessageCircle:Xn,MessageCircleCode:Tn,MessageCircleDashed:qn,MessageCircleHeart:Un,MessageCircleMore:On,MessageCircleOff:Zn,MessageCirclePlus:Gn,MessageCircleQuestion:v2,MessageCircleQuestionMark:v2,MessageCircleReply:In,MessageCircleWarning:Wn,MessageCircleX:En,MessageSquare:il,MessageSquareCode:jn,MessageSquareDashed:Nn,MessageSquareDiff:Kn,MessageSquareDot:Qn,MessageSquareHeart:Jn,MessageSquareLock:_n,MessageSquareMore:Yn,MessageSquareOff:xn,MessageSquarePlus:al,MessageSquareQuote:tl,MessageSquareReply:hl,MessageSquareShare:dl,MessageSquareText:cl,MessageSquareWarning:Ml,MessageSquareX:pl,MessagesSquare:nl,Mic:el,Mic2:$2,MicOff:ll,MicVocal:$2,Microchip:rl,Microscope:ol,Microwave:vl,Milestone:$l,Milk:yl,MilkOff:ml,Minimize:gl,Minimize2:sl,Minus:ul,MinusCircle:e1,MinusSquare:C0,Monitor:zl,MonitorCheck:Cl,MonitorCog:Hl,MonitorDot:Al,MonitorDown:wl,MonitorOff:Vl,MonitorPause:Sl,MonitorPlay:Ll,MonitorSmartphone:fl,MonitorSpeaker:kl,MonitorStop:Pl,MonitorUp:Bl,MonitorX:Dl,Moon:bl,MoonStar:Fl,MoreHorizontal:b1,MoreVertical:z1,Motorbike:Rl,Mountain:ql,MountainSnow:Tl,Mouse:Wl,MouseOff:Ul,MousePointer:Il,MousePointer2:Ol,MousePointerBan:Zl,MousePointerClick:Gl,MousePointerSquareDashed:e0,Move:he,Move3D:m2,Move3d:m2,MoveDiagonal:Xl,MoveDiagonal2:El,MoveDown:Kl,MoveDownLeft:jl,MoveDownRight:Nl,MoveHorizontal:Ql,MoveLeft:Jl,MoveRight:Yl,MoveUp:ae,MoveUpLeft:xl,MoveUpRight:_l,MoveVertical:te,Music:pe,Music2:de,Music3:ce,Music4:Me,Navigation:ee,Navigation2:ne,Navigation2Off:ie,NavigationOff:le,Network:re,Newspaper:oe,Nfc:ve,NonBinary:$e,Notebook:ge,NotebookPen:me,NotebookTabs:ye,NotebookText:se,NotepadText:Ce,NotepadTextDashed:ue,Nut:Ae,NutOff:He,Octagon:Ve,OctagonAlert:y2,OctagonMinus:we,OctagonPause:s2,OctagonX:g2,Omega:Se,Option:fe,Orbit:Le,Origami:ke,Outdent:o,Package:Te,Package2:Pe,PackageCheck:Be,PackageMinus:De,PackageOpen:ze,PackagePlus:Fe,PackageSearch:be,PackageX:Re,PaintBucket:qe,PaintRoller:Ue,Paintbrush:Oe,Paintbrush2:u2,PaintbrushVertical:u2,Palette:Ze,Palmtree:N0,Panda:Ge,PanelBottom:Ee,PanelBottomClose:Ie,PanelBottomDashed:C2,PanelBottomInactive:C2,PanelBottomOpen:We,PanelLeft:V2,PanelLeftClose:H2,PanelLeftDashed:A2,PanelLeftInactive:A2,PanelLeftOpen:w2,PanelLeftRightDashed:Xe,PanelRight:Ke,PanelRightClose:je,PanelRightDashed:S2,PanelRightInactive:S2,PanelRightOpen:Ne,PanelTop:_e,PanelTopBottomDashed:Qe,PanelTopClose:Je,PanelTopDashed:L2,PanelTopInactive:L2,PanelTopOpen:Ye,PanelsLeftBottom:xe,PanelsLeftRight:P1,PanelsRightBottom:ar,PanelsTopBottom:b2,PanelsTopLeft:f2,Paperclip:hr,Parentheses:tr,ParkingCircle:o1,ParkingCircleOff:r1,ParkingMeter:dr,ParkingSquare:A0,ParkingSquareOff:w0,PartyPopper:cr,Pause:Mr,PauseCircle:v1,PauseOctagon:s2,PawPrint:pr,PcCase:nr,Pen:P2,PenBox:p,PenLine:k2,PenOff:ir,PenSquare:p,PenTool:lr,Pencil:vr,PencilLine:er,PencilOff:rr,PencilRuler:or,Pentagon:$r,Percent:mr,PercentCircle:$1,PercentDiamond:D1,PercentSquare:V0,PersonStanding:yr,PhilippinePeso:sr,Phone:Vr,PhoneCall:gr,PhoneForwarded:ur,PhoneIncoming:Cr,PhoneMissed:Hr,PhoneOff:Ar,PhoneOutgoing:wr,Pi:Sr,PiSquare:S0,Piano:Lr,Pickaxe:fr,PictureInPicture:Pr,PictureInPicture2:kr,PieChart:X,PiggyBank:Br,Pilcrow:zr,PilcrowLeft:Dr,PilcrowRight:Fr,PilcrowSquare:L0,Pill:Rr,PillBottle:br,Pin:qr,PinOff:Tr,Pipette:Ur,Pizza:Or,Plane:Ir,PlaneLanding:Zr,PlaneTakeoff:Gr,Play:Wr,PlayCircle:m1,PlaySquare:f0,Plug:Xr,Plug2:Er,PlugZap:B2,PlugZap2:B2,Plus:jr,PlusCircle:y1,PlusSquare:k0,Pocket:Kr,PocketKnife:Nr,Podcast:Yr,Pointer:Jr,PointerOff:Qr,Popcorn:_r,Popsicle:xr,PoundSterling:ao,Power:ho,PowerCircle:s1,PowerOff:to,PowerSquare:P0,Presentation:co,Printer:po,PrinterCheck:Mo,Projector:io,Proportions:no,Puzzle:lo,Pyramid:eo,QrCode:ro,Quote:oo,Rabbit:vo,Radar:$o,Radiation:mo,Radical:yo,Radio:uo,RadioReceiver:so,RadioTower:go,Radius:Ho,RailSymbol:Co,Rainbow:Ao,Rat:wo,Ratio:Vo,Receipt:zo,ReceiptCent:So,ReceiptEuro:Lo,ReceiptIndianRupee:fo,ReceiptJapaneseYen:ko,ReceiptPoundSterling:Po,ReceiptRussianRuble:Bo,ReceiptSwissFranc:Do,ReceiptText:Fo,ReceiptTurkishLira:bo,RectangleCircle:Ro,RectangleEllipsis:D2,RectangleGoggles:To,RectangleHorizontal:qo,RectangleVertical:Oo,Recycle:Uo,Redo:Io,Redo2:Zo,RedoDot:Go,RefreshCcw:Wo,RefreshCcwDot:Eo,RefreshCw:jo,RefreshCwOff:Xo,Refrigerator:No,Regex:Ko,RemoveFormatting:Qo,Repeat:_o,Repeat1:Yo,Repeat2:Jo,Replace:av,ReplaceAll:xo,Reply:hv,ReplyAll:tv,Rewind:dv,Ribbon:cv,Rocket:Mv,RockingChair:pv,RollerCoaster:iv,Rose:nv,Rotate3D:F2,Rotate3d:F2,RotateCcw:ev,RotateCcwKey:lv,RotateCcwSquare:ov,RotateCw:$v,RotateCwSquare:rv,Route:mv,RouteOff:vv,Router:yv,Rows:z2,Rows2:z2,Rows3:b2,Rows4:sv,Rss:gv,Ruler:Cv,RulerDimensionLine:uv,RussianRuble:Hv,Sailboat:Av,Salad:wv,Sandwich:Vv,Satellite:Lv,SatelliteDish:Sv,SaudiRiyal:fv,Save:Bv,SaveAll:kv,SaveOff:Pv,Scale:Dv,Scale3D:R2,Scale3d:R2,Scaling:Fv,Scan:Gv,ScanBarcode:zv,ScanEye:bv,ScanFace:Rv,ScanHeart:Tv,ScanLine:qv,ScanQrCode:Uv,ScanSearch:Ov,ScanText:Zv,ScatterChart:E,School:Iv,School2:J0,Scissors:Ev,ScissorsLineDashed:Wv,ScissorsSquare:B0,ScissorsSquareDashedBottom:t0,ScreenShare:jv,ScreenShareOff:Xv,Scroll:Kv,ScrollText:Nv,Search:xv,SearchCheck:Qv,SearchCode:Jv,SearchSlash:Yv,SearchX:_v,Section:a$,Send:h$,SendHorizonal:T2,SendHorizontal:T2,SendToBack:t$,SeparatorHorizontal:d$,SeparatorVertical:c$,Server:n$,ServerCog:M$,ServerCrash:p$,ServerOff:i$,Settings:e$,Settings2:l$,Shapes:o$,Share:v$,Share2:r$,Sheet:$$,Shell:m$,Shield:S$,ShieldAlert:y$,ShieldBan:s$,ShieldCheck:g$,ShieldClose:U2,ShieldEllipsis:u$,ShieldHalf:C$,ShieldMinus:H$,ShieldOff:A$,ShieldPlus:w$,ShieldQuestion:q2,ShieldQuestionMark:q2,ShieldUser:V$,ShieldX:U2,Ship:f$,ShipWheel:L$,Shirt:k$,ShoppingBag:P$,ShoppingBasket:B$,ShoppingCart:D$,Shovel:F$,ShowerHead:z$,Shredder:b$,Shrimp:R$,Shrink:T$,Shrub:q$,Shuffle:U$,Sidebar:V2,SidebarClose:H2,SidebarOpen:w2,Sigma:O$,SigmaSquare:D0,Signal:E$,SignalHigh:Z$,SignalLow:G$,SignalMedium:I$,SignalZero:W$,Signature:X$,Signpost:N$,SignpostBig:j$,Siren:K$,SkipBack:Q$,SkipForward:J$,Skull:Y$,Slack:_$,Slash:x$,SlashSquare:F0,Slice:am,Sliders:O2,SlidersHorizontal:tm,SlidersVertical:O2,Smartphone:cm,SmartphoneCharging:hm,SmartphoneNfc:dm,Smile:pm,SmilePlus:Mm,Snail:im,Snowflake:nm,SoapDispenserDroplet:em,Sofa:lm,SortAsc:w,SortDesc:C,Soup:rm,Space:om,Spade:vm,Sparkle:$m,Sparkles:Z2,Speaker:mm,Speech:ym,SpellCheck:gm,SpellCheck2:sm,Spline:Cm,SplinePointer:um,Split:Hm,SplitSquareHorizontal:z0,SplitSquareVertical:b0,Spool:Am,Spotlight:wm,SprayCan:Vm,Sprout:Sm,Square:Tm,SquareActivity:G2,SquareArrowDown:E2,SquareArrowDownLeft:I2,SquareArrowDownRight:W2,SquareArrowLeft:X2,SquareArrowOutDownLeft:j2,SquareArrowOutDownRight:N2,SquareArrowOutUpLeft:K2,SquareArrowOutUpRight:Q2,SquareArrowRight:J2,SquareArrowUp:x2,SquareArrowUpLeft:Y2,SquareArrowUpRight:_2,SquareAsterisk:a0,SquareBottomDashedScissors:t0,SquareChartGantt:$,SquareCheck:d0,SquareCheckBig:h0,SquareChevronDown:c0,SquareChevronLeft:M0,SquareChevronRight:p0,SquareChevronUp:i0,SquareCode:n0,SquareDashed:r0,SquareDashedBottom:fm,SquareDashedBottomCode:Lm,SquareDashedKanban:l0,SquareDashedMousePointer:e0,SquareDashedTopSolid:km,SquareDivide:o0,SquareDot:v0,SquareEqual:$0,SquareFunction:m0,SquareGanttChart:$,SquareKanban:y0,SquareLibrary:s0,SquareM:g0,SquareMenu:u0,SquareMinus:C0,SquareMousePointer:H0,SquareParking:A0,SquareParkingOff:w0,SquarePause:Pm,SquarePen:p,SquarePercent:V0,SquarePi:S0,SquarePilcrow:L0,SquarePlay:f0,SquarePlus:k0,SquarePower:P0,SquareRadical:Bm,SquareRoundCorner:Dm,SquareScissors:B0,SquareSigma:D0,SquareSlash:F0,SquareSplitHorizontal:z0,SquareSplitVertical:b0,SquareSquare:Fm,SquareStack:zm,SquareStar:bm,SquareStop:Rm,SquareTerminal:R0,SquareUser:q0,SquareUserRound:T0,SquareX:U0,SquaresExclude:qm,SquaresIntersect:Um,SquaresSubtract:Om,SquaresUnite:Zm,Squircle:Gm,SquircleDashed:Im,Squirrel:Wm,Stamp:Em,Star:Nm,StarHalf:Xm,StarOff:jm,Stars:Z2,StepBack:Km,StepForward:Jm,Stethoscope:Qm,Sticker:Ym,StickyNote:_m,StopCircle:u1,Store:xm,StretchHorizontal:ay,StretchVertical:ty,Strikethrough:hy,Subscript:dy,Subtitles:F,Sun:ny,SunDim:cy,SunMedium:My,SunMoon:py,SunSnow:iy,Sunrise:ly,Sunset:ey,Superscript:oy,SwatchBook:ry,SwissFranc:vy,SwitchCamera:$y,Sword:my,Swords:yy,Syringe:sy,Table:Sy,Table2:gy,TableCellsMerge:uy,TableCellsSplit:Hy,TableColumnsSplit:Cy,TableConfig:e,TableOfContents:wy,TableProperties:Ay,TableRowsSplit:Vy,Tablet:fy,TabletSmartphone:Ly,Tablets:ky,Tag:Py,Tags:Dy,Tally1:By,Tally2:Fy,Tally3:zy,Tally4:by,Tally5:Ry,Tangent:Ty,Target:qy,Telescope:Uy,Tent:Zy,TentTree:Oy,Terminal:Gy,TerminalSquare:R0,TestTube:Iy,TestTube2:O0,TestTubeDiagonal:O0,TestTubes:Wy,Text:m,TextAlignCenter:Z0,TextAlignEnd:G0,TextAlignJustify:I0,TextAlignStart:m,TextCursor:Xy,TextCursorInput:Ey,TextInitial:W0,TextQuote:jy,TextSearch:Ny,TextSelect:E0,TextSelection:E0,TextWrap:X0,Theater:Ky,Thermometer:Yy,ThermometerSnowflake:Qy,ThermometerSun:Jy,ThumbsDown:_y,ThumbsUp:xy,Ticket:Ms,TicketCheck:as,TicketMinus:ts,TicketPercent:hs,TicketPlus:ds,TicketSlash:ps,TicketX:cs,Tickets:ns,TicketsPlane:is,Timer:rs,TimerOff:ls,TimerReset:es,ToggleLeft:os,ToggleRight:vs,Toilet:$s,ToolCase:ys,Tornado:ms,Torus:ss,Touchpad:us,TouchpadOff:gs,TowerControl:Hs,ToyBrick:Cs,Tractor:As,TrafficCone:ws,Train:j0,TrainFront:Ss,TrainFrontTunnel:Vs,TrainTrack:Ls,TramFront:j0,Transgender:fs,Trash:Ps,Trash2:ks,TreeDeciduous:Bs,TreePalm:N0,TreePine:Ds,Trees:Fs,Trello:bs,TrendingDown:zs,TrendingUp:Ts,TrendingUpDown:Rs,Triangle:Os,TriangleAlert:K0,TriangleDashed:qs,TriangleRight:Us,Trophy:Zs,Truck:Is,TruckElectric:Gs,TurkishLira:Es,Turntable:Ws,Turtle:Xs,Tv:Ns,Tv2:Q0,TvMinimal:Q0,TvMinimalPlay:js,Twitch:Ks,Twitter:Qs,Type:Ys,TypeOutline:Js,Umbrella:xs,UmbrellaOff:_s,Underline:ag,Undo:dg,Undo2:tg,UndoDot:hg,UnfoldHorizontal:cg,UnfoldVertical:Mg,Ungroup:pg,University:J0,Unlink:lg,Unlink2:ig,Unlock:e2,UnlockKeyhole:l2,Unplug:ng,Upload:eg,UploadCloud:L1,Usb:rg,User:wg,User2:ha,UserCheck:og,UserCheck2:_0,UserCircle:H1,UserCircle2:C1,UserCog:vg,UserCog2:Y0,UserLock:$g,UserMinus:mg,UserMinus2:x0,UserPen:yg,UserPlus:sg,UserPlus2:aa,UserRound:ha,UserRoundCheck:_0,UserRoundCog:Y0,UserRoundMinus:x0,UserRoundPen:gg,UserRoundPlus:aa,UserRoundSearch:ug,UserRoundX:ta,UserSearch:Cg,UserSquare:q0,UserSquare2:T0,UserStar:Hg,UserX:Ag,UserX2:ta,Users:Vg,Users2:da,UsersRound:da,Utensils:Ma,UtensilsCrossed:ca,UtilityPole:Sg,Variable:Lg,Vault:kg,VectorSquare:fg,Vegan:Pg,VenetianMask:Bg,Venus:Fg,VenusAndMars:Dg,Verified:L,Vibrate:bg,VibrateOff:zg,Video:Tg,VideoOff:Rg,Videotape:qg,View:Ug,Voicemail:Og,Volleyball:Zg,Volume:Xg,Volume1:Gg,Volume2:Ig,VolumeOff:Wg,VolumeX:Eg,Vote:jg,Wallet:Kg,Wallet2:pa,WalletCards:Ng,WalletMinimal:pa,Wallpaper:Qg,Wand:Jg,Wand2:ia,WandSparkles:ia,Warehouse:Yg,WashingMachine:_g,Watch:xg,Waves:tu,WavesLadder:au,Waypoints:hu,Webcam:du,Webhook:pu,WebhookOff:Mu,Weight:cu,Wheat:nu,WheatOff:iu,WholeWord:lu,Wifi:su,WifiCog:eu,WifiHigh:ru,WifiLow:ou,WifiOff:mu,WifiPen:vu,WifiSync:$u,WifiZero:yu,Wind:uu,WindArrowDown:gu,Wine:Hu,WineOff:Cu,Workflow:Au,Worm:wu,WrapText:X0,Wrench:Vu,X:Su,XCircle:A1,XOctagon:g2,XSquare:U0,Youtube:Lu,Zap:ku,ZapOff:fu,ZoomIn:Pu,ZoomOut:Bu});const Ou=({icons:t=Du,nameAttr:h="data-lucide",attrs:d={},root:c=document}={})=>{if(!Object.values(t).length)throw new Error(`Please provide an icons object.
-If you want to use all the icons you can import it like:
- \`import { createIcons, icons } from 'lucide';
-lucide.createIcons({icons});\``);if(typeof c>"u")throw new Error("`createIcons()` only works in a browser environment.");const M=c.querySelectorAll(`[${h}]`);if(Array.from(M).forEach(i=>ra(i,{nameAttr:h,icons:t,attrs:d})),h==="data-lucide"){const i=c.querySelectorAll("[icon-name]");i.length>0&&(console.warn("[Lucide] Some icons were found with the now deprecated icon-name attribute. These will still be replaced for backwards compatibility, but will no longer be supported in v1.0 and you should switch to data-lucide"),Array.from(i).forEach(na=>ra(na,{nameAttr:"icon-name",icons:t,attrs:d})))}};a.AArrowDown=oa,a.AArrowUp=va,a.ALargeSmall=$a,a.Accessibility=ma,a.Activity=ya,a.ActivitySquare=G2,a.AirVent=sa,a.Airplay=ga,a.AlarmCheck=y,a.AlarmClock=Ca,a.AlarmClockCheck=y,a.AlarmClockMinus=s,a.AlarmClockOff=ua,a.AlarmClockPlus=g,a.AlarmMinus=s,a.AlarmPlus=g,a.AlarmSmoke=Ha,a.Album=Aa,a.AlertCircle=N,a.AlertOctagon=y2,a.AlertTriangle=K0,a.AlignCenter=Z0,a.AlignCenterHorizontal=wa,a.AlignCenterVertical=Va,a.AlignEndHorizontal=Sa,a.AlignEndVertical=La,a.AlignHorizontalDistributeCenter=fa,a.AlignHorizontalDistributeEnd=ka,a.AlignHorizontalDistributeStart=Pa,a.AlignHorizontalJustifyCenter=Ba,a.AlignHorizontalJustifyEnd=Da,a.AlignHorizontalJustifyStart=Fa,a.AlignHorizontalSpaceAround=za,a.AlignHorizontalSpaceBetween=ba,a.AlignJustify=I0,a.AlignLeft=m,a.AlignRight=G0,a.AlignStartHorizontal=Ra,a.AlignStartVertical=Ta,a.AlignVerticalDistributeCenter=qa,a.AlignVerticalDistributeEnd=Ua,a.AlignVerticalDistributeStart=Oa,a.AlignVerticalJustifyCenter=Za,a.AlignVerticalJustifyEnd=Ga,a.AlignVerticalJustifyStart=Ia,a.AlignVerticalSpaceAround=Wa,a.AlignVerticalSpaceBetween=Ea,a.Ambulance=ja,a.Ampersand=Xa,a.Ampersands=Na,a.Amphora=Ka,a.Anchor=Qa,a.Angry=Ja,a.Annoyed=Ya,a.Antenna=_a,a.Anvil=xa,a.Aperture=at,a.AppWindow=ht,a.AppWindowMac=tt,a.Apple=dt,a.Archive=pt,a.ArchiveRestore=ct,a.ArchiveX=Mt,a.AreaChart=z,a.Armchair=it,a.ArrowBigDown=lt,a.ArrowBigDownDash=nt,a.ArrowBigLeft=rt,a.ArrowBigLeftDash=et,a.ArrowBigRight=vt,a.ArrowBigRightDash=ot,a.ArrowBigUp=st,a.ArrowBigUpDash=$t,a.ArrowDown=St,a.ArrowDown01=mt,a.ArrowDown10=yt,a.ArrowDownAZ=u,a.ArrowDownAz=u,a.ArrowDownCircle=K,a.ArrowDownFromLine=gt,a.ArrowDownLeft=Ct,a.ArrowDownLeftFromCircle=J,a.ArrowDownLeftFromSquare=j2,a.ArrowDownLeftSquare=I2,a.ArrowDownNarrowWide=ut,a.ArrowDownRight=At,a.ArrowDownRightFromCircle=_,a.ArrowDownRightFromSquare=N2,a.ArrowDownRightSquare=W2,a.ArrowDownSquare=E2,a.ArrowDownToDot=Ht,a.ArrowDownToLine=wt,a.ArrowDownUp=Vt,a.ArrowDownWideNarrow=C,a.ArrowDownZA=H,a.ArrowDownZa=H,a.ArrowLeft=Pt,a.ArrowLeftCircle=Q,a.ArrowLeftFromLine=Lt,a.ArrowLeftRight=ft,a.ArrowLeftSquare=X2,a.ArrowLeftToLine=kt,a.ArrowRight=zt,a.ArrowRightCircle=a1,a.ArrowRightFromLine=Bt,a.ArrowRightLeft=Ft,a.ArrowRightSquare=J2,a.ArrowRightToLine=Dt,a.ArrowUp=Wt,a.ArrowUp01=bt,a.ArrowUp10=Rt,a.ArrowUpAZ=A,a.ArrowUpAz=A,a.ArrowUpCircle=t1,a.ArrowUpDown=Tt,a.ArrowUpFromDot=Ut,a.ArrowUpFromLine=qt,a.ArrowUpLeft=Ot,a.ArrowUpLeftFromCircle=Y,a.ArrowUpLeftFromSquare=K2,a.ArrowUpLeftSquare=Y2,a.ArrowUpNarrowWide=w,a.ArrowUpRight=Zt,a.ArrowUpRightFromCircle=x,a.ArrowUpRightFromSquare=Q2,a.ArrowUpRightSquare=_2,a.ArrowUpSquare=x2,a.ArrowUpToLine=It,a.ArrowUpWideNarrow=Gt,a.ArrowUpZA=V,a.ArrowUpZa=V,a.ArrowsUpFromLine=Et,a.Asterisk=Xt,a.AsteriskSquare=a0,a.AtSign=jt,a.Atom=Qt,a.AudioLines=Nt,a.AudioWaveform=Kt,a.Award=Jt,a.Axe=Yt,a.Axis3D=S,a.Axis3d=S,a.Baby=_t,a.Backpack=xt,a.Badge=mh,a.BadgeAlert=ah,a.BadgeCent=th,a.BadgeCheck=L,a.BadgeDollarSign=hh,a.BadgeEuro=dh,a.BadgeHelp=f,a.BadgeIndianRupee=ch,a.BadgeInfo=Mh,a.BadgeJapaneseYen=ph,a.BadgeMinus=ih,a.BadgePercent=nh,a.BadgePlus=lh,a.BadgePoundSterling=eh,a.BadgeQuestionMark=f,a.BadgeRussianRuble=rh,a.BadgeSwissFranc=oh,a.BadgeTurkishLira=vh,a.BadgeX=$h,a.BaggageClaim=yh,a.Ban=sh,a.Banana=gh,a.Bandage=uh,a.Banknote=wh,a.BanknoteArrowDown=Ch,a.BanknoteArrowUp=Hh,a.BanknoteX=Ah,a.BarChart=G,a.BarChart2=I,a.BarChart3=O,a.BarChart4=U,a.BarChartBig=q,a.BarChartHorizontal=T,a.BarChartHorizontalBig=b,a.Barcode=Vh,a.Barrel=Sh,a.Baseline=Lh,a.Bath=fh,a.Battery=bh,a.BatteryCharging=kh,a.BatteryFull=Ph,a.BatteryLow=Bh,a.BatteryMedium=Dh,a.BatteryPlus=Fh,a.BatteryWarning=zh,a.Beaker=Rh,a.Bean=qh,a.BeanOff=Th,a.Bed=Zh,a.BedDouble=Uh,a.BedSingle=Oh,a.Beef=Gh,a.Beer=Wh,a.BeerOff=Ih,a.Bell=Jh,a.BellDot=Eh,a.BellElectric=Xh,a.BellMinus=jh,a.BellOff=Nh,a.BellPlus=Kh,a.BellRing=Qh,a.BetweenHorizonalEnd=k,a.BetweenHorizonalStart=P,a.BetweenHorizontalEnd=k,a.BetweenHorizontalStart=P,a.BetweenVerticalEnd=Yh,a.BetweenVerticalStart=_h,a.BicepsFlexed=xh,a.Bike=a4,a.Binary=t4,a.Binoculars=h4,a.Biohazard=d4,a.Bird=c4,a.Bitcoin=p4,a.Blend=M4,a.Blinds=n4,a.Blocks=i4,a.Bluetooth=r4,a.BluetoothConnected=l4,a.BluetoothOff=o4,a.BluetoothSearching=e4,a.Bold=v4,a.Bolt=$4,a.Bomb=m4,a.Bone=y4,a.Book=Z4,a.BookA=s4,a.BookAlert=g4,a.BookAudio=u4,a.BookCheck=C4,a.BookCopy=H4,a.BookDashed=B,a.BookDown=A4,a.BookHeadphones=w4,a.BookHeart=V4,a.BookImage=S4,a.BookKey=L4,a.BookLock=f4,a.BookMarked=k4,a.BookMinus=P4,a.BookOpen=F4,a.BookOpenCheck=B4,a.BookOpenText=D4,a.BookPlus=z4,a.BookTemplate=B,a.BookText=b4,a.BookType=T4,a.BookUp=q4,a.BookUp2=R4,a.BookUser=U4,a.BookX=O4,a.Bookmark=X4,a.BookmarkCheck=G4,a.BookmarkMinus=I4,a.BookmarkPlus=W4,a.BookmarkX=E4,a.BoomBox=j4,a.Bot=Q4,a.BotMessageSquare=K4,a.BotOff=N4,a.BottleWine=J4,a.BowArrow=Y4,a.Box=_4,a.BoxSelect=r0,a.Boxes=x4,a.Braces=D,a.Brackets=a5,a.Brain=d5,a.BrainCircuit=t5,a.BrainCog=h5,a.BrickWall=p5,a.BrickWallFire=c5,a.BrickWallShield=M5,a.Briefcase=e5,a.BriefcaseBusiness=i5,a.BriefcaseConveyorBelt=n5,a.BriefcaseMedical=l5,a.BringToFront=r5,a.Brush=v5,a.BrushCleaning=o5,a.Bubbles=$5,a.Bug=s5,a.BugOff=m5,a.BugPlay=y5,a.Building=u5,a.Building2=g5,a.Bus=H5,a.BusFront=C5,a.Cable=w5,a.CableCar=A5,a.Cake=S5,a.CakeSlice=V5,a.Calculator=L5,a.Calendar=N5,a.Calendar1=f5,a.CalendarArrowDown=k5,a.CalendarArrowUp=P5,a.CalendarCheck=D5,a.CalendarCheck2=B5,a.CalendarClock=F5,a.CalendarCog=R5,a.CalendarDays=z5,a.CalendarFold=b5,a.CalendarHeart=T5,a.CalendarMinus=U5,a.CalendarMinus2=q5,a.CalendarOff=O5,a.CalendarPlus=Z5,a.CalendarPlus2=I5,a.CalendarRange=G5,a.CalendarSearch=E5,a.CalendarSync=W5,a.CalendarX=j5,a.CalendarX2=X5,a.Camera=Q5,a.CameraOff=K5,a.CandlestickChart=R,a.Candy=_5,a.CandyCane=J5,a.CandyOff=Y5,a.Cannabis=x5,a.Captions=F,a.CaptionsOff=a3,a.Car=d3,a.CarFront=t3,a.CarTaxiFront=h3,a.Caravan=c3,a.CardSim=M3,a.Carrot=p3,a.CaseLower=i3,a.CaseSensitive=n3,a.CaseUpper=l3,a.CassetteTape=e3,a.Cast=r3,a.Castle=o3,a.Cat=v3,a.Cctv=$3,a.ChartArea=z,a.ChartBar=T,a.ChartBarBig=b,a.ChartBarDecreasing=m3,a.ChartBarIncreasing=y3,a.ChartBarStacked=s3,a.ChartCandlestick=R,a.ChartColumn=O,a.ChartColumnBig=q,a.ChartColumnDecreasing=g3,a.ChartColumnIncreasing=U,a.ChartColumnStacked=u3,a.ChartGantt=C3,a.ChartLine=Z,a.ChartNetwork=H3,a.ChartNoAxesColumn=I,a.ChartNoAxesColumnDecreasing=A3,a.ChartNoAxesColumnIncreasing=G,a.ChartNoAxesCombined=w3,a.ChartNoAxesGantt=W,a.ChartPie=X,a.ChartScatter=E,a.ChartSpline=V3,a.Check=f3,a.CheckCheck=S3,a.CheckCircle=h1,a.CheckCircle2=d1,a.CheckLine=L3,a.CheckSquare=h0,a.CheckSquare2=d0,a.ChefHat=k3,a.Cherry=B3,a.ChevronDown=P3,a.ChevronDownCircle=c1,a.ChevronDownSquare=c0,a.ChevronFirst=D3,a.ChevronLast=F3,a.ChevronLeft=z3,a.ChevronLeftCircle=M1,a.ChevronLeftSquare=M0,a.ChevronRight=b3,a.ChevronRightCircle=p1,a.ChevronRightSquare=p0,a.ChevronUp=R3,a.ChevronUpCircle=i1,a.ChevronUpSquare=i0,a.ChevronsDown=q3,a.ChevronsDownUp=T3,a.ChevronsLeft=Z3,a.ChevronsLeftRight=O3,a.ChevronsLeftRightEllipsis=U3,a.ChevronsRight=I3,a.ChevronsRightLeft=G3,a.ChevronsUp=E3,a.ChevronsUpDown=W3,a.Chrome=j,a.Chromium=j,a.Church=X3,a.Cigarette=N3,a.CigaretteOff=j3,a.Circle=id,a.CircleAlert=N,a.CircleArrowDown=K,a.CircleArrowLeft=Q,a.CircleArrowOutDownLeft=J,a.CircleArrowOutDownRight=_,a.CircleArrowOutUpLeft=Y,a.CircleArrowOutUpRight=x,a.CircleArrowRight=a1,a.CircleArrowUp=t1,a.CircleCheck=d1,a.CircleCheckBig=h1,a.CircleChevronDown=c1,a.CircleChevronLeft=M1,a.CircleChevronRight=p1,a.CircleChevronUp=i1,a.CircleDashed=K3,a.CircleDivide=n1,a.CircleDollarSign=Q3,a.CircleDot=Y3,a.CircleDotDashed=J3,a.CircleEllipsis=_3,a.CircleEqual=x3,a.CircleFadingArrowUp=ad,a.CircleFadingPlus=td,a.CircleGauge=l1,a.CircleHelp=l,a.CircleMinus=e1,a.CircleOff=hd,a.CircleParking=o1,a.CircleParkingOff=r1,a.CirclePause=v1,a.CirclePercent=$1,a.CirclePlay=m1,a.CirclePlus=y1,a.CirclePoundSterling=dd,a.CirclePower=s1,a.CircleQuestionMark=l,a.CircleSlash=cd,a.CircleSlash2=g1,a.CircleSlashed=g1,a.CircleSmall=Md,a.CircleStar=pd,a.CircleStop=u1,a.CircleUser=H1,a.CircleUserRound=C1,a.CircleX=A1,a.CircuitBoard=nd,a.Citrus=ld,a.Clapperboard=ed,a.Clipboard=Cd,a.ClipboardCheck=rd,a.ClipboardClock=od,a.ClipboardCopy=vd,a.ClipboardEdit=V1,a.ClipboardList=$d,a.ClipboardMinus=md,a.ClipboardPaste=yd,a.ClipboardPen=V1,a.ClipboardPenLine=w1,a.ClipboardPlus=sd,a.ClipboardSignature=w1,a.ClipboardType=gd,a.ClipboardX=ud,a.Clock=Ud,a.Clock1=Hd,a.Clock10=Ad,a.Clock11=wd,a.Clock12=Vd,a.Clock2=Sd,a.Clock3=Ld,a.Clock4=fd,a.Clock5=kd,a.Clock6=Pd,a.Clock7=Bd,a.Clock8=Dd,a.Clock9=Fd,a.ClockAlert=zd,a.ClockArrowDown=bd,a.ClockArrowUp=Rd,a.ClockFading=Td,a.ClockPlus=qd,a.ClosedCaption=Od,a.Cloud=t6,a.CloudAlert=Zd,a.CloudCheck=Gd,a.CloudCog=Id,a.CloudDownload=S1,a.CloudDrizzle=Wd,a.CloudFog=Ed,a.CloudHail=Xd,a.CloudLightning=Nd,a.CloudMoon=Kd,a.CloudMoonRain=jd,a.CloudOff=Qd,a.CloudRain=Yd,a.CloudRainWind=Jd,a.CloudSnow=_d,a.CloudSun=a6,a.CloudSunRain=xd,a.CloudUpload=L1,a.Cloudy=h6,a.Clover=d6,a.Club=c6,a.Code=p6,a.Code2=f1,a.CodeSquare=n0,a.CodeXml=f1,a.Codepen=M6,a.Codesandbox=i6,a.Coffee=n6,a.Cog=l6,a.Coins=e6,a.Columns=k1,a.Columns2=k1,a.Columns3=P1,a.Columns3Cog=e,a.Columns4=r6,a.ColumnsSettings=e,a.Combine=o6,a.Command=v6,a.Compass=$6,a.Component=m6,a.Computer=y6,a.ConciergeBell=s6,a.Cone=g6,a.Construction=u6,a.Contact=C6,a.Contact2=B1,a.ContactRound=B1,a.Container=H6,a.Contrast=A6,a.Cookie=w6,a.CookingPot=V6,a.Copy=B6,a.CopyCheck=S6,a.CopyMinus=L6,a.CopyPlus=f6,a.CopySlash=k6,a.CopyX=P6,a.Copyleft=D6,a.Copyright=F6,a.CornerDownLeft=b6,a.CornerDownRight=z6,a.CornerLeftDown=R6,a.CornerLeftUp=T6,a.CornerRightDown=q6,a.CornerRightUp=U6,a.CornerUpLeft=G6,a.CornerUpRight=O6,a.Cpu=Z6,a.CreativeCommons=I6,a.CreditCard=W6,a.Croissant=E6,a.Crop=X6,a.Cross=j6,a.Crosshair=N6,a.Crown=K6,a.Cuboid=Q6,a.CupSoda=J6,a.CurlyBraces=D,a.Currency=Y6,a.Cylinder=_6,a.Dam=x6,a.Database=h8,a.DatabaseBackup=a8,a.DatabaseZap=t8,a.DecimalsArrowLeft=d8,a.DecimalsArrowRight=c8,a.Delete=M8,a.Dessert=p8,a.Diameter=i8,a.Diamond=e8,a.DiamondMinus=n8,a.DiamondPercent=D1,a.DiamondPlus=l8,a.Dice1=r8,a.Dice2=o8,a.Dice3=v8,a.Dice4=$8,a.Dice5=m8,a.Dice6=y8,a.Dices=s8,a.Diff=g8,a.Disc=A8,a.Disc2=u8,a.Disc3=C8,a.DiscAlbum=H8,a.Divide=w8,a.DivideCircle=n1,a.DivideSquare=o0,a.Dna=S8,a.DnaOff=V8,a.Dock=L8,a.Dog=f8,a.DollarSign=k8,a.Donut=P8,a.DoorClosed=D8,a.DoorClosedLocked=B8,a.DoorOpen=F8,a.Dot=z8,a.DotSquare=v0,a.Download=b8,a.DownloadCloud=S1,a.DraftingCompass=R8,a.Drama=T8,a.Dribbble=q8,a.Drill=U8,a.Drone=O8,a.Droplet=G8,a.DropletOff=Z8,a.Droplets=I8,a.Drum=W8,a.Drumstick=E8,a.Dumbbell=X8,a.Ear=j8,a.EarOff=N8,a.Earth=F1,a.EarthLock=K8,a.Eclipse=Q8,a.Edit=p,a.Edit2=P2,a.Edit3=k2,a.Egg=Y8,a.EggFried=J8,a.EggOff=_8,a.Ellipsis=b1,a.EllipsisVertical=z1,a.Equal=tc,a.EqualApproximately=x8,a.EqualNot=ac,a.EqualSquare=$0,a.Eraser=dc,a.EthernetPort=hc,a.Euro=cc,a.EvCharger=Mc,a.Expand=pc,a.ExternalLink=ic,a.Eye=ec,a.EyeClosed=nc,a.EyeOff=lc,a.Facebook=rc,a.Factory=oc,a.Fan=vc,a.FastForward=$c,a.Feather=mc,a.Fence=yc,a.FerrisWheel=sc,a.Figma=gc,a.File=o7,a.FileArchive=uc,a.FileAudio=Hc,a.FileAudio2=Cc,a.FileAxis3D=R1,a.FileAxis3d=R1,a.FileBadge=wc,a.FileBadge2=Ac,a.FileBarChart=T1,a.FileBarChart2=q1,a.FileBox=Vc,a.FileChartColumn=q1,a.FileChartColumnIncreasing=T1,a.FileChartLine=U1,a.FileChartPie=O1,a.FileCheck=Lc,a.FileCheck2=Sc,a.FileClock=fc,a.FileCode=Pc,a.FileCode2=kc,a.FileCog=Z1,a.FileCog2=Z1,a.FileDiff=Bc,a.FileDigit=Dc,a.FileDown=Fc,a.FileEdit=I1,a.FileHeart=zc,a.FileImage=bc,a.FileInput=Rc,a.FileJson=qc,a.FileJson2=Tc,a.FileKey=Oc,a.FileKey2=Uc,a.FileLineChart=U1,a.FileLock=Gc,a.FileLock2=Zc,a.FileMinus=Wc,a.FileMinus2=Ic,a.FileMusic=Ec,a.FileOutput=Xc,a.FilePen=I1,a.FilePenLine=G1,a.FilePieChart=O1,a.FilePlay=W1,a.FilePlus=Nc,a.FilePlus2=jc,a.FileQuestion=E1,a.FileQuestionMark=E1,a.FileScan=Kc,a.FileSearch=Jc,a.FileSearch2=Qc,a.FileSignature=G1,a.FileSliders=Yc,a.FileSpreadsheet=_c,a.FileStack=xc,a.FileSymlink=a7,a.FileTerminal=t7,a.FileText=h7,a.FileType=c7,a.FileType2=d7,a.FileUp=M7,a.FileUser=p7,a.FileVideo=W1,a.FileVideo2=X1,a.FileVideoCamera=X1,a.FileVolume=n7,a.FileVolume2=i7,a.FileWarning=l7,a.FileX=r7,a.FileX2=e7,a.Files=v7,a.Film=$7,a.Filter=K1,a.FilterX=Q1,a.Fingerprint=y7,a.FireExtinguisher=m7,a.Fish=u7,a.FishOff=s7,a.FishSymbol=g7,a.Flag=w7,a.FlagOff=C7,a.FlagTriangleLeft=H7,a.FlagTriangleRight=A7,a.Flame=S7,a.FlameKindling=V7,a.Flashlight=f7,a.FlashlightOff=L7,a.FlaskConical=P7,a.FlaskConicalOff=k7,a.FlaskRound=B7,a.FlipHorizontal=F7,a.FlipHorizontal2=D7,a.FlipVertical=b7,a.FlipVertical2=z7,a.Flower=T7,a.Flower2=R7,a.Focus=q7,a.FoldHorizontal=U7,a.FoldVertical=O7,a.Folder=vM,a.FolderArchive=G7,a.FolderCheck=Z7,a.FolderClock=I7,a.FolderClosed=E7,a.FolderCode=W7,a.FolderCog=j1,a.FolderCog2=j1,a.FolderDot=X7,a.FolderDown=j7,a.FolderEdit=N1,a.FolderGit=K7,a.FolderGit2=N7,a.FolderHeart=Q7,a.FolderInput=J7,a.FolderKanban=_7,a.FolderKey=Y7,a.FolderLock=aM,a.FolderMinus=x7,a.FolderOpen=hM,a.FolderOpenDot=tM,a.FolderOutput=dM,a.FolderPen=N1,a.FolderPlus=cM,a.FolderRoot=MM,a.FolderSearch=iM,a.FolderSearch2=pM,a.FolderSymlink=nM,a.FolderSync=lM,a.FolderTree=eM,a.FolderUp=rM,a.FolderX=oM,a.Folders=$M,a.Footprints=mM,a.ForkKnife=Ma,a.ForkKnifeCrossed=ca,a.Forklift=yM,a.FormInput=D2,a.Forward=sM,a.Frame=gM,a.Framer=uM,a.Frown=CM,a.Fuel=HM,a.Fullscreen=AM,a.FunctionSquare=m0,a.Funnel=K1,a.FunnelPlus=wM,a.FunnelX=Q1,a.GalleryHorizontal=SM,a.GalleryHorizontalEnd=VM,a.GalleryThumbnails=LM,a.GalleryVertical=kM,a.GalleryVerticalEnd=fM,a.Gamepad=BM,a.Gamepad2=PM,a.GanttChart=W,a.GanttChartSquare=$,a.Gauge=DM,a.GaugeCircle=l1,a.Gavel=FM,a.Gem=zM,a.GeorgianLari=bM,a.Ghost=qM,a.Gift=RM,a.GitBranch=UM,a.GitBranchPlus=TM,a.GitCommit=J1,a.GitCommitHorizontal=J1,a.GitCommitVertical=OM,a.GitCompare=GM,a.GitCompareArrows=ZM,a.GitFork=IM,a.GitGraph=WM,a.GitMerge=EM,a.GitPullRequest=JM,a.GitPullRequestArrow=XM,a.GitPullRequestClosed=jM,a.GitPullRequestCreate=KM,a.GitPullRequestCreateArrow=NM,a.GitPullRequestDraft=QM,a.Github=YM,a.Gitlab=_M,a.GlassWater=xM,a.Glasses=a9,a.Globe=t9,a.Globe2=F1,a.GlobeLock=h9,a.Goal=d9,a.Gpu=c9,a.Grab=t2,a.GraduationCap=M9,a.Grape=p9,a.Grid=r,a.Grid2X2=a2,a.Grid2X2Check=_1,a.Grid2X2Plus=Y1,a.Grid2X2X=x1,a.Grid2x2=a2,a.Grid2x2Check=_1,a.Grid2x2Plus=Y1,a.Grid2x2X=x1,a.Grid3X3=r,a.Grid3x2=i9,a.Grid3x3=r,a.Grip=e9,a.GripHorizontal=n9,a.GripVertical=l9,a.Group=r9,a.Guitar=v9,a.Ham=o9,a.Hamburger=$9,a.Hammer=m9,a.Hand=A9,a.HandCoins=y9,a.HandFist=s9,a.HandGrab=t2,a.HandHeart=g9,a.HandHelping=h2,a.HandMetal=u9,a.HandPlatter=C9,a.Handbag=H9,a.Handshake=w9,a.HardDrive=f9,a.HardDriveDownload=V9,a.HardDriveUpload=S9,a.HardHat=L9,a.Hash=k9,a.HatGlasses=P9,a.Haze=D9,a.HdmiPort=B9,a.Heading=U9,a.Heading1=F9,a.Heading2=z9,a.Heading3=R9,a.Heading4=b9,a.Heading5=T9,a.Heading6=q9,a.HeadphoneOff=O9,a.Headphones=Z9,a.Headset=G9,a.Heart=K9,a.HeartCrack=I9,a.HeartHandshake=W9,a.HeartMinus=E9,a.HeartOff=X9,a.HeartPlus=j9,a.HeartPulse=N9,a.Heater=Q9,a.HelpCircle=l,a.HelpingHand=h2,a.Hexagon=J9,a.Highlighter=Y9,a.History=_9,a.Home=d2,a.Hop=ap,a.HopOff=x9,a.Hospital=tp,a.Hotel=hp,a.Hourglass=dp,a.House=d2,a.HouseHeart=cp,a.HousePlug=Mp,a.HousePlus=pp,a.HouseWifi=ip,a.IceCream=c2,a.IceCream2=M2,a.IceCreamBowl=M2,a.IceCreamCone=c2,a.IdCard=lp,a.IdCardLanyard=np,a.Image=sp,a.ImageDown=ep,a.ImageMinus=rp,a.ImageOff=op,a.ImagePlay=vp,a.ImagePlus=$p,a.ImageUp=mp,a.ImageUpscale=yp,a.Images=gp,a.Import=up,a.Inbox=Hp,a.Indent=v,a.IndentDecrease=o,a.IndentIncrease=v,a.IndianRupee=Cp,a.Infinity=Ap,a.Info=wp,a.Inspect=H0,a.InspectionPanel=Vp,a.Instagram=Sp,a.Italic=Lp,a.IterationCcw=fp,a.IterationCw=kp,a.JapaneseYen=Pp,a.Joystick=Bp,a.Kanban=Dp,a.KanbanSquare=y0,a.KanbanSquareDashed=l0,a.Kayak=zp,a.Key=Rp,a.KeyRound=Fp,a.KeySquare=bp,a.Keyboard=Up,a.KeyboardMusic=Tp,a.KeyboardOff=qp,a.Lamp=Ep,a.LampCeiling=Op,a.LampDesk=Zp,a.LampFloor=Gp,a.LampWallDown=Ip,a.LampWallUp=Wp,a.LandPlot=Xp,a.Landmark=jp,a.Languages=Np,a.Laptop=Qp,a.Laptop2=p2,a.LaptopMinimal=p2,a.LaptopMinimalCheck=Kp,a.Lasso=Yp,a.LassoSelect=Jp,a.Laugh=_p,a.Layers=i2,a.Layers2=xp,a.Layers3=i2,a.Layout=f2,a.LayoutDashboard=ai,a.LayoutGrid=ti,a.LayoutList=hi,a.LayoutPanelLeft=di,a.LayoutPanelTop=Mi,a.LayoutTemplate=ci,a.Leaf=pi,a.LeafyGreen=ii,a.Lectern=ni,a.LetterText=W0,a.Library=ei,a.LibraryBig=li,a.LibrarySquare=s0,a.LifeBuoy=ri,a.Ligature=vi,a.Lightbulb=$i,a.LightbulbOff=oi,a.LineChart=Z,a.LineSquiggle=mi,a.Link=gi,a.Link2=si,a.Link2Off=yi,a.Linkedin=ui,a.List=qi,a.ListCheck=Ci,a.ListChecks=Hi,a.ListChevronsDownUp=Ai,a.ListChevronsUpDown=wi,a.ListCollapse=Vi,a.ListEnd=Si,a.ListFilter=fi,a.ListFilterPlus=Li,a.ListIndentDecrease=o,a.ListIndentIncrease=v,a.ListMinus=ki,a.ListMusic=Pi,a.ListOrdered=Bi,a.ListPlus=Di,a.ListRestart=Fi,a.ListStart=zi,a.ListTodo=bi,a.ListTree=Ti,a.ListVideo=Ri,a.ListX=Ui,a.Loader=Zi,a.Loader2=n2,a.LoaderCircle=n2,a.LoaderPinwheel=Oi,a.Locate=Wi,a.LocateFixed=Ii,a.LocateOff=Gi,a.LocationEdit=o2,a.Lock=Xi,a.LockKeyhole=Ei,a.LockKeyholeOpen=l2,a.LockOpen=e2,a.LogIn=Ni,a.LogOut=ji,a.Logs=Ki,a.Lollipop=Qi,a.Luggage=Ji,a.MSquare=g0,a.Magnet=Yi,a.Mail=Mn,a.MailCheck=_i,a.MailMinus=xi,a.MailOpen=an,a.MailPlus=tn,a.MailQuestion=r2,a.MailQuestionMark=r2,a.MailSearch=hn,a.MailWarning=dn,a.MailX=cn,a.Mailbox=pn,a.Mails=nn,a.Map=wn,a.MapMinus=ln,a.MapPin=Cn,a.MapPinCheck=rn,a.MapPinCheckInside=en,a.MapPinHouse=on,a.MapPinMinus=$n,a.MapPinMinusInside=vn,a.MapPinOff=mn,a.MapPinPen=o2,a.MapPinPlus=gn,a.MapPinPlusInside=yn,a.MapPinX=un,a.MapPinXInside=sn,a.MapPinned=Hn,a.MapPlus=An,a.Mars=Sn,a.MarsStroke=Vn,a.Martini=Ln,a.Maximize=kn,a.Maximize2=fn,a.Medal=Pn,a.Megaphone=Dn,a.MegaphoneOff=Bn,a.Meh=Fn,a.MemoryStick=zn,a.Menu=bn,a.MenuSquare=u0,a.Merge=Rn,a.MessageCircle=Xn,a.MessageCircleCode=Tn,a.MessageCircleDashed=qn,a.MessageCircleHeart=Un,a.MessageCircleMore=On,a.MessageCircleOff=Zn,a.MessageCirclePlus=Gn,a.MessageCircleQuestion=v2,a.MessageCircleQuestionMark=v2,a.MessageCircleReply=In,a.MessageCircleWarning=Wn,a.MessageCircleX=En,a.MessageSquare=il,a.MessageSquareCode=jn,a.MessageSquareDashed=Nn,a.MessageSquareDiff=Kn,a.MessageSquareDot=Qn,a.MessageSquareHeart=Jn,a.MessageSquareLock=_n,a.MessageSquareMore=Yn,a.MessageSquareOff=xn,a.MessageSquarePlus=al,a.MessageSquareQuote=tl,a.MessageSquareReply=hl,a.MessageSquareShare=dl,a.MessageSquareText=cl,a.MessageSquareWarning=Ml,a.MessageSquareX=pl,a.MessagesSquare=nl,a.Mic=el,a.Mic2=$2,a.MicOff=ll,a.MicVocal=$2,a.Microchip=rl,a.Microscope=ol,a.Microwave=vl,a.Milestone=$l,a.Milk=yl,a.MilkOff=ml,a.Minimize=gl,a.Minimize2=sl,a.Minus=ul,a.MinusCircle=e1,a.MinusSquare=C0,a.Monitor=zl,a.MonitorCheck=Cl,a.MonitorCog=Hl,a.MonitorDot=Al,a.MonitorDown=wl,a.MonitorOff=Vl,a.MonitorPause=Sl,a.MonitorPlay=Ll,a.MonitorSmartphone=fl,a.MonitorSpeaker=kl,a.MonitorStop=Pl,a.MonitorUp=Bl,a.MonitorX=Dl,a.Moon=bl,a.MoonStar=Fl,a.MoreHorizontal=b1,a.MoreVertical=z1,a.Motorbike=Rl,a.Mountain=ql,a.MountainSnow=Tl,a.Mouse=Wl,a.MouseOff=Ul,a.MousePointer=Il,a.MousePointer2=Ol,a.MousePointerBan=Zl,a.MousePointerClick=Gl,a.MousePointerSquareDashed=e0,a.Move=he,a.Move3D=m2,a.Move3d=m2,a.MoveDiagonal=Xl,a.MoveDiagonal2=El,a.MoveDown=Kl,a.MoveDownLeft=jl,a.MoveDownRight=Nl,a.MoveHorizontal=Ql,a.MoveLeft=Jl,a.MoveRight=Yl,a.MoveUp=ae,a.MoveUpLeft=xl,a.MoveUpRight=_l,a.MoveVertical=te,a.Music=pe,a.Music2=de,a.Music3=ce,a.Music4=Me,a.Navigation=ee,a.Navigation2=ne,a.Navigation2Off=ie,a.NavigationOff=le,a.Network=re,a.Newspaper=oe,a.Nfc=ve,a.NonBinary=$e,a.Notebook=ge,a.NotebookPen=me,a.NotebookTabs=ye,a.NotebookText=se,a.NotepadText=Ce,a.NotepadTextDashed=ue,a.Nut=Ae,a.NutOff=He,a.Octagon=Ve,a.OctagonAlert=y2,a.OctagonMinus=we,a.OctagonPause=s2,a.OctagonX=g2,a.Omega=Se,a.Option=fe,a.Orbit=Le,a.Origami=ke,a.Outdent=o,a.Package=Te,a.Package2=Pe,a.PackageCheck=Be,a.PackageMinus=De,a.PackageOpen=ze,a.PackagePlus=Fe,a.PackageSearch=be,a.PackageX=Re,a.PaintBucket=qe,a.PaintRoller=Ue,a.Paintbrush=Oe,a.Paintbrush2=u2,a.PaintbrushVertical=u2,a.Palette=Ze,a.Palmtree=N0,a.Panda=Ge,a.PanelBottom=Ee,a.PanelBottomClose=Ie,a.PanelBottomDashed=C2,a.PanelBottomInactive=C2,a.PanelBottomOpen=We,a.PanelLeft=V2,a.PanelLeftClose=H2,a.PanelLeftDashed=A2,a.PanelLeftInactive=A2,a.PanelLeftOpen=w2,a.PanelLeftRightDashed=Xe,a.PanelRight=Ke,a.PanelRightClose=je,a.PanelRightDashed=S2,a.PanelRightInactive=S2,a.PanelRightOpen=Ne,a.PanelTop=_e,a.PanelTopBottomDashed=Qe,a.PanelTopClose=Je,a.PanelTopDashed=L2,a.PanelTopInactive=L2,a.PanelTopOpen=Ye,a.PanelsLeftBottom=xe,a.PanelsLeftRight=P1,a.PanelsRightBottom=ar,a.PanelsTopBottom=b2,a.PanelsTopLeft=f2,a.Paperclip=hr,a.Parentheses=tr,a.ParkingCircle=o1,a.ParkingCircleOff=r1,a.ParkingMeter=dr,a.ParkingSquare=A0,a.ParkingSquareOff=w0,a.PartyPopper=cr,a.Pause=Mr,a.PauseCircle=v1,a.PauseOctagon=s2,a.PawPrint=pr,a.PcCase=nr,a.Pen=P2,a.PenBox=p,a.PenLine=k2,a.PenOff=ir,a.PenSquare=p,a.PenTool=lr,a.Pencil=vr,a.PencilLine=er,a.PencilOff=rr,a.PencilRuler=or,a.Pentagon=$r,a.Percent=mr,a.PercentCircle=$1,a.PercentDiamond=D1,a.PercentSquare=V0,a.PersonStanding=yr,a.PhilippinePeso=sr,a.Phone=Vr,a.PhoneCall=gr,a.PhoneForwarded=ur,a.PhoneIncoming=Cr,a.PhoneMissed=Hr,a.PhoneOff=Ar,a.PhoneOutgoing=wr,a.Pi=Sr,a.PiSquare=S0,a.Piano=Lr,a.Pickaxe=fr,a.PictureInPicture=Pr,a.PictureInPicture2=kr,a.PieChart=X,a.PiggyBank=Br,a.Pilcrow=zr,a.PilcrowLeft=Dr,a.PilcrowRight=Fr,a.PilcrowSquare=L0,a.Pill=Rr,a.PillBottle=br,a.Pin=qr,a.PinOff=Tr,a.Pipette=Ur,a.Pizza=Or,a.Plane=Ir,a.PlaneLanding=Zr,a.PlaneTakeoff=Gr,a.Play=Wr,a.PlayCircle=m1,a.PlaySquare=f0,a.Plug=Xr,a.Plug2=Er,a.PlugZap=B2,a.PlugZap2=B2,a.Plus=jr,a.PlusCircle=y1,a.PlusSquare=k0,a.Pocket=Kr,a.PocketKnife=Nr,a.Podcast=Yr,a.Pointer=Jr,a.PointerOff=Qr,a.Popcorn=_r,a.Popsicle=xr,a.PoundSterling=ao,a.Power=ho,a.PowerCircle=s1,a.PowerOff=to,a.PowerSquare=P0,a.Presentation=co,a.Printer=po,a.PrinterCheck=Mo,a.Projector=io,a.Proportions=no,a.Puzzle=lo,a.Pyramid=eo,a.QrCode=ro,a.Quote=oo,a.Rabbit=vo,a.Radar=$o,a.Radiation=mo,a.Radical=yo,a.Radio=uo,a.RadioReceiver=so,a.RadioTower=go,a.Radius=Ho,a.RailSymbol=Co,a.Rainbow=Ao,a.Rat=wo,a.Ratio=Vo,a.Receipt=zo,a.ReceiptCent=So,a.ReceiptEuro=Lo,a.ReceiptIndianRupee=fo,a.ReceiptJapaneseYen=ko,a.ReceiptPoundSterling=Po,a.ReceiptRussianRuble=Bo,a.ReceiptSwissFranc=Do,a.ReceiptText=Fo,a.ReceiptTurkishLira=bo,a.RectangleCircle=Ro,a.RectangleEllipsis=D2,a.RectangleGoggles=To,a.RectangleHorizontal=qo,a.RectangleVertical=Oo,a.Recycle=Uo,a.Redo=Io,a.Redo2=Zo,a.RedoDot=Go,a.RefreshCcw=Wo,a.RefreshCcwDot=Eo,a.RefreshCw=jo,a.RefreshCwOff=Xo,a.Refrigerator=No,a.Regex=Ko,a.RemoveFormatting=Qo,a.Repeat=_o,a.Repeat1=Yo,a.Repeat2=Jo,a.Replace=av,a.ReplaceAll=xo,a.Reply=hv,a.ReplyAll=tv,a.Rewind=dv,a.Ribbon=cv,a.Rocket=Mv,a.RockingChair=pv,a.RollerCoaster=iv,a.Rose=nv,a.Rotate3D=F2,a.Rotate3d=F2,a.RotateCcw=ev,a.RotateCcwKey=lv,a.RotateCcwSquare=ov,a.RotateCw=$v,a.RotateCwSquare=rv,a.Route=mv,a.RouteOff=vv,a.Router=yv,a.Rows=z2,a.Rows2=z2,a.Rows3=b2,a.Rows4=sv,a.Rss=gv,a.Ruler=Cv,a.RulerDimensionLine=uv,a.RussianRuble=Hv,a.Sailboat=Av,a.Salad=wv,a.Sandwich=Vv,a.Satellite=Lv,a.SatelliteDish=Sv,a.SaudiRiyal=fv,a.Save=Bv,a.SaveAll=kv,a.SaveOff=Pv,a.Scale=Dv,a.Scale3D=R2,a.Scale3d=R2,a.Scaling=Fv,a.Scan=Gv,a.ScanBarcode=zv,a.ScanEye=bv,a.ScanFace=Rv,a.ScanHeart=Tv,a.ScanLine=qv,a.ScanQrCode=Uv,a.ScanSearch=Ov,a.ScanText=Zv,a.ScatterChart=E,a.School=Iv,a.School2=J0,a.Scissors=Ev,a.ScissorsLineDashed=Wv,a.ScissorsSquare=B0,a.ScissorsSquareDashedBottom=t0,a.ScreenShare=jv,a.ScreenShareOff=Xv,a.Scroll=Kv,a.ScrollText=Nv,a.Search=xv,a.SearchCheck=Qv,a.SearchCode=Jv,a.SearchSlash=Yv,a.SearchX=_v,a.Section=a$,a.Send=h$,a.SendHorizonal=T2,a.SendHorizontal=T2,a.SendToBack=t$,a.SeparatorHorizontal=d$,a.SeparatorVertical=c$,a.Server=n$,a.ServerCog=M$,a.ServerCrash=p$,a.ServerOff=i$,a.Settings=e$,a.Settings2=l$,a.Shapes=o$,a.Share=v$,a.Share2=r$,a.Sheet=$$,a.Shell=m$,a.Shield=S$,a.ShieldAlert=y$,a.ShieldBan=s$,a.ShieldCheck=g$,a.ShieldClose=U2,a.ShieldEllipsis=u$,a.ShieldHalf=C$,a.ShieldMinus=H$,a.ShieldOff=A$,a.ShieldPlus=w$,a.ShieldQuestion=q2,a.ShieldQuestionMark=q2,a.ShieldUser=V$,a.ShieldX=U2,a.Ship=f$,a.ShipWheel=L$,a.Shirt=k$,a.ShoppingBag=P$,a.ShoppingBasket=B$,a.ShoppingCart=D$,a.Shovel=F$,a.ShowerHead=z$,a.Shredder=b$,a.Shrimp=R$,a.Shrink=T$,a.Shrub=q$,a.Shuffle=U$,a.Sidebar=V2,a.SidebarClose=H2,a.SidebarOpen=w2,a.Sigma=O$,a.SigmaSquare=D0,a.Signal=E$,a.SignalHigh=Z$,a.SignalLow=G$,a.SignalMedium=I$,a.SignalZero=W$,a.Signature=X$,a.Signpost=N$,a.SignpostBig=j$,a.Siren=K$,a.SkipBack=Q$,a.SkipForward=J$,a.Skull=Y$,a.Slack=_$,a.Slash=x$,a.SlashSquare=F0,a.Slice=am,a.Sliders=O2,a.SlidersHorizontal=tm,a.SlidersVertical=O2,a.Smartphone=cm,a.SmartphoneCharging=hm,a.SmartphoneNfc=dm,a.Smile=pm,a.SmilePlus=Mm,a.Snail=im,a.Snowflake=nm,a.SoapDispenserDroplet=em,a.Sofa=lm,a.SortAsc=w,a.SortDesc=C,a.Soup=rm,a.Space=om,a.Spade=vm,a.Sparkle=$m,a.Sparkles=Z2,a.Speaker=mm,a.Speech=ym,a.SpellCheck=gm,a.SpellCheck2=sm,a.Spline=Cm,a.SplinePointer=um,a.Split=Hm,a.SplitSquareHorizontal=z0,a.SplitSquareVertical=b0,a.Spool=Am,a.Spotlight=wm,a.SprayCan=Vm,a.Sprout=Sm,a.Square=Tm,a.SquareActivity=G2,a.SquareArrowDown=E2,a.SquareArrowDownLeft=I2,a.SquareArrowDownRight=W2,a.SquareArrowLeft=X2,a.SquareArrowOutDownLeft=j2,a.SquareArrowOutDownRight=N2,a.SquareArrowOutUpLeft=K2,a.SquareArrowOutUpRight=Q2,a.SquareArrowRight=J2,a.SquareArrowUp=x2,a.SquareArrowUpLeft=Y2,a.SquareArrowUpRight=_2,a.SquareAsterisk=a0,a.SquareBottomDashedScissors=t0,a.SquareChartGantt=$,a.SquareCheck=d0,a.SquareCheckBig=h0,a.SquareChevronDown=c0,a.SquareChevronLeft=M0,a.SquareChevronRight=p0,a.SquareChevronUp=i0,a.SquareCode=n0,a.SquareDashed=r0,a.SquareDashedBottom=fm,a.SquareDashedBottomCode=Lm,a.SquareDashedKanban=l0,a.SquareDashedMousePointer=e0,a.SquareDashedTopSolid=km,a.SquareDivide=o0,a.SquareDot=v0,a.SquareEqual=$0,a.SquareFunction=m0,a.SquareGanttChart=$,a.SquareKanban=y0,a.SquareLibrary=s0,a.SquareM=g0,a.SquareMenu=u0,a.SquareMinus=C0,a.SquareMousePointer=H0,a.SquareParking=A0,a.SquareParkingOff=w0,a.SquarePause=Pm,a.SquarePen=p,a.SquarePercent=V0,a.SquarePi=S0,a.SquarePilcrow=L0,a.SquarePlay=f0,a.SquarePlus=k0,a.SquarePower=P0,a.SquareRadical=Bm,a.SquareRoundCorner=Dm,a.SquareScissors=B0,a.SquareSigma=D0,a.SquareSlash=F0,a.SquareSplitHorizontal=z0,a.SquareSplitVertical=b0,a.SquareSquare=Fm,a.SquareStack=zm,a.SquareStar=bm,a.SquareStop=Rm,a.SquareTerminal=R0,a.SquareUser=q0,a.SquareUserRound=T0,a.SquareX=U0,a.SquaresExclude=qm,a.SquaresIntersect=Um,a.SquaresSubtract=Om,a.SquaresUnite=Zm,a.Squircle=Gm,a.SquircleDashed=Im,a.Squirrel=Wm,a.Stamp=Em,a.Star=Nm,a.StarHalf=Xm,a.StarOff=jm,a.Stars=Z2,a.StepBack=Km,a.StepForward=Jm,a.Stethoscope=Qm,a.Sticker=Ym,a.StickyNote=_m,a.StopCircle=u1,a.Store=xm,a.StretchHorizontal=ay,a.StretchVertical=ty,a.Strikethrough=hy,a.Subscript=dy,a.Subtitles=F,a.Sun=ny,a.SunDim=cy,a.SunMedium=My,a.SunMoon=py,a.SunSnow=iy,a.Sunrise=ly,a.Sunset=ey,a.Superscript=oy,a.SwatchBook=ry,a.SwissFranc=vy,a.SwitchCamera=$y,a.Sword=my,a.Swords=yy,a.Syringe=sy,a.Table=Sy,a.Table2=gy,a.TableCellsMerge=uy,a.TableCellsSplit=Hy,a.TableColumnsSplit=Cy,a.TableConfig=e,a.TableOfContents=wy,a.TableProperties=Ay,a.TableRowsSplit=Vy,a.Tablet=fy,a.TabletSmartphone=Ly,a.Tablets=ky,a.Tag=Py,a.Tags=Dy,a.Tally1=By,a.Tally2=Fy,a.Tally3=zy,a.Tally4=by,a.Tally5=Ry,a.Tangent=Ty,a.Target=qy,a.Telescope=Uy,a.Tent=Zy,a.TentTree=Oy,a.Terminal=Gy,a.TerminalSquare=R0,a.TestTube=Iy,a.TestTube2=O0,a.TestTubeDiagonal=O0,a.TestTubes=Wy,a.Text=m,a.TextAlignCenter=Z0,a.TextAlignEnd=G0,a.TextAlignJustify=I0,a.TextAlignStart=m,a.TextCursor=Xy,a.TextCursorInput=Ey,a.TextInitial=W0,a.TextQuote=jy,a.TextSearch=Ny,a.TextSelect=E0,a.TextSelection=E0,a.TextWrap=X0,a.Theater=Ky,a.Thermometer=Yy,a.ThermometerSnowflake=Qy,a.ThermometerSun=Jy,a.ThumbsDown=_y,a.ThumbsUp=xy,a.Ticket=Ms,a.TicketCheck=as,a.TicketMinus=ts,a.TicketPercent=hs,a.TicketPlus=ds,a.TicketSlash=ps,a.TicketX=cs,a.Tickets=ns,a.TicketsPlane=is,a.Timer=rs,a.TimerOff=ls,a.TimerReset=es,a.ToggleLeft=os,a.ToggleRight=vs,a.Toilet=$s,a.ToolCase=ys,a.Tornado=ms,a.Torus=ss,a.Touchpad=us,a.TouchpadOff=gs,a.TowerControl=Hs,a.ToyBrick=Cs,a.Tractor=As,a.TrafficCone=ws,a.Train=j0,a.TrainFront=Ss,a.TrainFrontTunnel=Vs,a.TrainTrack=Ls,a.TramFront=j0,a.Transgender=fs,a.Trash=Ps,a.Trash2=ks,a.TreeDeciduous=Bs,a.TreePalm=N0,a.TreePine=Ds,a.Trees=Fs,a.Trello=bs,a.TrendingDown=zs,a.TrendingUp=Ts,a.TrendingUpDown=Rs,a.Triangle=Os,a.TriangleAlert=K0,a.TriangleDashed=qs,a.TriangleRight=Us,a.Trophy=Zs,a.Truck=Is,a.TruckElectric=Gs,a.TurkishLira=Es,a.Turntable=Ws,a.Turtle=Xs,a.Tv=Ns,a.Tv2=Q0,a.TvMinimal=Q0,a.TvMinimalPlay=js,a.Twitch=Ks,a.Twitter=Qs,a.Type=Ys,a.TypeOutline=Js,a.Umbrella=xs,a.UmbrellaOff=_s,a.Underline=ag,a.Undo=dg,a.Undo2=tg,a.UndoDot=hg,a.UnfoldHorizontal=cg,a.UnfoldVertical=Mg,a.Ungroup=pg,a.University=J0,a.Unlink=lg,a.Unlink2=ig,a.Unlock=e2,a.UnlockKeyhole=l2,a.Unplug=ng,a.Upload=eg,a.UploadCloud=L1,a.Usb=rg,a.User=wg,a.User2=ha,a.UserCheck=og,a.UserCheck2=_0,a.UserCircle=H1,a.UserCircle2=C1,a.UserCog=vg,a.UserCog2=Y0,a.UserLock=$g,a.UserMinus=mg,a.UserMinus2=x0,a.UserPen=yg,a.UserPlus=sg,a.UserPlus2=aa,a.UserRound=ha,a.UserRoundCheck=_0,a.UserRoundCog=Y0,a.UserRoundMinus=x0,a.UserRoundPen=gg,a.UserRoundPlus=aa,a.UserRoundSearch=ug,a.UserRoundX=ta,a.UserSearch=Cg,a.UserSquare=q0,a.UserSquare2=T0,a.UserStar=Hg,a.UserX=Ag,a.UserX2=ta,a.Users=Vg,a.Users2=da,a.UsersRound=da,a.Utensils=Ma,a.UtensilsCrossed=ca,a.UtilityPole=Sg,a.Variable=Lg,a.Vault=kg,a.VectorSquare=fg,a.Vegan=Pg,a.VenetianMask=Bg,a.Venus=Fg,a.VenusAndMars=Dg,a.Verified=L,a.Vibrate=bg,a.VibrateOff=zg,a.Video=Tg,a.VideoOff=Rg,a.Videotape=qg,a.View=Ug,a.Voicemail=Og,a.Volleyball=Zg,a.Volume=Xg,a.Volume1=Gg,a.Volume2=Ig,a.VolumeOff=Wg,a.VolumeX=Eg,a.Vote=jg,a.Wallet=Kg,a.Wallet2=pa,a.WalletCards=Ng,a.WalletMinimal=pa,a.Wallpaper=Qg,a.Wand=Jg,a.Wand2=ia,a.WandSparkles=ia,a.Warehouse=Yg,a.WashingMachine=_g,a.Watch=xg,a.Waves=tu,a.WavesLadder=au,a.Waypoints=hu,a.Webcam=du,a.Webhook=pu,a.WebhookOff=Mu,a.Weight=cu,a.Wheat=nu,a.WheatOff=iu,a.WholeWord=lu,a.Wifi=su,a.WifiCog=eu,a.WifiHigh=ru,a.WifiLow=ou,a.WifiOff=mu,a.WifiPen=vu,a.WifiSync=$u,a.WifiZero=yu,a.Wind=uu,a.WindArrowDown=gu,a.Wine=Hu,a.WineOff=Cu,a.Workflow=Au,a.Worm=wu,a.WrapText=X0,a.Wrench=Vu,a.X=Su,a.XCircle=A1,a.XOctagon=g2,a.XSquare=U0,a.Youtube=Lu,a.Zap=ku,a.ZapOff=fu,a.ZoomIn=Pu,a.ZoomOut=Bu,a.createElement=ea,a.createIcons=Ou,a.icons=Du}));
-//# sourceMappingURL=lucide.min.js.map
diff --git a/internal/server/swagger_gen.go b/internal/server/swagger_gen.go
deleted file mode 100644
index 03eae74..0000000
--- a/internal/server/swagger_gen.go
+++ /dev/null
@@ -1,3 +0,0 @@
-//go:generate swag init -g ../../cmd/proxy/main.go -d .,../handler -o ../../docs/swagger --outputTypes go,json --parseInternal
-
-package server
diff --git a/internal/server/templates.go b/internal/server/templates.go
index 217da41..c49c480 100644
--- a/internal/server/templates.go
+++ b/internal/server/templates.go
@@ -5,70 +5,58 @@ import (
"html/template"
"net/http"
"path/filepath"
- "sync"
)
//go:embed templates/**/*.html
var templatesFS embed.FS
-// Templates holds lazily-parsed templates for each page.
+// Templates holds parsed templates for each page.
type Templates struct {
- once sync.Once
pages map[string]*template.Template
- err error
}
-// load parses all templates from the embedded filesystem on first call.
-func (t *Templates) load() error {
- t.once.Do(func() {
- pages := make(map[string]*template.Template)
+// NewTemplates loads and parses all templates from the embedded filesystem.
+func NewTemplates() (*Templates, error) {
+ pages := make(map[string]*template.Template)
- funcMap := template.FuncMap{
- "add": func(a, b int) int { return a + b },
- "sub": func(a, b int) int { return a - b },
- "supportedEcosystems": supportedEcosystems,
- "ecosystemBadgeClass": ecosystemBadgeClasses,
- "ecosystemBadgeLabel": ecosystemBadgeLabel,
+ // Define custom template functions
+ funcMap := template.FuncMap{
+ "add": func(a, b int) int { return a + b },
+ "sub": func(a, b int) int { return a - b },
+ }
+
+ // Get all page files
+ pageFiles, err := templatesFS.ReadDir("templates/pages")
+ if err != nil {
+ return nil, err
+ }
+
+ for _, pageFile := range pageFiles {
+ if pageFile.IsDir() {
+ continue
}
- pageFiles, err := templatesFS.ReadDir("templates/pages")
+ pageName := pageFile.Name()
+ pageName = pageName[:len(pageName)-len(filepath.Ext(pageName))]
+
+ // Parse all layout files + components + this page with custom functions
+ tmpl, err := template.New("").Funcs(funcMap).ParseFS(templatesFS,
+ "templates/layout/*.html",
+ "templates/components/*.html",
+ "templates/pages/"+pageFile.Name(),
+ )
if err != nil {
- t.err = err
- return
+ return nil, err
}
- for _, pageFile := range pageFiles {
- if pageFile.IsDir() {
- continue
- }
+ pages[pageName] = tmpl
+ }
- pageName := pageFile.Name()
- pageName = pageName[:len(pageName)-len(filepath.Ext(pageName))]
-
- tmpl, err := template.New("").Funcs(funcMap).ParseFS(templatesFS,
- "templates/layout/*.html",
- "templates/components/*.html",
- "templates/pages/"+pageFile.Name(),
- )
- if err != nil {
- t.err = err
- return
- }
-
- pages[pageName] = tmpl
- }
-
- t.pages = pages
- })
- return t.err
+ return &Templates{pages: pages}, nil
}
// Render renders a page template with the given data.
func (t *Templates) Render(w http.ResponseWriter, pageName string, data any) error {
- if err := t.load(); err != nil {
- return err
- }
-
w.Header().Set("Content-Type", "text/html; charset=utf-8")
tmpl, ok := t.pages[pageName]
diff --git a/internal/server/templates/components/ecosystem_badge.html b/internal/server/templates/components/ecosystem_badge.html
index 4c7e882..ba1c286 100644
--- a/internal/server/templates/components/ecosystem_badge.html
+++ b/internal/server/templates/components/ecosystem_badge.html
@@ -1,3 +1,3 @@
{{define "ecosystem_badge"}}
-{{ecosystemBadgeLabel .}}
+{{.}}
{{end}}
diff --git a/internal/server/templates/layout/base.html b/internal/server/templates/layout/base.html
index f69489c..ee2549f 100644
--- a/internal/server/templates/layout/base.html
+++ b/internal/server/templates/layout/base.html
@@ -5,29 +5,19 @@
{{block "title" .}}git-pkgs proxy{{end}}
- {{if .UIBaseURL}}
-
-
-
-
- {{end}}
-
-
+
{{block "head" .}}{{end}}
-
+
{{template "header" .}}
-
+
{{block "content" .}}{{end}}
diff --git a/internal/server/templates/layout/footer.html b/internal/server/templates/layout/footer.html
index 33daddf..2c963f0 100644
--- a/internal/server/templates/layout/footer.html
+++ b/internal/server/templates/layout/footer.html
@@ -5,37 +5,34 @@
About
- git-pkgs proxy is a caching proxy for package registries supporting 17+ ecosystems.
+ git-pkgs proxy is a caching proxy for package registries supporting 16+ ecosystems.
-
-
- github.com/git-pkgs/proxy
-
-
- {{if .BuildInfo.Version}}
-
- proxy {{.BuildInfo.Version}}{{if .BuildInfo.Commit}} ({{.BuildInfo.Commit}}){{end}}
-
- {{end}}
Supported Ecosystems
- {{range supportedEcosystems}}
- {{template "ecosystem_badge" .}}
- {{end}}
+ npm
+ cargo
+ gem
+ go
+ pypi
+ maven
+
{{end}}
diff --git a/internal/server/templates/layout/header.html b/internal/server/templates/layout/header.html
index b3103f1..d893565 100644
--- a/internal/server/templates/layout/header.html
+++ b/internal/server/templates/layout/header.html
@@ -1,21 +1,31 @@
{{define "header"}}
-
@@ -28,7 +38,7 @@
{{template "ecosystem_badge" .Ecosystem}}
-
{{.Name}}
+
{{.Name}}
{{if .LatestVersion}}
@{{.LatestVersion}}
{{end}}
@@ -50,7 +60,7 @@
{{else}}
No packages found matching "{{.Query}}"
-
Return to dashboard
+
Return to dashboard
{{end}}
diff --git a/internal/server/templates/pages/version_show.html b/internal/server/templates/pages/version_show.html
index 81bc067..dede127 100644
--- a/internal/server/templates/pages/version_show.html
+++ b/internal/server/templates/pages/version_show.html
@@ -1,15 +1,15 @@
-{{define "title"}}{{.Version.DisplayPURL}} - git-pkgs proxy{{end}}
+{{define "title"}}{{.Package.Name}}@{{.Version.PURL}} - git-pkgs proxy{{end}}
{{define "content"}}
- Home
+ Home
/
- {{.Package.Name}}
+ {{.Package.Name}}
{{template "ecosystem_badge" .Package.Ecosystem}}
-
{{.Version.DisplayPURL}}
+
{{.Version.PURL}}
{{if .IsOutdated}}
outdated
{{end}}
@@ -22,7 +22,7 @@
{{end}}
{{if .HasCachedArtifact}}
-
diff --git a/internal/server/templates_test.go b/internal/server/templates_test.go
deleted file mode 100644
index a2d3b7d..0000000
--- a/internal/server/templates_test.go
+++ /dev/null
@@ -1,636 +0,0 @@
-package server
-
-import (
- "database/sql"
- "net/http"
- "net/http/httptest"
- "strings"
- "testing"
-
- "github.com/git-pkgs/proxy/internal/database"
-)
-
-func TestTemplatesRenderAllPages(t *testing.T) {
- templates := &Templates{}
-
- tests := []struct {
- page string
- data any
- }{
- {"dashboard", DashboardData{
- Stats: DashboardStats{
- CachedArtifacts: 42,
- TotalSize: "1.5 GB",
- TotalPackages: 10,
- TotalVersions: 25,
- },
- EnrichmentStats: EnrichmentStatsView{
- EnrichedPackages: 5,
- TotalVulnerabilities: 3,
- CriticalVulns: 1,
- HasVulns: true,
- },
- PopularPackages: []PackageInfo{
- {Ecosystem: "npm", Name: "lodash", Hits: 100, Size: "500 KB", License: "MIT", LicenseCategory: "permissive"},
- },
- RecentPackages: []PackageInfo{
- {Ecosystem: "cargo", Name: "serde", Version: "1.0.0", Size: "200 KB", CachedAt: "1 hour ago"},
- },
- }},
- {"install", struct {
- Layout
- BaseURL string
- Registries []RegistryConfig
- }{
- BaseURL: "http://localhost:8080",
- Registries: getRegistryConfigs("http://localhost:8080"),
- }},
- {"search", SearchPageData{
- Query: "lodash",
- Ecosystem: "npm",
- Results: []SearchResultItem{{Ecosystem: "npm", Name: "lodash", LatestVersion: "4.17.21", Hits: 50, SizeFormatted: "1 MB"}},
- Count: 1,
- Page: 1,
- PerPage: 50,
- TotalPages: 1,
- }},
- {"search", SearchPageData{
- Query: "nothing",
- Results: []SearchResultItem{},
- Count: 0,
- Page: 1,
- PerPage: 50,
- TotalPages: 0,
- }},
- {"packages_list", PackagesListPageData{
- Ecosystem: "",
- SortBy: defaultSortBy,
- Results: []SearchResultItem{{Ecosystem: "npm", Name: "express", Hits: 200, SizeFormatted: "2 MB"}},
- Count: 1,
- Page: 1,
- PerPage: 50,
- TotalPages: 1,
- }},
- {"package_show", PackageShowData{
- Package: &database.Package{
- PURL: "pkg:npm/lodash",
- Ecosystem: "npm",
- Name: "lodash",
- LatestVersion: sql.NullString{String: "4.17.21", Valid: true},
- License: sql.NullString{String: "MIT", Valid: true},
- },
- Versions: []database.Version{
- {PURL: "pkg:npm/lodash@4.17.21", PackagePURL: "pkg:npm/lodash"},
- },
- Vulnerabilities: []database.Vulnerability{},
- LicenseCategory: "permissive",
- }},
- {"package_show", PackageShowData{
- Package: &database.Package{
- PURL: "pkg:npm/minimal",
- Ecosystem: "npm",
- Name: "minimal",
- },
- Versions: []database.Version{},
- Vulnerabilities: []database.Vulnerability{},
- LicenseCategory: "unknown",
- }},
- {"version_show", VersionShowData{
- Package: &database.Package{
- PURL: "pkg:npm/lodash",
- Ecosystem: "npm",
- Name: "lodash",
- LatestVersion: sql.NullString{String: "4.17.21", Valid: true},
- License: sql.NullString{String: "MIT", Valid: true},
- },
- Version: &database.Version{
- PURL: "pkg:npm/lodash@4.17.20",
- PackagePURL: "pkg:npm/lodash",
- },
- Artifacts: []database.Artifact{
- {
- VersionPURL: "pkg:npm/lodash@4.17.20",
- Filename: "lodash-4.17.20.tgz",
- StoragePath: sql.NullString{String: "npm/lodash/4.17.20/lodash-4.17.20.tgz", Valid: true},
- Size: sql.NullInt64{Int64: 1024, Valid: true},
- HitCount: 42,
- },
- },
- Vulnerabilities: []database.Vulnerability{},
- IsOutdated: true,
- LicenseCategory: "permissive",
- HasCachedArtifact: true,
- }},
- {"browse_source", BrowseSourceData{
- Ecosystem: "npm",
- PackageName: "lodash",
- Version: "4.17.21",
- }},
- {"compare_versions", ComparePageData{
- Ecosystem: "npm",
- PackageName: "lodash",
- FromVersion: "4.17.20",
- ToVersion: "4.17.21",
- }},
- }
-
- for _, tt := range tests {
- t.Run(tt.page, func(t *testing.T) {
- w := httptest.NewRecorder()
- err := templates.Render(w, tt.page, tt.data)
- if err != nil {
- t.Fatalf("Render(%q) failed: %v", tt.page, err)
- }
- if w.Code != http.StatusOK {
- t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
- }
- if w.Header().Get("Content-Type") != "text/html; charset=utf-8" {
- t.Errorf("Content-Type = %q, want text/html", w.Header().Get("Content-Type"))
- }
- body := w.Body.String()
- if body == "" {
- t.Error("rendered page is empty")
- }
- if !strings.Contains(body, "") && !strings.Contains(body, "`,
- ` `,
- ` `,
- }
- for _, s := range want {
- if !strings.Contains(body, s) {
- t.Errorf("rendered body missing %q", s)
- }
- }
-}
-
-func TestFooterUsesBuildInfoWhenPageDefinesVersion(t *testing.T) {
- templates := &Templates{}
- buildInfo := BuildInfo{Version: "proxy-build-1.2.3", Commit: "abc123def"}
- wantFooter := "proxy proxy-build-1.2.3 (abc123def)"
-
- tests := []struct {
- name string
- page string
- data any
- shadow string
- }{
- {
- name: "version show page",
- page: "version_show",
- data: VersionShowData{
- Layout: Layout{BuildInfo: buildInfo},
- Package: &database.Package{
- PURL: "pkg:npm/lodash",
- Ecosystem: "npm",
- Name: "lodash",
- },
- Version: &database.Version{
- PURL: "pkg:npm/lodash@9.9.9",
- PackagePURL: "pkg:npm/lodash",
- },
- },
- shadow: "9.9.9",
- },
- {
- name: "browse source page",
- page: "browse_source",
- data: BrowseSourceData{
- Layout: Layout{BuildInfo: buildInfo},
- Ecosystem: "npm",
- PackageName: "lodash",
- Version: "9.9.9",
- },
- shadow: "9.9.9",
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- w := httptest.NewRecorder()
- if err := templates.Render(w, tt.page, tt.data); err != nil {
- t.Fatalf("Render(%q) failed: %v", tt.page, err)
- }
- body := w.Body.String()
- if !strings.Contains(body, wantFooter) {
- t.Errorf("footer missing build info %q", wantFooter)
- }
- if strings.Contains(body, "proxy "+tt.shadow) {
- t.Errorf("footer used page Version %q instead of BuildInfo", tt.shadow)
- }
- })
- }
-}
-
-func TestRenderOmitsCanonicalWhenUIBaseURLUnset(t *testing.T) {
- templates := &Templates{}
-
- w := httptest.NewRecorder()
- if err := templates.Render(w, "dashboard", DashboardData{}); err != nil {
- t.Fatalf("Render failed: %v", err)
- }
-
- body := w.Body.String()
- if strings.Contains(body, `rel="canonical"`) {
- t.Error("canonical tag should be omitted when UIBaseURL is empty")
- }
- if strings.Contains(body, `property="og:url"`) {
- t.Error("og:url tag should be omitted when UIBaseURL is empty")
- }
-}
-
-func TestInstallPageBannerWhenUIDiffersFromBaseURL(t *testing.T) {
- templates := &Templates{}
-
- data := struct {
- Layout
- BaseURL string
- Registries []RegistryConfig
- }{
- Layout: Layout{
- UIBaseURL: "https://ui.example.com/ui",
- CanonicalPath: "/ui/install",
- },
- BaseURL: "http://pkg-proxy:8080",
- Registries: getRegistryConfigs("http://pkg-proxy:8080"),
- }
-
- w := httptest.NewRecorder()
- if err := templates.Render(w, "install", data); err != nil {
- t.Fatalf("Render failed: %v", err)
- }
-
- body := w.Body.String()
- if !strings.Contains(body, "https://ui.example.com/ui") || !strings.Contains(body, "http://pkg-proxy:8080") {
- t.Error("install banner should mention both UIBaseURL and BaseURL when they differ")
- }
- if !strings.Contains(body, "package managers should be configured") {
- t.Error("install banner copy missing")
- }
-}
-
-func TestInstallPageNoBannerWhenURLsMatch(t *testing.T) {
- templates := &Templates{}
-
- data := struct {
- Layout
- BaseURL string
- Registries []RegistryConfig
- }{
- Layout: Layout{
- UIBaseURL: "http://localhost:8080",
- CanonicalPath: "/ui/install",
- },
- BaseURL: "http://localhost:8080",
- Registries: getRegistryConfigs("http://localhost:8080"),
- }
-
- w := httptest.NewRecorder()
- if err := templates.Render(w, "install", data); err != nil {
- t.Fatalf("Render failed: %v", err)
- }
-
- body := w.Body.String()
- if strings.Contains(body, "package managers should be configured") {
- t.Error("install banner should be hidden when UIBaseURL == BaseURL")
- }
-}
-
-func TestTemplatesLazyLoading(t *testing.T) {
- templates := &Templates{}
-
- if templates.pages != nil {
- t.Fatal("expected pages to be nil before first Render call")
- }
-
- w := httptest.NewRecorder()
- _ = templates.Render(w, "dashboard", DashboardData{})
-
- if templates.pages == nil {
- t.Fatal("expected pages to be populated after first Render call")
- }
-}
-
-func TestTemplatesRenderUnknownPage(t *testing.T) {
- templates := &Templates{}
-
- w := httptest.NewRecorder()
- err := templates.Render(w, "nonexistent_page", nil)
- if err == nil {
- t.Error("expected error for unknown page")
- }
-}
-
-func TestInstallPage(t *testing.T) {
- ts := newTestServer(t)
- defer ts.close()
-
- req := httptest.NewRequest("GET", "/ui/install", nil)
- w := httptest.NewRecorder()
- ts.handler.ServeHTTP(w, req)
-
- if w.Code != http.StatusOK {
- t.Fatalf("expected status 200, got %d", w.Code)
- }
-
- body := w.Body.String()
-
- // Should contain instructions for all registries
- registries := []string{"npm", "Cargo", "RubyGems", "Go Modules", "PyPI", "Maven", "Gradle Build Cache", "NuGet", "Composer", "Conan", "Conda", "CRAN"}
- for _, reg := range registries {
- if !strings.Contains(body, reg) {
- t.Errorf("install page should contain %s instructions", reg)
- }
- }
-}
-
-func TestPackageShowPage(t *testing.T) {
- ts := newTestServer(t)
- defer ts.close()
-
- // Create a package with license and version
- pkg := &database.Package{
- PURL: "pkg:npm/test-show",
- Ecosystem: "npm",
- Name: "test-show",
- LatestVersion: sql.NullString{String: "2.0.0", Valid: true},
- License: sql.NullString{String: "MIT", Valid: true},
- }
- if err := ts.db.UpsertPackage(pkg); err != nil {
- t.Fatalf("failed to upsert package: %v", err)
- }
- ver := &database.Version{PURL: "pkg:npm/test-show@2.0.0", PackagePURL: pkg.PURL}
- if err := ts.db.UpsertVersion(ver); err != nil {
- t.Fatalf("failed to upsert version: %v", err)
- }
-
- req := httptest.NewRequest("GET", "/ui/package/npm/test-show", nil)
- w := httptest.NewRecorder()
- ts.handler.ServeHTTP(w, req)
-
- if w.Code != http.StatusOK {
- t.Fatalf("expected status 200, got %d: %s", w.Code, w.Body.String())
- }
-
- body := w.Body.String()
- if !strings.Contains(body, "test-show") {
- t.Error("page should contain package name")
- }
- if !strings.Contains(body, "2.0.0") {
- t.Error("page should contain version")
- }
- if !strings.Contains(body, "MIT") {
- t.Error("page should contain license")
- }
-}
-
-func TestPackageShowPage_NotFound(t *testing.T) {
- ts := newTestServer(t)
- defer ts.close()
-
- req := httptest.NewRequest("GET", "/ui/package/npm/nonexistent", nil)
- w := httptest.NewRecorder()
- ts.handler.ServeHTTP(w, req)
-
- if w.Code != http.StatusNotFound {
- t.Errorf("expected status 404, got %d", w.Code)
- }
-}
-
-func TestVersionShowPage_NotFound(t *testing.T) {
- ts := newTestServer(t)
- defer ts.close()
-
- req := httptest.NewRequest("GET", "/ui/package/npm/nonexistent/1.0.0", nil)
- w := httptest.NewRecorder()
- ts.handler.ServeHTTP(w, req)
-
- if w.Code != http.StatusNotFound {
- t.Errorf("expected status 404, got %d", w.Code)
- }
-}
-
-func TestSearchPage_EmptyQuery(t *testing.T) {
- ts := newTestServer(t)
- defer ts.close()
-
- req := httptest.NewRequest("GET", "/ui/search", nil)
- w := httptest.NewRecorder()
- ts.handler.ServeHTTP(w, req)
-
- // Empty query should redirect to homepage
- if w.Code != http.StatusSeeOther {
- t.Errorf("expected redirect (303), got %d", w.Code)
- }
-}
-
-func TestSearchPage_WithQuery(t *testing.T) {
- ts := newTestServer(t)
- defer ts.close()
-
- req := httptest.NewRequest("GET", "/ui/search?q=test", nil)
- w := httptest.NewRecorder()
- ts.handler.ServeHTTP(w, req)
-
- if w.Code != http.StatusOK {
- t.Fatalf("expected status 200, got %d", w.Code)
- }
-
- body := w.Body.String()
- if !strings.Contains(body, "test") {
- t.Error("search page should contain the query")
- }
-}
-
-func TestSearchPage_Pagination(t *testing.T) {
- ts := newTestServer(t)
- defer ts.close()
-
- // Page 0 or negative should default to page 1
- req := httptest.NewRequest("GET", "/ui/search?q=test&page=0", nil)
- w := httptest.NewRecorder()
- ts.handler.ServeHTTP(w, req)
-
- if w.Code != http.StatusOK {
- t.Errorf("expected status 200, got %d", w.Code)
- }
-
- // Non-numeric page should default to page 1
- req = httptest.NewRequest("GET", "/ui/search?q=test&page=abc", nil)
- w = httptest.NewRecorder()
- ts.handler.ServeHTTP(w, req)
-
- if w.Code != http.StatusOK {
- t.Errorf("expected status 200, got %d", w.Code)
- }
-}
-
-func TestSearchPage_EcosystemFilter(t *testing.T) {
- ts := newTestServer(t)
- defer ts.close()
-
- req := httptest.NewRequest("GET", "/ui/search?q=test&ecosystem=npm", nil)
- w := httptest.NewRecorder()
- ts.handler.ServeHTTP(w, req)
-
- if w.Code != http.StatusOK {
- t.Errorf("expected status 200, got %d", w.Code)
- }
-}
-
-func TestEcosystemBadgeLabel(t *testing.T) {
- tests := []struct {
- ecosystem string
- want string
- }{
- {"oci", "container"},
- {"deb", "debian"},
- {"npm", "npm"},
- {"cargo", "cargo"},
- }
-
- for _, tt := range tests {
- got := ecosystemBadgeLabel(tt.ecosystem)
- if got != tt.want {
- t.Errorf("ecosystemBadgeLabel(%q) = %q, want %q", tt.ecosystem, got, tt.want)
- }
- }
-}
-
-func TestOCIRegistryInstructionsDockerPull(t *testing.T) {
- registries := getRegistryConfigs("http://package-proxy:8080")
- for _, registry := range registries {
- if registry.ID != "oci" {
- continue
- }
- want := "docker pull package-proxy:8080/library/nginx:latest"
- if !strings.Contains(string(registry.Instructions), want) {
- t.Errorf("OCI instructions = %q, want substring %q", registry.Instructions, want)
- }
- return
- }
- t.Fatal("OCI registry instructions not found")
-}
-
-func TestSwiftRegistryInstructionsAllowLocalHTTP(t *testing.T) {
- registries := getRegistryConfigs("http://localhost:8080")
- for _, registry := range registries {
- if registry.ID != "swift" {
- continue
- }
- if !strings.Contains(string(registry.Instructions), "--allow-insecure-http") {
- t.Error("Swift HTTP instructions do not allow the insecure local registry")
- }
- return
- }
- t.Fatal("Swift registry instructions not found")
-}
-
-func TestEcosystemBadgeClasses(t *testing.T) {
- // Every supported ecosystem should return a non-empty class string
- ecosystems := supportedEcosystems()
- for _, eco := range ecosystems {
- classes := ecosystemBadgeClasses(eco)
- if classes == "" {
- t.Errorf("ecosystemBadgeClasses(%q) returned empty string", eco)
- }
- if !strings.Contains(classes, "inline-flex") {
- t.Errorf("ecosystemBadgeClasses(%q) missing base classes", eco)
- }
- }
-
- // Unknown ecosystem should still return classes
- classes := ecosystemBadgeClasses("unknown")
- if !strings.Contains(classes, "gray") {
- t.Error("unknown ecosystem should use gray classes")
- }
-}
-
-func TestFormatSize(t *testing.T) {
- tests := []struct {
- bytes int64
- want string
- }{
- {0, "0 B"},
- {100, "100 B"},
- {1024, "1.0 KB"},
- {1536, "1.5 KB"},
- {1048576, "1.0 MB"},
- {1073741824, "1.0 GB"},
- }
-
- for _, tt := range tests {
- got := formatSize(tt.bytes)
- if got != tt.want {
- t.Errorf("formatSize(%d) = %q, want %q", tt.bytes, got, tt.want)
- }
- }
-}
-
-func TestCategorizeLicense(t *testing.T) {
- tests := []struct {
- license sql.NullString
- want string
- }{
- {sql.NullString{Valid: false}, "unknown"},
- {sql.NullString{String: "", Valid: true}, "unknown"},
- {sql.NullString{String: "MIT", Valid: true}, "permissive"},
- {sql.NullString{String: "GPL-3.0", Valid: true}, "copyleft"},
- }
-
- for _, tt := range tests {
- got := categorizeLicense(tt.license)
- if got != tt.want {
- t.Errorf("categorizeLicense(%v) = %q, want %q", tt.license, got, tt.want)
- }
- }
-}
-
-func BenchmarkTemplatesParse(b *testing.B) {
- for b.Loop() {
- t := &Templates{}
- if err := t.load(); err != nil {
- b.Fatal(err)
- }
- }
-}
-
-func BenchmarkServerCreate(b *testing.B) {
- for b.Loop() {
- _ = &Server{
- templates: &Templates{},
- }
- }
-}
-
-func BenchmarkFirstRender(b *testing.B) {
- for b.Loop() {
- t := &Templates{}
- w := httptest.NewRecorder()
- if err := t.Render(w, "dashboard", DashboardData{}); err != nil {
- b.Fatal(err)
- }
- }
-}
diff --git a/internal/server/transport_test.go b/internal/server/transport_test.go
deleted file mode 100644
index 7464282..0000000
--- a/internal/server/transport_test.go
+++ /dev/null
@@ -1,190 +0,0 @@
-package server
-
-import (
- "crypto/tls"
- "io"
- "net"
- "net/http"
- "net/http/httptest"
- "strings"
- "sync"
- "testing"
- "time"
-
- "github.com/git-pkgs/proxy/internal/config"
- "github.com/git-pkgs/registries/safehttp"
-)
-
-// tlsUpstream starts a TLS test server that counts accepted connections.
-func tlsUpstream(t *testing.T, handler http.HandlerFunc) (*httptest.Server, func() int) {
- t.Helper()
- var mu sync.Mutex
- accepted := 0
- srv := httptest.NewUnstartedServer(handler)
- srv.Config.ConnState = func(_ net.Conn, state http.ConnState) {
- if state == http.StateNew {
- mu.Lock()
- accepted++
- mu.Unlock()
- }
- }
- srv.StartTLS()
- t.Cleanup(srv.Close)
- return srv, func() int {
- mu.Lock()
- defer mu.Unlock()
- return accepted
- }
-}
-
-// trustUpstream makes transport trust srv's certificate and pins HTTP/1.1 so
-// every in-flight request needs its own connection.
-func trustUpstream(t *testing.T, transport *http.Transport, srv *httptest.Server) {
- t.Helper()
- transport.TLSClientConfig = &tls.Config{
- RootCAs: srv.Client().Transport.(*http.Transport).TLSClientConfig.RootCAs,
- NextProtos: []string{"http/1.1"},
- MinVersion: tls.VersionTLS12,
- }
- transport.ForceAttemptHTTP2 = false
- t.Cleanup(transport.CloseIdleConnections)
-}
-
-// burst issues n concurrent GETs and drains every body. The transport hands a
-// connection back to the idle pool before the body's final Read returns, so
-// the pool is settled when burst returns.
-func burst(t *testing.T, client *http.Client, url string, n int) {
- t.Helper()
- var wg sync.WaitGroup
- errs := make(chan error, n)
- for range n {
- wg.Add(1)
- go func() {
- defer wg.Done()
- resp, err := client.Get(url)
- if err != nil {
- errs <- err
- return
- }
- _, _ = io.Copy(io.Discard, resp.Body)
- _ = resp.Body.Close()
- }()
- }
- wg.Wait()
- close(errs)
- for err := range errs {
- t.Errorf("burst request: %v", err)
- }
-}
-
-// TestUpstreamClientReusesConnectionsAcrossBursts measures how many
-// connections a second burst of concurrent requests reuses. With Go's default
-// of two idle connections per host most of them are re-dialled; with the
-// tuned transport the second burst reuses all of them.
-func TestUpstreamClientReusesConnectionsAcrossBursts(t *testing.T) {
- const burstSize = 8
-
- // holdBurst returns a handler that answers a request only once burstSize
- // of them are waiting at the same time. With HTTP/1.1 pinned that puts
- // every burst on burstSize distinct connections, whatever the scheduling.
- holdBurst := func() http.HandlerFunc {
- var mu sync.Mutex
- waiting := 0
- release := make(chan struct{})
- return func(w http.ResponseWriter, r *http.Request) {
- mu.Lock()
- gate := release
- waiting++
- if waiting == burstSize {
- close(gate)
- waiting = 0
- release = make(chan struct{})
- }
- mu.Unlock()
- select {
- case <-gate:
- case <-r.Context().Done():
- }
- _, _ = w.Write([]byte("ok"))
- }
- }
-
- tests := []struct {
- name string
- client *http.Client
- // Bounds on how many connections the second burst has to dial.
- minNew, maxNew int
- }{
- {
- name: "go default keeps two idle connections",
- client: safehttp.New(nil, safehttp.Options{AllowLoopback: true}),
- minNew: burstSize - 2,
- maxNew: burstSize,
- },
- {
- name: "tuned transport reuses the whole burst",
- client: newUpstreamClient(config.UpstreamConfig{AllowLoopback: true}),
- minNew: 0,
- maxNew: 0,
- },
- }
- for _, tc := range tests {
- t.Run(tc.name, func(t *testing.T) {
- srv, accepted := tlsUpstream(t, holdBurst())
- transport := tc.client.Transport.(*http.Transport)
- trustUpstream(t, transport, srv)
-
- burst(t, tc.client, srv.URL, burstSize)
- afterFirst := accepted()
- if afterFirst < burstSize {
- t.Fatalf("first burst opened %d connections, want at least %d", afterFirst, burstSize)
- }
-
- burst(t, tc.client, srv.URL, burstSize)
- newInSecond := accepted() - afterFirst
- t.Logf("second burst: %d new connections, %d reused", newInSecond, burstSize-newInSecond)
-
- if newInSecond < tc.minNew || newInSecond > tc.maxNew {
- t.Errorf("second burst opened %d new connections, want between %d and %d", newInSecond, tc.minNew, tc.maxNew)
- }
- })
- }
-}
-
-// TestUpstreamClientBoundsStallBeforeHeaders pins the production transport
-// values, then lowers the header timeout so it can show within milliseconds
-// that this is what cuts off an upstream which accepts a request but never
-// sends headers.
-func TestUpstreamClientBoundsStallBeforeHeaders(t *testing.T) {
- client := newUpstreamClient(config.UpstreamConfig{AllowLoopback: true})
- transport := client.Transport.(*http.Transport)
- if transport.MaxIdleConnsPerHost != upstreamMaxIdleConnsPerHost {
- t.Fatalf("MaxIdleConnsPerHost = %d, want %d", transport.MaxIdleConnsPerHost, upstreamMaxIdleConnsPerHost)
- }
- if transport.ResponseHeaderTimeout != upstreamResponseHeaderTimeout {
- t.Fatalf("ResponseHeaderTimeout = %v, want %v", transport.ResponseHeaderTimeout, upstreamResponseHeaderTimeout)
- }
-
- stall := make(chan struct{})
- srv, _ := tlsUpstream(t, func(_ http.ResponseWriter, r *http.Request) {
- select {
- case <-stall:
- case <-r.Context().Done():
- }
- })
- t.Cleanup(func() { close(stall) })
- trustUpstream(t, transport, srv)
-
- // Far below the client's overall timeout, so the header timeout ends the
- // request; the error text tells the two timeouts apart.
- transport.ResponseHeaderTimeout = 200 * time.Millisecond
-
- resp, err := client.Get(srv.URL)
- if err == nil {
- _ = resp.Body.Close()
- t.Fatal("request to a stalled upstream succeeded, want a timeout")
- }
- if !strings.Contains(err.Error(), "timeout awaiting response headers") {
- t.Fatalf("error = %v, want a response-header timeout", err)
- }
-}
diff --git a/internal/storage/blob.go b/internal/storage/blob.go
index cdc7aa0..22643fa 100644
--- a/internal/storage/blob.go
+++ b/internal/storage/blob.go
@@ -6,35 +6,22 @@ import (
"encoding/hex"
"fmt"
"io"
- "net/http"
"os"
"path/filepath"
"runtime"
"strings"
- "time"
"gocloud.dev/blob"
- _ "gocloud.dev/blob/azureblob"
_ "gocloud.dev/blob/fileblob"
_ "gocloud.dev/blob/s3blob"
"gocloud.dev/gcerrors"
)
-const osWindows = "windows"
-
-// attrsExt is fileblob's sidecar suffix, kept only to clear sidecars an
-// earlier version wrote.
-const attrsExt = ".attrs"
-
// Blob implements Storage using gocloud.dev/blob.
// Supports local filesystem (file://) and S3 (s3://) URLs.
type Blob struct {
bucket *blob.Bucket
url string
-
- // fileRoot is the directory backing a file:// bucket, empty for cloud
- // backends. Used only to clear sidecars an earlier version wrote.
- fileRoot string
}
// OpenBucket opens a blob bucket from a URL.
@@ -43,29 +30,18 @@ type Blob struct {
// - file:///path/to/dir - Local filesystem storage
// - s3://bucket-name - Amazon S3 (uses AWS_* environment variables)
// - s3://bucket-name?region=us-east-1&endpoint=http://localhost:9000 - S3-compatible (MinIO, etc.)
-// - gs://bucket-name - Google Cloud Storage (uses Application Default Credentials;
-// supports Workload Identity on GKE/GCE without any extra configuration)
-// - azblob://container-name - Azure Blob Storage
//
// For local filesystem, the directory is created if it doesn't exist.
-//
-//nolint:ireturn // The URL scheme selects the storage implementation.
-func OpenBucket(ctx context.Context, urlStr string) (Storage, error) {
- if strings.HasPrefix(urlStr, "gs://") {
- return OpenGCS(ctx, urlStr)
- }
-
- var fileRoot string
-
+func OpenBucket(ctx context.Context, urlStr string) (*Blob, error) {
// Handle file:// URLs specially to create the directory
if strings.HasPrefix(urlStr, "file://") {
path := strings.TrimPrefix(urlStr, "file://")
// Handle file:/// (three slashes) for absolute paths
- if strings.HasPrefix(path, "/") && runtime.GOOS != osWindows {
+ if strings.HasPrefix(path, "/") && runtime.GOOS != "windows" {
// Unix: file:///path -> /path
// path is already correct
- } else if strings.HasPrefix(path, "/") && runtime.GOOS == osWindows {
+ } else if strings.HasPrefix(path, "/") && runtime.GOOS == "windows" {
// Windows: file:///C:/path -> C:/path
path = strings.TrimPrefix(path, "/")
}
@@ -74,7 +50,7 @@ func OpenBucket(ctx context.Context, urlStr string) (Storage, error) {
nativePath := filepath.FromSlash(path)
// Ensure directory exists
- if err := os.MkdirAll(nativePath, dirPermissions); err != nil {
+ if err := os.MkdirAll(nativePath, 0755); err != nil {
return nil, fmt.Errorf("creating directory: %w", err)
}
@@ -84,29 +60,14 @@ func OpenBucket(ctx context.Context, urlStr string) (Storage, error) {
return nil, fmt.Errorf("resolving path: %w", err)
}
- fileRoot = absPath
-
// Convert back to URL format with forward slashes
urlPath := filepath.ToSlash(absPath)
- if runtime.GOOS == osWindows {
+ if runtime.GOOS == "windows" {
// Windows needs file:///C:/path format
urlStr = "file:///" + urlPath
} else {
urlStr = "file://" + urlPath
}
-
- // Create temp files next to the final path instead of in os.TempDir.
- // This avoids "invalid cross-device link" errors from os.Rename when
- // the bucket directory and os.TempDir are on different filesystems
- // (e.g. Docker volume mounts).
- //
- // Do not write fileblob's ".attrs" sidecar. It is rewritten with
- // os.Create, truncating in place outside the atomic rename that
- // protects the blob, so a read overlapping a write can decode a
- // partial file; a missing one defaults cleanly, a truncated one does
- // not. Nothing in the proxy needs it: Store sets no ContentType, and
- // Size reads os.Stat via Attributes.
- urlStr += "?no_tmp_dir=true&metadata=skip"
}
bucket, err := blob.OpenBucket(ctx, urlStr)
@@ -114,87 +75,10 @@ func OpenBucket(ctx context.Context, urlStr string) (Storage, error) {
return nil, fmt.Errorf("opening bucket: %w", err)
}
- return &Blob{bucket: bucket, url: urlStr, fileRoot: fileRoot}, nil
-}
-
-// legacySidecarPath gives the ".attrs" path an earlier version wrote for key,
-// or "" when that path would not be a file inside fileRoot.
-//
-// The key is escaped the way fileblob escapes it on the way to disk, so the
-// sidecar is looked for where fileblob wrote it. filepath.Localize then
-// validates the escaped form: it rejects an empty, absolute or ".." path, and
-// "." would name fileRoot itself. What it declines are keys the proxy never
-// produces.
-func (b *Blob) legacySidecarPath(key string) string {
- if b.fileRoot == "" {
- return ""
- }
- rel, err := filepath.Localize(escapeKey(key))
- if err != nil || rel == "." {
- return ""
- }
- return filepath.Join(b.fileRoot, rel) + attrsExt
-}
-
-// escapeKey mirrors fileblob's unexported escapeKey, which hex-escapes a rune
-// as "__0x__". Slashes stay as "/" for filepath.Localize to convert.
-func escapeKey(key string) string {
- runes := []rune(key)
- var out strings.Builder
- for i, r := range runes {
- if escapeRune(runes, i) {
- fmt.Fprintf(&out, "__%#x__", r)
- } else {
- out.WriteRune(r)
- }
- }
- return out.String()
-}
-
-// escapeRune is fileblob's rule for which runes of a key to escape: control
-// characters, a raw path separator, a slash that would form "../", "//" or
-// end the key, and on Windows the characters its filesystem reserves.
-func escapeRune(r []rune, i int) bool {
- c := r[i]
- switch {
- case c < ' ':
- return true
- case os.PathSeparator != '/' && c == os.PathSeparator:
- return true
- case i > 1 && c == '/' && r[i-1] == '.' && r[i-2] == '.':
- return true
- case i > 0 && c == '/' && r[i-1] == '/':
- return true
- case c == '/' && i == len(r)-1:
- return true
- case os.PathSeparator == '\\' && strings.ContainsRune(`<>:"|?*`, c):
- return true
- }
- return false
-}
-
-// clearLegacySidecar removes the ".attrs" file an earlier version wrote for
-// key. Nothing rewrites one now, so a sidecar left partial by an interrupted
-// write would fail every read of that key for good. Removing is atomic where
-// the rewrite was not, so a concurrent reader gets the whole old file or
-// nothing.
-//
-// Failure is deliberately not fatal. Usually the key never had a sidecar and
-// os.Remove reports not-exist. A real failure leaves exactly the state this
-// change inherited, while failing the write would turn a cleanup miss into a
-// failed request. Windows makes that concrete: Go opens files without
-// FILE_SHARE_DELETE, so a reader holding the sidecar open blocks deletion, and
-// that reader is the very workload this change protects. The next store of the
-// key retries.
-func (b *Blob) clearLegacySidecar(key string) {
- if sidecar := b.legacySidecarPath(key); sidecar != "" {
- _ = os.Remove(sidecar)
- }
+ return &Blob{bucket: bucket, url: urlStr}, nil
}
func (b *Blob) Store(ctx context.Context, path string, r io.Reader) (int64, string, error) {
- b.clearLegacySidecar(path)
-
// Compute hash while writing
h := sha256.New()
tee := io.TeeReader(r, h)
@@ -246,20 +130,6 @@ func (b *Blob) Delete(ctx context.Context, path string) error {
return nil
}
-func (b *Blob) SignedURL(ctx context.Context, path string, expiry time.Duration) (string, error) {
- url, err := b.bucket.SignedURL(ctx, path, &blob.SignedURLOptions{
- Method: http.MethodGet,
- Expiry: expiry,
- })
- if err != nil {
- if gcerrors.Code(err) == gcerrors.Unimplemented {
- return "", ErrSignedURLUnsupported
- }
- return "", fmt.Errorf("signing URL: %w", err)
- }
- return url, nil
-}
-
func (b *Blob) Size(ctx context.Context, path string) (int64, error) {
attrs, err := b.bucket.Attributes(ctx, path)
if err != nil {
@@ -289,35 +159,6 @@ func (b *Blob) UsedSpace(ctx context.Context) (int64, error) {
return total, nil
}
-// ListPrefix returns object metadata for keys under a prefix.
-func (b *Blob) ListPrefix(ctx context.Context, prefix string) ([]ObjectInfo, error) {
- iter := b.bucket.List(&blob.ListOptions{Prefix: prefix})
- objects := make([]ObjectInfo, 0)
-
- for {
- obj, err := iter.Next(ctx)
- if err == io.EOF {
- break
- }
- if err != nil {
- return nil, fmt.Errorf("listing objects: %w", err)
- }
- if obj.IsDir {
- continue
- }
-
- info := ObjectInfo{
- Path: obj.Key,
- Size: obj.Size,
- ModTime: obj.ModTime,
- }
-
- objects = append(objects, info)
- }
-
- return objects, nil
-}
-
func (b *Blob) Close() error {
return b.bucket.Close()
}
diff --git a/internal/storage/blob_test.go b/internal/storage/blob_test.go
index 3a97704..3add0a7 100644
--- a/internal/storage/blob_test.go
+++ b/internal/storage/blob_test.go
@@ -1,22 +1,16 @@
package storage
import (
+ "bytes"
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"io"
- "io/fs"
- "os"
"path/filepath"
"runtime"
"strings"
- "sync"
- "sync/atomic"
"testing"
- "time"
-
- "gocloud.dev/blob"
)
func TestOpenBucket(t *testing.T) {
@@ -192,18 +186,32 @@ func TestBlobUsedSpace(t *testing.T) {
}
func TestBlobLargeFile(t *testing.T) {
- assertLargeFileRoundTrip(t, createTestBlob(t))
-}
-
-func TestBlobSignedURLUnsupported(t *testing.T) {
b := createTestBlob(t)
ctx := context.Background()
- // fileblob has no URL signer configured, so this must surface as
- // ErrSignedURLUnsupported rather than a generic error.
- _, err := b.SignedURL(ctx, "test/file.txt", time.Minute)
- if !errors.Is(err, ErrSignedURLUnsupported) {
- t.Errorf("SignedURL on fileblob = %v, want ErrSignedURLUnsupported", err)
+ // 1MB of data
+ data := bytes.Repeat([]byte("x"), 1024*1024)
+
+ size, hash, err := b.Store(ctx, "large/file.bin", bytes.NewReader(data))
+ if err != nil {
+ t.Fatalf("Store large file failed: %v", err)
+ }
+ if size != int64(len(data)) {
+ t.Errorf("size = %d, want %d", size, len(data))
+ }
+
+ h := sha256.Sum256(data)
+ wantHash := hex.EncodeToString(h[:])
+ if hash != wantHash {
+ t.Errorf("hash mismatch for large file")
+ }
+
+ // Read it back
+ r, _ := b.Open(ctx, "large/file.bin")
+ defer func() { _ = r.Close() }()
+ readBack, _ := io.ReadAll(r)
+ if !bytes.Equal(readBack, data) {
+ t.Error("large file content mismatch")
}
}
@@ -236,44 +244,6 @@ func TestBlobOverwrite(t *testing.T) {
}
}
-func TestOpenBucketSetsNoTmpDir(t *testing.T) {
- dir := t.TempDir()
- ctx := context.Background()
-
- b, err := OpenBucket(ctx, fileURLFromPath(dir))
- if err != nil {
- t.Fatalf("OpenBucket failed: %v", err)
- }
- defer func() { _ = b.Close() }()
-
- // fileblob uses os.TempDir() by default for temp files, then os.Rename to
- // the final path. This fails with "invalid cross-device link" when the bucket
- // dir and os.TempDir() are on different filesystems (e.g. Docker volumes).
- // OpenBucket must set no_tmp_dir=true so temp files are created next to the
- // final path instead.
- if !strings.Contains(b.URL(), "no_tmp_dir=true") {
- t.Errorf("URL should contain no_tmp_dir=true to avoid cross-device rename errors, got %q", b.URL())
- }
-
- // Verify Store still works with the parameter set
- content := "cross-device test"
- _, _, err = b.Store(ctx, "test/cross-device.txt", strings.NewReader(content))
- if err != nil {
- t.Fatalf("Store failed with no_tmp_dir=true: %v", err)
- }
-
- r, err := b.Open(ctx, "test/cross-device.txt")
- if err != nil {
- t.Fatalf("Open failed: %v", err)
- }
- defer func() { _ = r.Close() }()
-
- data, _ := io.ReadAll(r)
- if string(data) != content {
- t.Errorf("content = %q, want %q", string(data), content)
- }
-}
-
func createTestBlob(t *testing.T) *Blob {
t.Helper()
dir := t.TempDir()
@@ -284,332 +254,14 @@ func createTestBlob(t *testing.T) *Blob {
t.Fatalf("OpenBucket failed: %v", err)
}
t.Cleanup(func() { _ = b.Close() })
- blob, ok := b.(*Blob)
- if !ok {
- t.Fatalf("OpenBucket returned %T, want *Blob", b)
- }
- return blob
+ return b
}
func fileURLFromPath(path string) string {
- if runtime.GOOS == osWindows {
+ if runtime.GOOS == "windows" {
// Windows paths need file:///C:/path format
path = filepath.ToSlash(path)
return "file:///" + path
}
return "file://" + path
}
-
-func TestOpenBucketWritesNoAttrsSidecar(t *testing.T) {
- dir := t.TempDir()
- ctx := context.Background()
-
- b, err := OpenBucket(ctx, fileURLFromPath(dir))
- if err != nil {
- t.Fatalf("OpenBucket failed: %v", err)
- }
- defer func() { _ = b.Close() }()
-
- if _, _, err := b.Store(ctx, "pkg/thing-1.0.0.tgz", strings.NewReader("content")); err != nil {
- t.Fatalf("Store failed: %v", err)
- }
-
- sidecars, err := filepath.Glob(filepath.Join(dir, "*", "*.attrs"))
- if err != nil {
- t.Fatalf("Glob failed: %v", err)
- }
- if len(sidecars) != 0 {
- t.Errorf("got sidecar files %v, want none: a truncated sidecar fails reads that overlap a write", sidecars)
- }
-}
-
-// A read overlapping a write to the same key must not fail. fileblob rewrote
-// its ".attrs" sidecar in place, so a reader decoding it mid-write saw a
-// partial file, which the proxy served as a 502 on an artifact it held.
-func TestConcurrentReadsSurviveWritesToSameKey(t *testing.T) {
- if runtime.GOOS == "windows" {
- // Go opens files without FILE_SHARE_DELETE, so a writer cannot replace
- // a file a reader holds open: its rename fails with access denied
- // instead of contending. The other platforms exercise this race.
- t.Skip("Windows refuses to replace a file readers hold open")
- }
- const (
- key = "pkg/thing-1.0.0.tgz"
- readers = 4
- readsPerRead = 500
- )
- dir := t.TempDir()
- ctx := context.Background()
-
- b, err := OpenBucket(ctx, fileURLFromPath(dir))
- if err != nil {
- t.Fatalf("OpenBucket failed: %v", err)
- }
- defer func() { _ = b.Close() }()
-
- payload := strings.Repeat("x", 4096)
- if _, _, err := b.Store(ctx, key, strings.NewReader(payload)); err != nil {
- t.Fatalf("seeding Store failed: %v", err)
- }
-
- // The writer reports how it ended: a Store failure would otherwise stop
- // the writes silently and let zero read failures pass for a test that
- // never contended anything.
- done := make(chan struct{})
- var writers sync.WaitGroup
- var writes int
- var writeErr error
- writers.Add(1)
- go func() {
- defer writers.Done()
- for {
- select {
- case <-done:
- return
- default:
- }
- if _, _, err := b.Store(ctx, key, strings.NewReader(payload)); err != nil {
- writeErr = err
- return
- }
- writes++
- }
- }()
-
- var failures atomic.Int64
- var reading sync.WaitGroup
- for range readers {
- reading.Add(1)
- go func() {
- defer reading.Done()
- for range readsPerRead {
- r, err := b.Open(ctx, key)
- if err != nil {
- failures.Add(1)
- continue
- }
- if _, err := io.Copy(io.Discard, r); err != nil {
- failures.Add(1)
- }
- _ = r.Close()
- }
- }()
- }
- reading.Wait()
- close(done)
- writers.Wait()
-
- if writeErr != nil {
- t.Fatalf("writer stopped early: %v", writeErr)
- }
- if writes == 0 {
- t.Fatal("no write completed, so the reads were never contended")
- }
- if got := failures.Load(); got != 0 {
- t.Errorf("%d of %d reads failed while one writer rewrote the same key, want 0", got, readers*readsPerRead)
- }
-}
-
-// seedLegacySidecar stores key through a bucket that still writes sidecars, as
-// an earlier version did, and returns the path fileblob actually used. It is
-// discovered rather than assumed, so callers test the real mapping.
-func seedLegacySidecar(t *testing.T, dir, key, payload string) string {
- t.Helper()
- ctx := context.Background()
-
- legacy, err := blob.OpenBucket(ctx, fileURLFromPath(dir)+"?no_tmp_dir=true")
- if err != nil {
- t.Fatalf("opening legacy bucket: %v", err)
- }
- if err := legacy.WriteAll(ctx, key, []byte(payload), nil); err != nil {
- t.Fatalf("legacy WriteAll: %v", err)
- }
- if err := legacy.Close(); err != nil {
- t.Fatalf("closing legacy bucket: %v", err)
- }
-
- var found []string
- walkErr := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
- if err != nil {
- return err
- }
- if !d.IsDir() && strings.HasSuffix(path, ".attrs") {
- found = append(found, path)
- }
- return nil
- })
- if walkErr != nil {
- t.Fatalf("walking %s: %v", dir, walkErr)
- }
- if len(found) != 1 {
- t.Fatalf("got sidecars %v, want exactly one", found)
- }
- return found[0]
-}
-
-// An interrupted setAttrs leaves a partial sidecar that fails every read of the
-// key, and nothing rewrites one now, so a store has to clear it.
-//
-// One key per storage path the proxy builds: ArtifactPath across ecosystems,
-// metadata blobs, and the Gradle build cache. Scoped npm names, Go's "!" case
-// escaping and the ":" in OCI digests and Debian epochs are the characters
-// most likely to part fileblob's mapping from a plain path join.
-func TestStoreClearsLegacyAttrsSidecar(t *testing.T) {
- keys := []string{
- "npm/@babel/core/7.24.0/core-7.24.0.tgz",
- "maven/org.apache.commons/commons-lang3/3.14.0/commons-lang3-3.14.0.jar",
- "golang/github.com/!burnt!sushi/toml/v1.3.2/v1.3.2.zip",
- "oci/library/nginx/sha256:abc123def456/manifest",
- "debian/tzdata/1:2024a-1/tzdata_2024a-1_all.deb",
- "pypi/requests/2.31.0/requests-2.31.0-py3-none-any.whl",
- "cargo/serde/1.0.197/serde-1.0.197.crate",
- "julia/Example/a1b2c3/a1b2c3.tar.gz",
- "conda/numpy/1.26.4/numpy-1.26.4-py311.conda",
- "_metadata/npm/@babel/core/metadata",
- "_gradle/http-build-cache/0a1b2c3d4e5f",
- // Keys fileblob escapes on every platform.
- "npm/pkg//1.0.0/x.tgz",
- "npm/pkg/../1.0.0/x.tgz",
- }
-
- for _, key := range keys {
- t.Run(key, func(t *testing.T) {
- assertStoreClearsSidecar(t, key)
- })
- }
-}
-
-func assertStoreClearsSidecar(t *testing.T, key string) {
- t.Helper()
- const payload = "payload"
- ctx := context.Background()
- dir := t.TempDir()
-
- sidecar := seedLegacySidecar(t, dir, key, payload)
- if err := os.WriteFile(sidecar, []byte(`{"user.content_type":"appl`), 0o600); err != nil {
- t.Fatalf("corrupting sidecar: %v", err)
- }
-
- b := openFileBlob(t, dir)
- if _, err := b.Open(ctx, key); err == nil {
- t.Fatal("corrupt sidecar did not fail the read, so it is not the file fileblob reads for this key")
- }
-
- derived := b.legacySidecarPath(key)
- if _, _, err := b.Store(ctx, key, strings.NewReader(payload)); err != nil {
- t.Fatalf("Store failed: %v", err)
- }
-
- if derived == "" {
- t.Fatalf("legacySidecarPath declined %q, but fileblob wrote %q", key, sidecar)
- }
- if derived != sidecar {
- t.Fatalf("derived %q, but fileblob wrote %q", derived, sidecar)
- }
- if _, err := os.Stat(sidecar); !os.IsNotExist(err) {
- t.Errorf("sidecar still present after Store, stat err = %v", err)
- }
- assertReadsBack(t, b, key, payload)
-}
-
-func assertReadsBack(t *testing.T, b *Blob, key, want string) {
- t.Helper()
- r, err := b.Open(context.Background(), key)
- if err != nil {
- t.Fatalf("read still failing after Store cleared the sidecar: %v", err)
- }
- defer func() { _ = r.Close() }()
- got, err := io.ReadAll(r)
- if err != nil {
- t.Fatalf("ReadAll failed: %v", err)
- }
- if string(got) != want {
- t.Errorf("got %q, want %q", got, want)
- }
-}
-
-func openFileBlob(t *testing.T, dir string) *Blob {
- t.Helper()
- s, err := OpenBucket(context.Background(), fileURLFromPath(dir))
- if err != nil {
- t.Fatalf("OpenBucket failed: %v", err)
- }
- t.Cleanup(func() { _ = s.Close() })
- b, ok := s.(*Blob)
- if !ok {
- t.Fatalf("got %T, want *Blob", s)
- }
- return b
-}
-
-// fileblob escapes a non-local key in a way this cannot reproduce, and one
-// holding ".." resolves outside the cache directory. Removal declines both
-// rather than delete the wrong file.
-func TestLegacySidecarPathEscapesLikeFileblob(t *testing.T) {
- root := filepath.FromSlash("/var/cache/proxy")
- b := &Blob{fileRoot: root}
- windows := runtime.GOOS == osWindows
-
- for _, tc := range []struct{ key, unix, windows string }{
- {"npm/pkg/1.0.0/x.tgz", "npm/pkg/1.0.0/x.tgz", "npm/pkg/1.0.0/x.tgz"},
- {"npm/pkg//1.0.0/x.tgz", "npm/pkg/__0x2f__1.0.0/x.tgz", "npm/pkg/__0x2f__1.0.0/x.tgz"},
- {"npm/pkg/../../etc/passwd", "npm/pkg/..__0x2f__..__0x2f__etc/passwd", "npm/pkg/..__0x2f__..__0x2f__etc/passwd"},
- {"npm/pkg/1.0.0/", "npm/pkg/1.0.0__0x2f__", "npm/pkg/1.0.0__0x2f__"},
- {"npm/a\x01b", "npm/a__0x1__b", "npm/a__0x1__b"},
- {"oci/nginx/sha256:abc/manifest", "oci/nginx/sha256:abc/manifest", "oci/nginx/sha256__0x3a__abc/manifest"},
- {"debian/tzdata/1:2024a-1/x.deb", "debian/tzdata/1:2024a-1/x.deb", "debian/tzdata/1__0x3a__2024a-1/x.deb"},
- {`npm/a\b`, `npm/a\b`, "npm/a__0x5c__b"},
- } {
- want := tc.unix
- if windows {
- want = tc.windows
- }
- want = filepath.Join(root, filepath.FromSlash(want)) + attrsExt
- if got := b.legacySidecarPath(tc.key); got != want {
- t.Errorf("legacySidecarPath(%q) = %q, want %q", tc.key, got, want)
- }
- }
-}
-
-func TestLegacySidecarPathDeclinesNonLocalKeys(t *testing.T) {
- b := &Blob{fileRoot: filepath.FromSlash("/var/cache/proxy")}
-
- for _, key := range []string{"", ".", "..", "/etc/passwd"} {
- if got := b.legacySidecarPath(key); got != "" {
- t.Errorf("legacySidecarPath(%q) = %q, want \"\"", key, got)
- }
- }
-}
-
-// Cloud backends have no local directory, so nothing is removed for them.
-func TestLegacySidecarPathEmptyForCloudBackends(t *testing.T) {
- b := &Blob{}
- if got := b.legacySidecarPath("npm/pkg/1.0.0/x.tgz"); got != "" {
- t.Errorf("legacySidecarPath = %q, want \"\" when there is no file root", got)
- }
-}
-
-// Cleanup that cannot complete must not fail the write. A non-empty directory
-// at the sidecar path makes os.Remove fail with something other than not-exist
-// on every platform, which is what a Windows sharing violation would look like
-// here.
-func TestStoreSucceedsWhenSidecarCannotBeRemoved(t *testing.T) {
- const key = "npm/pkg/1.0.0/pkg-1.0.0.tgz"
- const payload = "payload"
- dir := t.TempDir()
- ctx := context.Background()
-
- b := openFileBlob(t, dir)
-
- sidecar := filepath.Join(dir, filepath.FromSlash(key)) + ".attrs"
- if err := os.MkdirAll(filepath.Join(sidecar, "blocker"), 0o750); err != nil {
- t.Fatalf("seeding an unremovable sidecar: %v", err)
- }
- if err := os.Remove(sidecar); err == nil {
- t.Fatal("sidecar path was removable, so the test proves nothing")
- }
-
- if _, _, err := b.Store(ctx, key, strings.NewReader(payload)); err != nil {
- t.Errorf("Store failed because cleanup could not complete: %v", err)
- }
-}
diff --git a/internal/storage/filesystem.go b/internal/storage/filesystem.go
new file mode 100644
index 0000000..01be90d
--- /dev/null
+++ b/internal/storage/filesystem.go
@@ -0,0 +1,174 @@
+package storage
+
+import (
+ "context"
+ "crypto/sha256"
+ "encoding/hex"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+)
+
+// Filesystem implements Storage using the local filesystem.
+type Filesystem struct {
+ root string
+}
+
+// NewFilesystem creates a new filesystem storage rooted at the given directory.
+// The directory will be created if it does not exist.
+func NewFilesystem(root string) (*Filesystem, error) {
+ absRoot, err := filepath.Abs(root)
+ if err != nil {
+ return nil, fmt.Errorf("resolving root path: %w", err)
+ }
+
+ if err := os.MkdirAll(absRoot, 0755); err != nil {
+ return nil, fmt.Errorf("creating root directory: %w", err)
+ }
+
+ return &Filesystem{root: absRoot}, nil
+}
+
+func (fs *Filesystem) fullPath(path string) string {
+ return filepath.Join(fs.root, filepath.FromSlash(path))
+}
+
+func (fs *Filesystem) Store(ctx context.Context, path string, r io.Reader) (int64, string, error) {
+ fullPath := fs.fullPath(path)
+
+ dir := filepath.Dir(fullPath)
+ if err := os.MkdirAll(dir, 0755); err != nil {
+ return 0, "", fmt.Errorf("creating directory: %w", err)
+ }
+
+ // Write to temp file first for atomic operation
+ tmpFile, err := os.CreateTemp(dir, ".tmp-*")
+ if err != nil {
+ return 0, "", fmt.Errorf("creating temp file: %w", err)
+ }
+ tmpPath := tmpFile.Name()
+
+ // Clean up temp file on error
+ success := false
+ defer func() {
+ if !success {
+ _ = tmpFile.Close()
+ _ = os.Remove(tmpPath)
+ }
+ }()
+
+ // Write content and compute hash
+ h := sha256.New()
+ w := io.MultiWriter(tmpFile, h)
+
+ size, err := io.Copy(w, r)
+ if err != nil {
+ return 0, "", fmt.Errorf("writing content: %w", err)
+ }
+
+ if err := tmpFile.Close(); err != nil {
+ return 0, "", fmt.Errorf("closing temp file: %w", err)
+ }
+
+ // Atomic rename
+ if err := os.Rename(tmpPath, fullPath); err != nil {
+ return 0, "", fmt.Errorf("renaming temp file: %w", err)
+ }
+
+ success = true
+ hash := hex.EncodeToString(h.Sum(nil))
+ return size, hash, nil
+}
+
+func (fs *Filesystem) Open(ctx context.Context, path string) (io.ReadCloser, error) {
+ fullPath := fs.fullPath(path)
+
+ f, err := os.Open(fullPath)
+ if err != nil {
+ if os.IsNotExist(err) {
+ return nil, ErrNotFound
+ }
+ return nil, fmt.Errorf("opening file: %w", err)
+ }
+
+ return f, nil
+}
+
+func (fs *Filesystem) Exists(ctx context.Context, path string) (bool, error) {
+ fullPath := fs.fullPath(path)
+
+ _, err := os.Stat(fullPath)
+ if err != nil {
+ if os.IsNotExist(err) {
+ return false, nil
+ }
+ return false, fmt.Errorf("checking file: %w", err)
+ }
+
+ return true, nil
+}
+
+func (fs *Filesystem) Delete(ctx context.Context, path string) error {
+ fullPath := fs.fullPath(path)
+
+ err := os.Remove(fullPath)
+ if err != nil && !os.IsNotExist(err) {
+ return fmt.Errorf("removing file: %w", err)
+ }
+
+ // Try to clean up empty parent directories
+ dir := filepath.Dir(fullPath)
+ for dir != fs.root {
+ if err := os.Remove(dir); err != nil {
+ break // Directory not empty or other error
+ }
+ dir = filepath.Dir(dir)
+ }
+
+ return nil
+}
+
+func (fs *Filesystem) Size(ctx context.Context, path string) (int64, error) {
+ fullPath := fs.fullPath(path)
+
+ info, err := os.Stat(fullPath)
+ if err != nil {
+ if os.IsNotExist(err) {
+ return 0, ErrNotFound
+ }
+ return 0, fmt.Errorf("stat file: %w", err)
+ }
+
+ return info.Size(), nil
+}
+
+func (fs *Filesystem) UsedSpace(ctx context.Context) (int64, error) {
+ var total int64
+
+ err := filepath.Walk(fs.root, func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+ if !info.IsDir() {
+ total += info.Size()
+ }
+ return nil
+ })
+ if err != nil {
+ return 0, fmt.Errorf("walking directory: %w", err)
+ }
+
+ return total, nil
+}
+
+// Root returns the root directory of the storage.
+func (fs *Filesystem) Root() string {
+ return fs.root
+}
+
+// FullPath returns the full filesystem path for a storage path.
+// Useful for serving files directly or debugging.
+func (fs *Filesystem) FullPath(path string) string {
+ return fs.fullPath(path)
+}
diff --git a/internal/storage/filesystem_test.go b/internal/storage/filesystem_test.go
new file mode 100644
index 0000000..16c7447
--- /dev/null
+++ b/internal/storage/filesystem_test.go
@@ -0,0 +1,275 @@
+package storage
+
+import (
+ "bytes"
+ "context"
+ "crypto/sha256"
+ "encoding/hex"
+ "errors"
+ "io"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+)
+
+func TestNewFilesystem(t *testing.T) {
+ dir := t.TempDir()
+ root := filepath.Join(dir, "cache")
+
+ fs, err := NewFilesystem(root)
+ if err != nil {
+ t.Fatalf("NewFilesystem failed: %v", err)
+ }
+
+ if _, err := os.Stat(root); err != nil {
+ t.Errorf("root directory not created: %v", err)
+ }
+
+ if fs.Root() != root {
+ t.Errorf("Root() = %q, want %q", fs.Root(), root)
+ }
+}
+
+func TestFilesystemStore(t *testing.T) {
+ fs := createTestFilesystem(t)
+ ctx := context.Background()
+ content := "test content for storage"
+
+ size, hash, err := fs.Store(ctx, "npm/lodash/4.17.21/lodash.tgz", strings.NewReader(content))
+ if err != nil {
+ t.Fatalf("Store failed: %v", err)
+ }
+
+ if size != int64(len(content)) {
+ t.Errorf("size = %d, want %d", size, len(content))
+ }
+
+ h := sha256.Sum256([]byte(content))
+ wantHash := hex.EncodeToString(h[:])
+ if hash != wantHash {
+ t.Errorf("hash = %s, want %s", hash, wantHash)
+ }
+
+ // Verify file exists on disk
+ fullPath := fs.FullPath("npm/lodash/4.17.21/lodash.tgz")
+ data, err := os.ReadFile(fullPath)
+ if err != nil {
+ t.Fatalf("reading stored file: %v", err)
+ }
+ if string(data) != content {
+ t.Errorf("stored content = %q, want %q", string(data), content)
+ }
+}
+
+func TestFilesystemStoreAtomic(t *testing.T) {
+ fs := createTestFilesystem(t)
+ ctx := context.Background()
+
+ // Store initial content
+ _, _, err := fs.Store(ctx, "test/file.txt", strings.NewReader("initial"))
+ if err != nil {
+ t.Fatalf("initial Store failed: %v", err)
+ }
+
+ // Overwrite with new content
+ _, _, err = fs.Store(ctx, "test/file.txt", strings.NewReader("updated"))
+ if err != nil {
+ t.Fatalf("update Store failed: %v", err)
+ }
+
+ // Verify updated content
+ r, err := fs.Open(ctx, "test/file.txt")
+ if err != nil {
+ t.Fatalf("Open failed: %v", err)
+ }
+ defer func() { _ = r.Close() }()
+
+ data, _ := io.ReadAll(r)
+ if string(data) != "updated" {
+ t.Errorf("content = %q, want %q", string(data), "updated")
+ }
+}
+
+func TestFilesystemOpen(t *testing.T) {
+ fs := createTestFilesystem(t)
+ ctx := context.Background()
+ content := "readable content"
+
+ _, _, _ = fs.Store(ctx, "test/read.txt", strings.NewReader(content))
+
+ r, err := fs.Open(ctx, "test/read.txt")
+ if err != nil {
+ t.Fatalf("Open failed: %v", err)
+ }
+ defer func() { _ = r.Close() }()
+
+ data, err := io.ReadAll(r)
+ if err != nil {
+ t.Fatalf("ReadAll failed: %v", err)
+ }
+ if string(data) != content {
+ t.Errorf("content = %q, want %q", string(data), content)
+ }
+}
+
+func TestFilesystemOpenNotFound(t *testing.T) {
+ fs := createTestFilesystem(t)
+ ctx := context.Background()
+
+ _, err := fs.Open(ctx, "does/not/exist.txt")
+ if !errors.Is(err, ErrNotFound) {
+ t.Errorf("Open non-existent = %v, want ErrNotFound", err)
+ }
+}
+
+func TestFilesystemExists(t *testing.T) {
+ fs := createTestFilesystem(t)
+ ctx := context.Background()
+
+ exists, err := fs.Exists(ctx, "test/exists.txt")
+ if err != nil {
+ t.Fatalf("Exists failed: %v", err)
+ }
+ if exists {
+ t.Error("Exists returned true for non-existent file")
+ }
+
+ _, _, _ = fs.Store(ctx, "test/exists.txt", strings.NewReader("content"))
+
+ exists, err = fs.Exists(ctx, "test/exists.txt")
+ if err != nil {
+ t.Fatalf("Exists after store failed: %v", err)
+ }
+ if !exists {
+ t.Error("Exists returned false for existing file")
+ }
+}
+
+func TestFilesystemDelete(t *testing.T) {
+ fs := createTestFilesystem(t)
+ ctx := context.Background()
+
+ _, _, _ = fs.Store(ctx, "test/delete/nested/file.txt", strings.NewReader("content"))
+
+ err := fs.Delete(ctx, "test/delete/nested/file.txt")
+ if err != nil {
+ t.Fatalf("Delete failed: %v", err)
+ }
+
+ exists, _ := fs.Exists(ctx, "test/delete/nested/file.txt")
+ if exists {
+ t.Error("file still exists after delete")
+ }
+
+ // Empty parent directories should be cleaned up
+ nestedDir := fs.FullPath("test/delete/nested")
+ if _, err := os.Stat(nestedDir); !os.IsNotExist(err) {
+ t.Error("empty nested directory not cleaned up")
+ }
+}
+
+func TestFilesystemDeleteNotFound(t *testing.T) {
+ fs := createTestFilesystem(t)
+ ctx := context.Background()
+
+ // Delete non-existent file should not error
+ err := fs.Delete(ctx, "does/not/exist.txt")
+ if err != nil {
+ t.Errorf("Delete non-existent = %v, want nil", err)
+ }
+}
+
+func TestFilesystemSize(t *testing.T) {
+ fs := createTestFilesystem(t)
+ ctx := context.Background()
+ content := "size test content"
+
+ _, _, _ = fs.Store(ctx, "test/size.txt", strings.NewReader(content))
+
+ size, err := fs.Size(ctx, "test/size.txt")
+ if err != nil {
+ t.Fatalf("Size failed: %v", err)
+ }
+ if size != int64(len(content)) {
+ t.Errorf("Size = %d, want %d", size, len(content))
+ }
+}
+
+func TestFilesystemSizeNotFound(t *testing.T) {
+ fs := createTestFilesystem(t)
+ ctx := context.Background()
+
+ _, err := fs.Size(ctx, "does/not/exist.txt")
+ if !errors.Is(err, ErrNotFound) {
+ t.Errorf("Size non-existent = %v, want ErrNotFound", err)
+ }
+}
+
+func TestFilesystemUsedSpace(t *testing.T) {
+ fs := createTestFilesystem(t)
+ ctx := context.Background()
+
+ // Empty storage
+ used, err := fs.UsedSpace(ctx)
+ if err != nil {
+ t.Fatalf("UsedSpace failed: %v", err)
+ }
+ if used != 0 {
+ t.Errorf("UsedSpace empty = %d, want 0", used)
+ }
+
+ // Add some files
+ _, _, _ = fs.Store(ctx, "a.txt", strings.NewReader("aaaa")) // 4 bytes
+ _, _, _ = fs.Store(ctx, "b.txt", strings.NewReader("bbbbbb")) // 6 bytes
+ _, _, _ = fs.Store(ctx, "c/d.txt", strings.NewReader("ccccc")) // 5 bytes
+
+ used, err = fs.UsedSpace(ctx)
+ if err != nil {
+ t.Fatalf("UsedSpace failed: %v", err)
+ }
+ if used != 15 {
+ t.Errorf("UsedSpace = %d, want 15", used)
+ }
+}
+
+func TestFilesystemLargeFile(t *testing.T) {
+ fs := createTestFilesystem(t)
+ ctx := context.Background()
+
+ // 1MB of data
+ data := bytes.Repeat([]byte("x"), 1024*1024)
+
+ size, hash, err := fs.Store(ctx, "large/file.bin", bytes.NewReader(data))
+ if err != nil {
+ t.Fatalf("Store large file failed: %v", err)
+ }
+ if size != int64(len(data)) {
+ t.Errorf("size = %d, want %d", size, len(data))
+ }
+
+ h := sha256.Sum256(data)
+ wantHash := hex.EncodeToString(h[:])
+ if hash != wantHash {
+ t.Errorf("hash mismatch for large file")
+ }
+
+ // Read it back
+ r, _ := fs.Open(ctx, "large/file.bin")
+ defer func() { _ = r.Close() }()
+ readBack, _ := io.ReadAll(r)
+ if !bytes.Equal(readBack, data) {
+ t.Error("large file content mismatch")
+ }
+}
+
+func createTestFilesystem(t *testing.T) *Filesystem {
+ t.Helper()
+ dir := t.TempDir()
+
+ fs, err := NewFilesystem(dir)
+ if err != nil {
+ t.Fatalf("NewFilesystem failed: %v", err)
+ }
+ return fs
+}
diff --git a/internal/storage/gcs.go b/internal/storage/gcs.go
deleted file mode 100644
index a096c18..0000000
--- a/internal/storage/gcs.go
+++ /dev/null
@@ -1,97 +0,0 @@
-package storage
-
-import (
- "context"
- "crypto/sha256"
- "encoding/hex"
- "errors"
- "io"
- "time"
-
- gcstorage "github.com/git-pkgs/gcs"
-)
-
-// GCS adapts a Google Cloud Storage bucket to Storage.
-type GCS struct {
- bucket *gcstorage.Bucket
- url string
-}
-
-// OpenGCS opens a Google Cloud Storage bucket from a gs:// URL.
-func OpenGCS(ctx context.Context, urlStr string) (*GCS, error) {
- bucket, err := gcstorage.OpenBucket(ctx, urlStr)
- if err != nil {
- return nil, err
- }
- return &GCS{bucket: bucket, url: urlStr}, nil
-}
-
-func (g *GCS) Store(ctx context.Context, path string, r io.Reader) (int64, string, error) {
- h := sha256.New()
- size, err := g.bucket.Write(ctx, path, io.TeeReader(r, h))
- if err != nil {
- return 0, "", err
- }
- return size, hex.EncodeToString(h.Sum(nil)), nil
-}
-
-func (g *GCS) Open(ctx context.Context, path string) (io.ReadCloser, error) {
- r, err := g.bucket.Open(ctx, path)
- if errors.Is(err, gcstorage.ErrNotFound) {
- return nil, ErrNotFound
- }
- return r, err
-}
-
-func (g *GCS) Exists(ctx context.Context, path string) (bool, error) {
- return g.bucket.Exists(ctx, path)
-}
-
-func (g *GCS) Delete(ctx context.Context, path string) error {
- return g.bucket.Delete(ctx, path)
-}
-
-func (g *GCS) Size(ctx context.Context, path string) (int64, error) {
- size, err := g.bucket.Size(ctx, path)
- if errors.Is(err, gcstorage.ErrNotFound) {
- return 0, ErrNotFound
- }
- return size, err
-}
-
-func (g *GCS) SignedURL(ctx context.Context, path string, expiry time.Duration) (string, error) {
- u, err := g.bucket.SignedURL(ctx, path, expiry)
- if errors.Is(err, gcstorage.ErrSignedURLUnsupported) {
- return "", ErrSignedURLUnsupported
- }
- return u, err
-}
-
-func (g *GCS) UsedSpace(ctx context.Context) (int64, error) {
- return g.bucket.UsedSpace(ctx)
-}
-
-func (g *GCS) ListPrefix(ctx context.Context, prefix string) ([]ObjectInfo, error) {
- objects, err := g.bucket.ListPrefix(ctx, prefix)
- if err != nil {
- return nil, err
- }
-
- result := make([]ObjectInfo, 0, len(objects))
- for _, object := range objects {
- result = append(result, ObjectInfo{
- Path: object.Name,
- Size: object.Size,
- ModTime: object.ModTime,
- })
- }
- return result, nil
-}
-
-func (g *GCS) Close() error {
- return nil
-}
-
-func (g *GCS) URL() string {
- return g.url
-}
diff --git a/internal/storage/gcs_test.go b/internal/storage/gcs_test.go
deleted file mode 100644
index 1dfd0a2..0000000
--- a/internal/storage/gcs_test.go
+++ /dev/null
@@ -1,152 +0,0 @@
-package storage
-
-import (
- "context"
- "crypto/sha256"
- "encoding/hex"
- "encoding/json"
- "errors"
- "io"
- "net/http"
- "net/http/httptest"
- "net/url"
- "sort"
- "strconv"
- "strings"
- "testing"
- "time"
-)
-
-func TestOpenBucketGCSRoundTripWithEmulator(t *testing.T) {
- server := httptest.NewServer(&fakeGCSServer{t: t, objects: map[string]string{}})
- defer server.Close()
- t.Setenv("STORAGE_EMULATOR_HOST", server.URL)
-
- ctx := context.Background()
- store, err := OpenBucket(ctx, "gs://test-bucket")
- if err != nil {
- t.Fatalf("OpenBucket failed: %v", err)
- }
-
- size, hash, err := store.Store(ctx, "npm/pkg/file.tgz", strings.NewReader("content"))
- if err != nil {
- t.Fatalf("Store failed: %v", err)
- }
- wantHash := sha256.Sum256([]byte("content"))
- if size != int64(len("content")) || hash != hex.EncodeToString(wantHash[:]) {
- t.Fatalf("Store returned size=%d hash=%q", size, hash)
- }
-
- exists, err := store.Exists(ctx, "npm/pkg/file.tgz")
- if err != nil || !exists {
- t.Fatalf("Exists = %v, %v; want true, nil", exists, err)
- }
-
- r, err := store.Open(ctx, "npm/pkg/file.tgz")
- if err != nil {
- t.Fatalf("Open failed: %v", err)
- }
- data, _ := io.ReadAll(r)
- _ = r.Close()
- if string(data) != "content" {
- t.Fatalf("Open content = %q, want content", data)
- }
-
- lister, ok := store.(interface {
- ListPrefix(context.Context, string) ([]ObjectInfo, error)
- })
- if !ok {
- t.Fatal("GCS storage does not support prefix listing")
- }
- list, err := lister.ListPrefix(ctx, "npm/")
- if err != nil {
- t.Fatalf("ListPrefix failed: %v", err)
- }
- if len(list) != 1 || list[0].Path != "npm/pkg/file.tgz" {
- t.Fatalf("ListPrefix = %#v", list)
- }
-
- if err := store.Delete(ctx, "npm/pkg/file.tgz"); err != nil {
- t.Fatalf("Delete failed: %v", err)
- }
- exists, err = store.Exists(ctx, "npm/pkg/file.tgz")
- if err != nil || exists {
- t.Fatalf("Exists after delete = %v, %v; want false, nil", exists, err)
- }
-
- reader, err := store.Open(ctx, "npm/pkg/file.tgz")
- if reader != nil || !errors.Is(err, ErrNotFound) {
- t.Fatalf("Open missing object = %v, %v; want nil, ErrNotFound", reader, err)
- }
- if _, err := store.Size(ctx, "npm/pkg/file.tgz"); !errors.Is(err, ErrNotFound) {
- t.Fatalf("Size missing object = %v, want ErrNotFound", err)
- }
- if _, err := store.SignedURL(ctx, "npm/pkg/file.tgz", time.Minute); !errors.Is(err, ErrSignedURLUnsupported) {
- t.Fatalf("SignedURL with emulator = %v, want ErrSignedURLUnsupported", err)
- }
-}
-
-type fakeGCSServer struct {
- t *testing.T
- objects map[string]string
-}
-
-func (f *fakeGCSServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- switch {
- case r.Method == http.MethodPost && r.URL.Path == "/upload/storage/v1/b/test-bucket/o":
- name := r.URL.Query().Get("name")
- data, _ := io.ReadAll(r.Body)
- f.objects[name] = string(data)
- writeJSON(w, fakeGCSObject{Name: name, Size: strconv.Itoa(len(data)), Updated: time.Now().UTC().Format(time.RFC3339Nano)})
- case r.Method == http.MethodGet && r.URL.Path == "/storage/v1/b/test-bucket/o":
- prefix := r.URL.Query().Get("prefix")
- page := fakeGCSListResponse{}
- for name, data := range f.objects {
- if strings.HasPrefix(name, prefix) {
- page.Items = append(page.Items, fakeGCSObject{Name: name, Size: strconv.Itoa(len(data)), Updated: time.Now().UTC().Format(time.RFC3339Nano)})
- }
- }
- sort.Slice(page.Items, func(i, j int) bool { return page.Items[i].Name < page.Items[j].Name })
- writeJSON(w, page)
- case r.Method == http.MethodGet && strings.HasPrefix(r.URL.Path, "/storage/v1/b/test-bucket/o/"):
- name := objectNameFromPath(r.URL.Path)
- data, ok := f.objects[name]
- if !ok {
- http.NotFound(w, r)
- return
- }
- if r.URL.Query().Get("alt") == "media" {
- _, _ = io.WriteString(w, data)
- return
- }
- writeJSON(w, fakeGCSObject{Name: name, Size: strconv.Itoa(len(data)), Updated: time.Now().UTC().Format(time.RFC3339Nano)})
- case r.Method == http.MethodDelete && strings.HasPrefix(r.URL.Path, "/storage/v1/b/test-bucket/o/"):
- delete(f.objects, objectNameFromPath(r.URL.Path))
- w.WriteHeader(http.StatusNoContent)
- default:
- f.t.Errorf("unexpected request: %s %s", r.Method, r.URL.String())
- http.Error(w, "unexpected request", http.StatusInternalServerError)
- }
-}
-
-type fakeGCSObject struct {
- Name string `json:"name"`
- Size string `json:"size"`
- Updated string `json:"updated"`
-}
-
-type fakeGCSListResponse struct {
- NextPageToken string `json:"nextPageToken"`
- Items []fakeGCSObject `json:"items"`
-}
-
-func writeJSON(w http.ResponseWriter, v any) {
- w.Header().Set("Content-Type", "application/json")
- _ = json.NewEncoder(w).Encode(v)
-}
-
-func objectNameFromPath(p string) string {
- escaped := strings.TrimPrefix(p, "/storage/v1/b/test-bucket/o/")
- name, _ := url.PathUnescape(escaped)
- return name
-}
diff --git a/internal/storage/storage.go b/internal/storage/storage.go
index 5ff86f2..1efbcba 100644
--- a/internal/storage/storage.go
+++ b/internal/storage/storage.go
@@ -5,37 +5,22 @@
// - file:///path/to/dir - Local filesystem storage
// - s3://bucket-name - Amazon S3
// - s3://bucket?endpoint=http://localhost:9000 - S3-compatible (MinIO)
-// - gs://bucket-name - Google Cloud Storage (supports GKE Workload Identity
-// via Application Default Credentials)
-// - azblob://container-name - Azure Blob Storage
//
// Use OpenBucket to create a storage backend from a URL.
package storage
import (
"context"
+ "crypto/sha256"
+ "encoding/hex"
"errors"
"io"
- "time"
)
-const dirPermissions = 0755
-
var (
ErrNotFound = errors.New("artifact not found")
-
- // ErrSignedURLUnsupported is returned by SignedURL when the backend
- // cannot generate presigned URLs (e.g. local filesystem).
- ErrSignedURLUnsupported = errors.New("signed URLs not supported by storage backend")
)
-// ObjectInfo contains metadata for a stored object.
-type ObjectInfo struct {
- Path string
- Size int64
- ModTime time.Time
-}
-
// Storage defines the interface for artifact storage backends.
type Storage interface {
// Store writes content from r to the given path.
@@ -58,18 +43,8 @@ type Storage interface {
// Returns ErrNotFound if the path does not exist.
Size(ctx context.Context, path string) (int64, error)
- // SignedURL returns a presigned URL granting time-limited GET access to path.
- // Returns ErrSignedURLUnsupported if the backend cannot generate presigned URLs.
- SignedURL(ctx context.Context, path string, expiry time.Duration) (string, error)
-
// UsedSpace returns the total bytes used by all stored content.
UsedSpace(ctx context.Context) (int64, error)
-
- // URL returns the storage backend URL (e.g. "file:///path" or "s3://bucket").
- URL() string
-
- // Close releases any resources held by the storage backend.
- Close() error
}
// ArtifactPath builds a storage path for an artifact.
@@ -81,3 +56,42 @@ func ArtifactPath(ecosystem, namespace, name, version, filename string) string {
}
return ecosystem + "/" + name + "/" + version + "/" + filename
}
+
+// HashingReader wraps a reader and computes SHA256 hash as content is read.
+type HashingReader struct {
+ r io.Reader
+ hash []byte
+ h interface{ Sum([]byte) []byte }
+ size int64
+ done bool
+}
+
+func NewHashingReader(r io.Reader) *HashingReader {
+ h := sha256.New()
+ return &HashingReader{
+ r: io.TeeReader(r, h),
+ h: h,
+ }
+}
+
+func (hr *HashingReader) Read(p []byte) (n int, err error) {
+ n, err = hr.r.Read(p)
+ hr.size += int64(n)
+ if err == io.EOF {
+ hr.done = true
+ hr.hash = hr.h.Sum(nil)
+ }
+ return
+}
+
+func (hr *HashingReader) Sum() string {
+ if !hr.done {
+ hr.hash = hr.h.Sum(nil)
+ hr.done = true
+ }
+ return hex.EncodeToString(hr.hash)
+}
+
+func (hr *HashingReader) Size() int64 {
+ return hr.size
+}
diff --git a/internal/storage/storage_test.go b/internal/storage/storage_test.go
index 65f5a23..c71df26 100644
--- a/internal/storage/storage_test.go
+++ b/internal/storage/storage_test.go
@@ -1,11 +1,10 @@
package storage
import (
- "bytes"
- "context"
"crypto/sha256"
"encoding/hex"
"io"
+ "strings"
"testing"
)
@@ -34,32 +33,26 @@ func TestArtifactPath(t *testing.T) {
}
}
-// assertLargeFileRoundTrip stores a 1MB file in the given storage, verifies size and
-// hash, then reads it back and confirms the content matches.
-func assertLargeFileRoundTrip(t *testing.T, s Storage) {
- t.Helper()
- ctx := context.Background()
+func TestHashingReader(t *testing.T) {
+ content := "hello world"
+ r := NewHashingReader(strings.NewReader(content))
- data := bytes.Repeat([]byte("x"), 1024*1024)
-
- size, hash, err := s.Store(ctx, "large/file.bin", bytes.NewReader(data))
+ data, err := io.ReadAll(r)
if err != nil {
- t.Fatalf("Store large file failed: %v", err)
- }
- if size != int64(len(data)) {
- t.Errorf("size = %d, want %d", size, len(data))
+ t.Fatalf("ReadAll failed: %v", err)
}
- h := sha256.Sum256(data)
+ if string(data) != content {
+ t.Errorf("got content %q, want %q", string(data), content)
+ }
+
+ if r.Size() != int64(len(content)) {
+ t.Errorf("got size %d, want %d", r.Size(), len(content))
+ }
+
+ h := sha256.Sum256([]byte(content))
wantHash := hex.EncodeToString(h[:])
- if hash != wantHash {
- t.Errorf("hash mismatch for large file")
- }
-
- r, _ := s.Open(ctx, "large/file.bin")
- defer func() { _ = r.Close() }()
- readBack, _ := io.ReadAll(r)
- if !bytes.Equal(readBack, data) {
- t.Error("large file content mismatch")
+ if r.Sum() != wantHash {
+ t.Errorf("got hash %s, want %s", r.Sum(), wantHash)
}
}
diff --git a/tools.go b/tools.go
deleted file mode 100644
index caf60b1..0000000
--- a/tools.go
+++ /dev/null
@@ -1,7 +0,0 @@
-//go:build tools
-
-package tools
-
-import (
- _ "github.com/swaggo/swag/cmd/swag"
-)