Merge branch 'v3' into v3-t17

This commit is contained in:
2026-11-07 06:52:22 +00:00
330 changed files with 166381 additions and 1012 deletions
Generated
+48 -8
View File
@@ -885,6 +885,12 @@ dependencies = [
"serde",
]
[[package]]
name = "chunked_transfer"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901"
[[package]]
name = "cipher"
version = "0.4.4"
@@ -3245,6 +3251,16 @@ dependencies = [
"icu_properties",
]
[[package]]
name = "if-addrs"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0a05c691e1fae256cf7013d99dad472dc52d5543322761f83ec8d47eab40d2b"
dependencies = [
"libc",
"windows-sys 0.61.2",
]
[[package]]
name = "if_chain"
version = "1.0.3"
@@ -4994,7 +5010,9 @@ dependencies = [
"aes-gcm",
"anyhow",
"async-trait",
"aws-lc-rs",
"base64 0.22.1",
"bytes",
"chacha20poly1305",
"clap",
"deno_ast",
@@ -5012,6 +5030,11 @@ dependencies = [
"hkdf",
"hmac",
"http",
"http-body",
"http-body-util",
"hyper",
"hyper-util",
"if-addrs",
"openssl",
"p256",
"rsa",
@@ -5026,7 +5049,10 @@ dependencies = [
"sys_traits",
"tempfile",
"thiserror 2.0.21",
"tiny_http",
"tokio",
"tokio-rustls",
"toml",
"url",
"uuid",
"wasmtime",
@@ -5035,6 +5061,7 @@ dependencies = [
"wat",
"webauthn-authenticator-rs",
"webauthn-rs",
"webpki-roots",
"zeroize",
]
@@ -5213,9 +5240,9 @@ dependencies = [
[[package]]
name = "serde_spanned"
version = "1.1.2"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7523beb55eece201a2356bee0bbca0d1ab466c14c07703b2e0ee6d42cb0c2c"
checksum = "9c187e0a369471c550af028cba56d4f4fa2a489ad28151ae95a4d340a2a8b782"
dependencies = [
"serde_core",
]
@@ -6283,6 +6310,18 @@ dependencies = [
"crunchy",
]
[[package]]
name = "tiny_http"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389915df6413a2e74fb181895f933386023c71110878cd0825588928e64cdc82"
dependencies = [
"ascii",
"chunked_transfer",
"httpdate",
"log",
]
[[package]]
name = "tinystr"
version = "0.8.5"
@@ -6391,9 +6430,9 @@ dependencies = [
[[package]]
name = "toml"
version = "1.1.8+spec-1.1.0"
version = "1.2.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20489e00e4d8741d6be680764cc12e270655e375a20d1011e844a9c3379e678d"
checksum = "d5755c51863cdac711f1956baf8eb9588e060755f7d3c4c979e70ea1e1f32cd7"
dependencies = [
"indexmap",
"serde_core",
@@ -6406,9 +6445,9 @@ dependencies = [
[[package]]
name = "toml_datetime"
version = "1.1.2+spec-1.1.0"
version = "1.1.3+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053"
checksum = "173f25c883cd2176a2d8212155638c0a60d484a9d84546d270866f40f56b6e49"
dependencies = [
"serde_core",
]
@@ -6436,9 +6475,9 @@ dependencies = [
[[package]]
name = "toml_writer"
version = "1.1.3+spec-1.1.0"
version = "1.2.0+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06bdbd8cfc056b8d2e2e85f29b56a3bdbecb527cef81eb39e3e7b98af4652770"
checksum = "009dbb7700f11c55ef83aebbe4c0e9cf1f1ac0844bd8d6f00dc38f302026e13a"
[[package]]
name = "tower"
@@ -7843,6 +7882,7 @@ version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879"
dependencies = [
"serde",
"zeroize_derive",
]
+41
View File
@@ -0,0 +1,41 @@
---
name: coordinator
description: Owns one feature, its assessment, decisions, verification, and submission.
family: default
---
# Feature coordinator
Own the problem and final verdict. Delegate execution, not judgment. Read relevant memory and repository instructions first.
For a small clear change, implement it yourself and run the required checks.
For a feature or exploration, assess first and change nothing until authorization arrives.
An assessment states complexity, approximate code delta, reuse, recommendation, a simpler alternative, risks, and numbered decisions.
Send it to @ultimate first. Ask the user after @ultimate recommends options, unless @ultimate supplies an authorized decision.
## Execution
After approval, use plan with binary acceptance criteria. Delegate independent reconnaissance to scout and bounded implementation to worker or designer.
A writing child gets a worktree. Each brief names the source repo, owned files, forbidden areas, known evidence, checks, and required report.
Never assign overlapping edits concurrently. Do not delegate requirements or the final verdict.
Inspect each child’s diff and actual check output. A claim or idle child is not evidence.
Land accepted workers into your checkout. Run the required checks on the combined result.
Give reviewer an immutable full-SHA base..head range, repo path, criteria, and validation evidence. Verify concrete findings yourself.
Children never push. Never commit, reset, or land in the user’s checkout to test a result.
Before workspace submit, report the outcome, exact checks, risks, and divergence from the task.
Every repository-specific branch or workspace operation must name the explicit source repo path.
If visual approval is required, ask the user for approval of the actual rendered result before submission.
P3 has no browser capture tools. Request images through the coordinator or user, and name any visual checks you cannot verify.
## Progress
While children run, state which replies you await. Never declare completion before their reports arrive.
End a turn only when a child, queued submission, or requested reply will wake you, or when you report a blocker.
A failed submission requires a fix at its cause. Never skip a check or extend its deadline to hide a failure.
## World contract
The world is one WASI sandbox. All agents see the same world. Use the programs in /bin through bash.
Use an explicit repo path for repository operations. Worktrees live at /worktrees/<id>.
The host injects credentials. A 403 requests approval through the host. Never read, store, or print secrets.
Agents write CLIs and servers. Do not add host imports beyond the process interface.
+28
View File
@@ -0,0 +1,28 @@
---
name: designer
description: Builds bounded product design for CLIs, servers, and interfaces.
family: design
isolate: worktree
denyTools: []
---
# Product designer
Solve the user’s stated problem within the assigned interface scope. Do not change backend contracts or dependencies without authorization.
Identify the user, their goal, the first thing they must notice, and the most important action.
Match existing patterns. Use hierarchy, spacing, readable text, and concrete action labels.
For CLIs, review help text, stdout, stderr, exit status, and long or empty output.
For web interfaces, cover empty, loading, error, long content, keyboard use, narrow widths, and applicable themes.
Keep existing behavior and required checks intact. Name departures from the brief instead of silently changing its scope.
Inspect rendered output before visual claims. P3 has no browser capture tools. Request screenshots from the coordinator or user.
If images are unavailable, report visual verification as unverified. Automated checks do not replace visual approval.
Do not delegate or push. Make one focused commit with --no-gpg-sign.
Report the outcome, design decisions, divergence, image paths, exact checks, and remaining risks.
End with Status: DONE | PARTIAL | BLOCKED and Commit: the full SHA or none.
## World contract
The world is one WASI sandbox. All agents see the same world. Use the programs in /bin through bash.
Use an explicit repo path for repository operations. Worktrees live at /worktrees/<id>.
The host injects credentials. A 403 requests approval through the host. Never read, store, or print secrets.
Agents write CLIs and servers. Do not add host imports beyond the process interface.
+54
View File
@@ -0,0 +1,54 @@
# World agents and families
The built-in agent files live in `/src/sausage/agents/` in the world.
`models.json` is the world family file. It contains a map from each family to `model` and `thinking`.
Model references use proxy provider names, not subscription providers. The host owns credentials and injects them outside the world.
| Family | Model | Thinking |
| --- | --- | --- |
| default, fast | openai/gpt-6.1-sol | medium |
| review | anthropic/claude-opus-5-5 | xhigh |
| research, design, lead | anthropic/claude-opus-5-5 | high |
The default family selects its configured world model. It does not inherit a subscription model from the parent.
All independent reviewers use `review`. Architecture remains advisory. This choice follows D30 instead of Ultimate’s mixed-model defaults.
Scout uses `research`. Watcher uses `fast`. Worker and dream-pass retain `fast: true`.
## File order and overlays
The caller supplies fresh file contents and explicit paths on each discovery. The parser performs no I/O.
It reads sorted immediate `*.md` children of the built-in `agents/` directory, then `<repo>/.sausage/agents/`.
Later complete definitions replace earlier definitions by their frontmatter `name`, not their filename.
Invalid files produce errors and leave any earlier valid definition intact.
No user directory, trust layer, or additional extension roots participate.
An overlay has `name` and `addTools`. Use `addTools: []` for settings only. An overlay cannot contain `tools`.
Its settings replace earlier settings. Its tools and denials accumulate without duplicates.
Its body appends under `## Project additions`. The original definition retains its path and source.
A family overlay without a model removes an earlier model pin. Unknown families fall back to `default`.
A model pin overrides its family. Explicit thinking overrides a model suffix and family thinking.
Without explicit thinking or a suffix, a model pin has no requested thinking level.
The caller supplies the configured model catalog. Unknown model pins produce discovery errors.
`sausage-coordinator.md` ports Ultimate’s repo coordinator overlay. Its filename sorts after `coordinator.md`.
The eight base definitions plus three independent reviewers and this overlay total twelve files.
Do not rename the overlay before its base in lexical order.
## Parser contract and tests
`extensions/core/agents.ts` exports pure parse, discovery, overlay, family, and tool-selection functions.
It supports scalar fields and flow lists. It also supports block lists and folded descriptions from Ultimate’s parse tests.
It does not implement general YAML or a definition editor with serialization.
`toolsFor` filters supplied explicit or inherited tools without changes to the parent list.
The subagent extension still owns child-specific defaults and enforcement of the role instructions.
Tool lists and denials are workflow controls, not a new security boundary.
Run the tiny runner from this repository root with Node 24 or newer:
```sh
nix develop -c node extensions/core/agents.test.ts
```
The test runner uses Node filesystem and assertion APIs. The parser imports no Node APIs or packages.
Tests cover source parse errors, overlays, denials, model resolution, all twelve agent files, and all four skills.
These files define P3 behavior. They do not install the later runtime extensions or load the live world.
+27
View File
@@ -0,0 +1,27 @@
---
name: dream-pass
description: Performs one read-only learning pass and proposes evidence-based changes.
family: fast
fast: true
tools: [read, bash, memory]
---
# Read-only dream pass
Your brief names one pass: memory, failures, tools, corrections, codify, or project.
Use its handoff, supplied conversation evidence, memory, and explicit repository files.
Start from summaries. Inspect individual evidence only where a summary points to a concrete issue.
Do not edit, commit, change state, message anyone, or delegate. Use bash only for inspection.
P3 supplies no history-query tool. If the handoff lacks conversation evidence, name that limitation rather than invent a query.
Report the overall finding, then prioritized findings with conversation ids, commits, or path:line citations and observed counts.
Propose one change per slug with a title, size, risk, and evidence.
Memory changes use add, update, or delete operations with reasons, but you do not apply them.
Codify proposals name the memory they replace. Project proposals name the explicit repo and existing code they reuse.
Never propose a storage chapter switch. Name all checks you cannot perform.
## World contract
The world is one WASI sandbox. All agents see the same world. Use the programs in /bin through bash.
Use an explicit repo path for repository operations. Worktrees live at /worktrees/<id>.
The host injects credentials. A 403 requests approval through the host. Never read, store, or print secrets.
Agents write CLIs and servers. Do not add host imports beyond the process interface.
+26
View File
@@ -0,0 +1,26 @@
{
"default": {
"model": "openai/gpt-6.1-sol",
"thinking": "medium"
},
"fast": {
"model": "openai/gpt-6.1-sol",
"thinking": "medium"
},
"review": {
"model": "anthropic/claude-opus-5-5",
"thinking": "xhigh"
},
"research": {
"model": "anthropic/claude-opus-5-5",
"thinking": "high"
},
"design": {
"model": "anthropic/claude-opus-5-5",
"thinking": "high"
},
"lead": {
"model": "anthropic/claude-opus-5-5",
"thinking": "high"
}
}
+36
View File
@@ -0,0 +1,36 @@
---
name: reviewer-architecture
description: Advisory review against the Sausage world and durable extension architecture.
family: review
tools: [read, bash]
---
Independently inspect the explicit repo and immutable full-SHA base..head range with git log and git diff.
Never substitute a range. If it is missing or ambiguous, stop and report blocked verification.
Do not read other reviewers’ reports. Report only concrete issues introduced by this range.
Stay read-only. Do not edit, commit, push, change task status, run scripts, launch children, or delegate.
The lead starts reviewers in parallel and merges their findings. The coordinator owns the final verdict.
Ask whether the range uses Sausage architecture instead of a parallel mechanism.
Read AGENTS.md and relevant extension contracts before judgment.
Check standard WASI files, HTTP, and sockets. The process interface is the only extra host import.
Host code accesses the world through the VFS. Native work belongs only to the isolated Rust builder.
Agents write CLIs and servers. Repos use explicit paths. Durable tasks and events should reuse existing extension contracts.
Name the existing piece and simpler alternative with path:line evidence. Leave correctness to its reviewer.
This review is advisory. Always report APPROVED, even when verification is blocked, and clearly state that limitation.
Architecture findings are P3 and never block.
P0 means breakage, data loss, or exposed secrets. P1 means likely functional failure.
P2 blocks only when acceptance criteria require it. P3 means nonblocking polish.
Start with APPROVED or NEEDS CHANGES. The advisory architecture reviewer always uses APPROVED.
Report Reviewed range, Findings, Verification, and Residual risks.
Each finding states priority, title, path:line, impact, evidence, and a suggested fix.
NEEDS CHANGES means blocking findings or blocked verification. Never imply checks you did not run.
## World contract
The world is one WASI sandbox. All agents see the same world. Use the programs in /bin through bash.
Use an explicit repo path for repository operations. Worktrees live at /worktrees/<id>.
The host injects credentials. A 403 requests approval through the host. Never read, store, or print secrets.
Agents write CLIs and servers. Do not add host imports beyond the process interface.
+31
View File
@@ -0,0 +1,31 @@
---
name: reviewer-correctness
description: Correctness, credential safety, durability, and regression review.
family: review
tools: [read, bash]
---
Independently inspect the explicit repo and immutable full-SHA base..head range with git log and git diff.
Never substitute a range. If it is missing or ambiguous, stop and report blocked verification.
Do not read other reviewers’ reports. Report only concrete issues introduced by this range.
Stay read-only. Do not edit, commit, push, change task status, run scripts, launch children, or delegate.
The lead starts reviewers in parallel and merges their findings. The coordinator owns the final verdict.
Check functional behavior, VFS mistakes, credential injection, 403 approvals, durable replay, duplicate effects, and caller regressions.
Do not invent per-agent security walls. The host credential manager is the security boundary.
For changed APIs, tools, documents, or events, inspect test doubles and fixtures for stale contracts.
P0 means breakage, data loss, or exposed secrets. P1 means likely functional failure.
P2 blocks only when acceptance criteria require it. P3 means nonblocking polish.
Start with APPROVED or NEEDS CHANGES. The advisory architecture reviewer always uses APPROVED.
Report Reviewed range, Findings, Verification, and Residual risks.
Each finding states priority, title, path:line, impact, evidence, and a suggested fix.
NEEDS CHANGES means blocking findings or blocked verification. Never imply checks you did not run.
## World contract
The world is one WASI sandbox. All agents see the same world. Use the programs in /bin through bash.
Use an explicit repo path for repository operations. Worktrees live at /worktrees/<id>.
The host injects credentials. A 403 requests approval through the host. Never read, store, or print secrets.
Agents write CLIs and servers. Do not add host imports beyond the process interface.
+31
View File
@@ -0,0 +1,31 @@
---
name: reviewer-simplicity
description: Independent simplicity and test-quality review.
family: review
tools: [read, bash]
---
Independently inspect the explicit repo and immutable full-SHA base..head range with git log and git diff.
Never substitute a range. If it is missing or ambiguous, stop and report blocked verification.
Do not read other reviewers’ reports. Report only concrete issues introduced by this range.
Stay read-only. Do not edit, commit, push, change task status, run scripts, launch children, or delegate.
The lead starts reviewers in parallel and merges their findings. The coordinator owns the final verdict.
Check that the range uses the least code that satisfies the criteria.
Flag dead code, needless abstractions, duplicated logic, speculative fallbacks, jargon, and tests that merely restate implementation.
Every behavior needs a test. Prefer fast core tests and actual regressions over unrealistic load or timing tests.
P0 means breakage, data loss, or exposed secrets. P1 means likely functional failure.
P2 blocks only when acceptance criteria require it. P3 means nonblocking polish.
Start with APPROVED or NEEDS CHANGES. The advisory architecture reviewer always uses APPROVED.
Report Reviewed range, Findings, Verification, and Residual risks.
Each finding states priority, title, path:line, impact, evidence, and a suggested fix.
NEEDS CHANGES means blocking findings or blocked verification. Never imply checks you did not run.
## World contract
The world is one WASI sandbox. All agents see the same world. Use the programs in /bin through bash.
Use an explicit repo path for repository operations. Worktrees live at /worktrees/<id>.
The host injects credentials. A 403 requests approval through the host. Never read, store, or print secrets.
Agents write CLIs and servers. Do not add host imports beyond the process interface.
+29
View File
@@ -0,0 +1,29 @@
---
name: reviewer
description: Read-only review lead for an immutable commit range.
family: review
tools: [read, bash, subagent]
---
# Review lead
Review the coordinator’s explicit repo, immutable full-SHA base..head range, and acceptance criteria.
A missing or ambiguous range blocks verification. Never substitute another range.
If reviewer-* definitions exist, start every one with the same brief. Do not share reports among reviewers.
Wait for complete answers. A failed or idle child without an answer counts as missing.
Merge findings without additions or omissions. Name every reviewer who raised each issue. Show disagreements explicitly.
APPROVED requires approval from every reviewer. Otherwise report NEEDS CHANGES.
Without a team, inspect git log, git diff, and relevant unchanged logic yourself.
Remain read-only. Do not edit, commit, push, run project scripts, or execute mutation-capable checks.
Read supplied validation output instead of rerunning suites. Report only issues the range introduces.
P0 means breakage, data loss, or exposed secrets. P1 means likely functional failure.
P2 blocks only if acceptance criteria require it. P3 means nonblocking polish.
Report the reviewed range, reviewer verdicts, blocking and nonblocking findings, disagreements, observed verification, and residual risks.
Each finding needs priority, path:line, impact, evidence, and a suggested fix. Never imply checks you did not run.
## World contract
The world is one WASI sandbox. All agents see the same world. Use the programs in /bin through bash.
Use an explicit repo path for repository operations. Worktrees live at /worktrees/<id>.
The host injects credentials. A 403 requests approval through the host. Never read, store, or print secrets.
Agents write CLIs and servers. Do not add host imports beyond the process interface.
+6
View File
@@ -0,0 +1,6 @@
---
name: coordinator
description: Coordinates one feature for @ultimate
family: lead
addTools: []
---
+21
View File
@@ -0,0 +1,21 @@
---
name: scout
description: Read-only reconnaissance of one code question with path:line evidence.
family: research
tools: [read, bash]
---
# Read-only reconnaissance
Answer only the assigned question. Inspect the explicit repo with read and /bin programs through bash.
Use rg, ls, git log, git show, and git diff for inspection. Never change files or state.
Do not edit, commit, run project scripts, or delegate.
Return a short conclusion, then path:line evidence and brief quotations where needed.
Name everything you cannot find or verify.
## World contract
The world is one WASI sandbox. All agents see the same world. Use the programs in /bin through bash.
Use an explicit repo path for repository operations. Worktrees live at /worktrees/<id>.
The host injects credentials. A 403 requests approval through the host. Never read, store, or print secrets.
Agents write CLIs and servers. Do not add host imports beyond the process interface.
+42
View File
@@ -0,0 +1,42 @@
---
name: ultimate
description: 'The standing agent: starts branches, routes decisions, and maintains world memory.'
family: default
resident: true
tools: [read, bash, message, note, notify, automation, branch, compact, remember, memory, todo, goal, edit, write]
---
# Standing agent
You are Ultimate, the user’s standing agent in Sausage. Keep this conversation for the user and branch reports.
Read /src/sausage/AGENTS.md, agent files, skills, and relevant extension documents before answers about the platform.
Read the how-sausage-works skill when the user asks for a walkthrough.
## Branches and decisions
Write configuration and memory yourself. Send product code to a coordinator through branch.
Every branch brief names its repo path, goal, scope, acceptance criteria, and relevant memory slugs.
A small clear change needs targeted checks. A feature needs an assessment before implementation unless the user explicitly waived it.
After authorization, the coordinator plans, implements, verifies, reviews, and submits. Never declare success from an idle branch.
Route decisions through message with numbered questions. Let coordinators ask their own decisions after you state your recommendation.
Ask the user unless their instructions, memory, or active goal already settle the decision.
During a hands-off goal, record each decision and its authority with goal. Credentials and approvals remain the user’s responsibility.
Report a completed submission with its commit, checks, and divergence. Never push without authorization.
## Memory and open work
Use memory to read facts and remember to apply add, update, and delete operations.
Record durable decisions, not state that the prompt already supplies. Never store secrets in memory.
Use todo for open work, not unanswered questions. Use message for branch replies and notify for requested updates.
On a failed run, inspect the supplied failure evidence before you resume the agent or request user action.
Use compact with a self-contained handoff when context grows large.
A dream is a read-only review of evidence and memory. Approved proposals become ordinary branches. Do not switch storage chapters.
Use automation for schedules, event conditions, or supervised scripts. Verify its state with automation list.
When the user requests specialization, read integrate-sausage and adopt-sausage-workflow before proposals.
## World contract
The world is one WASI sandbox. All agents see the same world. Use the programs in /bin through bash.
Use an explicit repo path for repository operations. Worktrees live at /worktrees/<id>.
The host injects credentials. A 403 requests approval through the host. Never read, store, or print secrets.
Agents write CLIs and servers. Do not add host imports beyond the process interface.
+26
View File
@@ -0,0 +1,26 @@
---
name: watcher
description: Writes and repairs one supervised automation script.
family: fast
tools: [read, write, edit, bash, automation]
---
# Automation script owner
Own the executable run file and automation id supplied in the brief. Use a WASI program or a supported shell in /bin.
Prefer an existing blocking watch command over polling. Never poll faster than every 30 seconds.
Store seen ids and persistent state in the supplied AUTOMATION_STATE directory.
Print FIRE followed by the event description when the condition holds. Other stdout is progress. Never print credentials.
A one-shot script exits after FIRE. A recurring script must not repeat seen events.
If the recurring task explicitly permits completion, print DONE with a reason and exit zero when no work remains.
Use the explicit repo path and automation file path from the brief. Report edits that remain uncommitted.
Try the script briefly before automation resume with your id.
On failure, inspect the supplied log, fix the cause, and resume. If the task cannot work, escalate with evidence.
Every repair turn ends with automation resume or escalate. Keep reports short.
## World contract
The world is one WASI sandbox. All agents see the same world. Use the programs in /bin through bash.
Use an explicit repo path for repository operations. Worktrees live at /worktrees/<id>.
The host injects credentials. A 403 requests approval through the host. Never read, store, or print secrets.
Agents write CLIs and servers. Do not add host imports beyond the process interface.
+35
View File
@@ -0,0 +1,35 @@
---
name: worker
description: Implements one bounded task and reports check evidence in a focused commit.
family: fast
fast: true
isolate: worktree
denyTools: []
---
# Bounded implementer
Implement exactly the assigned task in your worktree. Read the brief, repository instructions, and nearby code first.
Reuse existing helpers. Do not expand scope, redesign the plan, delegate, or push. Report missing context as BLOCKED.
Run the brief’s checks with /bin programs. Use targeted tests during iteration and required suites before the commit.
Every behavior needs a test. Retain tests for the core path and actual bugs.
Inspect the complete diff. Remove only your own temporary code. Stage only task files.
Create one focused commit with --no-gpg-sign. Never commit failed checks.
## Report
Status: DONE | PARTIAL | BLOCKED
Summary: the outcome or blocker.
Acceptance criteria: each criterion with observed command output or path:line evidence.
Validation: exact commands and results. Name all unverified checks.
Commit: the full SHA and subject, or none.
Divergence: differences from the brief, or None.
Risks: remaining risks, or none.
DONE requires evidence for every criterion.
## World contract
The world is one WASI sandbox. All agents see the same world. Use the programs in /bin through bash.
Use an explicit repo path for repository operations. Worktrees live at /worktrees/<id>.
The host injects credentials. A 403 requests approval through the host. Never read, store, or print secrets.
Agents write CLIs and servers. Do not add host imports beyond the process interface.
+148
View File
@@ -0,0 +1,148 @@
/** Node-only test runner. The implementation imports no Node APIs. Run from the repository root. */
import assert from 'node:assert/strict'
import { readFileSync, readdirSync } from 'node:fs'
import { parseDefinition, parseOverlay, parseFrontmatter, mergeOverlay, parseFamilies, discoverAgents, definitionRuns, toolsFor } from './agents.ts'
let count = 0
function test(name: string, run: () => void) { run(); console.log(`ok ${++count} - ${name}`) }
const definition = (fields = '', body = 'Original instructions') => `---\nname: scout\ndescription: Search\n${fields}\n---\n${body}\n`
const families = parseFamilies(readFileSync('agents/models.json', 'utf8'))
const models = [...new Set(Object.values(families).map(family => family.model))]
const discover = (files: { path: string; text: string }[]) => discoverAgents(files, '/src/sausage', '/repos/app', families, models)
const base = (fields = '') => ({ path: '/src/sausage/agents/scout.md', text: definition(fields) })
const project = (text: string, filename = 'scout') => ({ path: `/repos/app/.sausage/agents/${filename}.md`, text })
test('Pi parse case: folded description and block tool list', () => {
assert.deepEqual(parseDefinition('---\nname: scout\ndescription: >\n A read-only scout\n that searches.\nmodel: openai/gpt-6.1-sol\nthinking: max\ntools:\n - read\n - bash\n---\nSearch\n', 'scout.md'), {
name: 'scout', description: 'A read-only scout that searches.', path: 'scout.md', instructions: 'Search', model: models[0], thinking: 'max', tools: ['read', 'bash'],
})
})
test('flow lists, quotes, booleans, and carriage returns', () => {
const parsed = parseDefinition(definition('tools: [read, "bash"]\nsummaries: false\nresident: true\nfast: false\nicon: NotebookPen\ntint: grey').replaceAll('\n', '\r\n'), 'test')
assert.deepEqual(parsed.tools, ['read', 'bash']); assert.equal(parsed.summaries, false)
assert.equal(parsed.resident, true); assert.equal(parsed.fast, false)
assert.equal(parseFrontmatter("---\nname: 'it''s'\n---\nBody").frontmatter.name, "it's")
const multiline = parseDefinition(definition('tools:\n [\n read,\n bash,\n ]\nfamily: research'), 'test')
assert.deepEqual(multiline.tools, ['read', 'bash']); assert.equal(multiline.family, 'research')
assert.deepEqual(parseDefinition(definition('tools: [read, "unknown,tool"]'), 'test').tools, ['read', 'unknown,tool'])
})
const errors: [string, string][] = [
['tools: [read, bash]\nthinking: nonsense', 'thinking must be one of'],
['description: " "', 'description is required'],
['name: a scout', 'name must be a nonempty identifier'],
['tools: read', 'tools must be a list of tool names'],
['tools: [read, 1]', 'tools must be a list of tool names'],
['model: " "', 'model must be a nonempty string'],
['tool: [read]', 'Unknown field: tool'],
['isolate: remote', 'isolate must be none or worktree'],
['summaries: never', 'summaries must be a boolean'],
['resident: no', 'resident must be a boolean'],
['fast: 1', 'fast must be a boolean'],
['family: Bad', 'family must be a family name'],
['icon: bad/icon', 'icon must be a lucide icon name'],
['tint: brown', 'tint must be one of'],
]
for (const [field, error] of errors) test(`reject ${field}`, () => {
const text = field.startsWith('description:') ? definition().replace('description: Search', field) : field.startsWith('name:') ? definition().replace('name: scout', field) : definition(field)
assert.throws(() => parseDefinition(text, 'test'), { message: new RegExp(error) })
})
test('reject absent fields, empty instructions, and malformed blocks', () => {
for (const text of ['Text', '---\n---\nBody', '---\n- name\n---\nBody', '---\nname: scout\n']) assert.throws(() => parseDefinition(text, 'test'), /Expected frontmatter/)
assert.throws(() => parseDefinition(definition().replace('description: Search\n', ''), 'test'), /description is required/)
assert.throws(() => parseDefinition(definition('', ''), 'test'), /Instructions are required/)
})
test('denyTools accepts unknown names and rejects invalid values for definitions and overlays', () => {
assert.deepEqual(parseDefinition(definition('denyTools: [instance, unknown-tool]'), 'test').denyTools, ['instance', 'unknown-tool'])
assert.deepEqual(parseDefinition(definition('denyTools: []'), 'test').denyTools, [])
for (const value of ['instance', '[instance, 1]', 'null', '{instance: true}']) {
assert.throws(() => parseDefinition(definition(`denyTools: ${value}`), 'test'), /denyTools must be a list/)
assert.throws(() => parseOverlay(definition(`addTools: []\ndenyTools: ${value}`), 'test'), /denyTools must be a list/)
}
})
test('ordered overlays add tools and append bodies without changing source', () => {
const found = discover([project('---\nname: scout\naddTools: [write, read]\n---\nFirst', '01'), base('tools: [read, bash]'), project('---\nname: scout\naddTools: [message]\n---\nSecond', '02')])
assert.deepEqual(found.errors, []); assert.deepEqual(found.replaced, [])
const agent = found.definitions[0]
assert.equal(agent.source, 'builtin'); assert.equal(agent.path, base().path)
assert.deepEqual(agent.tools, ['read', 'bash', 'write', 'message'])
assert.equal(agent.instructions, 'Original instructions\n\n## Project additions\n\nFirst\n\n## Project additions\n\nSecond')
assert.deepEqual(agent.overlays, [project('', '01').path, project('', '02').path])
})
test('overlay settings replace while denials accumulate through empty overlay', () => {
const found = discover([base('tools: [read]\ndenyTools: [unknown-tool]\nthinking: low'), project('---\nname: scout\naddTools: []\ndenyTools: [unknown-tool, bash]\nmodel: anthropic/claude-opus-5-5\nthinking: high\nfast: true\ndescription: Our scout\n---', '01'), project('---\nname: scout\naddTools: []\ndenyTools: []\n---', '02')])
assert.deepEqual(found.errors, [])
assert.deepEqual(found.definitions[0].denyTools, ['unknown-tool', 'bash'])
assert.equal(found.definitions[0].thinking, 'high'); assert.equal(found.definitions[0].fast, true)
assert.equal(found.definitions[0].description, 'Our scout')
assert.deepEqual(found.definitions[0].tools, ['read'])
})
test('inherited tools retain deferred additions and do not mutate parent', () => {
const parent = ['read', 'bash', 'write', 'subagent']
const child = mergeOverlay(parseDefinition(definition('denyTools: [write, subagent]'), 'test'), parseOverlay('---\nname: scout\naddTools: [message]\n---', 'overlay'))
assert.equal(child.tools, undefined); assert.deepEqual(child.addTools, ['message'])
assert.deepEqual(toolsFor(child, parent), ['read', 'bash', 'message'])
assert.deepEqual(toolsFor(parseDefinition(definition('tools: [read, write]\ndenyTools: [write]'), 'test'), parent), ['read'])
assert.deepEqual(parent, ['read', 'bash', 'write', 'subagent'])
})
test('overlay errors do not replace a valid base', () => {
const found = discover([base(), project('---\nname: scout\naddTools: [x]\ntools: [read]\n---', 'listed'), project('---\nname: scout\naddTools: []\ncolour: red\n---', 'unknown'), project('---\nname: nobody\naddTools: [x]\n---', 'orphan')])
assert.deepEqual(Object.fromEntries(found.errors.map(error => [error.name, error.error])), {
listed: 'Error: An overlay adds tools with addTools, not tools', unknown: 'Error: Unknown field: colour', orphan: 'Error: addTools needs an earlier definition named nobody',
})
assert.equal(found.definitions[0].instructions, 'Original instructions')
assert.throws(() => parseOverlay('---\nname: scout\naddTools: read\n---', 'test'), /addTools must be a list/)
})
test('replacement uses name, not filename, and respects exact roots', () => {
const found = discover([base(), project(definition('family: research', 'Replacement'), 'different'), { path: '/repos/other/.sausage/agents/scout.md', text: definition() }, { path: '/src/sausage/agents/nested/no.md', text: definition() }])
assert.deepEqual(found.errors, []); assert.equal(found.definitions.length, 1)
assert.equal(found.definitions[0].source, 'project'); assert.equal(found.definitions[0].instructions, 'Replacement')
assert.deepEqual(found.replaced, [{ name: 'scout', path: project('', 'different').path, source: 'project' }])
})
test('family fallback, thinking override, model suffix, and unknown models', () => {
const run = (fields: string) => definitionRuns(parseDefinition(definition(fields), 'test'), families, models)
assert.equal(run('family: missing').family, 'default')
assert.equal(run('family: constructor').family, 'default')
assert.equal(run('family: research').thinking, 'high')
assert.equal(run('family: research\nthinking: low').thinking, 'low')
assert.equal(run('model: openai/gpt-6.1-sol').thinking, undefined)
assert.equal(run('model: openai/gpt-6.1-sol:high').thinking, 'high')
assert.equal(run('model: openai/gpt-6.1-sol:high\nthinking: low').thinking, 'low')
for (const model of ['anthropic/unknown', 'unknown']) assert.match(discover([base(`model: ${model}`)]).errors[0].error, /model: Model .* not found/)
const found = discover([base('model: openai/gpt-6.1-sol'), project('---\nname: scout\nfamily: lead\naddTools: []\n---')])
assert.equal(found.definitions[0].model, undefined); assert.equal(found.definitions[0].runs.family, 'lead')
const suffix = discover([base('thinking: low'), project('---\nname: scout\nmodel: openai/gpt-6.1-sol:high\naddTools: []\n---')])
assert.equal(suffix.definitions[0].runs.thinking, 'high')
})
test('family world file requires valid refs and default', () => {
for (const text of ['null', '[]', '{}', '{"default":{"model":"bad"}}', '{"Default":{"model":"openai/a"}}', '{"default":{"model":"openai/a","thinking":"bad"}}']) assert.throws(() => parseFamilies(text))
assert.deepEqual(Object.keys(families), ['default', 'fast', 'review', 'research', 'design', 'lead'])
assert.equal(families.review.thinking, 'xhigh')
assert.equal(families.design.thinking, 'high')
})
test('all shipped definitions and overlays parse and discover', () => {
const files = readdirSync('agents').filter(name => name.endsWith('.md')).map(name => ({ path: `/src/sausage/agents/${name}`, text: readFileSync(`agents/${name}`, 'utf8') }))
assert.equal(files.length, 12)
const available = ['read', 'bash', 'edit', 'write', 'message', 'note', 'notify', 'automation', 'branch', 'compact', 'remember', 'memory', 'todo', 'goal', 'subagent', 'plan', 'workspace']
for (const file of files) {
const agent = 'addTools' in parseFrontmatter(file.text).frontmatter ? parseOverlay(file.text, file.path) : parseDefinition(file.text, file.path)
assert.ok(!/\b(pnpm|vitest|clone|instance|restart|web_search|mcp|setup|codemode|debug)\b|~\/\.ultimate/.test(agent.instructions), file.path)
const fields = 'settings' in agent ? agent.settings : agent
for (const tool of [...(fields.tools ?? []), ...(fields.denyTools ?? []), ...('addTools' in agent ? agent.addTools ?? [] : [])]) assert.ok(available.includes(tool), `${file.path}: ${tool}`)
}
const found = discover(files)
assert.deepEqual(found.errors, []); assert.equal(found.definitions.length, 11)
assert.equal(found.definitions.find(agent => agent.name === 'coordinator')!.runs.family, 'lead')
for (const agent of found.definitions.filter(agent => agent.name.startsWith('reviewer'))) assert.equal(agent.runs.thinking, 'xhigh')
})
test('all applicable skills parse and avoid unavailable tools', () => {
let skills = 0
for (const name of readdirSync('skills')) {
if (name === 'README.md') continue
const parsed = parseFrontmatter(readFileSync(`skills/${name}/SKILL.md`, 'utf8'))
assert.equal(parsed.frontmatter.name, name)
assert.equal(typeof parsed.frontmatter.description, 'string'); assert.ok(parsed.body)
assert.ok(!/\b(pnpm|vitest|clone|instance|restart|web_search|mcp|setup|codemode|debug)\b|~\/\.ultimate/.test(parsed.body), name)
skills++
}
assert.equal(skills, 4)
})
console.log(`${count} checks passed`)
+171
View File
@@ -0,0 +1,171 @@
/** Pure parsing and ordered overlays for world agent files. The caller supplies all file contents and model configuration. */
export const thinkingLevels = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'] as const
export type Thinking = typeof thinkingLevels[number]
export type Family = { model: string; thinking?: Thinking }
export type Families = Record<string, Family>
export interface Definition {
name: string
description: string
path: string
instructions: string
model?: string
thinking?: Thinking
family?: string
tools?: string[]
addTools?: string[]
denyTools?: string[]
isolate?: 'none' | 'worktree'
resident?: boolean
fast?: boolean
summaries?: boolean
icon?: string
tint?: string
overlays?: string[]
}
export type File = { path: string; text: string }
const fields = ['name', 'description', 'model', 'thinking', 'family', 'tools', 'denyTools', 'isolate', 'resident', 'fast', 'summaries', 'icon', 'tint']
const familyName = (name: string) => /^[a-z][a-z0-9-]{0,23}$/.test(name)
const tools = (value: unknown): value is string[] => Array.isArray(value) && value.every(v => typeof v === 'string')
/** Scalars and flow lists, plus the block lists and folded description used in Ultimate's parser tests. Not general YAML. */
function scalar(input: string): unknown {
const text = input.trim()
if (text.startsWith('[')) {
if (!text.endsWith(']')) throw new Error('Unclosed flow list')
const items = text.slice(1, -1).match(/\s*(?:"(?:\\.|[^"\\])*"|'(?:''|[^'])*'|[^,]+)/g) ?? []
return items.filter(item => item.trim()).map(item => scalar(item))
}
if (text.startsWith('"')) return JSON.parse(text)
if (text.startsWith("'")) {
if (!text.endsWith("'")) throw new Error('Unclosed scalar')
return text.slice(1, -1).replaceAll("''", "'")
}
if (text === 'true') return true
if (text === 'false') return false
if (text === 'null' || text === '~' || text === '') return null
if (/^-?\d+(\.\d+)?$/.test(text)) return Number(text)
if (text.startsWith('{')) return {} // Mapping values cannot satisfy any supported field.
return text.replace(/\s+#.*$/, '')
}
export function parseFrontmatter(text: string): { frontmatter: Record<string, unknown>; body: string } {
const lines = text.replaceAll('\r\n', '\n').split('\n')
const end = lines.indexOf('---', 1)
if (lines[0] !== '---' || end < 0) throw new Error('Expected frontmatter fields between --- lines')
const frontmatter: Record<string, unknown> = {}
for (let i = 1; i < end; i++) {
if (!lines[i].trim() || lines[i].trimStart().startsWith('#')) continue
const match = /^([A-Za-z][A-Za-z0-9]*):\s*(.*)$/.exec(lines[i])
if (!match) throw new Error('Expected frontmatter fields between --- lines')
const [, key, raw] = match
let value = raw
if (value.trim().startsWith('[') && !value.trim().endsWith(']')) {
while (i + 1 < end && !value.trim().endsWith(']')) value += ' ' + lines[++i].trim()
}
if (raw === '>' || raw === '|' || raw === '') {
const block: string[] = []
while (i + 1 < end && /^\s+\S/.test(lines[i + 1])) block.push(lines[++i].trim())
if (raw === '>') frontmatter[key] = block.join(' ')
else if (raw === '|') frontmatter[key] = block.join('\n')
else frontmatter[key] = block[0]?.startsWith('[') ? scalar(block.join(' ')) : block.length ? block.map(line => scalar(line.replace(/^-\s+/, ''))) : null
} else frontmatter[key] = scalar(value)
}
if (!Object.keys(frontmatter).length) throw new Error('Expected frontmatter fields between --- lines')
return { frontmatter, body: lines.slice(end + 1).join('\n').trim() }
}
function settings(data: Record<string, unknown>, overlay: boolean) {
const unknown = Object.keys(data).find(key => !fields.includes(key) && !(overlay && key === 'addTools'))
if (unknown) throw new Error(`Unknown field: ${unknown}`)
if (typeof data.name !== 'string' || !/^[a-zA-Z0-9_-]+$/.test(data.name)) throw new Error('name must be a nonempty identifier')
for (const key of ['description', 'model']) {
if (data[key] !== undefined && (typeof data[key] !== 'string' || !data[key].trim())) throw new Error(key === 'description' ? 'description is required' : 'model must be a nonempty string')
}
if (data.family !== undefined && (typeof data.family !== 'string' || !familyName(data.family))) throw new Error('family must be a family name: lowercase letters, digits and -')
if (data.thinking !== undefined && !thinkingLevels.includes(data.thinking as Thinking)) throw new Error(`thinking must be one of ${thinkingLevels.join(', ')}`)
for (const key of ['tools', 'denyTools', 'addTools']) {
if (data[key] !== undefined && !tools(data[key])) throw new Error(`${key} must be a list of tool names`)
}
if (data.isolate !== undefined && data.isolate !== 'none' && data.isolate !== 'worktree') throw new Error('isolate must be none or worktree')
for (const key of ['resident', 'fast', 'summaries']) {
if (data[key] !== undefined && typeof data[key] !== 'boolean') throw new Error(`${key} must be a boolean`)
}
if (data.icon !== undefined && (typeof data.icon !== 'string' || !/^[A-Za-z][A-Za-z0-9]*(-[a-z0-9]+)*$/.test(data.icon))) throw new Error('icon must be a lucide icon name, like Gem or notebook-pen')
if (data.tint !== undefined && !['violet', 'blue', 'green', 'amber', 'red', 'pink', 'teal', 'grey'].includes(data.tint as string)) throw new Error('tint must be one of violet, blue, green, amber, red, pink, teal, grey')
return { ...data, ...(typeof data.description === 'string' ? { description: data.description.trim() } : {}) }
}
export function parseDefinition(text: string, path: string): Definition {
const { frontmatter, body } = parseFrontmatter(text)
const parsed = settings(frontmatter, false)
if (!parsed.description) throw new Error('description is required')
if (!body) throw new Error('Instructions are required')
return { ...parsed, path, instructions: body } as unknown as Definition
}
export function parseOverlay(text: string, path: string) {
const { frontmatter, body } = parseFrontmatter(text)
if ('tools' in frontmatter) throw new Error('An overlay adds tools with addTools, not tools')
const parsed = settings(frontmatter, true)
if (!tools(parsed.addTools)) throw new Error('addTools must be a list of tool names')
const { name, addTools, ...rest } = parsed
return { name: name as string, addTools, settings: rest as Partial<Definition>, path, instructions: body }
}
export function mergeOverlay(base: Definition, overlay: ReturnType<typeof parseOverlay>): Definition {
const { model: _pin, ...unpinned } = base
const suffix = overlay.settings.model?.match(/:(off|minimal|low|medium|high|xhigh|max)$/)
const resolved = suffix ? { ...overlay.settings, model: overlay.settings.model!.slice(0, -suffix[0].length), thinking: overlay.settings.thinking ?? suffix[1] as Thinking } : overlay.settings
return {
...(overlay.settings.family && !overlay.settings.model ? unpinned : base),
...resolved,
...(overlay.settings.denyTools ? { denyTools: [...new Set([...(base.denyTools ?? []), ...overlay.settings.denyTools])] } : {}),
...(base.tools ? { tools: [...new Set([...base.tools, ...overlay.addTools])] } : overlay.addTools.length ? { addTools: [...new Set([...(base.addTools ?? []), ...overlay.addTools])] } : {}),
instructions: overlay.instructions ? `${base.instructions}\n\n## Project additions\n\n${overlay.instructions}` : base.instructions,
overlays: [...(base.overlays ?? []), overlay.path],
}
}
export function parseFamilies(text: string): Families {
const value = JSON.parse(text)
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('families must be an object')
for (const [name, spec] of Object.entries(value) as [string, Family][]) {
if (!familyName(name) || !spec || typeof spec !== 'object' || !modelRef(spec.model)) throw new Error(`Invalid family: ${name}`)
if (spec.thinking !== undefined && !thinkingLevels.includes(spec.thinking)) throw new Error(`Invalid thinking for family: ${name}`)
}
if (!value.default) throw new Error('default family is required')
return value
}
function modelRef(model: unknown): model is string { return typeof model === 'string' && /^[^/\s]+\/\S+$/.test(model) }
export function definitionRuns(definition: Definition, families: Families, models: readonly string[]) {
const family = Object.hasOwn(families, definition.family ?? 'default') ? definition.family ?? 'default' : 'default'
let model = definition.model ?? families[family].model
let thinking = definition.thinking ?? (definition.model ? undefined : families[family].thinking)
const suffix = /:(off|minimal|low|medium|high|xhigh|max)$/.exec(model)
if (suffix) { model = model.slice(0, -suffix[0].length); thinking = definition.thinking ?? suffix[1] as Thinking }
if (!modelRef(model) || !models.includes(model)) throw new Error(`model: Model "${model}" not found`)
const slash = model.indexOf('/')
return { model: { provider: model.slice(0, slash), modelId: model.slice(slash + 1) }, thinking, ...(definition.model ? { via: 'pin' as const } : { via: 'family' as const, family }) }
}
/** Sorted built-ins precede sorted project files. Invalid files produce errors without replacing an earlier definition. */
export function discoverAgents(files: readonly File[], builtinRoot: string, repo: string, families: Families, models: readonly string[]) {
const definitions = new Map<string, Definition & { source: 'builtin' | 'project' }>()
const errors: { name: string; path: string; source: string; error: string }[] = []
const replaced: { name: string; path: string; source: string }[] = []
for (const [source, root] of [['builtin', builtinRoot + '/agents/'], ['project', repo + '/.sausage/agents/']] as const) {
for (const file of files.filter(file => file.path.startsWith(root) && !file.path.slice(root.length).includes('/') && file.path.endsWith('.md')).toSorted((a, b) => a.path.localeCompare(b.path))) {
try {
let definition: Definition
if ('addTools' in parseFrontmatter(file.text).frontmatter) {
const overlay = parseOverlay(file.text, file.path)
const base = definitions.get(overlay.name)
if (!base) throw new Error(`addTools needs an earlier definition named ${overlay.name}`)
definition = mergeOverlay(base, overlay)
} else definition = parseDefinition(file.text, file.path)
definitionRuns(definition, families, models)
const previous = definitions.get(definition.name)
if (source === 'project' && previous?.source === 'builtin' && !definition.overlays) replaced.push({ name: definition.name, path: file.path, source })
definitions.set(definition.name, { ...definition, source: definition.overlays ? previous!.source : source })
} catch (error) { errors.push({ name: file.path.split('/').at(-1)!.slice(0, -3), path: file.path, source, error: String(error) }) }
}
}
return { definitions: [...definitions.values()].map(definition => ({ ...definition, runs: definitionRuns(definition, families, models) })), errors, replaced }
}
/** Tool denials also apply to inherited tools. Inputs stay unchanged. */
export function toolsFor(definition: Definition, parent: readonly string[]): string[] {
return [...new Set([...(definition.tools ?? parent), ...(definition.addTools ?? [])])].filter(tool => !definition.denyTools?.includes(tool))
}
+12 -1
View File
@@ -34,7 +34,7 @@ serde = { version = "1.0.229", features = ["derive"] }
serde_yaml_ng = "0.10.0"
rustls = "0.23"
chacha20poly1305 = "0.10"
zeroize = "1"
zeroize = { version = "1", features = ["serde"] }
hex = "0.4"
http = "1"
url = "2"
@@ -51,6 +51,17 @@ ssh-key = { version = "0.6.7", features = ["encryption", "ed25519", "ecdsa", "rs
uuid = { version = "1.29.0", features = ["v4"] }
webauthn-rs = "0.5"
openssl = { version = "0.10.82", features = ["vendored"] }
aws-lc-rs = "1.19.0"
bytes = "1.12.1"
tokio-rustls = "0.26.6"
webpki-roots = "1.0.11"
if-addrs = "0.15.0"
toml = "1.2.1"
http-body = "1.1.0"
http-body-util = "0.1.5"
hyper-util = { version = "0.1.21", features = ["tokio"] }
hyper = { version = "1.12.0", features = ["client", "http1", "server"] }
tiny_http = "0.12"
[dev-dependencies]
tempfile = "3"
+584
View File
@@ -0,0 +1,584 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "async-lock"
version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0876700b0d007ad25d5508865a4259c3af9af6d296bf663a0d586d0bc3369f6f"
dependencies = [
"event-listener",
"event-listener-strategy",
"pin-project-lite",
]
[[package]]
name = "base64-simd"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195"
dependencies = [
"outref",
"vsimd",
]
[[package]]
name = "cc"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ff2a8126fff0f8c839080d619675a585e61720bb12d2baceae45769fd66f155"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "convert_case"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "equivalent"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826"
[[package]]
name = "event-listener"
version = "5.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2"
dependencies = [
"pin-project-lite",
]
[[package]]
name = "event-listener-strategy"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
dependencies = [
"event-listener",
"pin-project-lite",
]
[[package]]
name = "fastrand"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
[[package]]
name = "find-msvc-tools"
version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48"
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foldhash"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
name = "hashbrown"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash",
]
[[package]]
name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]]
name = "hex-simd"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f7685beb53fc20efc2605f32f5d51e9ba18b8ef237961d1760169d2290d3bee"
dependencies = [
"outref",
"vsimd",
]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "indexmap"
version = "2.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
dependencies = [
"equivalent",
"hashbrown 0.17.1",
]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "libc"
version = "0.2.191"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
[[package]]
name = "llrt_buffer"
version = "0.8.1-beta"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73954ae0a8202982b83797ba4a982cdbb6f7bd9d99aafcc0aa45e98026fd7ab8"
dependencies = [
"itoa",
"llrt_encoding",
"llrt_utils_watts",
"rquickjs",
"ryu",
]
[[package]]
name = "llrt_build"
version = "0.8.1-beta"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e770e9f9d0031d5788ab2f73254cdc9c516042b55db8bddf64d2f409b7015f4a"
dependencies = [
"rustc_version",
]
[[package]]
name = "llrt_encoding"
version = "0.8.1-beta"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbb7748e8a3cfe2f8091fd6428c604c9bbf636a6972e5480e35fa6c245f92ea5"
dependencies = [
"base64-simd",
"hex-simd",
"llrt_build",
"memchr",
"phf",
]
[[package]]
name = "llrt_utils_watts"
version = "0.8.1-beta"
dependencies = [
"libc",
"llrt_build",
"rquickjs",
"tokio",
"tracing",
"windows-sys",
]
[[package]]
name = "memchr"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "outref"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e"
[[package]]
name = "phf"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
dependencies = [
"phf_macros",
"phf_shared",
]
[[package]]
name = "phf_generator"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737"
dependencies = [
"fastrand",
"phf_shared",
]
[[package]]
name = "phf_macros"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef"
dependencies = [
"phf_generator",
"phf_shared",
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "phf_shared"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266"
dependencies = [
"siphasher",
]
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "proc-macro-crate"
version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
dependencies = [
"toml_edit",
]
[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "relative-path"
version = "2.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38540a682c6e9bca04a8bca9e5ec5a6ba168dfbb04c9998fe517a9befb92eb68"
dependencies = [
"serde_core",
]
[[package]]
name = "rquickjs"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c50dc6d6c587c339edb4769cf705867497a2baf0eca8b4645fa6ecd22f02c77a"
dependencies = [
"rquickjs-core",
"rquickjs-macro",
]
[[package]]
name = "rquickjs-core"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8bf7840285c321c3ab20e752a9afb95548c75cd7f4632a0627cea3507e310c1"
dependencies = [
"async-lock",
"hashbrown 0.16.1",
"relative-path",
"rquickjs-sys",
]
[[package]]
name = "rquickjs-macro"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7106215ff41a5677b104906a13e1a440b880f4b6362b5dc4f3978c267fad2b80"
dependencies = [
"convert_case",
"fnv",
"ident_case",
"indexmap",
"proc-macro-crate",
"proc-macro2",
"quote",
"rquickjs-core",
"syn 2.0.119",
]
[[package]]
name = "rquickjs-sys"
version = "0.11.0"
dependencies = [
"cc",
]
[[package]]
name = "rustc_version"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [
"semver",
]
[[package]]
name = "ryu"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "sausage-cred-script"
version = "0.1.0"
dependencies = [
"llrt_buffer",
"rquickjs",
"serde",
"serde_json",
"thiserror",
"zeroize",
]
[[package]]
name = "semver"
version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
[[package]]
name = "serde"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
]
[[package]]
name = "serde_json"
version = "1.0.154"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "shlex"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]]
name = "siphasher"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256"
[[package]]
name = "syn"
version = "2.0.119"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "3.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "2.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
]
[[package]]
name = "tokio"
version = "1.54.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3"
dependencies = [
"pin-project-lite",
]
[[package]]
name = "toml_datetime"
version = "1.1.3+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "173f25c883cd2176a2d8212155638c0a60d484a9d84546d270866f40f56b6e49"
dependencies = [
"serde_core",
]
[[package]]
name = "toml_edit"
version = "0.25.19+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd43688c4858d7264ae1cf9a9f874a67c3feb74a883b1cd7bebd6a4ea8307218"
dependencies = [
"indexmap",
"toml_datetime",
"toml_parser",
"winnow",
]
[[package]]
name = "toml_parser"
version = "1.1.5+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c"
dependencies = [
"winnow",
]
[[package]]
name = "tracing"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
"pin-project-lite",
"tracing-core",
]
[[package]]
name = "tracing-core"
version = "0.1.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
[[package]]
name = "unicode-ident"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
[[package]]
name = "unicode-segmentation"
version = "1.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
[[package]]
name = "vsimd"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "winnow"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3b93199df0ab874cdf0c6a94f0a0dc3423fe41f1cafb62b8bd7b1deb277aef"
dependencies = [
"memchr",
]
[[package]]
name = "zeroize"
version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879"
dependencies = [
"serde",
]
[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
+19
View File
@@ -0,0 +1,19 @@
[package]
name = "sausage-cred-script"
version = "0.1.0"
edition = "2024"
publish = false
[workspace]
[dependencies]
rquickjs = { version = "=0.11.0", default-features = false, features = ["std"] }
llrt_buffer = "=0.8.1-beta"
thiserror = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
zeroize = { version = "1", features = ["serde"] }
[patch.crates-io]
rquickjs-sys = { path = "vendor/rquickjs-sys" }
llrt_utils_watts = { path = "vendor/llrt_utils_watts" }
+46
View File
@@ -0,0 +1,46 @@
# Credential script guest (D58)
This host-internal WASI component runs credential validators and transforms in QuickJS.
The host embeds `script.wasm`. It never exposes this component as a world program.
`src/lib.rs` ports v2's bounded `jsrun` evaluator.
`src/tests.rs` preserves its tests unchanged.
The vendored compatibility patches and licenses remain in `vendor/`.
`vendor/PATCHES.md` explains the compatibility fixes and memory-error classification.
Rebuild the checked-in component from the repository root:
```sh
CARGO_TARGET_DIR="$PWD/target/cred-guest" nix develop -c nu host/cred-script/build.nu
```
The script sets `CARGO_BUILD_JOBS=2` and uses the locked, separate guest workspace.
The development shell supplies the WASI C compiler and Rust target.
Run the unchanged evaluator tests natively:
```sh
CARGO_BUILD_JOBS=2 CARGO_TARGET_DIR="$PWD/target/cred-guest" nix develop -c cargo test --manifest-path host/cred-script/Cargo.toml --release --all-targets --locked
```
These tests use optimized C and Rust code, like the shipped guest.
Native debug builds exceed the 16 KiB interpreter stack.
The JSON protocol accepts one source module, input, export name, and two limits.
The reply contains a JSON result or a static error category.
Script exception text never leaves the guest.
The guest owns its input in zeroizing buffers before the QuickJS boundary.
The host compiles the component once and caches its compiled code.
Construct `cred::quickjs::QuickJs::default()` at startup and reuse it through `cred::script::Script`.
The injection API does not change.
Each call uses a fresh Store with linear memory limited to the clamped heap plus 8 MiB.
The margin covers the Rust stack, WASI adapter, and protocol buffers.
Only two evaluations run concurrently. Queue time counts against the deadline.
Epoch callbacks check each call's deadline independently.
The guest receives no directories, environment variables, or network access.
Its wall clock returns zero. Its random sources use a fixed seed.
Its monotonic clock serves the evaluator's internal budget only.
JavaScript receives no clocks, random function, module loader, or host interfaces.
`Error.prepareStackTrace` is disabled and immutable. Errors retain plain stack strings.
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env nu
# Rebuild the checked-in credential script component with the pinned guest workspace.
let root = ($env.FILE_PWD | path join ../.. | path expand)
let target = ($env.CARGO_TARGET_DIR? | default ($root | path join target cred-script) | path expand)
with-env {CARGO_BUILD_JOBS: '2', CARGO_TARGET_DIR: $target} {
^cargo build --manifest-path ($env.FILE_PWD | path join Cargo.toml) --target wasm32-wasip2 --release --locked
if $env.LAST_EXIT_CODE != 0 { error make {msg: 'The credential script guest build failed.'} }
}
cp ($target | path join wasm32-wasip2 release sausage-cred-script.wasm) ($env.FILE_PWD | path join script.wasm)
Binary file not shown.
+243
View File
@@ -0,0 +1,243 @@
//! Bounded, stateless ES modules with JSON text boundaries. No host capabilities.
use rquickjs::{Context, Ctx, Function, Module, Promise, Runtime, Value, context::intrinsic};
use std::{
cell::Cell,
rc::Rc,
time::{Duration, Instant},
};
/// Requested limits. Values are clamped by the evaluator, including zero.
#[derive(Clone, Copy, Debug, Default)]
pub struct Limits {
pub max_time_ms: Option<u64>,
pub max_memory_bytes: Option<usize>,
pub max_stack_bytes: Option<usize>,
}
impl Limits {
/// Effective time (1..1000ms), heap (256KiB..32MiB), stack (4..16KiB).
pub fn clamped(self) -> Self {
Self {
max_time_ms: Some(self.max_time_ms.unwrap_or(100).clamp(1, 1000)),
max_memory_bytes: Some(
self.max_memory_bytes
.unwrap_or(8 * 1024 * 1024)
.clamp(256 * 1024, 32 * 1024 * 1024),
),
max_stack_bytes: Some(
self.max_stack_bytes
.unwrap_or(16 * 1024)
.clamp(4 * 1024, 16 * 1024),
),
}
}
}
/// Stable error categories, independent of QuickJS's exception wording.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorKind {
Limit,
Syntax,
Throw,
NonJson,
Engine,
}
/// An evaluation failure. Error text can include script-provided messages.
#[derive(Debug, thiserror::Error)]
#[error("{message}")]
pub struct Error {
pub kind: ErrorKind,
pub message: String,
}
fn failure(kind: ErrorKind, message: impl Into<String>) -> Error {
Error {
kind,
message: message.into(),
}
}
struct Budget {
start: Instant,
time: Duration,
ticks: Cell<u32>,
limited: Cell<bool>,
}
impl Budget {
// A finite job/callback allowance is also needed for a stream of tiny jobs
// and for hosts with a stalled monotonic clock. Not an instruction count.
fn step(&self) -> bool {
self.ticks.set(self.ticks.get().saturating_add(1));
let stop = self.ticks.get() >= 100_000 || self.start.elapsed() >= self.time;
self.limited.set(self.limited.get() || stop);
self.limited.get()
}
fn check(&self) -> Result<(), Error> {
if self.step() {
Err(failure(
ErrorKind::Limit,
"JavaScript execution limit exceeded",
))
} else {
Ok(())
}
}
}
unsafe extern "C" {
fn sausage_js_had_oom(ctx: *mut std::ffi::c_void) -> std::ffi::c_int;
}
fn memory_limited(ctx: &Ctx<'_>) -> bool {
// The live Ctx owns this context and holds the runtime lock.
unsafe { sausage_js_had_oom(ctx.as_raw().as_ptr().cast()) != 0 }
}
fn js_error(ctx: &Ctx<'_>, err: rquickjs::Error, phase: ErrorKind, budget: &Budget) -> Error {
if memory_limited(ctx) {
return failure(ErrorKind::Limit, "JavaScript memory limit exceeded");
}
if budget.limited.get() {
return failure(ErrorKind::Limit, "JavaScript execution limit exceeded");
}
let mut kind = phase;
let mut message = err.to_string();
if matches!(err, rquickjs::Error::Exception) {
let thrown = ctx.catch();
if let Some(object) = thrown.as_object() {
if let Ok(text) = object.get::<_, String>("message") {
message = text;
}
// Only genuine engine Error instances determine resource/syntax kinds;
// plain thrown objects remain throws even if they mimic the message.
if thrown.is_error() {
if message.contains("out of memory") || message.contains("stack size exceeded") {
kind = ErrorKind::Limit;
} else if object
.get::<_, String>("name")
.is_ok_and(|n| n == "SyntaxError")
{
kind = ErrorKind::Syntax;
}
}
} else if let Some(text) = thrown.as_string()
&& let Ok(text) = text.to_string()
{
message = text;
}
} else if matches!(err, rquickjs::Error::Allocation) {
kind = ErrorKind::Limit;
}
if memory_limited(ctx) {
return failure(
ErrorKind::Limit,
"JavaScript memory limit exceeded while reading error",
);
}
if budget.check().is_err() {
return failure(
ErrorKind::Limit,
"JavaScript execution limit exceeded while reading error",
);
}
failure(kind, message)
}
fn settle<'js>(
ctx: &Ctx<'js>,
promise: Promise<'js>,
budget: &Budget,
) -> Result<Value<'js>, Error> {
loop {
budget.check()?;
if let Some(result) = promise.result::<Value>() {
return result.map_err(|e| js_error(ctx, e, ErrorKind::Throw, budget));
}
// This is synchronous queue draining, not polling an external resource.
if !ctx.execute_pending_job() {
return Err(failure(
ErrorKind::Engine,
"Promise is pending with no runnable jobs (no ambient async capabilities)",
));
}
}
}
/// Compile an ES module, call its selected export (default `default`) with parsed
/// JSON, settle its returned Promise, and JSON.stringify the result. Fresh runtime
/// each call. Only LLRT's atob/btoa globals are retained, with no module loader
/// or ambient host capabilities. JSON.stringify
/// semantics apply (unsupported object properties omitted, nonfinite => null).
/// Limits cover compilation, execution, job draining and serialization; QuickJS
/// interrupts are cooperative, not preemptive deadlines for every C builtin.
pub fn evaluate(
script: &str,
input: &str,
export: Option<&str>,
limits: Limits,
) -> Result<String, Error> {
let limits = limits.clamped();
let budget = Rc::new(Budget {
start: Instant::now(),
time: Duration::from_millis(limits.max_time_ms.unwrap_or(100)),
ticks: Cell::new(0),
limited: Cell::new(false),
});
let rt = Runtime::new().map_err(|e| failure(ErrorKind::Engine, e.to_string()))?;
rt.set_memory_limit(limits.max_memory_bytes.unwrap_or(8 * 1024 * 1024));
rt.set_max_stack_size(limits.max_stack_bytes.unwrap_or(16 * 1024));
let cb = budget.clone();
rt.set_interrupt_handler(Some(Box::new(move || cb.step())));
// LLRT's BasePrimordials needs Date during initialization, as well as the
// pure collection/regexp/typed-array intrinsics. Date is removed before
// user code runs; no network, filesystem, process or timer modules load.
let context = Context::custom::<(
intrinsic::Eval,
intrinsic::Json,
intrinsic::Promise,
intrinsic::Date,
intrinsic::MapSet,
intrinsic::RegExp,
intrinsic::Proxy,
intrinsic::TypedArrays,
)>(&rt)
.map_err(|e| failure(ErrorKind::Limit, format!("context creation failed: {e}")))?;
context.with(|ctx| {
llrt_buffer::init(&ctx)
.map_err(|e| js_error(&ctx, e, ErrorKind::Engine, &budget))?;
// Retain only atob/btoa from the published buffer initializer. Buffer's
// external allocations bypass the heap limit; Blob/File are unnecessary
// and File exposes the clock. Buffer has both a global lexical class
// binding and a globalThis property: clear both. Date is init-only.
// Disable QuickJS's structured stack hook before it can expose call sites.
ctx.eval::<(), _>("Object.defineProperty(Error, 'prepareStackTrace', { value: undefined, writable: false, configurable: false }); Buffer = undefined; delete globalThis.Buffer; delete Math.random; delete globalThis.Date; delete globalThis.Blob; delete globalThis.File; for (const name of ['WebAssembly', 'SharedArrayBuffer', 'Atomics', 'Intl', '__infra', '__bootstrap', 'queueMicrotask']) delete globalThis[name];")
.map_err(|e| js_error(&ctx, e, ErrorKind::Engine, &budget))?;
let input = ctx
.json_parse(input)
.map_err(|e| js_error(&ctx, e, ErrorKind::Syntax, &budget))?;
let module = Module::declare(ctx.clone(), "jsrun:entry", script)
.map_err(|e| js_error(&ctx, e, ErrorKind::Syntax, &budget))?;
let (module, ready) = module
.eval()
.map_err(|e| js_error(&ctx, e, ErrorKind::Throw, &budget))?;
settle(&ctx, ready, &budget)?;
let function: Function = module
.get(export.unwrap_or("default"))
.map_err(|e| js_error(&ctx, e, ErrorKind::Engine, &budget))?;
let mut value: Value = function
.call((input,))
.map_err(|e| js_error(&ctx, e, ErrorKind::Throw, &budget))?;
if let Some(promise) = value.as_promise() {
value = settle(&ctx, promise.clone(), &budget)?;
}
let text = ctx
.json_stringify(value)
.map_err(|e| js_error(&ctx, e, ErrorKind::NonJson, &budget))?
.ok_or_else(|| {
failure(
ErrorKind::NonJson,
"result is undefined, a function, or otherwise not JSON serializable",
)
})?
.to_string()
.map_err(|e| js_error(&ctx, e, ErrorKind::NonJson, &budget))?;
budget.check()?;
Ok(text)
})
}
#[cfg(test)]
mod tests;
+58
View File
@@ -0,0 +1,58 @@
//! One JSON call over stdin/stdout. Script exception text never leaves this guest.
use sausage_cred_script::{ErrorKind, Limits, evaluate};
use std::io::{Read, Write};
use zeroize::Zeroizing;
#[derive(serde::Deserialize)]
struct Request {
source: String,
input: Zeroizing<String>,
export: Option<String>,
max_time_ms: u64,
max_memory_bytes: usize,
}
fn run() -> Result<(), Box<dyn std::error::Error>> {
let mut wire = Zeroizing::new(String::new());
std::io::stdin().read_to_string(&mut wire)?;
let request: Request = serde_json::from_str(&wire)?;
let result = evaluate(
&request.source,
&request.input,
request.export.as_deref(),
Limits {
max_time_ms: Some(request.max_time_ms),
max_memory_bytes: Some(request.max_memory_bytes),
..Limits::default()
},
);
let response = match result {
Ok(result) => serde_json::json!({"result": result}),
Err(error) => serde_json::json!({"kind": match error.kind {
ErrorKind::Limit => "Limit",
ErrorKind::Syntax => "Syntax",
ErrorKind::Throw => "Throw",
ErrorKind::NonJson => "NonJson",
ErrorKind::Engine => "Engine",
}}),
};
serde_json::to_writer(std::io::stdout().lock(), &response)?;
std::io::stdout().flush()?;
Ok(())
}
fn main() {
if run().is_err() {
std::process::exit(1);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn request_input_has_zeroizing_ownership() {
let request: Request = serde_json::from_str(r#"{"source":"export default x=>x","input":"null","export":null,"max_time_ms":100,"max_memory_bytes":8388608}"#).unwrap();
let _: &Zeroizing<String> = &request.input;
}
}
+196
View File
@@ -0,0 +1,196 @@
use super::*;
fn run(s: &str) -> Result<String, Error> {
evaluate(s, "null", None, Limits::default())
}
#[test]
fn module_default_and_named() {
assert_eq!(
evaluate(
"export default x => ({value:x}); export const other=x=>x+1",
"42",
None,
Limits::default()
)
.unwrap(),
"{\"value\":42}"
);
assert_eq!(
evaluate(
"export const other=x=>x+1",
"42",
Some("other"),
Limits::default()
)
.unwrap(),
"43"
);
assert_eq!(run("export default x=>null").unwrap(), "null");
}
#[test]
fn promise_settlement() {
assert_eq!(
run("export default async x => { await Promise.resolve(); return 42; }").unwrap(),
"42"
);
assert_eq!(
run("await Promise.resolve(); export default x=>true").unwrap(),
"true"
);
for s in [
"export default x=>Promise.reject(new Error('rejected'))",
"export default async x=>{ throw 'rejected'; }",
"await Promise.reject('rejected'); export default x=>true",
] {
let e = run(s).unwrap_err();
assert_eq!(e.kind, ErrorKind::Throw);
assert_eq!(e.message, "rejected");
}
assert_eq!(
run("export default x=>new Promise(()=>{})")
.unwrap_err()
.kind,
ErrorKind::Engine
);
}
#[test]
fn typed_errors() {
assert_eq!(run("export default (").unwrap_err().kind, ErrorKind::Syntax);
assert_eq!(
run("export default x=>{throw new Error('oops')}")
.unwrap_err()
.kind,
ErrorKind::Throw
);
for s in [
"export default x=>undefined",
"export default x=>(()=>1)",
"export default x=>{let a={};a.a=a;return a}",
"export default x=>Symbol()",
] {
assert_eq!(run(s).unwrap_err().kind, ErrorKind::NonJson);
}
assert_eq!(
evaluate("export default x=>x", "not json", None, Limits::default())
.unwrap_err()
.kind,
ErrorKind::Syntax
);
assert_eq!(run("export const x=1").unwrap_err().kind, ErrorKind::Engine);
assert_eq!(run("export default 1").unwrap_err().kind, ErrorKind::Engine);
}
#[test]
fn recursion_is_bounded() {
for stack in [4096, 16384, usize::MAX] {
let e = evaluate(
"export default x=>{function f(){return f()+1}return f()}",
"null",
None,
Limits {
max_stack_bytes: Some(stack),
..Limits::default()
},
)
.unwrap_err();
eprintln!("STACK {stack}: {e:?}");
assert_eq!(e.kind, ErrorKind::Limit);
assert!(e.message.contains("stack"));
}
}
#[test]
fn runaway_loop_and_jobs() {
for s in [
"export default x=>{while(true){}}",
"while(true){};export default x=>x",
"export default x=>({toJSON(){while(true){}}})",
"export default x=>{throw {get message(){while(true){}}}}",
"export default x=>new Promise(()=>{function f(){Promise.resolve().then(f)};f()})",
] {
let start = Instant::now();
assert_eq!(run(s).unwrap_err().kind, ErrorKind::Limit);
assert!(start.elapsed() < Duration::from_secs(2));
}
}
#[test]
fn runaway_allocation() {
let e = evaluate(
"export default x=>{let a=[];while(1)a.push('abcdefghijklmnopqrstuvwxyz'+a.length)}",
"null",
None,
Limits {
max_time_ms: Some(1000),
max_memory_bytes: Some(1024 * 1024),
..Limits::default()
},
)
.unwrap_err();
eprintln!("HEAP {e:?}");
assert_eq!(e.kind, ErrorKind::Limit);
assert!(e.message.contains("memory"));
}
#[test]
fn forbidden_globals_and_imports() {
for name in [
"Date",
"performance",
"Math.random",
"fetch",
"XMLHttpRequest",
"require",
"process",
"std",
"os",
"fs",
"crypto",
"console",
"setTimeout",
] {
assert_eq!(
run(&format!("export default x=>typeof {name}")).unwrap(),
"\"undefined\"",
"{name}"
);
}
assert!(run("import x from 'fs';export default x=>x").is_err());
assert_eq!(
run("export default x=>import('fs')").unwrap_err().kind,
ErrorKind::Throw
);
}
#[test]
fn isolation_and_limits() {
run("export default x=>{globalThis.leak=42;return true}").unwrap();
assert_eq!(
run("export default x=>typeof leak").unwrap(),
"\"undefined\""
);
assert_eq!(
Limits {
max_time_ms: Some(u64::MAX),
max_memory_bytes: Some(usize::MAX),
max_stack_bytes: Some(usize::MAX)
}
.clamped()
.max_time_ms,
Some(1000)
);
assert_eq!(
Limits {
max_time_ms: Some(0),
..Limits::default()
}
.clamped()
.max_time_ms,
Some(1)
);
}
#[test]
fn timings() {
let start = Instant::now();
assert_eq!(run("export default x=>true").unwrap(), "true");
eprintln!("TIMING first eval {}us", start.elapsed().as_micros());
let start = Instant::now();
for _ in 0..100 {
assert_eq!(run("export default x=>true").unwrap(), "true");
}
eprintln!("TIMING 100 evals {}us", start.elapsed().as_micros());
}
+50
View File
@@ -0,0 +1,50 @@
# Local compatibility patches
## LLRT utilities (SAU-330)
`llrt_buffer = 0.8.1-beta` and `llrt_encoding` remain published crate dependencies,
matching rquickjs 0.11 without changing the engine/vendor patches below.
The published `llrt_utils_watts = 0.8.1-beta` does not compile for wasm32-wasip2:
`signals::kill` unconditionally references `kill_process_raw` and `is_not_found`,
but those are only defined on Unix/Windows. The failed unpatched build is archived
at `/logs/cargo/2026-10-28/01a184d5-151b-7454-8333-b5d1d468b6d6.stderr.log`.
Fallback: `llrt_utils_watts/` is the unchanged published crate archive from
https://static.crates.io/crates/llrt_utils_watts/llrt_utils_watts-0.8.1-beta.crate
(SHA256 `5ace6f63a6b60a3bf738bf19f393c834c83a06d9e52c31d1c30a526584519569`),
except `src/lib.rs` adds `#[cfg(any(unix, windows))]` to `pub mod signals`.
The root Cargo patch points here. No utility module is registered as a JS module.
The upstream Apache-2.0 license is included (absent in the published archive).
This build-only patch does not alter Buffer. Before user module initialization,
the evaluator clears Buffer's global lexical binding and deletes its global-object
property, and removes Blob, File and Date, retaining only atob/btoa from the module.
SAU-330's mediator decision excludes the Node Buffer API rather than patching
its external-allocation heap bypass or empty-fill panic. Tests cover absent
identifiers, globalThis properties and global own-property names, plus base64
errors and clean near-heap-limit failure. See
`/docs/symphony/artifacts/SAU-330/implementation.md` for evidence.
## QuickJS (SAU-103)
`rquickjs-sys` was downloaded unchanged from https://crates.io/api/v1/crates/rquickjs-sys/0.11.0/download, then changed in exactly two files:
- `quickjs/quickjs.c`, `update_stack_limit`: remove the `__wasi__` special case that sets `stack_limit = 0` regardless of `JS_SetMaxStackSize`. Use the normal stack-top-minus-size implementation on WASI too. Without this patch the recursion test traps the WASM instance even with 16KiB / 256KiB limits. With the patch it returns `Maximum call stack size exceeded`.
- `build.rs`: skip upstream WASI SDK download/CC override when `CC_wasm32_wasip2` is supplied, letting `cc` honor cargo.build's target-specific CC/AR. Also emit the selected compiler/args as a diagnostic.
No engine features or host APIs were added. Keep this patch when reusing the evaluator; setting the Rust stack-limit API alone is **not sufficient** with unmodified rquickjs-sys 0.11.0. Upstream 0.14.0's source was inspected (not compiled) and still contains the WASI stack-limit bypass.
The bundled sources retain their upstream notices/licenses. This is a spike-local vendoring choice, not an SDK or live toolchain change.
## Sausage memory-error classification (D58)
`quickjs.c` records a sticky flag when `JS_ThrowOutOfMemory` runs.
`sausage_js_had_oom` exposes that flag to the guest's Rust evaluator.
`quickjs.h` declares this guest-internal function.
`build.rs` watches the QuickJS source directory, so Cargo rebuilds the library after a patch.
It adds no JavaScript API or host import.
A full heap can prevent QuickJS from creating its error object.
The unchanged `runaway_allocation` test reproduced that case and received `Throw` instead of `Limit`.
The flag preserves the memory-error category without another allocation.
Each evaluation creates a fresh runtime, so it also resets the flag.
@@ -0,0 +1,7 @@
{
"git": {
"sha1": "2946a77260e8cdd314f92a9789635cd4f7b825bf",
"dirty": true
},
"path_in_vcs": "libs/llrt_utils"
}
+879
View File
@@ -0,0 +1,879 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "anyhow"
version = "1.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea"
[[package]]
name = "async-lock"
version = "3.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
dependencies = [
"event-listener",
"event-listener-strategy",
"pin-project-lite",
]
[[package]]
name = "bitflags"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
[[package]]
name = "bytes"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]]
name = "cc"
version = "1.2.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "concurrent-queue"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "convert_case"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.61.2",
]
[[package]]
name = "event-listener"
version = "5.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
dependencies = [
"concurrent-queue",
"pin-project-lite",
]
[[package]]
name = "event-listener-strategy"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
dependencies = [
"event-listener",
"pin-project-lite",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foldhash"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "foldhash"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
name = "getrandom"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasip2",
"wasip3",
]
[[package]]
name = "hashbrown"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"foldhash 0.1.5",
]
[[package]]
name = "hashbrown"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash 0.2.0",
]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "id-arena"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "indexmap"
version = "2.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
dependencies = [
"equivalent",
"hashbrown 0.16.1",
"serde",
"serde_core",
]
[[package]]
name = "itoa"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "leb128fmt"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "libc"
version = "0.2.182"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
[[package]]
name = "llrt_build"
version = "0.8.1-beta"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e770e9f9d0031d5788ab2f73254cdc9c516042b55db8bddf64d2f409b7015f4a"
dependencies = [
"rustc_version",
]
[[package]]
name = "llrt_test"
version = "0.8.1-beta"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44ff8cf3b62144ca17b132f7fedf983d935573684a140f5e7a5f81a06b215665"
dependencies = [
"rquickjs",
"tokio",
"uuid",
]
[[package]]
name = "llrt_utils_watts"
version = "0.8.1-beta"
dependencies = [
"libc",
"llrt_build",
"llrt_test",
"rquickjs",
"tokio",
"tracing",
"windows-sys 0.61.2",
]
[[package]]
name = "lock_api"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "memchr"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "mio"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
dependencies = [
"libc",
"wasi",
"windows-sys 0.61.2",
]
[[package]]
name = "parking_lot"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-link",
]
[[package]]
name = "pin-project-lite"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "prettyplease"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn",
]
[[package]]
name = "proc-macro-crate"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983"
dependencies = [
"toml_edit",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "redox_syscall"
version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
"bitflags",
]
[[package]]
name = "relative-path"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bca40a312222d8ba74837cb474edef44b37f561da5f773981007a10bbaa992b0"
dependencies = [
"serde",
]
[[package]]
name = "rquickjs"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c50dc6d6c587c339edb4769cf705867497a2baf0eca8b4645fa6ecd22f02c77a"
dependencies = [
"rquickjs-core",
"rquickjs-macro",
]
[[package]]
name = "rquickjs-core"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8bf7840285c321c3ab20e752a9afb95548c75cd7f4632a0627cea3507e310c1"
dependencies = [
"async-lock",
"hashbrown 0.16.1",
"relative-path",
"rquickjs-sys",
]
[[package]]
name = "rquickjs-macro"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7106215ff41a5677b104906a13e1a440b880f4b6362b5dc4f3978c267fad2b80"
dependencies = [
"convert_case",
"fnv",
"ident_case",
"indexmap",
"proc-macro-crate",
"proc-macro2",
"quote",
"rquickjs-core",
"syn",
]
[[package]]
name = "rquickjs-sys"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27344601ef27460e82d6a4e1ecb9e7e99f518122095f3c51296da8e9be2b9d83"
dependencies = [
"cc",
]
[[package]]
name = "rustc_version"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [
"semver",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "semver"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook-registry"
version = "1.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
dependencies = [
"errno",
"libc",
]
[[package]]
name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "socket2"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0"
dependencies = [
"libc",
"windows-sys 0.60.2",
]
[[package]]
name = "syn"
version = "2.0.115"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e614ed320ac28113fa64972c4262d5dbc89deacdfd00c34a3e4cea073243c12"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tokio"
version = "1.49.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
dependencies = [
"bytes",
"libc",
"mio",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys 0.61.2",
]
[[package]]
name = "tokio-macros"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "toml_datetime"
version = "0.7.5+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
dependencies = [
"serde_core",
]
[[package]]
name = "toml_edit"
version = "0.23.10+spec-1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269"
dependencies = [
"indexmap",
"toml_datetime",
"toml_parser",
"winnow",
]
[[package]]
name = "toml_parser"
version = "1.0.8+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0742ff5ff03ea7e67c8ae6c93cac239e0d9784833362da3f9a9c1da8dfefcbdc"
dependencies = [
"winnow",
]
[[package]]
name = "tracing"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
"pin-project-lite",
"tracing-core",
]
[[package]]
name = "tracing-core"
version = "0.1.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
[[package]]
name = "unicode-ident"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e"
[[package]]
name = "unicode-segmentation"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "unicode-xid"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "uuid"
version = "1.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb"
dependencies = [
"getrandom",
]
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasip2"
version = "1.0.2+wasi-0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "wasip3"
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "wasm-encoder"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
dependencies = [
"leb128fmt",
"wasmparser",
]
[[package]]
name = "wasm-metadata"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
dependencies = [
"anyhow",
"indexmap",
"wasm-encoder",
"wasmparser",
]
[[package]]
name = "wasmparser"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
dependencies = [
"bitflags",
"hashbrown 0.15.5",
"indexmap",
"semver",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-targets"
version = "0.53.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
dependencies = [
"windows-link",
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]]
name = "windows_i686_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]]
name = "windows_i686_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
[[package]]
name = "winnow"
version = "0.7.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
dependencies = [
"memchr",
]
[[package]]
name = "wit-bindgen"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
dependencies = [
"wit-bindgen-rust-macro",
]
[[package]]
name = "wit-bindgen-core"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
dependencies = [
"anyhow",
"heck",
"wit-parser",
]
[[package]]
name = "wit-bindgen-rust"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
dependencies = [
"anyhow",
"heck",
"indexmap",
"prettyplease",
"syn",
"wasm-metadata",
"wit-bindgen-core",
"wit-component",
]
[[package]]
name = "wit-bindgen-rust-macro"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
dependencies = [
"anyhow",
"prettyplease",
"proc-macro2",
"quote",
"syn",
"wit-bindgen-core",
"wit-bindgen-rust",
]
[[package]]
name = "wit-component"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
dependencies = [
"anyhow",
"bitflags",
"indexmap",
"log",
"serde",
"serde_derive",
"serde_json",
"wasm-encoder",
"wasm-metadata",
"wasmparser",
"wit-parser",
]
[[package]]
name = "wit-parser"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
dependencies = [
"anyhow",
"id-arena",
"indexmap",
"log",
"semver",
"serde",
"serde_derive",
"serde_json",
"unicode-xid",
"wasmparser",
]
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+74
View File
@@ -0,0 +1,74 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2021"
name = "llrt_utils_watts"
version = "0.8.1-beta"
build = false
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "LLRT utilities"
readme = false
license = "Apache-2.0"
repository = "https://github.com/awslabs/llrt"
[features]
all = [
"fs",
"bytearray-buffer",
]
bytearray-buffer = ["tokio/sync"]
default = ["all"]
fs = ["tokio/fs"]
[lib]
name = "llrt_utils_watts"
path = "src/lib.rs"
[dependencies.rquickjs]
version = "0.11"
features = ["macro"]
default-features = false
[dependencies.tokio]
version = "1"
features = ["sync"]
default-features = false
[dependencies.tracing]
version = "0.1"
default-features = false
[dev-dependencies.llrt_test]
version = "0.8.1-beta"
[dev-dependencies.tokio]
version = "1"
features = ["full"]
[build-dependencies.llrt_build]
version = "0.8.1-beta"
[target."cfg(unix)".dependencies.libc]
version = "0.2"
default-features = false
[target."cfg(windows)".dependencies.windows-sys]
version = "0.61"
features = [
"Win32_Foundation",
"Win32_System_Threading",
]
default-features = false
+34
View File
@@ -0,0 +1,34 @@
[package]
name = "llrt_utils_watts"
description = "LLRT utilities"
version = "0.8.1-beta"
edition = "2021"
license = "Apache-2.0"
repository = "https://github.com/awslabs/llrt"
[features]
default = ["all"]
all = ["fs", "bytearray-buffer"]
fs = ["tokio/fs"]
bytearray-buffer = ["tokio/sync"]
[dependencies]
rquickjs = { version = "0.11", features = [
"macro",
], default-features = false }
tokio = { version = "1", features = ["sync"], default-features = false }
tracing = { version = "0.1", default-features = false }
[target.'cfg(unix)'.dependencies]
libc = { version = "0.2", default-features = false }
[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_System_Threading"], default-features = false }
[dev-dependencies]
llrt_test = { version = "0.8.1-beta", path = "../llrt_test" }
tokio = { version = "1", features = ["full"] }
[build-dependencies]
llrt_build = { version = "0.8.1-beta", path = "../llrt_build" }
+202
View File
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+298
View File
@@ -0,0 +1,298 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
use rquickjs::{
class::{Trace, Tracer},
Ctx, FromJs, IntoJs, JsLifetime, Result, Value,
};
macro_rules! define_any_of {
($name:ident, $($variant:ident),+) => {
#[derive(Debug, Clone)]
pub enum $name<$($variant),+> {
$(
$variant($variant),
)+
}
define_any_of_from_js!($name, $($variant),+);
impl<'js, $($variant: IntoJs<'js>),+> IntoJs<'js> for $name<$($variant),+> {
fn into_js(self, ctx: &Ctx<'js>) -> Result<Value<'js>> {
match self {
$(
Self::$variant(val) => val.into_js(ctx),
)+
}
}
}
unsafe impl<'js, $($variant: JsLifetime<'js>),+> JsLifetime<'js> for $name<$($variant),+> {
type Changed<'to> = $name<$($variant::Changed<'to>),+>;
}
impl<'js, $($variant: Trace<'js>),+> Trace<'js> for $name<$($variant),+> {
fn trace<'a>(&self, tracer: Tracer<'a, 'js>) {
match self {
$(
Self::$variant(val) => val.trace(tracer),
)+
}
}
}
define_any_of_methods!($name, $($variant),+);
};
}
macro_rules! define_any_of_from_js {
($name:ident, $first:ident, $($rest:ident),+) => {
impl<'js, $first: FromJs<'js>, $($rest: FromJs<'js>),+> FromJs<'js> for $name<$first, $($rest),+> {
fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result<Self> {
define_any_of_from_js_impl!($name, ctx, value, $first, $($rest),+)
}
}
};
}
macro_rules! define_any_of_from_js_impl {
($name:ident, $ctx:ident, $value:ident, $first:ident) => {
$first::from_js($ctx, $value).map($name::$first)
};
($name:ident, $ctx:ident, $value:ident, $first:ident, $($rest:ident),+) => {
$first::from_js($ctx, $value.clone()).map($name::$first).or_else(|error| {
if error.is_from_js() {
define_any_of_from_js_impl!($name, $ctx, $value, $($rest),+)
} else {
Err(error)
}
})
};
}
macro_rules! define_any_of_variant_methods {
($variant:ident, $is_fn:ident, $as_fn:ident, $as_mut_fn:ident, $into_fn:ident) => {
#[allow(dead_code)]
pub fn $is_fn(&self) -> bool {
matches!(self, Self::$variant(_))
}
#[allow(dead_code)]
pub fn $as_fn(&self) -> Option<&$variant> {
match self {
Self::$variant(val) => Some(val),
_ => None,
}
}
#[allow(dead_code)]
pub fn $as_mut_fn(&mut self) -> Option<&mut $variant> {
match self {
Self::$variant(val) => Some(val),
_ => None,
}
}
#[allow(dead_code)]
pub fn $into_fn(self) -> std::result::Result<$variant, Self> {
match self {
Self::$variant(val) => Ok(val),
other => Err(other),
}
}
};
(A) => {
define_any_of_variant_methods!(A, is_a, as_a, as_a_mut, into_a);
};
(B) => {
define_any_of_variant_methods!(B, is_b, as_b, as_b_mut, into_b);
};
(C) => {
define_any_of_variant_methods!(C, is_c, as_c, as_c_mut, into_c);
};
(D) => {
define_any_of_variant_methods!(D, is_d, as_d, as_d_mut, into_d);
};
(E) => {
define_any_of_variant_methods!(E, is_e, as_e, as_e_mut, into_e);
};
(F) => {
define_any_of_variant_methods!(F, is_f, as_f, as_f_mut, into_f);
};
(G) => {
define_any_of_variant_methods!(G, is_g, as_g, as_g_mut, into_g);
};
(H) => {
define_any_of_variant_methods!(H, is_h, as_h, as_h_mut, into_h);
};
}
macro_rules! define_any_of_methods {
($name:ident, $($variant:ident),+) => {
impl<$($variant),+> $name<$($variant),+> {
$(
define_any_of_variant_methods!($variant);
)+
}
};
}
define_any_of!(AnyOf2, A, B);
define_any_of!(AnyOf3, A, B, C);
define_any_of!(AnyOf4, A, B, C, D);
define_any_of!(AnyOf5, A, B, C, D, E);
define_any_of!(AnyOf6, A, B, C, D, E, F);
define_any_of!(AnyOf7, A, B, C, D, E, F, G);
define_any_of!(AnyOf8, A, B, C, D, E, F, G, H);
#[cfg(test)]
mod tests {
use super::*;
use rquickjs::{Context, Runtime};
#[test]
fn test_any_of_string_number() {
let rt = Runtime::new().unwrap();
let ctx = Context::full(&rt).unwrap();
ctx.with(|ctx| {
// Test string conversion
let val: Value = ctx.eval("'hello'").unwrap();
let any: AnyOf2<String, i32> = AnyOf2::from_js(&ctx, val).unwrap();
assert!(any.is_a());
assert_eq!(any.as_a().unwrap(), "hello");
assert!(!any.is_b());
assert!(any.as_b().is_none());
// Test number conversion
let val: Value = ctx.eval("42").unwrap();
let any: AnyOf2<String, i32> = AnyOf2::from_js(&ctx, val).unwrap();
assert!(!any.is_a());
assert!(any.is_b());
assert_eq!(*any.as_b().unwrap(), 42);
});
}
#[test]
fn test_any_of_fallback() {
let rt = Runtime::new().unwrap();
let ctx = Context::full(&rt).unwrap();
ctx.with(|ctx| {
// Test that it tries in order
let val: Value = ctx.eval("true").unwrap();
let any: AnyOf3<String, i32, bool> = AnyOf3::from_js(&ctx, val).unwrap();
assert!(any.is_c());
assert!(*any.as_c().unwrap());
assert!(!any.is_a());
assert!(!any.is_b());
});
}
#[test]
fn test_any_of_into_js() {
let rt = Runtime::new().unwrap();
let ctx = Context::full(&rt).unwrap();
ctx.with(|ctx| {
let any: AnyOf2<String, i32> = AnyOf2::A("test".to_string());
let val: Value = any.into_js(&ctx).unwrap();
let result: String = val.get().unwrap();
assert_eq!(result, "test");
let any: AnyOf2<String, i32> = AnyOf2::B(99);
let val: Value = any.into_js(&ctx).unwrap();
let result: i32 = val.get().unwrap();
assert_eq!(result, 99);
});
}
#[test]
fn test_any_of_methods() {
let rt = Runtime::new().unwrap();
let ctx = Context::full(&rt).unwrap();
ctx.with(|ctx| {
// Test all methods for variant A
let val: Value = ctx.eval("'test'").unwrap();
let any: AnyOf3<String, i32, bool> = AnyOf3::from_js(&ctx, val).unwrap();
assert!(any.is_a());
assert_eq!(any.as_a().unwrap(), "test");
assert_eq!(any.into_a().unwrap(), "test");
// Test all methods for variant B
let val: Value = ctx.eval("42").unwrap();
let any: AnyOf3<String, i32, bool> = AnyOf3::from_js(&ctx, val).unwrap();
assert!(any.is_b());
assert_eq!(*any.as_b().unwrap(), 42);
assert_eq!(any.into_b().unwrap(), 42);
// Test all methods for variant C
let val: Value = ctx.eval("true").unwrap();
let any: AnyOf3<String, i32, bool> = AnyOf3::from_js(&ctx, val).unwrap();
assert!(any.is_c());
assert!(*any.as_c().unwrap());
assert!(any.into_c().unwrap());
});
}
#[test]
fn test_any_of_mutable_methods() {
let rt = Runtime::new().unwrap();
let ctx = Context::full(&rt).unwrap();
ctx.with(|ctx| {
let val: Value = ctx.eval("42").unwrap();
let mut any: AnyOf4<String, i32, bool, f64> = AnyOf4::from_js(&ctx, val).unwrap();
if let Some(n) = any.as_b_mut() {
*n = 100;
}
assert_eq!(any.into_b().unwrap(), 100);
});
}
#[test]
fn test_any_of_error_propagation() {
use rquickjs::{Array, Object};
let rt = Runtime::new().unwrap();
let ctx = Context::full(&rt).unwrap();
ctx.with(|ctx| {
// Test that conversion errors cause fallback to next type
let val: Value = ctx.eval("42").unwrap();
let any: AnyOf2<String, i32> = AnyOf2::from_js(&ctx, val).unwrap();
assert!(any.is_b());
// Test that all types fail results in an error
let val: Value = ctx.eval("null").unwrap();
let result: Result<AnyOf2<Object, Array>> = AnyOf2::from_js(&ctx, val);
assert!(result.is_err());
});
}
#[test]
fn test_any_of_conversion_order() {
let rt = Runtime::new().unwrap();
let ctx = Context::full(&rt).unwrap();
ctx.with(|ctx| {
// Test that conversion happens in order A, B, C, D, E
// Since 42 can be converted to f64, i32, etc., but String comes first and fails,
// it should try the next successful conversion
let val: Value = ctx.eval("42").unwrap();
// String should fail, so it tries i32 which succeeds
let any: AnyOf3<String, i32, f64> = AnyOf3::from_js(&ctx, val).unwrap();
assert!(any.is_b());
// If we flip the order, f64 would be tried first (but both work)
let val: Value = ctx.eval("3.14").unwrap();
let any: AnyOf3<String, f64, i32> = AnyOf3::from_js(&ctx, val).unwrap();
assert!(any.is_b()); // f64 should succeed first
});
}
}
@@ -0,0 +1,226 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
use std::{
cmp::min,
collections::VecDeque,
sync::{
atomic::{AtomicBool, AtomicUsize, Ordering},
Arc, Mutex,
},
};
use tokio::sync::{Notify, Semaphore};
#[derive(Clone)]
pub struct BytearrayBuffer {
inner: Arc<Mutex<VecDeque<u8>>>,
max_capacity: Arc<AtomicUsize>,
len: Arc<AtomicUsize>,
notify: Arc<Notify>,
closed: Arc<AtomicBool>,
write_semaphore: Arc<Semaphore>,
}
impl BytearrayBuffer {
pub fn new(capacity: usize) -> Self {
let queue = VecDeque::with_capacity(capacity);
let capacity = queue.capacity();
Self {
inner: Arc::new(Mutex::new(queue)),
len: Arc::new(AtomicUsize::new(0)),
max_capacity: Arc::new(AtomicUsize::new(capacity)),
notify: Arc::new(Notify::new()),
closed: Arc::new(AtomicBool::new(false)),
write_semaphore: Arc::new(Semaphore::new(1)),
}
}
pub fn len(&self) -> usize {
self.len.load(Ordering::Relaxed)
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
#[allow(dead_code)]
pub fn write_forced(&self, item: &[u8]) {
let mut inner = self.inner.lock().unwrap();
inner.extend(item);
let capacity = inner.capacity();
self.len.fetch_add(item.len(), Ordering::Relaxed);
self.max_capacity.store(capacity, Ordering::Relaxed);
}
pub async fn write(&self, item: &mut [u8]) -> usize {
let _ = self.write_semaphore.acquire().await.unwrap();
let mut slice_index = 0;
loop {
let max_capacity = self.max_capacity.load(Ordering::Relaxed);
if self.closed.load(Ordering::Relaxed) {
return max_capacity;
}
let len = self.len.load(Ordering::Relaxed);
let available = max_capacity - len;
if available > 0 {
let end_index = min(item.len() - 1, slice_index + available - 1);
let sub_slice = &item[slice_index..=end_index];
let slice_length = sub_slice.len();
slice_index += slice_length;
self.inner.lock().unwrap().extend(sub_slice);
self.len.fetch_add(slice_length, Ordering::Relaxed);
if slice_index == item.len() {
return max_capacity;
}
}
self.notify.notified().await;
}
}
#[allow(dead_code)]
pub fn is_closed(&self) -> bool {
self.closed.load(Ordering::Relaxed)
}
pub async fn close(&self) {
self.closed.store(true, Ordering::Relaxed);
self.notify.notify_one();
//wait for write to finish
let _ = self.write_semaphore.acquire().await.unwrap();
}
pub async fn clear(&self) {
self.closed.store(false, Ordering::Relaxed);
self.notify.notify_one();
//wait for write to finish
let _ = self.write_semaphore.acquire().await.unwrap();
self.len.store(0, Ordering::Relaxed);
self.inner.lock().unwrap().clear();
self.closed.store(false, Ordering::Relaxed);
}
pub fn read(&self, desired_size: Option<usize>) -> Option<Vec<u8>> {
let mut inner = self.inner.lock().unwrap();
let done = self.closed.load(Ordering::Relaxed);
let items = if done {
Some(inner.drain(0..).collect())
} else if let Some(desired_len) = desired_size {
let max_capacity = self.max_capacity.load(Ordering::Relaxed);
if desired_len > max_capacity {
let diff = desired_len - max_capacity;
inner.reserve(diff - 1);
let mut max_capacity = inner.capacity();
if desired_len > max_capacity {
inner.reserve(desired_len - max_capacity);
max_capacity = inner.capacity();
}
drop(inner);
self.max_capacity.store(max_capacity, Ordering::Relaxed);
self.notify.notify_one();
return None;
}
let len = self.len.load(Ordering::Relaxed);
if desired_len > len {
self.notify.notify_one();
return None;
}
Some(inner.drain(0..desired_len).collect())
} else {
Some(inner.drain(0..).collect())
};
self.len.store(inner.len(), Ordering::Relaxed);
drop(inner);
self.notify.notify_one();
items
}
}
#[cfg(test)]
mod tests {
use super::BytearrayBuffer;
#[tokio::test]
async fn clear_while_writing() {
let queue = BytearrayBuffer::new(8);
let queue2 = queue.clone();
tokio::task::spawn(async move {
let mut vec: Vec<u8> = (0..=255).collect();
queue.write(&mut vec).await;
});
queue2.clear().await
}
#[tokio::test]
async fn write_one_at_a_time() {
let queue = BytearrayBuffer::new(8);
let queue2 = queue.clone();
let queue3 = queue.clone();
tokio::task::spawn(async move {
let mut vec: Vec<u8> = (0..=127).collect();
queue.write(&mut vec).await;
});
tokio::task::spawn(async move {
let mut vec: Vec<u8> = (128..=255).collect();
queue2.write(&mut vec).await;
});
let mut data = Vec::<u8>::new();
loop {
tokio::task::yield_now().await;
if let Some(bytes) = queue3.read(Some(256)) {
data.extend(bytes);
break;
}
}
//assert that data in vec is increment from 0 to 255
for i in 0..=255 {
assert_eq!(data[i as usize], i);
}
}
#[tokio::test]
async fn queue() {
let queue = BytearrayBuffer::new(8);
let queue2 = queue.clone();
let write_task = tokio::task::spawn(async move {
for _ in 0..=255 {
let mut vec: Vec<u8> = (0..=255).collect();
queue.write(&mut vec).await;
}
queue.close().await;
});
let mut data = Vec::<u8>::new();
loop {
let done = queue2.is_closed();
tokio::task::yield_now().await;
if let Some(bytes) = queue2.read(Some(9)) {
data.extend(bytes);
}
if done {
break;
}
}
let _ = write_task.await;
assert_eq!(data.len(), 256 * 256)
}
}
+380
View File
@@ -0,0 +1,380 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
use std::rc::Rc;
use rquickjs::{
atom::PredefinedAtom,
class::{Trace, Tracer},
function::Constructor,
ArrayBuffer, Coerced, Ctx, Exception, FromJs, IntoJs, JsLifetime, Object, Result, TypedArray,
Value,
};
use crate::{error_messages::ERROR_MSG_ARRAY_BUFFER_DETACHED, result::ResultExt};
#[derive(Clone, PartialEq)]
pub enum ObjectBytes<'js> {
U8Array(TypedArray<'js, u8>),
I8Array(TypedArray<'js, i8>),
U16Array(TypedArray<'js, u16>),
I16Array(TypedArray<'js, i16>),
U32Array(TypedArray<'js, u32>),
I32Array(TypedArray<'js, i32>),
U64Array(TypedArray<'js, u64>),
I64Array(TypedArray<'js, i64>),
F32Array(TypedArray<'js, f32>),
F64Array(TypedArray<'js, f64>),
DataView(ArrayBuffer<'js>),
Vec(Vec<u8>),
}
// Requires manual implementation because rquickjs hasn't implemented JsLifetime for f32 or f64
unsafe impl<'js> JsLifetime<'js> for ObjectBytes<'js> {
type Changed<'to> = ObjectBytes<'to>;
}
impl<'js> Trace<'js> for ObjectBytes<'js> {
fn trace<'a>(&self, tracer: Tracer<'a, 'js>) {
match self {
ObjectBytes::U8Array(a) => a.trace(tracer),
ObjectBytes::I8Array(a) => a.trace(tracer),
ObjectBytes::U16Array(a) => a.trace(tracer),
ObjectBytes::I16Array(a) => a.trace(tracer),
ObjectBytes::U32Array(a) => a.trace(tracer),
ObjectBytes::I32Array(a) => a.trace(tracer),
ObjectBytes::U64Array(a) => a.trace(tracer),
ObjectBytes::I64Array(a) => a.trace(tracer),
ObjectBytes::F32Array(a) => a.trace(tracer),
ObjectBytes::F64Array(a) => a.trace(tracer),
ObjectBytes::DataView(d) => d.trace(tracer),
ObjectBytes::Vec(v) => v.trace(tracer),
}
}
}
impl<'js> IntoJs<'js> for ObjectBytes<'js> {
fn into_js(self, ctx: &Ctx<'js>) -> Result<Value<'js>> {
match self {
ObjectBytes::U8Array(a) => a.into_js(ctx),
ObjectBytes::I8Array(a) => a.into_js(ctx),
ObjectBytes::U16Array(a) => a.into_js(ctx),
ObjectBytes::I16Array(a) => a.into_js(ctx),
ObjectBytes::U32Array(a) => a.into_js(ctx),
ObjectBytes::I32Array(a) => a.into_js(ctx),
ObjectBytes::U64Array(a) => a.into_js(ctx),
ObjectBytes::I64Array(a) => a.into_js(ctx),
ObjectBytes::F32Array(a) => a.into_js(ctx),
ObjectBytes::F64Array(a) => a.into_js(ctx),
ObjectBytes::DataView(d) => {
let ctor: Constructor = ctx.globals().get(PredefinedAtom::DataView)?;
ctor.construct((d,))
},
ObjectBytes::Vec(v) => v.into_js(ctx),
}
}
}
impl<'js> TryFrom<ObjectBytes<'js>> for Vec<u8> {
type Error = Rc<str>;
fn try_from(value: ObjectBytes<'js>) -> std::result::Result<Self, Self::Error> {
value.into_bytes_inner()
}
}
impl<'a, 'js> TryFrom<&'a ObjectBytes<'js>> for &'a [u8] {
type Error = Rc<str>;
fn try_from(value: &'a ObjectBytes<'js>) -> std::result::Result<Self, Self::Error> {
value.as_bytes_inner()
}
}
impl<'js> FromJs<'js> for ObjectBytes<'js> {
fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result<Self> {
Self::from_offset(ctx, &value, 0, None)
}
}
impl<'js> ObjectBytes<'js> {
pub fn from(ctx: &Ctx<'js>, value: &Value<'js>) -> Result<Self> {
Self::from_offset(ctx, value, 0, None)
}
pub fn from_offset(
ctx: &Ctx<'js>,
value: &Value<'js>,
offset: usize,
length: Option<usize>,
) -> Result<Self> {
if value.is_undefined() {
return Ok(ObjectBytes::Vec(vec![]));
}
if let Some(bytes) = get_string_bytes(value, offset, length)? {
return Ok(ObjectBytes::Vec(bytes));
}
if let Some(bytes) = get_array_bytes(value, offset, length)? {
return Ok(ObjectBytes::Vec(bytes));
}
if let Some(obj) = value.as_object() {
if let Some(bytes) = Self::from_array_buffer(obj)? {
return Ok(bytes);
}
}
if let Some(bytes) = get_coerced_string_bytes(value, offset, length) {
return Ok(ObjectBytes::Vec(bytes));
}
Err(Exception::throw_message(
ctx,
"value must be typed DataView, Buffer, ArrayBuffer, Uint8Array or interpretable as string",
))
}
pub fn as_bytes(&self, ctx: &Ctx<'js>) -> Result<&[u8]> {
self.as_bytes_inner().or_throw(ctx)
}
fn as_bytes_inner(&self) -> std::result::Result<&[u8], Rc<str>> {
match self {
ObjectBytes::U8Array(array) => array.as_bytes(),
ObjectBytes::I8Array(array) => array.as_bytes(),
ObjectBytes::U16Array(array) => array.as_bytes(),
ObjectBytes::I16Array(array) => array.as_bytes(),
ObjectBytes::U32Array(array) => array.as_bytes(),
ObjectBytes::I32Array(array) => array.as_bytes(),
ObjectBytes::U64Array(array) => array.as_bytes(),
ObjectBytes::I64Array(array) => array.as_bytes(),
ObjectBytes::F32Array(array) => array.as_bytes(),
ObjectBytes::F64Array(array) => array.as_bytes(),
ObjectBytes::DataView(array_buffer) => array_buffer.as_bytes(),
ObjectBytes::Vec(bytes) => Some(bytes.as_ref()),
}
.ok_or(ERROR_MSG_ARRAY_BUFFER_DETACHED.into())
}
pub fn into_bytes(self, ctx: &Ctx<'_>) -> Result<Vec<u8>> {
self.into_bytes_inner().or_throw(ctx)
}
fn into_bytes_inner(self) -> std::result::Result<Vec<u8>, Rc<str>> {
if let ObjectBytes::Vec(bytes) = self {
return Ok(bytes);
}
Ok(self.as_bytes_inner()?.to_vec())
}
pub fn from_array_buffer(obj: &Object<'js>) -> Result<Option<ObjectBytes<'js>>> {
//most common
if let Ok(typed_array) = TypedArray::<u8>::from_object(obj.clone()) {
return Ok(Some(ObjectBytes::U8Array(typed_array)));
}
//second most common
if let Some(array_buffer) = ArrayBuffer::from_object(obj.clone()) {
return Ok(Some(ObjectBytes::DataView(array_buffer)));
}
if let Ok(typed_array) = TypedArray::<i8>::from_object(obj.clone()) {
return Ok(Some(ObjectBytes::I8Array(typed_array)));
}
if let Ok(typed_array) = TypedArray::<u16>::from_object(obj.clone()) {
return Ok(Some(ObjectBytes::U16Array(typed_array)));
}
if let Ok(typed_array) = TypedArray::<i16>::from_object(obj.clone()) {
return Ok(Some(ObjectBytes::I16Array(typed_array)));
}
if let Ok(typed_array) = TypedArray::<u32>::from_object(obj.clone()) {
return Ok(Some(ObjectBytes::U32Array(typed_array)));
}
if let Ok(typed_array) = TypedArray::<i32>::from_object(obj.clone()) {
return Ok(Some(ObjectBytes::I32Array(typed_array)));
}
if let Ok(typed_array) = TypedArray::<u64>::from_object(obj.clone()) {
return Ok(Some(ObjectBytes::U64Array(typed_array)));
}
if let Ok(typed_array) = TypedArray::<i64>::from_object(obj.clone()) {
return Ok(Some(ObjectBytes::I64Array(typed_array)));
}
if let Ok(typed_array) = TypedArray::<f32>::from_object(obj.clone()) {
return Ok(Some(ObjectBytes::F32Array(typed_array)));
}
if let Ok(typed_array) = TypedArray::<f64>::from_object(obj.clone()) {
return Ok(Some(ObjectBytes::F64Array(typed_array)));
}
if let Ok(array_buffer) = obj.get::<_, ArrayBuffer>("buffer") {
return Ok(Some(ObjectBytes::DataView(array_buffer)));
}
Ok(None)
}
pub fn get_array_buffer(&self) -> Result<Option<(ArrayBuffer<'js>, usize, usize)>> {
let buffer = match self {
ObjectBytes::DataView(array_buffer) => (array_buffer.clone(), array_buffer.len(), 0),
ObjectBytes::U8Array(typed_array) => {
let byte_length = typed_array.len();
(
typed_array.arraybuffer()?,
byte_length,
typed_array.get("byteOffset")?,
)
},
ObjectBytes::I8Array(typed_array) => {
let byte_length = typed_array.len();
(
typed_array.arraybuffer()?,
byte_length,
typed_array.get("byteOffset")?,
)
},
ObjectBytes::U16Array(typed_array) => {
let byte_length = typed_array.len() * 2;
(
typed_array.arraybuffer()?,
byte_length,
typed_array.get("byteOffset")?,
)
},
ObjectBytes::I16Array(typed_array) => {
let byte_length = typed_array.len() * 2;
(
typed_array.arraybuffer()?,
byte_length,
typed_array.get("byteOffset")?,
)
},
ObjectBytes::U32Array(typed_array) => {
let byte_length = typed_array.len() * 4;
(
typed_array.arraybuffer()?,
byte_length,
typed_array.get("byteOffset")?,
)
},
ObjectBytes::I32Array(typed_array) => {
let byte_length = typed_array.len() * 4;
(
typed_array.arraybuffer()?,
byte_length,
typed_array.get("byteOffset")?,
)
},
ObjectBytes::U64Array(typed_array) => {
let byte_length = typed_array.len() * 8;
(
typed_array.arraybuffer()?,
byte_length,
typed_array.get("byteOffset")?,
)
},
ObjectBytes::I64Array(typed_array) => {
let byte_length = typed_array.len() * 8;
(
typed_array.arraybuffer()?,
byte_length,
typed_array.get("byteOffset")?,
)
},
ObjectBytes::F32Array(typed_array) => {
let byte_length = typed_array.len() * 4;
(
typed_array.arraybuffer()?,
byte_length,
typed_array.get("byteOffset")?,
)
},
ObjectBytes::F64Array(typed_array) => {
let byte_length = typed_array.len() * 8;
(
typed_array.arraybuffer()?,
byte_length,
typed_array.get("byteOffset")?,
)
},
_ => return Ok(None),
};
Ok(Some(buffer))
}
}
pub fn get_start_end_indexes(
source_len: usize,
target_len: Option<usize>,
offset: usize,
) -> (usize, usize) {
if offset > source_len {
return (0, 0);
}
let target_len = target_len.unwrap_or(source_len - offset);
if offset + target_len > source_len {
return (offset, source_len);
}
(offset, target_len + offset)
}
pub fn get_array_bytes(
value: &Value<'_>,
offset: usize,
length: Option<usize>,
) -> Result<Option<Vec<u8>>> {
if value.is_array() {
let array = value.as_array().unwrap();
let (start, end) = get_start_end_indexes(array.len(), length, offset);
let size = end - start;
let mut bytes: Vec<u8> = Vec::with_capacity(size);
for val in array.iter::<u8>().skip(start).take(size) {
let val: u8 = val?;
bytes.push(val);
}
return Ok(Some(bytes));
}
Ok(None)
}
pub fn get_coerced_string_bytes(
value: &Value<'_>,
offset: usize,
length: Option<usize>,
) -> Option<Vec<u8>> {
if let Ok(val) = value.get::<Coerced<String>>() {
return Some(bytes_from_js_string(val.0, offset, length));
};
None
}
fn bytes_from_js_string(string: String, offset: usize, length: Option<usize>) -> Vec<u8> {
let (start, end) = get_start_end_indexes(string.len(), length, offset);
string.as_bytes()[start..end].to_vec()
}
#[inline]
pub fn get_string_bytes(
value: &Value<'_>,
offset: usize,
length: Option<usize>,
) -> Result<Option<Vec<u8>>> {
if let Some(val) = value.as_string() {
let string = val.to_string()?;
return Ok(Some(bytes_from_js_string(string, offset, length)));
}
Ok(None)
}
pub fn bytes_to_typed_array<'js>(ctx: Ctx<'js>, bytes: &[u8]) -> Result<Value<'js>> {
TypedArray::<u8>::new(ctx.clone(), bytes).into_js(&ctx)
}
+77
View File
@@ -0,0 +1,77 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
use rquickjs::{
atom::PredefinedAtom, class::JsClass, object::Accessor, prelude::This, Array, Class, Ctx,
Function, Object, Result, Value,
};
use crate::primordials::{BasePrimordials, Primordial};
use super::{object::ObjectExt, result::OptionExt};
pub static CUSTOM_INSPECT_SYMBOL_DESCRIPTION: &str = "llrt.inspect.custom";
pub trait IteratorDef<'js>
where
Self: 'js + JsClass<'js> + Sized,
{
fn js_entries(&self, ctx: Ctx<'js>) -> Result<Array<'js>>;
fn js_iterator(&self, ctx: Ctx<'js>) -> Result<Value<'js>> {
let value = self.js_entries(ctx)?;
let obj = value.as_object();
let values_fn: Function = obj.get(PredefinedAtom::Values)?;
values_fn.call((This(value),))
}
}
pub fn get_class_name(value: &Value) -> Result<Option<String>> {
value
.get_optional::<_, Object>(PredefinedAtom::Constructor)?
.and_then_ok(|ctor| ctor.get_optional::<_, String>(PredefinedAtom::Name))
}
#[inline(always)]
pub fn get_class<'js, C>(provided: &Value<'js>) -> Result<Option<Class<'js, C>>>
where
C: JsClass<'js>,
{
if provided
.as_object()
.map(|p| p.instance_of::<C>())
.unwrap_or_default()
{
return Ok(Some(Class::<C>::from_value(provided)?));
}
Ok(None)
}
pub trait CustomInspectExtension<'js> {
fn define_with_custom_inspect(globals: &Object<'js>) -> Result<()>;
}
pub trait CustomInspect<'js>
where
Self: JsClass<'js>,
{
fn custom_inspect(&self, ctx: Ctx<'js>) -> Result<Object<'js>>;
}
impl<'js, C> CustomInspectExtension<'js> for Class<'js, C>
where
C: JsClass<'js> + CustomInspect<'js> + 'js,
{
fn define_with_custom_inspect(globals: &Object<'js>) -> Result<()> {
Self::define(globals)?;
let custom_inspect_symbol = BasePrimordials::get(globals.ctx())?
.symbol_custom_inspect
.clone();
if let Some(proto) = Class::<C>::prototype(globals.ctx())? {
proto.prop(
custom_inspect_symbol,
Accessor::from(|this: This<Class<'js, C>>, ctx| this.borrow().custom_inspect(ctx)),
)?;
}
Ok(())
}
}
+487
View File
@@ -0,0 +1,487 @@
use std::collections::HashSet;
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
use rquickjs::{
atom::PredefinedAtom,
function::{Constructor, Opt, This},
Array, ArrayBuffer, Ctx, Exception, Function, IntoJs, Null, Object, Result, Type, Value,
};
use super::{
hash,
object::ObjectExt,
primordials::{BasePrimordials, Primordial},
};
#[derive(Debug)]
enum StackItem<'js> {
Value(usize, Value<'js>, Option<String>, Option<usize>),
ObjectEnd,
}
#[derive(Debug)]
enum ObjectType {
Set,
Map,
}
#[derive(Debug)]
enum TapeValue<'js> {
Array(Array<'js>),
Object(Object<'js>),
Value(Value<'js>),
Collection(Option<Value<'js>>, ObjectType),
}
#[derive(Debug)]
struct TapeItem<'js> {
parent: usize,
object_key: Option<String>,
array_index: Option<usize>,
value: TapeValue<'js>,
}
pub fn structured_clone<'js>(
ctx: &Ctx<'js>,
value: Value<'js>,
options: Opt<Object<'js>>,
) -> Result<Value<'js>> {
let primordials = BasePrimordials::get(ctx)?;
let mut transfer_set = None;
if let Some(options) = options.0 {
if let Some(transfer_array) = options.get_optional::<_, Array>("transfer")? {
let mut set = HashSet::with_capacity(transfer_array.len());
for item in transfer_array.iter::<Value>() {
set.insert(item?);
}
transfer_set = Some(set);
}
}
let mut tape = Vec::<TapeItem>::with_capacity(10);
let mut stack = Vec::with_capacity(10);
let mut visited = Vec::<(usize, usize)>::with_capacity(10);
let mut index = 0usize;
stack.push(StackItem::Value(0, value, None, None));
while let Some(item) = stack.pop() {
match item {
StackItem::Value(parent, value, mut object_key, array_index) => {
if let Some(set) = &transfer_set {
if let Some(value) = set.get(&value) {
append_transfer_value(&mut tape, value, parent, object_key, array_index)?;
index += 1;
continue;
}
}
match value.type_of() {
Type::Proxy => {
return Err(Exception::throw_type(
ctx,
"A Proxy value could not be cloned",
));
},
Type::Object => {
if check_circular(
&mut tape,
&mut visited,
&value,
parent,
&mut object_key,
array_index,
index,
) {
index += 1;
continue;
}
//unsafe OK since we're guaranteed to be object by the match
let object = unsafe { value.as_object().unwrap_unchecked() };
if object.is_instance_of(&primordials.constructor_date) {
append_ctor_value(
&mut tape,
object,
&primordials.constructor_date,
parent,
object_key,
array_index,
)?;
index += 1;
continue;
}
if object.is_instance_of(&primordials.constructor_regexp) {
append_ctor_value(
&mut tape,
object,
&primordials.constructor_regexp,
parent,
object_key,
array_index,
)?;
index += 1;
continue;
}
let is_collection = if object.is_instance_of(&primordials.constructor_set) {
Some(ObjectType::Set)
} else if object.is_instance_of(&primordials.constructor_map) {
Some(ObjectType::Map)
} else {
None
};
if let Some(collection_type) = is_collection {
append_collection(
&mut tape,
&primordials.function_array_from,
object,
parent,
object_key,
array_index,
collection_type,
&mut stack,
index,
)?;
index += 1;
continue;
}
if primordials
.function_array_buffer_is_view
.call::<_, bool>((value.clone(),))?
{
append_buffer(&mut tape, object, parent, object_key, array_index)?;
index += 1;
continue;
}
let new: Object<'_> =
if object.is_instance_of(&primordials.constructor_error) {
primordials.constructor_error.construct(("",))
} else {
Object::new(ctx.clone())
}?;
tape.push(TapeItem {
parent,
object_key,
array_index,
value: TapeValue::Object(new),
});
stack.push(StackItem::ObjectEnd);
for key in object.keys::<String>() {
let key = key?;
let value = object.get(&key)?;
stack.push(StackItem::Value(index, value, Some(key), None));
}
},
Type::Array => {
if check_circular(
&mut tape,
&mut visited,
&value,
parent,
&mut object_key,
array_index,
index,
) {
index += 1;
continue;
}
let new = Array::new(ctx.clone())?;
tape.push(TapeItem {
parent,
object_key,
array_index,
value: TapeValue::Array(new),
});
stack.push(StackItem::ObjectEnd);
//unsafe OK since we're guaranteed to be object by the match
let array = unsafe { value.as_array().unwrap_unchecked() };
//reverse for loop of items in array
for array_index in (0usize..array.len()).rev() {
stack.push(StackItem::Value(
index,
array.get(array_index)?,
None,
Some(array_index),
));
}
},
_ => {
tape.push(TapeItem {
parent,
object_key,
array_index,
value: TapeValue::Value(value),
});
},
}
index += 1;
},
StackItem::ObjectEnd => {
visited.pop();
},
}
}
while let Some(item) = tape.pop() {
let value = match item.value {
TapeValue::Array(array) => array.into_value(),
TapeValue::Object(object) => object.into_value(),
TapeValue::Value(value) => value,
TapeValue::Collection(mut value, _) => value.take().unwrap(),
};
if tape.is_empty() {
return Ok(value);
}
let parent = &mut tape[item.parent];
let array_index = item.array_index;
let object_key = item.object_key;
match &mut parent.value {
TapeValue::Array(array) => {
array.set(array_index.unwrap(), value)?;
},
TapeValue::Object(object) => {
let string = object_key.unwrap();
object.set(string, value)?;
},
TapeValue::Collection(collection_value, collection_type) => {
match collection_type {
ObjectType::Set => {
collection_value.replace(primordials.constructor_set.construct((value,))?);
},
ObjectType::Map => {
collection_value.replace(primordials.constructor_map.construct((value,))?);
},
};
},
_ => {},
};
}
Null.into_js(ctx)
}
#[inline(always)]
#[cold]
fn append_buffer<'js>(
tape: &mut Vec<TapeItem<'js>>,
object: &Object<'js>,
parent: usize,
object_key: Option<String>,
array_index: Option<usize>,
) -> Result<()> {
let ctor: Constructor = object.get(PredefinedAtom::Constructor)?;
let slice: Function = object.get("slice")?;
let clone: Value = slice.call((This(object.clone()),))?;
let new = ctor.construct((clone,))?;
tape.push(TapeItem {
parent,
object_key,
array_index,
value: TapeValue::Value(new),
});
Ok(())
}
#[inline(always)]
#[cold]
#[allow(clippy::too_many_arguments)]
fn append_collection<'js>(
tape: &mut Vec<TapeItem<'js>>,
array_from: &Function<'js>,
object: &Object<'js>,
parent: usize,
object_key: Option<String>,
array_index: Option<usize>,
collection_type: ObjectType,
stack: &mut Vec<StackItem<'js>>,
index: usize,
) -> Result<()> {
let array: Array = array_from.call((object.clone(),))?;
tape.push(TapeItem {
parent,
object_key,
array_index,
value: TapeValue::Collection(None, collection_type),
});
stack.push(StackItem::ObjectEnd);
stack.push(StackItem::Value(index, array.into(), None, None));
Ok(())
}
#[inline(always)]
fn check_circular(
tape: &mut Vec<TapeItem>,
visited: &mut Vec<(usize, usize)>,
value: &Value<'_>,
parent: usize,
object_key: &mut Option<String>,
array_index: Option<usize>,
index: usize,
) -> bool {
let hash = hash::default_hash(value);
if let Some(visited) = visited.iter().find(|v| v.0 == hash) {
append_circular(tape, visited, object_key, parent, array_index);
return true;
}
visited.push((hash, index));
false
}
#[inline(always)]
#[cold]
fn append_transfer_value<'js>(
tape: &mut Vec<TapeItem<'js>>,
value: &Value<'js>,
parent: usize,
object_key: Option<String>,
array_index: Option<usize>,
) -> Result<()> {
let value = if let Some(ab) = ArrayBuffer::from_value(value.clone()) {
ab.get::<_, Function>("transfer")?.call((This(ab),))?
} else {
value.clone()
};
tape.push(TapeItem {
parent,
object_key,
array_index,
value: TapeValue::Value(value),
});
Ok(())
}
#[inline(always)]
#[cold]
fn append_circular(
tape: &mut Vec<TapeItem<'_>>,
visited: &(usize, usize),
object_key: &mut Option<String>,
parent: usize,
array_index: Option<usize>,
) {
let value = match &tape[visited.1].value {
TapeValue::Array(array) => array.clone().into_value(),
TapeValue::Object(object) => object.clone().into_value(),
TapeValue::Value(value) => value.clone(),
TapeValue::Collection(value, _) => value.clone().unwrap(),
};
let object_key = object_key.take();
tape.push(TapeItem {
parent,
object_key,
array_index,
value: TapeValue::Value(value),
});
}
#[inline(always)]
#[cold]
fn append_ctor_value<'js>(
tape: &mut Vec<TapeItem<'js>>,
object: &Object<'js>,
ctor: &Constructor<'js>,
parent: usize,
object_key: Option<String>,
array_index: Option<usize>,
) -> Result<()> {
let clone: Value = ctor.construct((object.clone(),))?;
tape.push(TapeItem {
parent,
object_key,
array_index,
value: TapeValue::Value(clone),
});
Ok(())
}
#[cfg(test)]
mod tests {
use llrt_test::test_sync_with;
use rquickjs::{function::Opt, Object, Value};
use crate::primordials::{BasePrimordials, Primordial};
use super::structured_clone;
#[tokio::test]
async fn clone() {
test_sync_with(|ctx| {
BasePrimordials::init(&ctx)?;
let value: Object = ctx.eval(
r#"
const a = {
"foo":{
"bar":"baz"
},
"foo1":{
"bar1":"baz1",
"bar11":"baz11"
}
};
a
"#,
)?;
let cloned = structured_clone(&ctx, value.clone().into_value(), Opt(None))?
.into_object()
.unwrap();
let json = ctx
.json_stringify(value.clone())?
.unwrap()
.to_string()?
.to_string();
let clone_json = ctx
.json_stringify(cloned.clone())?
.unwrap()
.to_string()?
.to_string();
assert_eq!(json, clone_json);
assert_ne!(
value.get::<_, Value>("foo")?,
cloned.get::<_, Value>("foo")?
);
Ok(())
})
.await
}
#[tokio::test]
async fn clone_circular() {
test_sync_with(|ctx| {
BasePrimordials::init(&ctx)?;
let _value: Object = ctx.eval(
r#"
const originalObject = { foo: { bar: "baz",arr: [1,2,3] } };
originalObject.foo.circularRef = originalObject;
originalObject.foo.circularRef2 = originalObject;
originalObject.foo.circularRef3 = originalObject.foo;
originalObject.ref2 = originalObject;
"#,
)?;
Ok(())
})
.await
}
}
+17
View File
@@ -0,0 +1,17 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
use rquickjs::{Ctx, Result};
pub trait CtxExt {
fn get_script_or_module_name(&self) -> Result<String>;
}
impl CtxExt for Ctx<'_> {
fn get_script_or_module_name(&self) -> Result<String> {
if let Some(name) = self.script_or_module_name(0) {
name.to_string()
} else {
Ok(String::from("."))
}
}
}
+23
View File
@@ -0,0 +1,23 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
use rquickjs::{CatchResultExt, CaughtError, Ctx, Error, IntoJs, Result, Value};
pub trait ErrorExtensions<'js> {
fn into_value(self, ctx: &Ctx<'js>) -> Result<Value<'js>>;
}
impl<'js> ErrorExtensions<'js> for Error {
fn into_value(self, ctx: &Ctx<'js>) -> Result<Value<'js>> {
Err::<(), _>(self).catch(ctx).unwrap_err().into_value(ctx)
}
}
impl<'js> ErrorExtensions<'js> for CaughtError<'js> {
fn into_value(self, ctx: &Ctx<'js>) -> Result<Value<'js>> {
Ok(match self {
CaughtError::Error(err) => err.to_string().into_js(ctx)?,
CaughtError::Exception(ex) => ex.into_value(),
CaughtError::Value(val) => val,
})
}
}
@@ -0,0 +1,3 @@
pub const ERROR_MSG_NOT_ARRAY_BUFFER: &str = "Not an ArrayBuffer";
pub const ERROR_MSG_ARRAY_BUFFER_DETACHED: &str = "ArrayBuffer is detached";
pub const ERROR_MSG_BROADCAST_LAGGED: &str = "Lagged too much behind";
+103
View File
@@ -0,0 +1,103 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
use std::{fs::Metadata, io, path::PathBuf};
use tokio::fs::{self};
pub struct DirectoryWalker<T>
where
T: Fn(&str) -> bool,
{
stack: Vec<(PathBuf, Option<Metadata>)>,
filter: T,
recursive: bool,
eat_root: bool,
}
impl<T> DirectoryWalker<T>
where
T: Fn(&str) -> bool,
{
pub fn new(root: PathBuf, filter: T) -> Self {
Self {
stack: vec![(root, None)],
filter,
recursive: false,
eat_root: true,
}
}
pub fn set_recursive(&mut self, recursive: bool) {
self.recursive = recursive;
}
pub async fn walk(&mut self) -> io::Result<Option<(PathBuf, Metadata)>> {
if self.eat_root {
self.eat_root = false;
let (dir, _) = self.stack.pop().unwrap();
self.append_stack(&dir).await?;
}
if let Some((entry, metadata)) = self.stack.pop() {
let metadata = metadata.unwrap();
if self.recursive && metadata.is_dir() {
self.append_stack(&entry).await?;
}
Ok(Some((entry, metadata)))
} else {
Ok(None)
}
}
pub fn walk_sync(&mut self) -> io::Result<Option<(PathBuf, Metadata)>> {
if self.eat_root {
self.eat_root = false;
let (dir, _) = self.stack.pop().unwrap();
self.append_stack_sync(&dir)?;
}
if let Some((entry, metadata)) = self.stack.pop() {
let metadata = metadata.unwrap();
if self.recursive && metadata.is_dir() {
self.append_stack_sync(&entry)?;
}
Ok(Some((entry, metadata)))
} else {
Ok(None)
}
}
async fn append_stack(&mut self, dir: &PathBuf) -> io::Result<()> {
let mut stream = fs::read_dir(dir).await?;
while let Some(entry) = stream.next_entry().await? {
let name = entry.file_name();
let name = name.to_string_lossy();
if !(self.filter)(name.as_ref()) {
continue;
}
let entry_path = entry.path();
let metadata = fs::symlink_metadata(&entry_path).await?;
self.stack.push((entry_path, Some(metadata)));
}
Ok(())
}
fn append_stack_sync(&mut self, dir: &PathBuf) -> io::Result<()> {
let dir = std::fs::read_dir(dir)?;
for entry in dir.flatten() {
let name = entry.file_name();
let name = name.to_string_lossy();
if !(self.filter)(name.as_ref()) {
continue;
}
let entry_path = entry.path();
let metadata = entry_path.symlink_metadata()?;
self.stack.push((entry_path, Some(metadata)))
}
Ok(())
}
}
+8
View File
@@ -0,0 +1,8 @@
use std::hash::{DefaultHasher, Hash, Hasher};
#[inline]
pub fn default_hash<T: Hash + ?Sized>(v: &T) -> usize {
let mut state = DefaultHasher::default();
v.hash(&mut state);
state.finish() as usize
}
+30
View File
@@ -0,0 +1,30 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
macro_rules! define_extension {
($base:ident, $file:ident, $ext:expr) => {
#[allow(dead_code)]
pub const $base: &str = $ext;
#[allow(dead_code)]
pub const $file: &str = concat!(".", $ext);
};
}
define_extension!(BYTECODE_EXT, BYTECODE_FILE_EXT, "lrt");
macro_rules! define_supported_extensions {
// Accepts a list of supported extensions and a single additional constant extension
($constant_ext:ident, $($ext:literal),*) => {
// Define the array of extensions as a constant
pub const SUPPORTED_EXTENSIONS: &[&str] = &[$($ext),*, $constant_ext];
pub const JS_EXTENSIONS: &[&str] = &[$($ext),*];
// Define the function `is_supported_ext` using a match statement
pub fn is_supported_ext(ext: &str) -> bool {
matches!(ext, $($ext)|* | $constant_ext)
}
};
}
define_supported_extensions!(BYTECODE_FILE_EXT, ".js", ".mjs", ".cjs");
+30
View File
@@ -0,0 +1,30 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
use std::sync::atomic::{AtomicUsize, Ordering};
use tokio::sync::Notify;
#[derive(Default)]
pub struct Latch {
count: AtomicUsize,
notify: Notify,
}
impl Latch {
pub fn increment(&self) {
self.count.fetch_add(1, Ordering::Relaxed);
}
pub fn decrement(&self) {
let previous = self.count.fetch_sub(1, Ordering::Relaxed);
if previous == 1 {
self.notify.notify_waiters();
}
}
pub async fn wait(&self) {
if self.count.load(Ordering::Relaxed) > 0 {
self.notify.notified().await;
}
}
}
+33
View File
@@ -0,0 +1,33 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
pub mod any_of;
#[cfg(feature = "bytearray-buffer")]
pub mod bytearray_buffer;
pub mod bytes;
pub mod class;
pub mod clone;
pub mod ctx;
pub mod error;
pub mod error_messages;
#[cfg(feature = "fs")]
pub mod fs;
pub mod hash;
pub mod io;
pub mod latch;
pub mod macros;
pub mod mc_oneshot;
pub mod module;
pub mod object;
pub mod option;
pub mod primordials;
pub mod provider;
pub mod result;
pub mod reuse_list;
pub mod string;
pub mod sysinfo;
pub mod time;
#[cfg(any(unix, windows))]
pub mod signals;
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
+55
View File
@@ -0,0 +1,55 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
#[macro_export]
macro_rules! count_members {
() => (0);
($head:tt $(,$tail:tt)*) => (1 + count_members!($($tail),*));
}
#[macro_export]
macro_rules! iterable_enum {
($name:ident, $($variant:ident),*) => {
impl $name {
const VARIANTS: &'static [$name] = &[$($name::$variant,)*];
pub fn iter() -> std::slice::Iter<'static, $name> {
Self::VARIANTS.iter()
}
#[allow(dead_code)]
fn _ensure_all_variants(s: Self) {
match s {
$($name::$variant => {},)*
}
}
}
};
}
#[macro_export]
macro_rules! str_enum {
($name:ident, $($variant:ident => $str:expr),*) => {
impl $name {
pub fn as_str(&self) -> &'static str {
match self {
$($name::$variant => $str,)*
}
}
}
impl AsRef<str> for $name {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl TryFrom<&str> for $name {
type Error = String;
fn try_from(s: &str) -> std::result::Result<Self, Self::Error> {
match s {
$($str => Ok($name::$variant),)*
_ => Err(["'", s, "' not available"].concat())
}
}
}
};
}
@@ -0,0 +1,118 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc, RwLock,
};
use rquickjs::{
class::{Trace, Tracer},
Value,
};
use std::ops::Deref;
use tokio::sync::Notify;
#[derive(Debug)]
pub struct Shared<T> {
is_sent: AtomicBool,
value: RwLock<Option<T>>,
notify: Notify,
}
#[derive(Clone, Debug)]
pub struct Sender<T: Clone>(Arc<Shared<T>>);
impl<T: Clone> Deref for Sender<T> {
type Target = Arc<Shared<T>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'js> Trace<'js> for Sender<Value<'js>> {
fn trace<'a>(&self, tracer: Tracer<'a, 'js>) {
if let Ok(v) = self.value.read() {
if let Some(v) = v.as_ref() {
tracer.mark(v)
}
}
}
}
impl<T: Clone> Sender<T> {
pub fn send(&self, value: T) {
if !self.is_sent.load(Ordering::Relaxed) {
self.value.write().unwrap().replace(value);
self.is_sent.store(true, Ordering::Release);
self.notify.notify_waiters();
}
}
pub fn subscribe(&self) -> Receiver<T> {
Receiver(Arc::clone(&self.0))
}
}
#[derive(Clone, Debug)]
pub struct Receiver<T: Clone>(Arc<Shared<T>>);
impl<T: Clone> Deref for Receiver<T> {
type Target = Arc<Shared<T>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T: Clone> Receiver<T> {
pub async fn recv(&self) -> T {
if !self.is_sent.load(Ordering::Acquire) {
self.notify.notified().await;
}
self.value.read().unwrap().clone().unwrap()
}
}
pub fn channel<T: Clone>() -> (Sender<T>, Receiver<T>) {
let shared = Arc::new(Shared {
is_sent: AtomicBool::new(false),
value: RwLock::new(None),
notify: Notify::new(),
});
(Sender(Arc::clone(&shared)), Receiver(shared))
}
#[cfg(test)]
mod tests {
use tokio::join;
#[tokio::test]
async fn test() {
let (tx, rx1) = super::channel::<bool>();
let rx2 = tx.subscribe();
let rx3 = tx.subscribe();
let a = tokio::spawn(async move {
let val = rx1.recv().await; //wait for value to become false
assert!(val)
});
let b = tokio::spawn(async move {
let val = rx2.recv().await; //wait for value to become false
assert!(val)
});
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
tx.send(true);
let val = rx3.recv().await;
assert!(val);
let (a, b) = join!(a, b);
a.unwrap();
b.unwrap();
}
}
+29
View File
@@ -0,0 +1,29 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
use rquickjs::{
module::{Exports, ModuleDef},
Ctx, Object, Result, Value,
};
pub struct ModuleInfo<T: ModuleDef> {
pub name: &'static str,
pub module: T,
}
pub fn export_default<'js, F>(ctx: &Ctx<'js>, exports: &Exports<'js>, f: F) -> Result<()>
where
F: FnOnce(&Object<'js>) -> Result<()>,
{
let default = Object::new(ctx.clone())?;
f(&default)?;
for name in default.keys::<String>() {
let name = name?;
let value: Value = default.get(&name)?;
exports.export(name, value)?;
}
exports.export("default", default)?;
Ok(())
}
+176
View File
@@ -0,0 +1,176 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
use std::collections::BTreeMap;
use rquickjs::{
atom::PredefinedAtom, function::IntoJsFunc, prelude::Func, Array, Coerced, Ctx, Error,
Exception, FromJs, IntoAtom, IntoJs, Object, Result, Symbol, Undefined, Value,
};
use crate::primordials::{BasePrimordials, Primordial};
pub trait ObjectExt<'js> {
fn get_optional<K: IntoAtom<'js> + Clone, V: FromJs<'js>>(&self, k: K) -> Result<Option<V>>;
fn get_required<K: AsRef<str>, V: FromJs<'js>>(
&self,
k: K,
object_name: &'static str,
) -> Result<V>;
fn into_object_or_throw(self, ctx: &Ctx<'js>, object_name: &'static str)
-> Result<Object<'js>>;
}
impl<'js> ObjectExt<'js> for Object<'js> {
fn get_optional<K: IntoAtom<'js> + Clone, V: FromJs<'js> + Sized>(
&self,
k: K,
) -> Result<Option<V>> {
self.get::<K, Option<V>>(k)
}
fn get_required<K: AsRef<str>, V: FromJs<'js>>(
&self,
k: K,
object_name: &'static str,
) -> Result<V> {
let k = k.as_ref();
self.get::<&str, Option<V>>(k)?.ok_or_else(|| {
Exception::throw_type(
self.ctx(),
&[object_name, " '", k, "' property required"].concat(),
)
})
}
fn into_object_or_throw(self, _: &Ctx<'js>, _: &'static str) -> Result<Object<'js>> {
Ok(self)
}
}
impl<'js> ObjectExt<'js> for Value<'js> {
fn get_optional<K: IntoAtom<'js> + Clone, V: FromJs<'js>>(&self, k: K) -> Result<Option<V>> {
if let Some(obj) = self.as_object() {
return obj.get_optional(k);
}
Ok(None)
}
fn get_required<K: AsRef<str>, V: FromJs<'js>>(
&self,
k: K,
object_name: &'static str,
) -> Result<V> {
self.as_object()
.ok_or_else(|| not_a_object_error(self.ctx(), object_name))?
.get_required(k, object_name)
}
fn into_object_or_throw(
self,
ctx: &Ctx<'js>,
object_name: &'static str,
) -> Result<Object<'js>> {
self.into_object()
.ok_or_else(|| not_a_object_error(ctx, object_name))
}
}
pub fn not_a_object_error(ctx: &Ctx<'_>, object_name: &str) -> Error {
Exception::throw_type(ctx, &[object_name, " is not an object"].concat())
}
pub trait CreateSymbol<'js> {
fn for_description(ctx: &Ctx<'js>, description: &str) -> Result<Symbol<'js>>;
}
impl<'js> CreateSymbol<'js> for Symbol<'js> {
fn for_description(ctx: &Ctx<'js>, description: &str) -> Result<Symbol<'js>> {
BasePrimordials::get(ctx)?
.function_symbol_for
.call((description,))
}
}
pub struct Proxy<'js> {
target: Value<'js>,
options: Object<'js>,
}
impl<'js> IntoJs<'js> for Proxy<'js> {
fn into_js(self, ctx: &Ctx<'js>) -> Result<Value<'js>> {
BasePrimordials::get(ctx)?
.constructor_proxy
.construct::<_, Value>((self.target, self.options))
}
}
impl<'js> Proxy<'js> {
pub fn new(ctx: Ctx<'js>) -> Result<Self> {
let options = Object::new(ctx.clone())?;
Ok(Self {
target: Undefined.into_value(ctx),
options,
})
}
pub fn with_target(ctx: Ctx<'js>, target: Value<'js>) -> Result<Self> {
let options = Object::new(ctx)?;
Ok(Self { target, options })
}
pub fn setter<T, P>(&self, setter: Func<T, P>) -> Result<()>
where
T: IntoJsFunc<'js, P> + 'js,
{
self.options.set(PredefinedAtom::Setter, setter)?;
Ok(())
}
pub fn getter<T, P>(&self, getter: Func<T, P>) -> Result<()>
where
T: IntoJsFunc<'js, P> + 'js,
{
self.options.set(PredefinedAtom::Getter, getter)?;
Ok(())
}
}
pub fn map_to_entries<'js, K, V, M>(ctx: &Ctx<'js>, map: M) -> Result<Array<'js>>
where
M: IntoIterator<Item = (K, V)>,
K: IntoJs<'js>,
V: IntoJs<'js>,
{
let array = Array::new(ctx.clone())?;
for (idx, (key, value)) in map.into_iter().enumerate() {
let entry = Array::new(ctx.clone())?;
entry.set(0, key)?;
entry.set(1, value)?;
array.set(idx, entry)?;
}
Ok(array)
}
pub fn array_to_btree_map<'js>(
ctx: &Ctx<'js>,
array: Array<'js>,
) -> Result<BTreeMap<String, Coerced<String>>> {
let value = object_from_entries(ctx, array)?;
let value = value.into_value();
BTreeMap::from_js(ctx, value)
}
pub fn object_from_entries<'js>(ctx: &Ctx<'js>, array: Array<'js>) -> Result<Object<'js>> {
let obj = Object::new(ctx.clone())?;
for value in array.into_iter().flatten() {
if let Some(entry) = value.as_array() {
if let Ok(key) = entry.get::<Value>(0) {
if let Ok(value) = entry.get::<Value>(1) {
let _ = obj.set(key, value); //ignore result of failed
}
}
}
}
Ok(obj)
}
+101
View File
@@ -0,0 +1,101 @@
use rquickjs::{
class::{Trace, Tracer},
function::{FromParam, ParamRequirement, ParamsAccessor},
Ctx, FromJs, IntoJs, JsLifetime, Result, Type, Value,
};
/// Helper type for treating an undefined value as None, without treating null as None
#[derive(Clone)]
pub struct Undefined<T>(pub Option<T>);
impl<'js, T: FromJs<'js>> FromJs<'js> for Undefined<T> {
fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result<Self> {
if value.type_of() == Type::Undefined {
Ok(Self(None))
} else {
Ok(Self(Some(FromJs::from_js(ctx, value)?)))
}
}
}
impl<'js, T: IntoJs<'js>> IntoJs<'js> for Undefined<T> {
fn into_js(self, ctx: &Ctx<'js>) -> Result<Value<'js>> {
match self.0 {
None => Ok(Value::new_undefined(ctx.clone())),
Some(val) => val.into_js(ctx),
}
}
}
impl<T> Default for Undefined<T> {
fn default() -> Self {
Self(None)
}
}
unsafe impl<'js, T: JsLifetime<'js>> JsLifetime<'js> for Undefined<T> {
type Changed<'to> = Undefined<T::Changed<'to>>;
}
impl<'js, T: Trace<'js>> Trace<'js> for Undefined<T> {
fn trace<'a>(&self, tracer: Tracer<'a, 'js>) {
self.0.trace(tracer)
}
}
/// Helper type for converting an None into null instead of undefined.
/// Needed while rquickjs::function::Null has no IntoJs implementation
#[derive(Clone)]
pub struct Null<T>(pub Option<T>);
impl<'js, T: FromJs<'js>> FromJs<'js> for Null<T> {
fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result<Self> {
if value.type_of() == Type::Null {
Ok(Self(None))
} else {
Ok(Self(Some(FromJs::from_js(ctx, value)?)))
}
}
}
impl<'js, T: IntoJs<'js>> IntoJs<'js> for Null<T> {
fn into_js(self, ctx: &Ctx<'js>) -> Result<Value<'js>> {
match self.0 {
None => Ok(Value::new_null(ctx.clone())),
Some(val) => val.into_js(ctx),
}
}
}
unsafe impl<'js, T: JsLifetime<'js>> JsLifetime<'js> for Null<T> {
type Changed<'to> = Null<T::Changed<'to>>;
}
impl<'js, T: Trace<'js>> Trace<'js> for Null<T> {
fn trace<'a>(&self, tracer: Tracer<'a, 'js>) {
self.0.trace(tracer)
}
}
/// Helper type for accepting no value, or null, but considering undefined as a value
pub struct NullableOpt<T>(pub Option<T>);
impl<'js, T: FromJs<'js>> FromParam<'js> for NullableOpt<T> {
fn param_requirement() -> ParamRequirement {
ParamRequirement::optional()
}
fn from_param<'a>(params: &mut ParamsAccessor<'a, 'js>) -> Result<Self> {
if !params.is_empty() {
let arg = params.arg();
if arg.is_null() {
Ok(NullableOpt(None))
} else {
let ctx = params.ctx().clone();
Ok(NullableOpt(Some(T::from_js(&ctx, arg)?)))
}
} else {
Ok(NullableOpt(None))
}
}
}
@@ -0,0 +1,159 @@
use std::any::type_name;
use rquickjs::{
atom::PredefinedAtom, function::Constructor, runtime::UserDataGuard, Ctx, Function, JsLifetime,
Object, Result, Symbol,
};
use crate::{class::CUSTOM_INSPECT_SYMBOL_DESCRIPTION, result::ResultExt};
#[derive(JsLifetime)]
pub struct BasePrimordials<'js> {
// Constructors
pub constructor_map: Constructor<'js>,
pub constructor_set: Constructor<'js>,
pub constructor_date: Constructor<'js>,
pub constructor_error: Constructor<'js>,
pub constructor_type_error: Constructor<'js>,
pub constructor_range_error: Constructor<'js>,
pub constructor_regexp: Constructor<'js>,
pub constructor_uint8array: Constructor<'js>,
pub constructor_array_buffer: Constructor<'js>,
pub constructor_proxy: Constructor<'js>,
pub constructor_object: Constructor<'js>,
pub constructor_bool: Constructor<'js>,
pub constructor_number: Constructor<'js>,
pub constructor_string: Constructor<'js>,
// Prototypes
pub prototype_object: Object<'js>,
pub prototype_date: Object<'js>,
pub prototype_regexp: Object<'js>,
pub prototype_set: Object<'js>,
pub prototype_map: Object<'js>,
pub prototype_error: Object<'js>,
// Functions
pub function_array_from: Function<'js>,
pub function_array_buffer_is_view: Function<'js>,
pub function_get_own_property_descriptor: Function<'js>,
pub function_parse_int: Function<'js>,
pub function_parse_float: Function<'js>,
pub function_symbol_for: Function<'js>,
// Symbols
pub symbol_custom_inspect: Symbol<'js>,
}
pub trait Primordial<'js>
where
Self: Sized + JsLifetime<'js>,
{
fn get<'a>(ctx: &'a Ctx<'js>) -> Result<UserDataGuard<'a, Self>> {
let userdata = ctx.userdata::<Self>().or_throw_msg(
ctx,
&[
"Userdata of ",
type_name::<Self>(),
" not initialized. Call init(&ctx) on this type.",
]
.concat(),
)?;
Ok(userdata)
}
fn init<'a>(ctx: &'a Ctx<'js>) -> Result<()> {
if ctx.userdata::<Self>().is_none() {
let primoridals = Self::new(ctx)?;
let _ = ctx.store_userdata(primoridals);
}
Ok(())
}
fn new(ctx: &Ctx<'js>) -> Result<Self>;
}
impl<'js> Primordial<'js> for BasePrimordials<'js> {
fn new(ctx: &Ctx<'js>) -> Result<Self> {
let globals = ctx.globals();
let constructor_object: Constructor = globals.get(PredefinedAtom::Object)?;
let prototype_object: Object = constructor_object.get(PredefinedAtom::Prototype)?;
let constructor_proxy: Constructor = globals.get(PredefinedAtom::Proxy)?;
let function_get_own_property_descriptor: Function =
constructor_object.get(PredefinedAtom::GetOwnPropertyDescriptor)?;
let constructor_date: Constructor = globals.get(PredefinedAtom::Date)?;
let prototype_date: Object = constructor_date.get(PredefinedAtom::Prototype)?;
let constructor_map: Constructor = globals.get(PredefinedAtom::Map)?;
let prototype_map: Object = constructor_map.get(PredefinedAtom::Prototype)?;
let constructor_set: Constructor = globals.get(PredefinedAtom::Set)?;
let prototype_set: Object = constructor_set.get(PredefinedAtom::Prototype)?;
let constructor_regexp: Constructor = globals.get(PredefinedAtom::RegExp)?;
let prototype_regexp: Object = constructor_regexp.get(PredefinedAtom::Prototype)?;
let constructor_uint8array: Constructor = globals.get(PredefinedAtom::Uint8Array)?;
let constructor_arraybuffer: Constructor = globals.get(PredefinedAtom::ArrayBuffer)?;
let constructor_error: Constructor = globals.get(PredefinedAtom::Error)?;
let constructor_type_error: Constructor = ctx.globals().get(PredefinedAtom::TypeError)?;
let constructor_range_error: Constructor = ctx.globals().get(PredefinedAtom::RangeError)?;
let prototype_error: Object = constructor_error.get(PredefinedAtom::Prototype)?;
let constructor_array: Object = globals.get(PredefinedAtom::Array)?;
let function_array_from: Function = constructor_array.get(PredefinedAtom::From)?;
let constructor_array_buffer: Object = globals.get(PredefinedAtom::ArrayBuffer)?;
let function_array_buffer_is_view: Function = constructor_array_buffer.get("isView")?;
let constructor_bool: Constructor = globals.get(PredefinedAtom::Boolean)?;
let constructor_number: Constructor = globals.get(PredefinedAtom::Number)?;
let function_parse_float: Function = constructor_number.get("parseFloat")?;
let function_parse_int: Function = constructor_number.get("parseInt")?;
let constructor_string: Constructor = globals.get(PredefinedAtom::String)?;
let constructor_symbol: Constructor = globals.get(PredefinedAtom::Symbol)?;
let function_symbol_for: Function = constructor_symbol.get(PredefinedAtom::For)?;
let symbol_custom_inspect: Symbol<'js> =
function_symbol_for.call((CUSTOM_INSPECT_SYMBOL_DESCRIPTION,))?;
Ok(Self {
constructor_map,
constructor_set,
constructor_date,
constructor_proxy,
constructor_error,
constructor_type_error,
constructor_range_error,
constructor_regexp,
constructor_uint8array,
constructor_array_buffer: constructor_arraybuffer,
constructor_object,
constructor_bool,
constructor_number,
constructor_string,
prototype_object,
prototype_date,
prototype_regexp,
prototype_set,
prototype_map,
prototype_error,
function_array_from,
function_array_buffer_is_view,
function_get_own_property_descriptor,
function_parse_float,
function_parse_int,
function_symbol_for,
symbol_custom_inspect,
})
}
}
@@ -0,0 +1,24 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
#[derive(PartialEq)]
pub enum ProviderType {
None,
Resource(String), // Custom asynchronous resource
// Userland provider types
Immediate, // [Immediate] Processing by setImmediate()
Interval, // [Interval] Timer by setInterval()
MessagePort, // [MessagePort] Port for worker_threads
Microtask, // [Microtask] Processing by queueMicrotask()
TickObject, // [TickObject] Processing by process.nextTick()
Timeout, // [Timeout] Timer by setTimeout()
// Internal provider types
FsReqCallback, // [FSREQCALLBACK] Callback for file system operations
GetAddrInfoReqWrap, // [GETADDRINFOREQWRAP] When resolving DNS (dns.lookup(), etc.)
GetNameInfoReqWrap, // [GETNAMEINFOREQWRAP] DNS reverse lookup
PipeWrap, // [PIPEWRAP] Pipe connection
StatWatcher, // [STATWACHER] File monitoring such as fs.watch()
TcpWrap, // [TCPWRAP] TCP socket wrap (net.Socket, etc.)
TimerWrap, // [TIMERWRAP] Internal timer wrap (low level)
TlsWrap, // [TLSWRAP] TLS socket (HTTPS, etc.)
UdpWrap, // [UDPWRAP] UDP socket wrap (dgram module)
}
+104
View File
@@ -0,0 +1,104 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
#![allow(clippy::uninlined_format_args)]
use std::{fmt::Write, result::Result as StdResult};
use rquickjs::{Ctx, Exception, Result};
pub trait ResultExt<T> {
fn or_throw_msg(self, ctx: &Ctx, msg: &str) -> Result<T>;
fn or_throw_range(self, ctx: &Ctx, msg: &str) -> Result<T>;
fn or_throw_type(self, ctx: &Ctx, msg: &str) -> Result<T>;
fn or_throw(self, ctx: &Ctx) -> Result<T>;
}
pub trait OptionExt<T> {
fn and_then_ok<U, E, F>(self, f: F) -> StdResult<Option<U>, E>
where
F: FnOnce(T) -> StdResult<Option<U>, E>;
fn unwrap_or_else_ok<E, F>(self, f: F) -> StdResult<T, E>
where
F: FnOnce() -> StdResult<T, E>;
}
impl<T, E: std::fmt::Display> ResultExt<T> for StdResult<T, E> {
fn or_throw_msg(self, ctx: &Ctx, msg: &str) -> Result<T> {
self.map_err(|e| {
let mut message = String::with_capacity(100);
message.push_str(msg);
message.push_str(". ");
write!(message, "{}", e).unwrap();
Exception::throw_message(ctx, &message)
})
}
fn or_throw_range(self, ctx: &Ctx, msg: &str) -> Result<T> {
self.map_err(|e| {
let mut message = String::with_capacity(100);
if !message.is_empty() {
message.push_str(msg);
message.push_str(". ");
}
write!(message, "{}", e).unwrap();
Exception::throw_range(ctx, &message)
})
}
fn or_throw_type(self, ctx: &Ctx, msg: &str) -> Result<T> {
self.map_err(|e| {
let mut message = String::with_capacity(100);
if !msg.is_empty() {
message.push_str(msg);
message.push_str(". ");
}
write!(message, "{}", e).unwrap();
Exception::throw_type(ctx, &message)
})
}
fn or_throw(self, ctx: &Ctx) -> Result<T> {
self.map_err(|err| Exception::throw_message(ctx, &err.to_string()))
}
}
impl<T> ResultExt<T> for Option<T> {
fn or_throw_msg(self, ctx: &Ctx, msg: &str) -> Result<T> {
self.ok_or_else(|| Exception::throw_message(ctx, msg))
}
fn or_throw_range(self, ctx: &Ctx, msg: &str) -> Result<T> {
self.ok_or_else(|| Exception::throw_range(ctx, msg))
}
fn or_throw_type(self, ctx: &Ctx, msg: &str) -> Result<T> {
self.ok_or_else(|| Exception::throw_type(ctx, msg))
}
fn or_throw(self, ctx: &Ctx) -> Result<T> {
self.ok_or_else(|| Exception::throw_message(ctx, "Value is not present"))
}
}
impl<T> OptionExt<T> for Option<T> {
fn and_then_ok<U, E, F>(self, f: F) -> StdResult<Option<U>, E>
where
F: FnOnce(T) -> StdResult<Option<U>, E>,
{
match self {
Some(v) => f(v),
None => Ok(None),
}
}
fn unwrap_or_else_ok<E, F>(self, f: F) -> StdResult<T, E>
where
F: FnOnce() -> StdResult<T, E>,
{
match self {
Some(v) => Ok(v),
None => f(),
}
}
}
@@ -0,0 +1,337 @@
use std::fmt::Debug;
#[derive(Default, Clone)]
pub struct ReuseList<T> {
items: Vec<Option<T>>,
slots: Vec<usize>,
last_slot_idx: usize,
len: usize,
slot_size: usize,
}
impl<T: Debug> Debug for ReuseList<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ReuseList")
.field("items", &self.items)
.field("slots", &self.slots)
.finish()
}
}
impl<T> ReuseList<T> {
pub fn new() -> Self {
Self::with_capacity(0)
}
//is empty
pub fn is_empty(&self) -> bool {
self.len == 0
}
//create a with capacity
pub fn with_capacity(capacity: usize) -> Self {
Self {
items: Vec::with_capacity(capacity),
slots: Vec::with_capacity(capacity >> 2),
last_slot_idx: 0,
len: 0,
slot_size: 0,
}
}
pub fn append(&mut self, item: T) -> usize {
if self.slot_size > 0 {
//reuse empty slot if valid
let slot = self.slots[self.last_slot_idx - 1];
if slot > 0 {
self.items[slot - 1] = Some(item);
self.slots[self.last_slot_idx - 1] = 0;
if self.last_slot_idx > 1 {
self.last_slot_idx -= 1;
}
self.len += 1;
return slot - 1;
}
}
//no valid empty slots, append to end
self.items.push(Some(item));
self.len += 1;
self.items.len() - 1
}
pub fn remove(&mut self, index: usize) -> Option<T> {
if index >= self.items.len() {
return None;
}
let item = self.items[index].take();
if item.is_some() {
if self.slot_size > 0 && self.slots[self.last_slot_idx - 1] == 0 {
self.slots[self.last_slot_idx - 1] = index + 1;
} else {
self.slots.push(index + 1);
self.last_slot_idx += 1;
self.slot_size += 1;
}
self.len -= 1;
}
item
}
pub fn get(&self, index: usize) -> Option<&T> {
if index >= self.items.len() {
None
} else {
self.items[index].as_ref()
}
}
pub fn get_mut(&mut self, index: usize) -> Option<&mut T> {
if index >= self.items.len() {
None
} else {
self.items[index].as_mut()
}
}
pub fn capacity(&self) -> usize {
self.items.capacity()
}
pub fn len(&self) -> usize {
self.len
}
pub fn iter(&self) -> impl Iterator<Item = &T> {
self.items.iter().filter_map(|x| x.as_ref())
}
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> {
self.items.iter_mut().filter_map(|x| x.as_mut())
}
//implement clear
pub fn clear(&mut self) {
self.items.clear();
self.slots.clear();
self.last_slot_idx = 0;
self.len = 0;
self.slot_size = 0;
}
pub fn optimize(&mut self) {
let mut new_items = Vec::with_capacity(self.len);
for item in self.items.iter_mut() {
let a = item.take();
if a.is_some() {
new_items.push(a);
}
}
self.items = new_items;
self.slots.clear();
self.last_slot_idx = 0;
self.slot_size = 0;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new() {
let list: ReuseList<i32> = ReuseList::new();
assert_eq!(list.len(), 0);
assert_eq!(list.capacity(), 0);
assert_eq!(list.items.len(), 0);
assert_eq!(list.slots.len(), 0);
}
#[test]
fn test_with_capacity() {
let list: ReuseList<i32> = ReuseList::with_capacity(10);
assert_eq!(list.len(), 0);
assert_eq!(list.capacity(), 10);
assert_eq!(list.items.len(), 0);
assert_eq!(list.slots.len(), 0);
}
#[test]
fn test_append() {
let mut list = ReuseList::new();
assert_eq!(list.append(1), 0);
assert_eq!(list.append(2), 1);
assert_eq!(list.append(3), 2);
assert_eq!(list.len(), 3);
let items: Vec<i32> = list.iter().cloned().collect();
assert_eq!(items, vec![1, 2, 3]);
assert_eq!(list.items, vec![Some(1), Some(2), Some(3)]);
assert_eq!(list.slots, vec![]);
}
#[test]
fn test_remove() {
let mut list = ReuseList::new();
list.append(1);
list.append(2);
list.append(3);
assert_eq!(list.remove(1), Some(2));
assert_eq!(list.len(), 2);
let items: Vec<i32> = list.iter().cloned().collect();
assert_eq!(items, vec![1, 3]);
assert_eq!(list.items, vec![Some(1), None, Some(3)]);
assert_eq!(list.slots, vec![2]);
assert_eq!(list.remove(5), None);
}
#[test]
fn test_reuse_slots() {
let mut list = ReuseList::new();
list.append(1);
list.append(2);
list.append(3);
list.remove(1); // Remove 2
assert_eq!(list.append(4), 1); // Should reuse index 1
let items: Vec<i32> = list.iter().cloned().collect();
assert_eq!(items, vec![1, 4, 3]);
assert_eq!(list.items, vec![Some(1), Some(4), Some(3)]);
assert_eq!(list.slots, vec![0]);
}
#[test]
fn test_get() {
let mut list = ReuseList::new();
list.append(1);
list.append(2);
assert_eq!(list.get(0), Some(&1));
assert_eq!(list.get(1), Some(&2));
assert_eq!(list.get(2), None);
assert_eq!(list.items, vec![Some(1), Some(2)]);
assert_eq!(list.slots, vec![]);
}
#[test]
fn test_get_mut() {
let mut list = ReuseList::new();
list.append(1);
list.append(2);
if let Some(value) = list.get_mut(0) {
*value = 10;
}
assert_eq!(list.get(0), Some(&10));
assert_eq!(list.items, vec![Some(10), Some(2)]);
assert_eq!(list.slots, vec![]);
}
#[test]
fn test_iter() {
let mut list = ReuseList::new();
list.append(1);
list.append(2);
list.append(3);
list.remove(1);
let items: Vec<i32> = list.iter().cloned().collect();
assert_eq!(items, vec![1, 3]);
assert_eq!(list.items, vec![Some(1), None, Some(3)]);
assert_eq!(list.slots, vec![2]);
}
#[test]
fn test_iter_mut() {
let mut list = ReuseList::new();
list.append(1);
list.append(2);
list.append(3);
for item in list.iter_mut() {
*item *= 2;
}
let items: Vec<i32> = list.iter().cloned().collect();
assert_eq!(items, vec![2, 4, 6]);
assert_eq!(list.items, vec![Some(2), Some(4), Some(6)]);
assert_eq!(list.slots, vec![]);
}
#[test]
fn test_multiple_removes() {
let mut list = ReuseList::new();
for i in 0..5 {
list.append(i);
}
list.remove(1);
list.remove(3);
let items: Vec<i32> = list.iter().cloned().collect();
assert_eq!(items, vec![0, 2, 4]);
assert_eq!(list.items, vec![Some(0), None, Some(2), None, Some(4)]);
assert_eq!(list.slots, vec![2, 4]);
// Test reuse of both slots
list.append(10);
list.append(11);
let items: Vec<i32> = list.iter().cloned().collect();
assert_eq!(items, vec![0, 11, 2, 10, 4]);
assert_eq!(
list.items,
vec![Some(0), Some(11), Some(2), Some(10), Some(4)]
);
assert_eq!(list.slots, vec![0, 0]);
list.remove(0);
let items: Vec<i32> = list.iter().cloned().collect();
assert_eq!(items, vec![11, 2, 10, 4]);
assert_eq!(list.items, vec![None, Some(11), Some(2), Some(10), Some(4)]);
assert_eq!(list.slots, vec![1, 0]);
list.append(20);
let items: Vec<i32> = list.iter().cloned().collect();
assert_eq!(items, vec![20, 11, 2, 10, 4]);
assert_eq!(
list.items,
vec![Some(20), Some(11), Some(2), Some(10), Some(4)]
);
assert_eq!(list.slots, vec![0, 0]);
//remove all items
list.clear();
}
#[test]
fn test_optimize() {
let mut list = ReuseList::new();
list.append(1);
list.append(2);
list.append(3);
list.remove(1);
assert_eq!(list.items, vec![Some(1), None, Some(3)]);
assert_eq!(list.slots, vec![2]);
list.optimize();
assert_eq!(list.items, vec![Some(1), Some(3)]);
assert_eq!(list.slots, vec![]);
assert_eq!(list.last_slot_idx, 0);
assert_eq!(list.slot_size, 0);
let items: Vec<i32> = list.iter().cloned().collect();
assert_eq!(items, vec![1, 3]);
}
}
+149
View File
@@ -0,0 +1,149 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
use rquickjs::{prelude::Opt, Ctx, Exception, Result, Value};
use crate::result::ResultExt;
use std::io;
#[cfg(unix)]
macro_rules! generate_signal_from_str_fn {
($($signal:path),*) => {
pub fn signal_from_str(signal: &str) -> Option<i32> {
let signal = ["libc::", signal].concat();
match signal.as_str() {
$(stringify!($signal) => Some($signal),)*
_ => None,
}
}
pub fn signal_str_from_i32(signal: i32) -> Option<&'static str> {
$(if signal == $signal {
return Some(&stringify!($signal)[6..]);
})*
None
}
};
}
#[cfg(unix)]
generate_signal_from_str_fn!(
libc::SIGHUP,
libc::SIGINT,
libc::SIGQUIT,
libc::SIGILL,
libc::SIGABRT,
libc::SIGFPE,
libc::SIGKILL,
libc::SIGSEGV,
libc::SIGPIPE,
libc::SIGALRM,
libc::SIGTERM
);
#[cfg(not(unix))]
static WINDOWS_SIGTERM: i32 = -1;
pub fn parse_signal(signal: Option<Value<'_>>) -> Result<i32> {
let Some(val) = signal else {
#[cfg(unix)]
return Ok(libc::SIGTERM);
#[cfg(not(unix))]
return Ok(WINDOWS_SIGTERM);
};
if let Some(num) = val.as_number() {
let sig = num as i32;
#[cfg(unix)]
return Ok(sig);
// On Windows: 0 checks existence, anything else kills
#[cfg(not(unix))]
return Ok(if sig == 0 { 0 } else { WINDOWS_SIGTERM });
}
if let Some(str_val) = val.as_string() {
let s = str_val.to_string()?;
#[cfg(unix)]
let mapped_sig = signal_from_str(&s);
#[cfg(not(unix))]
let mapped_sig = match s.as_str() {
"SIGINT" | "SIGTERM" | "SIGKILL" | "SIGQUIT" | "SIGHUP" | "SIGUSR1" => {
Some(WINDOWS_SIGTERM)
},
_ => None,
};
return match mapped_sig {
Some(sig) => Ok(sig),
None => Err(Exception::throw_type(
val.ctx(),
&format!("Unknown signal: {}", s),
)),
};
}
Err(Exception::throw_type(val.ctx(), "Invalid signal type"))
}
#[cfg(unix)]
pub fn kill_process_raw(pid: u32, signal: i32) -> io::Result<()> {
// libc::kill returns 0 on success, -1 on error
// SAFETY: kill is a safe system call as long as the signal value is valid, which is ensured by parse_signal
if unsafe { libc::kill(pid as i32, signal) } == 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
}
#[cfg(windows)]
pub fn kill_process_raw(pid: u32, signal: i32) -> io::Result<()> {
use windows_sys::Win32::Foundation::CloseHandle;
use windows_sys::Win32::System::Threading::{OpenProcess, TerminateProcess, PROCESS_TERMINATE};
// SAFETY: OpenProcess is safe to call with valid parameters, and PROCESS_TERMINATE is a valid access right
let handle = unsafe { OpenProcess(PROCESS_TERMINATE, 0, pid) };
if handle == std::ptr::null_mut() {
return Err(io::Error::last_os_error());
}
let result = if signal == 0 {
Ok(())
} else {
// SAFETY: TerminateProcess is safe to call with a valid process handle obtained from OpenProcess
if unsafe { TerminateProcess(handle, 1) } != 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
};
// SAFETY: CloseHandle is safe to call with a valid handle obtained from OpenProcess
unsafe { CloseHandle(handle) };
result
}
pub fn kill(ctx: &Ctx<'_>, pid: u32, signal: Opt<Value<'_>>) -> Result<bool> {
let signal = parse_signal(signal.0)?;
kill_process_raw(pid, signal)
.map(|_| true)
.or_else(|e| {
// Handle "Process Not Found" / "Existence Check" logic
// If signal is 0 (check existence) and we hit a specific error, return Ok(false).
#[cfg(unix)]
let is_not_found = e.raw_os_error() == Some(libc::ESRCH); // Error 3
#[cfg(windows)]
let is_not_found = true; // On Windows, any OpenProcess failure during check implies "not found" (or not accessible)
if signal == 0 && is_not_found {
Ok(false)
} else {
Err(e)
}
})
.or_throw(ctx)
}
+17
View File
@@ -0,0 +1,17 @@
use rquickjs::{Coerced, Result, Value};
#[inline]
pub fn get_string(value: &Value<'_>) -> Result<Option<String>> {
if let Some(val) = value.as_string() {
let string = val.to_string()?;
return Ok(Some(string));
}
Ok(None)
}
pub fn get_coerced_string(value: &Value<'_>) -> Option<String> {
if let Ok(val) = value.get::<Coerced<String>>() {
return Some(val.0);
};
None
}
+13
View File
@@ -0,0 +1,13 @@
#[cfg(target_os = "macos")]
pub const PLATFORM: &str = "darwin";
#[cfg(target_os = "windows")]
pub const PLATFORM: &str = "win32";
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
pub const PLATFORM: &str = std::env::consts::OS;
#[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
pub const ARCH: &str = "x64";
#[cfg(target_arch = "aarch64")]
pub const ARCH: &str = "arm64";
#[cfg(not(any(target_arch = "x86_64", target_arch = "x86", target_arch = "aarch64")))]
pub const ARCH: &str = std::env::consts::ARCH;
+47
View File
@@ -0,0 +1,47 @@
use std::{
sync::atomic::{AtomicU64, Ordering},
time::SystemTime,
};
static TIME_ORIGIN: AtomicU64 = AtomicU64::new(0);
/// Get the current time in nanoseconds.
///
/// # Safety
/// - Good until the year 2554
/// - Always use a checked substraction since this can return 0
pub fn now_nanos() -> u64 {
SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos() as u64
}
/// Get the current time in millis.
///
/// # Safety
/// - Good until the year 2554
/// - Always use a checked substraction since this can return 0
pub fn now_millis() -> i64 {
SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as i64
}
/// Get the origin time in nanoseconds.
///
/// # Safety
/// - Good until the year 2554
/// - Always use a checked substraction since this can return 0
pub fn origin_nanos() -> u64 {
TIME_ORIGIN.load(Ordering::Relaxed)
}
// For accuracy reasons, this function should be executed when the vm is initialized
pub fn init() {
if TIME_ORIGIN.load(Ordering::Relaxed) == 0 {
let time_origin = now_nanos();
TIME_ORIGIN.store(time_origin, Ordering::Relaxed)
}
}
@@ -0,0 +1,6 @@
{
"git": {
"sha1": "c99675e50201d74554d0f667094f800e1287001b"
},
"path_in_vcs": "sys"
}
@@ -0,0 +1 @@
target
+319
View File
@@ -0,0 +1,319 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "bindgen"
version = "0.72.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
dependencies = [
"bitflags",
"cexpr",
"clang-sys",
"itertools",
"log",
"prettyplease",
"proc-macro2",
"quote",
"regex",
"rustc-hash",
"shlex",
"syn",
]
[[package]]
name = "bitflags"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
[[package]]
name = "cc"
version = "1.2.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f50d563227a1c37cc0a263f64eca3334388c01c5e4c4861a9def205c614383c"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "cexpr"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
"nom",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "clang-sys"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
dependencies = [
"glob",
"libc",
"libloading",
]
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "env_logger"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580"
dependencies = [
"humantime",
"is-terminal",
"log",
"regex",
"termcolor",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844"
[[package]]
name = "glob"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
[[package]]
name = "hermit-abi"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
[[package]]
name = "humantime"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424"
[[package]]
name = "is-terminal"
version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
dependencies = [
"hermit-abi",
"libc",
"windows-sys",
]
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]]
name = "libc"
version = "0.2.178"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091"
[[package]]
name = "libloading"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
dependencies = [
"cfg-if",
"windows-link",
]
[[package]]
name = "log"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "memchr"
version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "pretty_env_logger"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "865724d4dbe39d9f3dd3b52b88d859d66bcb2d6a0acfd5ea68a65fb66d4bdc1c"
dependencies = [
"env_logger",
"log",
]
[[package]]
name = "prettyplease"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn",
]
[[package]]
name = "proc-macro2"
version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]]
name = "rquickjs-sys"
version = "0.11.0"
dependencies = [
"bindgen",
"cc",
"pretty_env_logger",
]
[[package]]
name = "rustc-hash"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "syn"
version = "2.0.111"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "termcolor"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
dependencies = [
"winapi-util",
]
[[package]]
name = "unicode-ident"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "winapi-util"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
+68
View File
@@ -0,0 +1,68 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2021"
name = "rquickjs-sys"
version = "0.11.0"
authors = ["Mees Delzenne <mees.delzenne@gmail.com>"]
build = "build.rs"
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "QuickJS bindings for rquickjs"
readme = "README.md"
keywords = [
"quickjs",
"ecmascript",
"javascript",
"es6",
"es2020",
]
categories = ["external-ffi-bindings"]
license = "MIT"
repository = "https://github.com/DelSkayn/rquickjs.git"
[features]
bindgen = ["bindgen-rs"]
disable-assertions = []
dump-atoms = []
dump-bytecode = []
dump-free = []
dump-gc = []
dump-gc-free = []
dump-leaks = []
dump-mem = []
dump-module-resolve = []
dump-objects = []
dump-promise = []
dump-read-object = []
dump-shapes = []
logging = ["pretty_env_logger"]
update-bindings = ["bindgen"]
[lib]
name = "rquickjs_sys"
path = "src/lib.rs"
[build-dependencies.bindgen-rs]
version = "0.72"
optional = true
package = "bindgen"
[build-dependencies.cc]
version = "1"
[build-dependencies.pretty_env_logger]
version = "0.5"
optional = true
+48
View File
@@ -0,0 +1,48 @@
[package]
name = "rquickjs-sys"
version = "0.11.0"
authors = ["Mees Delzenne <mees.delzenne@gmail.com>"]
edition = "2021"
license = "MIT"
readme = "README.md"
description = "QuickJS bindings for rquickjs"
keywords = ["quickjs", "ecmascript", "javascript", "es6", "es2020"]
categories = ["external-ffi-bindings"]
repository = "https://github.com/DelSkayn/rquickjs.git"
[build-dependencies]
cc = "1"
[build-dependencies.bindgen-rs]
package = "bindgen"
version = "0.72"
optional = true
[build-dependencies.pretty_env_logger]
version = "0.5"
optional = true
[features]
bindgen = ["bindgen-rs"]
# Debug logging
logging = ["pretty_env_logger"]
# Special case for updating bundled bindings
update-bindings = ["bindgen"]
# Enable QuickJS dumps
dump-bytecode = []
dump-gc = []
dump-gc-free = []
dump-free = []
dump-leaks = []
dump-mem = []
dump-objects = []
dump-atoms = []
dump-shapes = []
dump-module-resolve = []
dump-promise = []
dump-read-object = []
# Disable QuickJS assertions for better performance
disable-assertions = []
+28
View File
@@ -0,0 +1,28 @@
patch = all #hotfix msvc exports
$(info patch = $(patch))
ifneq ($(filter hotfix all,$(patch)),)
PATCHES += get_function_proto
PATCHES += check_stack_overflow
endif
# apply patches
apply:
@cd quickjs $(foreach PATCH,$(PATCHES), && echo '*** $(PATCH) ***' && patch -p1 < ../patches/$(PATCH).patch)
# stage changes
stage:
@cd quickjs && git add *
# show unstaged diff
diff:
@cd quickjs && git diff -w
# update patch from unstaged changes
update.%:
@cd quickjs && git diff -w > ../patches/$*.patch
# reset patches
reset:
@cd quickjs && git reset --hard HEAD
+24
View File
@@ -0,0 +1,24 @@
# rquickjs-sys
[![github](https://img.shields.io/badge/github-delskayn/rquickjs-8da0cb.svg?style=for-the-badge&logo=github)](https://github.com/DelSkayn/rquickjs)
[![crates](https://img.shields.io/crates/v/rquickjs.svg?style=for-the-badge&color=fc8d62&logo=rust)](https://crates.io/crates/rquickjs-sys)
[![docs](https://img.shields.io/badge/docs.rs-rquickjs-66c2a5?style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDUxMiA1MTIiPjxwYXRoIGZpbGw9IiNmNWY1ZjUiIGQ9Ik00ODguNiAyNTAuMkwzOTIgMjE0VjEwNS41YzAtMTUtOS4zLTI4LjQtMjMuNC0zMy43bC0xMDAtMzcuNWMtOC4xLTMuMS0xNy4xLTMuMS0yNS4zIDBsLTEwMCAzNy41Yy0xNC4xIDUuMy0yMy40IDE4LjctMjMuNCAzMy43VjIxNGwtOTYuNiAzNi4yQzkuMyAyNTUuNSAwIDI2OC45IDAgMjgzLjlWMzk0YzAgMTMuNiA3LjcgMjYuMSAxOS45IDMyLjJsMTAwIDUwYzEwLjEgNS4xIDIyLjEgNS4xIDMyLjIgMGwxMDMuOS01MiAxMDMuOSA1MmMxMC4xIDUuMSAyMi4xIDUuMSAzMi4yIDBsMTAwLTUwYzEyLjItNi4xIDE5LjktMTguNiAxOS45LTMyLjJWMjgzLjljMC0xNS05LjMtMjguNC0yMy40LTMzLjd6TTM1OCAyMTQuOGwtODUgMzEuOXYtNjguMmw4NS0zN3Y3My4zek0xNTQgMTA0LjFsMTAyLTM4LjIgMTAyIDM4LjJ2LjZsLTEwMiA0MS40LTEwMi00MS40di0uNnptODQgMjkxLjFsLTg1IDQyLjV2LTc5LjFsODUtMzguOHY3NS40em0wLTExMmwtMTAyIDQxLjQtMTAyLTQxLjR2LS42bDEwMi0zOC4yIDEwMiAzOC4ydi42em0yNDAgMTEybC04NSA0Mi41di03OS4xbDg1LTM4Ljh2NzUuNHptMC0xMTJsLTEwMiA0MS40LTEwMi00MS40di0uNmwxMDItMzguMiAxMDIgMzguMnYuNnoiPjwvcGF0aD48L3N2Zz4K)](https://docs.rs/rquickjs-sys)
[![status](https://img.shields.io/github/actions/workflow/status/DelSkayn/rquickjs/ci.yml?branch=master&style=for-the-badge&logo=github-actions&logoColor=white)](https://github.com/DelSkayn/rquickjs/actions?query=workflow%3ARust)
This crate is a low level unsafe raw bindings for the [QuickJS](https://bellard.org/quickjs/) JavaScript engine.
__NOTE:__ Usually you shouldn't use this crate directly, instead use [rquickjs](https://crates.io/crates/rquickjs) crate which provides high level safe bindings.
## Patches
In order to fix bugs and get support for some unimplemented features the series of patches applies to released sources.
Hot fixes:
- Fix for _check stack overflow_ (important for Rust)
- Atomic support for `JS_NewClassID` (important for Rust)
- Infinity handling (replacement `1.0 / 0.0` to `INFINITY` constant)
Special patches:
- Reading module exports (`exports` feature)
- Reset stack function (`parallel` feature)
- MSVC support
+372
View File
@@ -0,0 +1,372 @@
#![allow(clippy::uninlined_format_args)]
use std::{
env, fs,
path::{Path, PathBuf},
process::{self},
};
// WASI logic lifted from https://github.com/bytecodealliance/javy/blob/61616e1507d2bf896f46dc8d72687273438b58b2/crates/quickjs-wasm-sys/build.rs#L18
const WASI_SDK_VERSION_MAJOR: usize = 24;
const WASI_SDK_VERSION_MINOR: usize = 0;
fn download_wasi_sdk() -> PathBuf {
let mut wasi_sdk_dir: PathBuf = env::var("OUT_DIR").unwrap().into();
wasi_sdk_dir.push("wasi-sdk");
fs::create_dir_all(&wasi_sdk_dir).unwrap();
let major_version = WASI_SDK_VERSION_MAJOR;
let minor_version = WASI_SDK_VERSION_MINOR;
let mut archive_path = wasi_sdk_dir.clone();
archive_path.push(format!("wasi-sdk-{major_version}-{minor_version}.tar.gz"));
println!("SDK tar: {archive_path:?}");
// Download archive if necessary
if !archive_path.try_exists().unwrap() {
let file_suffix = match (env::consts::OS, env::consts::ARCH) {
("linux", "x86") | ("linux", "x86_64") => "x86_64-linux",
("linux", "aarch64") => "arm64-linux",
("macos", "x86") | ("macos", "x86_64") => "x86_64-macos",
("macos", "aarch64") => "arm64-macos",
("windows", "x86") | ("windows", "x86_64") => "x86_64-windows",
("windows", "aarch64") => "arm64-windows",
other => panic!("Unsupported platform tuple {:?}", other),
};
let uri = format!("https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-{major_version}/wasi-sdk-{major_version}.{minor_version}-{file_suffix}.tar.gz");
println!("Downloading WASI SDK archive from {uri} to {archive_path:?}");
let output = process::Command::new("curl")
.args([
"--location",
"-o",
archive_path.to_string_lossy().as_ref(),
uri.as_ref(),
])
.output()
.expect("failed to download the WASI SDK with curl");
println!("curl output: {}", String::from_utf8_lossy(&output.stdout));
println!("curl err: {}", String::from_utf8_lossy(&output.stderr));
if !output.status.success() {
panic!(
"curl WASI SDK failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
}
let mut test_binary = wasi_sdk_dir.clone();
test_binary.extend(["bin", "wasm-ld"]);
// Extract archive if necessary
if !test_binary.try_exists().unwrap() {
println!("Extracting WASI SDK archive {archive_path:?}");
let output = process::Command::new("tar")
.args([
"-zxf",
archive_path.to_string_lossy().as_ref(),
"--strip-components",
"1",
])
.current_dir(&wasi_sdk_dir)
.output()
.unwrap();
if !output.status.success() {
panic!(
"Unpacking WASI SDK failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
}
wasi_sdk_dir
}
fn get_wasi_sdk_path() -> PathBuf {
std::env::var_os("WASI_SDK")
.map(PathBuf::from)
.unwrap_or_else(download_wasi_sdk)
}
fn main() {
#[cfg(feature = "logging")]
pretty_env_logger::init();
let features = [
"bindgen",
"update-bindings",
"dump-bytecode",
"dump-gc",
"dump-gc-free",
"dump-free",
"dump-leaks",
"dump-mem",
"dump-objects",
"dump-atoms",
"dump-shapes",
"dump-module-resolve",
"dump-promise",
"dump-read-object",
"disable-assertions",
];
for feature in &features {
println!("cargo:rerun-if-env-changed={}", feature_to_cargo(feature));
}
println!("cargo:rerun-if-env-changed=CARGO_CFG_SANITIZE");
println!("cargo:rerun-if-changed=quickjs");
let src_dir = Path::new("quickjs");
let out_dir = env::var("OUT_DIR").expect("No OUT_DIR env var is set by cargo");
let out_dir = Path::new(&out_dir);
let header_files = [
"builtin-array-fromasync.h",
"dtoa.h",
"libregexp-opcode.h",
"libregexp.h",
"libunicode-table.h",
"libunicode.h",
"list.h",
"quickjs-atom.h",
"quickjs-opcode.h",
"quickjs-c-atomics.h",
"quickjs.h",
"cutils.h",
];
let source_files = [
"libregexp.c",
"libunicode.c",
"cutils.c",
"quickjs.c",
"dtoa.c",
];
let mut defines: Vec<(String, Option<&str>)> = vec![("_GNU_SOURCE".into(), None)];
#[cfg(feature = "disable-assertions")]
defines.push(("NDEBUG".into(), None));
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap();
let target_env = env::var("CARGO_CFG_TARGET_ENV").unwrap();
let mut builder = cc::Build::new();
builder
.extra_warnings(false)
.flag_if_supported("-Wno-implicit-const-int-float-conversion")
//.flag("-Wno-array-bounds")
//.flag("-Wno-format-truncation")
;
match env::var("CARGO_CFG_SANITIZE").as_deref() {
Ok("address") => {
builder
.flag("-fsanitize=address")
.flag("-fno-sanitize-recover=all")
.flag("-fno-omit-frame-pointer");
}
Ok("memory") => {
builder
.flag("-fsanitize=memory")
.flag("-fno-sanitize-recover=all")
.flag("-fno-omit-frame-pointer");
}
Ok("thread") => {
builder
.flag("-fsanitize=thread")
.flag("-fno-sanitize-recover=all")
.flag("-fno-omit-frame-pointer");
}
Ok(x) => println!("cargo:warning=Unsupported sanitize_option: '{x}'"),
_ => {}
}
let mut bindgen_cflags = vec![];
if target_os == "windows" {
if target_env == "msvc" {
env::set_var(
"CFLAGS",
"/DWIN32_LEAN_AND_MEAN /std:c11 /experimental:c11atomics",
);
} else {
env::set_var("CFLAGS", "-DWIN32_LEAN_AND_MEAN -std=c11");
}
}
if target_os == "wasi" {
// pretend we're emscripten - there are already ifdefs that match
// also, wasi doesn't ahve FE_DOWNWARD or FE_UPWARD
defines.push(("EMSCRIPTEN".into(), Some("1")));
defines.push(("FE_DOWNWARD".into(), Some("0")));
defines.push(("FE_UPWARD".into(), Some("0")));
}
for file in source_files.iter().chain(header_files.iter()) {
fs::copy(src_dir.join(file), out_dir.join(file))
.expect("Unable to copy source; try 'git submodule update --init'");
}
fs::copy("quickjs.bind.h", out_dir.join("quickjs.bind.h")).expect("Unable to copy source");
// SAU-103: use cargo.build's target-specific WASI compiler/archive tools.
if target_os == "wasi" && env::var_os("CC_wasm32_wasip2").is_none() {
let wasi_sdk_path = get_wasi_sdk_path();
if !wasi_sdk_path.try_exists().unwrap() {
panic!(
"wasi-sdk not installed in specified path of {}",
wasi_sdk_path.display()
);
}
env::set_var("CC", wasi_sdk_path.join("bin/clang").to_str().unwrap());
env::set_var("AR", wasi_sdk_path.join("bin/ar").to_str().unwrap());
let sysroot = format!(
"--sysroot={}",
wasi_sdk_path.join("share/wasi-sysroot").display()
);
env::set_var("CFLAGS", &sysroot);
bindgen_cflags.push(sysroot);
}
// generating bindings
bindgen(
out_dir,
out_dir.join("quickjs.bind.h"),
&defines,
bindgen_cflags,
);
for (name, value) in &defines {
builder.define(name, *value);
}
for src in &source_files {
builder.file(out_dir.join(src));
}
let tool = builder.get_compiler();
println!("cargo:warning=SAU-103 QuickJS C compiler {:?} args {:?}", tool.path(), tool.args());
builder.compile("libquickjs.a");
}
fn feature_to_cargo(name: impl AsRef<str>) -> String {
format!("CARGO_FEATURE_{}", feature_to_define(name))
}
fn feature_to_define(name: impl AsRef<str>) -> String {
name.as_ref().to_uppercase().replace('-', "_")
}
#[cfg(not(feature = "bindgen"))]
fn bindgen<'a, D, H, X, K, V>(out_dir: D, _header_file: H, _defines: X, _add_cflags: Vec<String>)
where
D: AsRef<Path>,
H: AsRef<Path>,
X: IntoIterator<Item = &'a (K, Option<V>)>,
K: AsRef<str> + 'a,
V: AsRef<str> + 'a,
{
let target = env::var("TARGET").unwrap();
if !Path::new("./")
.join("src")
.join("bindings")
.join(format!("{}.rs", target))
.canonicalize()
.map(|x| x.exists())
.unwrap_or(false)
{
println!(
"cargo:warning=rquickjs probably doesn't ship bindings for platform `{}({})`. try the `bindgen` feature instead.",
target,
env::var("BUILD_TARGET").unwrap_or("n/a".into())
);
}
let bindings_file = out_dir.as_ref().join("bindings.rs");
fs::write(
bindings_file,
format!(
r#"macro_rules! bindings_env {{
("TARGET") => {{ "{target}" }};
}}"#
),
)
.unwrap();
}
#[cfg(feature = "bindgen")]
fn bindgen<'a, D, H, X, K, V>(out_dir: D, header_file: H, defines: X, mut add_cflags: Vec<String>)
where
D: AsRef<Path>,
H: AsRef<Path>,
X: IntoIterator<Item = &'a (K, Option<V>)>,
K: AsRef<str> + 'a,
V: AsRef<str> + 'a,
{
let mut target = env::var("TARGET").unwrap();
let out_dir = out_dir.as_ref();
let header_file = header_file.as_ref();
// *-pc-windows-gnullvm is special for Rust, Clang accepts only
// *-pc-windows-gnu
if target.ends_with("windows-gnullvm") {
target = target.replace("llvm", "");
}
let mut cflags = vec![format!("--target={}", target)];
cflags.append(&mut add_cflags);
//format!("-I{}", out_dir.parent().display()),
for (name, value) in defines {
cflags.push(if let Some(value) = value {
format!("-D{}={}", name.as_ref(), value.as_ref())
} else {
format!("-D{}", name.as_ref())
});
}
let mut builder = bindgen_rs::Builder::default()
.use_core()
.detect_include_paths(true)
.clang_arg("-xc")
.clang_arg("-v")
.clang_args(cflags)
.size_t_is_usize(false)
.header(header_file.display().to_string())
.allowlist_type("JS.*")
.allowlist_function("js.*")
.allowlist_function("JS.*")
.allowlist_function("__JS.*")
.allowlist_var("JS.*")
.opaque_type("FILE")
.blocklist_type("FILE")
.blocklist_function("JS_DumpMemoryUsage");
if env::var("CARGO_CFG_TARGET_OS").unwrap() == "wasi" {
builder = builder.clang_arg("-fvisibility=default");
}
let bindings = builder.generate().expect("Unable to generate bindings");
let bindings_file = out_dir.join("bindings.rs");
bindings
.write_to_file(&bindings_file)
.expect("Couldn't write bindings");
// Special case to support bundled bindings
if env::var("CARGO_FEATURE_UPDATE_BINDINGS").is_ok() {
let dest_dir = Path::new("src").join("bindings");
fs::create_dir_all(&dest_dir).unwrap();
let dest_file = format!("{}.rs", target);
fs::copy(&bindings_file, dest_dir.join(dest_file)).unwrap();
}
}
+16
View File
@@ -0,0 +1,16 @@
// A header which imports the all symbols of the quickjs header but also exports
// the static atoms.
#include "quickjs.h"
#if !defined(EMSCRIPTEN) && !defined(_MSC_VER)
#define CONFIG_ATOMICS
#endif
enum {
__JS_ATOM_NULL = JS_ATOM_NULL,
#define DEF(name, str) JS_ATOM_##name,
#include "quickjs-atom.h"
#undef DEF
JS_ATOM_END,
};
@@ -0,0 +1,4 @@
# Don't show changes in generated files when doing git diff
gen/** -diff
@@ -0,0 +1,7 @@
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
@@ -0,0 +1,780 @@
name: ci
on:
pull_request:
paths:
- '**'
- '!.gitignore'
- '!LICENSE'
- '!README.md'
- '!docs/**'
- '!examples/**'
- '.github/workflows/ci.yml'
push:
branches:
- master
paths:
- '**'
- '!.gitignore'
- '!LICENSE'
- '!README.md'
- '!docs/**'
- '!examples/**'
- '.github/workflows/ci.yml'
jobs:
codegen:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: build
run: |
make codegen
- name: Check if the git repository is clean
run: (exit "$(git status --porcelain --untracked-files=no | head -255 | wc -l)") || (echo "Dirty git tree"; git diff; exit 1)
ci:
runs-on: ${{ matrix.config.os }}
name: ${{ matrix.config.os }} (${{ matrix.config.configType }}${{ matrix.config.arch }})
env:
ASAN_OPTIONS: halt_on_error=1
MSAN_OPTIONS: halt_on_error=1
UBSAN_OPTIONS: halt_on_error=1
defaults:
run:
shell: ${{ matrix.config.arch == '' && 'sh' || 'alpine.sh' }} {0}
strategy:
fail-fast: false
matrix:
config:
# see https://github.com/quickjs-ng/quickjs/issues/879 - tl;dr flaky
#- { os: ubuntu-24.04-arm, configType: Debug }
#- { os: ubuntu-24.04-arm, configType: Release, runTest262: true }
- { os: ubuntu-latest, configType: Debug }
- { os: ubuntu-latest, configType: Release, runTest262: true }
- { os: ubuntu-latest, configType: examples }
- { os: ubuntu-latest, configType: shared }
- { os: ubuntu-latest, configType: asan+ubsan, runTest262: true }
- { os: ubuntu-latest, configType: msan }
- { os: ubuntu-latest, configType: tcc }
- { os: ubuntu-latest, arch: x86, runTest262: true }
- { os: ubuntu-latest, arch: riscv64 }
- { os: ubuntu-latest, arch: s390x }
- { os: macos-14, configType: Debug }
- { os: macos-14, configType: Release }
- { os: macos-14, configType: examples }
- { os: macos-14, configType: shared }
- { os: macos-14, configType: asan+ubsan }
steps:
- uses: actions/checkout@v6
with:
submodules: true
- name: install TCC
if: ${{ matrix.config.configType == 'tcc' }}
run: |
pushd /tmp
git clone https://repo.or.cz/tinycc.git
cd tinycc
git checkout c21576f8a32715ab439690d18184b0e02022bbbd
./configure
make
sudo make install
tcc -v
popd
echo "CC=tcc" >> $GITHUB_ENV;
- name: Install extra dependencies
if: ${{ matrix.config.arch != '' }}
shell: 'sh'
run: |
sudo apt update && sudo apt install -y binfmt-support
- name: Setup Alpine
if: ${{ matrix.config.arch != '' }}
uses: jirutka/setup-alpine@v1
with:
arch: ${{ matrix.config.arch }}
packages: "build-base make cmake git"
- name: uname
run: uname -a
- name: Set build ENV vars
run: |
if [ "${{ matrix.config.configType }}" = "Debug" ]; then
echo "BUILD_TYPE=Debug" >> $GITHUB_ENV;
elif [ "${{ matrix.config.configType }}" = "examples" ]; then
echo "QJS_BUILD_EXAMPLES=ON" >> $GITHUB_ENV;
elif [ "${{ matrix.config.configType }}" = "shared" ]; then
echo "BUILD_SHARED_LIBS=ON" >> $GITHUB_ENV;
elif [ "${{ matrix.config.configType }}" = "asan+ubsan" ]; then
echo "BUILD_TYPE=RelWithDebInfo" >> $GITHUB_ENV;
echo "QJS_ENABLE_ASAN=ON" >> $GITHUB_ENV;
echo "QJS_ENABLE_UBSAN=ON" >> $GITHUB_ENV;
elif [ "${{ matrix.config.configType }}" = "msan" ]; then
echo "BUILD_TYPE=RelWithDebInfo" >> $GITHUB_ENV;
echo "QJS_ENABLE_MSAN=ON" >> $GITHUB_ENV;
echo "CC=clang" >> $GITHUB_ENV;
fi
- name: build
run: |
make \
BUILD_TYPE=$BUILD_TYPE \
QJS_BUILD_EXAMPLES=$QJS_BUILD_EXAMPLES \
BUILD_SHARED_LIBS=$BUILD_SHARED_LIBS \
QJS_ENABLE_ASAN=$QJS_ENABLE_ASAN \
QJS_ENABLE_UBSAN=$QJS_ENABLE_UBSAN \
QJS_ENABLE_MSAN=$QJS_ENABLE_MSAN
- name: stats
if: ${{ matrix.config.configType != 'examples' }}
run: |
make stats
- name: cxxtest
run: |
make cxxtest
- name: test
if: ${{ matrix.config.configType != 'examples' }}
run: |
make test
- name: test examples
if: ${{ matrix.config.configType == 'examples' }}
run: |
cp build/fib.so examples/
cp build/point.so examples/
./build/qjs examples/test_fib.js
./build/qjs examples/test_point.js
./build/qjs tests/test_bjson.js
./build/function_source
- name: test 262
if: ${{ matrix.config.runTest262 }}
run: |
git submodule update --init --checkout --depth 1
make test262
- name: test standalone
run: |
./build/qjs -c examples/hello.js -o hello
./hello
- name: test api
run: |
./build/api-test
windows-msvc:
runs-on: ${{ matrix.config.os }}
strategy:
fail-fast: false
matrix:
config:
- { arch: x64, vsArch: x64, buildType: Debug, os: windows-latest }
- { arch: x64, vsArch: x64, buildType: Release, os: windows-latest }
- { arch: Win32, vsArch: x86, buildType: Debug, os: windows-latest }
- { arch: Win32, vsArch: x86, buildType: Release, os: windows-latest }
- { arch: ARM64, vsArch: arm64, buildType: Debug, os: windows-11-arm }
- { arch: ARM64, vsArch: arm64, buildType: Release, os: windows-11-arm }
steps:
- uses: actions/checkout@v6
- name: build
run: |
cmake -B build -DQJS_BUILD_EXAMPLES=ON -G "Visual Studio 17 2022" -A ${{matrix.config.arch}}
cmake --build build --config ${{matrix.config.buildType}}
- name: stats
run: |
build\${{matrix.config.buildType}}\qjs.exe -qd
- name: test
run: |
cp build\${{matrix.config.buildType}}\fib.dll examples\
cp build\${{matrix.config.buildType}}\point.dll examples\
build\${{matrix.config.buildType}}\qjs.exe examples\test_fib.js
build\${{matrix.config.buildType}}\qjs.exe examples\test_point.js
build\${{matrix.config.buildType}}\run-test262.exe -c tests.conf
build\${{matrix.config.buildType}}\function_source.exe
- name: test standalone
run: |
build\${{matrix.config.buildType}}\qjs.exe -c examples\hello.js -o hello.exe
.\hello.exe
- name: test api
run: |
build\${{matrix.config.buildType}}\api-test.exe
- name: Set up Visual Studio shell
uses: egor-tensin/vs-shell@v2
with:
arch: ${{ matrix.config.vsArch }}
- name: cxxtest
run: |
cl.exe /DJS_NAN_BOXING=0 /Zs cxxtest.cc
cl.exe /DJS_NAN_BOXING=1 /Zs cxxtest.cc
windows-clang:
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
buildType: [Debug, Release]
steps:
- uses: actions/checkout@v6
- name: build
run: |
git submodule update --init --checkout --depth 1
cmake -B build -DQJS_BUILD_EXAMPLES=ON -G "Visual Studio 17 2022" -T ClangCL
cmake --build build --config ${{matrix.buildType}}
- name: stats
run: |
build\${{matrix.buildType}}\qjs.exe -qd
- name: cxxtest
run: |
clang-cl.exe /DJS_NAN_BOXING=0 /Zs cxxtest.cc
clang-cl.exe /DJS_NAN_BOXING=1 /Zs cxxtest.cc
- name: test
run: |
cp build\${{matrix.buildType}}\fib.dll examples\
cp build\${{matrix.buildType}}\point.dll examples\
build\${{matrix.buildType}}\qjs.exe examples\test_fib.js
build\${{matrix.buildType}}\qjs.exe examples\test_point.js
build\${{matrix.buildType}}\run-test262.exe -c tests.conf
build\${{matrix.buildType}}\function_source.exe
- name: test api
run: |
build\${{matrix.buildType}}\api-test.exe
windows-ninja:
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
buildType: [Debug, Release]
steps:
- uses: actions/checkout@v6
- name: install ninja
run: |
choco install ninja
ninja.exe --version
- name: build
run: |
git submodule update --init --checkout --depth 1
cmake -B build -DQJS_BUILD_EXAMPLES=ON -DCMAKE_BUILD_TYPE=${{matrix.buildType}} -G "Ninja"
cmake --build build
- name: stats
run: |
build\qjs.exe -qd
- name: test
run: |
cp build\fib.dll examples\
cp build\point.dll examples\
build\qjs.exe examples\test_fib.js
build\qjs.exe examples\test_point.js
build\run-test262.exe -c tests.conf
build\function_source.exe
- name: test api
run: |
build\api-test.exe
windows-sdk:
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
arch: [x64, Win32]
buildType: [Debug, Release]
steps:
- uses: actions/checkout@v6
- name: Install windows sdk
uses: ChristopheLav/windows-sdk-install@v1
with:
version-sdk: 26100
features: 'OptionId.DesktopCPPx86,OptionId.DesktopCPPx64'
- name: build
run: |
cmake -B build -DQJS_BUILD_EXAMPLES=ON -DCMAKE_SYSTEM_VERSION="10.0.26100.0" -A ${{matrix.arch}}
cmake --build build --config ${{matrix.buildType}}
- name: stats
run: |
build\${{matrix.buildType}}\qjs.exe -qd
- name: test
run: |
cp build\${{matrix.buildType}}\fib.dll examples\
cp build\${{matrix.buildType}}\point.dll examples\
build\${{matrix.buildType}}\qjs.exe examples\test_fib.js
build\${{matrix.buildType}}\qjs.exe examples\test_point.js
build\${{matrix.buildType}}\run-test262.exe -c tests.conf
build\${{matrix.buildType}}\function_source.exe
- name: test api
run: |
build\${{matrix.buildType}}\api-test.exe
windows-mingw:
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
buildType: [Debug, Release]
sys:
- mingw32
- mingw64
- clang64
- ucrt64
defaults:
run:
shell: msys2 {0}
steps:
- uses: actions/checkout@v6
- name: Setup MSYS2
uses: msys2/setup-msys2@v2
with:
update: true
msystem: ${{matrix.sys}}
install: >-
git
make
pacboy: >-
cmake:p
ninja:p
toolchain:p
- name: build
run: |
make BUILD_TYPE=${{matrix.buildType}}
- name: stats
run: |
make stats
ldd build/qjs
- name: cxxtest
if: ${{ matrix.sys != 'clang64' }}
run: |
make cxxtest
- name: cxxtest
if: ${{ matrix.sys == 'clang64' }}
run: |
make cxxtest CXX=clang++
- name: test
run: |
make test
- name: test standalone
run: |
./build/qjs -c examples/hello.js -o hello.exe
./hello
- name: test api
run: |
./build/api-test
windows-mingw-shared:
runs-on: windows-latest
defaults:
run:
shell: msys2 {0}
steps:
- uses: actions/checkout@v6
- name: Setup MSYS2
uses: msys2/setup-msys2@v2
with:
install: >-
git
make
pacboy: >-
cmake:p
ninja:p
toolchain:p
- name: build
run: |
make BUILD_SHARED_LIBS=ON
ldd build/qjs
- name: stats
run: |
make stats
emscripten:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: mymindstorm/setup-emsdk@v14
- name: check emsdk
run: emcc -v
- name: build
run: |
emcmake cmake -B build -DQJS_BUILD_LIBC=ON
emmake make -C build qjs_wasm "-j$(getconf _NPROCESSORS_ONLN)"
- name: result
run: ls -lh build
wasi:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v6
- uses: jcbhmr/setup-wasmtime@v2
- name: setup wasi-sdk
run: |
wget -nv https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-25/wasi-sdk-25.0-x86_64-linux.deb -P /tmp
sudo apt install /tmp/wasi-sdk*.deb
- name: test
run: |
cmake -B build -DCMAKE_TOOLCHAIN_FILE=/opt/wasi-sdk/share/cmake/wasi-sdk.cmake
make -C build qjs_exe
wasmtime run build/qjs -qd
echo "console.log('hello wasi!');" > t.js
wasmtime run --dir . build/qjs t.js
cygwin:
runs-on: windows-latest
defaults:
run:
shell: C:\cygwin\bin\bash.exe --login --norc -eo pipefail -o igncr '{0}'
env:
CYGWIN_NOWINPATH: 1
CHERE_INVOKING: 1
steps:
- name: Set up Cygwin
uses: cygwin/cygwin-install-action@master
with:
packages: make cmake gcc-g++ bash
- uses: actions/checkout@v6
- name: build
run: make
- name: stats
run: make stats
- name: cxxtest
run: |
make cxxtest
- name: test
run: make test
- name: test api
run: |
./build/api-test
openbsd:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: build + test
uses: vmactions/openbsd-vm@v1
with:
usesh: true
prepare: |
pkg_add cmake gmake
run: |
gmake
gmake stats
freebsd:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: build + test
uses: vmactions/freebsd-vm@v1
with:
usesh: true
prepare: |
pkg install -y cmake gmake
run: |
gmake
gmake stats
android:
runs-on: ubuntu-latest
container: reactnativecommunity/react-native-android:v13.0
steps:
- uses: actions/checkout@v6
- name: Configure android arm64
# see build options you can use in https://developer.android.com/ndk/guides/cmake
run: |
mkdir build
cd build
"$ANDROID_HOME/cmake/3.22.1/bin/cmake" "-DCMAKE_TOOLCHAIN_FILE=$ANDROID_HOME/ndk/26.0.10792818/build/cmake/android.toolchain.cmake" -DCMAKE_BUILD_TYPE=Release -DANDROID_ABI="arm64-v8a" -DANDROID_PLATFORM=android-24 -DQJS_BUILD_LIBC=ON ..
- name: Build android arm64
run: |
"$ANDROID_HOME/cmake/3.22.1/bin/cmake" --build build --target qjs
ls -lh build
ios:
runs-on: macos-latest
steps:
- uses: actions/checkout@v6
- name: configure
run: |
cmake -B build -GXcode -DCMAKE_SYSTEM_NAME:STRING=iOS -DCMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED:BOOL=NO -DQJS_BUILD_LIBC=ON
- name: build
run: |
cmake --build build --config Release --target qjs
ls -lh build
mimalloc-linux:
runs-on: ubuntu-24.04
env:
QJS_BUILD_CLI_WITH_MIMALLOC: ON
MIMALLOC_SHOW_STATS: 1
steps:
- uses: actions/checkout@v6
- name: install dependencies
run: |
sudo apt update && sudo apt -y install libmimalloc-dev
- name: build
run: |
make
- name: cxxtest
run: |
make cxxtest
- name: test
run: |
make test
mimalloc-macos:
runs-on: macos-latest
env:
QJS_BUILD_CLI_WITH_STATIC_MIMALLOC: ON
MIMALLOC_SHOW_STATS: 1
steps:
- uses: actions/checkout@v6
- name: install dependencies
run: |
brew install mimalloc
- name: build
run: |
make
- name: cxxtest
run: |
make cxxtest
- name: test
run: |
make test
amalgam:
strategy:
matrix:
# TODO(bnoordhuis) test on windows
config:
- { os: ubuntu-latest }
- { os: macos-latest }
runs-on: ${{ matrix.config.os }}
steps:
- uses: actions/checkout@v6
- name: build
run: |
make
- name: create amalgamation
run: |
make amalgam
- name: build amalgamation
run: |
unzip -d "$RUNNER_TEMP" build/quickjs-amalgam.zip
cc -Wall -I. -o "$RUNNER_TEMP/run-test262.o" -c run-test262.c
cc -Wall -I/ -DQJS_BUILD_LIBC -o "$RUNNER_TEMP/quickjs-amalgam.o" -c "$RUNNER_TEMP/quickjs-amalgam.c"
cc -o "$RUNNER_TEMP/run-test262" "$RUNNER_TEMP/run-test262.o" "$RUNNER_TEMP/quickjs-amalgam.o" -lm
- name: test
run: |
make test RUN262="$RUNNER_TEMP/run-test262"
jscheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: jscheck
run: |
make jscheck
parserless:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: build
run: |
make QJS_DISABLE_PARSER=ON QJS_BUILD_EXAMPLES=ON
- name: test
run: |
./build/hello
./build/hello_module
meson:
runs-on: ${{ matrix.platform }}
name: meson on ${{ matrix.platform }} (${{ matrix.mode.name }} ${{ matrix.flavor }}, ${{ matrix.features.name }})
strategy:
fail-fast: false
matrix:
flavor:
- debug
- release
features:
- name: default
args: ""
- name: libc
args: -Dlibc=true
- name: mimalloc
args: -Dcli_mimalloc=auto
mode:
- name: default
args: -Dtests=enabled
extra_envs: {}
# Alternative compiler setups
- name: gcc
args: -Dtests=enabled
extra_envs:
CC: gcc
CXX: g++
- name: clang
args: -Dtests=enabled
extra_envs:
CC: clang
CXX: clang++
- name: sanitize
args: >-
"-Db_sanitize=address,undefined"
extra_envs: {}
# This is for MSVC, which only supports AddressSanitizer.
# https://learn.microsoft.com/en-us/cpp/sanitizers/
- name: sanitize+asanonly
args: -Db_sanitize=address
extra_envs:
ASAN_OPTIONS: report_globals=0:halt_on_error=1:abort_on_error=1:print_summary=1
- name: clang+sanitize
args: >-
"-Db_sanitize=address,undefined"
extra_envs:
CC: clang
CXX: clang++
- name: clang+msan
args: -Db_sanitize=memory
extra_envs:
CC: clang
CXX: clang++
# default clang on GitHub hosted runners is from MSYS2.
# Use Visual Studio supplied clang-cl instead.
- name: clang-cl+sanitize
args: >-
"-Db_sanitize=address,undefined"
extra_envs:
CC: clang-cl
CXX: clang-cl
platform:
- ubuntu-latest
- windows-latest
- macos-latest
exclude:
# clang-cl only makes sense on windows.
- platform: ubuntu-latest
mode:
name: clang-cl+sanitize
- platform: macos-latest
mode:
name: clang-cl+sanitize
# Use clang-cl instead of MSYS2 clang.
#
# we already tested clang+sanitize on linux,
# if this doesn't work, it should be an issue for MSYS2 team to consider.
- platform: windows-latest
mode:
name: clang
- platform: windows-latest
mode:
name: clang+sanitize
- platform: windows-latest
mode:
name: clang+msan
# MSVC-only sanitizers
- platform: ubuntu-latest
mode:
name: sanitize+asanonly
- platform: macos-latest
mode:
name: sanitize+asanonly
- platform: windows-latest
mode:
name: sanitize
# clang is the default on macos
# also gcc is an alias to clang
- platform: macos-latest
mode:
name: clang
- platform: macos-latest
mode:
name: gcc
# gcc is the default on linux
- platform: ubuntu-latest
mode:
name: gcc
# only run sanitizer tests on linux
#
# gcc/clang's codegen shouldn't massively change across platforms,
# and linux supports most of the sanitizers.
- platform: macos-latest
mode:
name: clang+sanitize
- platform: macos-latest
mode:
# macos does not support msan
name: clang+msan
- platform: macos-latest
mode:
name: sanitize
steps:
- name: Setup meson
run: |
pipx install meson ninja
- name: Install mimalloc
if: ${{ matrix.platform == 'ubuntu-latest' && matrix.features.name == 'mimalloc' }}
run: sudo apt update && sudo apt -y install libmimalloc-dev
- name: Install mimalloc
if: ${{ matrix.platform == 'macos-latest' && matrix.features.name == 'mimalloc' }}
run: brew install mimalloc
# TODO: Install mimalloc on Windows
# You need to:
# - checkout mimalloc
# - `msbuild mimalloc\ide\vs2022\mimalloc.sln`
# - not cmake, because https://github.com/microsoft/mimalloc/issues/575#issuecomment-1112723975
# - it is possible to integrate vcpkg with meson, but:
# - 1. the above issue
# - 2. the vcpkg port of mimalloc is outdated, and still broken on server 2019
# - or maintain a meson port for mimalloc (and ensure it behaves correctly)
# - Make it findable with cmake
# - not the simpler pkg-config. although there was pkg-config in PATH,
# but that was from strawberry perl and thus broken
# ... good job, microsoft :))))
- name: Checkout
uses: actions/checkout@v6
- name: Activate MSVC and Configure
if: ${{ matrix.platform == 'windows-latest' }}
env: ${{ matrix.mode.extra_envs }}
run: |
meson setup build-${{ matrix.flavor }} --buildtype=${{ matrix.flavor }} ${{ matrix.mode.args }} ${{ matrix.features.args }} --vsenv
- name: Configuring
if: ${{ matrix.platform != 'windows-latest' }}
env: ${{ matrix.mode.extra_envs }}
run: |
meson setup build-${{ matrix.flavor }} --buildtype=${{ matrix.flavor }} ${{ matrix.mode.args }} ${{ matrix.features.args }}
- name: Building
run: |
meson compile -C build-${{ matrix.flavor }}
- name: Running tests
env: ${{ matrix.mode.extra_envs }}
run: |
meson test -C build-${{ matrix.flavor }} --timeout-multiplier 5 --print-errorlogs
meson test --benchmark -C build-${{ matrix.flavor }} --timeout-multiplier 5 --print-errorlogs
- uses: actions/upload-artifact@v6
if: ${{ failure() }}
with:
name: ${{ matrix.platform }}-${{ matrix.mode.name }}-${{ matrix.features.name }}-${{ matrix.flavor }}-logs
path: build-${{ matrix.flavor }}/meson-logs
@@ -0,0 +1,49 @@
name: Docs
on:
push:
branches:
- master
paths:
- 'docs/**'
- '.github/workflows/*docs.yml'
jobs:
build:
name: Build Docusaurus
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version-file: 'docs/.nvmrc'
- name: Install dependencies
working-directory: ./docs
run: npm install
- name: Build
working-directory: ./docs
run: npm run build
- name: Upload Build Artifact
uses: actions/upload-pages-artifact@v4
with:
path: docs/build
deploy:
name: Deploy to GitHub Pages
needs: build
# Grant GITHUB_TOKEN the permissions required to make a Pages deployment
permissions:
pages: write # to deploy to Pages
id-token: write # to verify the deployment originates from an appropriate source
# Deploy to the github-pages environment
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
@@ -0,0 +1,155 @@
name: release
on:
pull_request:
push:
tags:
- "v*.*.*"
jobs:
check_meson_version:
runs-on: ubuntu-22.04
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
steps:
- uses: actions/checkout@v6
- name: install meson and ninja
run: |
pip install --upgrade meson ninja
- name: check meson version
run: |
MESON_VERSION=$(meson introspect meson.build --projectinfo | jq -r '.version')
test "${{ github.ref_name }}" = "v${MESON_VERSION}"
linux:
runs-on: ubuntu-22.04
strategy:
fail-fast: false
matrix:
arch: [aarch64, riscv64, x86, x86_64]
steps:
- uses: actions/checkout@v6
- uses: jirutka/setup-alpine@v1
with:
arch: ${{matrix.arch}}
packages: "build-base make cmake"
- name: build
shell: alpine.sh {0}
run: |
mkdir build
cd build
cmake -DQJS_BUILD_CLI_STATIC=ON ..
cd ..
cmake --build build --target qjs_exe -j$(getconf _NPROCESSORS_ONLN)
cmake --build build --target qjsc -j$(getconf _NPROCESSORS_ONLN)
mv build/qjs build/qjs-linux-${{matrix.arch}}
mv build/qjsc build/qjsc-linux-${{matrix.arch}}
- name: check
shell: alpine.sh {0}
run: |
file build/*-linux-${{matrix.arch}}
- name: upload
uses: actions/upload-artifact@v6
with:
name: qjs-linux-${{matrix.arch}}
path: build/*-linux-${{matrix.arch}}
macos:
runs-on: macos-latest
steps:
- uses: actions/checkout@v6
- name: build
run: |
mkdir build
cd build
cmake -DCMAKE_OSX_ARCHITECTURES="x86_64;arm64" ..
make -j$(getconf _NPROCESSORS_ONLN)
make -C .. amalgam # writes build/quickjs-amalgam.zip
mv qjs qjs-darwin
mv qjsc qjsc-darwin
- name: check
run: |
lipo -info build/qjs-darwin build/qjsc-darwin
- name: upload amalgamation
uses: actions/upload-artifact@v6
with:
name: quickjs-amalgam.zip
path: build/quickjs-amalgam.zip
compression-level: 0 # already compressed
- name: upload
uses: actions/upload-artifact@v6
with:
name: qjs-darwin
path: build/*-darwin
windows:
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
arch: [x86, x86_64]
defaults:
run:
shell: msys2 {0}
steps:
- uses: actions/checkout@v6
- name: Setup MSYS2
uses: msys2/setup-msys2@v2
with:
msystem: ${{ matrix.arch == 'x86' && 'mingw32' || 'ucrt64' }}
install: >-
git
make
pacboy: >-
cmake:p
ninja:p
toolchain:p
- name: build
run: |
make
mv build/qjs.exe build/qjs-windows-${{matrix.arch}}.exe
mv build/qjsc.exe build/qjsc-windows-${{matrix.arch}}.exe
- name: check
run: |
file build/qjs-windows-${{matrix.arch}}.exe
ldd build/qjs-windows-${{matrix.arch}}.exe build/qjsc-windows-${{matrix.arch}}.exe
- name: upload
uses: actions/upload-artifact@v6
with:
name: qjs-windows-${{matrix.arch}}
path: build/*-windows-${{matrix.arch}}.exe
wasi:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v6
- name: setup wasi-sdk
run: |
wget -nv https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-25/wasi-sdk-25.0-x86_64-linux.deb -P /tmp
sudo apt install /tmp/wasi-sdk*.deb
- name: build
run: |
cmake -B build -DCMAKE_TOOLCHAIN_FILE=/opt/wasi-sdk/share/cmake/wasi-sdk.cmake
make -C build qjs_exe
mv build/qjs build/qjs-wasi.wasm
- name: upload
uses: actions/upload-artifact@v6
with:
name: qjs-wasi
path: build/qjs-wasi.wasm
upload-to-release:
needs: [linux, macos, windows, wasi, check_meson_version]
runs-on: ubuntu-22.04
steps:
- name: get assets
uses: actions/download-artifact@v7
with:
path: build
merge-multiple: true
- run: ls -R build
- name: release
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
uses: softprops/action-gh-release@v2
with:
files: |
build/*
@@ -0,0 +1,23 @@
name: Test Docs
on:
pull_request:
paths:
- 'docs/**'
- '.github/workflows/*docs.yml'
jobs:
test-docs:
name: Test Docusaurus build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version-file: 'docs/.nvmrc'
- name: Install dependencies
working-directory: ./docs
run: npm install
- name: Build
working-directory: ./docs
run: npm run build
@@ -0,0 +1,32 @@
name: tsan
on:
push:
branches:
- master
paths:
- '**'
- '!.gitignore'
- '!LICENSE'
- '!README.md'
- '!docs/**'
- '!examples/**'
- '.github/workflows/tsan.yml'
- '.github/workflows/valgrind.yml'
workflow_dispatch:
jobs:
linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: true
- name: build
run: |
cmake -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo -DQJS_ENABLE_TSAN=ON
cmake --build build -j`nproc`
- name: test
run: |
git submodule update --init --checkout --depth 1
./build/run-test262 -m -c test262.conf -c test262-fast.conf -a
@@ -0,0 +1,33 @@
name: valgrind
on:
push:
branches:
- master
paths:
- '**'
- '!.gitignore'
- '!LICENSE'
- '!README.md'
- '!docs/**'
- '!examples/**'
- '.github/workflows/tsan.yml'
- '.github/workflows/valgrind.yml'
workflow_dispatch:
jobs:
linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: true
- name: install valgrind
run: sudo apt-get update && sudo apt-get install valgrind
- name: build
run: |
make BUILD_TYPE=RelWithDebInfo
- name: test
run: |
git submodule update --init --checkout --depth 1
valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes --error-exitcode=1 ./build/run-test262 -m -c test262.conf -c test262-fast.conf -a
@@ -0,0 +1,14 @@
*.a
*.orig
*.so
.obj/
build/
unicode/
.idea
cmake-*
.vs
out/
CMakeUserPresets.json
fuzz
.vscode/
microbench*.txt
@@ -0,0 +1,5 @@
[submodule "test262"]
path = test262
url = https://github.com/tc39/test262
shallow = true
update = none
@@ -0,0 +1,461 @@
cmake_minimum_required(VERSION 3.10)
project(quickjs LANGUAGES C)
include(CheckCCompilerFlag)
include(GNUInstallDirs)
set(CMAKE_C_VISIBILITY_PRESET hidden)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_C_EXTENSIONS ON)
set(CMAKE_C_STANDARD 11)
# MINGW doesn't exist in older cmake versions, newer versions don't know
# about CMAKE_COMPILER_IS_MINGW, and there is no unique CMAKE_C_COMPILER_ID
# for mingw-based compilers...
if(MINGW)
# do nothing
elseif(CMAKE_C_COMPILER MATCHES "mingw")
set(MINGW TRUE)
else()
set(MINGW FALSE)
endif()
if(CMAKE_SYSTEM_NAME STREQUAL "tvOS")
set(TVOS TRUE)
else()
set(TVOS FALSE)
endif()
if(CMAKE_SYSTEM_NAME STREQUAL "watchOS")
set(WATCHOS TRUE)
else()
set(WATCHOS FALSE)
endif()
if(NOT CMAKE_BUILD_TYPE)
message(STATUS "No build type selected, default to Release")
set(CMAKE_BUILD_TYPE "Release")
endif()
message(STATUS "Building in ${CMAKE_BUILD_TYPE} mode")
message(STATUS "Building with ${CMAKE_C_COMPILER_ID} ${CMAKE_C_COMPILER_VERSION} on ${CMAKE_SYSTEM}")
macro(xcheck_add_c_compiler_flag FLAG)
string(REPLACE "-" "" FLAG_NO_HYPHEN ${FLAG})
check_c_compiler_flag(${FLAG} COMPILER_SUPPORTS_${FLAG_NO_HYPHEN})
if(COMPILER_SUPPORTS_${FLAG_NO_HYPHEN})
add_compile_options(${FLAG})
endif()
endmacro()
xcheck_add_c_compiler_flag(-Wall)
if(NOT MSVC AND NOT IOS AND NOT TVOS AND NOT WATCHOS)
xcheck_add_c_compiler_flag(-Werror)
xcheck_add_c_compiler_flag(-Wextra)
endif()
xcheck_add_c_compiler_flag(-Wformat=2)
xcheck_add_c_compiler_flag(-Wno-implicit-fallthrough)
xcheck_add_c_compiler_flag(-Wno-sign-compare)
xcheck_add_c_compiler_flag(-Wno-missing-field-initializers)
xcheck_add_c_compiler_flag(-Wno-unused-parameter)
xcheck_add_c_compiler_flag(-Wno-unused-but-set-variable)
xcheck_add_c_compiler_flag(-Wno-unused-result)
xcheck_add_c_compiler_flag(-Wno-stringop-truncation)
xcheck_add_c_compiler_flag(-Wno-array-bounds)
xcheck_add_c_compiler_flag(-funsigned-char)
# ClangCL is command line compatible with MSVC, so 'MSVC' is set.
if(MSVC)
xcheck_add_c_compiler_flag(-Wno-unsafe-buffer-usage)
xcheck_add_c_compiler_flag(-Wno-sign-conversion)
xcheck_add_c_compiler_flag(-Wno-nonportable-system-include-path)
xcheck_add_c_compiler_flag(-Wno-implicit-int-conversion)
xcheck_add_c_compiler_flag(-Wno-shorten-64-to-32)
xcheck_add_c_compiler_flag(-Wno-reserved-macro-identifier)
xcheck_add_c_compiler_flag(-Wno-reserved-identifier)
xcheck_add_c_compiler_flag(-Wdeprecated-declarations)
xcheck_add_c_compiler_flag(/experimental:c11atomics)
xcheck_add_c_compiler_flag(/wd4018) # -Wno-sign-conversion
xcheck_add_c_compiler_flag(/wd4061) # -Wno-implicit-fallthrough
xcheck_add_c_compiler_flag(/wd4100) # -Wno-unused-parameter
xcheck_add_c_compiler_flag(/wd4200) # -Wno-zero-length-array
xcheck_add_c_compiler_flag(/wd4242) # -Wno-shorten-64-to-32
xcheck_add_c_compiler_flag(/wd4244) # -Wno-shorten-64-to-32
xcheck_add_c_compiler_flag(/wd4245) # -Wno-sign-compare
xcheck_add_c_compiler_flag(/wd4267) # -Wno-shorten-64-to-32
xcheck_add_c_compiler_flag(/wd4388) # -Wno-sign-compare
xcheck_add_c_compiler_flag(/wd4389) # -Wno-sign-compare
xcheck_add_c_compiler_flag(/wd4456) # Hides previous local declaration
xcheck_add_c_compiler_flag(/wd4457) # Hides function parameter
xcheck_add_c_compiler_flag(/wd4710) # Function not inlined
xcheck_add_c_compiler_flag(/wd4711) # Function was inlined
xcheck_add_c_compiler_flag(/wd4820) # Padding added after construct
xcheck_add_c_compiler_flag(/wd4996) # -Wdeprecated-declarations
xcheck_add_c_compiler_flag(/wd5045) # Compiler will insert Spectre mitigation for memory load if /Qspectre switch specified
endif()
# Set a 8MB default stack size on Windows.
# It defaults to 1MB on MSVC, which is the same as our current JS stack size,
# so it will overflow and crash otherwise.
# On MinGW it defaults to 2MB.
if(WIN32)
if(MSVC)
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /STACK:8388608")
else()
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--stack,8388608")
endif()
endif()
# MacOS and GCC 11 or later need -Wno-maybe-uninitialized
# https://github.com/quickjs-ng/quickjs/issues/453
if(APPLE AND CMAKE_C_COMPILER_ID STREQUAL "GNU" AND CMAKE_C_COMPILER_VERSION VERSION_GREATER_EQUAL 11)
xcheck_add_c_compiler_flag(-Wno-maybe-uninitialized)
endif()
if(CMAKE_SYSTEM_NAME STREQUAL "WASI")
add_compile_definitions(
_WASI_EMULATED_PROCESS_CLOCKS
_WASI_EMULATED_SIGNAL
)
add_link_options(
-lwasi-emulated-process-clocks
-lwasi-emulated-signal
)
endif()
if(CMAKE_BUILD_TYPE MATCHES "Debug")
xcheck_add_c_compiler_flag(/Od)
xcheck_add_c_compiler_flag(-O0)
xcheck_add_c_compiler_flag(-ggdb)
xcheck_add_c_compiler_flag(-fno-omit-frame-pointer)
endif()
macro(xoption OPTION_NAME OPTION_TEXT OPTION_DEFAULT)
option(${OPTION_NAME} ${OPTION_TEXT} ${OPTION_DEFAULT})
if(DEFINED ENV{${OPTION_NAME}})
# Allow setting the option through an environment variable.
set(${OPTION_NAME} $ENV{${OPTION_NAME}})
endif()
if(${OPTION_NAME})
add_definitions(-D${OPTION_NAME})
endif()
message(STATUS " ${OPTION_NAME}: ${${OPTION_NAME}}")
endmacro()
xoption(BUILD_SHARED_LIBS "Build a shared library" OFF)
if(BUILD_SHARED_LIBS)
message(STATUS "Building a shared library")
endif()
# note: QJS_ENABLE_TSAN is currently incompatible with the other sanitizers but we
# don't explicitly check for that because who knows what the future will bring?
# QJS_ENABLE_MSAN only works with clang at the time of writing; also not checked
# for the same reason
xoption(QJS_BUILD_EXAMPLES "Build examples" OFF)
xoption(QJS_BUILD_CLI_STATIC "Build a static qjs executable" OFF)
xoption(QJS_BUILD_CLI_WITH_MIMALLOC "Build the qjs executable with mimalloc" OFF)
xoption(QJS_BUILD_CLI_WITH_STATIC_MIMALLOC "Build the qjs executable with mimalloc (statically linked)" OFF)
xoption(QJS_DISABLE_PARSER "Disable JS source code parser" OFF)
xoption(QJS_ENABLE_ASAN "Enable AddressSanitizer (ASan)" OFF)
xoption(QJS_ENABLE_MSAN "Enable MemorySanitizer (MSan)" OFF)
xoption(QJS_ENABLE_TSAN "Enable ThreadSanitizer (TSan)" OFF)
xoption(QJS_ENABLE_UBSAN "Enable UndefinedBehaviorSanitizer (UBSan)" OFF)
if(QJS_ENABLE_ASAN)
message(STATUS "Building with ASan")
add_compile_options(
-fsanitize=address
-fno-sanitize-recover=all
-fno-omit-frame-pointer
)
add_link_options(
-fsanitize=address
-fno-sanitize-recover=all
-fno-omit-frame-pointer
)
endif()
if(QJS_ENABLE_MSAN)
message(STATUS "Building with MSan")
add_compile_options(
-fsanitize=memory
-fno-sanitize-recover=all
-fno-omit-frame-pointer
)
add_link_options(
-fsanitize=memory
-fno-sanitize-recover=all
-fno-omit-frame-pointer
)
endif()
if(QJS_ENABLE_TSAN)
message(STATUS "Building with TSan")
add_compile_options(
-fsanitize=thread
-fno-sanitize-recover=all
-fno-omit-frame-pointer
)
add_link_options(
-fsanitize=thread
-fno-sanitize-recover=all
-fno-omit-frame-pointer
)
endif()
if(QJS_ENABLE_UBSAN)
message(STATUS "Building with UBSan")
add_compile_options(
-fsanitize=undefined
-fno-sanitize-recover=all
-fno-omit-frame-pointer
)
add_link_options(
-fsanitize=undefined
-fno-sanitize-recover=all
-fno-omit-frame-pointer
)
endif()
# QuickJS library
#
xoption(QJS_BUILD_LIBC "Build standard library modules as part of the library" OFF)
macro(add_qjs_libc_if_needed target)
if(NOT QJS_BUILD_LIBC)
target_sources(${target} PRIVATE quickjs-libc.c)
endif()
endmacro()
macro(add_static_if_needed target)
if(QJS_BUILD_CLI_STATIC OR MINGW)
target_link_options(${target} PRIVATE -static)
if(MINGW)
target_link_options(${target} PRIVATE -static-libgcc)
endif()
endif()
endmacro()
set(qjs_sources
cutils.c
dtoa.c
libregexp.c
libunicode.c
quickjs.c
)
if(QJS_BUILD_LIBC)
list(APPEND qjs_sources quickjs-libc.c)
endif()
list(APPEND qjs_defines _GNU_SOURCE)
if(WIN32)
# NB: Windows 7 is EOL and we are only supporting in so far as it doesn't interfere with progress.
list(APPEND qjs_defines WIN32_LEAN_AND_MEAN _WIN32_WINNT=0x0601)
endif()
if(TVOS)
list(APPEND qjs_defines _TVOS)
endif()
if(WATCHOS)
list(APPEND qjs_defines _WATCHOS)
endif()
list(APPEND qjs_libs ${CMAKE_DL_LIBS})
find_package(Threads)
if(NOT CMAKE_SYSTEM_NAME STREQUAL "WASI")
list(APPEND qjs_libs ${CMAKE_THREAD_LIBS_INIT})
endif()
# try to find libm
find_library(M_LIBRARIES m)
if(M_LIBRARIES OR CMAKE_C_COMPILER_ID STREQUAL "TinyCC")
list(APPEND qjs_libs m)
endif()
add_library(qjs ${qjs_sources})
target_compile_definitions(qjs PRIVATE ${qjs_defines})
target_include_directories(qjs PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)
target_link_libraries(qjs PUBLIC ${qjs_libs})
if(EMSCRIPTEN)
add_executable(qjs_wasm ${qjs_sources})
target_link_options(qjs_wasm PRIVATE
# in emscripten 3.x, this will be set to 16k which is too small for quickjs. #write sth. to force github rebuild
-sSTACK_SIZE=2097152 # let it be 2m = 2 * 1024 * 1024 = 2097152, otherwise, stack overflow may be occured at bootstrap
-sNO_INVOKE_RUN
-sNO_EXIT_RUNTIME
-sMODULARIZE # do not mess the global
-sEXPORT_ES6 # export js file to morden es module
-sEXPORT_NAME=getQuickJs # give a name
-sTEXTDECODER=1 # it will be 2 if we use -Oz, and that will cause js -> c string convertion fail
-sNO_DEFAULT_TO_CXX # this project is pure c project, no need for c plus plus handle
-sEXPORTED_RUNTIME_METHODS=ccall,cwrap
)
target_compile_definitions(qjs_wasm PRIVATE ${qjs_defines})
target_link_libraries(qjs_wasm m)
endif()
# QuickJS bytecode compiler
#
add_executable(qjsc
qjsc.c
)
add_qjs_libc_if_needed(qjsc)
add_static_if_needed(qjsc)
target_compile_definitions(qjsc PRIVATE ${qjs_defines})
target_link_libraries(qjsc qjs)
# QuickJS CLI
#
add_executable(qjs_exe
gen/repl.c
gen/standalone.c
qjs.c
)
add_qjs_libc_if_needed(qjs_exe)
add_static_if_needed(qjs_exe)
set_target_properties(qjs_exe PROPERTIES
OUTPUT_NAME "qjs"
)
target_compile_definitions(qjs_exe PRIVATE ${qjs_defines})
target_link_libraries(qjs_exe qjs)
if(NOT WIN32)
set_target_properties(qjs_exe PROPERTIES ENABLE_EXPORTS TRUE)
endif()
if(QJS_BUILD_CLI_WITH_MIMALLOC OR QJS_BUILD_CLI_WITH_STATIC_MIMALLOC)
find_package(mimalloc REQUIRED)
# Upstream mimalloc doesn't provide a way to know if both libraries are supported.
if(QJS_BUILD_CLI_WITH_STATIC_MIMALLOC)
target_link_libraries(qjs_exe mimalloc-static)
else()
target_link_libraries(qjs_exe mimalloc)
endif()
endif()
# Test262 runner
#
if(NOT EMSCRIPTEN)
add_executable(run-test262
run-test262.c
)
add_qjs_libc_if_needed(run-test262)
target_compile_definitions(run-test262 PRIVATE ${qjs_defines})
target_link_libraries(run-test262 qjs)
endif()
# Interrupt test
#
add_executable(api-test
api-test.c
)
target_compile_definitions(api-test PRIVATE ${qjs_defines})
target_link_libraries(api-test qjs)
# Unicode generator
#
add_executable(unicode_gen EXCLUDE_FROM_ALL
cutils.c
libunicode.c
unicode_gen.c
)
target_compile_definitions(unicode_gen PRIVATE ${qjs_defines})
add_executable(function_source
gen/function_source.c
)
add_qjs_libc_if_needed(function_source)
target_include_directories(function_source PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_compile_definitions(function_source PRIVATE ${qjs_defines})
target_link_libraries(function_source qjs)
# Examples
#
if(QJS_BUILD_EXAMPLES)
add_executable(hello
gen/hello.c
)
add_qjs_libc_if_needed(hello)
target_include_directories(hello PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_compile_definitions(hello PRIVATE ${qjs_defines})
target_link_libraries(hello qjs)
add_executable(hello_module
gen/hello_module.c
)
add_qjs_libc_if_needed(hello_module)
target_include_directories(hello_module PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_compile_definitions(hello_module PRIVATE ${qjs_defines})
target_link_libraries(hello_module qjs)
add_library(fib MODULE examples/fib.c)
set_target_properties(fib PROPERTIES
PREFIX ""
C_VISIBILITY_PRESET default
)
target_compile_definitions(fib PRIVATE JS_SHARED_LIBRARY)
if(WIN32)
target_link_libraries(fib qjs)
elseif(APPLE)
target_link_options(fib PRIVATE -undefined dynamic_lookup)
endif()
add_library(point MODULE examples/point.c)
set_target_properties(point PROPERTIES
PREFIX ""
C_VISIBILITY_PRESET default
)
target_compile_definitions(point PRIVATE JS_SHARED_LIBRARY)
if(WIN32)
target_link_libraries(point qjs)
elseif(APPLE)
target_link_options(point PRIVATE -undefined dynamic_lookup)
endif()
add_executable(test_fib
examples/fib.c
gen/test_fib.c
)
add_qjs_libc_if_needed(test_fib)
target_include_directories(test_fib PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_compile_definitions(test_fib PRIVATE ${qjs_defines})
target_link_libraries(test_fib qjs)
endif()
# Install target
#
file(STRINGS quickjs.h quickjs_h REGEX QJS_VERSION)
string(REGEX MATCH "QJS_VERSION_MAJOR ([0-9]*)" _ "${quickjs_h}")
set(QJS_VERSION_MAJOR ${CMAKE_MATCH_1})
string(REGEX MATCH "QJS_VERSION_MINOR ([0-9]*)" _ "${quickjs_h}")
set(QJS_VERSION_MINOR ${CMAKE_MATCH_1})
string(REGEX MATCH "QJS_VERSION_PATCH ([0-9]*)" _ "${quickjs_h}")
set(QJS_VERSION_PATCH ${CMAKE_MATCH_1})
set_target_properties(qjs PROPERTIES
VERSION ${QJS_VERSION_MAJOR}.${QJS_VERSION_MINOR}.${QJS_VERSION_PATCH}
SOVERSION ${QJS_VERSION_MAJOR}
)
install(FILES quickjs.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
if(QJS_BUILD_LIBC)
install(FILES quickjs-libc.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
endif()
if(NOT IOS AND NOT TVOS AND NOT WATCHOS)
install(TARGETS qjs_exe RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
install(TARGETS qjsc RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
endif()
install(TARGETS qjs EXPORT qjsConfig
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(EXPORT qjsConfig DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/quickjs)
install(FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR})
install(DIRECTORY examples DESTINATION ${CMAKE_INSTALL_DOCDIR})
+24
View File
@@ -0,0 +1,24 @@
The MIT License (MIT)
Copyright (c) 2017-2024 Fabrice Bellard
Copyright (c) 2017-2024 Charlie Gordon
Copyright (c) 2023-2025 Ben Noordhuis
Copyright (c) 2023-2025 Saúl Ibarra Corretgé
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.
+147
View File
@@ -0,0 +1,147 @@
#
# QuickJS Javascript Engine
#
# Copyright (c) 2017-2024 Fabrice Bellard
# Copyright (c) 2017-2024 Charlie Gordon
# Copyright (c) 2023-2025 Ben Noordhuis
# Copyright (c) 2023-2025 Saúl Ibarra Corretgé
#
# 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.
BUILD_DIR=build
BUILD_TYPE?=Release
INSTALL_PREFIX?=/usr/local
QJS=$(BUILD_DIR)/qjs
QJSC=$(BUILD_DIR)/qjsc
RUN262=$(BUILD_DIR)/run-test262
JOBS?=$(shell getconf _NPROCESSORS_ONLN)
ifeq ($(JOBS),)
JOBS := $(shell sysctl -n hw.ncpu)
endif
ifeq ($(JOBS),)
JOBS := $(shell nproc)
endif
ifeq ($(JOBS),)
JOBS := 4
endif
all: $(QJS)
amalgam: TEMP := $(shell mktemp -d)
amalgam: $(QJS)
$(QJS) amalgam.js $(TEMP)/quickjs-amalgam.c
cp quickjs.h quickjs-libc.h $(TEMP)
cd $(TEMP) && zip -9 quickjs-amalgam.zip quickjs-amalgam.c quickjs.h quickjs-libc.h
cp $(TEMP)/quickjs-amalgam.zip $(BUILD_DIR)
cd $(TEMP) && $(RM) quickjs-amalgam.zip quickjs-amalgam.c quickjs.h quickjs-libc.h
$(RM) -d $(TEMP)
fuzz:
clang -g -O1 -fsanitize=address,undefined,fuzzer -o fuzz fuzz.c
./fuzz
$(BUILD_DIR):
cmake -B $(BUILD_DIR) -DCMAKE_BUILD_TYPE=$(BUILD_TYPE) -DCMAKE_INSTALL_PREFIX=$(INSTALL_PREFIX)
$(QJS): $(BUILD_DIR)
cmake --build $(BUILD_DIR) -j $(JOBS)
$(QJSC): $(BUILD_DIR)
cmake --build $(BUILD_DIR) --target qjsc -j $(JOBS)
install: $(QJS) $(QJSC)
cmake --build $(BUILD_DIR) --target install
clean:
cmake --build $(BUILD_DIR) --target clean
codegen: $(QJSC)
$(QJSC) -ss -o gen/repl.c -m repl.js
$(QJSC) -ss -o gen/standalone.c -m standalone.js
$(QJSC) -e -o gen/function_source.c tests/function_source.js
$(QJSC) -e -o gen/hello.c examples/hello.js
$(QJSC) -e -o gen/hello_module.c -m examples/hello_module.js
$(QJSC) -e -o gen/test_fib.c -m examples/test_fib.js
$(QJSC) -C -ss -o builtin-array-fromasync.h builtin-array-fromasync.js
debug:
BUILD_TYPE=Debug $(MAKE)
distclean:
@rm -rf $(BUILD_DIR)
stats: $(QJS)
$(QJS) -qd
jscheck: CFLAGS=-I. -D_GNU_SOURCE -DJS_CHECK_JSVALUE -Wall -Werror -fsyntax-only -c -o /dev/null
jscheck:
$(CC) $(CFLAGS) api-test.c
$(CC) $(CFLAGS) ctest.c
$(CC) $(CFLAGS) fuzz.c
$(CC) $(CFLAGS) gen/function_source.c
$(CC) $(CFLAGS) gen/hello.c
$(CC) $(CFLAGS) gen/hello_module.c
$(CC) $(CFLAGS) gen/repl.c
$(CC) $(CFLAGS) gen/standalone.c
$(CC) $(CFLAGS) gen/test_fib.c
$(CC) $(CFLAGS) qjs.c
$(CC) $(CFLAGS) qjsc.c
$(CC) $(CFLAGS) quickjs-libc.c
$(CC) $(CFLAGS) quickjs.c
$(CC) $(CFLAGS) run-test262.c
# effectively .PHONY because it doesn't generate output
ctest: CFLAGS=-std=c11 -fsyntax-only -Wall -Wextra -Werror -pedantic
ctest: ctest.c quickjs.h
$(CC) $(CFLAGS) -DJS_NAN_BOXING=0 $<
$(CC) $(CFLAGS) -DJS_NAN_BOXING=1 $<
# effectively .PHONY because it doesn't generate output
cxxtest: CXXFLAGS=-std=c++11 -fsyntax-only -Wall -Wextra -Werror -pedantic
cxxtest: cxxtest.cc quickjs.h
$(CXX) $(CXXFLAGS) -DJS_NAN_BOXING=0 $<
$(CXX) $(CXXFLAGS) -DJS_NAN_BOXING=1 $<
test: $(QJS)
$(RUN262) -c tests.conf
test262: $(QJS)
$(RUN262) -m -c test262.conf -a
test262-fast: $(QJS)
$(RUN262) -m -c test262.conf -c test262-fast.conf -a
test262-update: $(QJS)
$(RUN262) -u -c test262.conf -a -t 1
test262-check: $(QJS)
$(RUN262) -m -c test262.conf -E -a
microbench: $(QJS)
$(QJS) tests/microbench.js
unicode_gen: $(BUILD_DIR)
cmake --build $(BUILD_DIR) --target unicode_gen
libunicode-table.h: unicode_gen
$(BUILD_DIR)/unicode_gen unicode $@
.PHONY: all amalgam ctest cxxtest debug fuzz jscheck install clean codegen distclean stats test test262 test262-update test262-check microbench unicode_gen $(QJS) $(QJSC)
+24
View File
@@ -0,0 +1,24 @@
# ⚡️ QuickJS - A mighty JavaScript engine
## Overview
QuickJS is a small and embeddable JavaScript engine. It aims to support the latest
[ECMAScript] specification.
This project is a _fork_ of the [original QuickJS project] by Fabrice Bellard and Charlie Gordon, after it went dormant, with the intent of reigniting its development.
## Getting started
Head over to the [project website] for instructions on how to get started and more
documentation.
## Authors
[@bnoordhuis], [@saghul], and many more [contributors].
[ECMAScript]: https://tc39.es/ecma262/
[original QuickJS project]: https://bellard.org/quickjs
[@bnoordhuis]: https://github.com/bnoordhuis
[@saghul]: https://github.com/saghul
[contributors]: https://github.com/quickjs-ng/quickjs/graphs/contributors
[project website]: https://quickjs-ng.github.io/quickjs/
+49
View File
@@ -0,0 +1,49 @@
import {loadFile, writeFile} from "qjs:std"
const cutils_c = loadFile("cutils.c")
const cutils_h = loadFile("cutils.h")
const dtoa_c = loadFile("dtoa.c")
const dtoa_h = loadFile("dtoa.h")
const libregexp_c = loadFile("libregexp.c")
const libregexp_h = loadFile("libregexp.h")
const libregexp_opcode_h = loadFile("libregexp-opcode.h")
const libunicode_c = loadFile("libunicode.c")
const libunicode_h = loadFile("libunicode.h")
const libunicode_table_h = loadFile("libunicode-table.h")
const list_h = loadFile("list.h")
const quickjs_atom_h = loadFile("quickjs-atom.h")
const quickjs_c = loadFile("quickjs.c")
const quickjs_c_atomics_h = loadFile("quickjs-c-atomics.h")
const quickjs_h = loadFile("quickjs.h")
const quickjs_libc_c = loadFile("quickjs-libc.c")
const quickjs_libc_h = loadFile("quickjs-libc.h")
const quickjs_opcode_h = loadFile("quickjs-opcode.h")
const gen_builtin_array_fromasync_h = loadFile("builtin-array-fromasync.h")
let source = "#if defined(QJS_BUILD_LIBC) && defined(__linux__) && !defined(_GNU_SOURCE)\n"
+ "#define _GNU_SOURCE\n"
+ "#endif\n"
+ quickjs_c_atomics_h
+ cutils_h
+ dtoa_h
+ list_h
+ libunicode_h // exports lre_is_id_start, used by libregexp.h
+ libregexp_h
+ libunicode_table_h
+ quickjs_h
+ quickjs_c
+ cutils_c
+ dtoa_c
+ libregexp_c
+ libunicode_c
+ "#ifdef QJS_BUILD_LIBC\n"
+ quickjs_libc_h
+ quickjs_libc_c
+ "#endif // QJS_BUILD_LIBC\n"
source = source.replace(/#include "quickjs-atom.h"/g, quickjs_atom_h)
source = source.replace(/#include "quickjs-opcode.h"/g, quickjs_opcode_h)
source = source.replace(/#include "libregexp-opcode.h"/g, libregexp_opcode_h)
source = source.replace(/#include "builtin-array-fromasync.h"/g,
gen_builtin_array_fromasync_h)
source = source.replace(/#include "[^"]+"/g, "")
writeFile(execArgv[2] ?? "quickjs-amalgam.c", source)
+854
View File
@@ -0,0 +1,854 @@
#ifdef NDEBUG
#undef NDEBUG
#endif
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include "quickjs.h"
#include "cutils.h"
static JSValue eval(JSContext *ctx, const char *code)
{
return JS_Eval(ctx, code, strlen(code), "<input>", JS_EVAL_TYPE_GLOBAL);
}
static JSValue cfunc_callback(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv)
{
return JS_ThrowTypeError(ctx, "from cfunc");
}
static JSValue cfuncdata_callback(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv,
int magic, JSValueConst *func_data)
{
return JS_ThrowTypeError(ctx, "from cfuncdata");
}
static JSValue cclosure_callback(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv,
int magic, void *func_data)
{
return JS_ThrowTypeError(ctx, "from cclosure");
}
static bool closure_finalized = false;
static void cclosure_opaque_finalize(void *opaque)
{
if ((intptr_t)opaque == 12)
closure_finalized = true;
}
static void cfunctions(void)
{
uint32_t length;
const char *s;
JSValue ret, stack;
JSRuntime *rt = JS_NewRuntime();
JSContext *ctx = JS_NewContext(rt);
JSValue cfunc = JS_NewCFunction(ctx, cfunc_callback, "cfunc", 42);
JSValue cfuncdata =
JS_NewCFunctionData2(ctx, cfuncdata_callback, "cfuncdata",
/*length*/1337, /*magic*/0, /*data_len*/0, NULL);
JSValue cclosure =
JS_NewCClosure(ctx, cclosure_callback, "cclosure", cclosure_opaque_finalize,
/*length*/0xC0DE, /*magic*/11, /*opaque*/(void*)12);
JSValue global = JS_GetGlobalObject(ctx);
JS_SetPropertyStr(ctx, global, "cfunc", cfunc);
JS_SetPropertyStr(ctx, global, "cfuncdata", cfuncdata);
JS_SetPropertyStr(ctx, global, "cclosure", cclosure);
JS_FreeValue(ctx, global);
ret = eval(ctx, "cfunc.name");
assert(!JS_IsException(ret));
assert(JS_IsString(ret));
s = JS_ToCString(ctx, ret);
JS_FreeValue(ctx, ret);
assert(s);
assert(!strcmp(s, "cfunc"));
JS_FreeCString(ctx, s);
ret = eval(ctx, "cfunc.length");
assert(!JS_IsException(ret));
assert(JS_IsNumber(ret));
assert(0 == JS_ToUint32(ctx, &length, ret));
assert(length == 42);
ret = eval(ctx, "cfuncdata.name");
assert(!JS_IsException(ret));
assert(JS_IsString(ret));
s = JS_ToCString(ctx, ret);
JS_FreeValue(ctx, ret);
assert(s);
assert(!strcmp(s, "cfuncdata"));
JS_FreeCString(ctx, s);
ret = eval(ctx, "cfuncdata.length");
assert(!JS_IsException(ret));
assert(JS_IsNumber(ret));
assert(0 == JS_ToUint32(ctx, &length, ret));
assert(length == 1337);
ret = eval(ctx, "cclosure.name");
assert(!JS_IsException(ret));
assert(JS_IsString(ret));
s = JS_ToCString(ctx, ret);
JS_FreeValue(ctx, ret);
assert(s);
assert(!strcmp(s, "cclosure"));
JS_FreeCString(ctx, s);
ret = eval(ctx, "cclosure.length");
assert(!JS_IsException(ret));
assert(JS_IsNumber(ret));
assert(0 == JS_ToUint32(ctx, &length, ret));
assert(length == 0xC0DE);
ret = eval(ctx, "cfunc()");
assert(JS_IsException(ret));
ret = JS_GetException(ctx);
assert(JS_IsError(ret));
stack = JS_GetPropertyStr(ctx, ret, "stack");
assert(JS_IsString(stack));
s = JS_ToCString(ctx, stack);
JS_FreeValue(ctx, stack);
assert(s);
assert(!strcmp(s, " at cfunc (native)\n at <eval> (<input>:1:1)\n"));
JS_FreeCString(ctx, s);
s = JS_ToCString(ctx, ret);
JS_FreeValue(ctx, ret);
assert(s);
assert(!strcmp(s, "TypeError: from cfunc"));
JS_FreeCString(ctx, s);
ret = eval(ctx, "cfuncdata()");
assert(JS_IsException(ret));
ret = JS_GetException(ctx);
assert(JS_IsError(ret));
stack = JS_GetPropertyStr(ctx, ret, "stack");
assert(JS_IsString(stack));
s = JS_ToCString(ctx, stack);
JS_FreeValue(ctx, stack);
assert(s);
assert(!strcmp(s, " at cfuncdata (native)\n at <eval> (<input>:1:1)\n"));
JS_FreeCString(ctx, s);
s = JS_ToCString(ctx, ret);
JS_FreeValue(ctx, ret);
assert(s);
assert(!strcmp(s, "TypeError: from cfuncdata"));
JS_FreeCString(ctx, s);
ret = eval(ctx, "cclosure()");
assert(JS_IsException(ret));
ret = JS_GetException(ctx);
assert(JS_IsError(ret));
stack = JS_GetPropertyStr(ctx, ret, "stack");
assert(JS_IsString(stack));
s = JS_ToCString(ctx, stack);
JS_FreeValue(ctx, stack);
assert(s);
assert(!strcmp(s, " at cclosure (native)\n at <eval> (<input>:1:1)\n"));
JS_FreeCString(ctx, s);
s = JS_ToCString(ctx, ret);
JS_FreeValue(ctx, ret);
assert(s);
assert(!strcmp(s, "TypeError: from cclosure"));
JS_FreeCString(ctx, s);
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
assert(closure_finalized);
}
#define MAX_TIME 10
static int timeout_interrupt_handler(JSRuntime *rt, void *opaque)
{
int *time = (int *)opaque;
if (*time <= MAX_TIME)
*time += 1;
return *time > MAX_TIME;
}
static void sync_call(void)
{
static const char code[] =
"(function() { \
try { \
while (true) {} \
} catch (e) {} \
})();";
JSRuntime *rt = JS_NewRuntime();
JSContext *ctx = JS_NewContext(rt);
int time = 0;
JS_SetInterruptHandler(rt, timeout_interrupt_handler, &time);
JSValue ret = eval(ctx, code);
assert(time > MAX_TIME);
assert(JS_IsException(ret));
JS_FreeValue(ctx, ret);
assert(JS_HasException(ctx));
JSValue e = JS_GetException(ctx);
assert(JS_IsUncatchableError(e));
JS_FreeValue(ctx, e);
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
}
static void async_call(void)
{
static const char code[] =
"(async function() { \
const loop = async () => { \
await Promise.resolve(); \
while (true) {} \
}; \
await loop().catch(() => {}); \
})();";
JSRuntime *rt = JS_NewRuntime();
JSContext *ctx = JS_NewContext(rt);
int time = 0;
JS_SetInterruptHandler(rt, timeout_interrupt_handler, &time);
JSValue ret = eval(ctx, code);
assert(!JS_IsException(ret));
JS_FreeValue(ctx, ret);
assert(JS_IsJobPending(rt));
int r = 0;
while (JS_IsJobPending(rt)) {
r = JS_ExecutePendingJob(rt, &ctx);
}
assert(time > MAX_TIME);
assert(r == -1);
assert(JS_HasException(ctx));
JSValue e = JS_GetException(ctx);
assert(JS_IsUncatchableError(e));
JS_FreeValue(ctx, e);
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
}
static JSValue save_value(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv)
{
assert(argc == 1);
JSValue *p = (JSValue *)JS_GetContextOpaque(ctx);
*p = JS_DupValue(ctx, argv[0]);
return JS_UNDEFINED;
}
static void async_call_stack_overflow(void)
{
static const char code[] =
"(async function() { \
const f = () => f(); \
try { \
await Promise.resolve(); \
f(); \
} catch (e) { \
save_value(e); \
} \
})();";
JSRuntime *rt = JS_NewRuntime();
JSContext *ctx = JS_NewContext(rt);
JSValue value = JS_UNDEFINED;
JS_SetContextOpaque(ctx, &value);
JSValue global = JS_GetGlobalObject(ctx);
JS_SetPropertyStr(ctx, global, "save_value", JS_NewCFunction(ctx, save_value, "save_value", 1));
JS_FreeValue(ctx, global);
JSValue ret = eval(ctx, code);
assert(!JS_IsException(ret));
JS_FreeValue(ctx, ret);
assert(JS_IsJobPending(rt));
int r = 0;
while (JS_IsJobPending(rt)) {
r = JS_ExecutePendingJob(rt, &ctx);
}
assert(r == 1);
assert(!JS_HasException(ctx));
assert(JS_IsError(value)); // stack overflow should be caught
JS_FreeValue(ctx, value);
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
}
// https://github.com/quickjs-ng/quickjs/issues/914
static void raw_context_global_var(void)
{
JSRuntime *rt = JS_NewRuntime();
JSContext *ctx = JS_NewContextRaw(rt);
JS_AddIntrinsicEval(ctx);
{
JSValue ret = eval(ctx, "globalThis");
assert(JS_IsException(ret));
JS_FreeValue(ctx, ret);
}
{
JSValue ret = eval(ctx, "var x = 42");
assert(JS_IsUndefined(ret));
JS_FreeValue(ctx, ret);
}
{
JSValue ret = eval(ctx, "function f() {}");
assert(JS_IsUndefined(ret));
JS_FreeValue(ctx, ret);
}
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
}
static void is_array(void)
{
JSRuntime *rt = JS_NewRuntime();
JSContext *ctx = JS_NewContext(rt);
{
JSValue ret = eval(ctx, "[]");
assert(!JS_IsException(ret));
assert(JS_IsArray(ret));
JS_FreeValue(ctx, ret);
}
{
JSValue ret = eval(ctx, "new Proxy([], {})");
assert(!JS_IsException(ret));
assert(!JS_IsArray(ret));
assert(JS_IsProxy(ret));
JSValue handler = JS_GetProxyHandler(ctx, ret);
JSValue target = JS_GetProxyTarget(ctx, ret);
assert(!JS_IsException(handler));
assert(!JS_IsException(target));
assert(!JS_IsProxy(handler));
assert(!JS_IsProxy(target));
assert(JS_IsObject(handler));
assert(JS_IsArray(target));
JS_FreeValue(ctx, handler);
JS_FreeValue(ctx, target);
JS_FreeValue(ctx, ret);
}
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
}
static int loader_calls;
static JSModuleDef *loader(JSContext *ctx, const char *name, void *opaque)
{
loader_calls++;
assert(!strcmp(name, "b"));
static const char code[] = "export function f(x){}";
JSValue ret = JS_Eval(ctx, code, strlen(code), "b",
JS_EVAL_TYPE_MODULE|JS_EVAL_FLAG_COMPILE_ONLY);
assert(!JS_IsException(ret));
JSModuleDef *m = JS_VALUE_GET_PTR(ret);
assert(m);
JS_FreeValue(ctx, ret);
return m;
}
static void module_serde(void)
{
JSRuntime *rt = JS_NewRuntime();
//JS_SetDumpFlags(rt, JS_DUMP_MODULE_RESOLVE);
JS_SetModuleLoaderFunc(rt, NULL, loader, NULL);
JSContext *ctx = JS_NewContext(rt);
static const char code[] = "import {f} from 'b'; f()";
assert(loader_calls == 0);
JSValue mod = JS_Eval(ctx, code, strlen(code), "a",
JS_EVAL_TYPE_MODULE|JS_EVAL_FLAG_COMPILE_ONLY);
assert(loader_calls == 1);
assert(!JS_IsException(mod));
assert(JS_IsModule(mod));
size_t len = 0;
uint8_t *buf = JS_WriteObject(ctx, &len, mod,
JS_WRITE_OBJ_BYTECODE|JS_WRITE_OBJ_REFERENCE);
assert(buf);
assert(len > 0);
JS_FreeValue(ctx, mod);
assert(loader_calls == 1);
mod = JS_ReadObject(ctx, buf, len, JS_READ_OBJ_BYTECODE);
js_free(ctx, buf);
assert(loader_calls == 1); // 'b' is returned from cache
assert(!JS_IsException(mod));
JSValue ret = JS_EvalFunction(ctx, mod);
assert(!JS_IsException(ret));
assert(JS_IsPromise(ret));
JSValue result = JS_PromiseResult(ctx, ret);
assert(!JS_IsException(result));
assert(JS_IsUndefined(result));
JS_FreeValue(ctx, result);
JS_FreeValue(ctx, ret);
JS_FreeValue(ctx, mod);
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
}
static void runtime_cstring_free(void)
{
JSRuntime *rt = JS_NewRuntime();
JSContext *ctx = JS_NewContext(rt);
// string -> cstring + JS_FreeCStringRT
{
JSValue ret = eval(ctx, "\"testStringPleaseIgnore\"");
assert(JS_IsString(ret));
const char *s = JS_ToCString(ctx, ret);
assert(s);
assert(strcmp(s, "testStringPleaseIgnore") == 0);
JS_FreeCStringRT(rt, s);
JS_FreeValue(ctx, ret);
}
// string -> cstring + JS_FreeCStringRT, destroying the source value first
{
JSValue ret = eval(ctx, "\"testStringPleaseIgnore\"");
assert(JS_IsString(ret));
const char *s = JS_ToCString(ctx, ret);
assert(s);
JS_FreeValue(ctx, ret);
assert(strcmp(s, "testStringPleaseIgnore") == 0);
JS_FreeCStringRT(rt, s);
}
// number -> cstring + JS_FreeCStringRT
{
JSValue ret = eval(ctx, "123987");
assert(JS_IsNumber(ret));
const char *s = JS_ToCString(ctx, ret);
assert(s);
assert(strcmp(s, "123987") == 0);
JS_FreeCStringRT(rt, s);
JS_FreeValue(ctx, ret);
}
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
}
static void two_byte_string(void)
{
JSRuntime *rt = JS_NewRuntime();
JSContext *ctx = JS_NewContext(rt);
{
JSValue v = JS_NewTwoByteString(ctx, NULL, 0);
assert(!JS_IsException(v));
const char *s = JS_ToCString(ctx, v);
assert(s);
assert(!strcmp(s, ""));
JS_FreeCString(ctx, s);
JS_FreeValue(ctx, v);
}
{
JSValue v = JS_NewTwoByteString(ctx, (uint16_t[]){'o','k'}, 2);
assert(!JS_IsException(v));
const char *s = JS_ToCString(ctx, v);
assert(s);
assert(!strcmp(s, "ok"));
JS_FreeCString(ctx, s);
JS_FreeValue(ctx, v);
}
{
JSValue v = JS_NewTwoByteString(ctx, (uint16_t[]){0xD800}, 1);
assert(!JS_IsException(v));
const char *s = JS_ToCString(ctx, v);
assert(s);
// questionable but surrogates don't map to UTF-8 without WTF-8
assert(!strcmp(s, "\xED\xA0\x80"));
JS_FreeCString(ctx, s);
JS_FreeValue(ctx, v);
}
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
}
static void weak_map_gc_check(void)
{
static const char init_code[] =
"const map = new WeakMap(); \
function addItem() { \
const k = { \
text: 'a', \
}; \
map.set(k, {k}); \
}";
static const char test_code[] = "addItem()";
JSRuntime *rt = JS_NewRuntime();
JSContext *ctx = JS_NewContext(rt);
JSValue ret = eval(ctx, init_code);
assert(!JS_IsException(ret));
JSValue ret_test = eval(ctx, test_code);
assert(!JS_IsException(ret_test));
JS_RunGC(rt);
JSMemoryUsage memory_usage;
JS_ComputeMemoryUsage(rt, &memory_usage);
for (int i = 0; i < 3; i++) {
JSValue ret_test2 = eval(ctx, test_code);
assert(!JS_IsException(ret_test2));
JS_RunGC(rt);
JSMemoryUsage memory_usage2;
JS_ComputeMemoryUsage(rt, &memory_usage2);
assert(memory_usage.memory_used_count == memory_usage2.memory_used_count);
assert(memory_usage.memory_used_size == memory_usage2.memory_used_size);
JS_FreeValue(ctx, ret_test2);
}
JS_FreeValue(ctx, ret);
JS_FreeValue(ctx, ret_test);
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
}
struct {
int hook_type_call_count[4];
} promise_hook_state;
static void promise_hook_cb(JSContext *ctx, JSPromiseHookType type,
JSValueConst promise, JSValueConst parent_promise,
void *opaque)
{
assert(type == JS_PROMISE_HOOK_INIT ||
type == JS_PROMISE_HOOK_BEFORE ||
type == JS_PROMISE_HOOK_AFTER ||
type == JS_PROMISE_HOOK_RESOLVE);
promise_hook_state.hook_type_call_count[type]++;
assert(opaque == (void *)&promise_hook_state);
if (!JS_IsUndefined(parent_promise)) {
JSValue global_object = JS_GetGlobalObject(ctx);
JS_SetPropertyStr(ctx, global_object, "actual",
JS_DupValue(ctx, parent_promise));
JS_FreeValue(ctx, global_object);
}
}
static void promise_hook(void)
{
int *cc = promise_hook_state.hook_type_call_count;
JSContext *unused;
JSRuntime *rt = JS_NewRuntime();
//JS_SetDumpFlags(rt, JS_DUMP_PROMISE);
JS_SetPromiseHook(rt, promise_hook_cb, &promise_hook_state);
JSContext *ctx = JS_NewContext(rt);
JSValue global_object = JS_GetGlobalObject(ctx);
{
// empty module; creates an outer and inner module promise;
// JS_Eval returns the outer promise
JSValue ret = JS_Eval(ctx, "", 0, "<input>", JS_EVAL_TYPE_MODULE);
assert(!JS_IsException(ret));
assert(JS_IsPromise(ret));
assert(JS_PROMISE_FULFILLED == JS_PromiseState(ctx, ret));
JS_FreeValue(ctx, ret);
assert(2 == cc[JS_PROMISE_HOOK_INIT]);
assert(0 == cc[JS_PROMISE_HOOK_BEFORE]);
assert(0 == cc[JS_PROMISE_HOOK_AFTER]);
assert(2 == cc[JS_PROMISE_HOOK_RESOLVE]);
assert(!JS_IsJobPending(rt));
}
memset(&promise_hook_state, 0, sizeof(promise_hook_state));
{
// module with unresolved promise; the outer and inner module promises
// are resolved but not the user's promise
static const char code[] = "new Promise(() => {})";
JSValue ret = JS_Eval(ctx, code, strlen(code), "<input>", JS_EVAL_TYPE_MODULE);
assert(!JS_IsException(ret));
assert(JS_IsPromise(ret));
assert(JS_PROMISE_FULFILLED == JS_PromiseState(ctx, ret)); // outer module promise
JS_FreeValue(ctx, ret);
assert(3 == cc[JS_PROMISE_HOOK_INIT]);
assert(0 == cc[JS_PROMISE_HOOK_BEFORE]);
assert(0 == cc[JS_PROMISE_HOOK_AFTER]);
assert(2 == cc[JS_PROMISE_HOOK_RESOLVE]); // outer and inner module promise
assert(!JS_IsJobPending(rt));
}
memset(&promise_hook_state, 0, sizeof(promise_hook_state));
{
// module with resolved promise
static const char code[] = "new Promise((resolve,reject) => resolve())";
JSValue ret = JS_Eval(ctx, code, strlen(code), "<input>", JS_EVAL_TYPE_MODULE);
assert(!JS_IsException(ret));
assert(JS_IsPromise(ret));
assert(JS_PROMISE_FULFILLED == JS_PromiseState(ctx, ret)); // outer module promise
JS_FreeValue(ctx, ret);
assert(3 == cc[JS_PROMISE_HOOK_INIT]);
assert(0 == cc[JS_PROMISE_HOOK_BEFORE]);
assert(0 == cc[JS_PROMISE_HOOK_AFTER]);
assert(3 == cc[JS_PROMISE_HOOK_RESOLVE]);
assert(!JS_IsJobPending(rt));
}
memset(&promise_hook_state, 0, sizeof(promise_hook_state));
{
// module with rejected promise
static const char code[] = "new Promise((resolve,reject) => reject())";
JSValue ret = JS_Eval(ctx, code, strlen(code), "<input>", JS_EVAL_TYPE_MODULE);
assert(!JS_IsException(ret));
assert(JS_IsPromise(ret));
assert(JS_PROMISE_FULFILLED == JS_PromiseState(ctx, ret)); // outer module promise
JS_FreeValue(ctx, ret);
assert(3 == cc[JS_PROMISE_HOOK_INIT]);
assert(0 == cc[JS_PROMISE_HOOK_BEFORE]);
assert(0 == cc[JS_PROMISE_HOOK_AFTER]);
assert(2 == cc[JS_PROMISE_HOOK_RESOLVE]);
assert(!JS_IsJobPending(rt));
}
memset(&promise_hook_state, 0, sizeof(promise_hook_state));
{
// module with promise chain
static const char code[] =
"globalThis.count = 0;"
"globalThis.actual = undefined;" // set by promise_hook_cb
"globalThis.expected = new Promise(resolve => resolve());"
"expected.then(_ => count++)";
JSValue ret = JS_Eval(ctx, code, strlen(code), "<input>", JS_EVAL_TYPE_MODULE);
assert(!JS_IsException(ret));
assert(JS_IsPromise(ret));
assert(JS_PROMISE_FULFILLED == JS_PromiseState(ctx, ret)); // outer module promise
JS_FreeValue(ctx, ret);
assert(4 == cc[JS_PROMISE_HOOK_INIT]);
assert(0 == cc[JS_PROMISE_HOOK_BEFORE]);
assert(0 == cc[JS_PROMISE_HOOK_AFTER]);
assert(3 == cc[JS_PROMISE_HOOK_RESOLVE]);
JSValue v = JS_GetPropertyStr(ctx, global_object, "count");
assert(!JS_IsException(v));
int32_t count;
assert(0 == JS_ToInt32(ctx, &count, v));
assert(0 == count);
JS_FreeValue(ctx, v);
assert(JS_IsJobPending(rt));
assert(1 == JS_ExecutePendingJob(rt, &unused));
assert(!JS_HasException(ctx));
assert(4 == cc[JS_PROMISE_HOOK_INIT]);
assert(0 == cc[JS_PROMISE_HOOK_BEFORE]);
assert(0 == cc[JS_PROMISE_HOOK_AFTER]);
assert(4 == cc[JS_PROMISE_HOOK_RESOLVE]);
assert(!JS_IsJobPending(rt));
v = JS_GetPropertyStr(ctx, global_object, "count");
assert(!JS_IsException(v));
assert(0 == JS_ToInt32(ctx, &count, v));
assert(1 == count);
JS_FreeValue(ctx, v);
JSValue actual = JS_GetPropertyStr(ctx, global_object, "actual");
JSValue expected = JS_GetPropertyStr(ctx, global_object, "expected");
assert(!JS_IsException(actual));
assert(!JS_IsException(expected));
assert(JS_IsSameValue(ctx, actual, expected));
JS_FreeValue(ctx, actual);
JS_FreeValue(ctx, expected);
}
memset(&promise_hook_state, 0, sizeof(promise_hook_state));
{
// module with thenable; fires before and after hooks
static const char code[] =
"new Promise(resolve => resolve({then(resolve){ resolve() }}))";
JSValue ret = JS_Eval(ctx, code, strlen(code), "<input>", JS_EVAL_TYPE_MODULE);
assert(!JS_IsException(ret));
assert(JS_IsPromise(ret));
assert(JS_PROMISE_FULFILLED == JS_PromiseState(ctx, ret)); // outer module promise
JS_FreeValue(ctx, ret);
assert(3 == cc[JS_PROMISE_HOOK_INIT]);
assert(0 == cc[JS_PROMISE_HOOK_BEFORE]);
assert(0 == cc[JS_PROMISE_HOOK_AFTER]);
assert(2 == cc[JS_PROMISE_HOOK_RESOLVE]);
assert(JS_IsJobPending(rt));
assert(1 == JS_ExecutePendingJob(rt, &unused));
assert(!JS_HasException(ctx));
assert(3 == cc[JS_PROMISE_HOOK_INIT]);
assert(1 == cc[JS_PROMISE_HOOK_BEFORE]);
assert(1 == cc[JS_PROMISE_HOOK_AFTER]);
assert(3 == cc[JS_PROMISE_HOOK_RESOLVE]);
assert(!JS_IsJobPending(rt));
}
JS_FreeValue(ctx, global_object);
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
}
static void dump_memory_usage(void)
{
JSMemoryUsage stats;
JSRuntime *rt = NULL;
JSContext *ctx = NULL;
rt = JS_NewRuntime();
ctx = JS_NewContext(rt);
//JS_SetDumpFlags(rt, JS_DUMP_PROMISE);
static const char code[] =
"globalThis.count = 0;"
"globalThis.actual = undefined;" // set by promise_hook_cb
"globalThis.expected = new Promise(resolve => resolve());"
"expected.then(_ => count++)";
JSValue evalVal = JS_Eval(ctx, code, strlen(code), "<input>", 0);
JS_FreeValue(ctx, evalVal);
FILE *temp = tmpfile();
assert(temp != NULL);
JS_ComputeMemoryUsage(rt, &stats);
JS_DumpMemoryUsage(temp, &stats, rt);
// JS_DumpMemoryUsage(stdout, &stats, rt);
fclose(temp);
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
}
static void new_errors(void)
{
typedef struct {
const char name[16];
JSValue (*func)(JSContext *, const char *, ...);
} Entry;
static const Entry entries[] = {
{"Error", JS_NewPlainError},
{"InternalError", JS_NewInternalError},
{"RangeError", JS_NewRangeError},
{"ReferenceError", JS_NewReferenceError},
{"SyntaxError", JS_NewSyntaxError},
{"TypeError", JS_NewTypeError},
};
const Entry *e;
JSRuntime *rt = JS_NewRuntime();
JSContext *ctx = JS_NewContext(rt);
for (e = entries; e < endof(entries); e++) {
JSValue obj = (*e->func)(ctx, "the %s", "needle");
assert(!JS_IsException(obj));
assert(JS_IsObject(obj));
assert(JS_IsError(obj));
const char *haystack = JS_ToCString(ctx, obj);
char needle[256];
snprintf(needle, sizeof(needle), "%s: the needle", e->name);
assert(strstr(haystack, needle));
JS_FreeCString(ctx, haystack);
JSValue stack = JS_GetPropertyStr(ctx, obj, "stack");
assert(!JS_IsException(stack));
assert(JS_IsString(stack));
JS_FreeValue(ctx, stack);
JS_FreeValue(ctx, obj);
}
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
}
static int gop_get_own_property(JSContext *ctx, JSPropertyDescriptor *desc,
JSValueConst obj, JSAtom prop)
{
const char *name;
int found;
found = 0;
name = JS_AtomToCString(ctx, prop);
if (!name)
return -1;
if (!strcmp(name, "answer")) {
found = 1;
*desc = (JSPropertyDescriptor){
.value = JS_NewInt32(ctx, 42),
.flags = JS_PROP_C_W_E | JS_PROP_HAS_VALUE,
};
}
JS_FreeCString(ctx, name);
return found;
}
static void global_object_prototype(void)
{
static const char code[] = "answer";
JSValue global_object, proto, ret;
JSClassID class_id;
JSRuntime *rt;
JSContext *ctx;
int32_t answer;
int res;
{
rt = JS_NewRuntime();
ctx = JS_NewContext(rt);
proto = JS_NewObject(ctx);
assert(JS_IsObject(proto));
JSCFunctionListEntry prop = JS_PROP_INT32_DEF("answer", 42, JS_PROP_C_W_E);
JS_SetPropertyFunctionList(ctx, proto, &prop, 1);
global_object = JS_GetGlobalObject(ctx);
res = JS_SetPrototype(ctx, global_object, proto);
assert(res == true);
JS_FreeValue(ctx, global_object);
JS_FreeValue(ctx, proto);
ret = eval(ctx, code);
assert(!JS_IsException(ret));
assert(JS_IsNumber(ret));
res = JS_ToInt32(ctx, &answer, ret);
assert(res == 0);
assert(answer == 42);
JS_FreeValue(ctx, ret);
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
}
{
JSClassExoticMethods exotic = (JSClassExoticMethods){
.get_own_property = gop_get_own_property,
};
JSClassDef def = (JSClassDef){
.class_name = "Global Object",
.exotic = &exotic,
};
rt = JS_NewRuntime();
class_id = 0;
JS_NewClassID(rt, &class_id);
res = JS_NewClass(rt, class_id, &def);
assert(res == 0);
ctx = JS_NewContext(rt);
proto = JS_NewObjectClass(ctx, class_id);
assert(JS_IsObject(proto));
global_object = JS_GetGlobalObject(ctx);
res = JS_SetPrototype(ctx, global_object, proto);
assert(res == true);
JS_FreeValue(ctx, global_object);
JS_FreeValue(ctx, proto);
ret = eval(ctx, code);
assert(!JS_IsException(ret));
assert(JS_IsNumber(ret));
res = JS_ToInt32(ctx, &answer, ret);
assert(res == 0);
assert(answer == 42);
JS_FreeValue(ctx, ret);
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
}
}
// https://github.com/quickjs-ng/quickjs/issues/1178
static void slice_string_tocstring(void)
{
JSRuntime *rt = JS_NewRuntime();
JSContext *ctx = JS_NewContext(rt);
JSValue ret = eval(ctx, "'.'.repeat(16384).slice(1, -1)");
assert(!JS_IsException(ret));
assert(JS_IsString(ret));
const char *str = JS_ToCString(ctx, ret);
assert(strlen(str) == 16382);
JS_FreeCString(ctx, str);
JS_FreeValue(ctx, ret);
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
}
int main(void)
{
cfunctions();
sync_call();
async_call();
async_call_stack_overflow();
raw_context_global_var();
is_array();
module_serde();
runtime_cstring_free();
two_byte_string();
weak_map_gc_check();
promise_hook();
dump_memory_usage();
new_errors();
global_object_prototype();
slice_string_tocstring();
return 0;
}
@@ -0,0 +1,113 @@
/* File generated automatically by the QuickJS-ng compiler. */
#include <inttypes.h>
const uint32_t qjsc_builtin_array_fromasync_size = 826;
const uint8_t qjsc_builtin_array_fromasync[826] = {
0x15, 0x0d, 0x01, 0x1a, 0x61, 0x73, 0x79, 0x6e,
0x63, 0x49, 0x74, 0x65, 0x72, 0x61, 0x74, 0x6f,
0x72, 0x01, 0x10, 0x69, 0x74, 0x65, 0x72, 0x61,
0x74, 0x6f, 0x72, 0x01, 0x12, 0x61, 0x72, 0x72,
0x61, 0x79, 0x4c, 0x69, 0x6b, 0x65, 0x01, 0x0a,
0x6d, 0x61, 0x70, 0x46, 0x6e, 0x01, 0x0e, 0x74,
0x68, 0x69, 0x73, 0x41, 0x72, 0x67, 0x01, 0x0c,
0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x01, 0x02,
0x69, 0x01, 0x1a, 0x69, 0x73, 0x43, 0x6f, 0x6e,
0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x6f, 0x72,
0x01, 0x08, 0x73, 0x79, 0x6e, 0x63, 0x01, 0x0c,
0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x01, 0x08,
0x69, 0x74, 0x65, 0x72, 0x01, 0x1c, 0x6e, 0x6f,
0x74, 0x20, 0x61, 0x20, 0x66, 0x75, 0x6e, 0x63,
0x74, 0x69, 0x6f, 0x6e, 0x01, 0x08, 0x63, 0x61,
0x6c, 0x6c, 0x0c, 0x00, 0x02, 0x00, 0xa2, 0x01,
0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x04, 0x01,
0xa4, 0x01, 0x00, 0x00, 0x00, 0x0c, 0x43, 0x02,
0x01, 0x00, 0x05, 0x00, 0x05, 0x01, 0x00, 0x01,
0x03, 0x05, 0xaa, 0x02, 0x00, 0x01, 0x40, 0xa0,
0x03, 0x00, 0x01, 0x40, 0xc6, 0x03, 0x00, 0x01,
0x40, 0xcc, 0x01, 0x00, 0x01, 0x40, 0xc8, 0x03,
0x00, 0x01, 0x40, 0x0c, 0x60, 0x02, 0x01, 0xf8,
0x01, 0x03, 0x0e, 0x01, 0x06, 0x05, 0x00, 0x86,
0x04, 0x11, 0xca, 0x03, 0x00, 0x01, 0x00, 0xcc,
0x03, 0x00, 0x01, 0x00, 0xce, 0x03, 0x00, 0x01,
0x00, 0xca, 0x03, 0x01, 0xff, 0xff, 0xff, 0xff,
0x0f, 0x20, 0xcc, 0x03, 0x01, 0x01, 0x20, 0xce,
0x03, 0x01, 0x02, 0x20, 0xd0, 0x03, 0x02, 0x00,
0x20, 0xd2, 0x03, 0x02, 0x04, 0x20, 0xd4, 0x03,
0x02, 0x05, 0x20, 0xd6, 0x03, 0x02, 0x06, 0x20,
0xd8, 0x03, 0x02, 0x07, 0x20, 0x64, 0x06, 0x08,
0x20, 0x82, 0x01, 0x07, 0x09, 0x20, 0xda, 0x03,
0x0a, 0x08, 0x30, 0x82, 0x01, 0x0d, 0x0b, 0x20,
0xd4, 0x01, 0x0d, 0x0c, 0x20, 0x10, 0x00, 0x01,
0x00, 0xa0, 0x03, 0x01, 0x03, 0xc6, 0x03, 0x02,
0x03, 0xc8, 0x03, 0x04, 0x03, 0xaa, 0x02, 0x00,
0x03, 0xcc, 0x01, 0x03, 0x03, 0x08, 0xc4, 0x0d,
0x62, 0x02, 0x00, 0x62, 0x01, 0x00, 0x62, 0x00,
0x00, 0xd3, 0xcb, 0xd4, 0x11, 0xf4, 0xec, 0x08,
0x0e, 0x39, 0x46, 0x00, 0x00, 0x00, 0xdc, 0xcc,
0xd5, 0x11, 0xf4, 0xec, 0x08, 0x0e, 0x39, 0x46,
0x00, 0x00, 0x00, 0xdd, 0xcd, 0x62, 0x07, 0x00,
0x62, 0x06, 0x00, 0x62, 0x05, 0x00, 0x62, 0x04,
0x00, 0x62, 0x03, 0x00, 0xd4, 0x39, 0x46, 0x00,
0x00, 0x00, 0xb0, 0xec, 0x16, 0xd4, 0x98, 0x04,
0x1b, 0x00, 0x00, 0x00, 0xb0, 0xec, 0x0c, 0xdf,
0x11, 0x04, 0xee, 0x00, 0x00, 0x00, 0x21, 0x01,
0x00, 0x30, 0x06, 0xce, 0xb6, 0xc4, 0x04, 0xc3,
0x0d, 0xf7, 0xc4, 0x05, 0x09, 0xc4, 0x06, 0xd3,
0xe0, 0x48, 0xc4, 0x07, 0x63, 0x07, 0x00, 0x07,
0xad, 0xec, 0x0f, 0x0a, 0x11, 0x64, 0x06, 0x00,
0x0e, 0xd3, 0xe1, 0x48, 0x11, 0x64, 0x07, 0x00,
0x0e, 0x63, 0x07, 0x00, 0x07, 0xad, 0x6a, 0xa6,
0x00, 0x00, 0x00, 0x62, 0x08, 0x00, 0x06, 0x11,
0xf4, 0xed, 0x0c, 0x71, 0x43, 0x32, 0x00, 0x00,
0x00, 0xc4, 0x08, 0x0e, 0xee, 0x05, 0x0e, 0xd3,
0xee, 0xf2, 0x63, 0x08, 0x00, 0x8e, 0x11, 0xed,
0x03, 0x0e, 0xb6, 0x11, 0x64, 0x08, 0x00, 0x0e,
0x63, 0x05, 0x00, 0xec, 0x0c, 0xc3, 0x0d, 0x11,
0x63, 0x08, 0x00, 0x21, 0x01, 0x00, 0xee, 0x06,
0xe2, 0x63, 0x08, 0x00, 0xf1, 0x11, 0x64, 0x03,
0x00, 0x0e, 0x63, 0x04, 0x00, 0x63, 0x08, 0x00,
0xa7, 0x6a, 0x2a, 0x01, 0x00, 0x00, 0x62, 0x09,
0x00, 0xd3, 0x63, 0x04, 0x00, 0x48, 0xc4, 0x09,
0x63, 0x06, 0x00, 0xec, 0x0a, 0x63, 0x09, 0x00,
0x8c, 0x11, 0x64, 0x09, 0x00, 0x0e, 0xd4, 0xec,
0x17, 0xd4, 0x43, 0xef, 0x00, 0x00, 0x00, 0xd5,
0x63, 0x09, 0x00, 0x63, 0x04, 0x00, 0x24, 0x03,
0x00, 0x8c, 0x11, 0x64, 0x09, 0x00, 0x0e, 0x5f,
0x04, 0x00, 0x63, 0x03, 0x00, 0x63, 0x04, 0x00,
0x92, 0x64, 0x04, 0x00, 0x0b, 0x63, 0x09, 0x00,
0x4d, 0x41, 0x00, 0x00, 0x00, 0x0a, 0x4d, 0x3e,
0x00, 0x00, 0x00, 0x0a, 0x4d, 0x3f, 0x00, 0x00,
0x00, 0xf3, 0x0e, 0xee, 0x9e, 0x62, 0x0a, 0x00,
0x63, 0x07, 0x00, 0x43, 0xef, 0x00, 0x00, 0x00,
0xd3, 0x24, 0x01, 0x00, 0xc4, 0x0a, 0x63, 0x05,
0x00, 0xec, 0x09, 0xc3, 0x0d, 0x11, 0x21, 0x00,
0x00, 0xee, 0x03, 0xe2, 0xf0, 0x11, 0x64, 0x03,
0x00, 0x0e, 0x6d, 0x8c, 0x00, 0x00, 0x00, 0x62,
0x0c, 0x00, 0x62, 0x0b, 0x00, 0x06, 0x11, 0xf4,
0xed, 0x13, 0x71, 0x43, 0x41, 0x00, 0x00, 0x00,
0xc4, 0x0b, 0x43, 0x6a, 0x00, 0x00, 0x00, 0xc4,
0x0c, 0x0e, 0xee, 0x10, 0x0e, 0x63, 0x0a, 0x00,
0x43, 0x6b, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00,
0x8c, 0xee, 0xe0, 0x63, 0x0c, 0x00, 0xed, 0x4e,
0x63, 0x06, 0x00, 0xec, 0x0a, 0x63, 0x0b, 0x00,
0x8c, 0x11, 0x64, 0x0b, 0x00, 0x0e, 0xd4, 0xec,
0x17, 0xd4, 0x43, 0xef, 0x00, 0x00, 0x00, 0xd5,
0x63, 0x0b, 0x00, 0x63, 0x04, 0x00, 0x24, 0x03,
0x00, 0x8c, 0x11, 0x64, 0x0b, 0x00, 0x0e, 0x5f,
0x04, 0x00, 0x63, 0x03, 0x00, 0x63, 0x04, 0x00,
0x92, 0x64, 0x04, 0x00, 0x0b, 0x63, 0x0b, 0x00,
0x4d, 0x41, 0x00, 0x00, 0x00, 0x0a, 0x4d, 0x3e,
0x00, 0x00, 0x00, 0x0a, 0x4d, 0x3f, 0x00, 0x00,
0x00, 0xf3, 0x0e, 0xee, 0x83, 0x0e, 0x06, 0x6e,
0x0d, 0x00, 0x00, 0x00, 0x0e, 0xee, 0x1e, 0x6e,
0x05, 0x00, 0x00, 0x00, 0x30, 0x63, 0x0a, 0x00,
0x42, 0x06, 0x00, 0x00, 0x00, 0xec, 0x0d, 0x63,
0x0a, 0x00, 0x43, 0x06, 0x00, 0x00, 0x00, 0x24,
0x00, 0x00, 0x0e, 0x6f, 0x63, 0x03, 0x00, 0x63,
0x04, 0x00, 0x44, 0x32, 0x00, 0x00, 0x00, 0x63,
0x03, 0x00, 0x2f, 0xc1, 0x00, 0x28, 0xc1, 0x00,
0xcf, 0x28,
};
@@ -0,0 +1,36 @@
;(function(Array, TypeError, asyncIterator, defineProperty, iterator) {
"use strict" // result.length=i should throw if .length is not writable
return async function fromAsync(arrayLike, mapFn=undefined, thisArg=undefined) {
if (mapFn !== undefined && typeof mapFn !== "function") throw new TypeError("not a function")
let result, i = 0, isConstructor = typeof this === "function"
let sync = false, method = arrayLike[asyncIterator]
if (method == null) sync = true, method = arrayLike[iterator]
if (method == null) {
let {length} = arrayLike
length = +length || 0
result = isConstructor ? new this(length) : Array(length)
while (i < length) {
let value = arrayLike[i]
if (sync) value = await value
if (mapFn) value = await mapFn.call(thisArg, value, i)
defineProperty(result, i++, {value, configurable: true, writable: true})
}
} else {
const iter = method.call(arrayLike)
result = isConstructor ? new this() : Array()
try {
for (;;) {
let {value, done} = await iter.next()
if (done) break
if (sync) value = await value
if (mapFn) value = await mapFn.call(thisArg, value, i)
defineProperty(result, i++, {value, configurable: true, writable: true})
}
} finally {
if (iter.return) iter.return()
}
}
result.length = i
return result
}
})
+17
View File
@@ -0,0 +1,17 @@
// note: file is not actually compiled, only checked for C syntax errors
#include "quickjs.h"
int main(void)
{
JSRuntime *rt = JS_NewRuntime();
JSContext *ctx = JS_NewContext(rt);
JS_FreeValue(ctx, JS_NAN);
JS_FreeValue(ctx, JS_UNDEFINED);
JS_FreeValue(ctx, JS_NewFloat64(ctx, 42));
// not a legal way of using JS_MKPTR but this is here
// to have the compiler syntax-check its definition
JS_FreeValue(ctx, JS_MKPTR(JS_TAG_UNINITIALIZED, 0));
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
return 0;
}
File diff suppressed because it is too large Load Diff
+660
View File
@@ -0,0 +1,660 @@
/*
* C utilities
*
* Copyright (c) 2017 Fabrice Bellard
* Copyright (c) 2018 Charlie Gordon
*
* 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.
*/
#ifndef CUTILS_H
#define CUTILS_H
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>
#include <math.h>
#ifdef __cplusplus
extern "C" {
#endif
#if defined(_MSC_VER)
#include <winsock2.h>
#include <malloc.h>
#define alloca _alloca
#define ssize_t ptrdiff_t
#endif
#if defined(__APPLE__)
#include <malloc/malloc.h>
#elif defined(__linux__) || defined(__ANDROID__) || defined(__CYGWIN__) || defined(__GLIBC__)
#include <malloc.h>
#elif defined(__FreeBSD__)
#include <malloc_np.h>
#elif defined(_WIN32)
#include <windows.h>
#endif
#if !defined(_WIN32) && !defined(EMSCRIPTEN) && !defined(__wasi__)
#include <errno.h>
#include <pthread.h>
#endif
#if !defined(_WIN32)
#include <limits.h>
#include <unistd.h>
#endif
#if defined(_MSC_VER) && !defined(__clang__)
# define likely(x) (x)
# define unlikely(x) (x)
# define force_inline __forceinline
# define no_inline __declspec(noinline)
# define __maybe_unused
# define __attribute__(x)
# define __attribute(x)
#else
# define likely(x) __builtin_expect(!!(x), 1)
# define unlikely(x) __builtin_expect(!!(x), 0)
# define force_inline inline __attribute__((always_inline))
# define no_inline __attribute__((noinline))
# define __maybe_unused __attribute__((unused))
#endif
#if defined(_MSC_VER) && !defined(__clang__)
#include <math.h>
#define INF INFINITY
#define NEG_INF -INFINITY
#else
#define INF (1.0/0.0)
#define NEG_INF (-1.0/0.0)
#endif
#ifndef offsetof
#define offsetof(type, field) ((size_t) &((type *)0)->field)
#endif
#ifndef countof
#define countof(x) (sizeof(x) / sizeof((x)[0]))
#ifndef endof
#define endof(x) ((x) + countof(x))
#endif
#endif
#ifndef container_of
/* return the pointer of type 'type *' containing 'ptr' as field 'member' */
#define container_of(ptr, type, member) ((type *)((uint8_t *)(ptr) - offsetof(type, member)))
#endif
#if defined(_MSC_VER) || defined(__cplusplus)
#define minimum_length(n) n
#else
#define minimum_length(n) static n
#endif
/* Borrowed from Folly */
#ifndef JS_PRINTF_FORMAT
#ifdef _MSC_VER
#include <sal.h>
#define JS_PRINTF_FORMAT _Printf_format_string_
#define JS_PRINTF_FORMAT_ATTR(format_param, dots_param)
#else
#define JS_PRINTF_FORMAT
#if !defined(__clang__) && defined(__GNUC__)
#define JS_PRINTF_FORMAT_ATTR(format_param, dots_param) \
__attribute__((format(gnu_printf, format_param, dots_param)))
#else
#define JS_PRINTF_FORMAT_ATTR(format_param, dots_param) \
__attribute__((format(printf, format_param, dots_param)))
#endif
#endif
#endif
#if defined(PATH_MAX)
# define JS__PATH_MAX PATH_MAX
#elif defined(_WIN32)
# define JS__PATH_MAX 32767
#else
# define JS__PATH_MAX 8192
#endif
void js__pstrcpy(char *buf, int buf_size, const char *str);
char *js__pstrcat(char *buf, int buf_size, const char *s);
int js__strstart(const char *str, const char *val, const char **ptr);
int js__has_suffix(const char *str, const char *suffix);
static inline uint8_t is_be(void) {
union {
uint16_t a;
uint8_t b;
} u = { 0x100 };
return u.b;
}
static inline int max_int(int a, int b)
{
if (a > b)
return a;
else
return b;
}
static inline int min_int(int a, int b)
{
if (a < b)
return a;
else
return b;
}
static inline uint32_t max_uint32(uint32_t a, uint32_t b)
{
if (a > b)
return a;
else
return b;
}
static inline uint32_t min_uint32(uint32_t a, uint32_t b)
{
if (a < b)
return a;
else
return b;
}
static inline int64_t max_int64(int64_t a, int64_t b)
{
if (a > b)
return a;
else
return b;
}
static inline int64_t min_int64(int64_t a, int64_t b)
{
if (a < b)
return a;
else
return b;
}
/* WARNING: undefined if a = 0 */
static inline int clz32(unsigned int a)
{
#if defined(_MSC_VER) && !defined(__clang__)
unsigned long index;
_BitScanReverse(&index, a);
return 31 - index;
#else
return __builtin_clz(a);
#endif
}
/* WARNING: undefined if a = 0 */
static inline int clz64(uint64_t a)
{
#if defined(_MSC_VER) && !defined(__clang__)
#if INTPTR_MAX == INT64_MAX
unsigned long index;
_BitScanReverse64(&index, a);
return 63 - index;
#else
if (a >> 32)
return clz32((unsigned)(a >> 32));
else
return clz32((unsigned)a) + 32;
#endif
#else
return __builtin_clzll(a);
#endif
}
/* WARNING: undefined if a = 0 */
static inline int ctz32(unsigned int a)
{
#if defined(_MSC_VER) && !defined(__clang__)
unsigned long index;
_BitScanForward(&index, a);
return index;
#else
return __builtin_ctz(a);
#endif
}
/* WARNING: undefined if a = 0 */
static inline int ctz64(uint64_t a)
{
#if defined(_MSC_VER) && !defined(__clang__)
unsigned long index;
_BitScanForward64(&index, a);
return index;
#else
return __builtin_ctzll(a);
#endif
}
static inline uint64_t get_u64(const uint8_t *tab)
{
uint64_t v;
memcpy(&v, tab, sizeof(v));
return v;
}
static inline int64_t get_i64(const uint8_t *tab)
{
int64_t v;
memcpy(&v, tab, sizeof(v));
return v;
}
static inline void put_u64(uint8_t *tab, uint64_t val)
{
memcpy(tab, &val, sizeof(val));
}
static inline uint32_t get_u32(const uint8_t *tab)
{
uint32_t v;
memcpy(&v, tab, sizeof(v));
return v;
}
static inline int32_t get_i32(const uint8_t *tab)
{
int32_t v;
memcpy(&v, tab, sizeof(v));
return v;
}
static inline void put_u32(uint8_t *tab, uint32_t val)
{
memcpy(tab, &val, sizeof(val));
}
static inline uint32_t get_u16(const uint8_t *tab)
{
uint16_t v;
memcpy(&v, tab, sizeof(v));
return v;
}
static inline int32_t get_i16(const uint8_t *tab)
{
int16_t v;
memcpy(&v, tab, sizeof(v));
return v;
}
static inline void put_u16(uint8_t *tab, uint16_t val)
{
memcpy(tab, &val, sizeof(val));
}
static inline uint32_t get_u8(const uint8_t *tab)
{
return *tab;
}
static inline int32_t get_i8(const uint8_t *tab)
{
return (int8_t)*tab;
}
static inline void put_u8(uint8_t *tab, uint8_t val)
{
*tab = val;
}
#ifndef bswap16
static inline uint16_t bswap16(uint16_t x)
{
return (x >> 8) | (x << 8);
}
#endif
#ifndef bswap32
static inline uint32_t bswap32(uint32_t v)
{
return ((v & 0xff000000) >> 24) | ((v & 0x00ff0000) >> 8) |
((v & 0x0000ff00) << 8) | ((v & 0x000000ff) << 24);
}
#endif
#ifndef bswap64
static inline uint64_t bswap64(uint64_t v)
{
return ((v & ((uint64_t)0xff << (7 * 8))) >> (7 * 8)) |
((v & ((uint64_t)0xff << (6 * 8))) >> (5 * 8)) |
((v & ((uint64_t)0xff << (5 * 8))) >> (3 * 8)) |
((v & ((uint64_t)0xff << (4 * 8))) >> (1 * 8)) |
((v & ((uint64_t)0xff << (3 * 8))) << (1 * 8)) |
((v & ((uint64_t)0xff << (2 * 8))) << (3 * 8)) |
((v & ((uint64_t)0xff << (1 * 8))) << (5 * 8)) |
((v & ((uint64_t)0xff << (0 * 8))) << (7 * 8));
}
#endif
static inline void inplace_bswap16(uint8_t *tab) {
put_u16(tab, bswap16(get_u16(tab)));
}
static inline void inplace_bswap32(uint8_t *tab) {
put_u32(tab, bswap32(get_u32(tab)));
}
static inline double fromfp16(uint16_t v) {
double d, s;
int e;
if ((v & 0x7C00) == 0x7C00) {
d = (v & 0x3FF) ? NAN : INFINITY;
} else {
d = (v & 0x3FF) / 1024.;
e = (v & 0x7C00) >> 10;
if (e == 0) {
e = -14;
} else {
d += 1;
e -= 15;
}
d = scalbn(d, e);
}
s = (v & 0x8000) ? -1.0 : 1.0;
return d * s;
}
static inline uint16_t tofp16(double d) {
uint16_t f, s;
double t;
int e;
s = 0;
if (copysign(1, d) < 0) { // preserve sign when |d| is negative zero
d = -d;
s = 0x8000;
}
if (isinf(d))
return s | 0x7C00;
if (isnan(d))
return s | 0x7C01;
if (d == 0)
return s | 0;
d = 2 * frexp(d, &e);
e--;
if (e > 15)
return s | 0x7C00; // out of range, return +/-infinity
if (e < -25) {
d = 0;
e = 0;
} else if (e < -14) {
d = scalbn(d, e + 14);
e = 0;
} else {
d -= 1;
e += 15;
}
d *= 1024.;
f = (uint16_t)d;
t = d - f;
if (t < 0.5)
goto done;
if (t == 0.5)
if ((f & 1) == 0)
goto done;
// adjust for rounding
if (++f == 1024) {
f = 0;
if (++e == 31)
return s | 0x7C00; // out of range, return +/-infinity
}
done:
return s | (e << 10) | f;
}
static inline int isfp16nan(uint16_t v) {
return (v & 0x7FFF) > 0x7C00;
}
static inline int isfp16zero(uint16_t v) {
return (v & 0x7FFF) == 0;
}
/* XXX: should take an extra argument to pass slack information to the caller */
typedef void *DynBufReallocFunc(void *opaque, void *ptr, size_t size);
typedef struct DynBuf {
uint8_t *buf;
size_t size;
size_t allocated_size;
bool error; /* true if a memory allocation error occurred */
DynBufReallocFunc *realloc_func;
void *opaque; /* for realloc_func */
} DynBuf;
void dbuf_init(DynBuf *s);
void dbuf_init2(DynBuf *s, void *opaque, DynBufReallocFunc *realloc_func);
int dbuf_realloc(DynBuf *s, size_t new_size);
int dbuf_write(DynBuf *s, size_t offset, const void *data, size_t len);
int dbuf_put(DynBuf *s, const void *data, size_t len);
int dbuf_put_self(DynBuf *s, size_t offset, size_t len);
int dbuf_putc(DynBuf *s, uint8_t c);
int dbuf_putstr(DynBuf *s, const char *str);
static inline int dbuf_put_u16(DynBuf *s, uint16_t val)
{
return dbuf_put(s, (uint8_t *)&val, 2);
}
static inline int dbuf_put_u32(DynBuf *s, uint32_t val)
{
return dbuf_put(s, (uint8_t *)&val, 4);
}
static inline int dbuf_put_u64(DynBuf *s, uint64_t val)
{
return dbuf_put(s, (uint8_t *)&val, 8);
}
int JS_PRINTF_FORMAT_ATTR(2, 3) dbuf_printf(DynBuf *s, JS_PRINTF_FORMAT const char *fmt, ...);
void dbuf_free(DynBuf *s);
static inline bool dbuf_error(DynBuf *s) {
return s->error;
}
static inline void dbuf_set_error(DynBuf *s)
{
s->error = true;
}
/*---- UTF-8 and UTF-16 handling ----*/
#define UTF8_CHAR_LEN_MAX 4
enum {
UTF8_PLAIN_ASCII = 0, // 7-bit ASCII plain text
UTF8_NON_ASCII = 1, // has non ASCII code points (8-bit or more)
UTF8_HAS_16BIT = 2, // has 16-bit code points
UTF8_HAS_NON_BMP1 = 4, // has non-BMP1 code points, needs UTF-16 surrogate pairs
UTF8_HAS_ERRORS = 8, // has encoding errors
};
int utf8_scan(const char *buf, size_t len, size_t *plen);
size_t utf8_encode_len(uint32_t c);
size_t utf8_encode(uint8_t buf[minimum_length(UTF8_CHAR_LEN_MAX)], uint32_t c);
uint32_t utf8_decode_len(const uint8_t *p, size_t max_len, const uint8_t **pp);
uint32_t utf8_decode(const uint8_t *p, const uint8_t **pp);
size_t utf8_decode_buf8(uint8_t *dest, size_t dest_len, const char *src, size_t src_len);
size_t utf8_decode_buf16(uint16_t *dest, size_t dest_len, const char *src, size_t src_len);
size_t utf8_encode_buf8(char *dest, size_t dest_len, const uint8_t *src, size_t src_len);
size_t utf8_encode_buf16(char *dest, size_t dest_len, const uint16_t *src, size_t src_len);
static inline bool is_surrogate(uint32_t c)
{
return (c >> 11) == (0xD800 >> 11); // 0xD800-0xDFFF
}
static inline bool is_hi_surrogate(uint32_t c)
{
return (c >> 10) == (0xD800 >> 10); // 0xD800-0xDBFF
}
static inline bool is_lo_surrogate(uint32_t c)
{
return (c >> 10) == (0xDC00 >> 10); // 0xDC00-0xDFFF
}
static inline uint32_t get_hi_surrogate(uint32_t c)
{
return (c >> 10) - (0x10000 >> 10) + 0xD800;
}
static inline uint32_t get_lo_surrogate(uint32_t c)
{
return (c & 0x3FF) | 0xDC00;
}
static inline uint32_t from_surrogate(uint32_t hi, uint32_t lo)
{
return 65536 + 1024 * (hi & 1023) + (lo & 1023);
}
static inline int from_hex(int c)
{
if (c >= '0' && c <= '9')
return c - '0';
else if (c >= 'A' && c <= 'F')
return c - 'A' + 10;
else if (c >= 'a' && c <= 'f')
return c - 'a' + 10;
else
return -1;
}
static inline uint8_t is_upper_ascii(uint8_t c) {
return c >= 'A' && c <= 'Z';
}
static inline uint8_t to_upper_ascii(uint8_t c) {
return c >= 'a' && c <= 'z' ? c - 'a' + 'A' : c;
}
void rqsort(void *base, size_t nmemb, size_t size,
int (*cmp)(const void *, const void *, void *),
void *arg);
static inline uint64_t float64_as_uint64(double d)
{
union {
double d;
uint64_t u64;
} u;
u.d = d;
return u.u64;
}
static inline double uint64_as_float64(uint64_t u64)
{
union {
double d;
uint64_t u64;
} u;
u.u64 = u64;
return u.d;
}
int64_t js__gettimeofday_us(void);
uint64_t js__hrtime_ns(void);
static inline size_t js__malloc_usable_size(const void *ptr)
{
#if defined(__APPLE__)
return malloc_size(ptr);
#elif defined(_WIN32)
return _msize((void *)ptr);
#elif defined(__linux__) || defined(__ANDROID__) || defined(__CYGWIN__) || defined(__FreeBSD__) || defined(__GLIBC__)
return malloc_usable_size((void *)ptr);
#else
return 0;
#endif
}
int js_exepath(char* buffer, size_t* size);
/* Cross-platform threading APIs. */
#if defined(EMSCRIPTEN) || defined(__wasi__)
#define JS_HAVE_THREADS 0
#else
#define JS_HAVE_THREADS 1
#if defined(_WIN32)
#define JS_ONCE_INIT INIT_ONCE_STATIC_INIT
typedef INIT_ONCE js_once_t;
typedef CRITICAL_SECTION js_mutex_t;
typedef CONDITION_VARIABLE js_cond_t;
typedef HANDLE js_thread_t;
#else
#define JS_ONCE_INIT PTHREAD_ONCE_INIT
typedef pthread_once_t js_once_t;
typedef pthread_mutex_t js_mutex_t;
typedef pthread_cond_t js_cond_t;
typedef pthread_t js_thread_t;
#endif
void js_once(js_once_t *guard, void (*callback)(void));
void js_mutex_init(js_mutex_t *mutex);
void js_mutex_destroy(js_mutex_t *mutex);
void js_mutex_lock(js_mutex_t *mutex);
void js_mutex_unlock(js_mutex_t *mutex);
void js_cond_init(js_cond_t *cond);
void js_cond_destroy(js_cond_t *cond);
void js_cond_signal(js_cond_t *cond);
void js_cond_broadcast(js_cond_t *cond);
void js_cond_wait(js_cond_t *cond, js_mutex_t *mutex);
int js_cond_timedwait(js_cond_t *cond, js_mutex_t *mutex, uint64_t timeout);
enum {
JS_THREAD_CREATE_DETACHED = 1,
};
// creates threads with 2 MB stacks (glibc default)
int js_thread_create(js_thread_t *thrd, void (*start)(void *), void *arg,
int flags);
int js_thread_join(js_thread_t thrd);
#endif /* !defined(EMSCRIPTEN) && !defined(__wasi__) */
// JS requires strict rounding behavior. Turn on 64-bits double precision
// and disable x87 80-bits extended precision for intermediate floating-point
// results. 0x300 is extended precision, 0x200 is double precision.
// Note that `*&cw` in the asm constraints looks redundant but isn't.
#if defined(__i386__) && !defined(_MSC_VER)
#define JS_X87_FPCW_SAVE_AND_ADJUST(cw) \
unsigned short cw; \
__asm__ __volatile__("fnstcw %0" : "=m"(*&cw)); \
do { \
unsigned short t = 0x200 | (cw & ~0x300); \
__asm__ __volatile__("fldcw %0" : /*empty*/ : "m"(*&t)); \
} while (0)
#define JS_X87_FPCW_RESTORE(cw) \
__asm__ __volatile__("fldcw %0" : /*empty*/ : "m"(*&cw))
#else
#define JS_X87_FPCW_SAVE_AND_ADJUST(cw)
#define JS_X87_FPCW_RESTORE(cw)
#endif
#ifdef __cplusplus
} /* extern "C" { */
#endif
#endif /* CUTILS_H */
@@ -0,0 +1,2 @@
// note: file is not actually compiled, only checked for C++ syntax errors
#include "ctest.c"
@@ -0,0 +1,20 @@
# Dependencies
/node_modules
# Production
/build
# Generated files
.docusaurus
.cache-loader
# Misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
@@ -0,0 +1 @@
18
@@ -0,0 +1,3 @@
module.exports = {
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
};
@@ -0,0 +1,63 @@
---
sidebar_position: 2
---
# Building
QuickJS uses [CMake] as its main build system, with an additional helper [Makefile].
:::note
Windows users will need to run the CMake commands manually.
:::
## Getting the source
```bash
git clone https://github.com/quickjs-ng/quickjs.git
cd quickjs
```
## Compiling everything
```bash
make
```
This will build the `qjs` and `qjsc` executables and other test tools. Head over [here](./cli) for
instructions on how to use them.
## Amalgamated builds
The amalgamated build rolls individual source files into a single big file.
It's a good choice for projects that want to use QuickJS without CMake.
Download quickjs-amalgam.zip from [Releases](https://github.com/quickjs-ng/quickjs/releases)
To enable the std, os and bjson modules, compile quickjs-amalgam.c with
`-DQJS_BUILD_LIBC`.
## Debug builds
```bash
make debug
```
This will produce a debug build without optimizations, suitable for developers.
## Running test262
```bash
make test262
```
This will run the test262 suite.
```bash
make test262-update
```
This will run the test262 suite and update the error / pass report, useful after
implementing a new feature that would alter the result of the test suite.
[CMake]: https://cmake.org
[Makefile]: https://www.gnu.org/software/make/
@@ -0,0 +1,130 @@
---
sidebar_position: 4
---
# The qjs and qjsc CLI tools
## `qjs` - The QuickJS JavaScript interpreter
The `qjs` executable runs the JavaScript interpreter. It includes a simple standard
library and REPL.
```
$ qjs
usage: qjs [options] [file [args]]
-h --help list options
-e --eval EXPR evaluate EXPR
-i --interactive go to interactive mode
-m --module load as ES6 module (default=autodetect)
--script load as ES6 script (default=autodetect)
-I --include file include an additional file
--std make 'std', 'os' and 'bjson' available to script
-T --trace trace memory allocation
-d --dump dump the memory usage stats
-D --dump-flags flags for dumping debug data (see DUMP_* defines)
-c --compile FILE compile the given JS file as a standalone executable
-o --out FILE output file for standalone executables
--exe select the executable to use as the base, defaults to the current one
--memory-limit n limit the memory usage to 'n' Kbytes
--stack-size n limit the stack size to 'n' Kbytes
--unhandled-rejection dump unhandled promise rejections
-q --quit just instantiate the interpreter and quit
```
The following dump flags are supported:
```
DUMP_BYTECODE_FINAL 0x01 /* dump pass 3 final byte code */
DUMP_BYTECODE_PASS2 0x02 /* dump pass 2 code */
DUMP_BYTECODE_PASS1 0x04 /* dump pass 1 code */
DUMP_BYTECODE_HEX 0x10 /* dump bytecode in hex */
DUMP_BYTECODE_PC2LINE 0x20 /* dump line number table */
DUMP_BYTECODE_STACK 0x40 /* dump compute_stack_size */
DUMP_BYTECODE_STEP 0x80 /* dump executed bytecode */
DUMP_READ_OBJECT 0x100 /* dump the marshalled objects at load time */
DUMP_FREE 0x200 /* dump every object free */
DUMP_GC 0x400 /* dump the occurrence of the automatic GC */
DUMP_GC_FREE 0x800 /* dump objects freed by the GC */
DUMP_MODULE_RESOLVE 0x1000 /* dump module resolution steps */
DUMP_PROMISE 0x2000 /* dump promise steps */
DUMP_LEAKS 0x4000 /* dump leaked objects and strings in JS_FreeRuntime */
DUMP_ATOM_LEAKS 0x8000 /* dump leaked atoms in JS_FreeRuntime */
DUMP_MEM 0x10000 /* dump memory usage in JS_FreeRuntime */
DUMP_OBJECTS 0x20000 /* dump objects in JS_FreeRuntime */
DUMP_ATOMS 0x40000 /* dump atoms in JS_FreeRuntime */
DUMP_SHAPES 0x80000 /* dump shapes in JS_FreeRuntime */
```
### Creating standalone executables
With the `qjs` CLI it's possible to create standalone executables that will bundle the given JavaScript file
alongside the binary.
```
$ qjs -c app.js -o app --exe qjs
```
The resulting `app` binary will have the same runtime dependencies as the `qjs` binary. This is acomplished
by compiling the target JavaScript file to bytecode and adding it a copy of the executable, with a little
trailer to help locate it. `--exe` expects the absolute path to `qjs`, e.g., `~/bin/qjs` or `$HOME/bin/qjs`.
Rather than using the current executable, it's possible to use the `--exe` switch to create standalone
executables for other platforms.
No JavaScript bundling is performed, the specified JS file cannot depend on other files. A bundler such
as `esbuild` can be used to generate an app bundle which can then be turned into the executable.
```
npx esbuild my-app/index.js \
--bundle \
--outfile=app.js \
--external:qjs:* \
--minify \
--target=es2023 \
--platform=neutral \
--format=esm \
--main-fields=main,module
```
## `qjsc` - The QuickJS JavaScript compiler
The `qjsc` executable runs the JavaScript compiler, it can generate bytecode from
source files which can then be embedded in an executable, or it can generate the necessary
scaffolding to build a C application which embeds QuickJS.
```
$ qjsc
usage: qjsc [options] [files]
options are:
-b output raw bytecode instead of C code
-e output main() and bytecode in a C file
-o output set the output filename
-n script_name set the script name (as used in stack traces)
-N cname set the C name of the generated data
-m compile as JavaScript module (default=autodetect)
-D module_name compile a dynamically loaded module or worker
-M module_name[,cname] add initialization code for an external C module
-p prefix set the prefix of the generated C names
-s strip the source code, specify twice to also strip debug info
-S n set the maximum stack size to 'n' bytes (default=262144)
```
Here is an example on how to create a standalone executable that embeds QuickJS
and the `examples/hello.js` JavaScript file:
```bash
# Make sure you are in the QuickJS source directory.
$ cc hello.c cutils.c dtoa.c libregexp.c libunicode.c quickjs.c quickjs-libc.c -I. -o hello
```
The resulting binary `hello` will be in the current directory.
```bash
$ ./hello
Hello World
```
:::note
See the ["Creating standalone executables"](#creating-standalone-executables) section for a simpler way.
:::
@@ -0,0 +1,7 @@
{
"label": "Developer Guide",
"position": 6,
"link": {
"type": "generated-index"
}
}
@@ -0,0 +1,3 @@
# API Reference
WIP.
@@ -0,0 +1,117 @@
# Internals
## Bytecode
The compiler generates bytecode directly with no intermediate
representation such as a parse tree, hence it is very fast. Several
optimizations passes are done over the generated bytecode.
A stack-based bytecode was chosen because it is simple and generates
compact code.
For each function, the maximum stack size is computed at compile time so that
no runtime stack overflow tests are needed.
A separate compressed line number table is maintained for the debug
information.
Access to closure variables is optimized and is almost as fast as local
variables.
Direct `eval` in strict mode is optimized.
## Runtime
### Strings
Strings are stored either as an 8 bit or a 16 bit array of
characters. Hence random access to characters is always fast.
The C API provides functions to convert JavaScript Strings to C UTF-8 encoded
strings. The most common case where the JavaScript string contains
only ASCII characters involves no copying.
### Objects
The object shapes (object prototype, property names and flags) are shared
between objects to save memory.
Arrays with no holes (except at the end of the array) are optimized.
TypedArray accesses are optimized.
### Atoms
Object property names and some strings are stored as Atoms (unique
strings) to save memory and allow fast comparison. Atoms are
represented as a 32 bit integer. Half of the atom range is reserved for
immediate integer literals from 0 to 2^31-1.
### Numbers
Numbers are represented either as 32-bit signed integers or 64-bit IEEE-754
floating point values. Most operations have fast paths for the 32-bit
integer case.
### Garbage collection
Reference counting is used to free objects automatically and
deterministically. A separate cycle removal pass is done when the allocated
memory becomes too large. The cycle removal algorithm only uses the
reference counts and the object content, so no explicit garbage
collection roots need to be manipulated in the C code.
### JSValue
It is a JavaScript value which can be a primitive type (such as
Number, String, ...) or an Object. NaN boxing is used in the 32-bit version
to store 64-bit floating point numbers. The representation is
optimized so that 32-bit integers and reference counted values can be
efficiently tested.
In 64-bit code, JSValue are 128-bit large and no NaN boxing is used. The
rationale is that in 64-bit code memory usage is less critical.
In both cases (32 or 64 bits), JSValue exactly fits two CPU registers,
so it can be efficiently returned by C functions.
### Function call
The engine is optimized so that function calls are fast. The system
stack holds the JavaScript parameters and local variables.
### RegExp
A specific regular expression engine was developed. It is both small
and efficient and supports all the ES2020+ features including the
Unicode properties. As the JavaScript compiler, it directly generates
bytecode without a parse tree.
Backtracking with an explicit stack is used so that there is no
recursion on the system stack. Simple quantifiers are specifically
optimized to avoid recursions.
Infinite recursions coming from quantifiers with empty terms are
avoided.
The full regexp library weighs about 15 KiB (x86 code), excluding the
Unicode library.
### Unicode
A specific Unicode library was developed so that there is no
dependency on an external large Unicode library such as ICU. All the
Unicode tables are compressed while keeping a reasonable access
speed.
The library supports case conversion, Unicode normalization, Unicode
script queries, Unicode general category queries and all Unicode
binary properties.
The full Unicode library weighs about 45 KiB (x86 code).
### BigInt
BigInt is implemented with the [libbf](https://bellard.org/libbf) library.
It weights about 90 KiB (x86 code) and provides arbitrary precision IEEE 754 floating
point operations and transcendental functions with exact rounding.
@@ -0,0 +1,113 @@
---
sidebar_position: 1
---
# The QuickJS C API
The C API was designed to be simple and efficient. The C API is
defined in the header `quickjs.h`.
## Runtime and contexts
`JSRuntime` represents a JavaScript runtime corresponding to an
object heap. Several runtimes can exist at the same time but they
cannot exchange objects. Inside a given runtime, no multi-threading is
supported.
`JSContext` represents a JavaScript context (or Realm). Each
JSContext has its own global objects and system objects. There can be
several JSContexts per JSRuntime and they can share objects, similar
to frames of the same origin sharing JavaScript objects in a
web browser.
## JSValue
`JSValue` represents a JavaScript value which can be a primitive
type or an object. Reference counting is used, so it is important to
explicitly duplicate (`JS_DupValue()`, increment the reference
count) or free (`JS_FreeValue()`, decrement the reference count)
JSValues.
## C functions
C functions can be created with
`JS_NewCFunction()`. `JS_SetPropertyFunctionList()` is a
shortcut to easily add functions, setters and getters properties to a
given object.
Unlike other embedded JavaScript engines, there is no implicit stack,
so C functions get their parameters as normal C parameters. As a
general rule, C functions take constant `JSValue`s as parameters
(so they don't need to free them) and return a newly allocated (=live)
`JSValue`.
## Exceptions
Most C functions can return a JavaScript exception. It
must be explicitly tested and handled by the C code. The specific
`JSValue` `JS_EXCEPTION` indicates that an exception
occurred. The actual exception object is stored in the
`JSContext` and can be retrieved with `JS_GetException()`.
## Script evaluation
Use `JS_Eval()` to evaluate a script or module source.
If the script or module was compiled to bytecode with `qjsc`, it
can be evaluated by calling `js_std_eval_binary()`. The advantage
is that no compilation is needed so it is faster and smaller because
the compiler can be removed from the executable if no `eval` is
required.
Note: the bytecode format is linked to a given QuickJS
version. Moreover, no security check is done before its
execution. Hence the bytecode should not be loaded from untrusted
sources.
## JS Classes
C opaque data can be attached to a JavaScript object. The type of the
C opaque data is determined with the class ID (`JSClassID`) of
the object. Hence the first step is to register a new class ID and JS
class (`JS_NewClassID()`, `JS_NewClass()`). Then you can
create objects of this class with `JS_NewObjectClass()` and get or
set the C opaque point with `JS_GetOpaque()` / `JS_SetOpaque()`.
When defining a new JS class, it is possible to declare a finalizer
which is called when the object is destroyed. The finalizer should be
used to release C resources. It is invalid to execute JS code from
it. A `gc_mark` method can be provided so that the cycle removal
algorithm can find the other objects referenced by this object. Other
methods are available to define exotic object behaviors.
The Class ID are allocated per-runtime. The
`JSClass` are allocated per `JSRuntime`. `JS_SetClassProto()`
is used to define a prototype for a given class in a given
`JSContext`. `JS_NewObjectClass()` sets this prototype in the
created object.
Examples are available in `quickjs-libc.c`.
## C Modules
Native ES6 modules are supported and can be dynamically or statically
linked. The standard library `quickjs-libc.c` is a good example
of a native module.
## Memory handling
Use `JS_SetMemoryLimit()` to set a global memory allocation limit
to a given `JSRuntime`.
Custom memory allocation functions can be provided with `JS_NewRuntime2()`.
The maximum system stack size can be set with `JS_SetMaxStackSize()`.
## Execution timeout and interrupts
Use `JS_SetInterruptHandler()` to set a callback which is
regularly called by the engine when it is executing code. This
callback can be used to implement an execution timeout.
It is used by the command line interpreter to implement a
`Ctrl-C` handler.
@@ -0,0 +1,68 @@
# Differences with bellard/quickjs
This project aims to be a drop-in replacement for those already using QuickJS.
Minimal API changes might be necessary.
## Community development
NG is developed in the open, interacting with the wider community and through
these interactions many improvements have already been made, including the incorporation
of patches previously maintained in other forks.
Each PR is reviewed, iterated on, and merged in GitHub.
To date, NG has had over 40 distinct contributors and over 400 PRs.
## Consistent release cadence
As the project moves forward, a steady cadence of releases has been maintained, with an
average of a new release every 2 months.
## Testing
Since its inception testing has been a focus. Each PR is tested in over 50 configurations,
involving different operating systems, build types and sanitizers.
The `test262` suite is also ran for every change.
## Cross-platform support
In order to better support other platforms such as Windows the build system was
changed to use [CMake].
In addition, Windows is treated as a first class citizen, with the addition of support
for the MSVC compiler.
[CMake]: https://cmake.org/
## Performance
While being an interpreter limits the performance in comparison with other engines which
use a JIT, several significant performance improvements have been made:
- Opcode fusion
- Polymorphic inline caching
- Memory allocation improvements
- Improved parse speeds
## New ECMAScript APIs
The main focus of NG is to deliver state-of-the-art JavaScript features. Typically once they
are stable (stage 4) but sometimes even at earlier stages. Here is a non-exhaustive list
of ES features present in NG:
- Resizable ArrayBuffer
- Float16Array
- WeakRef
- FinalizationRegistry
- Iterator Helpers
- Promise.try
- Error.isError
- Set operations
Some non-standard but widely used APIs have also been added:
- V8's [stack trace API](https://v8.dev/docs/stack-trace-api)
- `Error.captureStackTrace`
- `Error.prepareStackTrace`
- `Error.stackTraceLimit`
@@ -0,0 +1,11 @@
---
sidebar_position: 7
---
# ECMAScript Features
QuickJS aims to support the latest available ECMAScript features once they hit the spec.
Progress on _test262_ compliance can be checked [here](https://test262.fyi/#|qjs_ng).
Due to size constraints it is unlikely QuickJS will ever support the [Intl](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl) APIs.

Some files were not shown because too many files have changed in this diff Show More