feat(payment): standalone ZarinPal broker on pay.flatrender.ir

A generic multi-client payment gateway so FlatRender, meezi.ir and
bargevasat.ir can all pay through ZarinPal's single verified callback
domain (pay.flatrender.ir).

New Go service services/payment (clones the notification skeleton +
vendored deps):
- migration 31_payment_broker.sql — `payment` schema: client_apps,
  transactions, webhook_deliveries.
- ZarinPal v4 client ported from the proven identity PaymentService
  (request.json -> StartPay -> verify.json; codes 100/101).
- client API: POST /v1/pay/request + /v1/pay/inquiry, authed by
  X-Api-Key + HMAC body signature; GET /callback/zarinpal (the single
  verified endpoint) verifies, then 302s the user back to the site's
  return_url (signed) and fires a signed, retried webhook.
- per-client ZarinPal merchant override (default = shared merchant);
  amount stored canonically in Rial, unit to ZarinPal env-configurable.
- admin API /v1/admin/* (FlatRender admin JWT): client-app CRUD +
  key issue/rotate + transactions list.

Deploy wiring: payment-svc in docker-compose.v2.yml (host port 1607),
pay.flatrender.ir server block in mirror-nginx conf, ENV_FILE +
README updates (cert SAN + manual migration note).

Admin UI: src/components/admin/PaymentsAdmin.tsx (client apps with
one-time key reveal + rotate, transactions table) + /admin/payments
page + nav link + fa/en strings; pay-admin proxy route to payment-svc.

Docs/SDK: deploy/PAYMENTS.md (integration contract) + deploy/sdk/flatpay.js
(zero-dep Node client + webhook verifier) for meezi/any site.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-15 23:59:54 +03:30
parent 896ce3dfa9
commit ec51e87d2d
1830 changed files with 899129 additions and 8 deletions
+111
View File
@@ -0,0 +1,111 @@
-- =====================================================================
-- PAYMENT BROKER SCHEMA — generic multi-client ZarinPal gateway
-- Served on pay.flatrender.ir (the single ZarinPal-verified callback domain).
-- Other sites (meezi.ir, bargevasat.ir, FlatRender) register as client_apps
-- and route payments through this broker.
--
-- NOTE: migrations auto-run only on FIRST volume creation. On an existing
-- DB volume, apply this manually:
-- docker exec -i fr2-postgres psql -U postgres -d flatrender < 31_payment_broker.sql
-- =====================================================================
CREATE SCHEMA IF NOT EXISTS payment;
SET search_path TO payment, public;
-- ---------------------------------------------------------------------
-- client_apps — each site that pays through the broker
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS payment.client_apps (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID, -- optional link to identity.tenants
name TEXT NOT NULL, -- "meezi.ir", "FlatRender"
slug TEXT NOT NULL UNIQUE, -- "meezi"
api_key TEXT NOT NULL UNIQUE, -- public id (pk_...)
secret TEXT NOT NULL, -- shared HMAC secret (sk_...) — signs in+out
-- ZarinPal: per-client override; NULL → broker default merchant/sandbox
zarinpal_merchant_id TEXT,
zarinpal_sandbox BOOLEAN,
allowed_return_origins TEXT[] NOT NULL DEFAULT '{}', -- e.g. {'https://meezi.ir'}; empty = permissive
webhook_url TEXT, -- server-to-server result notification
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- ---------------------------------------------------------------------
-- transactions — one row per payment attempt
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS payment.transactions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
client_app_id UUID NOT NULL REFERENCES payment.client_apps(id) ON DELETE RESTRICT,
status TEXT NOT NULL DEFAULT 'Created', -- Created|Pending|Paid|Failed|Cancelled|Expired
gateway TEXT NOT NULL DEFAULT 'ZarinPal',
amount_rial BIGINT NOT NULL, -- canonical Rial
currency TEXT NOT NULL DEFAULT 'IRR',
description TEXT,
client_ref TEXT, -- the client's own order id
return_url TEXT NOT NULL, -- where the user is sent back
metadata JSONB, -- echoed back to the client
payer_mobile TEXT,
payer_email TEXT,
authority TEXT, -- ZarinPal authority token
ref_id TEXT, -- ZarinPal ref_id (receipt)
card_pan TEXT,
fee_rial BIGINT,
gateway_response JSONB,
failure_reason TEXT,
paid_at TIMESTAMPTZ,
failed_at TIMESTAMPTZ,
expires_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_pay_txn_client ON payment.transactions(client_app_id, created_at DESC);
CREATE UNIQUE INDEX IF NOT EXISTS idx_pay_txn_authority ON payment.transactions(authority) WHERE authority IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_pay_txn_clientref ON payment.transactions(client_app_id, client_ref) WHERE client_ref IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_pay_txn_status ON payment.transactions(status);
-- ---------------------------------------------------------------------
-- webhook_deliveries — outbound signed notifications with retry
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS payment.webhook_deliveries (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
transaction_id UUID NOT NULL REFERENCES payment.transactions(id) ON DELETE CASCADE,
url TEXT NOT NULL,
payload JSONB NOT NULL,
signature TEXT NOT NULL,
attempts INT NOT NULL DEFAULT 0,
delivered BOOLEAN NOT NULL DEFAULT FALSE,
last_status INT,
last_error TEXT,
next_attempt_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_pay_wh_pending ON payment.webhook_deliveries(delivered, next_attempt_at) WHERE delivered = FALSE;
-- ---------------------------------------------------------------------
-- updated_at triggers (helper tg_set_updated_at() created in 00_setup.sql)
-- ---------------------------------------------------------------------
DROP TRIGGER IF EXISTS tg_pay_client_apps_updated ON payment.client_apps;
CREATE TRIGGER tg_pay_client_apps_updated BEFORE UPDATE ON payment.client_apps
FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
DROP TRIGGER IF EXISTS tg_pay_transactions_updated ON payment.transactions;
CREATE TRIGGER tg_pay_transactions_updated BEFORE UPDATE ON payment.transactions
FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
DROP TRIGGER IF EXISTS tg_pay_webhook_updated ON payment.webhook_deliveries;
CREATE TRIGGER tg_pay_webhook_updated BEFORE UPDATE ON payment.webhook_deliveries
FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
+11
View File
@@ -22,6 +22,7 @@ EDGE_BIND=0.0.0.0
# nginx-facing host ports (must be free on 171.22.25.73 — :3000 is Gitea, avoid it). # nginx-facing host ports (must be free on 171.22.25.73 — :3000 is Gitea, avoid it).
FRONTEND_PORT=1600 FRONTEND_PORT=1600
GATEWAY_PORT=1605 GATEWAY_PORT=1605
PAY_PORT=1607
MINIO_PORT=1610 MINIO_PORT=1610
MINIO_CONSOLE_PORT=1611 MINIO_CONSOLE_PORT=1611
@@ -58,10 +59,20 @@ MINIO_HOST_USE_SSL=true
RENDER_DEV_WORKER=false RENDER_DEV_WORKER=false
RENDER_DEV_SNAPSHOTS=false RENDER_DEV_SNAPSHOTS=false
# ── Payment broker (pay.flatrender.ir) ───────────────────────────────────────
# Standalone ZarinPal gateway shared by FlatRender + meezi.ir + bargevasat.ir.
# ZARINPAL_MERCHANT_ID below is the SHARED merchant (verified domain = pay.flatrender.ir).
PAY_PUBLIC_URL=https://pay.flatrender.ir
# Unit ZarinPal expects in `amount`: "rial" (official v4) or "toman".
# ⚠️ Your identity service historically sends Toman — confirm with one sandbox
# payment which unit YOUR merchant settles in, then set this to match.
ZARINPAL_AMOUNT_UNIT=rial
# ── Payments (fill the providers you use; leave others blank) ──────────────── # ── Payments (fill the providers you use; leave others blank) ────────────────
STRIPE_SECRET_KEY= STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET= STRIPE_WEBHOOK_SECRET=
STRIPE_PUBLISHABLE_KEY= STRIPE_PUBLISHABLE_KEY=
# Shared ZarinPal merchant — used by BOTH the identity service and the pay broker.
ZARINPAL_MERCHANT_ID= ZARINPAL_MERCHANT_ID=
ZARINPAL_CALLBACK_URL=https://api.flatrender.ir/v1/payments/callback/zarinpal ZARINPAL_CALLBACK_URL=https://api.flatrender.ir/v1/payments/callback/zarinpal
ZARINPAL_SANDBOX=false ZARINPAL_SANDBOX=false
+149
View File
@@ -0,0 +1,149 @@
# FlatRender Pay — ZarinPal Broker Integration Guide
`pay.flatrender.ir` is a **standalone, multi-client ZarinPal gateway**. Any site
(FlatRender, meezi.ir, bargevasat.ir, …) routes payments through it, because
ZarinPal only accepts callbacks on the single verified domain `pay.flatrender.ir`.
```
your site ──POST /v1/pay/request──► pay.flatrender.ir ──► ZarinPal request.json
▲ (api key + HMAC) │ │
│ ▼ authority
└──◄ 302 return_url (signed) ◄── /callback/zarinpal ◄── user pays on ZarinPal
└──◄ POST webhook_url (signed) ◄────────┘ (verify.json → ref_id)
```
You get a **client app** from the FlatRender admin (Admin → پرداخت → اپلیکیشن‌ها):
- `api_key` — public id, sent as `X-Api-Key` (e.g. `pk_…`)
- `secret` — shown **once**; signs your requests AND verifies broker callbacks (`sk_…`)
- `webhook_url` — optional server-to-server result endpoint
- `allowed_return_origins` — the origins your `return_url` may use (empty = any)
---
## 1. Create a payment
`POST https://pay.flatrender.ir/v1/pay/request`
Headers:
| Header | Value |
|---|---|
| `Content-Type` | `application/json` |
| `X-Api-Key` | your `api_key` |
| `X-Signature` | `hex( HMAC_SHA256(secret, <raw request body bytes>) )` |
Body:
```json
{
"amount": 50000,
"currency": "IRT",
"description": "خرید اشتراک طلایی",
"client_ref": "order-1234",
"return_url": "https://meezi.ir/payment/return",
"mobile": "09120000000",
"email": "user@example.com",
"metadata": { "user_id": "42", "plan": "gold" }
}
```
- `amount` — integer. `currency` is `"IRR"` (Rial, default) or `"IRT"` (Toman). The
broker stores the canonical Rial value and converts for ZarinPal.
- `client_ref` — your own order id (echoed back everywhere).
- `return_url` — where the user's browser is sent after payment. Must match an
`allowed_return_origins` entry if you configured any.
- `metadata` — arbitrary JSON, echoed back in the redirect signature scope + webhook.
Response `200`:
```json
{
"id": "9b2c…", // broker transaction id
"status": "Pending",
"payment_url": "https://www.zarinpal.com/pg/StartPay/A000…",
"authority": "A000…",
"amount_rial": 500000
}
```
**Redirect the user's browser to `payment_url`.**
---
## 2. The user comes back (browser redirect)
After ZarinPal, the broker verifies the payment and `302`-redirects the browser to
your `return_url` with a **signed** result appended:
```
https://meezi.ir/payment/return?status=Paid&id=9b2c…&ref_id=123456789&sign=<hex>
```
- `status``Paid` | `Failed` | `Cancelled`
- `ref_id` — ZarinPal receipt (only when paid)
- `sign``hex( HMAC_SHA256(secret, "{id}.{status}.{ref_id}.{amount_rial}") )`
⚠️ The redirect is **not** proof of payment on its own (a user can craft a URL).
Treat it as a UX hint, then **confirm with the webhook (§3) or the inquiry API (§4)**.
To verify the redirect signature you need `amount_rial`; fetch it via the inquiry
API, or just rely on the webhook / inquiry as the source of truth.
---
## 3. Webhook (recommended — the source of truth)
If `webhook_url` is set, the broker POSTs a **signed** JSON body to it when a payment
finishes (with retry + exponential backoff up to ~1h):
Headers: `X-FlatPay-Signature: <hex>`, `X-FlatPay-Event: payment`
```json
{
"event": "payment.paid",
"id": "9b2c…",
"status": "Paid",
"amount_rial": 500000,
"currency": "IRR",
"client_ref": "order-1234",
"ref_id": "123456789",
"authority": "A000…",
"card_pan": "6037********1234",
"metadata": { "user_id": "42", "plan": "gold" },
"paid_at": "2026-06-15T14:00:00Z",
"ts": 1750000000
}
```
Verify: `HMAC_SHA256(secret, <raw body bytes>) == X-FlatPay-Signature`. Respond `2xx`
to acknowledge (anything else is retried). Make handling **idempotent** (keyed on
`id` or `client_ref`) — duplicate deliveries are possible.
---
## 4. Inquiry (authoritative pull)
`POST https://pay.flatrender.ir/v1/pay/inquiry` (same `X-Api-Key` + `X-Signature` as §1)
```json
{ "id": "9b2c…" }
```
Returns the full transaction (status, `ref_id`, `amount_rial`, …). Use this from your
`return_url` handler to confirm before granting the user anything.
---
## Reference signature recipe
```
signature = hex( HMAC_SHA256(client_secret, message_bytes) )
```
- **request / inquiry**: `message_bytes` = the exact raw JSON body you send.
- **return redirect**: `message_bytes` = UTF-8 of `"{id}.{status}.{ref_id}.{amount_rial}"`.
- **webhook**: `message_bytes` = the exact raw JSON body received.
See [`sdk/flatpay.js`](./sdk/flatpay.js) for a drop-in Node client + Express webhook
verifier.
+18 -7
View File
@@ -14,17 +14,27 @@ Stack: gateway · identity · content · studio (.NET/Go) · file · render · n
mirror-nginx (:443, /etc/ssl/flatrender) mirror-nginx (:443, /etc/ssl/flatrender)
flatrender.ir → 171.22.25.73:1600 (fr2-frontend) flatrender.ir → 171.22.25.73:1600 (fr2-frontend)
api.flatrender.ir → 171.22.25.73:1605 (fr2-gateway) api.flatrender.ir → 171.22.25.73:1605 (fr2-gateway)
pay.flatrender.ir → 171.22.25.73:1607 (fr2-payment, ZarinPal broker)
storage.flatrender.ir → 171.22.25.73:1610 (fr2-minio) storage.flatrender.ir → 171.22.25.73:1610 (fr2-minio)
``` ```
The **payment broker** (`fr2-payment`, `pay.flatrender.ir`) is a standalone generic
ZarinPal gateway shared by FlatRender + meezi.ir + bargevasat.ir — ZarinPal only
accepts callbacks on that one verified domain. It does NOT sit behind the API
gateway (clients authenticate with an API key + HMAC). See
[`PAYMENTS.md`](./PAYMENTS.md) for the integration contract. The `payment` schema
is migration `31_payment_broker.sql` — on an existing DB volume it must be applied
manually (migrations only auto-run on first volume creation):
`docker exec -i fr2-postgres psql -U postgres -d flatrender < backend/db/migrations/31_payment_broker.sql`.
## One-time setup (do these BEFORE the first `git push gitea master`) ## One-time setup (do these BEFORE the first `git push gitea master`)
1. **DNS** — this box sits BEHIND NAT: its interface IP is `171.22.25.73` (private), 1. **DNS** — this box sits BEHIND NAT: its interface IP is `171.22.25.73` (private),
public NAT IPs are `31.171.101.127/.211`, and inbound 443 normally arrives via the public NAT IPs are `31.171.101.127/.211`, and inbound 443 normally arrives via the
edge/CDN `185.239.1.100` (same entry your other sites use, e.g. `meezi.ir`). So a new edge/CDN `185.239.1.100` (same entry your other sites use, e.g. `meezi.ir`). So a new
domain must enter the SAME way the others do — either: domain must enter the SAME way the others do — either:
- register `flatrender.ir` + `api` + `storage` + `www` in that edge/CDN (origin = this - register `flatrender.ir` + `api` + `pay` + `storage` + `www` in that edge/CDN (origin =
server) and point DNS there, **or** this server) and point DNS there, **or**
- bypass the CDN and point DNS straight at the server's public IP (like the hokm `api` - bypass the CDN and point DNS straight at the server's public IP (like the hokm `api`
subdomain does — "must bypass"). subdomain does — "must bypass").
Pointing DNS at a random/registrar IP shows that host's default page (e.g. a "not Pointing DNS at a random/registrar IP shows that host's default page (e.g. a "not
@@ -37,7 +47,7 @@ Stack: gateway · identity · content · studio (.NET/Go) · file · render · n
cp <yourcert>/fullchain.pem /etc/ssl/soroushasadi/flatrender/ cp <yourcert>/fullchain.pem /etc/ssl/soroushasadi/flatrender/
cp <yourcert>/privateKey.pem /etc/ssl/soroushasadi/flatrender/ cp <yourcert>/privateKey.pem /etc/ssl/soroushasadi/flatrender/
``` ```
Cert must cover `flatrender.ir` + `api.` + `storage.` (wildcard `*.flatrender.ir` + apex, or SAN). Cert must cover `flatrender.ir` + `api.` + `pay.` + `storage.` (wildcard `*.flatrender.ir` + apex, or SAN).
3. **mirror-nginx** — add the server blocks from [`mirror-nginx-flatrender.conf`](./mirror-nginx-flatrender.conf) 3. **mirror-nginx** — add the server blocks from [`mirror-nginx-flatrender.conf`](./mirror-nginx-flatrender.conf)
to the proxy's `http{}` (the host file is `/root/mirror-server/nginx/nginx.conf`), then: to the proxy's `http{}` (the host file is `/root/mirror-server/nginx/nginx.conf`), then:
`docker exec mirror-nginx nginx -t && docker exec mirror-nginx nginx -s reload`. `docker exec mirror-nginx nginx -t && docker exec mirror-nginx nginx -s reload`.
@@ -63,10 +73,11 @@ blocks (step 3) and visit `https://flatrender.ir`.
## Host ports (must be free on 171.22.25.73) ## Host ports (must be free on 171.22.25.73)
`1600` frontend · `1605` gateway · `1610` MinIO · `1611` MinIO console. Postgres (5432) `1600` frontend · `1605` gateway · `1607` payment broker · `1610` MinIO · `1611` MinIO
and render (5010) bind to `127.0.0.1` only. Avoid `:3000` (Gitea), `:8081-8083` (Nexus), console. Postgres (5432) and render (5010) bind to `127.0.0.1` only. Avoid `:3000` (Gitea),
`:1500/1505/1520` (bargevasat), `:3010/3101-3103/5080/5081` (meezi), `:3020`, `:2569`. `:8081-8083` (Nexus), `:1500/1505/1520` (bargevasat), `:3010/3101-3103/5080/5081` (meezi),
Change them via `FRONTEND_PORT`/`GATEWAY_PORT`/`MINIO_PORT` in the secret if any collide. `:3020`, `:2569`. Change them via `FRONTEND_PORT`/`GATEWAY_PORT`/`PAY_PORT`/`MINIO_PORT` in
the secret if any collide.
## First-run notes ## First-run notes
+21 -1
View File
@@ -29,7 +29,7 @@
server { server {
listen 80; listen 80;
server_name flatrender.ir www.flatrender.ir api.flatrender.ir storage.flatrender.ir; server_name flatrender.ir www.flatrender.ir api.flatrender.ir storage.flatrender.ir pay.flatrender.ir;
return 301 https://$host$request_uri; return 301 https://$host$request_uri;
} }
@@ -87,3 +87,23 @@ server {
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
} }
} }
# ── Payment broker (→ PAY_PORT) — pay.flatrender.ir ───────────────────────
# The single ZarinPal-verified callback domain. ZarinPal redirects users to
# pay.flatrender.ir/callback/zarinpal; the broker then bounces them to the
# originating site's return_url. Must NOT be cached by any upstream CDN.
server {
listen 443 ssl; http2 on;
server_name pay.flatrender.ir;
client_max_body_size 5m;
ssl_certificate /etc/ssl/soroushasadi/flatrender/fullchain.pem;
ssl_certificate_key /etc/ssl/soroushasadi/flatrender/privateKey.pem;
location / {
proxy_pass http://171.22.25.73:1607;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
+112
View File
@@ -0,0 +1,112 @@
// FlatRender Pay — drop-in Node client for the ZarinPal broker (pay.flatrender.ir).
// Zero dependencies: Node 18+ (global fetch) + built-in crypto. CommonJS.
//
// const { FlatPay } = require("./flatpay");
// const pay = new FlatPay({ apiKey: process.env.FLATPAY_KEY, secret: process.env.FLATPAY_SECRET });
//
// // 1. create + redirect
// const r = await pay.createPayment({ amount: 50000, currency: "IRT",
// description: "اشتراک", clientRef: order.id, returnUrl: "https://meezi.ir/pay/return",
// metadata: { userId: user.id } });
// res.redirect(r.payment_url);
//
// // 2. on your return_url handler — confirm authoritatively
// const txn = await pay.inquire(req.query.id);
// if (txn.status === "Paid") { /* grant */ }
//
// // 3. webhook (recommended) — Express:
// app.post("/flatpay/webhook", express.raw({ type: "*/*" }), (req, res) => {
// if (!pay.verifyWebhook(req.body, req.get("X-FlatPay-Signature"))) return res.sendStatus(401);
// const ev = JSON.parse(req.body.toString("utf8"));
// if (ev.status === "Paid") { /* idempotent grant keyed on ev.id / ev.client_ref */ }
// res.sendStatus(200);
// });
const crypto = require("crypto");
const DEFAULT_BASE = "https://pay.flatrender.ir";
function hmac(secret, message) {
return crypto.createHmac("sha256", secret).update(message).digest("hex");
}
function timingSafeEqualHex(a, b) {
try {
const ba = Buffer.from(a, "hex");
const bb = Buffer.from(b, "hex");
return ba.length === bb.length && crypto.timingSafeEqual(ba, bb);
} catch {
return false;
}
}
class FlatPay {
constructor({ apiKey, secret, baseUrl = DEFAULT_BASE } = {}) {
if (!apiKey || !secret) throw new Error("FlatPay: apiKey and secret are required");
this.apiKey = apiKey;
this.secret = secret;
this.baseUrl = baseUrl.replace(/\/+$/, "");
}
async _signedPost(path, payload) {
const body = JSON.stringify(payload);
const res = await fetch(this.baseUrl + path, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Api-Key": this.apiKey,
"X-Signature": hmac(this.secret, body),
},
body,
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
const err = new Error(data.message || `FlatPay ${path} failed (${res.status})`);
err.code = data.code;
err.status = res.status;
throw err;
}
return data;
}
/** Create a payment. Returns { id, status, payment_url, authority, amount_rial }. */
createPayment({ amount, currency = "IRR", description, clientRef, returnUrl, mobile, email, metadata }) {
if (!returnUrl) throw new Error("FlatPay: returnUrl is required");
return this._signedPost("/v1/pay/request", {
amount,
currency,
description,
client_ref: clientRef,
return_url: returnUrl,
mobile,
email,
metadata,
});
}
/** Authoritative server-side status check. Returns the full transaction. */
inquire(id) {
return this._signedPost("/v1/pay/inquiry", { id });
}
/**
* Verify the signed return-redirect query.
* Pass the query params { id, status, ref_id, sign } AND the amount_rial you got
* from createPayment/inquire (the redirect itself doesn't carry the amount).
*/
verifyRedirect({ id, status, ref_id = "", sign }, amountRial) {
const message = `${id}.${status}.${ref_id}.${amountRial}`;
return !!sign && timingSafeEqualHex(hmac(this.secret, message), sign);
}
/**
* Verify a webhook. `rawBody` MUST be the exact bytes received (Buffer or string —
* do not re-stringify a parsed object, signatures won't match).
*/
verifyWebhook(rawBody, signatureHeader) {
const msg = Buffer.isBuffer(rawBody) ? rawBody : Buffer.from(String(rawBody), "utf8");
return !!signatureHeader && timingSafeEqualHex(hmac(this.secret, msg), signatureHeader);
}
}
module.exports = { FlatPay, hmac };
+37
View File
@@ -255,6 +255,41 @@ services:
retries: 5 retries: 5
start_period: 10s start_period: 10s
# ── Payment Broker (Go) — pay.flatrender.ir ─────────────────────────────────
# Standalone generic ZarinPal gateway. Other sites (meezi, bargevasat) and
# FlatRender register as client_apps and route payments through it, because
# ZarinPal only accepts callbacks on the single verified domain pay.flatrender.ir.
# Exposed on its OWN host port (mirror-nginx → pay.flatrender.ir → here);
# it does NOT sit behind the API gateway (clients auth with API key + HMAC).
payment-svc:
build:
context: ./services/payment
container_name: fr2-payment
restart: unless-stopped
ports:
- "${EDGE_BIND:-0.0.0.0}:${PAY_PORT:-8090}:8080"
environment:
DATABASE_URL: "postgres://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:5432/flatrender?search_path=payment,public"
JWT_SECRET: "${JWT_SECRET}"
PORT: "8080"
# Externally reachable base — ZarinPal callback + user redirect are built from it.
PUBLIC_BASE_URL: "${PAY_PUBLIC_URL:-http://localhost:8090}"
# Shared default ZarinPal merchant (a client_app may override per-site).
ZARINPAL_MERCHANT_ID: "${ZARINPAL_MERCHANT_ID:-}"
ZARINPAL_SANDBOX: "${ZARINPAL_SANDBOX:-true}"
# Unit ZarinPal expects in the amount field: "rial" (official v4) or "toman".
ZARINPAL_AMOUNT_UNIT: "${ZARINPAL_AMOUNT_UNIT:-rial}"
depends_on:
postgres:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/health || exit 1"]
interval: 15s
timeout: 5s
retries: 5
start_period: 10s
# ── API Gateway (Go) ──────────────────────────────────────────────────────── # ── API Gateway (Go) ────────────────────────────────────────────────────────
gateway: gateway:
@@ -318,6 +353,8 @@ services:
HOSTNAME: "0.0.0.0" HOSTNAME: "0.0.0.0"
# Server-side: Next route handlers reach the gateway over the internal network. # Server-side: Next route handlers reach the gateway over the internal network.
API_GATEWAY_URL: "http://gateway:8080" API_GATEWAY_URL: "http://gateway:8080"
# Admin proxy reaches the payment broker directly (not via the gateway).
PAYMENT_SVC_URL: "http://payment-svc:8080"
depends_on: depends_on:
gateway: gateway:
condition: service_healthy condition: service_healthy
+1
View File
@@ -387,6 +387,7 @@
"templates": "Templates", "templates": "Templates",
"media": "Media", "media": "Media",
"discounts": "Discounts", "discounts": "Discounts",
"payments": "Payments",
"siteSettings": "Settings", "siteSettings": "Settings",
"messaging": "Messaging", "messaging": "Messaging",
"marketing": "Marketing", "marketing": "Marketing",
+1
View File
@@ -387,6 +387,7 @@
"templates": "قالب‌ها", "templates": "قالب‌ها",
"media": "رسانه", "media": "رسانه",
"discounts": "تخفیف‌ها", "discounts": "تخفیف‌ها",
"payments": "درگاه پرداخت",
"siteSettings": "تنظیمات سایت", "siteSettings": "تنظیمات سایت",
"messaging": "پیام‌رسانی", "messaging": "پیام‌رسانی",
"marketing": "بازاریابی", "marketing": "بازاریابی",
+16
View File
@@ -0,0 +1,16 @@
# Build output
notification-svc
# Local env
.env
.env.*
# IDE / OS
.idea/
.DS_Store
Thumbs.db
# Docker files
Dockerfile
.dockerignore
docker-compose*.yml
+13
View File
@@ -0,0 +1,13 @@
FROM mirror.kargadan.ir/golang:1.25-alpine AS builder
ENV GOPROXY=https://mirror.kargadan.ir/repository/go-group/ GOSUMDB=off
WORKDIR /app
# Dependencies are vendored — build fully offline (proxy.golang.org is geo-blocked from some regions)
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -mod=vendor -ldflags="-s -w" -o payment-svc ./cmd/server
FROM mirror.soroushasadi.com/alpine:3.20
RUN apk add --no-cache ca-certificates tzdata
WORKDIR /app
COPY --from=builder /app/payment-svc .
EXPOSE 8080
CMD ["./payment-svc"]
+84
View File
@@ -0,0 +1,84 @@
package main
import (
"context"
"log"
"net/http"
"github.com/flatrender/payment-svc/internal/config"
"github.com/flatrender/payment-svc/internal/db"
"github.com/flatrender/payment-svc/internal/handlers"
"github.com/flatrender/payment-svc/internal/middleware"
"github.com/flatrender/payment-svc/internal/web"
"github.com/flatrender/payment-svc/internal/zarinpal"
"github.com/gin-gonic/gin"
"github.com/jackc/pgx/v5/pgxpool"
)
func main() {
cfg := config.Load()
pool, err := pgxpool.New(context.Background(), cfg.DatabaseURL)
if err != nil {
log.Fatalf("connect db: %v", err)
}
defer pool.Close()
if err := pool.Ping(context.Background()); err != nil {
log.Fatalf("ping db: %v", err)
}
store := db.NewStore(pool)
zp := zarinpal.New()
disp := handlers.NewDispatcher(store)
// Background webhook delivery loop.
go disp.Run(context.Background())
payH := handlers.NewPayHandler(store, zp, disp, cfg)
adminH := handlers.NewAdminHandler(store)
r := gin.Default()
r.SetTrustedProxies(nil) //nolint — behind mirror-nginx; we read X-Forwarded-* only informally
health := func(c *gin.Context) {
if err := store.Ping(c.Request.Context()); err != nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"status": "down", "error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"status": "ok", "service": "payment"})
}
r.GET("/health", health)
r.GET("/healthz", health)
// Public pages + ZarinPal callback (the single verified domain).
r.GET("/", web.Landing)
r.GET("/result", web.Result)
r.GET("/callback/zarinpal", payH.Callback)
v1 := r.Group("/v1")
// ── Client API (API key + HMAC signature) ────────────────────────────────
pay := v1.Group("/pay", middleware.ClientAuth(store))
{
pay.POST("/request", payH.Request)
pay.POST("/inquiry", payH.Inquiry)
}
// ── Admin API (FlatRender admin JWT) ─────────────────────────────────────
admin := v1.Group("/admin", middleware.JWTAuth(cfg.JWTSecret), middleware.RequireAdmin())
{
admin.GET("/clients", adminH.List)
admin.POST("/clients", adminH.Create)
admin.GET("/clients/:id", adminH.Get)
admin.PUT("/clients/:id", adminH.Update)
admin.DELETE("/clients/:id", adminH.Delete)
admin.POST("/clients/:id/rotate-secret", adminH.RotateSecret)
admin.GET("/transactions", adminH.ListTransactions)
}
log.Printf("payment-svc listening on :%s (sandbox=%v, unit=%s, base=%s)",
cfg.Port, cfg.ZarinPalSandbox, cfg.ZarinPalAmountUnit, cfg.PublicBaseURL)
if err := r.Run(":" + cfg.Port); err != nil {
log.Fatalf("server: %v", err)
}
}
+45
View File
@@ -0,0 +1,45 @@
module github.com/flatrender/payment-svc
go 1.25
require (
github.com/gin-gonic/gin v1.10.0
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.6.0
github.com/jackc/pgx/v5 v5.7.1
)
require (
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/rogpeppe/go-internal v1.15.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.27.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/text v0.18.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+111
View File
@@ -0,0 +1,111 @@
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs=
github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.15.0 h1:D0RCU5rMAp+SpgkiNdrjfJ+LX4J1M32V2NeCY7EJ6hc=
github.com/rogpeppe/go-internal v1.15.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
@@ -0,0 +1,54 @@
package config
import (
"os"
"strings"
)
// Config is the payment-broker runtime configuration, all from env.
type Config struct {
DatabaseURL string
JWTSecret string // verifies FlatRender admin JWTs for /v1/admin/* endpoints
Port string
// PublicBaseURL is the broker's externally reachable base (e.g. https://pay.flatrender.ir).
// ZarinPal callbacks and the user redirect target are built from it.
PublicBaseURL string
// ── ZarinPal (shared default merchant) ──────────────────────────────────
// A client_app may override MerchantId/Sandbox; otherwise these defaults apply.
ZarinPalMerchantID string
ZarinPalSandbox bool
// AmountUnit is the unit ZarinPal expects in the amount field: "rial" (official
// v4) or "toman". The broker always stores Rial canonically and converts here.
ZarinPalAmountUnit string
}
func getEnv(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}
func Load() Config {
base := strings.TrimRight(getEnv("PUBLIC_BASE_URL", "http://localhost:8080"), "/")
unit := strings.ToLower(getEnv("ZARINPAL_AMOUNT_UNIT", "rial"))
if unit != "toman" {
unit = "rial"
}
return Config{
DatabaseURL: getEnv("DATABASE_URL", "postgres://postgres:postgres@localhost:5432/flatrender?search_path=payment,public"),
JWTSecret: getEnv("JWT_SECRET", "change-me"),
Port: getEnv("PORT", "8080"),
PublicBaseURL: base,
ZarinPalMerchantID: getEnv("ZARINPAL_MERCHANT_ID", ""),
ZarinPalSandbox: getEnv("ZARINPAL_SANDBOX", "true") == "true",
ZarinPalAmountUnit: unit,
}
}
// CallbackURL is the single ZarinPal-verified callback endpoint on this broker.
func (c Config) CallbackURL() string {
return c.PublicBaseURL + "/callback/zarinpal"
}
+332
View File
@@ -0,0 +1,332 @@
package db
import (
"context"
"errors"
"fmt"
"time"
"github.com/flatrender/payment-svc/internal/models"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
var ErrNotFound = errors.New("not found")
type Store struct {
pool *pgxpool.Pool
}
func NewStore(pool *pgxpool.Pool) *Store { return &Store{pool: pool} }
func (s *Store) Ping(ctx context.Context) error { return s.pool.Ping(ctx) }
// ── Client apps ───────────────────────────────────────────────────────────────
const clientCols = `id, tenant_id, name, slug, api_key, secret,
zarinpal_merchant_id, zarinpal_sandbox, allowed_return_origins, webhook_url,
is_active, created_at, updated_at`
func scanClient(row pgx.Row, withSecret bool) (*models.ClientApp, error) {
var c models.ClientApp
var secret string
if err := row.Scan(
&c.ID, &c.TenantID, &c.Name, &c.Slug, &c.APIKey, &secret,
&c.ZarinPalMerchantID, &c.ZarinPalSandbox, &c.AllowedReturnOrigins, &c.WebhookURL,
&c.IsActive, &c.CreatedAt, &c.UpdatedAt,
); err != nil {
return nil, err
}
if withSecret {
c.Secret = secret
}
return &c, nil
}
func (s *Store) CreateClientApp(ctx context.Context, c *models.ClientApp) (*models.ClientApp, error) {
row := s.pool.QueryRow(ctx, `
INSERT INTO payment.client_apps
(tenant_id, name, slug, api_key, secret, zarinpal_merchant_id, zarinpal_sandbox,
allowed_return_origins, webhook_url, is_active)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)
RETURNING `+clientCols,
c.TenantID, c.Name, c.Slug, c.APIKey, c.Secret, c.ZarinPalMerchantID, c.ZarinPalSandbox,
c.AllowedReturnOrigins, c.WebhookURL, c.IsActive)
return scanClient(row, true)
}
func (s *Store) ListClientApps(ctx context.Context) ([]*models.ClientApp, error) {
rows, err := s.pool.Query(ctx, `SELECT `+clientCols+` FROM payment.client_apps ORDER BY created_at DESC`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []*models.ClientApp
for rows.Next() {
c, err := scanClient(rows, false)
if err != nil {
return nil, err
}
out = append(out, c)
}
return out, rows.Err()
}
func (s *Store) GetClientApp(ctx context.Context, id uuid.UUID) (*models.ClientApp, error) {
row := s.pool.QueryRow(ctx, `SELECT `+clientCols+` FROM payment.client_apps WHERE id = $1`, id)
c, err := scanClient(row, false)
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrNotFound
}
return c, err
}
// GetClientByAPIKey returns the client incl. its secret (for auth + signing).
func (s *Store) GetClientByAPIKey(ctx context.Context, apiKey string) (*models.ClientApp, error) {
row := s.pool.QueryRow(ctx, `SELECT `+clientCols+` FROM payment.client_apps WHERE api_key = $1 AND is_active = TRUE`, apiKey)
c, err := scanClient(row, true)
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrNotFound
}
return c, err
}
func (s *Store) UpdateClientApp(ctx context.Context, id uuid.UUID, c *models.ClientApp) (*models.ClientApp, error) {
row := s.pool.QueryRow(ctx, `
UPDATE payment.client_apps SET
name = $2, zarinpal_merchant_id = $3, zarinpal_sandbox = $4,
allowed_return_origins = $5, webhook_url = $6, is_active = $7
WHERE id = $1
RETURNING `+clientCols,
id, c.Name, c.ZarinPalMerchantID, c.ZarinPalSandbox,
c.AllowedReturnOrigins, c.WebhookURL, c.IsActive)
res, err := scanClient(row, false)
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrNotFound
}
return res, err
}
func (s *Store) RotateSecret(ctx context.Context, id uuid.UUID, newSecret string) (*models.ClientApp, error) {
row := s.pool.QueryRow(ctx, `UPDATE payment.client_apps SET secret = $2 WHERE id = $1 RETURNING `+clientCols, id, newSecret)
c, err := scanClient(row, true)
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrNotFound
}
return c, err
}
func (s *Store) DeleteClientApp(ctx context.Context, id uuid.UUID) error {
ct, err := s.pool.Exec(ctx, `DELETE FROM payment.client_apps WHERE id = $1`, id)
if err != nil {
return err
}
if ct.RowsAffected() == 0 {
return ErrNotFound
}
return nil
}
// ── Transactions ──────────────────────────────────────────────────────────────
const txnCols = `id, client_app_id, status, gateway, amount_rial, currency, description,
client_ref, return_url, metadata, payer_mobile, payer_email,
authority, ref_id, card_pan, fee_rial, gateway_response, failure_reason,
paid_at, failed_at, expires_at, created_at, updated_at`
func scanTxn(row pgx.Row) (*models.Transaction, error) {
var t models.Transaction
var meta, gwResp []byte
if err := row.Scan(
&t.ID, &t.ClientAppID, &t.Status, &t.Gateway, &t.AmountRial, &t.Currency, &t.Description,
&t.ClientRef, &t.ReturnURL, &meta, &t.PayerMobile, &t.PayerEmail,
&t.Authority, &t.RefID, &t.CardPan, &t.FeeRial, &gwResp, &t.FailureReason,
&t.PaidAt, &t.FailedAt, &t.ExpiresAt, &t.CreatedAt, &t.UpdatedAt,
); err != nil {
return nil, err
}
t.Metadata = meta
t.GatewayResponse = gwResp
return &t, nil
}
func (s *Store) CreateTransaction(ctx context.Context, t *models.Transaction) (*models.Transaction, error) {
var meta any
if len(t.Metadata) > 0 {
meta = []byte(t.Metadata)
}
row := s.pool.QueryRow(ctx, `
INSERT INTO payment.transactions
(client_app_id, status, gateway, amount_rial, currency, description,
client_ref, return_url, metadata, payer_mobile, payer_email, expires_at)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12)
RETURNING `+txnCols,
t.ClientAppID, t.Status, t.Gateway, t.AmountRial, t.Currency, t.Description,
t.ClientRef, t.ReturnURL, meta, t.PayerMobile, t.PayerEmail, t.ExpiresAt)
return scanTxn(row)
}
func (s *Store) SetAuthority(ctx context.Context, id uuid.UUID, authority string) error {
_, err := s.pool.Exec(ctx, `UPDATE payment.transactions SET authority = $2, status = $3 WHERE id = $1`,
id, authority, models.StatusPending)
return err
}
func (s *Store) GetTransaction(ctx context.Context, id uuid.UUID) (*models.Transaction, error) {
row := s.pool.QueryRow(ctx, `SELECT `+txnCols+` FROM payment.transactions WHERE id = $1`, id)
t, err := scanTxn(row)
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrNotFound
}
return t, err
}
func (s *Store) GetTransactionByAuthority(ctx context.Context, authority string) (*models.Transaction, error) {
row := s.pool.QueryRow(ctx, `SELECT `+txnCols+` FROM payment.transactions WHERE authority = $1`, authority)
t, err := scanTxn(row)
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrNotFound
}
return t, err
}
// MarkPaid transitions a pending txn → Paid (idempotent: only if not already terminal).
func (s *Store) MarkPaid(ctx context.Context, id uuid.UUID, refID, cardPan string, fee int64, gwResp []byte) (*models.Transaction, error) {
row := s.pool.QueryRow(ctx, `
UPDATE payment.transactions
SET status = $2, ref_id = $3, card_pan = NULLIF($4,''), fee_rial = $5,
gateway_response = $6, paid_at = NOW()
WHERE id = $1 AND status IN ($7,$8)
RETURNING `+txnCols,
id, models.StatusPaid, refID, cardPan, fee, gwResp, models.StatusPending, models.StatusCreated)
t, err := scanTxn(row)
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrNotFound // already terminal or missing
}
return t, err
}
func (s *Store) MarkFailed(ctx context.Context, id uuid.UUID, reason string, gwResp []byte) (*models.Transaction, error) {
row := s.pool.QueryRow(ctx, `
UPDATE payment.transactions
SET status = $2, failure_reason = $3, gateway_response = $4, failed_at = NOW()
WHERE id = $1 AND status IN ($5,$6)
RETURNING `+txnCols,
id, models.StatusFailed, reason, gwResp, models.StatusPending, models.StatusCreated)
t, err := scanTxn(row)
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrNotFound
}
return t, err
}
func (s *Store) ListTransactions(ctx context.Context, clientID *uuid.UUID, status string, page, pageSize int) ([]*models.Transaction, int64, error) {
where := "TRUE"
args := []any{}
i := 1
if clientID != nil {
where += fmt.Sprintf(" AND t.client_app_id = $%d", i)
args = append(args, *clientID)
i++
}
if status != "" {
where += fmt.Sprintf(" AND t.status = $%d", i)
args = append(args, status)
i++
}
var total int64
_ = s.pool.QueryRow(ctx, "SELECT COUNT(*) FROM payment.transactions t WHERE "+where, args...).Scan(&total)
args = append(args, pageSize, (page-1)*pageSize)
q := fmt.Sprintf(`
SELECT t.id, t.client_app_id, t.status, t.gateway, t.amount_rial, t.currency, t.description,
t.client_ref, t.return_url, t.metadata, t.payer_mobile, t.payer_email,
t.authority, t.ref_id, t.card_pan, t.fee_rial, t.gateway_response, t.failure_reason,
t.paid_at, t.failed_at, t.expires_at, t.created_at, t.updated_at, c.slug
FROM payment.transactions t
JOIN payment.client_apps c ON c.id = t.client_app_id
WHERE %s ORDER BY t.created_at DESC LIMIT $%d OFFSET $%d`, where, i, i+1)
rows, err := s.pool.Query(ctx, q, args...)
if err != nil {
return nil, 0, err
}
defer rows.Close()
var out []*models.Transaction
for rows.Next() {
var t models.Transaction
var meta, gwResp []byte
if err := rows.Scan(
&t.ID, &t.ClientAppID, &t.Status, &t.Gateway, &t.AmountRial, &t.Currency, &t.Description,
&t.ClientRef, &t.ReturnURL, &meta, &t.PayerMobile, &t.PayerEmail,
&t.Authority, &t.RefID, &t.CardPan, &t.FeeRial, &gwResp, &t.FailureReason,
&t.PaidAt, &t.FailedAt, &t.ExpiresAt, &t.CreatedAt, &t.UpdatedAt, &t.ClientSlug,
); err != nil {
return nil, 0, err
}
t.Metadata = meta
t.GatewayResponse = gwResp
out = append(out, &t)
}
return out, total, rows.Err()
}
// ── Webhook deliveries ────────────────────────────────────────────────────────
type WebhookDelivery struct {
ID uuid.UUID
TransactionID uuid.UUID
URL string
Payload []byte
Signature string
Attempts int
}
func (s *Store) EnqueueWebhook(ctx context.Context, txnID uuid.UUID, url string, payload []byte, signature string) (uuid.UUID, error) {
var id uuid.UUID
err := s.pool.QueryRow(ctx, `
INSERT INTO payment.webhook_deliveries (transaction_id, url, payload, signature, next_attempt_at)
VALUES ($1,$2,$3,$4, NOW()) RETURNING id`,
txnID, url, payload, signature).Scan(&id)
return id, err
}
// ClaimDueWebhooks returns undelivered webhooks whose next_attempt_at has passed.
func (s *Store) ClaimDueWebhooks(ctx context.Context, limit int) ([]*WebhookDelivery, error) {
rows, err := s.pool.Query(ctx, `
SELECT id, transaction_id, url, payload, signature, attempts
FROM payment.webhook_deliveries
WHERE delivered = FALSE AND attempts < 8 AND next_attempt_at <= NOW()
ORDER BY next_attempt_at ASC LIMIT $1`, limit)
if err != nil {
return nil, err
}
defer rows.Close()
var out []*WebhookDelivery
for rows.Next() {
var w WebhookDelivery
if err := rows.Scan(&w.ID, &w.TransactionID, &w.URL, &w.Payload, &w.Signature, &w.Attempts); err != nil {
return nil, err
}
out = append(out, &w)
}
return out, rows.Err()
}
func (s *Store) MarkWebhookDelivered(ctx context.Context, id uuid.UUID, statusCode int) error {
_, err := s.pool.Exec(ctx, `
UPDATE payment.webhook_deliveries
SET delivered = TRUE, last_status = $2, attempts = attempts + 1
WHERE id = $1`, id, statusCode)
return err
}
func (s *Store) MarkWebhookFailed(ctx context.Context, id uuid.UUID, statusCode int, errMsg string, backoff time.Duration) error {
_, err := s.pool.Exec(ctx, `
UPDATE payment.webhook_deliveries
SET attempts = attempts + 1, last_status = $2, last_error = $3,
next_attempt_at = NOW() + $4::interval
WHERE id = $1`, id, statusCode, errMsg, fmt.Sprintf("%d seconds", int(backoff.Seconds())))
return err
}
+204
View File
@@ -0,0 +1,204 @@
package handlers
import (
"crypto/rand"
"encoding/hex"
"net/http"
"regexp"
"strconv"
"strings"
"github.com/flatrender/payment-svc/internal/db"
"github.com/flatrender/payment-svc/internal/models"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type AdminHandler struct {
store *db.Store
}
func NewAdminHandler(store *db.Store) *AdminHandler { return &AdminHandler{store: store} }
var slugRe = regexp.MustCompile(`[^a-z0-9]+`)
func slugify(s string) string {
s = strings.ToLower(strings.TrimSpace(s))
s = slugRe.ReplaceAllString(s, "-")
return strings.Trim(s, "-")
}
func randToken(prefix string) string {
b := make([]byte, 24)
_, _ = rand.Read(b)
return prefix + hex.EncodeToString(b)
}
type clientInput struct {
Name string `json:"name"`
Slug string `json:"slug"`
ZarinPalMerchantID *string `json:"zarinpal_merchant_id"`
ZarinPalSandbox *bool `json:"zarinpal_sandbox"`
AllowedReturnOrigins []string `json:"allowed_return_origins"`
WebhookURL *string `json:"webhook_url"`
IsActive *bool `json:"is_active"`
}
func (h *AdminHandler) List(c *gin.Context) {
clients, err := h.store.ListClientApps(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, models.APIError{Code: "db_error", Message: err.Error()})
return
}
if clients == nil {
clients = []*models.ClientApp{}
}
c.JSON(http.StatusOK, gin.H{"data": clients})
}
func (h *AdminHandler) Create(c *gin.Context) {
var in clientInput
if err := c.ShouldBindJSON(&in); err != nil || strings.TrimSpace(in.Name) == "" {
c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "name is required"})
return
}
slug := slugify(in.Slug)
if slug == "" {
slug = slugify(in.Name)
}
active := true
if in.IsActive != nil {
active = *in.IsActive
}
if in.AllowedReturnOrigins == nil {
in.AllowedReturnOrigins = []string{}
}
client := &models.ClientApp{
Name: in.Name,
Slug: slug,
APIKey: randToken("pk_"),
Secret: randToken("sk_"),
ZarinPalMerchantID: in.ZarinPalMerchantID,
ZarinPalSandbox: in.ZarinPalSandbox,
AllowedReturnOrigins: in.AllowedReturnOrigins,
WebhookURL: in.WebhookURL,
IsActive: active,
}
created, err := h.store.CreateClientApp(c.Request.Context(), client)
if err != nil {
if strings.Contains(err.Error(), "duplicate") || strings.Contains(err.Error(), "unique") {
c.JSON(http.StatusConflict, models.APIError{Code: "conflict", Message: "slug already exists"})
return
}
c.JSON(http.StatusInternalServerError, models.APIError{Code: "db_error", Message: err.Error()})
return
}
// created includes the plaintext secret exactly once.
c.JSON(http.StatusCreated, created)
}
func (h *AdminHandler) Get(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "invalid id"})
return
}
client, err := h.store.GetClientApp(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusNotFound, models.APIError{Code: "not_found", Message: "client not found"})
return
}
c.JSON(http.StatusOK, client)
}
func (h *AdminHandler) Update(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "invalid id"})
return
}
existing, err := h.store.GetClientApp(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusNotFound, models.APIError{Code: "not_found", Message: "client not found"})
return
}
var in clientInput
if err := c.ShouldBindJSON(&in); err != nil {
c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "invalid body"})
return
}
if in.Name != "" {
existing.Name = in.Name
}
existing.ZarinPalMerchantID = in.ZarinPalMerchantID
existing.ZarinPalSandbox = in.ZarinPalSandbox
if in.AllowedReturnOrigins != nil {
existing.AllowedReturnOrigins = in.AllowedReturnOrigins
}
existing.WebhookURL = in.WebhookURL
if in.IsActive != nil {
existing.IsActive = *in.IsActive
}
updated, err := h.store.UpdateClientApp(c.Request.Context(), id, existing)
if err != nil {
c.JSON(http.StatusInternalServerError, models.APIError{Code: "db_error", Message: err.Error()})
return
}
c.JSON(http.StatusOK, updated)
}
func (h *AdminHandler) RotateSecret(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "invalid id"})
return
}
updated, err := h.store.RotateSecret(c.Request.Context(), id, randToken("sk_"))
if err != nil {
c.JSON(http.StatusNotFound, models.APIError{Code: "not_found", Message: "client not found"})
return
}
c.JSON(http.StatusOK, updated) // includes the new secret once
}
func (h *AdminHandler) Delete(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "invalid id"})
return
}
if err := h.store.DeleteClientApp(c.Request.Context(), id); err != nil {
c.JSON(http.StatusNotFound, models.APIError{Code: "not_found", Message: "client not found"})
return
}
c.Status(http.StatusNoContent)
}
func (h *AdminHandler) ListTransactions(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 200 {
pageSize = 20
}
var clientID *uuid.UUID
if cidStr := c.Query("client_id"); cidStr != "" {
if cid, err := uuid.Parse(cidStr); err == nil {
clientID = &cid
}
}
txns, total, err := h.store.ListTransactions(c.Request.Context(), clientID, c.Query("status"), page, pageSize)
if err != nil {
c.JSON(http.StatusInternalServerError, models.APIError{Code: "db_error", Message: err.Error()})
return
}
if txns == nil {
txns = []*models.Transaction{}
}
c.JSON(http.StatusOK, gin.H{
"data": txns,
"meta": gin.H{"page": page, "page_size": pageSize, "total": total, "has_more": int64(page*pageSize) < total},
})
}
+304
View File
@@ -0,0 +1,304 @@
package handlers
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"github.com/flatrender/payment-svc/internal/config"
"github.com/flatrender/payment-svc/internal/db"
"github.com/flatrender/payment-svc/internal/middleware"
"github.com/flatrender/payment-svc/internal/models"
"github.com/flatrender/payment-svc/internal/signing"
"github.com/flatrender/payment-svc/internal/zarinpal"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
const minAmountRial = 1000 // ZarinPal minimum (= 100 Toman)
type PayHandler struct {
store *db.Store
zp *zarinpal.Client
disp *Dispatcher
cfg config.Config
}
func NewPayHandler(store *db.Store, zp *zarinpal.Client, disp *Dispatcher, cfg config.Config) *PayHandler {
return &PayHandler{store: store, zp: zp, disp: disp, cfg: cfg}
}
// merchantFor resolves the ZarinPal merchant + sandbox flag for a client
// (per-client override falls back to the broker default).
func (h *PayHandler) merchantFor(client *models.ClientApp) (string, bool) {
merchant := h.cfg.ZarinPalMerchantID
if client.ZarinPalMerchantID != nil && *client.ZarinPalMerchantID != "" {
merchant = *client.ZarinPalMerchantID
}
sandbox := h.cfg.ZarinPalSandbox
if client.ZarinPalSandbox != nil {
sandbox = *client.ZarinPalSandbox
}
return merchant, sandbox
}
// zpAmount converts canonical Rial to the unit ZarinPal expects for this broker.
func (h *PayHandler) zpAmount(amountRial int64) int64 {
if h.cfg.ZarinPalAmountUnit == "toman" {
return amountRial / 10
}
return amountRial
}
// toRial normalizes the client-supplied amount + currency to canonical Rial.
func toRial(amount int64, currency string) int64 {
switch strings.ToUpper(strings.TrimSpace(currency)) {
case "IRT", "TOMAN", "TMN", "TOMANS":
return amount * 10
default: // IRR / RIAL / empty
return amount
}
}
// POST /v1/pay/request (client-authed: X-Api-Key + X-Signature)
func (h *PayHandler) Request(c *gin.Context) {
client := middleware.GetClient(c)
rawAny, _ := c.Get(middleware.CtxRawBody)
raw, _ := rawAny.([]byte)
var req models.PayRequest
if err := json.Unmarshal(raw, &req); err != nil {
c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "invalid JSON body"})
return
}
if req.ReturnURL == "" {
c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "return_url is required"})
return
}
if !originAllowed(client, req.ReturnURL) {
c.JSON(http.StatusBadRequest, models.APIError{Code: "return_url_not_allowed", Message: "return_url origin is not in the client's allowed list"})
return
}
amountRial := toRial(req.Amount, req.Currency)
if amountRial < minAmountRial {
c.JSON(http.StatusBadRequest, models.APIError{Code: "amount_too_low", Message: fmt.Sprintf("amount must be at least %d Rial (100 Toman)", minAmountRial)})
return
}
desc := req.Description
if desc == "" {
desc = "پرداخت آنلاین"
}
exp := time.Now().Add(30 * time.Minute)
txn := &models.Transaction{
ClientAppID: client.ID,
Status: models.StatusCreated,
Gateway: "ZarinPal",
AmountRial: amountRial,
Currency: "IRR",
Description: &desc,
ReturnURL: req.ReturnURL,
Metadata: req.Metadata,
ExpiresAt: &exp,
}
if req.ClientRef != "" {
txn.ClientRef = &req.ClientRef
}
if req.Mobile != "" {
txn.PayerMobile = &req.Mobile
}
if req.Email != "" {
txn.PayerEmail = &req.Email
}
created, err := h.store.CreateTransaction(c.Request.Context(), txn)
if err != nil {
c.JSON(http.StatusInternalServerError, models.APIError{Code: "db_error", Message: "could not create transaction"})
return
}
merchant, sandbox := h.merchantFor(client)
if merchant == "" {
c.JSON(http.StatusServiceUnavailable, models.APIError{Code: "gateway_unconfigured", Message: "ZarinPal merchant id is not configured"})
return
}
res, err := h.zp.Request(c.Request.Context(), sandbox, merchant, h.zpAmount(amountRial),
h.cfg.CallbackURL(), desc, map[string]string{"order_id": created.ID.String()})
if err != nil {
_, _ = h.store.MarkFailed(c.Request.Context(), created.ID, err.Error(), nil)
c.JSON(http.StatusBadGateway, models.APIError{Code: "gateway_error", Message: err.Error()})
return
}
if err := h.store.SetAuthority(c.Request.Context(), created.ID, res.Authority); err != nil {
c.JSON(http.StatusInternalServerError, models.APIError{Code: "db_error", Message: "could not persist authority"})
return
}
c.JSON(http.StatusOK, models.PayResponse{
ID: created.ID,
Status: models.StatusPending,
PaymentURL: res.StartPay,
Authority: res.Authority,
AmountRial: amountRial,
})
}
// GET /callback/zarinpal?Authority=..&Status=OK|NOK (PUBLIC — ZarinPal hits this)
func (h *PayHandler) Callback(c *gin.Context) {
authority := c.Query("Authority")
status := c.Query("Status")
if authority == "" {
c.String(http.StatusBadRequest, "missing Authority")
return
}
txn, err := h.store.GetTransactionByAuthority(c.Request.Context(), authority)
if err != nil {
c.String(http.StatusNotFound, "transaction not found")
return
}
client, err := h.store.GetClientApp(c.Request.Context(), txn.ClientAppID)
if err != nil {
c.String(http.StatusInternalServerError, "client not found")
return
}
// Re-fetch with secret for signing the redirect/webhook.
client, _ = h.store.GetClientByAPIKey(c.Request.Context(), client.APIKey)
now := time.Now().Unix()
// User cancelled / bank declined before verify.
if status != "OK" {
failed, _ := h.store.MarkFailed(c.Request.Context(), txn.ID, "user cancelled or status NOK", nil)
if failed != nil {
h.disp.Enqueue(c.Request.Context(), client, failed, now)
h.redirectBack(c, client, failed)
} else {
h.redirectBack(c, client, txn)
}
return
}
merchant, sandbox := h.merchantFor(client)
vr, err := h.zp.Verify(c.Request.Context(), sandbox, merchant, h.zpAmount(txn.AmountRial), authority)
if err != nil {
failed, _ := h.store.MarkFailed(c.Request.Context(), txn.ID, "verify error: "+err.Error(), rawJSON(vr))
final := pick(failed, txn)
h.disp.Enqueue(c.Request.Context(), client, final, now)
h.redirectBack(c, client, final)
return
}
if vr.Code == 100 || vr.Code == 101 {
paid, perr := h.store.MarkPaid(c.Request.Context(), txn.ID, vr.RefID, vr.CardPan, vr.Fee, rawJSON(vr))
if perr != nil {
// Already terminal (duplicate callback) — just bounce the user back with current state.
cur, _ := h.store.GetTransaction(c.Request.Context(), txn.ID)
h.redirectBack(c, client, pick(cur, txn))
return
}
h.disp.Enqueue(c.Request.Context(), client, paid, now)
h.redirectBack(c, client, paid)
return
}
failed, _ := h.store.MarkFailed(c.Request.Context(), txn.ID, fmt.Sprintf("zarinpal verify code %d", vr.Code), rawJSON(vr))
final := pick(failed, txn)
h.disp.Enqueue(c.Request.Context(), client, final, now)
h.redirectBack(c, client, final)
}
// redirectBack bounces the user to the client's return_url with a signed result.
// Signature canonical string: "{id}.{status}.{ref_id}.{amount_rial}".
func (h *PayHandler) redirectBack(c *gin.Context, client *models.ClientApp, t *models.Transaction) {
ref := ""
if t.RefID != nil {
ref = *t.RefID
}
canonical := fmt.Sprintf("%s.%s.%s.%d", t.ID, t.Status, ref, t.AmountRial)
sign := signing.Sign(client.Secret, []byte(canonical))
u, err := url.Parse(t.ReturnURL)
if err != nil {
c.String(http.StatusOK, "payment %s (ref %s)", t.Status, ref)
return
}
q := u.Query()
q.Set("status", t.Status)
q.Set("id", t.ID.String())
q.Set("ref_id", ref)
q.Set("sign", sign)
u.RawQuery = q.Encode()
c.Redirect(http.StatusFound, u.String())
}
// POST /v1/pay/inquiry (client-authed) body: { "id": "<txn uuid>" }
// Authoritative server-side status check — clients should NOT trust the redirect alone.
func (h *PayHandler) Inquiry(c *gin.Context) {
client := middleware.GetClient(c)
rawAny, _ := c.Get(middleware.CtxRawBody)
raw, _ := rawAny.([]byte)
var body struct {
ID string `json:"id"`
ClientRef string `json:"client_ref"`
}
_ = json.Unmarshal(raw, &body)
var txn *models.Transaction
var err error
if body.ID != "" {
id, perr := uuid.Parse(body.ID)
if perr != nil {
c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "invalid id"})
return
}
txn, err = h.store.GetTransaction(c.Request.Context(), id)
} else {
c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "id is required"})
return
}
if err != nil || txn.ClientAppID != client.ID {
c.JSON(http.StatusNotFound, models.APIError{Code: "not_found", Message: "transaction not found"})
return
}
c.JSON(http.StatusOK, txn)
}
// ── helpers ──────────────────────────────────────────────────────────────────
func originAllowed(client *models.ClientApp, returnURL string) bool {
if len(client.AllowedReturnOrigins) == 0 {
return true // no allow-list configured → permissive
}
u, err := url.Parse(returnURL)
if err != nil || u.Host == "" {
return false
}
origin := strings.ToLower(u.Scheme + "://" + u.Host)
for _, o := range client.AllowedReturnOrigins {
if strings.ToLower(strings.TrimRight(o, "/")) == origin {
return true
}
}
return false
}
func rawJSON(vr *zarinpal.VerifyResult) []byte {
if vr == nil {
return nil
}
return vr.Raw
}
func pick(primary, fallback *models.Transaction) *models.Transaction {
if primary != nil {
return primary
}
return fallback
}
@@ -0,0 +1,123 @@
package handlers
import (
"bytes"
"context"
"encoding/json"
"log"
"net/http"
"time"
"github.com/flatrender/payment-svc/internal/db"
"github.com/flatrender/payment-svc/internal/models"
"github.com/flatrender/payment-svc/internal/signing"
)
// Dispatcher delivers signed webhooks to client sites with retry/backoff.
type Dispatcher struct {
store *db.Store
http *http.Client
}
func NewDispatcher(store *db.Store) *Dispatcher {
return &Dispatcher{store: store, http: &http.Client{Timeout: 15 * time.Second}}
}
// Enqueue builds the signed payload for a finished transaction and queues delivery.
// No-op if the client has no webhook_url configured.
func (d *Dispatcher) Enqueue(ctx context.Context, client *models.ClientApp, t *models.Transaction, nowUnix int64) {
if client.WebhookURL == nil || *client.WebhookURL == "" {
return
}
event := "payment.failed"
if t.Status == models.StatusPaid {
event = "payment.paid"
}
payload := models.WebhookPayload{
Event: event,
ID: t.ID,
Status: t.Status,
AmountRial: t.AmountRial,
Currency: t.Currency,
Metadata: t.Metadata,
CreatedAtTs: nowUnix,
PaidAt: t.PaidAt,
}
if t.ClientRef != nil {
payload.ClientRef = *t.ClientRef
}
if t.RefID != nil {
payload.RefID = *t.RefID
}
if t.Authority != nil {
payload.Authority = *t.Authority
}
if t.CardPan != nil {
payload.CardPan = *t.CardPan
}
body, _ := json.Marshal(payload)
sig := signing.Sign(client.Secret, body)
if _, err := d.store.EnqueueWebhook(ctx, t.ID, *client.WebhookURL, body, sig); err != nil {
log.Printf("webhook enqueue failed for txn %s: %v", t.ID, err)
}
}
// Run starts the delivery loop until ctx is cancelled.
func (d *Dispatcher) Run(ctx context.Context) {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
d.tick(ctx)
}
}
}
func (d *Dispatcher) tick(ctx context.Context) {
due, err := d.store.ClaimDueWebhooks(ctx, 20)
if err != nil {
log.Printf("webhook claim failed: %v", err)
return
}
for _, w := range due {
d.deliver(ctx, w)
}
}
func (d *Dispatcher) deliver(ctx context.Context, w *db.WebhookDelivery) {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, w.URL, bytes.NewReader(w.Payload))
if err != nil {
_ = d.store.MarkWebhookFailed(ctx, w.ID, 0, err.Error(), backoff(w.Attempts))
return
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-FlatPay-Signature", w.Signature)
req.Header.Set("X-FlatPay-Event", "payment")
resp, err := d.http.Do(req)
if err != nil {
_ = d.store.MarkWebhookFailed(ctx, w.ID, 0, err.Error(), backoff(w.Attempts))
return
}
defer resp.Body.Close()
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
_ = d.store.MarkWebhookDelivered(ctx, w.ID, resp.StatusCode)
return
}
_ = d.store.MarkWebhookFailed(ctx, w.ID, resp.StatusCode, "non-2xx response", backoff(w.Attempts))
}
// backoff: 30s, 1m, 2m, 4m, 8m, ... capped at 1h.
func backoff(attempts int) time.Duration {
d := 30 * time.Second
for i := 0; i < attempts; i++ {
d *= 2
if d >= time.Hour {
return time.Hour
}
}
return d
}
@@ -0,0 +1,102 @@
package middleware
import (
"bytes"
"io"
"net/http"
"strings"
"github.com/flatrender/payment-svc/internal/db"
"github.com/flatrender/payment-svc/internal/models"
"github.com/flatrender/payment-svc/internal/signing"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
)
const (
CtxIsAdmin = "is_admin"
CtxClient = "client_app"
CtxRawBody = "raw_body"
)
// JWTAuth validates a FlatRender identity JWT (same secret) for admin endpoints.
func JWTAuth(secret string) gin.HandlerFunc {
return func(c *gin.Context) {
hdr := c.GetHeader("Authorization")
if !strings.HasPrefix(hdr, "Bearer ") {
c.AbortWithStatusJSON(http.StatusUnauthorized, models.APIError{Code: "unauthorized", Message: "missing bearer token"})
return
}
token, err := jwt.Parse(hdr[7:], func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, jwt.ErrSignatureInvalid
}
return []byte(secret), nil
})
if err != nil || !token.Valid {
c.AbortWithStatusJSON(http.StatusUnauthorized, models.APIError{Code: "unauthorized", Message: "invalid token"})
return
}
claims, _ := token.Claims.(jwt.MapClaims)
isAdmin := false
switch v := claims["is_admin"].(type) {
case bool:
isAdmin = v
case string:
isAdmin = v == "true"
}
c.Set(CtxIsAdmin, isAdmin)
c.Next()
}
}
func RequireAdmin() gin.HandlerFunc {
return func(c *gin.Context) {
v, _ := c.Get(CtxIsAdmin)
if b, _ := v.(bool); !b {
c.AbortWithStatusJSON(http.StatusForbidden, models.APIError{Code: "forbidden", Message: "admin required"})
return
}
c.Next()
}
}
// ClientAuth authenticates a client site by API key + HMAC body signature.
//
// X-Api-Key: the client's public key
// X-Signature: hex( HMAC-SHA256(client.secret, raw_request_body) )
//
// The verified ClientApp and the raw body are stashed in the gin context.
func ClientAuth(store *db.Store) gin.HandlerFunc {
return func(c *gin.Context) {
apiKey := c.GetHeader("X-Api-Key")
if apiKey == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, models.APIError{Code: "unauthorized", Message: "missing X-Api-Key"})
return
}
client, err := store.GetClientByAPIKey(c.Request.Context(), apiKey)
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, models.APIError{Code: "unauthorized", Message: "unknown or inactive api key"})
return
}
raw, _ := io.ReadAll(c.Request.Body)
c.Request.Body = io.NopCloser(bytes.NewReader(raw)) // restore for the handler
sig := c.GetHeader("X-Signature")
if sig == "" || !signing.Verify(client.Secret, raw, sig) {
c.AbortWithStatusJSON(http.StatusUnauthorized, models.APIError{Code: "bad_signature", Message: "invalid request signature"})
return
}
c.Set(CtxClient, client)
c.Set(CtxRawBody, raw)
c.Next()
}
}
func GetClient(c *gin.Context) *models.ClientApp {
v, _ := c.Get(CtxClient)
cl, _ := v.(*models.ClientApp)
return cl
}
+110
View File
@@ -0,0 +1,110 @@
package models
import (
"encoding/json"
"time"
"github.com/google/uuid"
)
// APIError is the standard error envelope across FlatRender services.
type APIError struct {
Code string `json:"code"`
Message string `json:"message"`
}
// ── Client apps (tenants of the broker — each site that pays through it) ───────
type ClientApp struct {
ID uuid.UUID `json:"id"`
TenantID *uuid.UUID `json:"tenant_id,omitempty"`
Name string `json:"name"`
Slug string `json:"slug"`
APIKey string `json:"api_key"`
Secret string `json:"secret,omitempty"` // returned only on create / rotate
ZarinPalMerchantID *string `json:"zarinpal_merchant_id,omitempty"`
ZarinPalSandbox *bool `json:"zarinpal_sandbox,omitempty"`
AllowedReturnOrigins []string `json:"allowed_return_origins"`
WebhookURL *string `json:"webhook_url,omitempty"`
IsActive bool `json:"is_active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// ── Transactions ──────────────────────────────────────────────────────────────
type Transaction struct {
ID uuid.UUID `json:"id"`
ClientAppID uuid.UUID `json:"client_app_id"`
ClientSlug string `json:"client_slug,omitempty"` // joined for admin views
Status string `json:"status"`
Gateway string `json:"gateway"`
AmountRial int64 `json:"amount_rial"`
Currency string `json:"currency"`
Description *string `json:"description,omitempty"`
ClientRef *string `json:"client_ref,omitempty"`
ReturnURL string `json:"return_url"`
Metadata json.RawMessage `json:"metadata,omitempty"`
PayerMobile *string `json:"payer_mobile,omitempty"`
PayerEmail *string `json:"payer_email,omitempty"`
Authority *string `json:"authority,omitempty"`
RefID *string `json:"ref_id,omitempty"`
CardPan *string `json:"card_pan,omitempty"`
FeeRial *int64 `json:"fee_rial,omitempty"`
GatewayResponse json.RawMessage `json:"gateway_response,omitempty"`
FailureReason *string `json:"failure_reason,omitempty"`
PaidAt *time.Time `json:"paid_at,omitempty"`
FailedAt *time.Time `json:"failed_at,omitempty"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// Transaction status values (kept as plain text — no PG enum, for flexibility).
const (
StatusCreated = "Created"
StatusPending = "Pending"
StatusPaid = "Paid"
StatusFailed = "Failed"
StatusCancelled = "Cancelled"
StatusExpired = "Expired"
)
// ── Client-facing request/response shapes ─────────────────────────────────────
// PayRequest is the body a client site POSTs to /v1/pay/request.
type PayRequest struct {
Amount int64 `json:"amount"` // in `currency` units
Currency string `json:"currency,omitempty"` // "IRR" (Rial, default) or "IRT" (Toman)
Description string `json:"description,omitempty"`
ClientRef string `json:"client_ref,omitempty"`
ReturnURL string `json:"return_url"`
Mobile string `json:"mobile,omitempty"`
Email string `json:"email,omitempty"`
Metadata json.RawMessage `json:"metadata,omitempty"`
}
// PayResponse is returned from /v1/pay/request.
type PayResponse struct {
ID uuid.UUID `json:"id"`
Status string `json:"status"`
PaymentURL string `json:"payment_url"`
Authority string `json:"authority"`
AmountRial int64 `json:"amount_rial"`
}
// WebhookPayload is the signed body POSTed to a client's webhook_url.
type WebhookPayload struct {
Event string `json:"event"` // payment.paid | payment.failed
ID uuid.UUID `json:"id"`
Status string `json:"status"`
AmountRial int64 `json:"amount_rial"`
Currency string `json:"currency"`
ClientRef string `json:"client_ref,omitempty"`
RefID string `json:"ref_id,omitempty"`
Authority string `json:"authority,omitempty"`
CardPan string `json:"card_pan,omitempty"`
Metadata json.RawMessage `json:"metadata,omitempty"`
PaidAt *time.Time `json:"paid_at,omitempty"`
CreatedAtTs int64 `json:"ts"`
}
@@ -0,0 +1,29 @@
// Package signing provides the HMAC-SHA256 helpers the broker uses to (a) verify
// inbound client requests and (b) sign outbound webhooks + return redirects so a
// client site can trust a result came from the broker untampered.
package signing
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
)
// Sign returns the lowercase hex HMAC-SHA256 of message keyed by secret.
func Sign(secret string, message []byte) string {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(message)
return hex.EncodeToString(mac.Sum(nil))
}
// Verify compares an expected hex signature against the computed one in constant time.
func Verify(secret string, message []byte, provided string) bool {
expected := Sign(secret, message)
// hmac.Equal is constant-time; compare the raw bytes after decoding.
pb, err := hex.DecodeString(provided)
if err != nil {
return false
}
eb, _ := hex.DecodeString(expected)
return hmac.Equal(eb, pb)
}
+63
View File
@@ -0,0 +1,63 @@
// Package web serves the broker's minimal hosted pages (RTL Persian). Clients
// normally redirect users back to their OWN return_url; these pages are a
// branded landing + a fallback result screen for direct visits.
package web
import (
"fmt"
"html"
"net/http"
"github.com/gin-gonic/gin"
)
const shell = `<!doctype html><html lang="fa" dir="rtl"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>%s</title>
<style>
:root{color-scheme:dark}
body{margin:0;font-family:Tahoma,Vazirmatn,system-ui,sans-serif;background:#0b0d17;color:#e7e9f3;
display:flex;min-height:100vh;align-items:center;justify-content:center;text-align:center}
.card{background:#141726;border:1px solid #232742;border-radius:20px;padding:40px 32px;max-width:420px;width:90%%}
.logo{font-weight:800;font-size:22px;letter-spacing:.5px;color:#7c8cff}
h1{font-size:20px;margin:18px 0 8px}
p{color:#9aa0bd;line-height:1.9;font-size:14px;margin:6px 0}
.badge{display:inline-block;padding:6px 16px;border-radius:999px;font-size:13px;margin-top:14px}
.ok{background:rgba(34,197,94,.15);color:#4ade80}
.fail{background:rgba(239,68,68,.15);color:#f87171}
.muted{font-size:12px;color:#6a708f;margin-top:18px}
</style></head><body><div class="card">%s</div></body></html>`
func page(title, inner string) string {
return "<!--FlatPay-->" + fmt.Sprintf(shell, html.EscapeString(title), inner)
}
func Landing(c *gin.Context) {
inner := `<div class="logo">FlatRender Pay</div>
<h1>درگاه پرداخت امن</h1>
<p>این سرویس پرداخت‌های آنلاین را از طریق زرین‌پال پردازش می‌کند.</p>
<p class="muted">برای پرداخت، از طریق وب‌سایت مربوطه اقدام کنید.</p>`
c.Header("Content-Type", "text/html; charset=utf-8")
c.String(http.StatusOK, page("FlatRender Pay", inner))
}
// Result is an optional fallback screen a client may point its return_url at.
func Result(c *gin.Context) {
status := c.Query("status")
ref := c.Query("ref_id")
var inner string
if status == "Paid" {
inner = `<div class="logo">FlatRender Pay</div>
<h1>پرداخت موفق</h1>
<span class="badge ok">پرداخت با موفقیت انجام شد</span>`
if ref != "" {
inner += `<p class="muted">کد پیگیری: ` + html.EscapeString(ref) + `</p>`
}
} else {
inner = `<div class="logo">FlatRender Pay</div>
<h1>پرداخت ناموفق</h1>
<span class="badge fail">پرداخت انجام نشد یا لغو شد</span>`
}
c.Header("Content-Type", "text/html; charset=utf-8")
c.String(http.StatusOK, page("نتیجه پرداخت", inner))
}
@@ -0,0 +1,172 @@
// Package zarinpal is a thin client for ZarinPal Payment Gateway v4.
// Ported from the proven implementation in the identity service
// (services/identity/.../PaymentService.cs).
//
// Flow:
// request.json → { authority } → redirect user to StartPay/{authority}
// user pays, ZarinPal calls back → verify.json → { code, ref_id }
// code 100 = success, 101 = already verified (idempotent).
package zarinpal
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"time"
)
const (
prodAPI = "https://api.zarinpal.com/pg/v4/payment"
prodStart = "https://www.zarinpal.com/pg/StartPay/"
sandboxAPI = "https://sandbox.zarinpal.com/pg/v4/payment"
sandStart = "https://sandbox.zarinpal.com/pg/StartPay/"
)
type Client struct {
http *http.Client
}
func New() *Client {
return &Client{http: &http.Client{Timeout: 20 * time.Second}}
}
// RequestResult is the outcome of a payment request.
type RequestResult struct {
Authority string
StartPay string
Code int
Raw json.RawMessage
}
// VerifyResult is the outcome of a verify call.
type VerifyResult struct {
Code int
RefID string
CardPan string
Fee int64
Raw json.RawMessage
}
func apiBase(sandbox bool) (string, string) {
if sandbox {
return sandboxAPI, sandStart
}
return prodAPI, prodStart
}
// Request creates a ZarinPal payment authority. amount is in the unit the merchant
// expects (caller converts Rial↔Toman per config).
func (c *Client) Request(ctx context.Context, sandbox bool, merchantID string, amount int64, callbackURL, description string, metadata map[string]string) (*RequestResult, error) {
base, start := apiBase(sandbox)
body := map[string]any{
"merchant_id": merchantID,
"amount": amount,
"callback_url": callbackURL,
"description": description,
}
if len(metadata) > 0 {
body["metadata"] = metadata
}
root, raw, err := c.post(ctx, base+"/request.json", body)
if err != nil {
return nil, err
}
data, ok := root["data"].(map[string]any)
if !ok {
return nil, fmt.Errorf("zarinpal request: missing data (errors=%v)", root["errors"])
}
code := toInt(data["code"])
if code != 100 {
return &RequestResult{Code: code, Raw: raw}, fmt.Errorf("zarinpal request failed (code=%d): %v", code, root["errors"])
}
authority, _ := data["authority"].(string)
return &RequestResult{Authority: authority, StartPay: start + authority, Code: code, Raw: raw}, nil
}
// Verify confirms a payment by authority. Codes 100/101 mean success.
func (c *Client) Verify(ctx context.Context, sandbox bool, merchantID string, amount int64, authority string) (*VerifyResult, error) {
base, _ := apiBase(sandbox)
body := map[string]any{
"merchant_id": merchantID,
"amount": amount,
"authority": authority,
}
root, raw, err := c.post(ctx, base+"/verify.json", body)
if err != nil {
return nil, err
}
data, ok := root["data"].(map[string]any)
if !ok {
return &VerifyResult{Code: 0, Raw: raw}, fmt.Errorf("zarinpal verify: missing data (errors=%v)", root["errors"])
}
res := &VerifyResult{Code: toInt(data["code"]), Raw: raw}
if ref, ok := data["ref_id"]; ok {
res.RefID = fmt.Sprintf("%v", toInt64(ref))
}
if pan, ok := data["card_pan"].(string); ok {
res.CardPan = pan
}
if fee, ok := data["fee"]; ok {
res.Fee = toInt64(fee)
}
return res, nil
}
func (c *Client) post(ctx context.Context, url string, body map[string]any) (map[string]any, json.RawMessage, error) {
buf, _ := json.Marshal(body)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(buf))
if err != nil {
return nil, nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
resp, err := c.http.Do(req)
if err != nil {
return nil, nil, err
}
defer resp.Body.Close()
var raw json.RawMessage
dec := json.NewDecoder(resp.Body)
if err := dec.Decode(&raw); err != nil {
return nil, nil, fmt.Errorf("zarinpal: decode response: %w", err)
}
var root map[string]any
if err := json.Unmarshal(raw, &root); err != nil {
return nil, raw, fmt.Errorf("zarinpal: parse response: %w", err)
}
return root, raw, nil
}
func toInt(v any) int {
switch n := v.(type) {
case float64:
return int(n)
case int:
return n
case json.Number:
i, _ := n.Int64()
return int(i)
}
return 0
}
func toInt64(v any) int64 {
switch n := v.(type) {
case float64:
return int64(n)
case int64:
return n
case int:
return int64(n)
case json.Number:
i, _ := n.Int64()
return i
}
return 0
}
@@ -0,0 +1,52 @@
*.o
*.swp
*.swm
*.swn
*.a
*.so
_obj
_test
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.exe~
*.test
*.prof
*.rar
*.zip
*.gz
*.psd
*.bmd
*.cfg
*.pptx
*.log
*nohup.out
*settings.pyc
*.sublime-project
*.sublime-workspace
.DS_Store
/.idea/
/.vscode/
/output/
/vendor/
/Gopkg.lock
/Gopkg.toml
coverage.html
coverage.out
coverage.xml
junit.xml
*.profile
*.svg
*.out
ast/test.out
ast/bench.sh
!testdata/*.json.gz
fuzz/testdata
*__debug_bin
@@ -0,0 +1,6 @@
[submodule "cloudwego"]
path = tools/asm2asm
url = https://github.com/cloudwego/asm2asm.git
[submodule "tools/simde"]
path = tools/simde
url = https://github.com/simd-everywhere/simde.git
+24
View File
@@ -0,0 +1,24 @@
header:
license:
spdx-id: Apache-2.0
copyright-owner: ByteDance Inc.
paths:
- '**/*.go'
- '**/*.s'
paths-ignore:
- 'ast/asm.s' # empty file
- 'decoder/asm.s' # empty file
- 'encoder/asm.s' # empty file
- 'internal/caching/asm.s' # empty file
- 'internal/jit/asm.s' # empty file
- 'internal/native/avx/native_amd64.s' # auto-generated by asm2asm
- 'internal/native/avx/native_subr_amd64.go' # auto-generated by asm2asm
- 'internal/native/avx2/native_amd64.s' # auto-generated by asm2asm
- 'internal/native/avx2/native_subr_amd64.go' # auto-generated by asm2asm
- 'internal/resolver/asm.s' # empty file
- 'internal/rt/asm.s' # empty file
- 'internal/loader/asm.s' # empty file
comment: on-failure
+128
View File
@@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
wudi.daniel@bytedance.com.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
+63
View File
@@ -0,0 +1,63 @@
# How to Contribute
## Your First Pull Request
We use GitHub for our codebase. You can start by reading [How To Pull Request](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests).
## Without Semantic Versioning
We keep the stable code in branch `main` like `golang.org/x`. Development base on branch `develop`. We promise the **Forward Compatibility** by adding new package directory with suffix `v2/v3` when code has break changes.
## Branch Organization
We use [git-flow](https://nvie.com/posts/a-successful-git-branching-model/) as our branch organization, as known as [FDD](https://en.wikipedia.org/wiki/Feature-driven_development)
## Bugs
### 1. How to Find Known Issues
We are using [Github Issues](https://github.com/bytedance/sonic/issues) for our public bugs. We keep a close eye on this and try to make it clear when we have an internal fix in progress. Before filing a new task, try to make sure your problem doesnt already exist.
### 2. Reporting New Issues
Providing a reduced test code is a recommended way for reporting issues. Then can be placed in:
- Just in issues
- [Golang Playground](https://play.golang.org/)
### 3. Security Bugs
Please do not report the safe disclosure of bugs to public issues. Contact us by [Support Email](mailto:sonic@bytedance.com)
## How to Get in Touch
- [Email](mailto:wudi.daniel@bytedance.com)
## Submit a Pull Request
Before you submit your Pull Request (PR) consider the following guidelines:
1. Search [GitHub](https://github.com/bytedance/sonic/pulls) for an open or closed PR that relates to your submission. You don't want to duplicate existing efforts.
2. Be sure that an issue describes the problem you're fixing, or documents the design for the feature you'd like to add. Discussing the design upfront helps to ensure that we're ready to accept your work.
3. [Fork](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo) the bytedance/sonic repo.
4. In your forked repository, make your changes in a new git branch:
```
git checkout -b bugfix/security_bug develop
```
5. Create your patch, including appropriate test cases.
6. Follow our [Style Guides](#code-style-guides).
7. Commit your changes using a descriptive commit message that follows [AngularJS Git Commit Message Conventions](https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit).
Adherence to these conventions is necessary because release notes will be automatically generated from these messages.
8. Push your branch to GitHub:
```
git push origin bugfix/security_bug
```
9. In GitHub, send a pull request to `sonic:main`
Note: you must use one of `optimize/feature/bugfix/doc/ci/test/refactor` following a slash(`/`) as the branch prefix.
Your pr title and commit message should follow https://www.conventionalcommits.org/.
## Contribution Prerequisites
- Our development environment keeps up with [Go Official](https://golang.org/project/).
- You need fully checking with lint tools before submit your pull request. [gofmt](https://golang.org/pkg/cmd/gofmt/) & [golangci-lint](https://github.com/golangci/golangci-lint)
- You are familiar with [Github](https://github.com)
- Maybe you need familiar with [Actions](https://github.com/features/actions)(our default workflow tool).
## Code Style Guides
See [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments).
Good resources:
- [Effective Go](https://golang.org/doc/effective_go)
- [Pingcap General advice](https://pingcap.github.io/style-guide/general.html)
- [Uber Go Style Guide](https://github.com/uber-go/guide/blob/master/style.md)
View File
+201
View File
@@ -0,0 +1,201 @@
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.
+471
View File
@@ -0,0 +1,471 @@
# Sonic
English | [中文](README_ZH_CN.md)
A blazingly fast JSON serializing &amp; deserializing library, accelerated by JIT (just-in-time compiling) and SIMD (single-instruction-multiple-data).
## Requirement
- Go 1.16~1.22
- Linux / MacOS / Windows(need go1.17 above)
- Amd64 ARCH
## Features
- Runtime object binding without code generation
- Complete APIs for JSON value manipulation
- Fast, fast, fast!
## APIs
see [go.dev](https://pkg.go.dev/github.com/bytedance/sonic)
## Benchmarks
For **all sizes** of json and **all scenarios** of usage, **Sonic performs best**.
- [Medium](https://github.com/bytedance/sonic/blob/main/decoder/testdata_test.go#L19) (13KB, 300+ key, 6 layers)
```powershell
goversion: 1.17.1
goos: darwin
goarch: amd64
cpu: Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz
BenchmarkEncoder_Generic_Sonic-16 32393 ns/op 402.40 MB/s 11965 B/op 4 allocs/op
BenchmarkEncoder_Generic_Sonic_Fast-16 21668 ns/op 601.57 MB/s 10940 B/op 4 allocs/op
BenchmarkEncoder_Generic_JsonIter-16 42168 ns/op 309.12 MB/s 14345 B/op 115 allocs/op
BenchmarkEncoder_Generic_GoJson-16 65189 ns/op 199.96 MB/s 23261 B/op 16 allocs/op
BenchmarkEncoder_Generic_StdLib-16 106322 ns/op 122.60 MB/s 49136 B/op 789 allocs/op
BenchmarkEncoder_Binding_Sonic-16 6269 ns/op 2079.26 MB/s 14173 B/op 4 allocs/op
BenchmarkEncoder_Binding_Sonic_Fast-16 5281 ns/op 2468.16 MB/s 12322 B/op 4 allocs/op
BenchmarkEncoder_Binding_JsonIter-16 20056 ns/op 649.93 MB/s 9488 B/op 2 allocs/op
BenchmarkEncoder_Binding_GoJson-16 8311 ns/op 1568.32 MB/s 9481 B/op 1 allocs/op
BenchmarkEncoder_Binding_StdLib-16 16448 ns/op 792.52 MB/s 9479 B/op 1 allocs/op
BenchmarkEncoder_Parallel_Generic_Sonic-16 6681 ns/op 1950.93 MB/s 12738 B/op 4 allocs/op
BenchmarkEncoder_Parallel_Generic_Sonic_Fast-16 4179 ns/op 3118.99 MB/s 10757 B/op 4 allocs/op
BenchmarkEncoder_Parallel_Generic_JsonIter-16 9861 ns/op 1321.84 MB/s 14362 B/op 115 allocs/op
BenchmarkEncoder_Parallel_Generic_GoJson-16 18850 ns/op 691.52 MB/s 23278 B/op 16 allocs/op
BenchmarkEncoder_Parallel_Generic_StdLib-16 45902 ns/op 283.97 MB/s 49174 B/op 789 allocs/op
BenchmarkEncoder_Parallel_Binding_Sonic-16 1480 ns/op 8810.09 MB/s 13049 B/op 4 allocs/op
BenchmarkEncoder_Parallel_Binding_Sonic_Fast-16 1209 ns/op 10785.23 MB/s 11546 B/op 4 allocs/op
BenchmarkEncoder_Parallel_Binding_JsonIter-16 6170 ns/op 2112.58 MB/s 9504 B/op 2 allocs/op
BenchmarkEncoder_Parallel_Binding_GoJson-16 3321 ns/op 3925.52 MB/s 9496 B/op 1 allocs/op
BenchmarkEncoder_Parallel_Binding_StdLib-16 3739 ns/op 3486.49 MB/s 9480 B/op 1 allocs/op
BenchmarkDecoder_Generic_Sonic-16 66812 ns/op 195.10 MB/s 57602 B/op 723 allocs/op
BenchmarkDecoder_Generic_Sonic_Fast-16 54523 ns/op 239.07 MB/s 49786 B/op 313 allocs/op
BenchmarkDecoder_Generic_StdLib-16 124260 ns/op 104.90 MB/s 50869 B/op 772 allocs/op
BenchmarkDecoder_Generic_JsonIter-16 91274 ns/op 142.81 MB/s 55782 B/op 1068 allocs/op
BenchmarkDecoder_Generic_GoJson-16 88569 ns/op 147.17 MB/s 66367 B/op 973 allocs/op
BenchmarkDecoder_Binding_Sonic-16 32557 ns/op 400.38 MB/s 28302 B/op 137 allocs/op
BenchmarkDecoder_Binding_Sonic_Fast-16 28649 ns/op 455.00 MB/s 24999 B/op 34 allocs/op
BenchmarkDecoder_Binding_StdLib-16 111437 ns/op 116.97 MB/s 10576 B/op 208 allocs/op
BenchmarkDecoder_Binding_JsonIter-16 35090 ns/op 371.48 MB/s 14673 B/op 385 allocs/op
BenchmarkDecoder_Binding_GoJson-16 28738 ns/op 453.59 MB/s 22039 B/op 49 allocs/op
BenchmarkDecoder_Parallel_Generic_Sonic-16 12321 ns/op 1057.91 MB/s 57233 B/op 723 allocs/op
BenchmarkDecoder_Parallel_Generic_Sonic_Fast-16 10644 ns/op 1224.64 MB/s 49362 B/op 313 allocs/op
BenchmarkDecoder_Parallel_Generic_StdLib-16 57587 ns/op 226.35 MB/s 50874 B/op 772 allocs/op
BenchmarkDecoder_Parallel_Generic_JsonIter-16 38666 ns/op 337.12 MB/s 55789 B/op 1068 allocs/op
BenchmarkDecoder_Parallel_Generic_GoJson-16 30259 ns/op 430.79 MB/s 66370 B/op 974 allocs/op
BenchmarkDecoder_Parallel_Binding_Sonic-16 5965 ns/op 2185.28 MB/s 27747 B/op 137 allocs/op
BenchmarkDecoder_Parallel_Binding_Sonic_Fast-16 5170 ns/op 2521.31 MB/s 24715 B/op 34 allocs/op
BenchmarkDecoder_Parallel_Binding_StdLib-16 27582 ns/op 472.58 MB/s 10576 B/op 208 allocs/op
BenchmarkDecoder_Parallel_Binding_JsonIter-16 13571 ns/op 960.51 MB/s 14685 B/op 385 allocs/op
BenchmarkDecoder_Parallel_Binding_GoJson-16 10031 ns/op 1299.51 MB/s 22111 B/op 49 allocs/op
BenchmarkGetOne_Sonic-16 3276 ns/op 3975.78 MB/s 24 B/op 1 allocs/op
BenchmarkGetOne_Gjson-16 9431 ns/op 1380.81 MB/s 0 B/op 0 allocs/op
BenchmarkGetOne_Jsoniter-16 51178 ns/op 254.46 MB/s 27936 B/op 647 allocs/op
BenchmarkGetOne_Parallel_Sonic-16 216.7 ns/op 60098.95 MB/s 24 B/op 1 allocs/op
BenchmarkGetOne_Parallel_Gjson-16 1076 ns/op 12098.62 MB/s 0 B/op 0 allocs/op
BenchmarkGetOne_Parallel_Jsoniter-16 17741 ns/op 734.06 MB/s 27945 B/op 647 allocs/op
BenchmarkSetOne_Sonic-16 9571 ns/op 1360.61 MB/s 1584 B/op 17 allocs/op
BenchmarkSetOne_Sjson-16 36456 ns/op 357.22 MB/s 52180 B/op 9 allocs/op
BenchmarkSetOne_Jsoniter-16 79475 ns/op 163.86 MB/s 45862 B/op 964 allocs/op
BenchmarkSetOne_Parallel_Sonic-16 850.9 ns/op 15305.31 MB/s 1584 B/op 17 allocs/op
BenchmarkSetOne_Parallel_Sjson-16 18194 ns/op 715.77 MB/s 52247 B/op 9 allocs/op
BenchmarkSetOne_Parallel_Jsoniter-16 33560 ns/op 388.05 MB/s 45892 B/op 964 allocs/op
BenchmarkLoadNode/LoadAll()-16 11384 ns/op 1143.93 MB/s 6307 B/op 25 allocs/op
BenchmarkLoadNode_Parallel/LoadAll()-16 5493 ns/op 2370.68 MB/s 7145 B/op 25 allocs/op
BenchmarkLoadNode/Interface()-16 17722 ns/op 734.85 MB/s 13323 B/op 88 allocs/op
BenchmarkLoadNode_Parallel/Interface()-16 10330 ns/op 1260.70 MB/s 15178 B/op 88 allocs/op
```
- [Small](https://github.com/bytedance/sonic/blob/main/testdata/small.go) (400B, 11 keys, 3 layers)
![small benchmarks](./docs/imgs/bench-small.png)
- [Large](https://github.com/bytedance/sonic/blob/main/testdata/twitter.json) (635KB, 10000+ key, 6 layers)
![large benchmarks](./docs/imgs/bench-large.png)
See [bench.sh](https://github.com/bytedance/sonic/blob/main/scripts/bench.sh) for benchmark codes.
## How it works
See [INTRODUCTION.md](./docs/INTRODUCTION.md).
## Usage
### Marshal/Unmarshal
Default behaviors are mostly consistent with `encoding/json`, except HTML escaping form (see [Escape HTML](https://github.com/bytedance/sonic/blob/main/README.md#escape-html)) and `SortKeys` feature (optional support see [Sort Keys](https://github.com/bytedance/sonic/blob/main/README.md#sort-keys)) that is **NOT** in conformity to [RFC8259](https://datatracker.ietf.org/doc/html/rfc8259).
```go
import "github.com/bytedance/sonic"
var data YourSchema
// Marshal
output, err := sonic.Marshal(&data)
// Unmarshal
err := sonic.Unmarshal(output, &data)
```
### Streaming IO
Sonic supports decoding json from `io.Reader` or encoding objects into `io.Writer`, aims at handling multiple values as well as reducing memory consumption.
- encoder
```go
var o1 = map[string]interface{}{
"a": "b",
}
var o2 = 1
var w = bytes.NewBuffer(nil)
var enc = sonic.ConfigDefault.NewEncoder(w)
enc.Encode(o1)
enc.Encode(o2)
fmt.Println(w.String())
// Output:
// {"a":"b"}
// 1
```
- decoder
```go
var o = map[string]interface{}{}
var r = strings.NewReader(`{"a":"b"}{"1":"2"}`)
var dec = sonic.ConfigDefault.NewDecoder(r)
dec.Decode(&o)
dec.Decode(&o)
fmt.Printf("%+v", o)
// Output:
// map[1:2 a:b]
```
### Use Number/Use Int64
```go
import "github.com/bytedance/sonic/decoder"
var input = `1`
var data interface{}
// default float64
dc := decoder.NewDecoder(input)
dc.Decode(&data) // data == float64(1)
// use json.Number
dc = decoder.NewDecoder(input)
dc.UseNumber()
dc.Decode(&data) // data == json.Number("1")
// use int64
dc = decoder.NewDecoder(input)
dc.UseInt64()
dc.Decode(&data) // data == int64(1)
root, err := sonic.GetFromString(input)
// Get json.Number
jn := root.Number()
jm := root.InterfaceUseNumber().(json.Number) // jn == jm
// Get float64
fn := root.Float64()
fm := root.Interface().(float64) // jn == jm
```
### Sort Keys
On account of the performance loss from sorting (roughly 10%), sonic doesn't enable this feature by default. If your component depends on it to work (like [zstd](https://github.com/facebook/zstd)), Use it like this:
```go
import "github.com/bytedance/sonic"
import "github.com/bytedance/sonic/encoder"
// Binding map only
m := map[string]interface{}{}
v, err := encoder.Encode(m, encoder.SortMapKeys)
// Or ast.Node.SortKeys() before marshal
var root := sonic.Get(JSON)
err := root.SortKeys()
```
### Escape HTML
On account of the performance loss (roughly 15%), sonic doesn't enable this feature by default. You can use `encoder.EscapeHTML` option to open this feature (align with `encoding/json.HTMLEscape`).
```go
import "github.com/bytedance/sonic"
v := map[string]string{"&&":"<>"}
ret, err := Encode(v, EscapeHTML) // ret == `{"\u0026\u0026":{"X":"\u003c\u003e"}}`
```
### Compact Format
Sonic encodes primitive objects (struct/map...) as compact-format JSON by default, except marshaling `json.RawMessage` or `json.Marshaler`: sonic ensures validating their output JSON but **DONOT** compacting them for performance concerns. We provide the option `encoder.CompactMarshaler` to add compacting process.
### Print Error
If there invalid syntax in input JSON, sonic will return `decoder.SyntaxError`, which supports pretty-printing of error position
```go
import "github.com/bytedance/sonic"
import "github.com/bytedance/sonic/decoder"
var data interface{}
err := sonic.UnmarshalString("[[[}]]", &data)
if err != nil {
/* One line by default */
println(e.Error()) // "Syntax error at index 3: invalid char\n\n\t[[[}]]\n\t...^..\n"
/* Pretty print */
if e, ok := err.(decoder.SyntaxError); ok {
/*Syntax error at index 3: invalid char
[[[}]]
...^..
*/
print(e.Description())
} else if me, ok := err.(*decoder.MismatchTypeError); ok {
// decoder.MismatchTypeError is new to Sonic v1.6.0
print(me.Description())
}
}
```
#### Mismatched Types [Sonic v1.6.0]
If there a **mismatch-typed** value for a given key, sonic will report `decoder.MismatchTypeError` (if there are many, report the last one), but still skip wrong the value and keep decoding next JSON.
```go
import "github.com/bytedance/sonic"
import "github.com/bytedance/sonic/decoder"
var data = struct{
A int
B int
}{}
err := UnmarshalString(`{"A":"1","B":1}`, &data)
println(err.Error()) // Mismatch type int with value string "at index 5: mismatched type with value\n\n\t{\"A\":\"1\",\"B\":1}\n\t.....^.........\n"
fmt.Printf("%+v", data) // {A:0 B:1}
```
### Ast.Node
Sonic/ast.Node is a completely self-contained AST for JSON. It implements serialization and deserialization both and provides robust APIs for obtaining and modification of generic data.
#### Get/Index
Search partial JSON by given paths, which must be non-negative integer or string, or nil
```go
import "github.com/bytedance/sonic"
input := []byte(`{"key1":[{},{"key2":{"key3":[1,2,3]}}]}`)
// no path, returns entire json
root, err := sonic.Get(input)
raw := root.Raw() // == string(input)
// multiple paths
root, err := sonic.Get(input, "key1", 1, "key2")
sub := root.Get("key3").Index(2).Int64() // == 3
```
**Tip**: since `Index()` uses offset to locate data, which is much faster than scanning like `Get()`, we suggest you use it as much as possible. And sonic also provides another API `IndexOrGet()` to underlying use offset as well as ensure the key is matched.
#### Set/Unset
Modify the json content by Set()/Unset()
```go
import "github.com/bytedance/sonic"
// Set
exist, err := root.Set("key4", NewBool(true)) // exist == false
alias1 := root.Get("key4")
println(alias1.Valid()) // true
alias2 := root.Index(1)
println(alias1 == alias2) // true
// Unset
exist, err := root.UnsetByIndex(1) // exist == true
println(root.Get("key4").Check()) // "value not exist"
```
#### Serialize
To encode `ast.Node` as json, use `MarshalJson()` or `json.Marshal()` (MUST pass the node's pointer)
```go
import (
"encoding/json"
"github.com/bytedance/sonic"
)
buf, err := root.MarshalJson()
println(string(buf)) // {"key1":[{},{"key2":{"key3":[1,2,3]}}]}
exp, err := json.Marshal(&root) // WARN: use pointer
println(string(buf) == string(exp)) // true
```
#### APIs
- validation: `Check()`, `Error()`, `Valid()`, `Exist()`
- searching: `Index()`, `Get()`, `IndexPair()`, `IndexOrGet()`, `GetByPath()`
- go-type casting: `Int64()`, `Float64()`, `String()`, `Number()`, `Bool()`, `Map[UseNumber|UseNode]()`, `Array[UseNumber|UseNode]()`, `Interface[UseNumber|UseNode]()`
- go-type packing: `NewRaw()`, `NewNumber()`, `NewNull()`, `NewBool()`, `NewString()`, `NewObject()`, `NewArray()`
- iteration: `Values()`, `Properties()`, `ForEach()`, `SortKeys()`
- modification: `Set()`, `SetByIndex()`, `Add()`
### Ast.Visitor
Sonic provides an advanced API for fully parsing JSON into non-standard types (neither `struct` not `map[string]interface{}`) without using any intermediate representation (`ast.Node` or `interface{}`). For example, you might have the following types which are like `interface{}` but actually not `interface{}`:
```go
type UserNode interface {}
// the following types implement the UserNode interface.
type (
UserNull struct{}
UserBool struct{ Value bool }
UserInt64 struct{ Value int64 }
UserFloat64 struct{ Value float64 }
UserString struct{ Value string }
UserObject struct{ Value map[string]UserNode }
UserArray struct{ Value []UserNode }
)
```
Sonic provides the following API to return **the preorder traversal of a JSON AST**. The `ast.Visitor` is a SAX style interface which is used in some C++ JSON library. You should implement `ast.Visitor` by yourself and pass it to `ast.Preorder()` method. In your visitor you can make your custom types to represent JSON values. There may be an O(n) space container (such as stack) in your visitor to record the object / array hierarchy.
```go
func Preorder(str string, visitor Visitor, opts *VisitorOptions) error
type Visitor interface {
OnNull() error
OnBool(v bool) error
OnString(v string) error
OnInt64(v int64, n json.Number) error
OnFloat64(v float64, n json.Number) error
OnObjectBegin(capacity int) error
OnObjectKey(key string) error
OnObjectEnd() error
OnArrayBegin(capacity int) error
OnArrayEnd() error
}
```
See [ast/visitor.go](https://github.com/bytedance/sonic/blob/main/ast/visitor.go) for detailed usage. We also implement a demo visitor for `UserNode` in [ast/visitor_test.go](https://github.com/bytedance/sonic/blob/main/ast/visitor_test.go).
## Compatibility
Sonic **DOES NOT** ensure to support all environments, due to the difficulty of developing high-performance codes. For developers who use sonic to build their applications in different environments, we have the following suggestions:
- Developing on **Mac M1**: Make sure you have Rosetta 2 installed on your machine, and set `GOARCH=amd64` when building your application. Rosetta 2 can automatically translate x86 binaries to arm64 binaries and run x86 applications on Mac M1.
- Developing on **Linux arm64**: You can install qemu and use the `qemu-x86_64 -cpu max` command to convert x86 binaries to amr64 binaries for applications built with sonic. The qemu can achieve a similar transfer effect to Rosetta 2 on Mac M1.
For developers who want to use sonic on Linux arm64 without qemu, or those who want to handle JSON strictly consistent with `encoding/json`, we provide some compatible APIs as `sonic.API`
- `ConfigDefault`: the sonic's default config (`EscapeHTML=false`,`SortKeys=false`...) to run on sonic-supporting environment. It will fall back to `encoding/json` with the corresponding config, and some options like `SortKeys=false` will be invalid.
- `ConfigStd`: the std-compatible config (`EscapeHTML=true`,`SortKeys=true`...) to run on sonic-supporting environment. It will fall back to `encoding/json`.
- `ConfigFastest`: the fastest config (`NoQuoteTextMarshaler=true`) to run on sonic-supporting environment. It will fall back to `encoding/json` with the corresponding config, and some options will be invalid.
## Tips
### Pretouch
Since Sonic uses [golang-asm](https://github.com/twitchyliquid64/golang-asm) as a JIT assembler, which is NOT very suitable for runtime compiling, first-hit running of a huge schema may cause request-timeout or even process-OOM. For better stability, we advise **using `Pretouch()` for huge-schema or compact-memory applications** before `Marshal()/Unmarshal()`.
```go
import (
"reflect"
"github.com/bytedance/sonic"
"github.com/bytedance/sonic/option"
)
func init() {
var v HugeStruct
// For most large types (nesting depth <= option.DefaultMaxInlineDepth)
err := sonic.Pretouch(reflect.TypeOf(v))
// with more CompileOption...
err := sonic.Pretouch(reflect.TypeOf(v),
// If the type is too deep nesting (nesting depth > option.DefaultMaxInlineDepth),
// you can set compile recursive loops in Pretouch for better stability in JIT.
option.WithCompileRecursiveDepth(loop),
// For a large nested struct, try to set a smaller depth to reduce compiling time.
option.WithCompileMaxInlineDepth(depth),
)
}
```
### Copy string
When decoding **string values without any escaped characters**, sonic references them from the origin JSON buffer instead of mallocing a new buffer to copy. This helps a lot for CPU performance but may leave the whole JSON buffer in memory as long as the decoded objects are being used. In practice, we found the extra memory introduced by referring JSON buffer is usually 20% ~ 80% of decoded objects. Once an application holds these objects for a long time (for example, cache the decoded objects for reusing), its in-use memory on the server may go up. - `Config.CopyString`/`decoder.CopyString()`: We provide the option for `Decode()` / `Unmarshal()` users to choose not to reference the JSON buffer, which may cause a decline in CPU performance to some degree.
- `GetFromStringNoCopy()`: For memory safety, `sonic.Get()` / `sonic.GetFromString()` now copies return JSON. If users want to get json more quickly and not care about memory usage, you can use `GetFromStringNoCopy()` to return a JSON directly referenced from source.
### Pass string or []byte?
For alignment to `encoding/json`, we provide API to pass `[]byte` as an argument, but the string-to-bytes copy is conducted at the same time considering safety, which may lose performance when the origin JSON is huge. Therefore, you can use `UnmarshalString()` and `GetFromString()` to pass a string, as long as your origin data is a string or **nocopy-cast** is safe for your []byte. We also provide API `MarshalString()` for convenient **nocopy-cast** of encoded JSON []byte, which is safe since sonic's output bytes is always duplicated and unique.
### Accelerate `encoding.TextMarshaler`
To ensure data security, sonic.Encoder quotes and escapes string values from `encoding.TextMarshaler` interfaces by default, which may degrade performance much if most of your data is in form of them. We provide `encoder.NoQuoteTextMarshaler` to skip these operations, which means you **MUST** ensure their output string escaped and quoted following [RFC8259](https://datatracker.ietf.org/doc/html/rfc8259).
### Better performance for generic data
In **fully-parsed** scenario, `Unmarshal()` performs better than `Get()`+`Node.Interface()`. But if you only have a part of the schema for specific json, you can combine `Get()` and `Unmarshal()` together:
```go
import "github.com/bytedance/sonic"
node, err := sonic.GetFromString(_TwitterJson, "statuses", 3, "user")
var user User // your partial schema...
err = sonic.UnmarshalString(node.Raw(), &user)
```
Even if you don't have any schema, use `ast.Node` as the container of generic values instead of `map` or `interface`:
```go
import "github.com/bytedance/sonic"
root, err := sonic.GetFromString(_TwitterJson)
user := root.GetByPath("statuses", 3, "user") // === root.Get("status").Index(3).Get("user")
err = user.Check()
// err = user.LoadAll() // only call this when you want to use 'user' concurrently...
go someFunc(user)
```
Why? Because `ast.Node` stores its children using `array`:
- `Array`'s performance is **much better** than `Map` when Inserting (Deserialize) and Scanning (Serialize) data;
- **Hashing** (`map[x]`) is not as efficient as **Indexing** (`array[x]`), which `ast.Node` can conduct on **both array and object**;
- Using `Interface()`/`Map()` means Sonic must parse all the underlying values, while `ast.Node` can parse them **on demand**.
**CAUTION:** `ast.Node` **DOESN'T** ensure concurrent security directly, due to its **lazy-load** design. However, you can call `Node.Load()`/`Node.LoadAll()` to achieve that, which may bring performance reduction while it still works faster than converting to `map` or `interface{}`
### Ast.Node or Ast.Visitor?
For generic data, `ast.Node` should be enough for your needs in most cases.
However, `ast.Node` is designed for partially processing JSON string. It has some special designs such as lazy-load which might not be suitable for directly parsing the whole JSON string like `Unmarshal()`. Although `ast.Node` is better then `map` or `interface{}`, it's also a kind of intermediate representation after all if your final types are customized and you have to convert the above types to your custom types after parsing.
For better performance, in previous case the `ast.Visitor` will be the better choice. It performs JSON decoding like `Unmarshal()` and you can directly use your final types to represents a JSON AST without any intermediate representations.
But `ast.Visitor` is not a very handy API. You might need to write a lot of code to implement your visitor and carefully maintain the tree hierarchy during decoding. Please read the comments in [ast/visitor.go](https://github.com/bytedance/sonic/blob/main/ast/visitor.go) carefully if you decide to use this API.
## Community
Sonic is a subproject of [CloudWeGo](https://www.cloudwego.io/). We are committed to building a cloud native ecosystem.
+469
View File
@@ -0,0 +1,469 @@
# Sonic
[English](README.md) | 中文
一个速度奇快的 JSON 序列化/反序列化库,由 JIT (即时编译)和 SIMD (单指令流多数据流)加速。
## 依赖
- Go 1.16~1.22
- Linux / MacOS / Windows(需要 Go1.17 以上)
- Amd64 架构
## 接口
详见 [go.dev](https://pkg.go.dev/github.com/bytedance/sonic)
## 特色
- 运行时对象绑定,无需代码生成
- 完备的 JSON 操作 API
- 快,更快,还要更快!
## 基准测试
对于**所有大小**的 json 和**所有使用场景** **Sonic 表现均为最佳**
- [中型](https://github.com/bytedance/sonic/blob/main/decoder/testdata_test.go#L19) (13kB, 300+ 键, 6 层)
```powershell
goversion: 1.17.1
goos: darwin
goarch: amd64
cpu: Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz
BenchmarkEncoder_Generic_Sonic-16 32393 ns/op 402.40 MB/s 11965 B/op 4 allocs/op
BenchmarkEncoder_Generic_Sonic_Fast-16 21668 ns/op 601.57 MB/s 10940 B/op 4 allocs/op
BenchmarkEncoder_Generic_JsonIter-16 42168 ns/op 309.12 MB/s 14345 B/op 115 allocs/op
BenchmarkEncoder_Generic_GoJson-16 65189 ns/op 199.96 MB/s 23261 B/op 16 allocs/op
BenchmarkEncoder_Generic_StdLib-16 106322 ns/op 122.60 MB/s 49136 B/op 789 allocs/op
BenchmarkEncoder_Binding_Sonic-16 6269 ns/op 2079.26 MB/s 14173 B/op 4 allocs/op
BenchmarkEncoder_Binding_Sonic_Fast-16 5281 ns/op 2468.16 MB/s 12322 B/op 4 allocs/op
BenchmarkEncoder_Binding_JsonIter-16 20056 ns/op 649.93 MB/s 9488 B/op 2 allocs/op
BenchmarkEncoder_Binding_GoJson-16 8311 ns/op 1568.32 MB/s 9481 B/op 1 allocs/op
BenchmarkEncoder_Binding_StdLib-16 16448 ns/op 792.52 MB/s 9479 B/op 1 allocs/op
BenchmarkEncoder_Parallel_Generic_Sonic-16 6681 ns/op 1950.93 MB/s 12738 B/op 4 allocs/op
BenchmarkEncoder_Parallel_Generic_Sonic_Fast-16 4179 ns/op 3118.99 MB/s 10757 B/op 4 allocs/op
BenchmarkEncoder_Parallel_Generic_JsonIter-16 9861 ns/op 1321.84 MB/s 14362 B/op 115 allocs/op
BenchmarkEncoder_Parallel_Generic_GoJson-16 18850 ns/op 691.52 MB/s 23278 B/op 16 allocs/op
BenchmarkEncoder_Parallel_Generic_StdLib-16 45902 ns/op 283.97 MB/s 49174 B/op 789 allocs/op
BenchmarkEncoder_Parallel_Binding_Sonic-16 1480 ns/op 8810.09 MB/s 13049 B/op 4 allocs/op
BenchmarkEncoder_Parallel_Binding_Sonic_Fast-16 1209 ns/op 10785.23 MB/s 11546 B/op 4 allocs/op
BenchmarkEncoder_Parallel_Binding_JsonIter-16 6170 ns/op 2112.58 MB/s 9504 B/op 2 allocs/op
BenchmarkEncoder_Parallel_Binding_GoJson-16 3321 ns/op 3925.52 MB/s 9496 B/op 1 allocs/op
BenchmarkEncoder_Parallel_Binding_StdLib-16 3739 ns/op 3486.49 MB/s 9480 B/op 1 allocs/op
BenchmarkDecoder_Generic_Sonic-16 66812 ns/op 195.10 MB/s 57602 B/op 723 allocs/op
BenchmarkDecoder_Generic_Sonic_Fast-16 54523 ns/op 239.07 MB/s 49786 B/op 313 allocs/op
BenchmarkDecoder_Generic_StdLib-16 124260 ns/op 104.90 MB/s 50869 B/op 772 allocs/op
BenchmarkDecoder_Generic_JsonIter-16 91274 ns/op 142.81 MB/s 55782 B/op 1068 allocs/op
BenchmarkDecoder_Generic_GoJson-16 88569 ns/op 147.17 MB/s 66367 B/op 973 allocs/op
BenchmarkDecoder_Binding_Sonic-16 32557 ns/op 400.38 MB/s 28302 B/op 137 allocs/op
BenchmarkDecoder_Binding_Sonic_Fast-16 28649 ns/op 455.00 MB/s 24999 B/op 34 allocs/op
BenchmarkDecoder_Binding_StdLib-16 111437 ns/op 116.97 MB/s 10576 B/op 208 allocs/op
BenchmarkDecoder_Binding_JsonIter-16 35090 ns/op 371.48 MB/s 14673 B/op 385 allocs/op
BenchmarkDecoder_Binding_GoJson-16 28738 ns/op 453.59 MB/s 22039 B/op 49 allocs/op
BenchmarkDecoder_Parallel_Generic_Sonic-16 12321 ns/op 1057.91 MB/s 57233 B/op 723 allocs/op
BenchmarkDecoder_Parallel_Generic_Sonic_Fast-16 10644 ns/op 1224.64 MB/s 49362 B/op 313 allocs/op
BenchmarkDecoder_Parallel_Generic_StdLib-16 57587 ns/op 226.35 MB/s 50874 B/op 772 allocs/op
BenchmarkDecoder_Parallel_Generic_JsonIter-16 38666 ns/op 337.12 MB/s 55789 B/op 1068 allocs/op
BenchmarkDecoder_Parallel_Generic_GoJson-16 30259 ns/op 430.79 MB/s 66370 B/op 974 allocs/op
BenchmarkDecoder_Parallel_Binding_Sonic-16 5965 ns/op 2185.28 MB/s 27747 B/op 137 allocs/op
BenchmarkDecoder_Parallel_Binding_Sonic_Fast-16 5170 ns/op 2521.31 MB/s 24715 B/op 34 allocs/op
BenchmarkDecoder_Parallel_Binding_StdLib-16 27582 ns/op 472.58 MB/s 10576 B/op 208 allocs/op
BenchmarkDecoder_Parallel_Binding_JsonIter-16 13571 ns/op 960.51 MB/s 14685 B/op 385 allocs/op
BenchmarkDecoder_Parallel_Binding_GoJson-16 10031 ns/op 1299.51 MB/s 22111 B/op 49 allocs/op
BenchmarkGetOne_Sonic-16 3276 ns/op 3975.78 MB/s 24 B/op 1 allocs/op
BenchmarkGetOne_Gjson-16 9431 ns/op 1380.81 MB/s 0 B/op 0 allocs/op
BenchmarkGetOne_Jsoniter-16 51178 ns/op 254.46 MB/s 27936 B/op 647 allocs/op
BenchmarkGetOne_Parallel_Sonic-16 216.7 ns/op 60098.95 MB/s 24 B/op 1 allocs/op
BenchmarkGetOne_Parallel_Gjson-16 1076 ns/op 12098.62 MB/s 0 B/op 0 allocs/op
BenchmarkGetOne_Parallel_Jsoniter-16 17741 ns/op 734.06 MB/s 27945 B/op 647 allocs/op
BenchmarkSetOne_Sonic-16 9571 ns/op 1360.61 MB/s 1584 B/op 17 allocs/op
BenchmarkSetOne_Sjson-16 36456 ns/op 357.22 MB/s 52180 B/op 9 allocs/op
BenchmarkSetOne_Jsoniter-16 79475 ns/op 163.86 MB/s 45862 B/op 964 allocs/op
BenchmarkSetOne_Parallel_Sonic-16 850.9 ns/op 15305.31 MB/s 1584 B/op 17 allocs/op
BenchmarkSetOne_Parallel_Sjson-16 18194 ns/op 715.77 MB/s 52247 B/op 9 allocs/op
BenchmarkSetOne_Parallel_Jsoniter-16 33560 ns/op 388.05 MB/s 45892 B/op 964 allocs/op
BenchmarkLoadNode/LoadAll()-16 11384 ns/op 1143.93 MB/s 6307 B/op 25 allocs/op
BenchmarkLoadNode_Parallel/LoadAll()-16 5493 ns/op 2370.68 MB/s 7145 B/op 25 allocs/op
BenchmarkLoadNode/Interface()-16 17722 ns/op 734.85 MB/s 13323 B/op 88 allocs/op
BenchmarkLoadNode_Parallel/Interface()-16 10330 ns/op 1260.70 MB/s 15178 B/op 88 allocs/op
```
- [小型](https://github.com/bytedance/sonic/blob/main/testdata/small.go) (400B, 11 个键, 3 层)
![small benchmarks](./docs/imgs/bench-small.png)
- [大型](https://github.com/bytedance/sonic/blob/main/testdata/twitter.json) (635kB, 10000+ 个键, 6 层)
![large benchmarks](./docs/imgs/bench-large.png)
要查看基准测试代码,请参阅 [bench.sh](https://github.com/bytedance/sonic/blob/main/scripts/bench.sh) 。
## 工作原理
请参阅 [INTRODUCTION_ZH_CN.md](./docs/INTRODUCTION_ZH_CN.md).
## 使用方式
### 序列化/反序列化
默认的行为基本上与 `encoding/json` 相一致,除了 HTML 转义形式(参见 [Escape HTML](https://github.com/bytedance/sonic/blob/main/README.md#escape-html)) 和 `SortKeys` 功能(参见 [Sort Keys](https://github.com/bytedance/sonic/blob/main/README.md#sort-keys)**没有**遵循 [RFC8259](https://datatracker.ietf.org/doc/html/rfc8259) 。
```go
import "github.com/bytedance/sonic"
var data YourSchema
// Marshal
output, err := sonic.Marshal(&data)
// Unmarshal
err := sonic.Unmarshal(output, &data)
```
### 流式输入输出
Sonic 支持解码 `io.Reader` 中输入的 json,或将对象编码为 json 后输出至 `io.Writer`,以处理多个值并减少内存消耗。
- 编码器
```go
var o1 = map[string]interface{}{
"a": "b",
}
var o2 = 1
var w = bytes.NewBuffer(nil)
var enc = sonic.ConfigDefault.NewEncoder(w)
enc.Encode(o1)
enc.Encode(o2)
fmt.Println(w.String())
// Output:
// {"a":"b"}
// 1
```
- 解码器
```go
var o = map[string]interface{}{}
var r = strings.NewReader(`{"a":"b"}{"1":"2"}`)
var dec = sonic.ConfigDefault.NewDecoder(r)
dec.Decode(&o)
dec.Decode(&o)
fmt.Printf("%+v", o)
// Output:
// map[1:2 a:b]
```
### 使用 `Number` / `int64`
```go
import "github.com/bytedance/sonic/decoder"
var input = `1`
var data interface{}
// default float64
dc := decoder.NewDecoder(input)
dc.Decode(&data) // data == float64(1)
// use json.Number
dc = decoder.NewDecoder(input)
dc.UseNumber()
dc.Decode(&data) // data == json.Number("1")
// use int64
dc = decoder.NewDecoder(input)
dc.UseInt64()
dc.Decode(&data) // data == int64(1)
root, err := sonic.GetFromString(input)
// Get json.Number
jn := root.Number()
jm := root.InterfaceUseNumber().(json.Number) // jn == jm
// Get float64
fn := root.Float64()
fm := root.Interface().(float64) // jn == jm
```
### 对键排序
考虑到排序带来的性能损失(约 10% ), sonic 默认不会启用这个功能。如果你的组件依赖这个行为(如 [zstd](https://github.com/facebook/zstd)) ,可以仿照下面的例子:
```go
import "github.com/bytedance/sonic"
import "github.com/bytedance/sonic/encoder"
// Binding map only
m := map[string]interface{}{}
v, err := encoder.Encode(m, encoder.SortMapKeys)
// Or ast.Node.SortKeys() before marshal
var root := sonic.Get(JSON)
err := root.SortKeys()
```
### HTML 转义
考虑到性能损失(约15%), sonic 默认不会启用这个功能。你可以使用 `encoder.EscapeHTML` 选项来开启(与 `encoding/json.HTMLEscape` 行为一致)。
```go
import "github.com/bytedance/sonic"
v := map[string]string{"&&":"<>"}
ret, err := Encode(v, EscapeHTML) // ret == `{"\u0026\u0026":{"X":"\u003c\u003e"}}`
```
### 紧凑格式
Sonic 默认将基本类型( `struct` `map` 等)编码为紧凑格式的 JSON ,除非使用 `json.RawMessage` or `json.Marshaler` 进行编码: sonic 确保输出的 JSON 合法,但出于性能考虑,**不会**加工成紧凑格式。我们提供选项 `encoder.CompactMarshaler` 来添加此过程,
### 打印错误
如果输入的 JSON 存在无效的语法,sonic 将返回 `decoder.SyntaxError`,该错误支持错误位置的美化输出。
```go
import "github.com/bytedance/sonic"
import "github.com/bytedance/sonic/decoder"
var data interface{}
err := sonic.UnmarshalString("[[[}]]", &data)
if err != nil {
/* One line by default */
println(e.Error()) // "Syntax error at index 3: invalid char\n\n\t[[[}]]\n\t...^..\n"
/* Pretty print */
if e, ok := err.(decoder.SyntaxError); ok {
/*Syntax error at index 3: invalid char
[[[}]]
...^..
*/
print(e.Description())
} else if me, ok := err.(*decoder.MismatchTypeError); ok {
// decoder.MismatchTypeError is new to Sonic v1.6.0
print(me.Description())
}
}
```
#### 类型不匹配 [Sonic v1.6.0]
如果给定键中存在**类型不匹配**的值, sonic 会抛出 `decoder.MismatchTypeError` (如果有多个,只会报告最后一个),但仍会跳过错误的值并解码下一个 JSON 。
```go
import "github.com/bytedance/sonic"
import "github.com/bytedance/sonic/decoder"
var data = struct{
A int
B int
}{}
err := UnmarshalString(`{"A":"1","B":1}`, &data)
println(err.Error()) // Mismatch type int with value string "at index 5: mismatched type with value\n\n\t{\"A\":\"1\",\"B\":1}\n\t.....^.........\n"
fmt.Printf("%+v", data) // {A:0 B:1}
```
### `Ast.Node`
Sonic/ast.Node 是完全独立的 JSON 抽象语法树库。它实现了序列化和反序列化,并提供了获取和修改通用数据的鲁棒的 API。
#### 查找/索引
通过给定的路径搜索 JSON 片段,路径必须为非负整数,字符串或 `nil` 。
```go
import "github.com/bytedance/sonic"
input := []byte(`{"key1":[{},{"key2":{"key3":[1,2,3]}}]}`)
// no path, returns entire json
root, err := sonic.Get(input)
raw := root.Raw() // == string(input)
// multiple paths
root, err := sonic.Get(input, "key1", 1, "key2")
sub := root.Get("key3").Index(2).Int64() // == 3
```
**注意**:由于 `Index()` 使用偏移量来定位数据,比使用扫描的 `Get()` 要快的多,建议尽可能的使用 `Index` 。 Sonic 也提供了另一个 API, `IndexOrGet()` ,以偏移量为基础并且也确保键的匹配。
#### 修改
使用 `Set()` / `Unset()` 修改 json 的内容
```go
import "github.com/bytedance/sonic"
// Set
exist, err := root.Set("key4", NewBool(true)) // exist == false
alias1 := root.Get("key4")
println(alias1.Valid()) // true
alias2 := root.Index(1)
println(alias1 == alias2) // true
// Unset
exist, err := root.UnsetByIndex(1) // exist == true
println(root.Get("key4").Check()) // "value not exist"
```
#### 序列化
要将 `ast.Node` 编码为 json ,使用 `MarshalJson()` 或者 `json.Marshal()` (必须传递指向节点的指针)
```go
import (
"encoding/json"
"github.com/bytedance/sonic"
)
buf, err := root.MarshalJson()
println(string(buf)) // {"key1":[{},{"key2":{"key3":[1,2,3]}}]}
exp, err := json.Marshal(&root) // WARN: use pointer
println(string(buf) == string(exp)) // true
```
#### APIs
- 合法性检查: `Check()`, `Error()`, `Valid()`, `Exist()`
- 索引: `Index()`, `Get()`, `IndexPair()`, `IndexOrGet()`, `GetByPath()`
- 转换至 go 内置类型: `Int64()`, `Float64()`, `String()`, `Number()`, `Bool()`, `Map[UseNumber|UseNode]()`, `Array[UseNumber|UseNode]()`, `Interface[UseNumber|UseNode]()`
- go 类型打包: `NewRaw()`, `NewNumber()`, `NewNull()`, `NewBool()`, `NewString()`, `NewObject()`, `NewArray()`
- 迭代: `Values()`, `Properties()`, `ForEach()`, `SortKeys()`
- 修改: `Set()`, `SetByIndex()`, `Add()`
### `Ast.Visitor`
Sonic 提供了一个高级的 API 用于直接全量解析 JSON 到非标准容器里 (既不是 `struct` 也不是 `map[string]interface{}`) 且不需要借助任何中间表示 (`ast.Node` 或 `interface{}`)。举个例子,你可能定义了下述的类型,它们看起来像 `interface{}`,但实际上并不是:
```go
type UserNode interface {}
// the following types implement the UserNode interface.
type (
UserNull struct{}
UserBool struct{ Value bool }
UserInt64 struct{ Value int64 }
UserFloat64 struct{ Value float64 }
UserString struct{ Value string }
UserObject struct{ Value map[string]UserNode }
UserArray struct{ Value []UserNode }
)
```
Sonic 提供了下述的 API 来返回 **“对 JSON AST 的前序遍历”**。`ast.Visitor` 是一个 SAX 风格的接口,这在某些 C++ 的 JSON 解析库中被使用到。你需要自己实现一个 `ast.Visitor`,将它传递给 `ast.Preorder()` 方法。在你的实现中你可以使用自定义的类型来表示 JSON 的值。在你的 `ast.Visitor` 中,可能需要有一个 O(n) 空间复杂度的容器(比如说栈)来记录 object / array 的层级。
```go
func Preorder(str string, visitor Visitor, opts *VisitorOptions) error
type Visitor interface {
OnNull() error
OnBool(v bool) error
OnString(v string) error
OnInt64(v int64, n json.Number) error
OnFloat64(v float64, n json.Number) error
OnObjectBegin(capacity int) error
OnObjectKey(key string) error
OnObjectEnd() error
OnArrayBegin(capacity int) error
OnArrayEnd() error
}
```
详细用法参看 [ast/visitor.go](https://github.com/bytedance/sonic/blob/main/ast/visitor.go),我们还为 `UserNode` 实现了一个示例 `ast.Visitor`,你可以在 [ast/visitor_test.go](https://github.com/bytedance/sonic/blob/main/ast/visitor_test.go) 中找到它。
## 兼容性
由于开发高性能代码的困难性, Sonic **不**保证对所有环境的支持。对于在不同环境中使用 Sonic 构建应用程序的开发者,我们有以下建议:
- 在 **Mac M1** 上开发:确保在您的计算机上安装了 Rosetta 2,并在构建时设置 `GOARCH=amd64` 。 Rosetta 2 可以自动将 x86 二进制文件转换为 arm64 二进制文件,并在 Mac M1 上运行 x86 应用程序。
- 在 **Linux arm64** 上开发:您可以安装 qemu 并使用 `qemu-x86_64 -cpu max` 命令来将 x86 二进制文件转换为 arm64 二进制文件。qemu可以实现与Mac M1上的Rosetta 2类似的转换效果。
对于希望在不使用 qemu 下使用 sonic 的开发者,或者希望处理 JSON 时与 `encoding/JSON` 严格保持一致的开发者,我们在 `sonic.API` 中提供了一些兼容性 API
- `ConfigDefault`: 在支持 sonic 的环境下 sonic 的默认配置(`EscapeHTML=false``SortKeys=false`等)。行为与具有相应配置的 `encoding/json` 一致,一些选项,如 `SortKeys=false` 将无效。
- `ConfigStd`: 在支持 sonic 的环境下与标准库兼容的配置(`EscapeHTML=true``SortKeys=true`等)。行为与 `encoding/json` 一致。
- `ConfigFastest`: 在支持 sonic 的环境下运行最快的配置(`NoQuoteTextMarshaler=true`)。行为与具有相应配置的 `encoding/json` 一致,某些选项将无效。
## 注意事项
### 预热
由于 Sonic 使用 [golang-asm](https://github.com/twitchyliquid64/golang-asm) 作为 JIT 汇编器,这个库并不适用于运行时编译,第一次运行一个大型模式可能会导致请求超时甚至进程内存溢出。为了更好地稳定性,我们建议在运行大型模式或在内存有限的应用中,在使用 `Marshal()/Unmarshal()` 前运行 `Pretouch()`。
```go
import (
"reflect"
"github.com/bytedance/sonic"
"github.com/bytedance/sonic/option"
)
func init() {
var v HugeStruct
// For most large types (nesting depth <= option.DefaultMaxInlineDepth)
err := sonic.Pretouch(reflect.TypeOf(v))
// with more CompileOption...
err := sonic.Pretouch(reflect.TypeOf(v),
// If the type is too deep nesting (nesting depth > option.DefaultMaxInlineDepth),
// you can set compile recursive loops in Pretouch for better stability in JIT.
option.WithCompileRecursiveDepth(loop),
// For a large nested struct, try to set a smaller depth to reduce compiling time.
option.WithCompileMaxInlineDepth(depth),
)
}
```
### 拷贝字符串
当解码 **没有转义字符的字符串**时, sonic 会从原始的 JSON 缓冲区内引用而不是复制到新的一个缓冲区中。这对 CPU 的性能方面很有帮助,但是可能因此在解码后对象仍在使用的时候将整个 JSON 缓冲区保留在内存中。实践中我们发现,通过引用 JSON 缓冲区引入的额外内存通常是解码后对象的 20% 至 80% ,一旦应用长期保留这些对象(如缓存以备重用),服务器所使用的内存可能会增加。我们提供了选项 `decoder.CopyString()` 供用户选择,不引用 JSON 缓冲区。这可能在一定程度上降低 CPU 性能。
### 传递字符串还是字节数组?
为了和 `encoding/json` 保持一致,我们提供了传递 `[]byte` 作为参数的 API ,但考虑到安全性,字符串到字节的复制是同时进行的,这在原始 JSON 非常大时可能会导致性能损失。因此,你可以使用 `UnmarshalString()` 和 `GetFromString()` 来传递字符串,只要你的原始数据是字符串,或**零拷贝类型转换**对于你的字节数组是安全的。我们也提供了 `MarshalString()` 的 API ,以便对编码的 JSON 字节数组进行**零拷贝类型转换**,因为 sonic 输出的字节始终是重复并且唯一的,所以这样是安全的。
### 加速 `encoding.TextMarshaler`
为了保证数据安全性, `sonic.Encoder` 默认会对来自 `encoding.TextMarshaler` 接口的字符串进行引用和转义,如果大部分数据都是这种形式那可能会导致很大的性能损失。我们提供了 `encoder.NoQuoteTextMarshaler` 选项来跳过这些操作,但你**必须**保证他们的输出字符串依照 [RFC8259](https://datatracker.ietf.org/doc/html/rfc8259) 进行了转义和引用。
### 泛型的性能优化
在 **完全解析**的场景下, `Unmarshal()` 表现得比 `Get()`+`Node.Interface()` 更好。但是如果你只有特定 JSON 的部分模式,你可以将 `Get()` 和 `Unmarshal()` 结合使用:
```go
import "github.com/bytedance/sonic"
node, err := sonic.GetFromString(_TwitterJson, "statuses", 3, "user")
var user User // your partial schema...
err = sonic.UnmarshalString(node.Raw(), &user)
```
甚至如果你没有任何模式,可以用 `ast.Node` 代替 `map` 或 `interface` 作为泛型的容器:
```go
import "github.com/bytedance/sonic"
root, err := sonic.GetFromString(_TwitterJson)
user := root.GetByPath("statuses", 3, "user") // === root.Get("status").Index(3).Get("user")
err = user.Check()
// err = user.LoadAll() // only call this when you want to use 'user' concurrently...
go someFunc(user)
```
为什么?因为 `ast.Node` 使用 `array` 来存储其子节点:
- 在插入(反序列化)和扫描(序列化)数据时,`Array` 的性能比 `Map` **好得多**
- **哈希**`map[x]`)的效率不如**索引**`array[x]`)高效,而 `ast.Node` 可以在数组和对象上使用索引;
- 使用 `Interface()` / `Map()` 意味着 sonic 必须解析所有的底层值,而 `ast.Node` 可以**按需解析**它们。
**注意**:由于 `ast.Node` 的惰性加载设计,其**不能**直接保证并发安全性,但你可以调用 `Node.Load()` / `Node.LoadAll()` 来实现并发安全。尽管可能会带来性能损失,但仍比转换成 `map` 或 `interface{}` 更为高效。
### 使用 `ast.Node` 还是 `ast.Visitor`
对于泛型数据的解析,`ast.Node` 在大多数场景上应该能够满足你的需求。
然而,`ast.Node` 是一种针对部分解析 JSON 而设计的泛型容器,它包含一些特殊设计,比如惰性加载,如果你希望像 `Unmarshal()` 那样直接解析整个 JSON,这些设计可能并不合适。尽管 `ast.Node` 相较于 `map` 或 `interface{}` 来说是更好的一种泛型容器,但它毕竟也是一种中间表示,如果你的最终类型是自定义的,你还得在解析完成后将上述类型转化成你自定义的类型。
在上述场景中,如果想要有更极致的性能,`ast.Visitor` 会是更好的选择。它采用和 `Unmarshal()` 类似的形式解析 JSON,并且你可以直接使用你的最终类型去表示 JSON AST,而不需要经过额外的任何中间表示。
但是,`ast.Visitor` 并不是一个很易用的 API。你可能需要写大量的代码去实现自己的 `ast.Visitor`,并且需要在解析过程中仔细维护树的层级。如果你决定要使用这个 API,请先仔细阅读 [ast/visitor.go](https://github.com/bytedance/sonic/blob/main/ast/visitor.go) 中的注释。
## 社区
Sonic 是 [CloudWeGo](https://www.cloudwego.io/) 下的一个子项目。我们致力于构建云原生生态系统。
+214
View File
@@ -0,0 +1,214 @@
/*
* Copyright 2021 ByteDance Inc.
*
* 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.
*/
package sonic
import (
`io`
`github.com/bytedance/sonic/ast`
`github.com/bytedance/sonic/internal/rt`
)
// Config is a combination of sonic/encoder.Options and sonic/decoder.Options
type Config struct {
// EscapeHTML indicates encoder to escape all HTML characters
// after serializing into JSON (see https://pkg.go.dev/encoding/json#HTMLEscape).
// WARNING: This hurts performance A LOT, USE WITH CARE.
EscapeHTML bool
// SortMapKeys indicates encoder that the keys of a map needs to be sorted
// before serializing into JSON.
// WARNING: This hurts performance A LOT, USE WITH CARE.
SortMapKeys bool
// CompactMarshaler indicates encoder that the output JSON from json.Marshaler
// is always compact and needs no validation
CompactMarshaler bool
// NoQuoteTextMarshaler indicates encoder that the output text from encoding.TextMarshaler
// is always escaped string and needs no quoting
NoQuoteTextMarshaler bool
// NoNullSliceOrMap indicates encoder that all empty Array or Object are encoded as '[]' or '{}',
// instead of 'null'
NoNullSliceOrMap bool
// UseInt64 indicates decoder to unmarshal an integer into an interface{} as an
// int64 instead of as a float64.
UseInt64 bool
// UseNumber indicates decoder to unmarshal a number into an interface{} as a
// json.Number instead of as a float64.
UseNumber bool
// UseUnicodeErrors indicates decoder to return an error when encounter invalid
// UTF-8 escape sequences.
UseUnicodeErrors bool
// DisallowUnknownFields indicates decoder to return an error when the destination
// is a struct and the input contains object keys which do not match any
// non-ignored, exported fields in the destination.
DisallowUnknownFields bool
// CopyString indicates decoder to decode string values by copying instead of referring.
CopyString bool
// ValidateString indicates decoder and encoder to valid string values: decoder will return errors
// when unescaped control chars(\u0000-\u001f) in the string value of JSON.
ValidateString bool
// NoValidateJSONMarshaler indicates that the encoder should not validate the output string
// after encoding the JSONMarshaler to JSON.
NoValidateJSONMarshaler bool
// NoEncoderNewline indicates that the encoder should not add a newline after every message
NoEncoderNewline bool
}
var (
// ConfigDefault is the default config of APIs, aiming at efficiency and safety.
ConfigDefault = Config{}.Froze()
// ConfigStd is the standard config of APIs, aiming at being compatible with encoding/json.
ConfigStd = Config{
EscapeHTML : true,
SortMapKeys: true,
CompactMarshaler: true,
CopyString : true,
ValidateString : true,
}.Froze()
// ConfigFastest is the fastest config of APIs, aiming at speed.
ConfigFastest = Config{
NoQuoteTextMarshaler: true,
NoValidateJSONMarshaler: true,
}.Froze()
)
// API is a binding of specific config.
// This interface is inspired by github.com/json-iterator/go,
// and has same behaviors under equavilent config.
type API interface {
// MarshalToString returns the JSON encoding string of v
MarshalToString(v interface{}) (string, error)
// Marshal returns the JSON encoding bytes of v.
Marshal(v interface{}) ([]byte, error)
// MarshalIndent returns the JSON encoding bytes with indent and prefix.
MarshalIndent(v interface{}, prefix, indent string) ([]byte, error)
// UnmarshalFromString parses the JSON-encoded bytes and stores the result in the value pointed to by v.
UnmarshalFromString(str string, v interface{}) error
// Unmarshal parses the JSON-encoded string and stores the result in the value pointed to by v.
Unmarshal(data []byte, v interface{}) error
// NewEncoder create a Encoder holding writer
NewEncoder(writer io.Writer) Encoder
// NewDecoder create a Decoder holding reader
NewDecoder(reader io.Reader) Decoder
// Valid validates the JSON-encoded bytes and reports if it is valid
Valid(data []byte) bool
}
// Encoder encodes JSON into io.Writer
type Encoder interface {
// Encode writes the JSON encoding of v to the stream, followed by a newline character.
Encode(val interface{}) error
// SetEscapeHTML specifies whether problematic HTML characters
// should be escaped inside JSON quoted strings.
// The default behavior NOT ESCAPE
SetEscapeHTML(on bool)
// SetIndent instructs the encoder to format each subsequent encoded value
// as if indented by the package-level function Indent(dst, src, prefix, indent).
// Calling SetIndent("", "") disables indentation
SetIndent(prefix, indent string)
}
// Decoder decodes JSON from io.Read
type Decoder interface {
// Decode reads the next JSON-encoded value from its input and stores it in the value pointed to by v.
Decode(val interface{}) error
// Buffered returns a reader of the data remaining in the Decoder's buffer.
// The reader is valid until the next call to Decode.
Buffered() io.Reader
// DisallowUnknownFields causes the Decoder to return an error when the destination is a struct
// and the input contains object keys which do not match any non-ignored, exported fields in the destination.
DisallowUnknownFields()
// More reports whether there is another element in the current array or object being parsed.
More() bool
// UseNumber causes the Decoder to unmarshal a number into an interface{} as a Number instead of as a float64.
UseNumber()
}
// Marshal returns the JSON encoding bytes of v.
func Marshal(val interface{}) ([]byte, error) {
return ConfigDefault.Marshal(val)
}
// MarshalString returns the JSON encoding string of v.
func MarshalString(val interface{}) (string, error) {
return ConfigDefault.MarshalToString(val)
}
// Unmarshal parses the JSON-encoded data and stores the result in the value pointed to by v.
// NOTICE: This API copies given buffer by default,
// if you want to pass JSON more efficiently, use UnmarshalString instead.
func Unmarshal(buf []byte, val interface{}) error {
return ConfigDefault.Unmarshal(buf, val)
}
// UnmarshalString is like Unmarshal, except buf is a string.
func UnmarshalString(buf string, val interface{}) error {
return ConfigDefault.UnmarshalFromString(buf, val)
}
// Get searches and locates the given path from src json,
// and returns a ast.Node representing the partially json.
//
// Each path arg must be integer or string:
// - Integer is target index(>=0), means searching current node as array.
// - String is target key, means searching current node as object.
//
//
// Notice: It expects the src json is **Well-formed** and **Immutable** when calling,
// otherwise it may return unexpected result.
// Considering memory safety, the returned JSON is **Copied** from the input
func Get(src []byte, path ...interface{}) (ast.Node, error) {
return GetCopyFromString(rt.Mem2Str(src), path...)
}
// GetFromString is same with Get except src is string.
//
// WARNING: The returned JSON is **Referenced** from the input.
// Caching or long-time holding the returned node may cause OOM.
// If your src is big, consider use GetFromStringCopy().
func GetFromString(src string, path ...interface{}) (ast.Node, error) {
return ast.NewSearcher(src).GetByPath(path...)
}
// GetCopyFromString is same with Get except src is string
func GetCopyFromString(src string, path ...interface{}) (ast.Node, error) {
return ast.NewSearcher(src).GetByPathCopy(path...)
}
// Valid reports whether data is a valid JSON encoding.
func Valid(data []byte) bool {
return ConfigDefault.Valid(data)
}
// Valid reports whether data is a valid JSON encoding.
func ValidString(data string) bool {
return ConfigDefault.Valid(rt.Str2Mem(data))
}
+135
View File
@@ -0,0 +1,135 @@
//go:build (amd64 && go1.16 && !go1.23) || (arm64 && go1.20 && !go1.23)
// +build amd64,go1.16,!go1.23 arm64,go1.20,!go1.23
/*
* Copyright 2022 ByteDance Inc.
*
* 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.
*/
package ast
import (
`runtime`
`unsafe`
`github.com/bytedance/sonic/encoder`
`github.com/bytedance/sonic/internal/native`
`github.com/bytedance/sonic/internal/native/types`
`github.com/bytedance/sonic/internal/rt`
uq `github.com/bytedance/sonic/unquote`
`github.com/bytedance/sonic/utf8`
)
var typeByte = rt.UnpackEface(byte(0)).Type
//go:nocheckptr
func quote(buf *[]byte, val string) {
*buf = append(*buf, '"')
if len(val) == 0 {
*buf = append(*buf, '"')
return
}
sp := rt.IndexChar(val, 0)
nb := len(val)
b := (*rt.GoSlice)(unsafe.Pointer(buf))
// input buffer
for nb > 0 {
// output buffer
dp := unsafe.Pointer(uintptr(b.Ptr) + uintptr(b.Len))
dn := b.Cap - b.Len
// call native.Quote, dn is byte count it outputs
ret := native.Quote(sp, nb, dp, &dn, 0)
// update *buf length
b.Len += dn
// no need more output
if ret >= 0 {
break
}
// double buf size
*b = growslice(typeByte, *b, b.Cap*2)
// ret is the complement of consumed input
ret = ^ret
// update input buffer
nb -= ret
sp = unsafe.Pointer(uintptr(sp) + uintptr(ret))
}
runtime.KeepAlive(buf)
runtime.KeepAlive(sp)
*buf = append(*buf, '"')
}
func unquote(src string) (string, types.ParsingError) {
return uq.String(src)
}
func (self *Parser) decodeValue() (val types.JsonState) {
sv := (*rt.GoString)(unsafe.Pointer(&self.s))
flag := types.F_USE_NUMBER
if self.dbuf != nil {
flag = 0
val.Dbuf = self.dbuf
val.Dcap = types.MaxDigitNums
}
self.p = native.Value(sv.Ptr, sv.Len, self.p, &val, uint64(flag))
return
}
func (self *Parser) skip() (int, types.ParsingError) {
fsm := types.NewStateMachine()
start := native.SkipOne(&self.s, &self.p, fsm, 0)
types.FreeStateMachine(fsm)
if start < 0 {
return self.p, types.ParsingError(-start)
}
return start, 0
}
func (self *Node) encodeInterface(buf *[]byte) error {
//WARN: NOT compatible with json.Encoder
return encoder.EncodeInto(buf, self.packAny(), encoder.NoEncoderNewline)
}
func (self *Parser) skipFast() (int, types.ParsingError) {
start := native.SkipOneFast(&self.s, &self.p)
if start < 0 {
return self.p, types.ParsingError(-start)
}
return start, 0
}
func (self *Parser) getByPath(validate bool, path ...interface{}) (int, types.ParsingError) {
var fsm *types.StateMachine
if validate {
fsm = types.NewStateMachine()
}
start := native.GetByPath(&self.s, &self.p, &path, fsm)
if validate {
types.FreeStateMachine(fsm)
}
runtime.KeepAlive(path)
if start < 0 {
return self.p, types.ParsingError(-start)
}
return start, 0
}
func validate_utf8(str string) bool {
return utf8.ValidateString(str)
}
+114
View File
@@ -0,0 +1,114 @@
// +build !amd64,!arm64 go1.23 !go1.16 arm64,!go1.20
/*
* Copyright 2022 ByteDance Inc.
*
* 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.
*/
package ast
import (
`encoding/json`
`unicode/utf8`
`github.com/bytedance/sonic/internal/native/types`
`github.com/bytedance/sonic/internal/rt`
)
func init() {
println("WARNING:(ast) sonic only supports Go1.16~1.22, but your environment is not suitable")
}
func quote(buf *[]byte, val string) {
quoteString(buf, val)
}
// unquote unescapes a internal JSON string (it doesn't count quotas at the begining and end)
func unquote(src string) (string, types.ParsingError) {
sp := rt.IndexChar(src, -1)
out, ok := unquoteBytes(rt.BytesFrom(sp, len(src)+2, len(src)+2))
if !ok {
return "", types.ERR_INVALID_ESCAPE
}
return rt.Mem2Str(out), 0
}
func (self *Parser) decodeValue() (val types.JsonState) {
e, v := decodeValue(self.s, self.p, self.dbuf == nil)
if e < 0 {
return v
}
self.p = e
return v
}
func (self *Parser) skip() (int, types.ParsingError) {
e, s := skipValue(self.s, self.p)
if e < 0 {
return self.p, types.ParsingError(-e)
}
self.p = e
return s, 0
}
func (self *Parser) skipFast() (int, types.ParsingError) {
e, s := skipValueFast(self.s, self.p)
if e < 0 {
return self.p, types.ParsingError(-e)
}
self.p = e
return s, 0
}
func (self *Node) encodeInterface(buf *[]byte) error {
out, err := json.Marshal(self.packAny())
if err != nil {
return err
}
*buf = append(*buf, out...)
return nil
}
func (self *Parser) getByPath(validate bool, path ...interface{}) (int, types.ParsingError) {
for _, p := range path {
if idx, ok := p.(int); ok && idx >= 0 {
if err := self.searchIndex(idx); err != 0 {
return self.p, err
}
} else if key, ok := p.(string); ok {
if err := self.searchKey(key); err != 0 {
return self.p, err
}
} else {
panic("path must be either int(>=0) or string")
}
}
var start int
var e types.ParsingError
if validate {
start, e = self.skip()
} else {
start, e = self.skipFast()
}
if e != 0 {
return self.p, e
}
return start, 0
}
func validate_utf8(str string) bool {
return utf8.ValidString(str)
}
View File
+31
View File
@@ -0,0 +1,31 @@
// +build amd64,go1.16
/**
* Copyright 2023 ByteDance Inc.
*
* 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.
*/
package ast
import (
`github.com/cloudwego/base64x`
)
func decodeBase64(src string) ([]byte, error) {
return base64x.StdEncoding.DecodeString(src)
}
func encodeBase64(src []byte) string {
return base64x.StdEncoding.EncodeToString(src)
}
+31
View File
@@ -0,0 +1,31 @@
// +build !amd64 !go1.16
/*
* Copyright 2022 ByteDance Inc.
*
* 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.
*/
package ast
import (
`encoding/base64`
)
func decodeBase64(src string) ([]byte, error) {
return base64.StdEncoding.DecodeString(src)
}
func encodeBase64(src []byte) string {
return base64.StdEncoding.EncodeToString(src)
}
+409
View File
@@ -0,0 +1,409 @@
/**
* Copyright 2023 ByteDance Inc.
*
* 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.
*/
package ast
import (
`sort`
`unsafe`
)
type nodeChunk [_DEFAULT_NODE_CAP]Node
type linkedNodes struct {
head nodeChunk
tail []*nodeChunk
size int
}
func (self *linkedNodes) Cap() int {
if self == nil {
return 0
}
return (len(self.tail)+1)*_DEFAULT_NODE_CAP
}
func (self *linkedNodes) Len() int {
if self == nil {
return 0
}
return self.size
}
func (self *linkedNodes) At(i int) (*Node) {
if self == nil {
return nil
}
if i >= 0 && i<self.size && i < _DEFAULT_NODE_CAP {
return &self.head[i]
} else if i >= _DEFAULT_NODE_CAP && i<self.size {
a, b := i/_DEFAULT_NODE_CAP-1, i%_DEFAULT_NODE_CAP
if a < len(self.tail) {
return &self.tail[a][b]
}
}
return nil
}
func (self *linkedNodes) MoveOne(source int, target int) {
if source == target {
return
}
if source < 0 || source >= self.size || target < 0 || target >= self.size {
return
}
// reserve source
n := *self.At(source)
if source < target {
// move every element (source,target] one step back
for i:=source; i<target; i++ {
*self.At(i) = *self.At(i+1)
}
} else {
// move every element [target,source) one step forward
for i:=source; i>target; i-- {
*self.At(i) = *self.At(i-1)
}
}
// set target
*self.At(target) = n
}
func (self *linkedNodes) Pop() {
if self == nil || self.size == 0 {
return
}
self.Set(self.size-1, Node{})
self.size--
}
func (self *linkedPairs) Pop() {
if self == nil || self.size == 0 {
return
}
self.Set(self.size-1, Pair{})
self.size--
}
func (self *linkedNodes) Push(v Node) {
self.Set(self.size, v)
}
func (self *linkedNodes) Set(i int, v Node) {
if i < _DEFAULT_NODE_CAP {
self.head[i] = v
if self.size <= i {
self.size = i+1
}
return
}
a, b := i/_DEFAULT_NODE_CAP-1, i%_DEFAULT_NODE_CAP
if a < 0 {
self.head[b] = v
} else {
self.growTailLength(a+1)
var n = &self.tail[a]
if *n == nil {
*n = new(nodeChunk)
}
(*n)[b] = v
}
if self.size <= i {
self.size = i+1
}
}
func (self *linkedNodes) growTailLength(l int) {
if l <= len(self.tail) {
return
}
c := cap(self.tail)
for c < l {
c += 1 + c>>_APPEND_GROW_SHIFT
}
if c == cap(self.tail) {
self.tail = self.tail[:l]
return
}
tmp := make([]*nodeChunk, l, c)
copy(tmp, self.tail)
self.tail = tmp
}
func (self *linkedNodes) ToSlice(con []Node) {
if len(con) < self.size {
return
}
i := (self.size-1)
a, b := i/_DEFAULT_NODE_CAP-1, i%_DEFAULT_NODE_CAP
if a < 0 {
copy(con, self.head[:b+1])
return
} else {
copy(con, self.head[:])
con = con[_DEFAULT_NODE_CAP:]
}
for i:=0; i<a; i++ {
copy(con, self.tail[i][:])
con = con[_DEFAULT_NODE_CAP:]
}
copy(con, self.tail[a][:b+1])
}
func (self *linkedNodes) FromSlice(con []Node) {
self.size = len(con)
i := self.size-1
a, b := i/_DEFAULT_NODE_CAP-1, i%_DEFAULT_NODE_CAP
if a < 0 {
copy(self.head[:b+1], con)
return
} else {
copy(self.head[:], con)
con = con[_DEFAULT_NODE_CAP:]
}
if cap(self.tail) <= a {
c := (a+1) + (a+1)>>_APPEND_GROW_SHIFT
self.tail = make([]*nodeChunk, a+1, c)
}
self.tail = self.tail[:a+1]
for i:=0; i<a; i++ {
self.tail[i] = new(nodeChunk)
copy(self.tail[i][:], con)
con = con[_DEFAULT_NODE_CAP:]
}
self.tail[a] = new(nodeChunk)
copy(self.tail[a][:b+1], con)
}
type pairChunk [_DEFAULT_NODE_CAP]Pair
type linkedPairs struct {
head pairChunk
tail []*pairChunk
size int
}
func (self *linkedPairs) Cap() int {
if self == nil {
return 0
}
return (len(self.tail)+1)*_DEFAULT_NODE_CAP
}
func (self *linkedPairs) Len() int {
if self == nil {
return 0
}
return self.size
}
func (self *linkedPairs) At(i int) *Pair {
if self == nil {
return nil
}
if i >= 0 && i < _DEFAULT_NODE_CAP && i<self.size {
return &self.head[i]
} else if i >= _DEFAULT_NODE_CAP && i<self.size {
a, b := i/_DEFAULT_NODE_CAP-1, i%_DEFAULT_NODE_CAP
if a < len(self.tail) {
return &self.tail[a][b]
}
}
return nil
}
func (self *linkedPairs) Push(v Pair) {
self.Set(self.size, v)
}
func (self *linkedPairs) Set(i int, v Pair) {
if i < _DEFAULT_NODE_CAP {
self.head[i] = v
if self.size <= i {
self.size = i+1
}
return
}
a, b := i/_DEFAULT_NODE_CAP-1, i%_DEFAULT_NODE_CAP
if a < 0 {
self.head[b] = v
} else {
self.growTailLength(a+1)
var n = &self.tail[a]
if *n == nil {
*n = new(pairChunk)
}
(*n)[b] = v
}
if self.size <= i {
self.size = i+1
}
}
func (self *linkedPairs) growTailLength(l int) {
if l <= len(self.tail) {
return
}
c := cap(self.tail)
for c < l {
c += 1 + c>>_APPEND_GROW_SHIFT
}
if c == cap(self.tail) {
self.tail = self.tail[:l]
return
}
tmp := make([]*pairChunk, l, c)
copy(tmp, self.tail)
self.tail = tmp
}
// linear search
func (self *linkedPairs) Get(key string) (*Pair, int) {
for i:=0; i<self.size; i++ {
if n := self.At(i); n.Key == key {
return n, i
}
}
return nil, -1
}
func (self *linkedPairs) ToSlice(con []Pair) {
if len(con) < self.size {
return
}
i := self.size-1
a, b := i/_DEFAULT_NODE_CAP-1, i%_DEFAULT_NODE_CAP
if a < 0 {
copy(con, self.head[:b+1])
return
} else {
copy(con, self.head[:])
con = con[_DEFAULT_NODE_CAP:]
}
for i:=0; i<a; i++ {
copy(con, self.tail[i][:])
con = con[_DEFAULT_NODE_CAP:]
}
copy(con, self.tail[a][:b+1])
}
func (self *linkedPairs) ToMap(con map[string]Node) {
for i:=0; i<self.size; i++ {
n := self.At(i)
con[n.Key] = n.Value
}
}
func (self *linkedPairs) FromSlice(con []Pair) {
self.size = len(con)
i := self.size-1
a, b := i/_DEFAULT_NODE_CAP-1, i%_DEFAULT_NODE_CAP
if a < 0 {
copy(self.head[:b+1], con)
return
} else {
copy(self.head[:], con)
con = con[_DEFAULT_NODE_CAP:]
}
if cap(self.tail) <= a {
c := (a+1) + (a+1)>>_APPEND_GROW_SHIFT
self.tail = make([]*pairChunk, a+1, c)
}
self.tail = self.tail[:a+1]
for i:=0; i<a; i++ {
self.tail[i] = new(pairChunk)
copy(self.tail[i][:], con)
con = con[_DEFAULT_NODE_CAP:]
}
self.tail[a] = new(pairChunk)
copy(self.tail[a][:b+1], con)
}
func (self *linkedPairs) Less(i, j int) bool {
return lessFrom(self.At(i).Key, self.At(j).Key, 0)
}
func (self *linkedPairs) Swap(i, j int) {
a, b := self.At(i), self.At(j)
*a, *b = *b, *a
}
func (self *linkedPairs) Sort() {
sort.Stable(self)
}
// Compare two strings from the pos d.
func lessFrom(a, b string, d int) bool {
l := len(a)
if l > len(b) {
l = len(b)
}
for i := d; i < l; i++ {
if a[i] == b[i] {
continue
}
return a[i] < b[i]
}
return len(a) < len(b)
}
type parseObjectStack struct {
parser Parser
v linkedPairs
}
type parseArrayStack struct {
parser Parser
v linkedNodes
}
func newLazyArray(p *Parser) Node {
s := new(parseArrayStack)
s.parser = *p
return Node{
t: _V_ARRAY_LAZY,
p: unsafe.Pointer(s),
}
}
func newLazyObject(p *Parser) Node {
s := new(parseObjectStack)
s.parser = *p
return Node{
t: _V_OBJECT_LAZY,
p: unsafe.Pointer(s),
}
}
func (self *Node) getParserAndArrayStack() (*Parser, *parseArrayStack) {
stack := (*parseArrayStack)(self.p)
return &stack.parser, stack
}
func (self *Node) getParserAndObjectStack() (*Parser, *parseObjectStack) {
stack := (*parseObjectStack)(self.p)
return &stack.parser, stack
}
+618
View File
@@ -0,0 +1,618 @@
/*
* Copyright 2022 ByteDance Inc.
*
* 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.
*/
package ast
import (
`encoding/base64`
`runtime`
`strconv`
`unsafe`
`github.com/bytedance/sonic/internal/native/types`
`github.com/bytedance/sonic/internal/rt`
)
const _blankCharsMask = (1 << ' ') | (1 << '\t') | (1 << '\r') | (1 << '\n')
const (
bytesNull = "null"
bytesTrue = "true"
bytesFalse = "false"
bytesObject = "{}"
bytesArray = "[]"
)
func isSpace(c byte) bool {
return (int(1<<c) & _blankCharsMask) != 0
}
//go:nocheckptr
func skipBlank(src string, pos int) int {
se := uintptr(rt.IndexChar(src, len(src)))
sp := uintptr(rt.IndexChar(src, pos))
for sp < se {
if !isSpace(*(*byte)(unsafe.Pointer(sp))) {
break
}
sp += 1
}
if sp >= se {
return -int(types.ERR_EOF)
}
runtime.KeepAlive(src)
return int(sp - uintptr(rt.IndexChar(src, 0)))
}
func decodeNull(src string, pos int) (ret int) {
ret = pos + 4
if ret > len(src) {
return -int(types.ERR_EOF)
}
if src[pos:ret] == bytesNull {
return ret
} else {
return -int(types.ERR_INVALID_CHAR)
}
}
func decodeTrue(src string, pos int) (ret int) {
ret = pos + 4
if ret > len(src) {
return -int(types.ERR_EOF)
}
if src[pos:ret] == bytesTrue {
return ret
} else {
return -int(types.ERR_INVALID_CHAR)
}
}
func decodeFalse(src string, pos int) (ret int) {
ret = pos + 5
if ret > len(src) {
return -int(types.ERR_EOF)
}
if src[pos:ret] == bytesFalse {
return ret
}
return -int(types.ERR_INVALID_CHAR)
}
//go:nocheckptr
func decodeString(src string, pos int) (ret int, v string) {
ret, ep := skipString(src, pos)
if ep == -1 {
(*rt.GoString)(unsafe.Pointer(&v)).Ptr = rt.IndexChar(src, pos+1)
(*rt.GoString)(unsafe.Pointer(&v)).Len = ret - pos - 2
return ret, v
}
vv, ok := unquoteBytes(rt.Str2Mem(src[pos:ret]))
if !ok {
return -int(types.ERR_INVALID_CHAR), ""
}
runtime.KeepAlive(src)
return ret, rt.Mem2Str(vv)
}
func decodeBinary(src string, pos int) (ret int, v []byte) {
var vv string
ret, vv = decodeString(src, pos)
if ret < 0 {
return ret, nil
}
var err error
v, err = base64.StdEncoding.DecodeString(vv)
if err != nil {
return -int(types.ERR_INVALID_CHAR), nil
}
return ret, v
}
func isDigit(c byte) bool {
return c >= '0' && c <= '9'
}
//go:nocheckptr
func decodeInt64(src string, pos int) (ret int, v int64, err error) {
sp := uintptr(rt.IndexChar(src, pos))
ss := uintptr(sp)
se := uintptr(rt.IndexChar(src, len(src)))
if uintptr(sp) >= se {
return -int(types.ERR_EOF), 0, nil
}
if c := *(*byte)(unsafe.Pointer(sp)); c == '-' {
sp += 1
}
if sp == se {
return -int(types.ERR_EOF), 0, nil
}
for ; sp < se; sp += uintptr(1) {
if !isDigit(*(*byte)(unsafe.Pointer(sp))) {
break
}
}
if sp < se {
if c := *(*byte)(unsafe.Pointer(sp)); c == '.' || c == 'e' || c == 'E' {
return -int(types.ERR_INVALID_NUMBER_FMT), 0, nil
}
}
var vv string
ret = int(uintptr(sp) - uintptr((*rt.GoString)(unsafe.Pointer(&src)).Ptr))
(*rt.GoString)(unsafe.Pointer(&vv)).Ptr = unsafe.Pointer(ss)
(*rt.GoString)(unsafe.Pointer(&vv)).Len = ret - pos
v, err = strconv.ParseInt(vv, 10, 64)
if err != nil {
//NOTICE: allow overflow here
if err.(*strconv.NumError).Err == strconv.ErrRange {
return ret, 0, err
}
return -int(types.ERR_INVALID_CHAR), 0, err
}
runtime.KeepAlive(src)
return ret, v, nil
}
func isNumberChars(c byte) bool {
return (c >= '0' && c <= '9') || c == '+' || c == '-' || c == 'e' || c == 'E' || c == '.'
}
//go:nocheckptr
func decodeFloat64(src string, pos int) (ret int, v float64, err error) {
sp := uintptr(rt.IndexChar(src, pos))
ss := uintptr(sp)
se := uintptr(rt.IndexChar(src, len(src)))
if uintptr(sp) >= se {
return -int(types.ERR_EOF), 0, nil
}
if c := *(*byte)(unsafe.Pointer(sp)); c == '-' {
sp += 1
}
if sp == se {
return -int(types.ERR_EOF), 0, nil
}
for ; sp < se; sp += uintptr(1) {
if !isNumberChars(*(*byte)(unsafe.Pointer(sp))) {
break
}
}
var vv string
ret = int(uintptr(sp) - uintptr((*rt.GoString)(unsafe.Pointer(&src)).Ptr))
(*rt.GoString)(unsafe.Pointer(&vv)).Ptr = unsafe.Pointer(ss)
(*rt.GoString)(unsafe.Pointer(&vv)).Len = ret - pos
v, err = strconv.ParseFloat(vv, 64)
if err != nil {
//NOTICE: allow overflow here
if err.(*strconv.NumError).Err == strconv.ErrRange {
return ret, 0, err
}
return -int(types.ERR_INVALID_CHAR), 0, err
}
runtime.KeepAlive(src)
return ret, v, nil
}
func decodeValue(src string, pos int, skipnum bool) (ret int, v types.JsonState) {
pos = skipBlank(src, pos)
if pos < 0 {
return pos, types.JsonState{Vt: types.ValueType(pos)}
}
switch c := src[pos]; c {
case 'n':
ret = decodeNull(src, pos)
if ret < 0 {
return ret, types.JsonState{Vt: types.ValueType(ret)}
}
return ret, types.JsonState{Vt: types.V_NULL}
case '"':
var ep int
ret, ep = skipString(src, pos)
if ret < 0 {
return ret, types.JsonState{Vt: types.ValueType(ret)}
}
return ret, types.JsonState{Vt: types.V_STRING, Iv: int64(pos + 1), Ep: ep}
case '{':
return pos + 1, types.JsonState{Vt: types.V_OBJECT}
case '[':
return pos + 1, types.JsonState{Vt: types.V_ARRAY}
case 't':
ret = decodeTrue(src, pos)
if ret < 0 {
return ret, types.JsonState{Vt: types.ValueType(ret)}
}
return ret, types.JsonState{Vt: types.V_TRUE}
case 'f':
ret = decodeFalse(src, pos)
if ret < 0 {
return ret, types.JsonState{Vt: types.ValueType(ret)}
}
return ret, types.JsonState{Vt: types.V_FALSE}
case '-', '+', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
if skipnum {
ret = skipNumber(src, pos)
if ret >= 0 {
return ret, types.JsonState{Vt: types.V_DOUBLE, Iv: 0, Ep: pos}
} else {
return ret, types.JsonState{Vt: types.ValueType(ret)}
}
} else {
var iv int64
ret, iv, _ = decodeInt64(src, pos)
if ret >= 0 {
return ret, types.JsonState{Vt: types.V_INTEGER, Iv: iv, Ep: pos}
} else if ret != -int(types.ERR_INVALID_NUMBER_FMT) {
return ret, types.JsonState{Vt: types.ValueType(ret)}
}
var fv float64
ret, fv, _ = decodeFloat64(src, pos)
if ret >= 0 {
return ret, types.JsonState{Vt: types.V_DOUBLE, Dv: fv, Ep: pos}
} else {
return ret, types.JsonState{Vt: types.ValueType(ret)}
}
}
default:
return -int(types.ERR_INVALID_CHAR), types.JsonState{Vt:-types.ValueType(types.ERR_INVALID_CHAR)}
}
}
//go:nocheckptr
func skipNumber(src string, pos int) (ret int) {
sp := uintptr(rt.IndexChar(src, pos))
se := uintptr(rt.IndexChar(src, len(src)))
if uintptr(sp) >= se {
return -int(types.ERR_EOF)
}
if c := *(*byte)(unsafe.Pointer(sp)); c == '-' {
sp += 1
}
ss := sp
var pointer bool
var exponent bool
var lastIsDigit bool
var nextNeedDigit = true
for ; sp < se; sp += uintptr(1) {
c := *(*byte)(unsafe.Pointer(sp))
if isDigit(c) {
lastIsDigit = true
nextNeedDigit = false
continue
} else if nextNeedDigit {
return -int(types.ERR_INVALID_CHAR)
} else if c == '.' {
if !lastIsDigit || pointer || exponent || sp == ss {
return -int(types.ERR_INVALID_CHAR)
}
pointer = true
lastIsDigit = false
nextNeedDigit = true
continue
} else if c == 'e' || c == 'E' {
if !lastIsDigit || exponent {
return -int(types.ERR_INVALID_CHAR)
}
if sp == se-1 {
return -int(types.ERR_EOF)
}
exponent = true
lastIsDigit = false
nextNeedDigit = false
continue
} else if c == '-' || c == '+' {
if prev := *(*byte)(unsafe.Pointer(sp - 1)); prev != 'e' && prev != 'E' {
return -int(types.ERR_INVALID_CHAR)
}
lastIsDigit = false
nextNeedDigit = true
continue
} else {
break
}
}
if nextNeedDigit {
return -int(types.ERR_EOF)
}
runtime.KeepAlive(src)
return int(uintptr(sp) - uintptr((*rt.GoString)(unsafe.Pointer(&src)).Ptr))
}
//go:nocheckptr
func skipString(src string, pos int) (ret int, ep int) {
if pos+1 >= len(src) {
return -int(types.ERR_EOF), -1
}
sp := uintptr(rt.IndexChar(src, pos))
se := uintptr(rt.IndexChar(src, len(src)))
// not start with quote
if *(*byte)(unsafe.Pointer(sp)) != '"' {
return -int(types.ERR_INVALID_CHAR), -1
}
sp += 1
ep = -1
for sp < se {
c := *(*byte)(unsafe.Pointer(sp))
if c == '\\' {
if ep == -1 {
ep = int(uintptr(sp) - uintptr((*rt.GoString)(unsafe.Pointer(&src)).Ptr))
}
sp += 2
continue
}
sp += 1
if c == '"' {
return int(uintptr(sp) - uintptr((*rt.GoString)(unsafe.Pointer(&src)).Ptr)), ep
}
}
runtime.KeepAlive(src)
// not found the closed quote until EOF
return -int(types.ERR_EOF), -1
}
//go:nocheckptr
func skipPair(src string, pos int, lchar byte, rchar byte) (ret int) {
if pos+1 >= len(src) {
return -int(types.ERR_EOF)
}
sp := uintptr(rt.IndexChar(src, pos))
se := uintptr(rt.IndexChar(src, len(src)))
if *(*byte)(unsafe.Pointer(sp)) != lchar {
return -int(types.ERR_INVALID_CHAR)
}
sp += 1
nbrace := 1
inquote := false
for sp < se {
c := *(*byte)(unsafe.Pointer(sp))
if c == '\\' {
sp += 2
continue
} else if c == '"' {
inquote = !inquote
} else if c == lchar {
if !inquote {
nbrace += 1
}
} else if c == rchar {
if !inquote {
nbrace -= 1
if nbrace == 0 {
sp += 1
break
}
}
}
sp += 1
}
if nbrace != 0 {
return -int(types.ERR_INVALID_CHAR)
}
runtime.KeepAlive(src)
return int(uintptr(sp) - uintptr((*rt.GoString)(unsafe.Pointer(&src)).Ptr))
}
func skipValueFast(src string, pos int) (ret int, start int) {
pos = skipBlank(src, pos)
if pos < 0 {
return pos, -1
}
switch c := src[pos]; c {
case 'n':
ret = decodeNull(src, pos)
case '"':
ret, _ = skipString(src, pos)
case '{':
ret = skipPair(src, pos, '{', '}')
case '[':
ret = skipPair(src, pos, '[', ']')
case 't':
ret = decodeTrue(src, pos)
case 'f':
ret = decodeFalse(src, pos)
case '-', '+', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
ret = skipNumber(src, pos)
default:
ret = -int(types.ERR_INVALID_CHAR)
}
return ret, pos
}
func skipValue(src string, pos int) (ret int, start int) {
pos = skipBlank(src, pos)
if pos < 0 {
return pos, -1
}
switch c := src[pos]; c {
case 'n':
ret = decodeNull(src, pos)
case '"':
ret, _ = skipString(src, pos)
case '{':
ret, _ = skipObject(src, pos)
case '[':
ret, _ = skipArray(src, pos)
case 't':
ret = decodeTrue(src, pos)
case 'f':
ret = decodeFalse(src, pos)
case '-', '+', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
ret = skipNumber(src, pos)
default:
ret = -int(types.ERR_INVALID_CHAR)
}
return ret, pos
}
func skipObject(src string, pos int) (ret int, start int) {
start = skipBlank(src, pos)
if start < 0 {
return start, -1
}
if src[start] != '{' {
return -int(types.ERR_INVALID_CHAR), -1
}
pos = start + 1
pos = skipBlank(src, pos)
if pos < 0 {
return pos, -1
}
if src[pos] == '}' {
return pos + 1, start
}
for {
pos, _ = skipString(src, pos)
if pos < 0 {
return pos, -1
}
pos = skipBlank(src, pos)
if pos < 0 {
return pos, -1
}
if src[pos] != ':' {
return -int(types.ERR_INVALID_CHAR), -1
}
pos++
pos, _ = skipValue(src, pos)
if pos < 0 {
return pos, -1
}
pos = skipBlank(src, pos)
if pos < 0 {
return pos, -1
}
if src[pos] == '}' {
return pos + 1, start
}
if src[pos] != ',' {
return -int(types.ERR_INVALID_CHAR), -1
}
pos++
pos = skipBlank(src, pos)
if pos < 0 {
return pos, -1
}
}
}
func skipArray(src string, pos int) (ret int, start int) {
start = skipBlank(src, pos)
if start < 0 {
return start, -1
}
if src[start] != '[' {
return -int(types.ERR_INVALID_CHAR), -1
}
pos = start + 1
pos = skipBlank(src, pos)
if pos < 0 {
return pos, -1
}
if src[pos] == ']' {
return pos + 1, start
}
for {
pos, _ = skipValue(src, pos)
if pos < 0 {
return pos, -1
}
pos = skipBlank(src, pos)
if pos < 0 {
return pos, -1
}
if src[pos] == ']' {
return pos + 1, start
}
if src[pos] != ',' {
return -int(types.ERR_INVALID_CHAR), -1
}
pos++
}
}
// DecodeString decodes a JSON string from pos and return golang string.
// - needEsc indicates if to unescaped escaping chars
// - hasEsc tells if the returned string has escaping chars
// - validStr enables validating UTF8 charset
//
func _DecodeString(src string, pos int, needEsc bool, validStr bool) (v string, ret int, hasEsc bool) {
p := NewParserObj(src)
p.p = pos
switch val := p.decodeValue(); val.Vt {
case types.V_STRING:
str := p.s[val.Iv : p.p-1]
if validStr && !validate_utf8(str) {
return "", -int(types.ERR_INVALID_UTF8), false
}
/* fast path: no escape sequence */
if val.Ep == -1 {
return str, p.p, false
} else if !needEsc {
return str, p.p, true
}
/* unquote the string */
out, err := unquote(str)
/* check for errors */
if err != 0 {
return "", -int(err), true
} else {
return out, p.p, true
}
default:
return "", -int(_ERR_UNSUPPORT_TYPE), false
}
}
+259
View File
@@ -0,0 +1,259 @@
/*
* Copyright 2021 ByteDance Inc.
*
* 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.
*/
package ast
import (
`sync`
`unicode/utf8`
)
const (
_MaxBuffer = 1024 // 1KB buffer size
)
func quoteString(e *[]byte, s string) {
*e = append(*e, '"')
start := 0
for i := 0; i < len(s); {
if b := s[i]; b < utf8.RuneSelf {
if safeSet[b] {
i++
continue
}
if start < i {
*e = append(*e, s[start:i]...)
}
*e = append(*e, '\\')
switch b {
case '\\', '"':
*e = append(*e, b)
case '\n':
*e = append(*e, 'n')
case '\r':
*e = append(*e, 'r')
case '\t':
*e = append(*e, 't')
default:
// This encodes bytes < 0x20 except for \t, \n and \r.
// If escapeHTML is set, it also escapes <, >, and &
// because they can lead to security holes when
// user-controlled strings are rendered into JSON
// and served to some browsers.
*e = append(*e, `u00`...)
*e = append(*e, hex[b>>4])
*e = append(*e, hex[b&0xF])
}
i++
start = i
continue
}
c, size := utf8.DecodeRuneInString(s[i:])
// if c == utf8.RuneError && size == 1 {
// if start < i {
// e.Write(s[start:i])
// }
// e.WriteString(`\ufffd`)
// i += size
// start = i
// continue
// }
if c == '\u2028' || c == '\u2029' {
if start < i {
*e = append(*e, s[start:i]...)
}
*e = append(*e, `\u202`...)
*e = append(*e, hex[c&0xF])
i += size
start = i
continue
}
i += size
}
if start < len(s) {
*e = append(*e, s[start:]...)
}
*e = append(*e, '"')
}
var bytesPool = sync.Pool{}
func (self *Node) MarshalJSON() ([]byte, error) {
buf := newBuffer()
err := self.encode(buf)
if err != nil {
freeBuffer(buf)
return nil, err
}
ret := make([]byte, len(*buf))
copy(ret, *buf)
freeBuffer(buf)
return ret, err
}
func newBuffer() *[]byte {
if ret := bytesPool.Get(); ret != nil {
return ret.(*[]byte)
} else {
buf := make([]byte, 0, _MaxBuffer)
return &buf
}
}
func freeBuffer(buf *[]byte) {
*buf = (*buf)[:0]
bytesPool.Put(buf)
}
func (self *Node) encode(buf *[]byte) error {
if self.IsRaw() {
return self.encodeRaw(buf)
}
switch self.Type() {
case V_NONE : return ErrNotExist
case V_ERROR : return self.Check()
case V_NULL : return self.encodeNull(buf)
case V_TRUE : return self.encodeTrue(buf)
case V_FALSE : return self.encodeFalse(buf)
case V_ARRAY : return self.encodeArray(buf)
case V_OBJECT: return self.encodeObject(buf)
case V_STRING: return self.encodeString(buf)
case V_NUMBER: return self.encodeNumber(buf)
case V_ANY : return self.encodeInterface(buf)
default : return ErrUnsupportType
}
}
func (self *Node) encodeRaw(buf *[]byte) error {
raw, err := self.Raw()
if err != nil {
return err
}
*buf = append(*buf, raw...)
return nil
}
func (self *Node) encodeNull(buf *[]byte) error {
*buf = append(*buf, bytesNull...)
return nil
}
func (self *Node) encodeTrue(buf *[]byte) error {
*buf = append(*buf, bytesTrue...)
return nil
}
func (self *Node) encodeFalse(buf *[]byte) error {
*buf = append(*buf, bytesFalse...)
return nil
}
func (self *Node) encodeNumber(buf *[]byte) error {
str := self.toString()
*buf = append(*buf, str...)
return nil
}
func (self *Node) encodeString(buf *[]byte) error {
if self.l == 0 {
*buf = append(*buf, '"', '"')
return nil
}
quote(buf, self.toString())
return nil
}
func (self *Node) encodeArray(buf *[]byte) error {
if self.isLazy() {
if err := self.skipAllIndex(); err != nil {
return err
}
}
nb := self.len()
if nb == 0 {
*buf = append(*buf, bytesArray...)
return nil
}
*buf = append(*buf, '[')
var started bool
for i := 0; i < nb; i++ {
n := self.nodeAt(i)
if !n.Exists() {
continue
}
if started {
*buf = append(*buf, ',')
}
started = true
if err := n.encode(buf); err != nil {
return err
}
}
*buf = append(*buf, ']')
return nil
}
func (self *Pair) encode(buf *[]byte) error {
if len(*buf) == 0 {
*buf = append(*buf, '"', '"', ':')
return self.Value.encode(buf)
}
quote(buf, self.Key)
*buf = append(*buf, ':')
return self.Value.encode(buf)
}
func (self *Node) encodeObject(buf *[]byte) error {
if self.isLazy() {
if err := self.skipAllKey(); err != nil {
return err
}
}
nb := self.len()
if nb == 0 {
*buf = append(*buf, bytesObject...)
return nil
}
*buf = append(*buf, '{')
var started bool
for i := 0; i < nb; i++ {
n := self.pairAt(i)
if n == nil || !n.Value.Exists() {
continue
}
if started {
*buf = append(*buf, ',')
}
started = true
if err := n.encode(buf); err != nil {
return err
}
}
*buf = append(*buf, '}')
return nil
}
+130
View File
@@ -0,0 +1,130 @@
package ast
import (
`fmt`
`strings`
`unsafe`
`github.com/bytedance/sonic/internal/native/types`
)
func newError(err types.ParsingError, msg string) *Node {
return &Node{
t: V_ERROR,
l: uint(err),
p: unsafe.Pointer(&msg),
}
}
// Error returns error message if the node is invalid
func (self Node) Error() string {
if self.t != V_ERROR {
return ""
} else {
return *(*string)(self.p)
}
}
func newSyntaxError(err SyntaxError) *Node {
msg := err.Description()
return &Node{
t: V_ERROR,
l: uint(err.Code),
p: unsafe.Pointer(&msg),
}
}
func (self *Parser) syntaxError(err types.ParsingError) SyntaxError {
return SyntaxError{
Pos : self.p,
Src : self.s,
Code: err,
}
}
func unwrapError(err error) *Node {
if se, ok := err.(*Node); ok {
return se
}else if sse, ok := err.(Node); ok {
return &sse
} else {
msg := err.Error()
return &Node{
t: V_ERROR,
p: unsafe.Pointer(&msg),
}
}
}
type SyntaxError struct {
Pos int
Src string
Code types.ParsingError
Msg string
}
func (self SyntaxError) Error() string {
return fmt.Sprintf("%q", self.Description())
}
func (self SyntaxError) Description() string {
return "Syntax error " + self.description()
}
func (self SyntaxError) description() string {
i := 16
p := self.Pos - i
q := self.Pos + i
/* check for empty source */
if self.Src == "" {
return fmt.Sprintf("no sources available: %#v", self)
}
/* prevent slicing before the beginning */
if p < 0 {
p, q, i = 0, q - p, i + p
}
/* prevent slicing beyond the end */
if n := len(self.Src); q > n {
n = q - n
q = len(self.Src)
/* move the left bound if possible */
if p > n {
i += n
p -= n
}
}
/* left and right length */
x := clamp_zero(i)
y := clamp_zero(q - p - i - 1)
/* compose the error description */
return fmt.Sprintf(
"at index %d: %s\n\n\t%s\n\t%s^%s\n",
self.Pos,
self.Message(),
self.Src[p:q],
strings.Repeat(".", x),
strings.Repeat(".", y),
)
}
func (self SyntaxError) Message() string {
if self.Msg == "" {
return self.Code.Message()
}
return self.Msg
}
func clamp_zero(v int) int {
if v < 0 {
return 0
} else {
return v
}
}
+203
View File
@@ -0,0 +1,203 @@
/*
* Copyright 2021 ByteDance Inc.
*
* 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.
*/
package ast
import (
`fmt`
`github.com/bytedance/sonic/internal/native/types`
)
type Pair struct {
Key string
Value Node
}
// Values returns iterator for array's children traversal
func (self *Node) Values() (ListIterator, error) {
if err := self.should(types.V_ARRAY, "an array"); err != nil {
return ListIterator{}, err
}
return self.values(), nil
}
func (self *Node) values() ListIterator {
return ListIterator{Iterator{p: self}}
}
// Properties returns iterator for object's children traversal
func (self *Node) Properties() (ObjectIterator, error) {
if err := self.should(types.V_OBJECT, "an object"); err != nil {
return ObjectIterator{}, err
}
return self.properties(), nil
}
func (self *Node) properties() ObjectIterator {
return ObjectIterator{Iterator{p: self}}
}
type Iterator struct {
i int
p *Node
}
func (self *Iterator) Pos() int {
return self.i
}
func (self *Iterator) Len() int {
return self.p.len()
}
// HasNext reports if it is the end of iteration or has error.
func (self *Iterator) HasNext() bool {
if !self.p.isLazy() {
return self.p.Valid() && self.i < self.p.len()
} else if self.p.t == _V_ARRAY_LAZY {
return self.p.skipNextNode().Valid()
} else if self.p.t == _V_OBJECT_LAZY {
pair := self.p.skipNextPair()
if pair == nil {
return false
}
return pair.Value.Valid()
}
return false
}
// ListIterator is specialized iterator for V_ARRAY
type ListIterator struct {
Iterator
}
// ObjectIterator is specialized iterator for V_ARRAY
type ObjectIterator struct {
Iterator
}
func (self *ListIterator) next() *Node {
next_start:
if !self.HasNext() {
return nil
} else {
n := self.p.nodeAt(self.i)
self.i++
if !n.Exists() {
goto next_start
}
return n
}
}
// Next scans through children of underlying V_ARRAY,
// copies each child to v, and returns .HasNext().
func (self *ListIterator) Next(v *Node) bool {
n := self.next()
if n == nil {
return false
}
*v = *n
return true
}
func (self *ObjectIterator) next() *Pair {
next_start:
if !self.HasNext() {
return nil
} else {
n := self.p.pairAt(self.i)
self.i++
if n == nil || !n.Value.Exists() {
goto next_start
}
return n
}
}
// Next scans through children of underlying V_OBJECT,
// copies each child to v, and returns .HasNext().
func (self *ObjectIterator) Next(p *Pair) bool {
n := self.next()
if n == nil {
return false
}
*p = *n
return true
}
// Sequence represents scanning path of single-layer nodes.
// Index indicates the value's order in both V_ARRAY and V_OBJECT json.
// Key is the value's key (for V_OBJECT json only, otherwise it will be nil).
type Sequence struct {
Index int
Key *string
// Level int
}
// String is string representation of one Sequence
func (s Sequence) String() string {
k := ""
if s.Key != nil {
k = *s.Key
}
return fmt.Sprintf("Sequence(%d, %q)", s.Index, k)
}
type Scanner func(path Sequence, node *Node) bool
// ForEach scans one V_OBJECT node's children from JSON head to tail,
// and pass the Sequence and Node of corresponding JSON value.
//
// Especailly, if the node is not V_ARRAY or V_OBJECT,
// the node itself will be returned and Sequence.Index == -1.
//
// NOTICE: A unsetted node WON'T trigger sc, but its index still counts into Path.Index
func (self *Node) ForEach(sc Scanner) error {
switch self.itype() {
case types.V_ARRAY:
iter, err := self.Values()
if err != nil {
return err
}
v := iter.next()
for v != nil {
if !sc(Sequence{iter.i-1, nil}, v) {
return nil
}
v = iter.next()
}
case types.V_OBJECT:
iter, err := self.Properties()
if err != nil {
return err
}
v := iter.next()
for v != nil {
if !sc(Sequence{iter.i-1, &v.Key}, &v.Value) {
return nil
}
v = iter.next()
}
default:
if self.Check() != nil {
return self
}
sc(Sequence{-1, nil}, self)
}
return nil
}
File diff suppressed because it is too large Load Diff
+660
View File
@@ -0,0 +1,660 @@
/*
* Copyright 2021 ByteDance Inc.
*
* 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.
*/
package ast
import (
`fmt`
`github.com/bytedance/sonic/internal/native/types`
`github.com/bytedance/sonic/internal/rt`
)
const (
_DEFAULT_NODE_CAP int = 8
_APPEND_GROW_SHIFT = 1
)
const (
_ERR_NOT_FOUND types.ParsingError = 33
_ERR_UNSUPPORT_TYPE types.ParsingError = 34
)
var (
// ErrNotExist means both key and value doesn't exist
ErrNotExist error = newError(_ERR_NOT_FOUND, "value not exists")
// ErrUnsupportType means API on the node is unsupported
ErrUnsupportType error = newError(_ERR_UNSUPPORT_TYPE, "unsupported type")
)
type Parser struct {
p int
s string
noLazy bool
skipValue bool
dbuf *byte
}
/** Parser Private Methods **/
func (self *Parser) delim() types.ParsingError {
n := len(self.s)
p := self.lspace(self.p)
/* check for EOF */
if p >= n {
return types.ERR_EOF
}
/* check for the delimtier */
if self.s[p] != ':' {
return types.ERR_INVALID_CHAR
}
/* update the read pointer */
self.p = p + 1
return 0
}
func (self *Parser) object() types.ParsingError {
n := len(self.s)
p := self.lspace(self.p)
/* check for EOF */
if p >= n {
return types.ERR_EOF
}
/* check for the delimtier */
if self.s[p] != '{' {
return types.ERR_INVALID_CHAR
}
/* update the read pointer */
self.p = p + 1
return 0
}
func (self *Parser) array() types.ParsingError {
n := len(self.s)
p := self.lspace(self.p)
/* check for EOF */
if p >= n {
return types.ERR_EOF
}
/* check for the delimtier */
if self.s[p] != '[' {
return types.ERR_INVALID_CHAR
}
/* update the read pointer */
self.p = p + 1
return 0
}
func (self *Parser) lspace(sp int) int {
ns := len(self.s)
for ; sp<ns && isSpace(self.s[sp]); sp+=1 {}
return sp
}
func (self *Parser) decodeArray(ret *linkedNodes) (Node, types.ParsingError) {
sp := self.p
ns := len(self.s)
/* check for EOF */
if self.p = self.lspace(sp); self.p >= ns {
return Node{}, types.ERR_EOF
}
/* check for empty array */
if self.s[self.p] == ']' {
self.p++
return Node{t: types.V_ARRAY}, 0
}
/* allocate array space and parse every element */
for {
var val Node
var err types.ParsingError
if self.skipValue {
/* skip the value */
var start int
if start, err = self.skipFast(); err != 0 {
return Node{}, err
}
if self.p > ns {
return Node{}, types.ERR_EOF
}
t := switchRawType(self.s[start])
if t == _V_NONE {
return Node{}, types.ERR_INVALID_CHAR
}
val = newRawNode(self.s[start:self.p], t)
}else{
/* decode the value */
if val, err = self.Parse(); err != 0 {
return Node{}, err
}
}
/* add the value to result */
ret.Push(val)
self.p = self.lspace(self.p)
/* check for EOF */
if self.p >= ns {
return Node{}, types.ERR_EOF
}
/* check for the next character */
switch self.s[self.p] {
case ',' : self.p++
case ']' : self.p++; return newArray(ret), 0
default:
// if val.isLazy() {
// return newLazyArray(self, ret), 0
// }
return Node{}, types.ERR_INVALID_CHAR
}
}
}
func (self *Parser) decodeObject(ret *linkedPairs) (Node, types.ParsingError) {
sp := self.p
ns := len(self.s)
/* check for EOF */
if self.p = self.lspace(sp); self.p >= ns {
return Node{}, types.ERR_EOF
}
/* check for empty object */
if self.s[self.p] == '}' {
self.p++
return Node{t: types.V_OBJECT}, 0
}
/* decode each pair */
for {
var val Node
var njs types.JsonState
var err types.ParsingError
/* decode the key */
if njs = self.decodeValue(); njs.Vt != types.V_STRING {
return Node{}, types.ERR_INVALID_CHAR
}
/* extract the key */
idx := self.p - 1
key := self.s[njs.Iv:idx]
/* check for escape sequence */
if njs.Ep != -1 {
if key, err = unquote(key); err != 0 {
return Node{}, err
}
}
/* expect a ':' delimiter */
if err = self.delim(); err != 0 {
return Node{}, err
}
if self.skipValue {
/* skip the value */
var start int
if start, err = self.skipFast(); err != 0 {
return Node{}, err
}
if self.p > ns {
return Node{}, types.ERR_EOF
}
t := switchRawType(self.s[start])
if t == _V_NONE {
return Node{}, types.ERR_INVALID_CHAR
}
val = newRawNode(self.s[start:self.p], t)
} else {
/* decode the value */
if val, err = self.Parse(); err != 0 {
return Node{}, err
}
}
/* add the value to result */
// FIXME: ret's address may change here, thus previous referred node in ret may be invalid !!
ret.Push(Pair{Key: key, Value: val})
self.p = self.lspace(self.p)
/* check for EOF */
if self.p >= ns {
return Node{}, types.ERR_EOF
}
/* check for the next character */
switch self.s[self.p] {
case ',' : self.p++
case '}' : self.p++; return newObject(ret), 0
default:
// if val.isLazy() {
// return newLazyObject(self, ret), 0
// }
return Node{}, types.ERR_INVALID_CHAR
}
}
}
func (self *Parser) decodeString(iv int64, ep int) (Node, types.ParsingError) {
p := self.p - 1
s := self.s[iv:p]
/* fast path: no escape sequence */
if ep == -1 {
return NewString(s), 0
}
/* unquote the string */
out, err := unquote(s)
/* check for errors */
if err != 0 {
return Node{}, err
} else {
return newBytes(rt.Str2Mem(out)), 0
}
}
/** Parser Interface **/
func (self *Parser) Pos() int {
return self.p
}
func (self *Parser) Parse() (Node, types.ParsingError) {
switch val := self.decodeValue(); val.Vt {
case types.V_EOF : return Node{}, types.ERR_EOF
case types.V_NULL : return nullNode, 0
case types.V_TRUE : return trueNode, 0
case types.V_FALSE : return falseNode, 0
case types.V_STRING : return self.decodeString(val.Iv, val.Ep)
case types.V_ARRAY:
if p := skipBlank(self.s, self.p); p >= self.p && self.s[p] == ']' {
self.p = p + 1
return Node{t: types.V_ARRAY}, 0
}
if self.noLazy {
return self.decodeArray(new(linkedNodes))
}
return newLazyArray(self), 0
case types.V_OBJECT:
if p := skipBlank(self.s, self.p); p >= self.p && self.s[p] == '}' {
self.p = p + 1
return Node{t: types.V_OBJECT}, 0
}
if self.noLazy {
return self.decodeObject(new(linkedPairs))
}
return newLazyObject(self), 0
case types.V_DOUBLE : return NewNumber(self.s[val.Ep:self.p]), 0
case types.V_INTEGER : return NewNumber(self.s[val.Ep:self.p]), 0
default : return Node{}, types.ParsingError(-val.Vt)
}
}
func (self *Parser) searchKey(match string) types.ParsingError {
ns := len(self.s)
if err := self.object(); err != 0 {
return err
}
/* check for EOF */
if self.p = self.lspace(self.p); self.p >= ns {
return types.ERR_EOF
}
/* check for empty object */
if self.s[self.p] == '}' {
self.p++
return _ERR_NOT_FOUND
}
var njs types.JsonState
var err types.ParsingError
/* decode each pair */
for {
/* decode the key */
if njs = self.decodeValue(); njs.Vt != types.V_STRING {
return types.ERR_INVALID_CHAR
}
/* extract the key */
idx := self.p - 1
key := self.s[njs.Iv:idx]
/* check for escape sequence */
if njs.Ep != -1 {
if key, err = unquote(key); err != 0 {
return err
}
}
/* expect a ':' delimiter */
if err = self.delim(); err != 0 {
return err
}
/* skip value */
if key != match {
if _, err = self.skipFast(); err != 0 {
return err
}
} else {
return 0
}
/* check for EOF */
self.p = self.lspace(self.p)
if self.p >= ns {
return types.ERR_EOF
}
/* check for the next character */
switch self.s[self.p] {
case ',':
self.p++
case '}':
self.p++
return _ERR_NOT_FOUND
default:
return types.ERR_INVALID_CHAR
}
}
}
func (self *Parser) searchIndex(idx int) types.ParsingError {
ns := len(self.s)
if err := self.array(); err != 0 {
return err
}
/* check for EOF */
if self.p = self.lspace(self.p); self.p >= ns {
return types.ERR_EOF
}
/* check for empty array */
if self.s[self.p] == ']' {
self.p++
return _ERR_NOT_FOUND
}
var err types.ParsingError
/* allocate array space and parse every element */
for i := 0; i < idx; i++ {
/* decode the value */
if _, err = self.skipFast(); err != 0 {
return err
}
/* check for EOF */
self.p = self.lspace(self.p)
if self.p >= ns {
return types.ERR_EOF
}
/* check for the next character */
switch self.s[self.p] {
case ',':
self.p++
case ']':
self.p++
return _ERR_NOT_FOUND
default:
return types.ERR_INVALID_CHAR
}
}
return 0
}
func (self *Node) skipNextNode() *Node {
if !self.isLazy() {
return nil
}
parser, stack := self.getParserAndArrayStack()
ret := &stack.v
sp := parser.p
ns := len(parser.s)
/* check for EOF */
if parser.p = parser.lspace(sp); parser.p >= ns {
return newSyntaxError(parser.syntaxError(types.ERR_EOF))
}
/* check for empty array */
if parser.s[parser.p] == ']' {
parser.p++
self.setArray(ret)
return nil
}
var val Node
/* skip the value */
if start, err := parser.skipFast(); err != 0 {
return newSyntaxError(parser.syntaxError(err))
} else {
t := switchRawType(parser.s[start])
if t == _V_NONE {
return newSyntaxError(parser.syntaxError(types.ERR_INVALID_CHAR))
}
val = newRawNode(parser.s[start:parser.p], t)
}
/* add the value to result */
ret.Push(val)
self.l++
parser.p = parser.lspace(parser.p)
/* check for EOF */
if parser.p >= ns {
return newSyntaxError(parser.syntaxError(types.ERR_EOF))
}
/* check for the next character */
switch parser.s[parser.p] {
case ',':
parser.p++
return ret.At(ret.Len()-1)
case ']':
parser.p++
self.setArray(ret)
return ret.At(ret.Len()-1)
default:
return newSyntaxError(parser.syntaxError(types.ERR_INVALID_CHAR))
}
}
func (self *Node) skipNextPair() (*Pair) {
if !self.isLazy() {
return nil
}
parser, stack := self.getParserAndObjectStack()
ret := &stack.v
sp := parser.p
ns := len(parser.s)
/* check for EOF */
if parser.p = parser.lspace(sp); parser.p >= ns {
return &Pair{"", *newSyntaxError(parser.syntaxError(types.ERR_EOF))}
}
/* check for empty object */
if parser.s[parser.p] == '}' {
parser.p++
self.setObject(ret)
return nil
}
/* decode one pair */
var val Node
var njs types.JsonState
var err types.ParsingError
/* decode the key */
if njs = parser.decodeValue(); njs.Vt != types.V_STRING {
return &Pair{"", *newSyntaxError(parser.syntaxError(types.ERR_INVALID_CHAR))}
}
/* extract the key */
idx := parser.p - 1
key := parser.s[njs.Iv:idx]
/* check for escape sequence */
if njs.Ep != -1 {
if key, err = unquote(key); err != 0 {
return &Pair{key, *newSyntaxError(parser.syntaxError(err))}
}
}
/* expect a ':' delimiter */
if err = parser.delim(); err != 0 {
return &Pair{key, *newSyntaxError(parser.syntaxError(err))}
}
/* skip the value */
if start, err := parser.skipFast(); err != 0 {
return &Pair{key, *newSyntaxError(parser.syntaxError(err))}
} else {
t := switchRawType(parser.s[start])
if t == _V_NONE {
return &Pair{key, *newSyntaxError(parser.syntaxError(types.ERR_INVALID_CHAR))}
}
val = newRawNode(parser.s[start:parser.p], t)
}
/* add the value to result */
ret.Push(Pair{Key: key, Value: val})
self.l++
parser.p = parser.lspace(parser.p)
/* check for EOF */
if parser.p >= ns {
return &Pair{key, *newSyntaxError(parser.syntaxError(types.ERR_EOF))}
}
/* check for the next character */
switch parser.s[parser.p] {
case ',':
parser.p++
return ret.At(ret.Len()-1)
case '}':
parser.p++
self.setObject(ret)
return ret.At(ret.Len()-1)
default:
return &Pair{key, *newSyntaxError(parser.syntaxError(types.ERR_INVALID_CHAR))}
}
}
/** Parser Factory **/
// Loads parse all json into interface{}
func Loads(src string) (int, interface{}, error) {
ps := &Parser{s: src}
np, err := ps.Parse()
/* check for errors */
if err != 0 {
return 0, nil, ps.ExportError(err)
} else {
x, err := np.Interface()
if err != nil {
return 0, nil, err
}
return ps.Pos(), x, nil
}
}
// LoadsUseNumber parse all json into interface{}, with numeric nodes casted to json.Number
func LoadsUseNumber(src string) (int, interface{}, error) {
ps := &Parser{s: src}
np, err := ps.Parse()
/* check for errors */
if err != 0 {
return 0, nil, err
} else {
x, err := np.InterfaceUseNumber()
if err != nil {
return 0, nil, err
}
return ps.Pos(), x, nil
}
}
// NewParser returns pointer of new allocated parser
func NewParser(src string) *Parser {
return &Parser{s: src}
}
// NewParser returns new allocated parser
func NewParserObj(src string) Parser {
return Parser{s: src}
}
// decodeNumber controls if parser decodes the number values instead of skip them
// WARN: once you set decodeNumber(true), please set decodeNumber(false) before you drop the parser
// otherwise the memory CANNOT be reused
func (self *Parser) decodeNumber(decode bool) {
if !decode && self.dbuf != nil {
types.FreeDbuf(self.dbuf)
self.dbuf = nil
return
}
if decode && self.dbuf == nil {
self.dbuf = types.NewDbuf()
}
}
// ExportError converts types.ParsingError to std Error
func (self *Parser) ExportError(err types.ParsingError) error {
if err == _ERR_NOT_FOUND {
return ErrNotExist
}
return fmt.Errorf("%q", SyntaxError{
Pos : self.p,
Src : self.s,
Code: err,
}.Description())
}
func backward(src string, i int) int {
for ; i>=0 && isSpace(src[i]); i-- {}
return i
}
+138
View File
@@ -0,0 +1,138 @@
/*
* Copyright 2021 ByteDance Inc.
*
* 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.
*/
package ast
import (
`github.com/bytedance/sonic/internal/rt`
`github.com/bytedance/sonic/internal/native/types`
)
type Searcher struct {
parser Parser
}
func NewSearcher(str string) *Searcher {
return &Searcher{
parser: Parser{
s: str,
noLazy: false,
},
}
}
// GetByPathCopy search in depth from top json and returns a **Copied** json node at the path location
func (self *Searcher) GetByPathCopy(path ...interface{}) (Node, error) {
return self.getByPath(true, true, path...)
}
// GetByPathNoCopy search in depth from top json and returns a **Referenced** json node at the path location
//
// WARN: this search directly refer partial json from top json, which has faster speed,
// may consumes more memory.
func (self *Searcher) GetByPath(path ...interface{}) (Node, error) {
return self.getByPath(false, true, path...)
}
func (self *Searcher) getByPath(copystring bool, validate bool, path ...interface{}) (Node, error) {
var err types.ParsingError
var start int
self.parser.p = 0
start, err = self.parser.getByPath(validate, path...)
if err != 0 {
// for compatibility with old version
if err == types.ERR_NOT_FOUND {
return Node{}, ErrNotExist
}
if err == types.ERR_UNSUPPORT_TYPE {
panic("path must be either int(>=0) or string")
}
return Node{}, self.parser.syntaxError(err)
}
t := switchRawType(self.parser.s[start])
if t == _V_NONE {
return Node{}, self.parser.ExportError(err)
}
// copy string to reducing memory usage
var raw string
if copystring {
raw = rt.Mem2Str([]byte(self.parser.s[start:self.parser.p]))
} else {
raw = self.parser.s[start:self.parser.p]
}
return newRawNode(raw, t), nil
}
// GetByPath searches a path and returns relaction and types of target
func _GetByPath(src string, path ...interface{}) (start int, end int, typ int, err error) {
p := NewParserObj(src)
s, e := p.getByPath(false, path...)
if e != 0 {
// for compatibility with old version
if e == types.ERR_NOT_FOUND {
return -1, -1, 0, ErrNotExist
}
if e == types.ERR_UNSUPPORT_TYPE {
panic("path must be either int(>=0) or string")
}
return -1, -1, 0, p.syntaxError(e)
}
t := switchRawType(p.s[s])
if t == _V_NONE {
return -1, -1, 0, ErrNotExist
}
if t == _V_NUMBER {
p.p = 1 + backward(p.s, p.p-1)
}
return s, p.p, int(t), nil
}
// ValidSyntax check if a json has a valid JSON syntax,
// while not validate UTF-8 charset
func _ValidSyntax(json string) bool {
p := NewParserObj(json)
_, e := p.skip()
if e != 0 {
return false
}
if skipBlank(p.s, p.p) != -int(types.ERR_EOF) {
return false
}
return true
}
// SkipFast skip a json value in fast-skip algs,
// while not strictly validate JSON syntax and UTF-8 charset.
func _SkipFast(src string, i int) (int, int, error) {
p := NewParserObj(src)
p.p = i
s, e := p.skipFast()
if e != 0 {
return -1, -1, p.ExportError(e)
}
t := switchRawType(p.s[s])
if t == _V_NONE {
return -1, -1, ErrNotExist
}
if t == _V_NUMBER {
p.p = 1 + backward(p.s, p.p-1)
}
return s, p.p, nil
}
+55
View File
@@ -0,0 +1,55 @@
// +build !go1.20
/*
* Copyright 2021 ByteDance Inc.
*
* 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.
*/
package ast
import (
`unsafe`
`unicode/utf8`
`github.com/bytedance/sonic/internal/rt`
)
//go:noescape
//go:linkname memmove runtime.memmove
//goland:noinspection GoUnusedParameter
func memmove(to unsafe.Pointer, from unsafe.Pointer, n uintptr)
//go:linkname unsafe_NewArray reflect.unsafe_NewArray
//goland:noinspection GoUnusedParameter
func unsafe_NewArray(typ *rt.GoType, n int) unsafe.Pointer
//go:linkname growslice runtime.growslice
//goland:noinspection GoUnusedParameter
func growslice(et *rt.GoType, old rt.GoSlice, cap int) rt.GoSlice
//go:nosplit
func mem2ptr(s []byte) unsafe.Pointer {
return (*rt.GoSlice)(unsafe.Pointer(&s)).Ptr
}
var (
//go:linkname safeSet encoding/json.safeSet
safeSet [utf8.RuneSelf]bool
//go:linkname hex encoding/json.hex
hex string
)
//go:linkname unquoteBytes encoding/json.unquoteBytes
func unquoteBytes(s []byte) (t []byte, ok bool)
+55
View File
@@ -0,0 +1,55 @@
// +build go1.20
/*
* Copyright 2021 ByteDance Inc.
*
* 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.
*/
package ast
import (
`unsafe`
`unicode/utf8`
`github.com/bytedance/sonic/internal/rt`
)
//go:noescape
//go:linkname memmove runtime.memmove
//goland:noinspection GoUnusedParameter
func memmove(to unsafe.Pointer, from unsafe.Pointer, n uintptr)
//go:linkname unsafe_NewArray reflect.unsafe_NewArray
//goland:noinspection GoUnusedParameter
func unsafe_NewArray(typ *rt.GoType, n int) unsafe.Pointer
//go:linkname growslice reflect.growslice
//goland:noinspection GoUnusedParameter
func growslice(et *rt.GoType, old rt.GoSlice, cap int) rt.GoSlice
//go:nosplit
func mem2ptr(s []byte) unsafe.Pointer {
return (*rt.GoSlice)(unsafe.Pointer(&s)).Ptr
}
var (
//go:linkname safeSet encoding/json.safeSet
safeSet [utf8.RuneSelf]bool
//go:linkname hex encoding/json.hex
hex string
)
//go:linkname unquoteBytes encoding/json.unquoteBytes
func unquoteBytes(s []byte) (t []byte, ok bool)
+315
View File
@@ -0,0 +1,315 @@
/*
* Copyright 2021 ByteDance Inc.
*
* 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.
*/
package ast
import (
`encoding/json`
`github.com/bytedance/sonic/internal/native/types`
)
// Visitor handles the callbacks during preorder traversal of a JSON AST.
//
// According to the JSON RFC8259, a JSON AST can be defined by
// the following rules without separator / whitespace tokens.
//
// JSON-AST = value
// value = false / null / true / object / array / number / string
// object = begin-object [ member *( member ) ] end-object
// member = string value
// array = begin-array [ value *( value ) ] end-array
//
type Visitor interface {
// OnNull handles a JSON null value.
OnNull() error
// OnBool handles a JSON true / false value.
OnBool(v bool) error
// OnString handles a JSON string value.
OnString(v string) error
// OnInt64 handles a JSON number value with int64 type.
OnInt64(v int64, n json.Number) error
// OnFloat64 handles a JSON number value with float64 type.
OnFloat64(v float64, n json.Number) error
// OnObjectBegin handles the beginning of a JSON object value with a
// suggested capacity that can be used to make your custom object container.
//
// After this point the visitor will receive a sequence of callbacks like
// [string, value, string, value, ......, ObjectEnd].
//
// Note:
// 1. This is a recursive definition which means the value can
// also be a JSON object / array described by a sequence of callbacks.
// 2. The suggested capacity will be 0 if current object is empty.
// 3. Currently sonic use a fixed capacity for non-empty object (keep in
// sync with ast.Node) which might not be very suitable. This may be
// improved in future version.
OnObjectBegin(capacity int) error
// OnObjectKey handles a JSON object key string in member.
OnObjectKey(key string) error
// OnObjectEnd handles the ending of a JSON object value.
OnObjectEnd() error
// OnArrayBegin handles the beginning of a JSON array value with a
// suggested capacity that can be used to make your custom array container.
//
// After this point the visitor will receive a sequence of callbacks like
// [value, value, value, ......, ArrayEnd].
//
// Note:
// 1. This is a recursive definition which means the value can
// also be a JSON object / array described by a sequence of callbacks.
// 2. The suggested capacity will be 0 if current array is empty.
// 3. Currently sonic use a fixed capacity for non-empty array (keep in
// sync with ast.Node) which might not be very suitable. This may be
// improved in future version.
OnArrayBegin(capacity int) error
// OnArrayEnd handles the ending of a JSON array value.
OnArrayEnd() error
}
// VisitorOptions contains all Visitor's options. The default value is an
// empty VisitorOptions{}.
type VisitorOptions struct {
// OnlyNumber indicates parser to directly return number value without
// conversion, then the first argument of OnInt64 / OnFloat64 will always
// be zero.
OnlyNumber bool
}
var defaultVisitorOptions = &VisitorOptions{}
// Preorder decodes the whole JSON string and callbacks each AST node to visitor
// during preorder traversal. Any visitor method with an error returned will
// break the traversal and the given error will be directly returned. The opts
// argument can be reused after every call.
func Preorder(str string, visitor Visitor, opts *VisitorOptions) error {
if opts == nil {
opts = defaultVisitorOptions
}
// process VisitorOptions first to guarantee that all options will be
// constant during decoding and make options more readable.
var (
optDecodeNumber = !opts.OnlyNumber
)
tv := &traverser{
parser: Parser{
s: str,
noLazy: true,
skipValue: false,
},
visitor: visitor,
}
if optDecodeNumber {
tv.parser.decodeNumber(true)
}
err := tv.decodeValue()
if optDecodeNumber {
tv.parser.decodeNumber(false)
}
return err
}
type traverser struct {
parser Parser
visitor Visitor
}
// NOTE: keep in sync with (*Parser).Parse method.
func (self *traverser) decodeValue() error {
switch val := self.parser.decodeValue(); val.Vt {
case types.V_EOF:
return types.ERR_EOF
case types.V_NULL:
return self.visitor.OnNull()
case types.V_TRUE:
return self.visitor.OnBool(true)
case types.V_FALSE:
return self.visitor.OnBool(false)
case types.V_STRING:
return self.decodeString(val.Iv, val.Ep)
case types.V_DOUBLE:
return self.visitor.OnFloat64(val.Dv,
json.Number(self.parser.s[val.Ep:self.parser.p]))
case types.V_INTEGER:
return self.visitor.OnInt64(val.Iv,
json.Number(self.parser.s[val.Ep:self.parser.p]))
case types.V_ARRAY:
return self.decodeArray()
case types.V_OBJECT:
return self.decodeObject()
default:
return types.ParsingError(-val.Vt)
}
}
// NOTE: keep in sync with (*Parser).decodeArray method.
func (self *traverser) decodeArray() error {
sp := self.parser.p
ns := len(self.parser.s)
/* check for EOF */
self.parser.p = self.parser.lspace(sp)
if self.parser.p >= ns {
return types.ERR_EOF
}
/* check for empty array */
if self.parser.s[self.parser.p] == ']' {
self.parser.p++
if err := self.visitor.OnArrayBegin(0); err != nil {
return err
}
return self.visitor.OnArrayEnd()
}
/* allocate array space and parse every element */
if err := self.visitor.OnArrayBegin(_DEFAULT_NODE_CAP); err != nil {
return err
}
for {
/* decode the value */
if err := self.decodeValue(); err != nil {
return err
}
self.parser.p = self.parser.lspace(self.parser.p)
/* check for EOF */
if self.parser.p >= ns {
return types.ERR_EOF
}
/* check for the next character */
switch self.parser.s[self.parser.p] {
case ',':
self.parser.p++
case ']':
self.parser.p++
return self.visitor.OnArrayEnd()
default:
return types.ERR_INVALID_CHAR
}
}
}
// NOTE: keep in sync with (*Parser).decodeObject method.
func (self *traverser) decodeObject() error {
sp := self.parser.p
ns := len(self.parser.s)
/* check for EOF */
self.parser.p = self.parser.lspace(sp)
if self.parser.p >= ns {
return types.ERR_EOF
}
/* check for empty object */
if self.parser.s[self.parser.p] == '}' {
self.parser.p++
if err := self.visitor.OnObjectBegin(0); err != nil {
return err
}
return self.visitor.OnObjectEnd()
}
/* allocate object space and decode each pair */
if err := self.visitor.OnObjectBegin(_DEFAULT_NODE_CAP); err != nil {
return err
}
for {
var njs types.JsonState
var err types.ParsingError
/* decode the key */
if njs = self.parser.decodeValue(); njs.Vt != types.V_STRING {
return types.ERR_INVALID_CHAR
}
/* extract the key */
idx := self.parser.p - 1
key := self.parser.s[njs.Iv:idx]
/* check for escape sequence */
if njs.Ep != -1 {
if key, err = unquote(key); err != 0 {
return err
}
}
if err := self.visitor.OnObjectKey(key); err != nil {
return err
}
/* expect a ':' delimiter */
if err = self.parser.delim(); err != 0 {
return err
}
/* decode the value */
if err := self.decodeValue(); err != nil {
return err
}
self.parser.p = self.parser.lspace(self.parser.p)
/* check for EOF */
if self.parser.p >= ns {
return types.ERR_EOF
}
/* check for the next character */
switch self.parser.s[self.parser.p] {
case ',':
self.parser.p++
case '}':
self.parser.p++
return self.visitor.OnObjectEnd()
default:
return types.ERR_INVALID_CHAR
}
}
}
// NOTE: keep in sync with (*Parser).decodeString method.
func (self *traverser) decodeString(iv int64, ep int) error {
p := self.parser.p - 1
s := self.parser.s[iv:p]
/* fast path: no escape sequence */
if ep == -1 {
return self.visitor.OnString(s)
}
/* unquote the string */
out, err := unquote(s)
if err != 0 {
return err
}
return self.visitor.OnString(out)
}
+131
View File
@@ -0,0 +1,131 @@
// +build !amd64 !go1.16 go1.23
/*
* Copyright 2021 ByteDance Inc.
*
* 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.
*/
package sonic
import (
`bytes`
`encoding/json`
`io`
`reflect`
`github.com/bytedance/sonic/option`
)
type frozenConfig struct {
Config
}
// Froze convert the Config to API
func (cfg Config) Froze() API {
api := &frozenConfig{Config: cfg}
return api
}
func (cfg frozenConfig) marshalOptions(val interface{}, prefix, indent string) ([]byte, error) {
w := bytes.NewBuffer([]byte{})
enc := json.NewEncoder(w)
enc.SetEscapeHTML(cfg.EscapeHTML)
enc.SetIndent(prefix, indent)
err := enc.Encode(val)
out := w.Bytes()
// json.Encoder always appends '\n' after encoding,
// which is not same with json.Marshal()
if len(out) > 0 && out[len(out)-1] == '\n' {
out = out[:len(out)-1]
}
return out, err
}
// Marshal is implemented by sonic
func (cfg frozenConfig) Marshal(val interface{}) ([]byte, error) {
if !cfg.EscapeHTML {
return cfg.marshalOptions(val, "", "")
}
return json.Marshal(val)
}
// MarshalToString is implemented by sonic
func (cfg frozenConfig) MarshalToString(val interface{}) (string, error) {
out, err := cfg.Marshal(val)
return string(out), err
}
// MarshalIndent is implemented by sonic
func (cfg frozenConfig) MarshalIndent(val interface{}, prefix, indent string) ([]byte, error) {
if !cfg.EscapeHTML {
return cfg.marshalOptions(val, prefix, indent)
}
return json.MarshalIndent(val, prefix, indent)
}
// UnmarshalFromString is implemented by sonic
func (cfg frozenConfig) UnmarshalFromString(buf string, val interface{}) error {
r := bytes.NewBufferString(buf)
dec := json.NewDecoder(r)
if cfg.UseNumber {
dec.UseNumber()
}
if cfg.DisallowUnknownFields {
dec.DisallowUnknownFields()
}
return dec.Decode(val)
}
// Unmarshal is implemented by sonic
func (cfg frozenConfig) Unmarshal(buf []byte, val interface{}) error {
return cfg.UnmarshalFromString(string(buf), val)
}
// NewEncoder is implemented by sonic
func (cfg frozenConfig) NewEncoder(writer io.Writer) Encoder {
enc := json.NewEncoder(writer)
if !cfg.EscapeHTML {
enc.SetEscapeHTML(cfg.EscapeHTML)
}
return enc
}
// NewDecoder is implemented by sonic
func (cfg frozenConfig) NewDecoder(reader io.Reader) Decoder {
dec := json.NewDecoder(reader)
if cfg.UseNumber {
dec.UseNumber()
}
if cfg.DisallowUnknownFields {
dec.DisallowUnknownFields()
}
return dec
}
// Valid is implemented by sonic
func (cfg frozenConfig) Valid(data []byte) bool {
return json.Valid(data)
}
// Pretouch compiles vt ahead-of-time to avoid JIT compilation on-the-fly, in
// order to reduce the first-hit latency at **amd64** Arch.
// Opts are the compile options, for example, "option.WithCompileRecursiveDepth" is
// a compile option to set the depth of recursive compile for the nested struct type.
// * This is the none implement for !amd64.
// It will be useful for someone who develop with !amd64 arch,like Mac M1.
func Pretouch(vt reflect.Type, opts ...option.CompileOption) error {
return nil
}
@@ -0,0 +1,68 @@
// +build amd64,go1.16,!go1.23
/*
* Copyright 2023 ByteDance Inc.
*
* 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.
*/
package decoder
import (
`github.com/bytedance/sonic/internal/decoder`
)
// Decoder is the decoder context object
type Decoder = decoder.Decoder
// SyntaxError represents json syntax error
type SyntaxError = decoder.SyntaxError
// MismatchTypeError represents dismatching between json and object
type MismatchTypeError = decoder.MismatchTypeError
// Options for decode.
type Options = decoder.Options
const (
OptionUseInt64 Options = decoder.OptionUseInt64
OptionUseNumber Options = decoder.OptionUseNumber
OptionUseUnicodeErrors Options = decoder.OptionUseUnicodeErrors
OptionDisableUnknown Options = decoder.OptionDisableUnknown
OptionCopyString Options = decoder.OptionCopyString
OptionValidateString Options = decoder.OptionValidateString
)
// StreamDecoder is the decoder context object for streaming input.
type StreamDecoder = decoder.StreamDecoder
var (
// NewDecoder creates a new decoder instance.
NewDecoder = decoder.NewDecoder
// NewStreamDecoder adapts to encoding/json.NewDecoder API.
//
// NewStreamDecoder returns a new decoder that reads from r.
NewStreamDecoder = decoder.NewStreamDecoder
// Pretouch compiles vt ahead-of-time to avoid JIT compilation on-the-fly, in
// order to reduce the first-hit latency.
//
// Opts are the compile options, for example, "option.WithCompileRecursiveDepth" is
// a compile option to set the depth of recursive compile for the nested struct type.
Pretouch = decoder.Pretouch
// Skip skips only one json value, and returns first non-blank character position and its ending position if it is valid.
// Otherwise, returns negative error code using start and invalid character position using end
Skip = decoder.Skip
)
@@ -0,0 +1,194 @@
// +build !amd64 !go1.16 go1.23
/*
* Copyright 2023 ByteDance Inc.
*
* 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.
*/
package decoder
import (
`bytes`
`encoding/json`
`io`
`reflect`
`unsafe`
`github.com/bytedance/sonic/internal/native/types`
`github.com/bytedance/sonic/option`
)
func init() {
println("WARNING: sonic only supports Go1.16~1.22 && CPU amd64, but your environment is not suitable")
}
const (
_F_use_int64 = 0
_F_disable_urc = 2
_F_disable_unknown = 3
_F_copy_string = 4
_F_use_number = types.B_USE_NUMBER
_F_validate_string = types.B_VALIDATE_STRING
_F_allow_control = types.B_ALLOW_CONTROL
)
type Options uint64
const (
OptionUseInt64 Options = 1 << _F_use_int64
OptionUseNumber Options = 1 << _F_use_number
OptionUseUnicodeErrors Options = 1 << _F_disable_urc
OptionDisableUnknown Options = 1 << _F_disable_unknown
OptionCopyString Options = 1 << _F_copy_string
OptionValidateString Options = 1 << _F_validate_string
)
func (self *Decoder) SetOptions(opts Options) {
if (opts & OptionUseNumber != 0) && (opts & OptionUseInt64 != 0) {
panic("can't set OptionUseInt64 and OptionUseNumber both!")
}
self.f = uint64(opts)
}
// Decoder is the decoder context object
type Decoder struct {
i int
f uint64
s string
}
// NewDecoder creates a new decoder instance.
func NewDecoder(s string) *Decoder {
return &Decoder{s: s}
}
// Pos returns the current decoding position.
func (self *Decoder) Pos() int {
return self.i
}
func (self *Decoder) Reset(s string) {
self.s = s
self.i = 0
// self.f = 0
}
// NOTE: api fallback do nothing
func (self *Decoder) CheckTrailings() error {
pos := self.i
buf := self.s
/* skip all the trailing spaces */
if pos != len(buf) {
for pos < len(buf) && (types.SPACE_MASK & (1 << buf[pos])) != 0 {
pos++
}
}
/* then it must be at EOF */
if pos == len(buf) {
return nil
}
/* junk after JSON value */
return nil
}
// Decode parses the JSON-encoded data from current position and stores the result
// in the value pointed to by val.
func (self *Decoder) Decode(val interface{}) error {
r := bytes.NewBufferString(self.s)
dec := json.NewDecoder(r)
if (self.f & uint64(OptionUseNumber)) != 0 {
dec.UseNumber()
}
if (self.f & uint64(OptionDisableUnknown)) != 0 {
dec.DisallowUnknownFields()
}
return dec.Decode(val)
}
// UseInt64 indicates the Decoder to unmarshal an integer into an interface{} as an
// int64 instead of as a float64.
func (self *Decoder) UseInt64() {
self.f |= 1 << _F_use_int64
self.f &^= 1 << _F_use_number
}
// UseNumber indicates the Decoder to unmarshal a number into an interface{} as a
// json.Number instead of as a float64.
func (self *Decoder) UseNumber() {
self.f &^= 1 << _F_use_int64
self.f |= 1 << _F_use_number
}
// UseUnicodeErrors indicates the Decoder to return an error when encounter invalid
// UTF-8 escape sequences.
func (self *Decoder) UseUnicodeErrors() {
self.f |= 1 << _F_disable_urc
}
// DisallowUnknownFields indicates the Decoder to return an error when the destination
// is a struct and the input contains object keys which do not match any
// non-ignored, exported fields in the destination.
func (self *Decoder) DisallowUnknownFields() {
self.f |= 1 << _F_disable_unknown
}
// CopyString indicates the Decoder to decode string values by copying instead of referring.
func (self *Decoder) CopyString() {
self.f |= 1 << _F_copy_string
}
// ValidateString causes the Decoder to validate string values when decoding string value
// in JSON. Validation is that, returning error when unescaped control chars(0x00-0x1f) or
// invalid UTF-8 chars in the string value of JSON.
func (self *Decoder) ValidateString() {
self.f |= 1 << _F_validate_string
}
// Pretouch compiles vt ahead-of-time to avoid JIT compilation on-the-fly, in
// order to reduce the first-hit latency.
//
// Opts are the compile options, for example, "option.WithCompileRecursiveDepth" is
// a compile option to set the depth of recursive compile for the nested struct type.
func Pretouch(vt reflect.Type, opts ...option.CompileOption) error {
return nil
}
type StreamDecoder = json.Decoder
// NewStreamDecoder adapts to encoding/json.NewDecoder API.
//
// NewStreamDecoder returns a new decoder that reads from r.
func NewStreamDecoder(r io.Reader) *StreamDecoder {
return json.NewDecoder(r)
}
// SyntaxError represents json syntax error
type SyntaxError json.SyntaxError
// Description
func (s SyntaxError) Description() string {
return (*json.SyntaxError)(unsafe.Pointer(&s)).Error()
}
// Error
func (s SyntaxError) Error() string {
return (*json.SyntaxError)(unsafe.Pointer(&s)).Error()
}
// MismatchTypeError represents dismatching between json and object
type MismatchTypeError json.UnmarshalTypeError
@@ -0,0 +1,117 @@
// +build amd64,go1.16,!go1.23
/*
* Copyright 2023 ByteDance Inc.
*
* 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.
*/
package encoder
import (
`github.com/bytedance/sonic/internal/encoder`
)
// EnableFallback indicates if encoder use fallback
const EnableFallback = false
// Encoder represents a specific set of encoder configurations.
type Encoder = encoder.Encoder
// StreamEncoder uses io.Writer as input.
type StreamEncoder = encoder.StreamEncoder
// Options is a set of encoding options.
type Options = encoder.Options
const (
// SortMapKeys indicates that the keys of a map needs to be sorted
// before serializing into JSON.
// WARNING: This hurts performance A LOT, USE WITH CARE.
SortMapKeys Options = encoder.SortMapKeys
// EscapeHTML indicates encoder to escape all HTML characters
// after serializing into JSON (see https://pkg.go.dev/encoding/json#HTMLEscape).
// WARNING: This hurts performance A LOT, USE WITH CARE.
EscapeHTML Options = encoder.EscapeHTML
// CompactMarshaler indicates that the output JSON from json.Marshaler
// is always compact and needs no validation
CompactMarshaler Options = encoder.CompactMarshaler
// NoQuoteTextMarshaler indicates that the output text from encoding.TextMarshaler
// is always escaped string and needs no quoting
NoQuoteTextMarshaler Options = encoder.NoQuoteTextMarshaler
// NoNullSliceOrMap indicates all empty Array or Object are encoded as '[]' or '{}',
// instead of 'null'
NoNullSliceOrMap Options = encoder.NoNullSliceOrMap
// ValidateString indicates that encoder should validate the input string
// before encoding it into JSON.
ValidateString Options = encoder.ValidateString
// NoValidateJSONMarshaler indicates that the encoder should not validate the output string
// after encoding the JSONMarshaler to JSON.
NoValidateJSONMarshaler Options = encoder.NoValidateJSONMarshaler
// NoEncoderNewline indicates that the encoder should not add a newline after every message
NoEncoderNewline Options = encoder.NoEncoderNewline
// CompatibleWithStd is used to be compatible with std encoder.
CompatibleWithStd Options = encoder.CompatibleWithStd
)
var (
// Encode returns the JSON encoding of val, encoded with opts.
Encode = encoder.Encode
// EncodeInto is like Encode but uses a user-supplied buffer instead of allocating a new one.
EncodeIndented = encoder.EncodeIndented
// EncodeIndented is like Encode but applies Indent to format the output.
// Each JSON element in the output will begin on a new line beginning with prefix
// followed by one or more copies of indent according to the indentation nesting.
EncodeInto = encoder.EncodeInto
// HTMLEscape appends to dst the JSON-encoded src with <, >, &, U+2028 and U+2029
// characters inside string literals changed to \u003c, \u003e, \u0026, \u2028, \u2029
// so that the JSON will be safe to embed inside HTML <script> tags.
// For historical reasons, web browsers don't honor standard HTML
// escaping within <script> tags, so an alternative JSON encoding must
// be used.
HTMLEscape = encoder.HTMLEscape
// Pretouch compiles vt ahead-of-time to avoid JIT compilation on-the-fly, in
// order to reduce the first-hit latency.
//
// Opts are the compile options, for example, "option.WithCompileRecursiveDepth" is
// a compile option to set the depth of recursive compile for the nested struct type.
Pretouch = encoder.Pretouch
// Quote returns the JSON-quoted version of s.
Quote = encoder.Quote
// Valid validates json and returns first non-blank character position,
// if it is only one valid json value.
// Otherwise returns invalid character position using start.
//
// Note: it does not check for the invalid UTF-8 characters.
Valid = encoder.Valid
// NewStreamEncoder adapts to encoding/json.NewDecoder API.
//
// NewStreamEncoder returns a new encoder that write to w.
NewStreamEncoder = encoder.NewStreamEncoder
)
@@ -0,0 +1,261 @@
// +build !amd64 !go1.16 go1.23
/*
* Copyright 2023 ByteDance Inc.
*
* 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.
*/
package encoder
import (
`io`
`bytes`
`encoding/json`
`reflect`
`github.com/bytedance/sonic/option`
)
func init() {
println("WARNING:(encoder) sonic only supports Go1.16~1.22 && CPU amd64, but your environment is not suitable")
}
// EnableFallback indicates if encoder use fallback
const EnableFallback = true
// Options is a set of encoding options.
type Options uint64
const (
bitSortMapKeys = iota
bitEscapeHTML
bitCompactMarshaler
bitNoQuoteTextMarshaler
bitNoNullSliceOrMap
bitValidateString
bitNoValidateJSONMarshaler
bitNoEncoderNewline
// used for recursive compile
bitPointerValue = 63
)
const (
// SortMapKeys indicates that the keys of a map needs to be sorted
// before serializing into JSON.
// WARNING: This hurts performance A LOT, USE WITH CARE.
SortMapKeys Options = 1 << bitSortMapKeys
// EscapeHTML indicates encoder to escape all HTML characters
// after serializing into JSON (see https://pkg.go.dev/encoding/json#HTMLEscape).
// WARNING: This hurts performance A LOT, USE WITH CARE.
EscapeHTML Options = 1 << bitEscapeHTML
// CompactMarshaler indicates that the output JSON from json.Marshaler
// is always compact and needs no validation
CompactMarshaler Options = 1 << bitCompactMarshaler
// NoQuoteTextMarshaler indicates that the output text from encoding.TextMarshaler
// is always escaped string and needs no quoting
NoQuoteTextMarshaler Options = 1 << bitNoQuoteTextMarshaler
// NoNullSliceOrMap indicates all empty Array or Object are encoded as '[]' or '{}',
// instead of 'null'
NoNullSliceOrMap Options = 1 << bitNoNullSliceOrMap
// ValidateString indicates that encoder should validate the input string
// before encoding it into JSON.
ValidateString Options = 1 << bitValidateString
// NoValidateJSONMarshaler indicates that the encoder should not validate the output string
// after encoding the JSONMarshaler to JSON.
NoValidateJSONMarshaler Options = 1 << bitNoValidateJSONMarshaler
// NoEncoderNewline indicates that the encoder should not add a newline after every message
NoEncoderNewline Options = 1 << bitNoEncoderNewline
// CompatibleWithStd is used to be compatible with std encoder.
CompatibleWithStd Options = SortMapKeys | EscapeHTML | CompactMarshaler
)
// Encoder represents a specific set of encoder configurations.
type Encoder struct {
Opts Options
prefix string
indent string
}
// Encode returns the JSON encoding of v.
func (self *Encoder) Encode(v interface{}) ([]byte, error) {
if self.indent != "" || self.prefix != "" {
return EncodeIndented(v, self.prefix, self.indent, self.Opts)
}
return Encode(v, self.Opts)
}
// SortKeys enables the SortMapKeys option.
func (self *Encoder) SortKeys() *Encoder {
self.Opts |= SortMapKeys
return self
}
// SetEscapeHTML specifies if option EscapeHTML opens
func (self *Encoder) SetEscapeHTML(f bool) {
if f {
self.Opts |= EscapeHTML
} else {
self.Opts &= ^EscapeHTML
}
}
// SetValidateString specifies if option ValidateString opens
func (self *Encoder) SetValidateString(f bool) {
if f {
self.Opts |= ValidateString
} else {
self.Opts &= ^ValidateString
}
}
// SetNoValidateJSONMarshaler specifies if option NoValidateJSONMarshaler opens
func (self *Encoder) SetNoValidateJSONMarshaler(f bool) {
if f {
self.Opts |= NoValidateJSONMarshaler
} else {
self.Opts &= ^NoValidateJSONMarshaler
}
}
// SetNoEncoderNewline specifies if option NoEncoderNewline opens
func (self *Encoder) SetNoEncoderNewline(f bool) {
if f {
self.Opts |= NoEncoderNewline
} else {
self.Opts &= ^NoEncoderNewline
}
}
// SetCompactMarshaler specifies if option CompactMarshaler opens
func (self *Encoder) SetCompactMarshaler(f bool) {
if f {
self.Opts |= CompactMarshaler
} else {
self.Opts &= ^CompactMarshaler
}
}
// SetNoQuoteTextMarshaler specifies if option NoQuoteTextMarshaler opens
func (self *Encoder) SetNoQuoteTextMarshaler(f bool) {
if f {
self.Opts |= NoQuoteTextMarshaler
} else {
self.Opts &= ^NoQuoteTextMarshaler
}
}
// SetIndent instructs the encoder to format each subsequent encoded
// value as if indented by the package-level function EncodeIndent().
// Calling SetIndent("", "") disables indentation.
func (enc *Encoder) SetIndent(prefix, indent string) {
enc.prefix = prefix
enc.indent = indent
}
// Quote returns the JSON-quoted version of s.
func Quote(s string) string {
/* check for empty string */
if s == "" {
return `""`
}
out, _ := json.Marshal(s)
return string(out)
}
// Encode returns the JSON encoding of val, encoded with opts.
func Encode(val interface{}, opts Options) ([]byte, error) {
return json.Marshal(val)
}
// EncodeInto is like Encode but uses a user-supplied buffer instead of allocating
// a new one.
func EncodeInto(buf *[]byte, val interface{}, opts Options) error {
if buf == nil {
panic("user-supplied buffer buf is nil")
}
w := bytes.NewBuffer(*buf)
enc := json.NewEncoder(w)
enc.SetEscapeHTML((opts & EscapeHTML) != 0)
err := enc.Encode(val)
*buf = w.Bytes()
l := len(*buf)
if l > 0 && (opts & NoEncoderNewline != 0) && (*buf)[l-1] == '\n' {
*buf = (*buf)[:l-1]
}
return err
}
// HTMLEscape appends to dst the JSON-encoded src with <, >, &, U+2028 and U+2029
// characters inside string literals changed to \u003c, \u003e, \u0026, \u2028, \u2029
// so that the JSON will be safe to embed inside HTML <script> tags.
// For historical reasons, web browsers don't honor standard HTML
// escaping within <script> tags, so an alternative JSON encoding must
// be used.
func HTMLEscape(dst []byte, src []byte) []byte {
d := bytes.NewBuffer(dst)
json.HTMLEscape(d, src)
return d.Bytes()
}
// EncodeIndented is like Encode but applies Indent to format the output.
// Each JSON element in the output will begin on a new line beginning with prefix
// followed by one or more copies of indent according to the indentation nesting.
func EncodeIndented(val interface{}, prefix string, indent string, opts Options) ([]byte, error) {
w := bytes.NewBuffer([]byte{})
enc := json.NewEncoder(w)
enc.SetEscapeHTML((opts & EscapeHTML) != 0)
enc.SetIndent(prefix, indent)
err := enc.Encode(val)
out := w.Bytes()
return out, err
}
// Pretouch compiles vt ahead-of-time to avoid JIT compilation on-the-fly, in
// order to reduce the first-hit latency.
//
// Opts are the compile options, for example, "option.WithCompileRecursiveDepth" is
// a compile option to set the depth of recursive compile for the nested struct type.
func Pretouch(vt reflect.Type, opts ...option.CompileOption) error {
return nil
}
// Valid validates json and returns first non-blank character position,
// if it is only one valid json value.
// Otherwise returns invalid character position using start.
//
// Note: it does not check for the invalid UTF-8 characters.
func Valid(data []byte) (ok bool, start int) {
return json.Valid(data), 0
}
// StreamEncoder uses io.Writer as
type StreamEncoder = json.Encoder
// NewStreamEncoder adapts to encoding/json.NewDecoder API.
//
// NewStreamEncoder returns a new encoder that write to w.
func NewStreamEncoder(w io.Writer) *StreamEncoder {
return json.NewEncoder(w)
}
+9
View File
@@ -0,0 +1,9 @@
go 1.18
use (
.
./external_jsonlib_test
./fuzz
./generic_test
./loader
)
@@ -0,0 +1,115 @@
/*
* Copyright 2021 ByteDance Inc.
*
* 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.
*/
package caching
import (
`strings`
`unsafe`
`github.com/bytedance/sonic/internal/rt`
)
type FieldMap struct {
N uint64
b unsafe.Pointer
m map[string]int
}
type FieldEntry struct {
ID int
Name string
Hash uint64
}
const (
FieldMap_N = int64(unsafe.Offsetof(FieldMap{}.N))
FieldMap_b = int64(unsafe.Offsetof(FieldMap{}.b))
FieldEntrySize = int64(unsafe.Sizeof(FieldEntry{}))
)
func newBucket(n int) unsafe.Pointer {
v := make([]FieldEntry, n)
return (*rt.GoSlice)(unsafe.Pointer(&v)).Ptr
}
func CreateFieldMap(n int) *FieldMap {
return &FieldMap {
N: uint64(n * 2),
b: newBucket(n * 2), // LoadFactor = 0.5
m: make(map[string]int, n * 2),
}
}
func (self *FieldMap) At(p uint64) *FieldEntry {
off := uintptr(p) * uintptr(FieldEntrySize)
return (*FieldEntry)(unsafe.Pointer(uintptr(self.b) + off))
}
// Get searches FieldMap by name. JIT generated assembly does NOT call this
// function, rather it implements its own version directly in assembly. So
// we must ensure this function stays in sync with the JIT generated one.
func (self *FieldMap) Get(name string) int {
h := StrHash(name)
p := h % self.N
s := self.At(p)
/* find the element;
* the hash map is never full, so the loop will always terminate */
for s.Hash != 0 {
if s.Hash == h && s.Name == name {
return s.ID
} else {
p = (p + 1) % self.N
s = self.At(p)
}
}
/* not found */
return -1
}
func (self *FieldMap) Set(name string, i int) {
h := StrHash(name)
p := h % self.N
s := self.At(p)
/* searching for an empty slot;
* the hash map is never full, so the loop will always terminate */
for s.Hash != 0 {
p = (p + 1) % self.N
s = self.At(p)
}
/* set the value */
s.ID = i
s.Hash = h
s.Name = name
/* add the case-insensitive version, prefer the one with smaller field ID */
key := strings.ToLower(name)
if v, ok := self.m[key]; !ok || i < v {
self.m[key] = i
}
}
func (self *FieldMap) GetCaseInsensitive(name string) int {
if i, ok := self.m[strings.ToLower(name)]; ok {
return i
} else {
return -1
}
}
@@ -0,0 +1,40 @@
/*
* Copyright 2021 ByteDance Inc.
*
* 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.
*/
package caching
import (
`unsafe`
`github.com/bytedance/sonic/internal/rt`
)
var (
V_strhash = rt.UnpackEface(strhash)
S_strhash = *(*uintptr)(V_strhash.Value)
)
//go:noescape
//go:linkname strhash runtime.strhash
func strhash(_ unsafe.Pointer, _ uintptr) uintptr
func StrHash(s string) uint64 {
if v := strhash(unsafe.Pointer(&s), 0); v == 0 {
return 1
} else {
return uint64(v)
}
}
@@ -0,0 +1,173 @@
/*
* Copyright 2021 ByteDance Inc.
*
* 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.
*/
package caching
import (
`sync`
`sync/atomic`
`unsafe`
`github.com/bytedance/sonic/internal/rt`
)
/** Program Map **/
const (
_LoadFactor = 0.5
_InitCapacity = 4096 // must be a power of 2
)
type _ProgramMap struct {
n uint64
m uint32
b []_ProgramEntry
}
type _ProgramEntry struct {
vt *rt.GoType
fn interface{}
}
func newProgramMap() *_ProgramMap {
return &_ProgramMap {
n: 0,
m: _InitCapacity - 1,
b: make([]_ProgramEntry, _InitCapacity),
}
}
func (self *_ProgramMap) copy() *_ProgramMap {
fork := &_ProgramMap{
n: self.n,
m: self.m,
b: make([]_ProgramEntry, len(self.b)),
}
for i, f := range self.b {
fork.b[i] = f
}
return fork
}
func (self *_ProgramMap) get(vt *rt.GoType) interface{} {
i := self.m + 1
p := vt.Hash & self.m
/* linear probing */
for ; i > 0; i-- {
if b := self.b[p]; b.vt == vt {
return b.fn
} else if b.vt == nil {
break
} else {
p = (p + 1) & self.m
}
}
/* not found */
return nil
}
func (self *_ProgramMap) add(vt *rt.GoType, fn interface{}) *_ProgramMap {
p := self.copy()
f := float64(atomic.LoadUint64(&p.n) + 1) / float64(p.m + 1)
/* check for load factor */
if f > _LoadFactor {
p = p.rehash()
}
/* insert the value */
p.insert(vt, fn)
return p
}
func (self *_ProgramMap) rehash() *_ProgramMap {
c := (self.m + 1) << 1
r := &_ProgramMap{m: c - 1, b: make([]_ProgramEntry, int(c))}
/* rehash every entry */
for i := uint32(0); i <= self.m; i++ {
if b := self.b[i]; b.vt != nil {
r.insert(b.vt, b.fn)
}
}
/* rebuild successful */
return r
}
func (self *_ProgramMap) insert(vt *rt.GoType, fn interface{}) {
h := vt.Hash
p := h & self.m
/* linear probing */
for i := uint32(0); i <= self.m; i++ {
if b := &self.b[p]; b.vt != nil {
p += 1
p &= self.m
} else {
b.vt = vt
b.fn = fn
atomic.AddUint64(&self.n, 1)
return
}
}
/* should never happens */
panic("no available slots")
}
/** RCU Program Cache **/
type ProgramCache struct {
m sync.Mutex
p unsafe.Pointer
}
func CreateProgramCache() *ProgramCache {
return &ProgramCache {
m: sync.Mutex{},
p: unsafe.Pointer(newProgramMap()),
}
}
func (self *ProgramCache) Get(vt *rt.GoType) interface{} {
return (*_ProgramMap)(atomic.LoadPointer(&self.p)).get(vt)
}
func (self *ProgramCache) Compute(vt *rt.GoType, compute func(*rt.GoType, ... interface{}) (interface{}, error), ex ...interface{}) (interface{}, error) {
var err error
var val interface{}
/* use defer to prevent inlining of this function */
self.m.Lock()
defer self.m.Unlock()
/* double check with write lock held */
if val = self.Get(vt); val != nil {
return val, nil
}
/* compute the value */
if val, err = compute(vt, ex...); err != nil {
return nil, err
}
/* update the RCU cache */
atomic.StorePointer(&self.p, unsafe.Pointer((*_ProgramMap)(atomic.LoadPointer(&self.p)).add(vt, val)))
return val, nil
}
@@ -0,0 +1,40 @@
/*
* Copyright 2021 ByteDance Inc.
*
* 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.
*/
package cpu
import (
`fmt`
`os`
`github.com/klauspost/cpuid/v2`
)
var (
HasAVX = cpuid.CPU.Has(cpuid.AVX)
HasAVX2 = cpuid.CPU.Has(cpuid.AVX2)
HasSSE = cpuid.CPU.Has(cpuid.SSE)
)
func init() {
switch v := os.Getenv("SONIC_MODE"); v {
case "" : break
case "auto" : break
case "noavx" : HasAVX = false; fallthrough
case "noavx2" : HasAVX2 = false
default : panic(fmt.Sprintf("invalid mode: '%s', should be one of 'auto', 'noavx', 'noavx2'", v))
}
}
@@ -0,0 +1,130 @@
// +build go1.16,!go1.17
// Copyright 2023 CloudWeGo Authors
//
// 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.
package decoder
import (
`strconv`
_ `unsafe`
`github.com/bytedance/sonic/internal/jit`
`github.com/bytedance/sonic/internal/rt`
`github.com/twitchyliquid64/golang-asm/obj`
`github.com/twitchyliquid64/golang-asm/obj/x86`
)
var _runtime_writeBarrier uintptr = rt.GcwbAddr()
//go:linkname gcWriteBarrierAX runtime.gcWriteBarrier
func gcWriteBarrierAX()
var (
_V_writeBarrier = jit.Imm(int64(_runtime_writeBarrier))
_F_gcWriteBarrierAX = jit.Func(gcWriteBarrierAX)
)
func (self *_Assembler) WritePtrAX(i int, rec obj.Addr, saveDI bool) {
self.Emit("MOVQ", _V_writeBarrier, _R10)
self.Emit("CMPL", jit.Ptr(_R10, 0), jit.Imm(0))
self.Sjmp("JE", "_no_writeBarrier" + strconv.Itoa(i) + "_{n}")
if saveDI {
self.save(_DI)
}
self.Emit("LEAQ", rec, _DI)
self.Emit("MOVQ", _F_gcWriteBarrierAX, _R10) // MOVQ ${fn}, AX
self.Rjmp("CALL", _R10)
if saveDI {
self.load(_DI)
}
self.Sjmp("JMP", "_end_writeBarrier" + strconv.Itoa(i) + "_{n}")
self.Link("_no_writeBarrier" + strconv.Itoa(i) + "_{n}")
self.Emit("MOVQ", _AX, rec)
self.Link("_end_writeBarrier" + strconv.Itoa(i) + "_{n}")
}
func (self *_Assembler) WriteRecNotAX(i int, ptr obj.Addr, rec obj.Addr, saveDI bool, saveAX bool) {
if rec.Reg == x86.REG_AX || rec.Index == x86.REG_AX {
panic("rec contains AX!")
}
self.Emit("MOVQ", _V_writeBarrier, _R10)
self.Emit("CMPL", jit.Ptr(_R10, 0), jit.Imm(0))
self.Sjmp("JE", "_no_writeBarrier" + strconv.Itoa(i) + "_{n}")
if saveAX {
self.Emit("XCHGQ", ptr, _AX)
} else {
self.Emit("MOVQ", ptr, _AX)
}
if saveDI {
self.save(_DI)
}
self.Emit("LEAQ", rec, _DI)
self.Emit("MOVQ", _F_gcWriteBarrierAX, _R10) // MOVQ ${fn}, AX
self.Rjmp("CALL", _R10)
if saveDI {
self.load(_DI)
}
if saveAX {
self.Emit("XCHGQ", ptr, _AX)
}
self.Sjmp("JMP", "_end_writeBarrier" + strconv.Itoa(i) + "_{n}")
self.Link("_no_writeBarrier" + strconv.Itoa(i) + "_{n}")
self.Emit("MOVQ", ptr, rec)
self.Link("_end_writeBarrier" + strconv.Itoa(i) + "_{n}")
}
func (self *_ValueDecoder) WritePtrAX(i int, rec obj.Addr, saveDI bool) {
self.Emit("MOVQ", _V_writeBarrier, _R10)
self.Emit("CMPL", jit.Ptr(_R10, 0), jit.Imm(0))
self.Sjmp("JE", "_no_writeBarrier" + strconv.Itoa(i) + "_{n}")
if saveDI {
self.save(_DI)
}
self.Emit("LEAQ", rec, _DI)
self.Emit("MOVQ", _F_gcWriteBarrierAX, _R10) // MOVQ ${fn}, AX
self.Rjmp("CALL", _R10)
if saveDI {
self.load(_DI)
}
self.Sjmp("JMP", "_end_writeBarrier" + strconv.Itoa(i) + "_{n}")
self.Link("_no_writeBarrier" + strconv.Itoa(i) + "_{n}")
self.Emit("MOVQ", _AX, rec)
self.Link("_end_writeBarrier" + strconv.Itoa(i) + "_{n}")
}
func (self *_ValueDecoder) WriteRecNotAX(i int, ptr obj.Addr, rec obj.Addr, saveDI bool) {
if rec.Reg == x86.REG_AX || rec.Index == x86.REG_AX {
panic("rec contains AX!")
}
self.Emit("MOVQ", _V_writeBarrier, _R10)
self.Emit("CMPL", jit.Ptr(_R10, 0), jit.Imm(0))
self.Sjmp("JE", "_no_writeBarrier" + strconv.Itoa(i) + "_{n}")
self.Emit("MOVQ", ptr, _AX)
if saveDI {
self.save(_DI)
}
self.Emit("LEAQ", rec, _DI)
self.Emit("MOVQ", _F_gcWriteBarrierAX, _R10) // MOVQ ${fn}, AX
self.Rjmp("CALL", _R10)
if saveDI {
self.load(_DI)
}
self.Sjmp("JMP", "_end_writeBarrier" + strconv.Itoa(i) + "_{n}")
self.Link("_no_writeBarrier" + strconv.Itoa(i) + "_{n}")
self.Emit("MOVQ", ptr, rec)
self.Link("_end_writeBarrier" + strconv.Itoa(i) + "_{n}")
}
@@ -0,0 +1,126 @@
// +build go1.17,!go1.21
// Copyright 2023 CloudWeGo Authors
//
// 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.
package decoder
import (
`strconv`
`unsafe`
`github.com/bytedance/sonic/internal/jit`
`github.com/twitchyliquid64/golang-asm/obj`
`github.com/twitchyliquid64/golang-asm/obj/x86`
)
//go:linkname _runtime_writeBarrier runtime.writeBarrier
var _runtime_writeBarrier uintptr
//go:linkname gcWriteBarrierAX runtime.gcWriteBarrier
func gcWriteBarrierAX()
var (
_V_writeBarrier = jit.Imm(int64(uintptr(unsafe.Pointer(&_runtime_writeBarrier))))
_F_gcWriteBarrierAX = jit.Func(gcWriteBarrierAX)
)
func (self *_Assembler) WritePtrAX(i int, rec obj.Addr, saveDI bool) {
self.Emit("MOVQ", _V_writeBarrier, _R9)
self.Emit("CMPL", jit.Ptr(_R9, 0), jit.Imm(0))
self.Sjmp("JE", "_no_writeBarrier" + strconv.Itoa(i) + "_{n}")
if saveDI {
self.save(_DI)
}
self.Emit("LEAQ", rec, _DI)
self.call(_F_gcWriteBarrierAX)
if saveDI {
self.load(_DI)
}
self.Sjmp("JMP", "_end_writeBarrier" + strconv.Itoa(i) + "_{n}")
self.Link("_no_writeBarrier" + strconv.Itoa(i) + "_{n}")
self.Emit("MOVQ", _AX, rec)
self.Link("_end_writeBarrier" + strconv.Itoa(i) + "_{n}")
}
func (self *_Assembler) WriteRecNotAX(i int, ptr obj.Addr, rec obj.Addr, saveDI bool, saveAX bool) {
if rec.Reg == x86.REG_AX || rec.Index == x86.REG_AX {
panic("rec contains AX!")
}
self.Emit("MOVQ", _V_writeBarrier, _R9)
self.Emit("CMPL", jit.Ptr(_R9, 0), jit.Imm(0))
self.Sjmp("JE", "_no_writeBarrier" + strconv.Itoa(i) + "_{n}")
if saveAX {
self.Emit("XCHGQ", ptr, _AX)
} else {
self.Emit("MOVQ", ptr, _AX)
}
if saveDI {
self.save(_DI)
}
self.Emit("LEAQ", rec, _DI)
self.call(_F_gcWriteBarrierAX)
if saveDI {
self.load(_DI)
}
if saveAX {
self.Emit("XCHGQ", ptr, _AX)
}
self.Sjmp("JMP", "_end_writeBarrier" + strconv.Itoa(i) + "_{n}")
self.Link("_no_writeBarrier" + strconv.Itoa(i) + "_{n}")
self.Emit("MOVQ", ptr, rec)
self.Link("_end_writeBarrier" + strconv.Itoa(i) + "_{n}")
}
func (self *_ValueDecoder) WritePtrAX(i int, rec obj.Addr, saveDI bool) {
self.Emit("MOVQ", _V_writeBarrier, _R9)
self.Emit("CMPL", jit.Ptr(_R9, 0), jit.Imm(0))
self.Sjmp("JE", "_no_writeBarrier" + strconv.Itoa(i) + "_{n}")
if saveDI {
self.save(_DI)
}
self.Emit("LEAQ", rec, _DI)
self.call(_F_gcWriteBarrierAX)
if saveDI {
self.load(_DI)
}
self.Sjmp("JMP", "_end_writeBarrier" + strconv.Itoa(i) + "_{n}")
self.Link("_no_writeBarrier" + strconv.Itoa(i) + "_{n}")
self.Emit("MOVQ", _AX, rec)
self.Link("_end_writeBarrier" + strconv.Itoa(i) + "_{n}")
}
func (self *_ValueDecoder) WriteRecNotAX(i int, ptr obj.Addr, rec obj.Addr, saveDI bool) {
if rec.Reg == x86.REG_AX || rec.Index == x86.REG_AX {
panic("rec contains AX!")
}
self.Emit("MOVQ", _V_writeBarrier, _AX)
self.Emit("CMPL", jit.Ptr(_AX, 0), jit.Imm(0))
self.Sjmp("JE", "_no_writeBarrier" + strconv.Itoa(i) + "_{n}")
self.Emit("MOVQ", ptr, _AX)
if saveDI {
self.save(_DI)
}
self.Emit("LEAQ", rec, _DI)
self.call(_F_gcWriteBarrierAX)
if saveDI {
self.load(_DI)
}
self.Sjmp("JMP", "_end_writeBarrier" + strconv.Itoa(i) + "_{n}")
self.Link("_no_writeBarrier" + strconv.Itoa(i) + "_{n}")
self.Emit("MOVQ", ptr, rec)
self.Link("_end_writeBarrier" + strconv.Itoa(i) + "_{n}")
}
@@ -0,0 +1,132 @@
// +build go1.21,!go1.23
// Copyright 2023 CloudWeGo Authors
//
// 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.
package decoder
import (
`strconv`
`unsafe`
`github.com/bytedance/sonic/internal/jit`
`github.com/twitchyliquid64/golang-asm/obj`
`github.com/twitchyliquid64/golang-asm/obj/x86`
)
//go:linkname _runtime_writeBarrier runtime.writeBarrier
var _runtime_writeBarrier uintptr
//go:nosplit
//go:linkname gcWriteBarrier2 runtime.gcWriteBarrier2
func gcWriteBarrier2()
// Notice: gcWriteBarrier must use R11 register!!
var _R11 = _IC
var (
_V_writeBarrier = jit.Imm(int64(uintptr(unsafe.Pointer(&_runtime_writeBarrier))))
_F_gcWriteBarrier2 = jit.Func(gcWriteBarrier2)
)
func (self *_Assembler) WritePtrAX(i int, rec obj.Addr, saveDI bool) {
self.Emit("MOVQ", _V_writeBarrier, _R9)
self.Emit("CMPL", jit.Ptr(_R9, 0), jit.Imm(0))
self.Sjmp("JE", "_no_writeBarrier" + strconv.Itoa(i) + "_{n}")
if saveDI {
self.save(_DI, _R11)
} else {
self.save(_R11)
}
self.Emit("MOVQ", _F_gcWriteBarrier2, _R11)
self.Rjmp("CALL", _R11)
self.Emit("MOVQ", _AX, jit.Ptr(_R11, 0))
self.Emit("MOVQ", rec, _DI)
self.Emit("MOVQ", _DI, jit.Ptr(_R11, 8))
if saveDI {
self.load(_DI, _R11)
} else {
self.load(_R11)
}
self.Link("_no_writeBarrier" + strconv.Itoa(i) + "_{n}")
self.Emit("MOVQ", _AX, rec)
}
func (self *_Assembler) WriteRecNotAX(i int, ptr obj.Addr, rec obj.Addr, saveDI bool, saveAX bool) {
if rec.Reg == x86.REG_AX || rec.Index == x86.REG_AX {
panic("rec contains AX!")
}
self.Emit("MOVQ", _V_writeBarrier, _R9)
self.Emit("CMPL", jit.Ptr(_R9, 0), jit.Imm(0))
self.Sjmp("JE", "_no_writeBarrier" + strconv.Itoa(i) + "_{n}")
if saveAX {
self.save(_AX, _R11)
} else {
self.save(_R11)
}
self.Emit("MOVQ", _F_gcWriteBarrier2, _R11)
self.Rjmp("CALL", _R11)
self.Emit("MOVQ", ptr, jit.Ptr(_R11, 0))
self.Emit("MOVQ", rec, _AX)
self.Emit("MOVQ", _AX, jit.Ptr(_R11, 8))
if saveAX {
self.load(_AX, _R11)
} else {
self.load(_R11)
}
self.Link("_no_writeBarrier" + strconv.Itoa(i) + "_{n}")
self.Emit("MOVQ", ptr, rec)
}
func (self *_ValueDecoder) WritePtrAX(i int, rec obj.Addr, saveDI bool) {
self.Emit("MOVQ", _V_writeBarrier, _R9)
self.Emit("CMPL", jit.Ptr(_R9, 0), jit.Imm(0))
self.Sjmp("JE", "_no_writeBarrier" + strconv.Itoa(i) + "_{n}")
if saveDI {
self.save(_DI, _R11)
} else {
self.save(_R11)
}
self.Emit("MOVQ", _F_gcWriteBarrier2, _R11)
self.Rjmp("CALL", _R11)
self.Emit("MOVQ", _AX, jit.Ptr(_R11, 0))
self.Emit("MOVQ", rec, _DI)
self.Emit("MOVQ", _DI, jit.Ptr(_R11, 8))
if saveDI {
self.load(_DI, _R11)
} else {
self.load(_R11)
}
self.Link("_no_writeBarrier" + strconv.Itoa(i) + "_{n}")
self.Emit("MOVQ", _AX, rec)
}
func (self *_ValueDecoder) WriteRecNotAX(i int, ptr obj.Addr, rec obj.Addr, saveDI bool) {
if rec.Reg == x86.REG_AX || rec.Index == x86.REG_AX {
panic("rec contains AX!")
}
self.Emit("MOVQ", _V_writeBarrier, _AX)
self.Emit("CMPL", jit.Ptr(_AX, 0), jit.Imm(0))
self.Sjmp("JE", "_no_writeBarrier" + strconv.Itoa(i) + "_{n}")
self.save(_R11)
self.Emit("MOVQ", _F_gcWriteBarrier2, _R11)
self.Rjmp("CALL", _R11)
self.Emit("MOVQ", ptr, jit.Ptr(_R11, 0))
self.Emit("MOVQ", rec, _AX)
self.Emit("MOVQ", _AX, jit.Ptr(_R11, 8))
self.load(_R11)
self.Link("_no_writeBarrier" + strconv.Itoa(i) + "_{n}")
self.Emit("MOVQ", ptr, rec)
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,70 @@
/*
* Copyright 2021 ByteDance Inc.
*
* 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.
*/
package decoder
import (
`os`
`runtime`
`runtime/debug`
`strings`
`github.com/bytedance/sonic/internal/jit`
)
var (
debugSyncGC = os.Getenv("SONIC_SYNC_GC") != ""
debugAsyncGC = os.Getenv("SONIC_NO_ASYNC_GC") == ""
)
var (
_Instr_End _Instr = newInsOp(_OP_nil_1)
_F_gc = jit.Func(runtime.GC)
_F_force_gc = jit.Func(debug.FreeOSMemory)
_F_println = jit.Func(println_wrapper)
_F_print = jit.Func(print)
)
func println_wrapper(i int, op1 int, op2 int){
println(i, " Intrs ", op1, _OpNames[op1], "next: ", op2, _OpNames[op2])
}
func print(i int){
println(i)
}
func (self *_Assembler) force_gc() {
self.call_go(_F_gc)
self.call_go(_F_force_gc)
}
func (self *_Assembler) debug_instr(i int, v *_Instr) {
if debugSyncGC {
if (i+1 == len(self.p)) {
self.print_gc(i, v, &_Instr_End)
} else {
next := &(self.p[i+1])
self.print_gc(i, v, next)
name := _OpNames[next.op()]
if strings.Contains(name, "save") {
return
}
}
self.force_gc()
}
}
@@ -0,0 +1,255 @@
/*
* Copyright 2021 ByteDance Inc.
*
* 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.
*/
package decoder
import (
`unsafe`
`encoding/json`
`reflect`
`runtime`
`github.com/bytedance/sonic/internal/native`
`github.com/bytedance/sonic/internal/native/types`
`github.com/bytedance/sonic/internal/rt`
`github.com/bytedance/sonic/option`
`github.com/bytedance/sonic/utf8`
)
const (
_F_use_int64 = 0
_F_disable_urc = 2
_F_disable_unknown = 3
_F_copy_string = 4
_F_use_number = types.B_USE_NUMBER
_F_validate_string = types.B_VALIDATE_STRING
_F_allow_control = types.B_ALLOW_CONTROL
)
type Options uint64
const (
OptionUseInt64 Options = 1 << _F_use_int64
OptionUseNumber Options = 1 << _F_use_number
OptionUseUnicodeErrors Options = 1 << _F_disable_urc
OptionDisableUnknown Options = 1 << _F_disable_unknown
OptionCopyString Options = 1 << _F_copy_string
OptionValidateString Options = 1 << _F_validate_string
)
func (self *Decoder) SetOptions(opts Options) {
if (opts & OptionUseNumber != 0) && (opts & OptionUseInt64 != 0) {
panic("can't set OptionUseInt64 and OptionUseNumber both!")
}
self.f = uint64(opts)
}
// Decoder is the decoder context object
type Decoder struct {
i int
f uint64
s string
}
// NewDecoder creates a new decoder instance.
func NewDecoder(s string) *Decoder {
return &Decoder{s: s}
}
// Pos returns the current decoding position.
func (self *Decoder) Pos() int {
return self.i
}
func (self *Decoder) Reset(s string) {
self.s = s
self.i = 0
// self.f = 0
}
func (self *Decoder) CheckTrailings() error {
pos := self.i
buf := self.s
/* skip all the trailing spaces */
if pos != len(buf) {
for pos < len(buf) && (types.SPACE_MASK & (1 << buf[pos])) != 0 {
pos++
}
}
/* then it must be at EOF */
if pos == len(buf) {
return nil
}
/* junk after JSON value */
return SyntaxError {
Src : buf,
Pos : pos,
Code : types.ERR_INVALID_CHAR,
}
}
// Decode parses the JSON-encoded data from current position and stores the result
// in the value pointed to by val.
func (self *Decoder) Decode(val interface{}) error {
/* validate json if needed */
if (self.f & (1 << _F_validate_string)) != 0 && !utf8.ValidateString(self.s){
dbuf := utf8.CorrectWith(nil, rt.Str2Mem(self.s), "\ufffd")
self.s = rt.Mem2Str(dbuf)
}
vv := rt.UnpackEface(val)
vp := vv.Value
/* check for nil type */
if vv.Type == nil {
return &json.InvalidUnmarshalError{}
}
/* must be a non-nil pointer */
if vp == nil || vv.Type.Kind() != reflect.Ptr {
return &json.InvalidUnmarshalError{Type: vv.Type.Pack()}
}
etp := rt.PtrElem(vv.Type)
/* check the defined pointer type for issue 379 */
if vv.Type.IsNamed() {
newp := vp
etp = vv.Type
vp = unsafe.Pointer(&newp)
}
/* create a new stack, and call the decoder */
sb := newStack()
nb, err := decodeTypedPointer(self.s, self.i, etp, vp, sb, self.f)
/* return the stack back */
self.i = nb
freeStack(sb)
/* avoid GC ahead */
runtime.KeepAlive(vv)
return err
}
// UseInt64 indicates the Decoder to unmarshal an integer into an interface{} as an
// int64 instead of as a float64.
func (self *Decoder) UseInt64() {
self.f |= 1 << _F_use_int64
self.f &^= 1 << _F_use_number
}
// UseNumber indicates the Decoder to unmarshal a number into an interface{} as a
// json.Number instead of as a float64.
func (self *Decoder) UseNumber() {
self.f &^= 1 << _F_use_int64
self.f |= 1 << _F_use_number
}
// UseUnicodeErrors indicates the Decoder to return an error when encounter invalid
// UTF-8 escape sequences.
func (self *Decoder) UseUnicodeErrors() {
self.f |= 1 << _F_disable_urc
}
// DisallowUnknownFields indicates the Decoder to return an error when the destination
// is a struct and the input contains object keys which do not match any
// non-ignored, exported fields in the destination.
func (self *Decoder) DisallowUnknownFields() {
self.f |= 1 << _F_disable_unknown
}
// CopyString indicates the Decoder to decode string values by copying instead of referring.
func (self *Decoder) CopyString() {
self.f |= 1 << _F_copy_string
}
// ValidateString causes the Decoder to validate string values when decoding string value
// in JSON. Validation is that, returning error when unescaped control chars(0x00-0x1f) or
// invalid UTF-8 chars in the string value of JSON.
func (self *Decoder) ValidateString() {
self.f |= 1 << _F_validate_string
}
// Pretouch compiles vt ahead-of-time to avoid JIT compilation on-the-fly, in
// order to reduce the first-hit latency.
//
// Opts are the compile options, for example, "option.WithCompileRecursiveDepth" is
// a compile option to set the depth of recursive compile for the nested struct type.
func Pretouch(vt reflect.Type, opts ...option.CompileOption) error {
cfg := option.DefaultCompileOptions()
for _, opt := range opts {
opt(&cfg)
}
return pretouchRec(map[reflect.Type]bool{vt:true}, cfg)
}
func pretouchType(_vt reflect.Type, opts option.CompileOptions) (map[reflect.Type]bool, error) {
/* compile function */
compiler := newCompiler().apply(opts)
decoder := func(vt *rt.GoType, _ ...interface{}) (interface{}, error) {
if pp, err := compiler.compile(_vt); err != nil {
return nil, err
} else {
as := newAssembler(pp)
as.name = _vt.String()
return as.Load(), nil
}
}
/* find or compile */
vt := rt.UnpackType(_vt)
if val := programCache.Get(vt); val != nil {
return nil, nil
} else if _, err := programCache.Compute(vt, decoder); err == nil {
return compiler.rec, nil
} else {
return nil, err
}
}
func pretouchRec(vtm map[reflect.Type]bool, opts option.CompileOptions) error {
if opts.RecursiveDepth < 0 || len(vtm) == 0 {
return nil
}
next := make(map[reflect.Type]bool)
for vt := range(vtm) {
sub, err := pretouchType(vt, opts)
if err != nil {
return err
}
for svt := range(sub) {
next[svt] = true
}
}
opts.RecursiveDepth -= 1
return pretouchRec(next, opts)
}
// Skip skips only one json value, and returns first non-blank character position and its ending position if it is valid.
// Otherwise, returns negative error code using start and invalid character position using end
func Skip(data []byte) (start int, end int) {
s := rt.Mem2Str(data)
p := 0
m := types.NewStateMachine()
ret := native.SkipOne(&s, &p, m, uint64(0))
types.FreeStateMachine(m)
return ret, p
}
@@ -0,0 +1,191 @@
/*
* Copyright 2021 ByteDance Inc.
*
* 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.
*/
package decoder
import (
`encoding/json`
`errors`
`fmt`
`reflect`
`strconv`
`strings`
`github.com/bytedance/sonic/internal/native/types`
`github.com/bytedance/sonic/internal/rt`
)
type SyntaxError struct {
Pos int
Src string
Code types.ParsingError
Msg string
}
func (self SyntaxError) Error() string {
return fmt.Sprintf("%q", self.Description())
}
func (self SyntaxError) Description() string {
return "Syntax error " + self.description()
}
func (self SyntaxError) description() string {
/* check for empty source */
if self.Src == "" {
return fmt.Sprintf("no sources available: %#v", self)
}
p, x, q, y := calcBounds(len(self.Src), self.Pos)
/* compose the error description */
return fmt.Sprintf(
"at index %d: %s\n\n\t%s\n\t%s^%s\n",
self.Pos,
self.Message(),
self.Src[p:q],
strings.Repeat(".", x),
strings.Repeat(".", y),
)
}
func calcBounds(size int, pos int) (lbound int, lwidth int, rbound int, rwidth int) {
if pos >= size || pos < 0 {
return 0, 0, size, 0
}
i := 16
lbound = pos - i
rbound = pos + i
/* prevent slicing before the beginning */
if lbound < 0 {
lbound, rbound, i = 0, rbound - lbound, i + lbound
}
/* prevent slicing beyond the end */
if n := size; rbound > n {
n = rbound - n
rbound = size
/* move the left bound if possible */
if lbound > n {
i += n
lbound -= n
}
}
/* left and right length */
lwidth = clamp_zero(i)
rwidth = clamp_zero(rbound - lbound - i - 1)
return
}
func (self SyntaxError) Message() string {
if self.Msg == "" {
return self.Code.Message()
}
return self.Msg
}
func clamp_zero(v int) int {
if v < 0 {
return 0
} else {
return v
}
}
/** JIT Error Helpers **/
var stackOverflow = &json.UnsupportedValueError {
Str : "Value nesting too deep",
Value : reflect.ValueOf("..."),
}
func error_wrap(src string, pos int, code types.ParsingError) error {
return *error_wrap_heap(src, pos, code)
}
//go:noinline
func error_wrap_heap(src string, pos int, code types.ParsingError) *SyntaxError {
return &SyntaxError {
Pos : pos,
Src : src,
Code : code,
}
}
func error_type(vt *rt.GoType) error {
return &json.UnmarshalTypeError{Type: vt.Pack()}
}
type MismatchTypeError struct {
Pos int
Src string
Type reflect.Type
}
func swithchJSONType (src string, pos int) string {
var val string
switch src[pos] {
case 'f': fallthrough
case 't': val = "bool"
case '"': val = "string"
case '{': val = "object"
case '[': val = "array"
case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': val = "number"
}
return val
}
func (self MismatchTypeError) Error() string {
se := SyntaxError {
Pos : self.Pos,
Src : self.Src,
Code : types.ERR_MISMATCH,
}
return fmt.Sprintf("Mismatch type %s with value %s %q", self.Type.String(), swithchJSONType(self.Src, self.Pos), se.description())
}
func (self MismatchTypeError) Description() string {
se := SyntaxError {
Pos : self.Pos,
Src : self.Src,
Code : types.ERR_MISMATCH,
}
return fmt.Sprintf("Mismatch type %s with value %s %s", self.Type.String(), swithchJSONType(self.Src, self.Pos), se.description())
}
func error_mismatch(src string, pos int, vt *rt.GoType) error {
return &MismatchTypeError {
Pos : pos,
Src : src,
Type : vt.Pack(),
}
}
func error_field(name string) error {
return errors.New("json: unknown field " + strconv.Quote(name))
}
func error_value(value string, vtype reflect.Type) error {
return &json.UnmarshalTypeError {
Type : vtype,
Value : value,
}
}
@@ -0,0 +1,729 @@
// +build go1.17,!go1.23
/*
* Copyright 2021 ByteDance Inc.
*
* 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.
*/
package decoder
import (
`encoding/json`
`fmt`
`reflect`
`github.com/bytedance/sonic/internal/jit`
`github.com/bytedance/sonic/internal/native`
`github.com/bytedance/sonic/internal/native/types`
`github.com/twitchyliquid64/golang-asm/obj`
)
/** Crucial Registers:
*
* ST(R13) && 0(SP) : ro, decoder stack
* DF(AX) : ro, decoder flags
* EP(BX) : wo, error pointer
* IP(R10) : ro, input pointer
* IL(R12) : ro, input length
* IC(R11) : rw, input cursor
* VP(R15) : ro, value pointer (to an interface{})
*/
const (
_VD_args = 8 // 8 bytes for passing arguments to this functions
_VD_fargs = 64 // 64 bytes for passing arguments to other Go functions
_VD_saves = 48 // 48 bytes for saving the registers before CALL instructions
_VD_locals = 96 // 96 bytes for local variables
)
const (
_VD_offs = _VD_fargs + _VD_saves + _VD_locals
_VD_size = _VD_offs + 8 // 8 bytes for the parent frame pointer
)
var (
_VAR_ss = _VAR_ss_Vt
_VAR_df = jit.Ptr(_SP, _VD_fargs + _VD_saves)
)
var (
_VAR_ss_Vt = jit.Ptr(_SP, _VD_fargs + _VD_saves + 8)
_VAR_ss_Dv = jit.Ptr(_SP, _VD_fargs + _VD_saves + 16)
_VAR_ss_Iv = jit.Ptr(_SP, _VD_fargs + _VD_saves + 24)
_VAR_ss_Ep = jit.Ptr(_SP, _VD_fargs + _VD_saves + 32)
_VAR_ss_Db = jit.Ptr(_SP, _VD_fargs + _VD_saves + 40)
_VAR_ss_Dc = jit.Ptr(_SP, _VD_fargs + _VD_saves + 48)
)
var (
_VAR_R9 = jit.Ptr(_SP, _VD_fargs + _VD_saves + 56)
)
type _ValueDecoder struct {
jit.BaseAssembler
}
var (
_VAR_cs_LR = jit.Ptr(_SP, _VD_fargs + _VD_saves + 64)
_VAR_cs_p = jit.Ptr(_SP, _VD_fargs + _VD_saves + 72)
_VAR_cs_n = jit.Ptr(_SP, _VD_fargs + _VD_saves + 80)
_VAR_cs_d = jit.Ptr(_SP, _VD_fargs + _VD_saves + 88)
)
func (self *_ValueDecoder) build() uintptr {
self.Init(self.compile)
return *(*uintptr)(self.Load("decode_value", _VD_size, _VD_args, argPtrs_generic, localPtrs_generic))
}
/** Function Calling Helpers **/
func (self *_ValueDecoder) save(r ...obj.Addr) {
for i, v := range r {
if i > _VD_saves / 8 - 1 {
panic("too many registers to save")
} else {
self.Emit("MOVQ", v, jit.Ptr(_SP, _VD_fargs + int64(i) * 8))
}
}
}
func (self *_ValueDecoder) load(r ...obj.Addr) {
for i, v := range r {
if i > _VD_saves / 8 - 1 {
panic("too many registers to load")
} else {
self.Emit("MOVQ", jit.Ptr(_SP, _VD_fargs + int64(i) * 8), v)
}
}
}
func (self *_ValueDecoder) call(fn obj.Addr) {
self.Emit("MOVQ", fn, _R9) // MOVQ ${fn}, AX
self.Rjmp("CALL", _R9) // CALL AX
}
func (self *_ValueDecoder) call_go(fn obj.Addr) {
self.save(_REG_go...) // SAVE $REG_go
self.call(fn) // CALL ${fn}
self.load(_REG_go...) // LOAD $REG_go
}
func (self *_ValueDecoder) callc(fn obj.Addr) {
self.save(_IP)
self.call(fn)
self.load(_IP)
}
func (self *_ValueDecoder) call_c(fn obj.Addr) {
self.Emit("XCHGQ", _IC, _BX)
self.callc(fn)
self.Emit("XCHGQ", _IC, _BX)
}
/** Decoder Assembler **/
const (
_S_val = iota + 1
_S_arr
_S_arr_0
_S_obj
_S_obj_0
_S_obj_delim
_S_obj_sep
)
const (
_S_omask_key = (1 << _S_obj_0) | (1 << _S_obj_sep)
_S_omask_end = (1 << _S_obj_0) | (1 << _S_obj)
_S_vmask = (1 << _S_val) | (1 << _S_arr_0)
)
const (
_A_init_len = 1
_A_init_cap = 16
)
const (
_ST_Sp = 0
_ST_Vt = _PtrBytes
_ST_Vp = _PtrBytes * (types.MAX_RECURSE + 1)
)
var (
_V_true = jit.Imm(int64(pbool(true)))
_V_false = jit.Imm(int64(pbool(false)))
_F_value = jit.Imm(int64(native.S_value))
)
var (
_V_max = jit.Imm(int64(types.V_MAX))
_E_eof = jit.Imm(int64(types.ERR_EOF))
_E_invalid = jit.Imm(int64(types.ERR_INVALID_CHAR))
_E_recurse = jit.Imm(int64(types.ERR_RECURSE_EXCEED_MAX))
)
var (
_F_convTslice = jit.Func(convTslice)
_F_convTstring = jit.Func(convTstring)
_F_invalid_vtype = jit.Func(invalid_vtype)
)
var (
_T_map = jit.Type(reflect.TypeOf((map[string]interface{})(nil)))
_T_bool = jit.Type(reflect.TypeOf(false))
_T_int64 = jit.Type(reflect.TypeOf(int64(0)))
_T_eface = jit.Type(reflect.TypeOf((*interface{})(nil)).Elem())
_T_slice = jit.Type(reflect.TypeOf(([]interface{})(nil)))
_T_string = jit.Type(reflect.TypeOf(""))
_T_number = jit.Type(reflect.TypeOf(json.Number("")))
_T_float64 = jit.Type(reflect.TypeOf(float64(0)))
)
var _R_tab = map[int]string {
'[': "_decode_V_ARRAY",
'{': "_decode_V_OBJECT",
':': "_decode_V_KEY_SEP",
',': "_decode_V_ELEM_SEP",
']': "_decode_V_ARRAY_END",
'}': "_decode_V_OBJECT_END",
}
func (self *_ValueDecoder) compile() {
self.Emit("SUBQ", jit.Imm(_VD_size), _SP) // SUBQ $_VD_size, SP
self.Emit("MOVQ", _BP, jit.Ptr(_SP, _VD_offs)) // MOVQ BP, _VD_offs(SP)
self.Emit("LEAQ", jit.Ptr(_SP, _VD_offs), _BP) // LEAQ _VD_offs(SP), BP
/* initialize the state machine */
self.Emit("XORL", _CX, _CX) // XORL CX, CX
self.Emit("MOVQ", _DF, _VAR_df) // MOVQ DF, df
/* initialize digital buffer first */
self.Emit("MOVQ", jit.Imm(_MaxDigitNums), _VAR_ss_Dc) // MOVQ $_MaxDigitNums, ss.Dcap
self.Emit("LEAQ", jit.Ptr(_ST, _DbufOffset), _AX) // LEAQ _DbufOffset(ST), AX
self.Emit("MOVQ", _AX, _VAR_ss_Db) // MOVQ AX, ss.Dbuf
/* add ST offset */
self.Emit("ADDQ", jit.Imm(_FsmOffset), _ST) // ADDQ _FsmOffset, _ST
self.Emit("MOVQ", _CX, jit.Ptr(_ST, _ST_Sp)) // MOVQ CX, ST.Sp
self.WriteRecNotAX(0, _VP, jit.Ptr(_ST, _ST_Vp), false) // MOVQ VP, ST.Vp[0]
self.Emit("MOVQ", jit.Imm(_S_val), jit.Ptr(_ST, _ST_Vt)) // MOVQ _S_val, ST.Vt[0]
self.Sjmp("JMP" , "_next") // JMP _next
/* set the value from previous round */
self.Link("_set_value") // _set_value:
self.Emit("MOVL" , jit.Imm(_S_vmask), _DX) // MOVL _S_vmask, DX
self.Emit("MOVQ" , jit.Ptr(_ST, _ST_Sp), _CX) // MOVQ ST.Sp, CX
self.Emit("MOVQ" , jit.Sib(_ST, _CX, 8, _ST_Vt), _AX) // MOVQ ST.Vt[CX], AX
self.Emit("BTQ" , _AX, _DX) // BTQ AX, DX
self.Sjmp("JNC" , "_vtype_error") // JNC _vtype_error
self.Emit("XORL" , _SI, _SI) // XORL SI, SI
self.Emit("SUBQ" , jit.Imm(1), jit.Ptr(_ST, _ST_Sp)) // SUBQ $1, ST.Sp
self.Emit("XCHGQ", jit.Sib(_ST, _CX, 8, _ST_Vp), _SI) // XCHGQ ST.Vp[CX], SI
self.Emit("MOVQ" , _R8, jit.Ptr(_SI, 0)) // MOVQ R8, (SI)
self.WriteRecNotAX(1, _R9, jit.Ptr(_SI, 8), false) // MOVQ R9, 8(SI)
/* check for value stack */
self.Link("_next") // _next:
self.Emit("MOVQ" , jit.Ptr(_ST, _ST_Sp), _AX) // MOVQ ST.Sp, AX
self.Emit("TESTQ", _AX, _AX) // TESTQ AX, AX
self.Sjmp("JS" , "_return") // JS _return
/* fast path: test up to 4 characters manually */
self.Emit("CMPQ" , _IC, _IL) // CMPQ IC, IL
self.Sjmp("JAE" , "_decode_V_EOF") // JAE _decode_V_EOF
self.Emit("MOVBQZX", jit.Sib(_IP, _IC, 1, 0), _AX) // MOVBQZX (IP)(IC), AX
self.Emit("MOVQ" , jit.Imm(_BM_space), _DX) // MOVQ _BM_space, DX
self.Emit("CMPQ" , _AX, jit.Imm(' ')) // CMPQ AX, $' '
self.Sjmp("JA" , "_decode_fast") // JA _decode_fast
self.Emit("BTQ" , _AX, _DX) // BTQ _AX, _DX
self.Sjmp("JNC" , "_decode_fast") // JNC _decode_fast
self.Emit("ADDQ" , jit.Imm(1), _IC) // ADDQ $1, IC
/* at least 1 to 3 spaces */
for i := 0; i < 3; i++ {
self.Emit("CMPQ" , _IC, _IL) // CMPQ IC, IL
self.Sjmp("JAE" , "_decode_V_EOF") // JAE _decode_V_EOF
self.Emit("MOVBQZX", jit.Sib(_IP, _IC, 1, 0), _AX) // MOVBQZX (IP)(IC), AX
self.Emit("CMPQ" , _AX, jit.Imm(' ')) // CMPQ AX, $' '
self.Sjmp("JA" , "_decode_fast") // JA _decode_fast
self.Emit("BTQ" , _AX, _DX) // BTQ _AX, _DX
self.Sjmp("JNC" , "_decode_fast") // JNC _decode_fast
self.Emit("ADDQ" , jit.Imm(1), _IC) // ADDQ $1, IC
}
/* at least 4 spaces */
self.Emit("CMPQ" , _IC, _IL) // CMPQ IC, IL
self.Sjmp("JAE" , "_decode_V_EOF") // JAE _decode_V_EOF
self.Emit("MOVBQZX", jit.Sib(_IP, _IC, 1, 0), _AX) // MOVBQZX (IP)(IC), AX
/* fast path: use lookup table to select decoder */
self.Link("_decode_fast") // _decode_fast:
self.Byte(0x48, 0x8d, 0x3d) // LEAQ ?(PC), DI
self.Sref("_decode_tab", 4) // .... &_decode_tab
self.Emit("MOVLQSX", jit.Sib(_DI, _AX, 4, 0), _AX) // MOVLQSX (DI)(AX*4), AX
self.Emit("TESTQ" , _AX, _AX) // TESTQ AX, AX
self.Sjmp("JZ" , "_decode_native") // JZ _decode_native
self.Emit("ADDQ" , jit.Imm(1), _IC) // ADDQ $1, IC
self.Emit("ADDQ" , _DI, _AX) // ADDQ DI, AX
self.Rjmp("JMP" , _AX) // JMP AX
/* decode with native decoder */
self.Link("_decode_native") // _decode_native:
self.Emit("MOVQ", _IP, _DI) // MOVQ IP, DI
self.Emit("MOVQ", _IL, _SI) // MOVQ IL, SI
self.Emit("MOVQ", _IC, _DX) // MOVQ IC, DX
self.Emit("LEAQ", _VAR_ss, _CX) // LEAQ ss, CX
self.Emit("MOVQ", _VAR_df, _R8) // MOVQ $df, R8
self.Emit("BTSQ", jit.Imm(_F_allow_control), _R8) // ANDQ $1<<_F_allow_control, R8
self.callc(_F_value) // CALL value
self.Emit("MOVQ", _AX, _IC) // MOVQ AX, IC
/* check for errors */
self.Emit("MOVQ" , _VAR_ss_Vt, _AX) // MOVQ ss.Vt, AX
self.Emit("TESTQ", _AX, _AX) // TESTQ AX, AX
self.Sjmp("JS" , "_parsing_error")
self.Sjmp("JZ" , "_invalid_vtype") // JZ _invalid_vtype
self.Emit("CMPQ" , _AX, _V_max) // CMPQ AX, _V_max
self.Sjmp("JA" , "_invalid_vtype") // JA _invalid_vtype
/* jump table selector */
self.Byte(0x48, 0x8d, 0x3d) // LEAQ ?(PC), DI
self.Sref("_switch_table", 4) // .... &_switch_table
self.Emit("MOVLQSX", jit.Sib(_DI, _AX, 4, -4), _AX) // MOVLQSX -4(DI)(AX*4), AX
self.Emit("ADDQ" , _DI, _AX) // ADDQ DI, AX
self.Rjmp("JMP" , _AX) // JMP AX
/** V_EOF **/
self.Link("_decode_V_EOF") // _decode_V_EOF:
self.Emit("MOVL", _E_eof, _EP) // MOVL _E_eof, EP
self.Sjmp("JMP" , "_error") // JMP _error
/** V_NULL **/
self.Link("_decode_V_NULL") // _decode_V_NULL:
self.Emit("XORL", _R8, _R8) // XORL R8, R8
self.Emit("XORL", _R9, _R9) // XORL R9, R9
self.Emit("LEAQ", jit.Ptr(_IC, -4), _DI) // LEAQ -4(IC), DI
self.Sjmp("JMP" , "_set_value") // JMP _set_value
/** V_TRUE **/
self.Link("_decode_V_TRUE") // _decode_V_TRUE:
self.Emit("MOVQ", _T_bool, _R8) // MOVQ _T_bool, R8
// TODO: maybe modified by users?
self.Emit("MOVQ", _V_true, _R9) // MOVQ _V_true, R9
self.Emit("LEAQ", jit.Ptr(_IC, -4), _DI) // LEAQ -4(IC), DI
self.Sjmp("JMP" , "_set_value") // JMP _set_value
/** V_FALSE **/
self.Link("_decode_V_FALSE") // _decode_V_FALSE:
self.Emit("MOVQ", _T_bool, _R8) // MOVQ _T_bool, R8
self.Emit("MOVQ", _V_false, _R9) // MOVQ _V_false, R9
self.Emit("LEAQ", jit.Ptr(_IC, -5), _DI) // LEAQ -5(IC), DI
self.Sjmp("JMP" , "_set_value") // JMP _set_value
/** V_ARRAY **/
self.Link("_decode_V_ARRAY") // _decode_V_ARRAY
self.Emit("MOVL", jit.Imm(_S_vmask), _DX) // MOVL _S_vmask, DX
self.Emit("MOVQ", jit.Ptr(_ST, _ST_Sp), _CX) // MOVQ ST.Sp, CX
self.Emit("MOVQ", jit.Sib(_ST, _CX, 8, _ST_Vt), _AX) // MOVQ ST.Vt[CX], AX
self.Emit("BTQ" , _AX, _DX) // BTQ AX, DX
self.Sjmp("JNC" , "_invalid_char") // JNC _invalid_char
/* create a new array */
self.Emit("MOVQ", _T_eface, _AX) // MOVQ _T_eface, AX
self.Emit("MOVQ", jit.Imm(_A_init_len), _BX) // MOVQ _A_init_len, BX
self.Emit("MOVQ", jit.Imm(_A_init_cap), _CX) // MOVQ _A_init_cap, CX
self.call_go(_F_makeslice) // CALL_GO runtime.makeslice
/* pack into an interface */
self.Emit("MOVQ", jit.Imm(_A_init_len), _BX) // MOVQ _A_init_len, BX
self.Emit("MOVQ", jit.Imm(_A_init_cap), _CX) // MOVQ _A_init_cap, CX
self.call_go(_F_convTslice) // CALL_GO runtime.convTslice
self.Emit("MOVQ", _AX, _R8) // MOVQ AX, R8
/* replace current state with an array */
self.Emit("MOVQ", jit.Ptr(_ST, _ST_Sp), _CX) // MOVQ ST.Sp, CX
self.Emit("MOVQ", jit.Sib(_ST, _CX, 8, _ST_Vp), _SI) // MOVQ ST.Vp[CX], SI
self.Emit("MOVQ", jit.Imm(_S_arr), jit.Sib(_ST, _CX, 8, _ST_Vt)) // MOVQ _S_arr, ST.Vt[CX]
self.Emit("MOVQ", _T_slice, _AX) // MOVQ _T_slice, AX
self.Emit("MOVQ", _AX, jit.Ptr(_SI, 0)) // MOVQ AX, (SI)
self.WriteRecNotAX(2, _R8, jit.Ptr(_SI, 8), false) // MOVQ R8, 8(SI)
/* add a new slot for the first element */
self.Emit("ADDQ", jit.Imm(1), _CX) // ADDQ $1, CX
self.Emit("CMPQ", _CX, jit.Imm(types.MAX_RECURSE)) // CMPQ CX, ${types.MAX_RECURSE}
self.Sjmp("JAE" , "_stack_overflow") // JA _stack_overflow
self.Emit("MOVQ", jit.Ptr(_R8, 0), _AX) // MOVQ (R8), AX
self.Emit("MOVQ", _CX, jit.Ptr(_ST, _ST_Sp)) // MOVQ CX, ST.Sp
self.WritePtrAX(3, jit.Sib(_ST, _CX, 8, _ST_Vp), false) // MOVQ AX, ST.Vp[CX]
self.Emit("MOVQ", jit.Imm(_S_arr_0), jit.Sib(_ST, _CX, 8, _ST_Vt)) // MOVQ _S_arr_0, ST.Vt[CX]
self.Sjmp("JMP" , "_next") // JMP _next
/** V_OBJECT **/
self.Link("_decode_V_OBJECT") // _decode_V_OBJECT:
self.Emit("MOVL", jit.Imm(_S_vmask), _DX) // MOVL _S_vmask, DX
self.Emit("MOVQ", jit.Ptr(_ST, _ST_Sp), _CX) // MOVQ ST.Sp, CX
self.Emit("MOVQ", jit.Sib(_ST, _CX, 8, _ST_Vt), _AX) // MOVQ ST.Vt[CX], AX
self.Emit("BTQ" , _AX, _DX) // BTQ AX, DX
self.Sjmp("JNC" , "_invalid_char") // JNC _invalid_char
self.call_go(_F_makemap_small) // CALL_GO runtime.makemap_small
self.Emit("MOVQ", jit.Ptr(_ST, _ST_Sp), _CX) // MOVQ ST.Sp, CX
self.Emit("MOVQ", jit.Imm(_S_obj_0), jit.Sib(_ST, _CX, 8, _ST_Vt)) // MOVQ _S_obj_0, ST.Vt[CX]
self.Emit("MOVQ", jit.Sib(_ST, _CX, 8, _ST_Vp), _SI) // MOVQ ST.Vp[CX], SI
self.Emit("MOVQ", _T_map, _DX) // MOVQ _T_map, DX
self.Emit("MOVQ", _DX, jit.Ptr(_SI, 0)) // MOVQ DX, (SI)
self.WritePtrAX(4, jit.Ptr(_SI, 8), false) // MOVQ AX, 8(SI)
self.Sjmp("JMP" , "_next") // JMP _next
/** V_STRING **/
self.Link("_decode_V_STRING") // _decode_V_STRING:
self.Emit("MOVQ", _VAR_ss_Iv, _CX) // MOVQ ss.Iv, CX
self.Emit("MOVQ", _IC, _AX) // MOVQ IC, AX
self.Emit("SUBQ", _CX, _AX) // SUBQ CX, AX
/* check for escapes */
self.Emit("CMPQ", _VAR_ss_Ep, jit.Imm(-1)) // CMPQ ss.Ep, $-1
self.Sjmp("JNE" , "_unquote") // JNE _unquote
self.Emit("SUBQ", jit.Imm(1), _AX) // SUBQ $1, AX
self.Emit("LEAQ", jit.Sib(_IP, _CX, 1, 0), _R8) // LEAQ (IP)(CX), R8
self.Byte(0x48, 0x8d, 0x3d) // LEAQ (PC), DI
self.Sref("_copy_string_end", 4)
self.Emit("BTQ", jit.Imm(_F_copy_string), _VAR_df)
self.Sjmp("JC", "copy_string")
self.Link("_copy_string_end")
self.Emit("XORL", _DX, _DX)
/* strings with no escape sequences */
self.Link("_noescape") // _noescape:
self.Emit("MOVL", jit.Imm(_S_omask_key), _DI) // MOVL _S_omask, DI
self.Emit("MOVQ", jit.Ptr(_ST, _ST_Sp), _CX) // MOVQ ST.Sp, CX
self.Emit("MOVQ", jit.Sib(_ST, _CX, 8, _ST_Vt), _SI) // MOVQ ST.Vt[CX], SI
self.Emit("BTQ" , _SI, _DI) // BTQ SI, DI
self.Sjmp("JC" , "_object_key") // JC _object_key
/* check for pre-packed strings, avoid 1 allocation */
self.Emit("TESTQ", _DX, _DX) // TESTQ DX, DX
self.Sjmp("JNZ" , "_packed_str") // JNZ _packed_str
self.Emit("MOVQ" , _AX, _BX) // MOVQ AX, BX
self.Emit("MOVQ" , _R8, _AX) // MOVQ R8, AX
self.call_go(_F_convTstring) // CALL_GO runtime.convTstring
self.Emit("MOVQ" , _AX, _R9) // MOVQ AX, R9
/* packed string already in R9 */
self.Link("_packed_str") // _packed_str:
self.Emit("MOVQ", _T_string, _R8) // MOVQ _T_string, R8
self.Emit("MOVQ", _VAR_ss_Iv, _DI) // MOVQ ss.Iv, DI
self.Emit("SUBQ", jit.Imm(1), _DI) // SUBQ $1, DI
self.Sjmp("JMP" , "_set_value") // JMP _set_value
/* the string is an object key, get the map */
self.Link("_object_key")
self.Emit("MOVQ", jit.Ptr(_ST, _ST_Sp), _CX) // MOVQ ST.Sp, CX
self.Emit("MOVQ", jit.Sib(_ST, _CX, 8, _ST_Vp), _SI) // MOVQ ST.Vp[CX], SI
self.Emit("MOVQ", jit.Ptr(_SI, 8), _SI) // MOVQ 8(SI), SI
/* add a new delimiter */
self.Emit("ADDQ", jit.Imm(1), _CX) // ADDQ $1, CX
self.Emit("CMPQ", _CX, jit.Imm(types.MAX_RECURSE)) // CMPQ CX, ${types.MAX_RECURSE}
self.Sjmp("JAE" , "_stack_overflow") // JA _stack_overflow
self.Emit("MOVQ", _CX, jit.Ptr(_ST, _ST_Sp)) // MOVQ CX, ST.Sp
self.Emit("MOVQ", jit.Imm(_S_obj_delim), jit.Sib(_ST, _CX, 8, _ST_Vt)) // MOVQ _S_obj_delim, ST.Vt[CX]
/* add a new slot int the map */
self.Emit("MOVQ", _AX, _DI) // MOVQ AX, DI
self.Emit("MOVQ", _T_map, _AX) // MOVQ _T_map, AX
self.Emit("MOVQ", _SI, _BX) // MOVQ SI, BX
self.Emit("MOVQ", _R8, _CX) // MOVQ R9, CX
self.call_go(_F_mapassign_faststr) // CALL_GO runtime.mapassign_faststr
/* add to the pointer stack */
self.Emit("MOVQ", jit.Ptr(_ST, _ST_Sp), _CX) // MOVQ ST.Sp, CX
self.WritePtrAX(6, jit.Sib(_ST, _CX, 8, _ST_Vp), false) // MOVQ AX, ST.Vp[CX]
self.Sjmp("JMP" , "_next") // JMP _next
/* allocate memory to store the string header and unquoted result */
self.Link("_unquote") // _unquote:
self.Emit("ADDQ", jit.Imm(15), _AX) // ADDQ $15, AX
self.Emit("MOVQ", _T_byte, _BX) // MOVQ _T_byte, BX
self.Emit("MOVB", jit.Imm(0), _CX) // MOVB $0, CX
self.call_go(_F_mallocgc) // CALL_GO runtime.mallocgc
self.Emit("MOVQ", _AX, _R9) // MOVQ AX, R9
/* prepare the unquoting parameters */
self.Emit("MOVQ" , _VAR_ss_Iv, _CX) // MOVQ ss.Iv, CX
self.Emit("LEAQ" , jit.Sib(_IP, _CX, 1, 0), _DI) // LEAQ (IP)(CX), DI
self.Emit("NEGQ" , _CX) // NEGQ CX
self.Emit("LEAQ" , jit.Sib(_IC, _CX, 1, -1), _SI) // LEAQ -1(IC)(CX), SI
self.Emit("LEAQ" , jit.Ptr(_R9, 16), _DX) // LEAQ 16(R8), DX
self.Emit("LEAQ" , _VAR_ss_Ep, _CX) // LEAQ ss.Ep, CX
self.Emit("XORL" , _R8, _R8) // XORL R8, R8
self.Emit("BTQ" , jit.Imm(_F_disable_urc), _VAR_df) // BTQ ${_F_disable_urc}, fv
self.Emit("SETCC", _R8) // SETCC R8
self.Emit("SHLQ" , jit.Imm(types.B_UNICODE_REPLACE), _R8) // SHLQ ${types.B_UNICODE_REPLACE}, R8
/* unquote the string, with R9 been preserved */
self.Emit("MOVQ", _R9, _VAR_R9) // SAVE R9
self.call_c(_F_unquote) // CALL unquote
self.Emit("MOVQ", _VAR_R9, _R9) // LOAD R9
/* check for errors */
self.Emit("TESTQ", _AX, _AX) // TESTQ AX, AX
self.Sjmp("JS" , "_unquote_error") // JS _unquote_error
self.Emit("MOVL" , jit.Imm(1), _DX) // MOVL $1, DX
self.Emit("LEAQ" , jit.Ptr(_R9, 16), _R8) // ADDQ $16, R8
self.Emit("MOVQ" , _R8, jit.Ptr(_R9, 0)) // MOVQ R8, (R9)
self.Emit("MOVQ" , _AX, jit.Ptr(_R9, 8)) // MOVQ AX, 8(R9)
self.Sjmp("JMP" , "_noescape") // JMP _noescape
/** V_DOUBLE **/
self.Link("_decode_V_DOUBLE") // _decode_V_DOUBLE:
self.Emit("BTQ" , jit.Imm(_F_use_number), _VAR_df) // BTQ _F_use_number, df
self.Sjmp("JC" , "_use_number") // JC _use_number
self.Emit("MOVSD", _VAR_ss_Dv, _X0) // MOVSD ss.Dv, X0
self.Sjmp("JMP" , "_use_float64") // JMP _use_float64
/** V_INTEGER **/
self.Link("_decode_V_INTEGER") // _decode_V_INTEGER:
self.Emit("BTQ" , jit.Imm(_F_use_number), _VAR_df) // BTQ _F_use_number, df
self.Sjmp("JC" , "_use_number") // JC _use_number
self.Emit("BTQ" , jit.Imm(_F_use_int64), _VAR_df) // BTQ _F_use_int64, df
self.Sjmp("JC" , "_use_int64") // JC _use_int64
//TODO: use ss.Dv directly
self.Emit("MOVSD", _VAR_ss_Dv, _X0) // MOVSD ss.Dv, X0
/* represent numbers as `float64` */
self.Link("_use_float64") // _use_float64:
self.Emit("MOVQ" , _X0, _AX) // MOVQ X0, AX
self.call_go(_F_convT64) // CALL_GO runtime.convT64
self.Emit("MOVQ" , _T_float64, _R8) // MOVQ _T_float64, R8
self.Emit("MOVQ" , _AX, _R9) // MOVQ AX, R9
self.Emit("MOVQ" , _VAR_ss_Ep, _DI) // MOVQ ss.Ep, DI
self.Sjmp("JMP" , "_set_value") // JMP _set_value
/* represent numbers as `json.Number` */
self.Link("_use_number") // _use_number
self.Emit("MOVQ", _VAR_ss_Ep, _AX) // MOVQ ss.Ep, AX
self.Emit("LEAQ", jit.Sib(_IP, _AX, 1, 0), _SI) // LEAQ (IP)(AX), SI
self.Emit("MOVQ", _IC, _CX) // MOVQ IC, CX
self.Emit("SUBQ", _AX, _CX) // SUBQ AX, CX
self.Emit("MOVQ", _SI, _AX) // MOVQ SI, AX
self.Emit("MOVQ", _CX, _BX) // MOVQ CX, BX
self.call_go(_F_convTstring) // CALL_GO runtime.convTstring
self.Emit("MOVQ", _T_number, _R8) // MOVQ _T_number, R8
self.Emit("MOVQ", _AX, _R9) // MOVQ AX, R9
self.Emit("MOVQ", _VAR_ss_Ep, _DI) // MOVQ ss.Ep, DI
self.Sjmp("JMP" , "_set_value") // JMP _set_value
/* represent numbers as `int64` */
self.Link("_use_int64") // _use_int64:
self.Emit("MOVQ", _VAR_ss_Iv, _AX) // MOVQ ss.Iv, AX
self.call_go(_F_convT64) // CALL_GO runtime.convT64
self.Emit("MOVQ", _T_int64, _R8) // MOVQ _T_int64, R8
self.Emit("MOVQ", _AX, _R9) // MOVQ AX, R9
self.Emit("MOVQ", _VAR_ss_Ep, _DI) // MOVQ ss.Ep, DI
self.Sjmp("JMP" , "_set_value") // JMP _set_value
/** V_KEY_SEP **/
self.Link("_decode_V_KEY_SEP") // _decode_V_KEY_SEP:
self.Emit("MOVQ", jit.Ptr(_ST, _ST_Sp), _CX) // MOVQ ST.Sp, CX
self.Emit("MOVQ", jit.Sib(_ST, _CX, 8, _ST_Vt), _AX) // MOVQ ST.Vt[CX], AX
self.Emit("CMPQ", _AX, jit.Imm(_S_obj_delim)) // CMPQ AX, _S_obj_delim
self.Sjmp("JNE" , "_invalid_char") // JNE _invalid_char
self.Emit("MOVQ", jit.Imm(_S_val), jit.Sib(_ST, _CX, 8, _ST_Vt)) // MOVQ _S_val, ST.Vt[CX]
self.Emit("MOVQ", jit.Imm(_S_obj), jit.Sib(_ST, _CX, 8, _ST_Vt - 8)) // MOVQ _S_obj, ST.Vt[CX - 1]
self.Sjmp("JMP" , "_next") // JMP _next
/** V_ELEM_SEP **/
self.Link("_decode_V_ELEM_SEP") // _decode_V_ELEM_SEP:
self.Emit("MOVQ" , jit.Ptr(_ST, _ST_Sp), _CX) // MOVQ ST.Sp, CX
self.Emit("MOVQ" , jit.Sib(_ST, _CX, 8, _ST_Vt), _AX) // MOVQ ST.Vt[CX], AX
self.Emit("CMPQ" , _AX, jit.Imm(_S_arr))
self.Sjmp("JE" , "_array_sep") // JZ _next
self.Emit("CMPQ" , _AX, jit.Imm(_S_obj)) // CMPQ _AX, _S_arr
self.Sjmp("JNE" , "_invalid_char") // JNE _invalid_char
self.Emit("MOVQ" , jit.Imm(_S_obj_sep), jit.Sib(_ST, _CX, 8, _ST_Vt))
self.Sjmp("JMP" , "_next") // JMP _next
/* arrays */
self.Link("_array_sep")
self.Emit("MOVQ", jit.Sib(_ST, _CX, 8, _ST_Vp), _SI) // MOVQ ST.Vp[CX], SI
self.Emit("MOVQ", jit.Ptr(_SI, 8), _SI) // MOVQ 8(SI), SI
self.Emit("MOVQ", jit.Ptr(_SI, 8), _DX) // MOVQ 8(SI), DX
self.Emit("CMPQ", _DX, jit.Ptr(_SI, 16)) // CMPQ DX, 16(SI)
self.Sjmp("JAE" , "_array_more") // JAE _array_more
/* add a slot for the new element */
self.Link("_array_append") // _array_append:
self.Emit("ADDQ", jit.Imm(1), jit.Ptr(_SI, 8)) // ADDQ $1, 8(SI)
self.Emit("MOVQ", jit.Ptr(_SI, 0), _SI) // MOVQ (SI), SI
self.Emit("ADDQ", jit.Imm(1), _CX) // ADDQ $1, CX
self.Emit("CMPQ", _CX, jit.Imm(types.MAX_RECURSE)) // CMPQ CX, ${types.MAX_RECURSE}
self.Sjmp("JAE" , "_stack_overflow") // JA _stack_overflow
self.Emit("SHLQ", jit.Imm(1), _DX) // SHLQ $1, DX
self.Emit("LEAQ", jit.Sib(_SI, _DX, 8, 0), _SI) // LEAQ (SI)(DX*8), SI
self.Emit("MOVQ", _CX, jit.Ptr(_ST, _ST_Sp)) // MOVQ CX, ST.Sp
self.WriteRecNotAX(7 , _SI, jit.Sib(_ST, _CX, 8, _ST_Vp), false) // MOVQ SI, ST.Vp[CX]
self.Emit("MOVQ", jit.Imm(_S_val), jit.Sib(_ST, _CX, 8, _ST_Vt)) // MOVQ _S_val, ST.Vt[CX}
self.Sjmp("JMP" , "_next") // JMP _next
/** V_ARRAY_END **/
self.Link("_decode_V_ARRAY_END") // _decode_V_ARRAY_END:
self.Emit("XORL", _DX, _DX) // XORL DX, DX
self.Emit("MOVQ", jit.Ptr(_ST, _ST_Sp), _CX) // MOVQ ST.Sp, CX
self.Emit("MOVQ", jit.Sib(_ST, _CX, 8, _ST_Vt), _AX) // MOVQ ST.Vt[CX], AX
self.Emit("CMPQ", _AX, jit.Imm(_S_arr_0)) // CMPQ AX, _S_arr_0
self.Sjmp("JE" , "_first_item") // JE _first_item
self.Emit("CMPQ", _AX, jit.Imm(_S_arr)) // CMPQ AX, _S_arr
self.Sjmp("JNE" , "_invalid_char") // JNE _invalid_char
self.Emit("SUBQ", jit.Imm(1), jit.Ptr(_ST, _ST_Sp)) // SUBQ $1, ST.Sp
self.Emit("MOVQ", _DX, jit.Sib(_ST, _CX, 8, _ST_Vp)) // MOVQ DX, ST.Vp[CX]
self.Sjmp("JMP" , "_next") // JMP _next
/* first element of an array */
self.Link("_first_item") // _first_item:
self.Emit("MOVQ", jit.Ptr(_ST, _ST_Sp), _CX) // MOVQ ST.Sp, CX
self.Emit("SUBQ", jit.Imm(2), jit.Ptr(_ST, _ST_Sp)) // SUBQ $2, ST.Sp
self.Emit("MOVQ", jit.Sib(_ST, _CX, 8, _ST_Vp - 8), _SI) // MOVQ ST.Vp[CX - 1], SI
self.Emit("MOVQ", jit.Ptr(_SI, 8), _SI) // MOVQ 8(SI), SI
self.Emit("MOVQ", _DX, jit.Sib(_ST, _CX, 8, _ST_Vp - 8)) // MOVQ DX, ST.Vp[CX - 1]
self.Emit("MOVQ", _DX, jit.Sib(_ST, _CX, 8, _ST_Vp)) // MOVQ DX, ST.Vp[CX]
self.Emit("MOVQ", _DX, jit.Ptr(_SI, 8)) // MOVQ DX, 8(SI)
self.Sjmp("JMP" , "_next") // JMP _next
/** V_OBJECT_END **/
self.Link("_decode_V_OBJECT_END") // _decode_V_OBJECT_END:
self.Emit("MOVL", jit.Imm(_S_omask_end), _DI) // MOVL _S_omask, DI
self.Emit("MOVQ", jit.Ptr(_ST, _ST_Sp), _CX) // MOVQ ST.Sp, CX
self.Emit("MOVQ", jit.Sib(_ST, _CX, 8, _ST_Vt), _AX) // MOVQ ST.Vt[CX], AX
self.Emit("BTQ" , _AX, _DI)
self.Sjmp("JNC" , "_invalid_char") // JNE _invalid_char
self.Emit("XORL", _AX, _AX) // XORL AX, AX
self.Emit("SUBQ", jit.Imm(1), jit.Ptr(_ST, _ST_Sp)) // SUBQ $1, ST.Sp
self.Emit("MOVQ", _AX, jit.Sib(_ST, _CX, 8, _ST_Vp)) // MOVQ AX, ST.Vp[CX]
self.Sjmp("JMP" , "_next") // JMP _next
/* return from decoder */
self.Link("_return") // _return:
self.Emit("XORL", _EP, _EP) // XORL EP, EP
self.Emit("MOVQ", _EP, jit.Ptr(_ST, _ST_Vp)) // MOVQ EP, ST.Vp[0]
self.Link("_epilogue") // _epilogue:
self.Emit("SUBQ", jit.Imm(_FsmOffset), _ST) // SUBQ _FsmOffset, _ST
self.Emit("MOVQ", jit.Ptr(_SP, _VD_offs), _BP) // MOVQ _VD_offs(SP), BP
self.Emit("ADDQ", jit.Imm(_VD_size), _SP) // ADDQ $_VD_size, SP
self.Emit("RET") // RET
/* array expand */
self.Link("_array_more") // _array_more:
self.Emit("MOVQ" , _T_eface, _AX) // MOVQ _T_eface, AX
self.Emit("MOVQ" , jit.Ptr(_SI, 0), _BX) // MOVQ (SI), BX
self.Emit("MOVQ" , jit.Ptr(_SI, 8), _CX) // MOVQ 8(SI), CX
self.Emit("MOVQ" , jit.Ptr(_SI, 16), _DI) // MOVQ 16(SI), DI
self.Emit("MOVQ" , _DI, _SI) // MOVQ DI, 24(SP)
self.Emit("SHLQ" , jit.Imm(1), _SI) // SHLQ $1, SI
self.call_go(_F_growslice) // CALL_GO runtime.growslice
self.Emit("MOVQ" , _AX, _DI) // MOVQ AX, DI
self.Emit("MOVQ" , _BX, _DX) // MOVQ BX, DX
self.Emit("MOVQ" , _CX, _AX) // MOVQ CX, AX
/* update the slice */
self.Emit("MOVQ", jit.Ptr(_ST, _ST_Sp), _CX) // MOVQ ST.Sp, CX
self.Emit("MOVQ", jit.Sib(_ST, _CX, 8, _ST_Vp), _SI) // MOVQ ST.Vp[CX], SI
self.Emit("MOVQ", jit.Ptr(_SI, 8), _SI) // MOVQ 8(SI), SI
self.Emit("MOVQ", _DX, jit.Ptr(_SI, 8)) // MOVQ DX, 8(SI)
self.Emit("MOVQ", _AX, jit.Ptr(_SI, 16)) // MOVQ AX, 16(AX)
self.WriteRecNotAX(8 , _DI, jit.Ptr(_SI, 0), false) // MOVQ R10, (SI)
self.Sjmp("JMP" , "_array_append") // JMP _array_append
/* copy string */
self.Link("copy_string") // pointer: R8, length: AX, return addr: DI
self.Emit("MOVQ", _R8, _VAR_cs_p)
self.Emit("MOVQ", _AX, _VAR_cs_n)
self.Emit("MOVQ", _DI, _VAR_cs_LR)
self.Emit("MOVQ", _AX, _BX)
self.Emit("MOVQ", _AX, _CX)
self.Emit("MOVQ", _T_byte, _AX)
self.call_go(_F_makeslice)
self.Emit("MOVQ", _AX, _VAR_cs_d)
self.Emit("MOVQ", _VAR_cs_p, _BX)
self.Emit("MOVQ", _VAR_cs_n, _CX)
self.call_go(_F_memmove)
self.Emit("MOVQ", _VAR_cs_d, _R8)
self.Emit("MOVQ", _VAR_cs_n, _AX)
self.Emit("MOVQ", _VAR_cs_LR, _DI)
self.Rjmp("JMP", _DI)
/* error handlers */
self.Link("_stack_overflow")
self.Emit("MOVL" , _E_recurse, _EP) // MOVQ _E_recurse, EP
self.Sjmp("JMP" , "_error") // JMP _error
self.Link("_vtype_error") // _vtype_error:
self.Emit("MOVQ" , _DI, _IC) // MOVQ DI, IC
self.Emit("MOVL" , _E_invalid, _EP) // MOVL _E_invalid, EP
self.Sjmp("JMP" , "_error") // JMP _error
self.Link("_invalid_char") // _invalid_char:
self.Emit("SUBQ" , jit.Imm(1), _IC) // SUBQ $1, IC
self.Emit("MOVL" , _E_invalid, _EP) // MOVL _E_invalid, EP
self.Sjmp("JMP" , "_error") // JMP _error
self.Link("_unquote_error") // _unquote_error:
self.Emit("MOVQ" , _VAR_ss_Iv, _IC) // MOVQ ss.Iv, IC
self.Emit("SUBQ" , jit.Imm(1), _IC) // SUBQ $1, IC
self.Link("_parsing_error") // _parsing_error:
self.Emit("NEGQ" , _AX) // NEGQ AX
self.Emit("MOVQ" , _AX, _EP) // MOVQ AX, EP
self.Link("_error") // _error:
self.Emit("PXOR" , _X0, _X0) // PXOR X0, X0
self.Emit("MOVOU", _X0, jit.Ptr(_VP, 0)) // MOVOU X0, (VP)
self.Sjmp("JMP" , "_epilogue") // JMP _epilogue
/* invalid value type, never returns */
self.Link("_invalid_vtype")
self.call_go(_F_invalid_vtype) // CALL invalid_type
self.Emit("UD2") // UD2
/* switch jump table */
self.Link("_switch_table") // _switch_table:
self.Sref("_decode_V_EOF", 0) // SREF &_decode_V_EOF, $0
self.Sref("_decode_V_NULL", -4) // SREF &_decode_V_NULL, $-4
self.Sref("_decode_V_TRUE", -8) // SREF &_decode_V_TRUE, $-8
self.Sref("_decode_V_FALSE", -12) // SREF &_decode_V_FALSE, $-12
self.Sref("_decode_V_ARRAY", -16) // SREF &_decode_V_ARRAY, $-16
self.Sref("_decode_V_OBJECT", -20) // SREF &_decode_V_OBJECT, $-20
self.Sref("_decode_V_STRING", -24) // SREF &_decode_V_STRING, $-24
self.Sref("_decode_V_DOUBLE", -28) // SREF &_decode_V_DOUBLE, $-28
self.Sref("_decode_V_INTEGER", -32) // SREF &_decode_V_INTEGER, $-32
self.Sref("_decode_V_KEY_SEP", -36) // SREF &_decode_V_KEY_SEP, $-36
self.Sref("_decode_V_ELEM_SEP", -40) // SREF &_decode_V_ELEM_SEP, $-40
self.Sref("_decode_V_ARRAY_END", -44) // SREF &_decode_V_ARRAY_END, $-44
self.Sref("_decode_V_OBJECT_END", -48) // SREF &_decode_V_OBJECT_END, $-48
/* fast character lookup table */
self.Link("_decode_tab") // _decode_tab:
self.Sref("_decode_V_EOF", 0) // SREF &_decode_V_EOF, $0
/* generate rest of the tabs */
for i := 1; i < 256; i++ {
if to, ok := _R_tab[i]; ok {
self.Sref(to, -int64(i) * 4)
} else {
self.Byte(0x00, 0x00, 0x00, 0x00)
}
}
}
/** Generic Decoder **/
var (
_subr_decode_value = new(_ValueDecoder).build()
)
//go:nosplit
func invalid_vtype(vt types.ValueType) {
throw(fmt.Sprintf("invalid value type: %d", vt))
}
@@ -0,0 +1,37 @@
// +build go1.17,!go1.23
//
// Copyright 2021 ByteDance Inc.
//
// 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.
//
#include "go_asm.h"
#include "funcdata.h"
#include "textflag.h"
TEXT ·decodeValueStub(SB), NOSPLIT, $0 - 72
NO_LOCAL_POINTERS
PXOR X0, X0
MOVOU X0, rv+48(FP)
MOVQ st+0(FP) , R13
MOVQ sp+8(FP) , R10
MOVQ sn+16(FP), R12
MOVQ ic+24(FP), R11
MOVQ vp+32(FP), R15
MOVQ df+40(FP), AX
MOVQ ·_subr_decode_value(SB), BX
CALL BX
MOVQ R11, rp+48(FP)
MOVQ BX, ex+56(FP)
RET
@@ -0,0 +1,733 @@
// +build go1.16,!go1.17
/*
* Copyright 2021 ByteDance Inc.
*
* 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.
*/
package decoder
import (
`encoding/json`
`fmt`
`reflect`
`github.com/bytedance/sonic/internal/jit`
`github.com/bytedance/sonic/internal/native`
`github.com/bytedance/sonic/internal/native/types`
`github.com/twitchyliquid64/golang-asm/obj`
)
/** Crucial Registers:
*
* ST(BX) : ro, decoder stack
* DF(R10) : ro, decoder flags
* EP(R11) : wo, error pointer
* IP(R12) : ro, input pointer
* IL(R13) : ro, input length
* IC(R14) : rw, input cursor
* VP(R15) : ro, value pointer (to an interface{})
*/
const (
_VD_args = 8 // 8 bytes for passing arguments to this functions
_VD_fargs = 64 // 64 bytes for passing arguments to other Go functions
_VD_saves = 40 // 40 bytes for saving the registers before CALL instructions
_VD_locals = 88 // 88 bytes for local variables
)
const (
_VD_offs = _VD_fargs + _VD_saves + _VD_locals
_VD_size = _VD_offs + 8 // 8 bytes for the parent frame pointer
)
var (
_VAR_ss = _VAR_ss_Vt
_VAR_df = jit.Ptr(_SP, _VD_fargs + _VD_saves)
)
var (
_VAR_ss_Vt = jit.Ptr(_SP, _VD_fargs + _VD_saves + 8)
_VAR_ss_Dv = jit.Ptr(_SP, _VD_fargs + _VD_saves + 16)
_VAR_ss_Iv = jit.Ptr(_SP, _VD_fargs + _VD_saves + 24)
_VAR_ss_Ep = jit.Ptr(_SP, _VD_fargs + _VD_saves + 32)
_VAR_ss_Db = jit.Ptr(_SP, _VD_fargs + _VD_saves + 40)
_VAR_ss_Dc = jit.Ptr(_SP, _VD_fargs + _VD_saves + 48)
)
var (
_VAR_cs_LR = jit.Ptr(_SP, _VD_fargs + _VD_saves + 56)
_VAR_cs_p = jit.Ptr(_SP, _VD_fargs + _VD_saves + 64)
_VAR_cs_n = jit.Ptr(_SP, _VD_fargs + _VD_saves + 72)
_VAR_cs_d = jit.Ptr(_SP, _VD_fargs + _VD_saves + 80)
)
type _ValueDecoder struct {
jit.BaseAssembler
}
func (self *_ValueDecoder) build() uintptr {
self.Init(self.compile)
return *(*uintptr)(self.Load("decode_value", _VD_size, _VD_args, argPtrs_generic, localPtrs_generic))
}
/** Function Calling Helpers **/
func (self *_ValueDecoder) save(r ...obj.Addr) {
for i, v := range r {
if i > _VD_saves / 8 - 1 {
panic("too many registers to save")
} else {
self.Emit("MOVQ", v, jit.Ptr(_SP, _VD_fargs + int64(i) * 8))
}
}
}
func (self *_ValueDecoder) load(r ...obj.Addr) {
for i, v := range r {
if i > _VD_saves / 8 - 1 {
panic("too many registers to load")
} else {
self.Emit("MOVQ", jit.Ptr(_SP, _VD_fargs + int64(i) * 8), v)
}
}
}
func (self *_ValueDecoder) call(fn obj.Addr) {
self.Emit("MOVQ", fn, _AX) // MOVQ ${fn}, AX
self.Rjmp("CALL", _AX) // CALL AX
}
func (self *_ValueDecoder) call_go(fn obj.Addr) {
self.save(_REG_go...) // SAVE $REG_go
self.call(fn) // CALL ${fn}
self.load(_REG_go...) // LOAD $REG_go
}
/** Decoder Assembler **/
const (
_S_val = iota + 1
_S_arr
_S_arr_0
_S_obj
_S_obj_0
_S_obj_delim
_S_obj_sep
)
const (
_S_omask_key = (1 << _S_obj_0) | (1 << _S_obj_sep)
_S_omask_end = (1 << _S_obj_0) | (1 << _S_obj)
_S_vmask = (1 << _S_val) | (1 << _S_arr_0)
)
const (
_A_init_len = 1
_A_init_cap = 16
)
const (
_ST_Sp = 0
_ST_Vt = _PtrBytes
_ST_Vp = _PtrBytes * (types.MAX_RECURSE + 1)
)
var (
_V_true = jit.Imm(int64(pbool(true)))
_V_false = jit.Imm(int64(pbool(false)))
_F_value = jit.Imm(int64(native.S_value))
)
var (
_V_max = jit.Imm(int64(types.V_MAX))
_E_eof = jit.Imm(int64(types.ERR_EOF))
_E_invalid = jit.Imm(int64(types.ERR_INVALID_CHAR))
_E_recurse = jit.Imm(int64(types.ERR_RECURSE_EXCEED_MAX))
)
var (
_F_convTslice = jit.Func(convTslice)
_F_convTstring = jit.Func(convTstring)
_F_invalid_vtype = jit.Func(invalid_vtype)
)
var (
_T_map = jit.Type(reflect.TypeOf((map[string]interface{})(nil)))
_T_bool = jit.Type(reflect.TypeOf(false))
_T_int64 = jit.Type(reflect.TypeOf(int64(0)))
_T_eface = jit.Type(reflect.TypeOf((*interface{})(nil)).Elem())
_T_slice = jit.Type(reflect.TypeOf(([]interface{})(nil)))
_T_string = jit.Type(reflect.TypeOf(""))
_T_number = jit.Type(reflect.TypeOf(json.Number("")))
_T_float64 = jit.Type(reflect.TypeOf(float64(0)))
)
var _R_tab = map[int]string {
'[': "_decode_V_ARRAY",
'{': "_decode_V_OBJECT",
':': "_decode_V_KEY_SEP",
',': "_decode_V_ELEM_SEP",
']': "_decode_V_ARRAY_END",
'}': "_decode_V_OBJECT_END",
}
func (self *_ValueDecoder) compile() {
self.Emit("SUBQ", jit.Imm(_VD_size), _SP) // SUBQ $_VD_size, SP
self.Emit("MOVQ", _BP, jit.Ptr(_SP, _VD_offs)) // MOVQ BP, _VD_offs(SP)
self.Emit("LEAQ", jit.Ptr(_SP, _VD_offs), _BP) // LEAQ _VD_offs(SP), BP
/* initialize the state machine */
self.Emit("XORL", _CX, _CX) // XORL CX, CX
self.Emit("MOVQ", _DF, _VAR_df) // MOVQ DF, df
/* initialize digital buffer first */
self.Emit("MOVQ", jit.Imm(_MaxDigitNums), _VAR_ss_Dc) // MOVQ $_MaxDigitNums, ss.Dcap
self.Emit("LEAQ", jit.Ptr(_ST, _DbufOffset), _AX) // LEAQ _DbufOffset(ST), AX
self.Emit("MOVQ", _AX, _VAR_ss_Db) // MOVQ AX, ss.Dbuf
/* add ST offset */
self.Emit("ADDQ", jit.Imm(_FsmOffset), _ST) // ADDQ _FsmOffset, _ST
self.Emit("MOVQ", _CX, jit.Ptr(_ST, _ST_Sp)) // MOVQ CX, ST.Sp
self.WriteRecNotAX(0, _VP, jit.Ptr(_ST, _ST_Vp), false) // MOVQ VP, ST.Vp[0]
self.Emit("MOVQ", jit.Imm(_S_val), jit.Ptr(_ST, _ST_Vt)) // MOVQ _S_val, ST.Vt[0]
self.Sjmp("JMP" , "_next") // JMP _next
/* set the value from previous round */
self.Link("_set_value") // _set_value:
self.Emit("MOVL" , jit.Imm(_S_vmask), _DX) // MOVL _S_vmask, DX
self.Emit("MOVQ" , jit.Ptr(_ST, _ST_Sp), _CX) // MOVQ ST.Sp, CX
self.Emit("MOVQ" , jit.Sib(_ST, _CX, 8, _ST_Vt), _AX) // MOVQ ST.Vt[CX], AX
self.Emit("BTQ" , _AX, _DX) // BTQ AX, DX
self.Sjmp("JNC" , "_vtype_error") // JNC _vtype_error
self.Emit("XORL" , _SI, _SI) // XORL SI, SI
self.Emit("SUBQ" , jit.Imm(1), jit.Ptr(_ST, _ST_Sp)) // SUBQ $1, ST.Sp
self.Emit("XCHGQ", jit.Sib(_ST, _CX, 8, _ST_Vp), _SI) // XCHGQ ST.Vp[CX], SI
self.Emit("MOVQ" , _R8, jit.Ptr(_SI, 0)) // MOVQ R8, (SI)
self.WriteRecNotAX(1, _R9, jit.Ptr(_SI, 8), false) // MOVQ R9, 8(SI)
/* check for value stack */
self.Link("_next") // _next:
self.Emit("MOVQ" , jit.Ptr(_ST, _ST_Sp), _AX) // MOVQ ST.Sp, AX
self.Emit("TESTQ", _AX, _AX) // TESTQ AX, AX
self.Sjmp("JS" , "_return") // JS _return
/* fast path: test up to 4 characters manually */
self.Emit("CMPQ" , _IC, _IL) // CMPQ IC, IL
self.Sjmp("JAE" , "_decode_V_EOF") // JAE _decode_V_EOF
self.Emit("MOVBQZX", jit.Sib(_IP, _IC, 1, 0), _AX) // MOVBQZX (IP)(IC), AX
self.Emit("MOVQ" , jit.Imm(_BM_space), _DX) // MOVQ _BM_space, DX
self.Emit("CMPQ" , _AX, jit.Imm(' ')) // CMPQ AX, $' '
self.Sjmp("JA" , "_decode_fast") // JA _decode_fast
self.Emit("BTQ" , _AX, _DX) // BTQ _AX, _DX
self.Sjmp("JNC" , "_decode_fast") // JNC _decode_fast
self.Emit("ADDQ" , jit.Imm(1), _IC) // ADDQ $1, IC
/* at least 1 to 3 spaces */
for i := 0; i < 3; i++ {
self.Emit("CMPQ" , _IC, _IL) // CMPQ IC, IL
self.Sjmp("JAE" , "_decode_V_EOF") // JAE _decode_V_EOF
self.Emit("MOVBQZX", jit.Sib(_IP, _IC, 1, 0), _AX) // MOVBQZX (IP)(IC), AX
self.Emit("CMPQ" , _AX, jit.Imm(' ')) // CMPQ AX, $' '
self.Sjmp("JA" , "_decode_fast") // JA _decode_fast
self.Emit("BTQ" , _AX, _DX) // BTQ _AX, _DX
self.Sjmp("JNC" , "_decode_fast") // JNC _decode_fast
self.Emit("ADDQ" , jit.Imm(1), _IC) // ADDQ $1, IC
}
/* at least 4 spaces */
self.Emit("CMPQ" , _IC, _IL) // CMPQ IC, IL
self.Sjmp("JAE" , "_decode_V_EOF") // JAE _decode_V_EOF
self.Emit("MOVBQZX", jit.Sib(_IP, _IC, 1, 0), _AX) // MOVBQZX (IP)(IC), AX
/* fast path: use lookup table to select decoder */
self.Link("_decode_fast") // _decode_fast:
self.Byte(0x48, 0x8d, 0x3d) // LEAQ ?(PC), DI
self.Sref("_decode_tab", 4) // .... &_decode_tab
self.Emit("MOVLQSX", jit.Sib(_DI, _AX, 4, 0), _AX) // MOVLQSX (DI)(AX*4), AX
self.Emit("TESTQ" , _AX, _AX) // TESTQ AX, AX
self.Sjmp("JZ" , "_decode_native") // JZ _decode_native
self.Emit("ADDQ" , jit.Imm(1), _IC) // ADDQ $1, IC
self.Emit("ADDQ" , _DI, _AX) // ADDQ DI, AX
self.Rjmp("JMP" , _AX) // JMP AX
/* decode with native decoder */
self.Link("_decode_native") // _decode_native:
self.Emit("MOVQ", _IP, _DI) // MOVQ IP, DI
self.Emit("MOVQ", _IL, _SI) // MOVQ IL, SI
self.Emit("MOVQ", _IC, _DX) // MOVQ IC, DX
self.Emit("LEAQ", _VAR_ss, _CX) // LEAQ ss, CX
self.Emit("MOVQ", _VAR_df, _R8) // MOVQ $df, R8
self.Emit("BTSQ", jit.Imm(_F_allow_control), _R8) // ANDQ $1<<_F_allow_control, R8
self.call(_F_value) // CALL value
self.Emit("MOVQ", _AX, _IC) // MOVQ AX, IC
/* check for errors */
self.Emit("MOVQ" , _VAR_ss_Vt, _AX) // MOVQ ss.Vt, AX
self.Emit("TESTQ", _AX, _AX) // TESTQ AX, AX
self.Sjmp("JS" , "_parsing_error")
self.Sjmp("JZ" , "_invalid_vtype") // JZ _invalid_vtype
self.Emit("CMPQ" , _AX, _V_max) // CMPQ AX, _V_max
self.Sjmp("JA" , "_invalid_vtype") // JA _invalid_vtype
/* jump table selector */
self.Byte(0x48, 0x8d, 0x3d) // LEAQ ?(PC), DI
self.Sref("_switch_table", 4) // .... &_switch_table
self.Emit("MOVLQSX", jit.Sib(_DI, _AX, 4, -4), _AX) // MOVLQSX -4(DI)(AX*4), AX
self.Emit("ADDQ" , _DI, _AX) // ADDQ DI, AX
self.Rjmp("JMP" , _AX) // JMP AX
/** V_EOF **/
self.Link("_decode_V_EOF") // _decode_V_EOF:
self.Emit("MOVL", _E_eof, _EP) // MOVL _E_eof, EP
self.Sjmp("JMP" , "_error") // JMP _error
/** V_NULL **/
self.Link("_decode_V_NULL") // _decode_V_NULL:
self.Emit("XORL", _R8, _R8) // XORL R8, R8
self.Emit("XORL", _R9, _R9) // XORL R9, R9
self.Emit("LEAQ", jit.Ptr(_IC, -4), _DI) // LEAQ -4(IC), DI
self.Sjmp("JMP" , "_set_value") // JMP _set_value
/** V_TRUE **/
self.Link("_decode_V_TRUE") // _decode_V_TRUE:
self.Emit("MOVQ", _T_bool, _R8) // MOVQ _T_bool, R8
// TODO: maybe modified by users?
self.Emit("MOVQ", _V_true, _R9) // MOVQ _V_true, R9
self.Emit("LEAQ", jit.Ptr(_IC, -4), _DI) // LEAQ -4(IC), DI
self.Sjmp("JMP" , "_set_value") // JMP _set_value
/** V_FALSE **/
self.Link("_decode_V_FALSE") // _decode_V_FALSE:
self.Emit("MOVQ", _T_bool, _R8) // MOVQ _T_bool, R8
self.Emit("MOVQ", _V_false, _R9) // MOVQ _V_false, R9
self.Emit("LEAQ", jit.Ptr(_IC, -5), _DI) // LEAQ -5(IC), DI
self.Sjmp("JMP" , "_set_value") // JMP _set_value
/** V_ARRAY **/
self.Link("_decode_V_ARRAY") // _decode_V_ARRAY
self.Emit("MOVL", jit.Imm(_S_vmask), _DX) // MOVL _S_vmask, DX
self.Emit("MOVQ", jit.Ptr(_ST, _ST_Sp), _CX) // MOVQ ST.Sp, CX
self.Emit("MOVQ", jit.Sib(_ST, _CX, 8, _ST_Vt), _AX) // MOVQ ST.Vt[CX], AX
self.Emit("BTQ" , _AX, _DX) // BTQ AX, DX
self.Sjmp("JNC" , "_invalid_char") // JNC _invalid_char
/* create a new array */
self.Emit("MOVQ", _T_eface, _AX) // MOVQ _T_eface, AX
self.Emit("MOVQ", _AX, jit.Ptr(_SP, 0)) // MOVQ AX, (SP)
self.Emit("MOVQ", jit.Imm(_A_init_len), jit.Ptr(_SP, 8)) // MOVQ _A_init_len, 8(SP)
self.Emit("MOVQ", jit.Imm(_A_init_cap), jit.Ptr(_SP, 16)) // MOVQ _A_init_cap, 16(SP)
self.call_go(_F_makeslice) // CALL_GO runtime.makeslice
self.Emit("MOVQ", jit.Ptr(_SP, 24), _DX) // MOVQ 24(SP), DX
/* pack into an interface */
self.Emit("MOVQ", _DX, jit.Ptr(_SP, 0)) // MOVQ DX, (SP)
self.Emit("MOVQ", jit.Imm(_A_init_len), jit.Ptr(_SP, 8)) // MOVQ _A_init_len, 8(SP)
self.Emit("MOVQ", jit.Imm(_A_init_cap), jit.Ptr(_SP, 16)) // MOVQ _A_init_cap, 16(SP)
self.call_go(_F_convTslice) // CALL_GO runtime.convTslice
self.Emit("MOVQ", jit.Ptr(_SP, 24), _R8) // MOVQ 24(SP), R8
/* replace current state with an array */
self.Emit("MOVQ", jit.Ptr(_ST, _ST_Sp), _CX) // MOVQ ST.Sp, CX
self.Emit("MOVQ", jit.Sib(_ST, _CX, 8, _ST_Vp), _SI) // MOVQ ST.Vp[CX], SI
self.Emit("MOVQ", jit.Imm(_S_arr), jit.Sib(_ST, _CX, 8, _ST_Vt)) // MOVQ _S_arr, ST.Vt[CX]
self.Emit("MOVQ", _T_slice, _AX) // MOVQ _T_slice, AX
self.Emit("MOVQ", _AX, jit.Ptr(_SI, 0)) // MOVQ AX, (SI)
self.WriteRecNotAX(2, _R8, jit.Ptr(_SI, 8), false) // MOVQ R8, 8(SI)
/* add a new slot for the first element */
self.Emit("ADDQ", jit.Imm(1), _CX) // ADDQ $1, CX
self.Emit("CMPQ", _CX, jit.Imm(types.MAX_RECURSE)) // CMPQ CX, ${types.MAX_RECURSE}
self.Sjmp("JAE" , "_stack_overflow") // JA _stack_overflow
self.Emit("MOVQ", jit.Ptr(_R8, 0), _AX) // MOVQ (R8), AX
self.Emit("MOVQ", _CX, jit.Ptr(_ST, _ST_Sp)) // MOVQ CX, ST.Sp
self.WritePtrAX(3, jit.Sib(_ST, _CX, 8, _ST_Vp), false) // MOVQ AX, ST.Vp[CX]
self.Emit("MOVQ", jit.Imm(_S_arr_0), jit.Sib(_ST, _CX, 8, _ST_Vt)) // MOVQ _S_arr_0, ST.Vt[CX]
self.Sjmp("JMP" , "_next") // JMP _next
/** V_OBJECT **/
self.Link("_decode_V_OBJECT") // _decode_V_OBJECT:
self.Emit("MOVL", jit.Imm(_S_vmask), _DX) // MOVL _S_vmask, DX
self.Emit("MOVQ", jit.Ptr(_ST, _ST_Sp), _CX) // MOVQ ST.Sp, CX
self.Emit("MOVQ", jit.Sib(_ST, _CX, 8, _ST_Vt), _AX) // MOVQ ST.Vt[CX], AX
self.Emit("BTQ" , _AX, _DX) // BTQ AX, DX
self.Sjmp("JNC" , "_invalid_char") // JNC _invalid_char
self.call_go(_F_makemap_small) // CALL_GO runtime.makemap_small
self.Emit("MOVQ", jit.Ptr(_SP, 0), _AX) // MOVQ (SP), AX
self.Emit("MOVQ", jit.Ptr(_ST, _ST_Sp), _CX) // MOVQ ST.Sp, CX
self.Emit("MOVQ", jit.Imm(_S_obj_0), jit.Sib(_ST, _CX, 8, _ST_Vt)) // MOVQ _S_obj, ST.Vt[CX]
self.Emit("MOVQ", jit.Sib(_ST, _CX, 8, _ST_Vp), _SI) // MOVQ ST.Vp[CX], SI
self.Emit("MOVQ", _T_map, _DX) // MOVQ _T_map, DX
self.Emit("MOVQ", _DX, jit.Ptr(_SI, 0)) // MOVQ DX, (SI)
self.WritePtrAX(4, jit.Ptr(_SI, 8), false) // MOVQ AX, 8(SI)
self.Sjmp("JMP" , "_next") // JMP _next
/** V_STRING **/
self.Link("_decode_V_STRING") // _decode_V_STRING:
self.Emit("MOVQ", _VAR_ss_Iv, _CX) // MOVQ ss.Iv, CX
self.Emit("MOVQ", _IC, _AX) // MOVQ IC, AX
self.Emit("SUBQ", _CX, _AX) // SUBQ CX, AX
/* check for escapes */
self.Emit("CMPQ", _VAR_ss_Ep, jit.Imm(-1)) // CMPQ ss.Ep, $-1
self.Sjmp("JNE" , "_unquote") // JNE _unquote
self.Emit("SUBQ", jit.Imm(1), _AX) // SUBQ $1, AX
self.Emit("LEAQ", jit.Sib(_IP, _CX, 1, 0), _R8) // LEAQ (IP)(CX), R8
self.Byte(0x48, 0x8d, 0x3d) // LEAQ (PC), DI
self.Sref("_copy_string_end", 4)
self.Emit("BTQ", jit.Imm(_F_copy_string), _VAR_df)
self.Sjmp("JC", "copy_string")
self.Link("_copy_string_end")
self.Emit("XORL", _DX, _DX) // XORL DX, DX
/* strings with no escape sequences */
self.Link("_noescape") // _noescape:
self.Emit("MOVL", jit.Imm(_S_omask_key), _DI) // MOVL _S_omask, DI
self.Emit("MOVQ", jit.Ptr(_ST, _ST_Sp), _CX) // MOVQ ST.Sp, CX
self.Emit("MOVQ", jit.Sib(_ST, _CX, 8, _ST_Vt), _SI) // MOVQ ST.Vt[CX], SI
self.Emit("BTQ" , _SI, _DI) // BTQ SI, DI
self.Sjmp("JC" , "_object_key") // JC _object_key
/* check for pre-packed strings, avoid 1 allocation */
self.Emit("TESTQ", _DX, _DX) // TESTQ DX, DX
self.Sjmp("JNZ" , "_packed_str") // JNZ _packed_str
self.Emit("MOVQ" , _R8, jit.Ptr(_SP, 0)) // MOVQ R8, (SP)
self.Emit("MOVQ" , _AX, jit.Ptr(_SP, 8)) // MOVQ AX, 8(SP)
self.call_go(_F_convTstring) // CALL_GO runtime.convTstring
self.Emit("MOVQ" , jit.Ptr(_SP, 16), _R9) // MOVQ 16(SP), R9
/* packed string already in R9 */
self.Link("_packed_str") // _packed_str:
self.Emit("MOVQ", _T_string, _R8) // MOVQ _T_string, R8
self.Emit("MOVQ", _VAR_ss_Iv, _DI) // MOVQ ss.Iv, DI
self.Emit("SUBQ", jit.Imm(1), _DI) // SUBQ $1, DI
self.Sjmp("JMP" , "_set_value") // JMP _set_value
/* the string is an object key, get the map */
self.Link("_object_key")
self.Emit("MOVQ", jit.Ptr(_ST, _ST_Sp), _CX) // MOVQ ST.Sp, CX
self.Emit("MOVQ", jit.Sib(_ST, _CX, 8, _ST_Vp), _SI) // MOVQ ST.Vp[CX], SI
self.Emit("MOVQ", jit.Ptr(_SI, 8), _SI) // MOVQ 8(SI), SI
/* add a new delimiter */
self.Emit("ADDQ", jit.Imm(1), _CX) // ADDQ $1, CX
self.Emit("CMPQ", _CX, jit.Imm(types.MAX_RECURSE)) // CMPQ CX, ${types.MAX_RECURSE}
self.Sjmp("JAE" , "_stack_overflow") // JA _stack_overflow
self.Emit("MOVQ", _CX, jit.Ptr(_ST, _ST_Sp)) // MOVQ CX, ST.Sp
self.Emit("MOVQ", jit.Imm(_S_obj_delim), jit.Sib(_ST, _CX, 8, _ST_Vt)) // MOVQ _S_obj_delim, ST.Vt[CX]
/* add a new slot int the map */
self.Emit("MOVQ", _T_map, _DX) // MOVQ _T_map, DX
self.Emit("MOVQ", _DX, jit.Ptr(_SP, 0)) // MOVQ DX, (SP)
self.Emit("MOVQ", _SI, jit.Ptr(_SP, 8)) // MOVQ SI, 8(SP)
self.Emit("MOVQ", _R8, jit.Ptr(_SP, 16)) // MOVQ R9, 16(SP)
self.Emit("MOVQ", _AX, jit.Ptr(_SP, 24)) // MOVQ AX, 24(SP)
self.call_go(_F_mapassign_faststr) // CALL_GO runtime.mapassign_faststr
self.Emit("MOVQ", jit.Ptr(_SP, 32), _AX) // MOVQ 32(SP), AX
/* add to the pointer stack */
self.Emit("MOVQ", jit.Ptr(_ST, _ST_Sp), _CX) // MOVQ ST.Sp, CX
self.WritePtrAX(6, jit.Sib(_ST, _CX, 8, _ST_Vp), false) // MOVQ AX, ST.Vp[CX]
self.Sjmp("JMP" , "_next") // JMP _next
/* allocate memory to store the string header and unquoted result */
self.Link("_unquote") // _unquote:
self.Emit("ADDQ", jit.Imm(15), _AX) // ADDQ $15, AX
self.Emit("MOVQ", _T_byte, _CX) // MOVQ _T_byte, CX
self.Emit("MOVQ", _AX, jit.Ptr(_SP, 0)) // MOVQ AX, (SP)
self.Emit("MOVQ", _CX, jit.Ptr(_SP, 8)) // MOVQ CX, 8(SP)
self.Emit("MOVB", jit.Imm(0), jit.Ptr(_SP, 16)) // MOVB $0, 16(SP)
self.call_go(_F_mallocgc) // CALL_GO runtime.mallocgc
self.Emit("MOVQ", jit.Ptr(_SP, 24), _R9) // MOVQ 24(SP), R9
/* prepare the unquoting parameters */
self.Emit("MOVQ" , _VAR_ss_Iv, _CX) // MOVQ ss.Iv, CX
self.Emit("LEAQ" , jit.Sib(_IP, _CX, 1, 0), _DI) // LEAQ (IP)(CX), DI
self.Emit("NEGQ" , _CX) // NEGQ CX
self.Emit("LEAQ" , jit.Sib(_IC, _CX, 1, -1), _SI) // LEAQ -1(IC)(CX), SI
self.Emit("LEAQ" , jit.Ptr(_R9, 16), _DX) // LEAQ 16(R8), DX
self.Emit("LEAQ" , _VAR_ss_Ep, _CX) // LEAQ ss.Ep, CX
self.Emit("XORL" , _R8, _R8) // XORL R8, R8
self.Emit("BTQ" , jit.Imm(_F_disable_urc), _VAR_df) // BTQ ${_F_disable_urc}, fv
self.Emit("SETCC", _R8) // SETCC R8
self.Emit("SHLQ" , jit.Imm(types.B_UNICODE_REPLACE), _R8) // SHLQ ${types.B_UNICODE_REPLACE}, R8
/* unquote the string, with R9 been preserved */
self.save(_R9) // SAVE R9
self.call(_F_unquote) // CALL unquote
self.load(_R9) // LOAD R9
/* check for errors */
self.Emit("TESTQ", _AX, _AX) // TESTQ AX, AX
self.Sjmp("JS" , "_unquote_error") // JS _unquote_error
self.Emit("MOVL" , jit.Imm(1), _DX) // MOVL $1, DX
self.Emit("LEAQ" , jit.Ptr(_R9, 16), _R8) // ADDQ $16, R8
self.Emit("MOVQ" , _R8, jit.Ptr(_R9, 0)) // MOVQ R8, (R9)
self.Emit("MOVQ" , _AX, jit.Ptr(_R9, 8)) // MOVQ AX, 8(R9)
self.Sjmp("JMP" , "_noescape") // JMP _noescape
/** V_DOUBLE **/
self.Link("_decode_V_DOUBLE") // _decode_V_DOUBLE:
self.Emit("BTQ" , jit.Imm(_F_use_number), _VAR_df) // BTQ _F_use_number, df
self.Sjmp("JC" , "_use_number") // JC _use_number
self.Emit("MOVSD", _VAR_ss_Dv, _X0) // MOVSD ss.Dv, X0
self.Sjmp("JMP" , "_use_float64") // JMP _use_float64
/** V_INTEGER **/
self.Link("_decode_V_INTEGER") // _decode_V_INTEGER:
self.Emit("BTQ" , jit.Imm(_F_use_number), _VAR_df) // BTQ _F_use_number, df
self.Sjmp("JC" , "_use_number") // JC _use_number
self.Emit("BTQ" , jit.Imm(_F_use_int64), _VAR_df) // BTQ _F_use_int64, df
self.Sjmp("JC" , "_use_int64") // JC _use_int64
self.Emit("MOVQ" , _VAR_ss_Iv, _AX) // MOVQ ss.Iv, AX
self.Emit("CVTSQ2SD", _AX, _X0) // CVTSQ2SD AX, X0
/* represent numbers as `float64` */
self.Link("_use_float64") // _use_float64:
self.Emit("MOVSD", _X0, jit.Ptr(_SP, 0)) // MOVSD X0, (SP)
self.call_go(_F_convT64) // CALL_GO runtime.convT64
self.Emit("MOVQ" , _T_float64, _R8) // MOVQ _T_float64, R8
self.Emit("MOVQ" , jit.Ptr(_SP, 8), _R9) // MOVQ 8(SP), R9
self.Emit("MOVQ" , _VAR_ss_Ep, _DI) // MOVQ ss.Ep, DI
self.Sjmp("JMP" , "_set_value") // JMP _set_value
/* represent numbers as `json.Number` */
self.Link("_use_number") // _use_number
self.Emit("MOVQ", _VAR_ss_Ep, _AX) // MOVQ ss.Ep, AX
self.Emit("LEAQ", jit.Sib(_IP, _AX, 1, 0), _SI) // LEAQ (IP)(AX), SI
self.Emit("MOVQ", _IC, _CX) // MOVQ IC, CX
self.Emit("SUBQ", _AX, _CX) // SUBQ AX, CX
self.Emit("MOVQ", _SI, jit.Ptr(_SP, 0)) // MOVQ SI, (SP)
self.Emit("MOVQ", _CX, jit.Ptr(_SP, 8)) // MOVQ CX, 8(SP)
self.call_go(_F_convTstring) // CALL_GO runtime.convTstring
self.Emit("MOVQ", _T_number, _R8) // MOVQ _T_number, R8
self.Emit("MOVQ", jit.Ptr(_SP, 16), _R9) // MOVQ 16(SP), R9
self.Emit("MOVQ", _VAR_ss_Ep, _DI) // MOVQ ss.Ep, DI
self.Sjmp("JMP" , "_set_value") // JMP _set_value
/* represent numbers as `int64` */
self.Link("_use_int64") // _use_int64:
self.Emit("MOVQ", _VAR_ss_Iv, _AX) // MOVQ ss.Iv, AX
self.Emit("MOVQ", _AX, jit.Ptr(_SP, 0)) // MOVQ AX, (SP)
self.call_go(_F_convT64) // CALL_GO runtime.convT64
self.Emit("MOVQ", _T_int64, _R8) // MOVQ _T_int64, R8
self.Emit("MOVQ", jit.Ptr(_SP, 8), _R9) // MOVQ 8(SP), R9
self.Emit("MOVQ", _VAR_ss_Ep, _DI) // MOVQ ss.Ep, DI
self.Sjmp("JMP" , "_set_value") // JMP _set_value
/** V_KEY_SEP **/
self.Link("_decode_V_KEY_SEP") // _decode_V_KEY_SEP:
// self.Byte(0xcc)
self.Emit("MOVQ", jit.Ptr(_ST, _ST_Sp), _CX) // MOVQ ST.Sp, CX
self.Emit("MOVQ", jit.Sib(_ST, _CX, 8, _ST_Vt), _AX) // MOVQ ST.Vt[CX], AX
self.Emit("CMPQ", _AX, jit.Imm(_S_obj_delim)) // CMPQ AX, _S_obj_delim
self.Sjmp("JNE" , "_invalid_char") // JNE _invalid_char
self.Emit("MOVQ", jit.Imm(_S_val), jit.Sib(_ST, _CX, 8, _ST_Vt)) // MOVQ _S_val, ST.Vt[CX]
self.Emit("MOVQ", jit.Imm(_S_obj), jit.Sib(_ST, _CX, 8, _ST_Vt - 8)) // MOVQ _S_obj, ST.Vt[CX - 1]
self.Sjmp("JMP" , "_next") // JMP _next
/** V_ELEM_SEP **/
self.Link("_decode_V_ELEM_SEP") // _decode_V_ELEM_SEP:
self.Emit("MOVQ" , jit.Ptr(_ST, _ST_Sp), _CX) // MOVQ ST.Sp, CX
self.Emit("MOVQ" , jit.Sib(_ST, _CX, 8, _ST_Vt), _AX) // MOVQ ST.Vt[CX], AX
self.Emit("CMPQ" , _AX, jit.Imm(_S_arr)) // CMPQ _AX, _S_arr
self.Sjmp("JE" , "_array_sep") // JZ _next
self.Emit("CMPQ" , _AX, jit.Imm(_S_obj)) // CMPQ _AX, _S_arr
self.Sjmp("JNE" , "_invalid_char") // JNE _invalid_char
self.Emit("MOVQ" , jit.Imm(_S_obj_sep), jit.Sib(_ST, _CX, 8, _ST_Vt))
self.Sjmp("JMP" , "_next") // JMP _next
/* arrays */
self.Link("_array_sep")
self.Emit("MOVQ", jit.Sib(_ST, _CX, 8, _ST_Vp), _SI) // MOVQ ST.Vp[CX], SI
self.Emit("MOVQ", jit.Ptr(_SI, 8), _SI) // MOVQ 8(SI), SI
self.Emit("MOVQ", jit.Ptr(_SI, 8), _DX) // MOVQ 8(SI), DX
self.Emit("CMPQ", _DX, jit.Ptr(_SI, 16)) // CMPQ DX, 16(SI)
self.Sjmp("JAE" , "_array_more") // JAE _array_more
/* add a slot for the new element */
self.Link("_array_append") // _array_append:
self.Emit("ADDQ", jit.Imm(1), jit.Ptr(_SI, 8)) // ADDQ $1, 8(SI)
self.Emit("MOVQ", jit.Ptr(_SI, 0), _SI) // MOVQ (SI), SI
self.Emit("ADDQ", jit.Imm(1), _CX) // ADDQ $1, CX
self.Emit("CMPQ", _CX, jit.Imm(types.MAX_RECURSE)) // CMPQ CX, ${types.MAX_RECURSE}
self.Sjmp("JAE" , "_stack_overflow")
self.Emit("SHLQ", jit.Imm(1), _DX) // SHLQ $1, DX
self.Emit("LEAQ", jit.Sib(_SI, _DX, 8, 0), _SI) // LEAQ (SI)(DX*8), SI
self.Emit("MOVQ", _CX, jit.Ptr(_ST, _ST_Sp)) // MOVQ CX, ST.Sp
self.WriteRecNotAX(7 , _SI, jit.Sib(_ST, _CX, 8, _ST_Vp), false) // MOVQ SI, ST.Vp[CX]
self.Emit("MOVQ", jit.Imm(_S_val), jit.Sib(_ST, _CX, 8, _ST_Vt)) // MOVQ _S_val, ST.Vt[CX}
self.Sjmp("JMP" , "_next") // JMP _next
/** V_ARRAY_END **/
self.Link("_decode_V_ARRAY_END") // _decode_V_ARRAY_END:
self.Emit("XORL", _DX, _DX) // XORL DX, DX
self.Emit("MOVQ", jit.Ptr(_ST, _ST_Sp), _CX) // MOVQ ST.Sp, CX
self.Emit("MOVQ", jit.Sib(_ST, _CX, 8, _ST_Vt), _AX) // MOVQ ST.Vt[CX], AX
self.Emit("CMPQ", _AX, jit.Imm(_S_arr_0)) // CMPQ AX, _S_arr_0
self.Sjmp("JE" , "_first_item") // JE _first_item
self.Emit("CMPQ", _AX, jit.Imm(_S_arr)) // CMPQ AX, _S_arr
self.Sjmp("JNE" , "_invalid_char") // JNE _invalid_char
self.Emit("SUBQ", jit.Imm(1), jit.Ptr(_ST, _ST_Sp)) // SUBQ $1, ST.Sp
self.Emit("MOVQ", _DX, jit.Sib(_ST, _CX, 8, _ST_Vp)) // MOVQ DX, ST.Vp[CX]
self.Sjmp("JMP" , "_next") // JMP _next
/* first element of an array */
self.Link("_first_item") // _first_item:
self.Emit("MOVQ", jit.Ptr(_ST, _ST_Sp), _CX) // MOVQ ST.Sp, CX
self.Emit("SUBQ", jit.Imm(2), jit.Ptr(_ST, _ST_Sp)) // SUBQ $2, ST.Sp
self.Emit("MOVQ", jit.Sib(_ST, _CX, 8, _ST_Vp - 8), _SI) // MOVQ ST.Vp[CX - 1], SI
self.Emit("MOVQ", jit.Ptr(_SI, 8), _SI) // MOVQ 8(SI), SI
self.Emit("MOVQ", _DX, jit.Sib(_ST, _CX, 8, _ST_Vp - 8)) // MOVQ DX, ST.Vp[CX - 1]
self.Emit("MOVQ", _DX, jit.Sib(_ST, _CX, 8, _ST_Vp)) // MOVQ DX, ST.Vp[CX]
self.Emit("MOVQ", _DX, jit.Ptr(_SI, 8)) // MOVQ DX, 8(SI)
self.Sjmp("JMP" , "_next") // JMP _next
/** V_OBJECT_END **/
self.Link("_decode_V_OBJECT_END") // _decode_V_OBJECT_END:
self.Emit("MOVL", jit.Imm(_S_omask_end), _DX) // MOVL _S_omask, DI
self.Emit("MOVQ", jit.Ptr(_ST, _ST_Sp), _CX) // MOVQ ST.Sp, CX
self.Emit("MOVQ", jit.Sib(_ST, _CX, 8, _ST_Vt), _AX) // MOVQ ST.Vt[CX], AX
self.Emit("BTQ" , _AX, _DX)
self.Sjmp("JNC" , "_invalid_char") // JNE _invalid_char
self.Emit("XORL", _AX, _AX) // XORL AX, AX
self.Emit("SUBQ", jit.Imm(1), jit.Ptr(_ST, _ST_Sp)) // SUBQ $1, ST.Sp
self.Emit("MOVQ", _AX, jit.Sib(_ST, _CX, 8, _ST_Vp)) // MOVQ AX, ST.Vp[CX]
self.Sjmp("JMP" , "_next") // JMP _next
/* return from decoder */
self.Link("_return") // _return:
self.Emit("XORL", _EP, _EP) // XORL EP, EP
self.Emit("MOVQ", _EP, jit.Ptr(_ST, _ST_Vp)) // MOVQ EP, ST.Vp[0]
self.Link("_epilogue") // _epilogue:
self.Emit("SUBQ", jit.Imm(_FsmOffset), _ST) // SUBQ _FsmOffset, _ST
self.Emit("MOVQ", jit.Ptr(_SP, _VD_offs), _BP) // MOVQ _VD_offs(SP), BP
self.Emit("ADDQ", jit.Imm(_VD_size), _SP) // ADDQ $_VD_size, SP
self.Emit("RET") // RET
/* array expand */
self.Link("_array_more") // _array_more:
self.Emit("MOVQ" , _T_eface, _AX) // MOVQ _T_eface, AX
self.Emit("MOVOU", jit.Ptr(_SI, 0), _X0) // MOVOU (SI), X0
self.Emit("MOVQ" , jit.Ptr(_SI, 16), _DX) // MOVQ 16(SI), DX
self.Emit("MOVQ" , _AX, jit.Ptr(_SP, 0)) // MOVQ AX, (SP)
self.Emit("MOVOU", _X0, jit.Ptr(_SP, 8)) // MOVOU X0, 8(SP)
self.Emit("MOVQ" , _DX, jit.Ptr(_SP, 24)) // MOVQ DX, 24(SP)
self.Emit("SHLQ" , jit.Imm(1), _DX) // SHLQ $1, DX
self.Emit("MOVQ" , _DX, jit.Ptr(_SP, 32)) // MOVQ DX, 32(SP)
self.call_go(_F_growslice) // CALL_GO runtime.growslice
self.Emit("MOVQ" , jit.Ptr(_SP, 40), _DI) // MOVOU 40(SP), DI
self.Emit("MOVQ" , jit.Ptr(_SP, 48), _DX) // MOVOU 48(SP), DX
self.Emit("MOVQ" , jit.Ptr(_SP, 56), _AX) // MOVQ 56(SP), AX
/* update the slice */
self.Emit("MOVQ", jit.Ptr(_ST, _ST_Sp), _CX) // MOVQ ST.Sp, CX
self.Emit("MOVQ", jit.Sib(_ST, _CX, 8, _ST_Vp), _SI) // MOVQ ST.Vp[CX], SI
self.Emit("MOVQ", jit.Ptr(_SI, 8), _SI) // MOVQ 8(SI), SI
self.Emit("MOVQ", _DX, jit.Ptr(_SI, 8)) // MOVQ DX, 8(SI)
self.Emit("MOVQ", _AX, jit.Ptr(_SI, 16)) // MOVQ AX, 16(AX)
self.WriteRecNotAX(8 , _DI, jit.Ptr(_SI, 0), false) // MOVQ R10, (SI)
self.Sjmp("JMP" , "_array_append") // JMP _array_append
/* copy string */
self.Link("copy_string") // pointer: R8, length: AX, return addr: DI
// self.Byte(0xcc)
self.Emit("MOVQ", _R8, _VAR_cs_p)
self.Emit("MOVQ", _AX, _VAR_cs_n)
self.Emit("MOVQ", _DI, _VAR_cs_LR)
self.Emit("MOVQ", _T_byte, _R8)
self.Emit("MOVQ", _R8, jit.Ptr(_SP, 0))
self.Emit("MOVQ", _AX, jit.Ptr(_SP, 8))
self.Emit("MOVQ", _AX, jit.Ptr(_SP, 16))
self.call_go(_F_makeslice)
self.Emit("MOVQ", jit.Ptr(_SP, 24), _R8)
self.Emit("MOVQ", _R8, _VAR_cs_d)
self.Emit("MOVQ", _R8, jit.Ptr(_SP, 0))
self.Emit("MOVQ", _VAR_cs_p, _R8)
self.Emit("MOVQ", _R8, jit.Ptr(_SP, 8))
self.Emit("MOVQ", _VAR_cs_n, _AX)
self.Emit("MOVQ", _AX, jit.Ptr(_SP, 16))
self.call_go(_F_memmove)
self.Emit("MOVQ", _VAR_cs_d, _R8)
self.Emit("MOVQ", _VAR_cs_n, _AX)
self.Emit("MOVQ", _VAR_cs_LR, _DI)
// self.Byte(0xcc)
self.Rjmp("JMP", _DI)
/* error handlers */
self.Link("_stack_overflow")
self.Emit("MOVL" , _E_recurse, _EP) // MOVQ _E_recurse, EP
self.Sjmp("JMP" , "_error") // JMP _error
self.Link("_vtype_error") // _vtype_error:
self.Emit("MOVQ" , _DI, _IC) // MOVQ DI, IC
self.Emit("MOVL" , _E_invalid, _EP) // MOVL _E_invalid, EP
self.Sjmp("JMP" , "_error") // JMP _error
self.Link("_invalid_char") // _invalid_char:
self.Emit("SUBQ" , jit.Imm(1), _IC) // SUBQ $1, IC
self.Emit("MOVL" , _E_invalid, _EP) // MOVL _E_invalid, EP
self.Sjmp("JMP" , "_error") // JMP _error
self.Link("_unquote_error") // _unquote_error:
self.Emit("MOVQ" , _VAR_ss_Iv, _IC) // MOVQ ss.Iv, IC
self.Emit("SUBQ" , jit.Imm(1), _IC) // SUBQ $1, IC
self.Link("_parsing_error") // _parsing_error:
self.Emit("NEGQ" , _AX) // NEGQ AX
self.Emit("MOVQ" , _AX, _EP) // MOVQ AX, EP
self.Link("_error") // _error:
self.Emit("PXOR" , _X0, _X0) // PXOR X0, X0
self.Emit("MOVOU", _X0, jit.Ptr(_VP, 0)) // MOVOU X0, (VP)
self.Sjmp("JMP" , "_epilogue") // JMP _epilogue
/* invalid value type, never returns */
self.Link("_invalid_vtype")
self.Emit("MOVQ", _AX, jit.Ptr(_SP, 0)) // MOVQ AX, (SP)
self.call(_F_invalid_vtype) // CALL invalid_type
self.Emit("UD2") // UD2
/* switch jump table */
self.Link("_switch_table") // _switch_table:
self.Sref("_decode_V_EOF", 0) // SREF &_decode_V_EOF, $0
self.Sref("_decode_V_NULL", -4) // SREF &_decode_V_NULL, $-4
self.Sref("_decode_V_TRUE", -8) // SREF &_decode_V_TRUE, $-8
self.Sref("_decode_V_FALSE", -12) // SREF &_decode_V_FALSE, $-12
self.Sref("_decode_V_ARRAY", -16) // SREF &_decode_V_ARRAY, $-16
self.Sref("_decode_V_OBJECT", -20) // SREF &_decode_V_OBJECT, $-20
self.Sref("_decode_V_STRING", -24) // SREF &_decode_V_STRING, $-24
self.Sref("_decode_V_DOUBLE", -28) // SREF &_decode_V_DOUBLE, $-28
self.Sref("_decode_V_INTEGER", -32) // SREF &_decode_V_INTEGER, $-32
self.Sref("_decode_V_KEY_SEP", -36) // SREF &_decode_V_KEY_SEP, $-36
self.Sref("_decode_V_ELEM_SEP", -40) // SREF &_decode_V_ELEM_SEP, $-40
self.Sref("_decode_V_ARRAY_END", -44) // SREF &_decode_V_ARRAY_END, $-44
self.Sref("_decode_V_OBJECT_END", -48) // SREF &_decode_V_OBJECT_END, $-48
/* fast character lookup table */
self.Link("_decode_tab") // _decode_tab:
self.Sref("_decode_V_EOF", 0) // SREF &_decode_V_EOF, $0
/* generate rest of the tabs */
for i := 1; i < 256; i++ {
if to, ok := _R_tab[i]; ok {
self.Sref(to, -int64(i) * 4)
} else {
self.Byte(0x00, 0x00, 0x00, 0x00)
}
}
}
/** Generic Decoder **/
var (
_subr_decode_value = new(_ValueDecoder).build()
)
//go:nosplit
func invalid_vtype(vt types.ValueType) {
throw(fmt.Sprintf("invalid value type: %d", vt))
}
@@ -0,0 +1,37 @@
// +build go1.16,!go1.17
//
// Copyright 2021 ByteDance Inc.
//
// 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.
//
#include "go_asm.h"
#include "funcdata.h"
#include "textflag.h"
TEXT ·decodeValueStub(SB), NOSPLIT, $0 - 72
NO_LOCAL_POINTERS
PXOR X0, X0
MOVOU X0, rv+48(FP)
MOVQ st+0(FP), BX
MOVQ sp+8(FP), R12
MOVQ sn+16(FP), R13
MOVQ ic+24(FP), R14
MOVQ vp+32(FP), R15
MOVQ df+40(FP), R10
MOVQ ·_subr_decode_value(SB), AX
CALL AX
MOVQ R14, rp+48(FP)
MOVQ R11, ex+56(FP)
RET
@@ -0,0 +1,143 @@
/*
* Copyright 2021 ByteDance Inc.
*
* 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.
*/
package decoder
import (
`sync`
`unsafe`
`github.com/bytedance/sonic/internal/caching`
`github.com/bytedance/sonic/internal/native/types`
`github.com/bytedance/sonic/internal/rt`
)
const (
_MinSlice = 2
_MaxStack = 4096 // 4k slots
_MaxStackBytes = _MaxStack * _PtrBytes
_MaxDigitNums = types.MaxDigitNums // used in atof fallback algorithm
)
const (
_PtrBytes = _PTR_SIZE / 8
_FsmOffset = (_MaxStack + 1) * _PtrBytes
_DbufOffset = _FsmOffset + int64(unsafe.Sizeof(types.StateMachine{})) + types.MAX_RECURSE * _PtrBytes
_StackSize = unsafe.Sizeof(_Stack{})
)
var (
stackPool = sync.Pool{}
valueCache = []unsafe.Pointer(nil)
fieldCache = []*caching.FieldMap(nil)
fieldCacheMux = sync.Mutex{}
programCache = caching.CreateProgramCache()
)
type _Stack struct {
sp uintptr
sb [_MaxStack]unsafe.Pointer
mm types.StateMachine
vp [types.MAX_RECURSE]unsafe.Pointer
dp [_MaxDigitNums]byte
}
type _Decoder func(
s string,
i int,
vp unsafe.Pointer,
sb *_Stack,
fv uint64,
sv string, // DO NOT pass value to this arguement, since it is only used for local _VAR_sv
vk unsafe.Pointer, // DO NOT pass value to this arguement, since it is only used for local _VAR_vk
) (int, error)
var _KeepAlive struct {
s string
i int
vp unsafe.Pointer
sb *_Stack
fv uint64
sv string
vk unsafe.Pointer
ret int
err error
frame_decoder [_FP_offs]byte
frame_generic [_VD_offs]byte
}
var (
argPtrs = []bool{true, false, false, true, true, false, true, false, true}
localPtrs = []bool{}
)
var (
argPtrs_generic = []bool{true}
localPtrs_generic = []bool{}
)
func newStack() *_Stack {
if ret := stackPool.Get(); ret == nil {
return new(_Stack)
} else {
return ret.(*_Stack)
}
}
func resetStack(p *_Stack) {
memclrNoHeapPointers(unsafe.Pointer(p), _StackSize)
}
func freeStack(p *_Stack) {
p.sp = 0
stackPool.Put(p)
}
func freezeValue(v unsafe.Pointer) uintptr {
valueCache = append(valueCache, v)
return uintptr(v)
}
func freezeFields(v *caching.FieldMap) int64 {
fieldCacheMux.Lock()
fieldCache = append(fieldCache, v)
fieldCacheMux.Unlock()
return referenceFields(v)
}
func referenceFields(v *caching.FieldMap) int64 {
return int64(uintptr(unsafe.Pointer(v)))
}
func makeDecoder(vt *rt.GoType, _ ...interface{}) (interface{}, error) {
if pp, err := newCompiler().compile(vt.Pack()); err != nil {
return nil, err
} else {
return newAssembler(pp).Load(), nil
}
}
func findOrCompile(vt *rt.GoType) (_Decoder, error) {
if val := programCache.Get(vt); val != nil {
return val.(_Decoder), nil
} else if ret, err := programCache.Compute(vt, makeDecoder); err == nil {
return ret.(_Decoder), nil
} else {
return nil, err
}
}
@@ -0,0 +1,44 @@
/*
* Copyright 2021 ByteDance Inc.
*
* 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.
*/
package decoder
import (
`encoding`
`encoding/json`
`unsafe`
`github.com/bytedance/sonic/internal/native`
`github.com/bytedance/sonic/internal/rt`
)
func decodeTypedPointer(s string, i int, vt *rt.GoType, vp unsafe.Pointer, sb *_Stack, fv uint64) (int, error) {
if fn, err := findOrCompile(vt); err != nil {
return 0, err
} else {
rt.MoreStack(_FP_size + _VD_size + native.MaxFrameSize)
ret, err := fn(s, i, vp, sb, fv, "", nil)
return ret, err
}
}
func decodeJsonUnmarshaler(vv interface{}, s string) error {
return vv.(json.Unmarshaler).UnmarshalJSON(rt.Str2Mem(s))
}
func decodeTextUnmarshaler(vv interface{}, s string) error {
return vv.(encoding.TextUnmarshaler).UnmarshalText(rt.Str2Mem(s))
}
@@ -0,0 +1,257 @@
/*
* Copyright 2021 ByteDance Inc.
*
* 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.
*/
package decoder
import (
`bytes`
`io`
`sync`
`github.com/bytedance/sonic/internal/native`
`github.com/bytedance/sonic/internal/native/types`
`github.com/bytedance/sonic/internal/rt`
`github.com/bytedance/sonic/option`
)
var (
minLeftBufferShift uint = 1
)
// StreamDecoder is the decoder context object for streaming input.
type StreamDecoder struct {
r io.Reader
buf []byte
scanp int
scanned int64
err error
Decoder
}
var bufPool = sync.Pool{
New: func () interface{} {
return make([]byte, 0, option.DefaultDecoderBufferSize)
},
}
// NewStreamDecoder adapts to encoding/json.NewDecoder API.
//
// NewStreamDecoder returns a new decoder that reads from r.
func NewStreamDecoder(r io.Reader) *StreamDecoder {
return &StreamDecoder{r : r}
}
// Decode decodes input stream into val with corresponding data.
// Redundantly bytes may be read and left in its buffer, and can be used at next call.
// Either io error from underlying io.Reader (except io.EOF)
// or syntax error from data will be recorded and stop subsequently decoding.
func (self *StreamDecoder) Decode(val interface{}) (err error) {
// read more data into buf
if self.More() {
// println(string(self.buf))
var s = self.scanp
try_skip:
var e = len(self.buf)
// println("s:", s, "e:", e, "scanned:",self.scanned, "scanp:",self.scanp, self.buf)
var src = rt.Mem2Str(self.buf[s:e])
// if len(src) > 5 {
// println(src[:5], src[len(src)-5:])
// } else {
// println(src)
// }
// try skip
var x = 0;
if y := native.SkipOneFast(&src, &x); y < 0 {
if self.readMore() {
// println("more")
goto try_skip
} else {
// println("no more")
err = SyntaxError{e, self.s, types.ParsingError(-s), ""}
self.setErr(err)
return
}
} else {
s = y + s
e = x + s
}
// println("decode: ", s, e)
// must copy string here for safety
self.Decoder.Reset(string(self.buf[s:e]))
err = self.Decoder.Decode(val)
if err != nil {
self.setErr(err)
return
}
self.scanp = e
_, empty := self.scan()
if empty {
// println("recycle")
// no remain valid bytes, thus we just recycle buffer
mem := self.buf
self.buf = nil
bufPool.Put(mem[:0])
} else {
// println("keep")
// remain undecoded bytes, move them onto head
n := copy(self.buf, self.buf[self.scanp:])
self.buf = self.buf[:n]
}
self.scanned += int64(self.scanp)
self.scanp = 0
}
return self.err
}
// InputOffset returns the input stream byte offset of the current decoder position.
// The offset gives the location of the end of the most recently returned token and the beginning of the next token.
func (self *StreamDecoder) InputOffset() int64 {
// println("input offset",self.scanned, self.scanp)
return self.scanned + int64(self.scanp)
}
// Buffered returns a reader of the data remaining in the Decoder's buffer.
// The reader is valid until the next call to Decode.
func (self *StreamDecoder) Buffered() io.Reader {
return bytes.NewReader(self.buf[self.scanp:])
}
// More reports whether there is another element in the
// current array or object being parsed.
func (self *StreamDecoder) More() bool {
if self.err != nil {
return false
}
c, err := self.peek()
return err == nil && c != ']' && c != '}'
}
// More reports whether there is another element in the
// current array or object being parsed.
func (self *StreamDecoder) readMore() bool {
if self.err != nil {
return false
}
var err error
var n int
for {
// Grow buffer if not large enough.
l := len(self.buf)
realloc(&self.buf)
n, err = self.r.Read(self.buf[l:cap(self.buf)])
self.buf = self.buf[: l+n]
self.scanp = l
_, empty := self.scan()
if !empty {
return true
}
// buffer has been scanned, now report any error
if err != nil {
self.setErr(err)
return false
}
}
}
func (self *StreamDecoder) setErr(err error) {
self.err = err
mem := self.buf[:0]
self.buf = nil
bufPool.Put(mem)
}
func (self *StreamDecoder) peek() (byte, error) {
var err error
for {
c, empty := self.scan()
if !empty {
return byte(c), nil
}
// buffer has been scanned, now report any error
if err != nil {
self.setErr(err)
return 0, err
}
err = self.refill()
}
}
func (self *StreamDecoder) scan() (byte, bool) {
for i := self.scanp; i < len(self.buf); i++ {
c := self.buf[i]
if isSpace(c) {
continue
}
self.scanp = i
return c, false
}
return 0, true
}
func isSpace(c byte) bool {
return types.SPACE_MASK & (1 << c) != 0
}
func (self *StreamDecoder) refill() error {
// Make room to read more into the buffer.
// First slide down data already consumed.
if self.scanp > 0 {
self.scanned += int64(self.scanp)
n := copy(self.buf, self.buf[self.scanp:])
self.buf = self.buf[:n]
self.scanp = 0
}
// Grow buffer if not large enough.
realloc(&self.buf)
// Read. Delay error for next iteration (after scan).
n, err := self.r.Read(self.buf[len(self.buf):cap(self.buf)])
self.buf = self.buf[0 : len(self.buf)+n]
return err
}
func realloc(buf *[]byte) bool {
l := uint(len(*buf))
c := uint(cap(*buf))
if c == 0 {
// println("use pool!")
*buf = bufPool.Get().([]byte)
return true
}
if c - l <= c >> minLeftBufferShift {
// println("realloc!")
e := l+(l>>minLeftBufferShift)
if e <= c {
e = c*2
}
tmp := make([]byte, l, e)
copy(tmp, *buf)
*buf = tmp
return true
}
return false
}
@@ -0,0 +1,111 @@
// +build go1.16,!go1.20
/*
* Copyright 2021 ByteDance Inc.
*
* 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.
*/
package decoder
import (
`unsafe`
`reflect`
_ `github.com/cloudwego/base64x`
`github.com/bytedance/sonic/internal/rt`
)
//go:linkname _subr__b64decode github.com/cloudwego/base64x._subr__b64decode
var _subr__b64decode uintptr
// runtime.maxElementSize
const _max_map_element_size uintptr = 128
func mapfast(vt reflect.Type) bool {
return vt.Elem().Size() <= _max_map_element_size
}
//go:nosplit
//go:linkname throw runtime.throw
//goland:noinspection GoUnusedParameter
func throw(s string)
//go:linkname convT64 runtime.convT64
//goland:noinspection GoUnusedParameter
func convT64(v uint64) unsafe.Pointer
//go:linkname convTslice runtime.convTslice
//goland:noinspection GoUnusedParameter
func convTslice(v []byte) unsafe.Pointer
//go:linkname convTstring runtime.convTstring
//goland:noinspection GoUnusedParameter
func convTstring(v string) unsafe.Pointer
//go:noescape
//go:linkname memequal runtime.memequal
//goland:noinspection GoUnusedParameter
func memequal(a unsafe.Pointer, b unsafe.Pointer, size uintptr) bool
//go:noescape
//go:linkname memmove runtime.memmove
//goland:noinspection GoUnusedParameter
func memmove(to unsafe.Pointer, from unsafe.Pointer, n uintptr)
//go:linkname mallocgc runtime.mallocgc
//goland:noinspection GoUnusedParameter
func mallocgc(size uintptr, typ *rt.GoType, needzero bool) unsafe.Pointer
//go:linkname makeslice runtime.makeslice
//goland:noinspection GoUnusedParameter
func makeslice(et *rt.GoType, len int, cap int) unsafe.Pointer
//go:noescape
//go:linkname growslice runtime.growslice
//goland:noinspection GoUnusedParameter
func growslice(et *rt.GoType, old rt.GoSlice, cap int) rt.GoSlice
//go:linkname makemap_small runtime.makemap_small
func makemap_small() unsafe.Pointer
//go:linkname mapassign runtime.mapassign
//goland:noinspection GoUnusedParameter
func mapassign(t *rt.GoType, h unsafe.Pointer, k unsafe.Pointer) unsafe.Pointer
//go:linkname mapassign_fast32 runtime.mapassign_fast32
//goland:noinspection GoUnusedParameter
func mapassign_fast32(t *rt.GoType, h unsafe.Pointer, k uint32) unsafe.Pointer
//go:linkname mapassign_fast64 runtime.mapassign_fast64
//goland:noinspection GoUnusedParameter
func mapassign_fast64(t *rt.GoType, h unsafe.Pointer, k uint64) unsafe.Pointer
//go:linkname mapassign_fast64ptr runtime.mapassign_fast64ptr
//goland:noinspection GoUnusedParameter
func mapassign_fast64ptr(t *rt.GoType, h unsafe.Pointer, k unsafe.Pointer) unsafe.Pointer
//go:linkname mapassign_faststr runtime.mapassign_faststr
//goland:noinspection GoUnusedParameter
func mapassign_faststr(t *rt.GoType, h unsafe.Pointer, s string) unsafe.Pointer
//go:nosplit
//go:linkname memclrHasPointers runtime.memclrHasPointers
//goland:noinspection GoUnusedParameter
func memclrHasPointers(ptr unsafe.Pointer, n uintptr)
//go:noescape
//go:linkname memclrNoHeapPointers runtime.memclrNoHeapPointers
//goland:noinspection GoUnusedParameter
func memclrNoHeapPointers(ptr unsafe.Pointer, n uintptr)
@@ -0,0 +1,111 @@
// +build go1.20
/*
* Copyright 2021 ByteDance Inc.
*
* 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.
*/
package decoder
import (
`unsafe`
`reflect`
_ `github.com/cloudwego/base64x`
`github.com/bytedance/sonic/internal/rt`
)
//go:linkname _subr__b64decode github.com/cloudwego/base64x._subr__b64decode
var _subr__b64decode uintptr
// runtime.maxElementSize
const _max_map_element_size uintptr = 128
func mapfast(vt reflect.Type) bool {
return vt.Elem().Size() <= _max_map_element_size
}
//go:nosplit
//go:linkname throw runtime.throw
//goland:noinspection GoUnusedParameter
func throw(s string)
//go:linkname convT64 runtime.convT64
//goland:noinspection GoUnusedParameter
func convT64(v uint64) unsafe.Pointer
//go:linkname convTslice runtime.convTslice
//goland:noinspection GoUnusedParameter
func convTslice(v []byte) unsafe.Pointer
//go:linkname convTstring runtime.convTstring
//goland:noinspection GoUnusedParameter
func convTstring(v string) unsafe.Pointer
//go:noescape
//go:linkname memequal runtime.memequal
//goland:noinspection GoUnusedParameter
func memequal(a unsafe.Pointer, b unsafe.Pointer, size uintptr) bool
//go:noescape
//go:linkname memmove runtime.memmove
//goland:noinspection GoUnusedParameter
func memmove(to unsafe.Pointer, from unsafe.Pointer, n uintptr)
//go:linkname mallocgc runtime.mallocgc
//goland:noinspection GoUnusedParameter
func mallocgc(size uintptr, typ *rt.GoType, needzero bool) unsafe.Pointer
//go:linkname makeslice runtime.makeslice
//goland:noinspection GoUnusedParameter
func makeslice(et *rt.GoType, len int, cap int) unsafe.Pointer
//go:noescape
//go:linkname growslice reflect.growslice
//goland:noinspection GoUnusedParameter
func growslice(et *rt.GoType, old rt.GoSlice, cap int) rt.GoSlice
//go:linkname makemap_small runtime.makemap_small
func makemap_small() unsafe.Pointer
//go:linkname mapassign runtime.mapassign
//goland:noinspection GoUnusedParameter
func mapassign(t *rt.GoMapType, h unsafe.Pointer, k unsafe.Pointer) unsafe.Pointer
//go:linkname mapassign_fast32 runtime.mapassign_fast32
//goland:noinspection GoUnusedParameter
func mapassign_fast32(t *rt.GoMapType, h unsafe.Pointer, k uint32) unsafe.Pointer
//go:linkname mapassign_fast64 runtime.mapassign_fast64
//goland:noinspection GoUnusedParameter
func mapassign_fast64(t *rt.GoMapType, h unsafe.Pointer, k uint64) unsafe.Pointer
//go:linkname mapassign_fast64ptr runtime.mapassign_fast64ptr
//goland:noinspection GoUnusedParameter
func mapassign_fast64ptr(t *rt.GoMapType, h unsafe.Pointer, k unsafe.Pointer) unsafe.Pointer
//go:linkname mapassign_faststr runtime.mapassign_faststr
//goland:noinspection GoUnusedParameter
func mapassign_faststr(t *rt.GoMapType, h unsafe.Pointer, s string) unsafe.Pointer
//go:nosplit
//go:linkname memclrHasPointers runtime.memclrHasPointers
//goland:noinspection GoUnusedParameter
func memclrHasPointers(ptr unsafe.Pointer, n uintptr)
//go:noescape
//go:linkname memclrNoHeapPointers runtime.memclrNoHeapPointers
//goland:noinspection GoUnusedParameter
func memclrNoHeapPointers(ptr unsafe.Pointer, n uintptr)
@@ -0,0 +1,58 @@
/*
* Copyright 2021 ByteDance Inc.
*
* 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.
*/
package decoder
import (
`encoding`
`encoding/base64`
`encoding/json`
`reflect`
`unsafe`
`github.com/bytedance/sonic/internal/rt`
)
var (
byteType = reflect.TypeOf(byte(0))
intType = reflect.TypeOf(int(0))
int8Type = reflect.TypeOf(int8(0))
int16Type = reflect.TypeOf(int16(0))
int32Type = reflect.TypeOf(int32(0))
int64Type = reflect.TypeOf(int64(0))
uintType = reflect.TypeOf(uint(0))
uint8Type = reflect.TypeOf(uint8(0))
uint16Type = reflect.TypeOf(uint16(0))
uint32Type = reflect.TypeOf(uint32(0))
uint64Type = reflect.TypeOf(uint64(0))
float32Type = reflect.TypeOf(float32(0))
float64Type = reflect.TypeOf(float64(0))
stringType = reflect.TypeOf("")
bytesType = reflect.TypeOf([]byte(nil))
jsonNumberType = reflect.TypeOf(json.Number(""))
base64CorruptInputError = reflect.TypeOf(base64.CorruptInputError(0))
)
var (
errorType = reflect.TypeOf((*error)(nil)).Elem()
jsonUnmarshalerType = reflect.TypeOf((*json.Unmarshaler)(nil)).Elem()
encodingTextUnmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem()
)
func rtype(t reflect.Type) (*rt.GoItab, *rt.GoType) {
p := (*rt.GoIface)(unsafe.Pointer(&t))
return p.Itab, (*rt.GoType)(p.Value)
}
@@ -0,0 +1,39 @@
/*
* Copyright 2021 ByteDance Inc.
*
* 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.
*/
package decoder
import (
`unsafe`
`github.com/bytedance/sonic/loader`
)
//go:nosplit
func pbool(v bool) uintptr {
return freezeValue(unsafe.Pointer(&v))
}
//go:nosplit
func ptodec(p loader.Function) _Decoder {
return *(*_Decoder)(unsafe.Pointer(&p))
}
func assert_eq(v int64, exp int64, msg string) {
if v != exp {
panic(msg)
}
}
@@ -0,0 +1,51 @@
// +build go1.16,!go1.17
// Copyright 2023 CloudWeGo Authors
//
// 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.
package encoder
import (
`strconv`
`github.com/bytedance/sonic/internal/jit`
`github.com/twitchyliquid64/golang-asm/obj`
`github.com/twitchyliquid64/golang-asm/obj/x86`
)
var (
_V_writeBarrier = jit.Imm(int64(_runtime_writeBarrier))
_F_gcWriteBarrierAX = jit.Func(gcWriteBarrierAX)
)
func (self *_Assembler) WritePtr(i int, ptr obj.Addr, rec obj.Addr) {
if rec.Reg == x86.REG_AX || rec.Index == x86.REG_AX {
panic("rec contains AX!")
}
self.Emit("MOVQ", _V_writeBarrier, _R10)
self.Emit("CMPL", jit.Ptr(_R10, 0), jit.Imm(0))
self.Sjmp("JE", "_no_writeBarrier" + strconv.Itoa(i) + "_{n}")
self.Emit("MOVQ", ptr, _AX)
self.xsave(_DI)
self.Emit("LEAQ", rec, _DI)
self.Emit("MOVQ", _F_gcWriteBarrierAX, _R10) // MOVQ ${fn}, AX
self.Rjmp("CALL", _R10)
self.xload(_DI)
self.Sjmp("JMP", "_end_writeBarrier" + strconv.Itoa(i) + "_{n}")
self.Link("_no_writeBarrier" + strconv.Itoa(i) + "_{n}")
self.Emit("MOVQ", ptr, rec)
self.Link("_end_writeBarrier" + strconv.Itoa(i) + "_{n}")
}
@@ -0,0 +1,51 @@
// +build go1.17,!go1.21
// Copyright 2023 CloudWeGo Authors
//
// 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.
package encoder
import (
`strconv`
`unsafe`
`github.com/bytedance/sonic/internal/jit`
`github.com/twitchyliquid64/golang-asm/obj`
`github.com/twitchyliquid64/golang-asm/obj/x86`
)
var (
_V_writeBarrier = jit.Imm(int64(uintptr(unsafe.Pointer(&_runtime_writeBarrier))))
_F_gcWriteBarrierAX = jit.Func(gcWriteBarrierAX)
)
func (self *_Assembler) WritePtr(i int, ptr obj.Addr, rec obj.Addr) {
if rec.Reg == x86.REG_AX || rec.Index == x86.REG_AX {
panic("rec contains AX!")
}
self.Emit("MOVQ", _V_writeBarrier, _BX)
self.Emit("CMPL", jit.Ptr(_BX, 0), jit.Imm(0))
self.Sjmp("JE", "_no_writeBarrier" + strconv.Itoa(i) + "_{n}")
self.xsave(_DI)
self.Emit("MOVQ", ptr, _AX)
self.Emit("LEAQ", rec, _DI)
self.Emit("MOVQ", _F_gcWriteBarrierAX, _BX) // MOVQ ${fn}, AX
self.Rjmp("CALL", _BX)
self.xload(_DI)
self.Sjmp("JMP", "_end_writeBarrier" + strconv.Itoa(i) + "_{n}")
self.Link("_no_writeBarrier" + strconv.Itoa(i) + "_{n}")
self.Emit("MOVQ", ptr, rec)
self.Link("_end_writeBarrier" + strconv.Itoa(i) + "_{n}")
}
@@ -0,0 +1,50 @@
// +build go1.21,!go1.23
// Copyright 2023 CloudWeGo Authors
//
// 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.
package encoder
import (
`strconv`
`unsafe`
`github.com/bytedance/sonic/internal/jit`
`github.com/twitchyliquid64/golang-asm/obj`
`github.com/twitchyliquid64/golang-asm/obj/x86`
)
var (
_V_writeBarrier = jit.Imm(int64(uintptr(unsafe.Pointer(&_runtime_writeBarrier))))
_F_gcWriteBarrier2 = jit.Func(gcWriteBarrier2)
)
func (self *_Assembler) WritePtr(i int, ptr obj.Addr, old obj.Addr) {
if old.Reg == x86.REG_AX || old.Index == x86.REG_AX {
panic("rec contains AX!")
}
self.Emit("MOVQ", _V_writeBarrier, _BX)
self.Emit("CMPL", jit.Ptr(_BX, 0), jit.Imm(0))
self.Sjmp("JE", "_no_writeBarrier" + strconv.Itoa(i) + "_{n}")
self.xsave(_SP_q)
self.Emit("MOVQ", _F_gcWriteBarrier2, _BX) // MOVQ ${fn}, AX
self.Rjmp("CALL", _BX)
self.Emit("MOVQ", ptr, jit.Ptr(_SP_q, 0))
self.Emit("MOVQ", old, _AX)
self.Emit("MOVQ", _AX, jit.Ptr(_SP_q, 8))
self.xload(_SP_q)
self.Link("_no_writeBarrier" + strconv.Itoa(i) + "_{n}")
self.Emit("MOVQ", ptr, old)
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,885 @@
/*
* Copyright 2021 ByteDance Inc.
*
* 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.
*/
package encoder
import (
`fmt`
`reflect`
`strconv`
`strings`
`unsafe`
`github.com/bytedance/sonic/internal/resolver`
`github.com/bytedance/sonic/internal/rt`
`github.com/bytedance/sonic/option`
)
type _Op uint8
const (
_OP_null _Op = iota + 1
_OP_empty_arr
_OP_empty_obj
_OP_bool
_OP_i8
_OP_i16
_OP_i32
_OP_i64
_OP_u8
_OP_u16
_OP_u32
_OP_u64
_OP_f32
_OP_f64
_OP_str
_OP_bin
_OP_quote
_OP_number
_OP_eface
_OP_iface
_OP_byte
_OP_text
_OP_deref
_OP_index
_OP_load
_OP_save
_OP_drop
_OP_drop_2
_OP_recurse
_OP_is_nil
_OP_is_nil_p1
_OP_is_zero_1
_OP_is_zero_2
_OP_is_zero_4
_OP_is_zero_8
_OP_is_zero_map
_OP_goto
_OP_map_iter
_OP_map_stop
_OP_map_check_key
_OP_map_write_key
_OP_map_value_next
_OP_slice_len
_OP_slice_next
_OP_marshal
_OP_marshal_p
_OP_marshal_text
_OP_marshal_text_p
_OP_cond_set
_OP_cond_testc
)
const (
_INT_SIZE = 32 << (^uint(0) >> 63)
_PTR_SIZE = 32 << (^uintptr(0) >> 63)
_PTR_BYTE = unsafe.Sizeof(uintptr(0))
)
const (
_MAX_ILBUF = 100000 // cutoff at 100k of IL instructions
_MAX_FIELDS = 50 // cutoff at 50 fields struct
)
var _OpNames = [256]string {
_OP_null : "null",
_OP_empty_arr : "empty_arr",
_OP_empty_obj : "empty_obj",
_OP_bool : "bool",
_OP_i8 : "i8",
_OP_i16 : "i16",
_OP_i32 : "i32",
_OP_i64 : "i64",
_OP_u8 : "u8",
_OP_u16 : "u16",
_OP_u32 : "u32",
_OP_u64 : "u64",
_OP_f32 : "f32",
_OP_f64 : "f64",
_OP_str : "str",
_OP_bin : "bin",
_OP_quote : "quote",
_OP_number : "number",
_OP_eface : "eface",
_OP_iface : "iface",
_OP_byte : "byte",
_OP_text : "text",
_OP_deref : "deref",
_OP_index : "index",
_OP_load : "load",
_OP_save : "save",
_OP_drop : "drop",
_OP_drop_2 : "drop_2",
_OP_recurse : "recurse",
_OP_is_nil : "is_nil",
_OP_is_nil_p1 : "is_nil_p1",
_OP_is_zero_1 : "is_zero_1",
_OP_is_zero_2 : "is_zero_2",
_OP_is_zero_4 : "is_zero_4",
_OP_is_zero_8 : "is_zero_8",
_OP_is_zero_map : "is_zero_map",
_OP_goto : "goto",
_OP_map_iter : "map_iter",
_OP_map_stop : "map_stop",
_OP_map_check_key : "map_check_key",
_OP_map_write_key : "map_write_key",
_OP_map_value_next : "map_value_next",
_OP_slice_len : "slice_len",
_OP_slice_next : "slice_next",
_OP_marshal : "marshal",
_OP_marshal_p : "marshal_p",
_OP_marshal_text : "marshal_text",
_OP_marshal_text_p : "marshal_text_p",
_OP_cond_set : "cond_set",
_OP_cond_testc : "cond_testc",
}
func (self _Op) String() string {
if ret := _OpNames[self]; ret != "" {
return ret
} else {
return "<invalid>"
}
}
func _OP_int() _Op {
switch _INT_SIZE {
case 32: return _OP_i32
case 64: return _OP_i64
default: panic("unsupported int size")
}
}
func _OP_uint() _Op {
switch _INT_SIZE {
case 32: return _OP_u32
case 64: return _OP_u64
default: panic("unsupported uint size")
}
}
func _OP_uintptr() _Op {
switch _PTR_SIZE {
case 32: return _OP_u32
case 64: return _OP_u64
default: panic("unsupported pointer size")
}
}
func _OP_is_zero_ints() _Op {
switch _INT_SIZE {
case 32: return _OP_is_zero_4
case 64: return _OP_is_zero_8
default: panic("unsupported integer size")
}
}
type _Instr struct {
u uint64 // union {op: 8, _: 8, vi: 48}, vi maybe int or len(str)
p unsafe.Pointer // maybe GoString.Ptr, or *GoType
}
func packOp(op _Op) uint64 {
return uint64(op) << 56
}
func newInsOp(op _Op) _Instr {
return _Instr{u: packOp(op)}
}
func newInsVi(op _Op, vi int) _Instr {
return _Instr{u: packOp(op) | rt.PackInt(vi)}
}
func newInsVs(op _Op, vs string) _Instr {
return _Instr {
u: packOp(op) | rt.PackInt(len(vs)),
p: (*rt.GoString)(unsafe.Pointer(&vs)).Ptr,
}
}
func newInsVt(op _Op, vt reflect.Type) _Instr {
return _Instr {
u: packOp(op),
p: unsafe.Pointer(rt.UnpackType(vt)),
}
}
func newInsVp(op _Op, vt reflect.Type, pv bool) _Instr {
i := 0
if pv {
i = 1
}
return _Instr {
u: packOp(op) | rt.PackInt(i),
p: unsafe.Pointer(rt.UnpackType(vt)),
}
}
func (self _Instr) op() _Op {
return _Op(self.u >> 56)
}
func (self _Instr) vi() int {
return rt.UnpackInt(self.u)
}
func (self _Instr) vf() uint8 {
return (*rt.GoType)(self.p).KindFlags
}
func (self _Instr) vs() (v string) {
(*rt.GoString)(unsafe.Pointer(&v)).Ptr = self.p
(*rt.GoString)(unsafe.Pointer(&v)).Len = self.vi()
return
}
func (self _Instr) vk() reflect.Kind {
return (*rt.GoType)(self.p).Kind()
}
func (self _Instr) vt() reflect.Type {
return (*rt.GoType)(self.p).Pack()
}
func (self _Instr) vp() (vt reflect.Type, pv bool) {
return (*rt.GoType)(self.p).Pack(), rt.UnpackInt(self.u) == 1
}
func (self _Instr) i64() int64 {
return int64(self.vi())
}
func (self _Instr) vlen() int {
return int((*rt.GoType)(self.p).Size)
}
func (self _Instr) isBranch() bool {
switch self.op() {
case _OP_goto : fallthrough
case _OP_is_nil : fallthrough
case _OP_is_nil_p1 : fallthrough
case _OP_is_zero_1 : fallthrough
case _OP_is_zero_2 : fallthrough
case _OP_is_zero_4 : fallthrough
case _OP_is_zero_8 : fallthrough
case _OP_map_check_key : fallthrough
case _OP_map_write_key : fallthrough
case _OP_slice_next : fallthrough
case _OP_cond_testc : return true
default : return false
}
}
func (self _Instr) disassemble() string {
switch self.op() {
case _OP_byte : return fmt.Sprintf("%-18s%s", self.op().String(), strconv.QuoteRune(rune(self.vi())))
case _OP_text : return fmt.Sprintf("%-18s%s", self.op().String(), strconv.Quote(self.vs()))
case _OP_index : return fmt.Sprintf("%-18s%d", self.op().String(), self.vi())
case _OP_recurse : fallthrough
case _OP_map_iter : fallthrough
case _OP_marshal : fallthrough
case _OP_marshal_p : fallthrough
case _OP_marshal_text : fallthrough
case _OP_marshal_text_p : return fmt.Sprintf("%-18s%s", self.op().String(), self.vt())
case _OP_goto : fallthrough
case _OP_is_nil : fallthrough
case _OP_is_nil_p1 : fallthrough
case _OP_is_zero_1 : fallthrough
case _OP_is_zero_2 : fallthrough
case _OP_is_zero_4 : fallthrough
case _OP_is_zero_8 : fallthrough
case _OP_is_zero_map : fallthrough
case _OP_cond_testc : fallthrough
case _OP_map_check_key : fallthrough
case _OP_map_write_key : return fmt.Sprintf("%-18sL_%d", self.op().String(), self.vi())
case _OP_slice_next : return fmt.Sprintf("%-18sL_%d, %s", self.op().String(), self.vi(), self.vt())
default : return self.op().String()
}
}
type (
_Program []_Instr
)
func (self _Program) pc() int {
return len(self)
}
func (self _Program) tag(n int) {
if n >= _MaxStack {
panic("type nesting too deep")
}
}
func (self _Program) pin(i int) {
v := &self[i]
v.u &= 0xffff000000000000
v.u |= rt.PackInt(self.pc())
}
func (self _Program) rel(v []int) {
for _, i := range v {
self.pin(i)
}
}
func (self *_Program) add(op _Op) {
*self = append(*self, newInsOp(op))
}
func (self *_Program) key(op _Op) {
*self = append(*self,
newInsVi(_OP_byte, '"'),
newInsOp(op),
newInsVi(_OP_byte, '"'),
)
}
func (self *_Program) int(op _Op, vi int) {
*self = append(*self, newInsVi(op, vi))
}
func (self *_Program) str(op _Op, vs string) {
*self = append(*self, newInsVs(op, vs))
}
func (self *_Program) rtt(op _Op, vt reflect.Type) {
*self = append(*self, newInsVt(op, vt))
}
func (self *_Program) vp(op _Op, vt reflect.Type, pv bool) {
*self = append(*self, newInsVp(op, vt, pv))
}
func (self _Program) disassemble() string {
nb := len(self)
tab := make([]bool, nb + 1)
ret := make([]string, 0, nb + 1)
/* prescan to get all the labels */
for _, ins := range self {
if ins.isBranch() {
tab[ins.vi()] = true
}
}
/* disassemble each instruction */
for i, ins := range self {
if !tab[i] {
ret = append(ret, "\t" + ins.disassemble())
} else {
ret = append(ret, fmt.Sprintf("L_%d:\n\t%s", i, ins.disassemble()))
}
}
/* add the last label, if needed */
if tab[nb] {
ret = append(ret, fmt.Sprintf("L_%d:", nb))
}
/* add an "end" indicator, and join all the strings */
return strings.Join(append(ret, "\tend"), "\n")
}
type _Compiler struct {
opts option.CompileOptions
pv bool
tab map[reflect.Type]bool
rec map[reflect.Type]uint8
}
func newCompiler() *_Compiler {
return &_Compiler {
opts: option.DefaultCompileOptions(),
tab: map[reflect.Type]bool{},
rec: map[reflect.Type]uint8{},
}
}
func (self *_Compiler) apply(opts option.CompileOptions) *_Compiler {
self.opts = opts
if self.opts.RecursiveDepth > 0 {
self.rec = map[reflect.Type]uint8{}
}
return self
}
func (self *_Compiler) rescue(ep *error) {
if val := recover(); val != nil {
if err, ok := val.(error); ok {
*ep = err
} else {
panic(val)
}
}
}
func (self *_Compiler) compile(vt reflect.Type, pv bool) (ret _Program, err error) {
defer self.rescue(&err)
self.compileOne(&ret, 0, vt, pv)
return
}
func (self *_Compiler) compileOne(p *_Program, sp int, vt reflect.Type, pv bool) {
if self.tab[vt] {
p.vp(_OP_recurse, vt, pv)
} else {
self.compileRec(p, sp, vt, pv)
}
}
func (self *_Compiler) compileRec(p *_Program, sp int, vt reflect.Type, pv bool) {
pr := self.pv
pt := reflect.PtrTo(vt)
/* check for addressable `json.Marshaler` with pointer receiver */
if pv && pt.Implements(jsonMarshalerType) {
p.rtt(_OP_marshal_p, pt)
return
}
/* check for `json.Marshaler` */
if vt.Implements(jsonMarshalerType) {
self.compileMarshaler(p, _OP_marshal, vt, jsonMarshalerType)
return
}
/* check for addressable `encoding.TextMarshaler` with pointer receiver */
if pv && pt.Implements(encodingTextMarshalerType) {
p.rtt(_OP_marshal_text_p, pt)
return
}
/* check for `encoding.TextMarshaler` */
if vt.Implements(encodingTextMarshalerType) {
self.compileMarshaler(p, _OP_marshal_text, vt, encodingTextMarshalerType)
return
}
/* enter the recursion, and compile the type */
self.pv = pv
self.tab[vt] = true
self.compileOps(p, sp, vt)
/* exit the recursion */
self.pv = pr
delete(self.tab, vt)
}
func (self *_Compiler) compileOps(p *_Program, sp int, vt reflect.Type) {
switch vt.Kind() {
case reflect.Bool : p.add(_OP_bool)
case reflect.Int : p.add(_OP_int())
case reflect.Int8 : p.add(_OP_i8)
case reflect.Int16 : p.add(_OP_i16)
case reflect.Int32 : p.add(_OP_i32)
case reflect.Int64 : p.add(_OP_i64)
case reflect.Uint : p.add(_OP_uint())
case reflect.Uint8 : p.add(_OP_u8)
case reflect.Uint16 : p.add(_OP_u16)
case reflect.Uint32 : p.add(_OP_u32)
case reflect.Uint64 : p.add(_OP_u64)
case reflect.Uintptr : p.add(_OP_uintptr())
case reflect.Float32 : p.add(_OP_f32)
case reflect.Float64 : p.add(_OP_f64)
case reflect.String : self.compileString (p, vt)
case reflect.Array : self.compileArray (p, sp, vt.Elem(), vt.Len())
case reflect.Interface : self.compileInterface (p, vt)
case reflect.Map : self.compileMap (p, sp, vt)
case reflect.Ptr : self.compilePtr (p, sp, vt.Elem())
case reflect.Slice : self.compileSlice (p, sp, vt.Elem())
case reflect.Struct : self.compileStruct (p, sp, vt)
default : panic (error_type(vt))
}
}
func (self *_Compiler) compileNil(p *_Program, sp int, vt reflect.Type, nil_op _Op, fn func(*_Program, int, reflect.Type)) {
x := p.pc()
p.add(_OP_is_nil)
fn(p, sp, vt)
e := p.pc()
p.add(_OP_goto)
p.pin(x)
p.add(nil_op)
p.pin(e)
}
func (self *_Compiler) compilePtr(p *_Program, sp int, vt reflect.Type) {
self.compileNil(p, sp, vt, _OP_null, self.compilePtrBody)
}
func (self *_Compiler) compilePtrBody(p *_Program, sp int, vt reflect.Type) {
p.tag(sp)
p.add(_OP_save)
p.add(_OP_deref)
self.compileOne(p, sp + 1, vt, true)
p.add(_OP_drop)
}
func (self *_Compiler) compileMap(p *_Program, sp int, vt reflect.Type) {
self.compileNil(p, sp, vt, _OP_empty_obj, self.compileMapBody)
}
func (self *_Compiler) compileMapBody(p *_Program, sp int, vt reflect.Type) {
p.tag(sp + 1)
p.int(_OP_byte, '{')
p.add(_OP_save)
p.rtt(_OP_map_iter, vt)
p.add(_OP_save)
i := p.pc()
p.add(_OP_map_check_key)
u := p.pc()
p.add(_OP_map_write_key)
self.compileMapBodyKey(p, vt.Key())
p.pin(u)
p.int(_OP_byte, ':')
p.add(_OP_map_value_next)
self.compileOne(p, sp + 2, vt.Elem(), false)
j := p.pc()
p.add(_OP_map_check_key)
p.int(_OP_byte, ',')
v := p.pc()
p.add(_OP_map_write_key)
self.compileMapBodyKey(p, vt.Key())
p.pin(v)
p.int(_OP_byte, ':')
p.add(_OP_map_value_next)
self.compileOne(p, sp + 2, vt.Elem(), false)
p.int(_OP_goto, j)
p.pin(i)
p.pin(j)
p.add(_OP_map_stop)
p.add(_OP_drop_2)
p.int(_OP_byte, '}')
}
func (self *_Compiler) compileMapBodyKey(p *_Program, vk reflect.Type) {
if !vk.Implements(encodingTextMarshalerType) {
self.compileMapBodyTextKey(p, vk)
} else {
self.compileMapBodyUtextKey(p, vk)
}
}
func (self *_Compiler) compileMapBodyTextKey(p *_Program, vk reflect.Type) {
switch vk.Kind() {
case reflect.Invalid : panic("map key is nil")
case reflect.Bool : p.key(_OP_bool)
case reflect.Int : p.key(_OP_int())
case reflect.Int8 : p.key(_OP_i8)
case reflect.Int16 : p.key(_OP_i16)
case reflect.Int32 : p.key(_OP_i32)
case reflect.Int64 : p.key(_OP_i64)
case reflect.Uint : p.key(_OP_uint())
case reflect.Uint8 : p.key(_OP_u8)
case reflect.Uint16 : p.key(_OP_u16)
case reflect.Uint32 : p.key(_OP_u32)
case reflect.Uint64 : p.key(_OP_u64)
case reflect.Uintptr : p.key(_OP_uintptr())
case reflect.Float32 : p.key(_OP_f32)
case reflect.Float64 : p.key(_OP_f64)
case reflect.String : self.compileString(p, vk)
default : panic(error_type(vk))
}
}
func (self *_Compiler) compileMapBodyUtextKey(p *_Program, vk reflect.Type) {
if vk.Kind() != reflect.Ptr {
p.rtt(_OP_marshal_text, vk)
} else {
self.compileMapBodyUtextPtr(p, vk)
}
}
func (self *_Compiler) compileMapBodyUtextPtr(p *_Program, vk reflect.Type) {
i := p.pc()
p.add(_OP_is_nil)
p.rtt(_OP_marshal_text, vk)
j := p.pc()
p.add(_OP_goto)
p.pin(i)
p.str(_OP_text, "\"\"")
p.pin(j)
}
func (self *_Compiler) compileSlice(p *_Program, sp int, vt reflect.Type) {
self.compileNil(p, sp, vt, _OP_empty_arr, self.compileSliceBody)
}
func (self *_Compiler) compileSliceBody(p *_Program, sp int, vt reflect.Type) {
if isSimpleByte(vt) {
p.add(_OP_bin)
} else {
self.compileSliceArray(p, sp, vt)
}
}
func (self *_Compiler) compileSliceArray(p *_Program, sp int, vt reflect.Type) {
p.tag(sp)
p.int(_OP_byte, '[')
p.add(_OP_save)
p.add(_OP_slice_len)
i := p.pc()
p.rtt(_OP_slice_next, vt)
self.compileOne(p, sp + 1, vt, true)
j := p.pc()
p.rtt(_OP_slice_next, vt)
p.int(_OP_byte, ',')
self.compileOne(p, sp + 1, vt, true)
p.int(_OP_goto, j)
p.pin(i)
p.pin(j)
p.add(_OP_drop)
p.int(_OP_byte, ']')
}
func (self *_Compiler) compileArray(p *_Program, sp int, vt reflect.Type, nb int) {
p.tag(sp)
p.int(_OP_byte, '[')
p.add(_OP_save)
/* first item */
if nb != 0 {
self.compileOne(p, sp + 1, vt, self.pv)
p.add(_OP_load)
}
/* remaining items */
for i := 1; i < nb; i++ {
p.int(_OP_byte, ',')
p.int(_OP_index, i * int(vt.Size()))
self.compileOne(p, sp + 1, vt, self.pv)
p.add(_OP_load)
}
/* end of array */
p.add(_OP_drop)
p.int(_OP_byte, ']')
}
func (self *_Compiler) compileString(p *_Program, vt reflect.Type) {
if vt != jsonNumberType {
p.add(_OP_str)
} else {
p.add(_OP_number)
}
}
func (self *_Compiler) compileStruct(p *_Program, sp int, vt reflect.Type) {
if sp >= self.opts.MaxInlineDepth || p.pc() >= _MAX_ILBUF || (sp > 0 && vt.NumField() >= _MAX_FIELDS) {
p.vp(_OP_recurse, vt, self.pv)
if self.opts.RecursiveDepth > 0 {
if self.pv {
self.rec[vt] = 1
} else {
self.rec[vt] = 0
}
}
} else {
self.compileStructBody(p, sp, vt)
}
}
func (self *_Compiler) compileStructBody(p *_Program, sp int, vt reflect.Type) {
p.tag(sp)
p.int(_OP_byte, '{')
p.add(_OP_save)
p.add(_OP_cond_set)
/* compile each field */
for _, fv := range resolver.ResolveStruct(vt) {
var s []int
var o resolver.Offset
/* "omitempty" for arrays */
if fv.Type.Kind() == reflect.Array {
if fv.Type.Len() == 0 && (fv.Opts & resolver.F_omitempty) != 0 {
continue
}
}
/* index to the field */
for _, o = range fv.Path {
if p.int(_OP_index, int(o.Size)); o.Kind == resolver.F_deref {
s = append(s, p.pc())
p.add(_OP_is_nil)
p.add(_OP_deref)
}
}
/* check for "omitempty" option */
if fv.Type.Kind() != reflect.Struct && fv.Type.Kind() != reflect.Array && (fv.Opts & resolver.F_omitempty) != 0 {
s = append(s, p.pc())
self.compileStructFieldZero(p, fv.Type)
}
/* add the comma if not the first element */
i := p.pc()
p.add(_OP_cond_testc)
p.int(_OP_byte, ',')
p.pin(i)
/* compile the key and value */
ft := fv.Type
p.str(_OP_text, Quote(fv.Name) + ":")
/* check for "stringnize" option */
if (fv.Opts & resolver.F_stringize) == 0 {
self.compileOne(p, sp + 1, ft, self.pv)
} else {
self.compileStructFieldStr(p, sp + 1, ft)
}
/* patch the skipping jumps and reload the struct pointer */
p.rel(s)
p.add(_OP_load)
}
/* end of object */
p.add(_OP_drop)
p.int(_OP_byte, '}')
}
func (self *_Compiler) compileStructFieldStr(p *_Program, sp int, vt reflect.Type) {
pc := -1
ft := vt
sv := false
/* dereference the pointer if needed */
if ft.Kind() == reflect.Ptr {
ft = ft.Elem()
}
/* check if it can be stringized */
switch ft.Kind() {
case reflect.Bool : sv = true
case reflect.Int : sv = true
case reflect.Int8 : sv = true
case reflect.Int16 : sv = true
case reflect.Int32 : sv = true
case reflect.Int64 : sv = true
case reflect.Uint : sv = true
case reflect.Uint8 : sv = true
case reflect.Uint16 : sv = true
case reflect.Uint32 : sv = true
case reflect.Uint64 : sv = true
case reflect.Uintptr : sv = true
case reflect.Float32 : sv = true
case reflect.Float64 : sv = true
case reflect.String : sv = true
}
/* if it's not, ignore the "string" and follow the regular path */
if !sv {
self.compileOne(p, sp, vt, self.pv)
return
}
/* dereference the pointer */
if vt.Kind() == reflect.Ptr {
pc = p.pc()
vt = vt.Elem()
p.add(_OP_is_nil)
p.add(_OP_deref)
}
/* special case of a double-quoted string */
if ft != jsonNumberType && ft.Kind() == reflect.String {
p.add(_OP_quote)
} else {
self.compileStructFieldQuoted(p, sp, vt)
}
/* the "null" case of the pointer */
if pc != -1 {
e := p.pc()
p.add(_OP_goto)
p.pin(pc)
p.add(_OP_null)
p.pin(e)
}
}
func (self *_Compiler) compileStructFieldZero(p *_Program, vt reflect.Type) {
switch vt.Kind() {
case reflect.Bool : p.add(_OP_is_zero_1)
case reflect.Int : p.add(_OP_is_zero_ints())
case reflect.Int8 : p.add(_OP_is_zero_1)
case reflect.Int16 : p.add(_OP_is_zero_2)
case reflect.Int32 : p.add(_OP_is_zero_4)
case reflect.Int64 : p.add(_OP_is_zero_8)
case reflect.Uint : p.add(_OP_is_zero_ints())
case reflect.Uint8 : p.add(_OP_is_zero_1)
case reflect.Uint16 : p.add(_OP_is_zero_2)
case reflect.Uint32 : p.add(_OP_is_zero_4)
case reflect.Uint64 : p.add(_OP_is_zero_8)
case reflect.Uintptr : p.add(_OP_is_nil)
case reflect.Float32 : p.add(_OP_is_zero_4)
case reflect.Float64 : p.add(_OP_is_zero_8)
case reflect.String : p.add(_OP_is_nil_p1)
case reflect.Interface : p.add(_OP_is_nil)
case reflect.Map : p.add(_OP_is_zero_map)
case reflect.Ptr : p.add(_OP_is_nil)
case reflect.Slice : p.add(_OP_is_nil_p1)
default : panic(error_type(vt))
}
}
func (self *_Compiler) compileStructFieldQuoted(p *_Program, sp int, vt reflect.Type) {
p.int(_OP_byte, '"')
self.compileOne(p, sp, vt, self.pv)
p.int(_OP_byte, '"')
}
func (self *_Compiler) compileInterface(p *_Program, vt reflect.Type) {
x := p.pc()
p.add(_OP_is_nil_p1)
/* iface and efaces are different */
if vt.NumMethod() == 0 {
p.add(_OP_eface)
} else {
p.add(_OP_iface)
}
/* the "null" value */
e := p.pc()
p.add(_OP_goto)
p.pin(x)
p.add(_OP_null)
p.pin(e)
}
func (self *_Compiler) compileMarshaler(p *_Program, op _Op, vt reflect.Type, mt reflect.Type) {
pc := p.pc()
vk := vt.Kind()
/* direct receiver */
if vk != reflect.Ptr {
p.rtt(op, vt)
return
}
/* value receiver with a pointer type, check for nil before calling the marshaler */
p.add(_OP_is_nil)
p.rtt(op, vt)
i := p.pc()
p.add(_OP_goto)
p.pin(pc)
p.add(_OP_null)
p.pin(i)
}
@@ -0,0 +1,66 @@
// +build go1.16,!go1.17
/*
* Copyright 2021 ByteDance Inc.
*
* 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.
*/
package encoder
import (
`os`
`strings`
`runtime`
`runtime/debug`
`github.com/bytedance/sonic/internal/jit`
)
var (
debugSyncGC = os.Getenv("SONIC_SYNC_GC") != ""
debugAsyncGC = os.Getenv("SONIC_NO_ASYNC_GC") == ""
)
var (
_Instr_End _Instr = newInsOp(_OP_null)
_F_gc = jit.Func(runtime.GC)
_F_force_gc = jit.Func(debug.FreeOSMemory)
_F_println = jit.Func(println_wrapper)
)
func println_wrapper(i int, op1 int, op2 int){
println(i, " Intrs ", op1, _OpNames[op1], "next: ", op2, _OpNames[op2])
}
func (self *_Assembler) force_gc() {
self.call_go(_F_gc)
self.call_go(_F_force_gc)
}
func (self *_Assembler) debug_instr(i int, v *_Instr) {
if debugSyncGC {
if (i+1 == len(self.p)) {
self.print_gc(i, v, &_Instr_End)
} else {
next := &(self.p[i+1])
self.print_gc(i, v, next)
name := _OpNames[next.op()]
if strings.Contains(name, "save") {
return
}
}
self.force_gc()
}
}
@@ -0,0 +1,205 @@
// +build go1.17,!go1.23
/*
* Copyright 2021 ByteDance Inc.
*
* 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.
*/
package encoder
import (
`fmt`
`os`
`runtime`
`strings`
`unsafe`
`github.com/bytedance/sonic/internal/jit`
`github.com/twitchyliquid64/golang-asm/obj`
)
const _FP_debug = 128
var (
debugSyncGC = os.Getenv("SONIC_SYNC_GC") != ""
debugAsyncGC = os.Getenv("SONIC_NO_ASYNC_GC") == ""
debugCheckPtr = os.Getenv("SONIC_CHECK_POINTER") != ""
)
var (
_Instr_End = newInsOp(_OP_is_nil)
_F_gc = jit.Func(gc)
_F_println = jit.Func(println_wrapper)
_F_print = jit.Func(print)
)
func (self *_Assembler) dsave(r ...obj.Addr) {
for i, v := range r {
if i > _FP_debug / 8 - 1 {
panic("too many registers to save")
} else {
self.Emit("MOVQ", v, jit.Ptr(_SP, _FP_fargs + _FP_saves + _FP_locals + int64(i) * 8))
}
}
}
func (self *_Assembler) dload(r ...obj.Addr) {
for i, v := range r {
if i > _FP_debug / 8 - 1 {
panic("too many registers to load")
} else {
self.Emit("MOVQ", jit.Ptr(_SP, _FP_fargs + _FP_saves + _FP_locals + int64(i) * 8), v)
}
}
}
func println_wrapper(i int, op1 int, op2 int){
println(i, " Intrs ", op1, _OpNames[op1], "next: ", op2, _OpNames[op2])
}
func print(i int){
println(i)
}
func gc() {
if !debugSyncGC {
return
}
runtime.GC()
// debug.FreeOSMemory()
}
func (self *_Assembler) dcall(fn obj.Addr) {
self.Emit("MOVQ", fn, _R10) // MOVQ ${fn}, R10
self.Rjmp("CALL", _R10) // CALL R10
}
func (self *_Assembler) debug_gc() {
if !debugSyncGC {
return
}
self.dsave(_REG_debug...)
self.dcall(_F_gc)
self.dload(_REG_debug...)
}
func (self *_Assembler) debug_instr(i int, v *_Instr) {
if debugSyncGC {
if i+1 == len(self.p) {
self.print_gc(i, v, &_Instr_End)
} else {
next := &(self.p[i+1])
self.print_gc(i, v, next)
name := _OpNames[next.op()]
if strings.Contains(name, "save") {
return
}
}
// self.debug_gc()
}
}
//go:noescape
//go:linkname checkptrBase runtime.checkptrBase
func checkptrBase(p unsafe.Pointer) uintptr
//go:noescape
//go:linkname findObject runtime.findObject
func findObject(p, refBase, refOff uintptr) (base uintptr, s unsafe.Pointer, objIndex uintptr)
var (
_F_checkptr = jit.Func(checkptr)
_F_printptr = jit.Func(printptr)
)
var (
_R10 = jit.Reg("R10")
)
var _REG_debug = []obj.Addr {
jit.Reg("AX"),
jit.Reg("BX"),
jit.Reg("CX"),
jit.Reg("DX"),
jit.Reg("DI"),
jit.Reg("SI"),
jit.Reg("BP"),
jit.Reg("SP"),
jit.Reg("R8"),
jit.Reg("R9"),
jit.Reg("R10"),
jit.Reg("R11"),
jit.Reg("R12"),
jit.Reg("R13"),
jit.Reg("R14"),
jit.Reg("R15"),
}
func checkptr(ptr uintptr) {
if ptr == 0 {
return
}
fmt.Printf("pointer: %x\n", ptr)
f := checkptrBase(unsafe.Pointer(uintptr(ptr)))
if f == 0 {
fmt.Printf("! unknown-based pointer: %x\n", ptr)
} else if f == 1 {
fmt.Printf("! stack pointer: %x\n", ptr)
} else {
fmt.Printf("base: %x\n", f)
}
findobj(ptr)
}
func findobj(ptr uintptr) {
base, s, objIndex := findObject(ptr, 0, 0)
if s != nil && base == 0 {
fmt.Printf("! invalid pointer: %x\n", ptr)
}
fmt.Printf("objIndex: %d\n", objIndex)
}
func (self *_Assembler) check_ptr(ptr obj.Addr, lea bool) {
if !debugCheckPtr {
return
}
self.dsave(_REG_debug...)
if lea {
self.Emit("LEAQ", ptr, _R10)
} else {
self.Emit("MOVQ", ptr, _R10)
}
self.Emit("MOVQ", _R10, jit.Ptr(_SP, 0))
self.dcall(_F_checkptr)
self.dload(_REG_debug...)
}
func printptr(i int, ptr uintptr) {
fmt.Printf("[%d] ptr: %x\n", i, ptr)
}
func (self *_Assembler) print_ptr(i int, ptr obj.Addr, lea bool) {
self.dsave(_REG_debug...)
if lea {
self.Emit("LEAQ", ptr, _R10)
} else {
self.Emit("MOVQ", ptr, _R10)
}
self.Emit("MOVQ", jit.Imm(int64(i)), _AX)
self.Emit("MOVQ", _R10, _BX)
self.dcall(_F_printptr)
self.dload(_REG_debug...)
}
@@ -0,0 +1,355 @@
/*
* Copyright 2021 ByteDance Inc.
*
* 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.
*/
package encoder
import (
`bytes`
`encoding/json`
`reflect`
`runtime`
`unsafe`
`github.com/bytedance/sonic/internal/native`
`github.com/bytedance/sonic/internal/native/types`
`github.com/bytedance/sonic/internal/rt`
`github.com/bytedance/sonic/utf8`
`github.com/bytedance/sonic/option`
)
// Options is a set of encoding options.
type Options uint64
const (
bitSortMapKeys = iota
bitEscapeHTML
bitCompactMarshaler
bitNoQuoteTextMarshaler
bitNoNullSliceOrMap
bitValidateString
bitNoValidateJSONMarshaler
bitNoEncoderNewline
// used for recursive compile
bitPointerValue = 63
)
const (
// SortMapKeys indicates that the keys of a map needs to be sorted
// before serializing into JSON.
// WARNING: This hurts performance A LOT, USE WITH CARE.
SortMapKeys Options = 1 << bitSortMapKeys
// EscapeHTML indicates encoder to escape all HTML characters
// after serializing into JSON (see https://pkg.go.dev/encoding/json#HTMLEscape).
// WARNING: This hurts performance A LOT, USE WITH CARE.
EscapeHTML Options = 1 << bitEscapeHTML
// CompactMarshaler indicates that the output JSON from json.Marshaler
// is always compact and needs no validation
CompactMarshaler Options = 1 << bitCompactMarshaler
// NoQuoteTextMarshaler indicates that the output text from encoding.TextMarshaler
// is always escaped string and needs no quoting
NoQuoteTextMarshaler Options = 1 << bitNoQuoteTextMarshaler
// NoNullSliceOrMap indicates all empty Array or Object are encoded as '[]' or '{}',
// instead of 'null'
NoNullSliceOrMap Options = 1 << bitNoNullSliceOrMap
// ValidateString indicates that encoder should validate the input string
// before encoding it into JSON.
ValidateString Options = 1 << bitValidateString
// NoValidateJSONMarshaler indicates that the encoder should not validate the output string
// after encoding the JSONMarshaler to JSON.
NoValidateJSONMarshaler Options = 1 << bitNoValidateJSONMarshaler
// NoEncoderNewline indicates that the encoder should not add a newline after every message
NoEncoderNewline Options = 1 << bitNoEncoderNewline
// CompatibleWithStd is used to be compatible with std encoder.
CompatibleWithStd Options = SortMapKeys | EscapeHTML | CompactMarshaler
)
// Encoder represents a specific set of encoder configurations.
type Encoder struct {
Opts Options
prefix string
indent string
}
// Encode returns the JSON encoding of v.
func (self *Encoder) Encode(v interface{}) ([]byte, error) {
if self.indent != "" || self.prefix != "" {
return EncodeIndented(v, self.prefix, self.indent, self.Opts)
}
return Encode(v, self.Opts)
}
// SortKeys enables the SortMapKeys option.
func (self *Encoder) SortKeys() *Encoder {
self.Opts |= SortMapKeys
return self
}
// SetEscapeHTML specifies if option EscapeHTML opens
func (self *Encoder) SetEscapeHTML(f bool) {
if f {
self.Opts |= EscapeHTML
} else {
self.Opts &= ^EscapeHTML
}
}
// SetValidateString specifies if option ValidateString opens
func (self *Encoder) SetValidateString(f bool) {
if f {
self.Opts |= ValidateString
} else {
self.Opts &= ^ValidateString
}
}
// SetNoValidateJSONMarshaler specifies if option NoValidateJSONMarshaler opens
func (self *Encoder) SetNoValidateJSONMarshaler(f bool) {
if f {
self.Opts |= NoValidateJSONMarshaler
} else {
self.Opts &= ^NoValidateJSONMarshaler
}
}
// SetNoEncoderNewline specifies if option NoEncoderNewline opens
func (self *Encoder) SetNoEncoderNewline(f bool) {
if f {
self.Opts |= NoEncoderNewline
} else {
self.Opts &= ^NoEncoderNewline
}
}
// SetCompactMarshaler specifies if option CompactMarshaler opens
func (self *Encoder) SetCompactMarshaler(f bool) {
if f {
self.Opts |= CompactMarshaler
} else {
self.Opts &= ^CompactMarshaler
}
}
// SetNoQuoteTextMarshaler specifies if option NoQuoteTextMarshaler opens
func (self *Encoder) SetNoQuoteTextMarshaler(f bool) {
if f {
self.Opts |= NoQuoteTextMarshaler
} else {
self.Opts &= ^NoQuoteTextMarshaler
}
}
// SetIndent instructs the encoder to format each subsequent encoded
// value as if indented by the package-level function EncodeIndent().
// Calling SetIndent("", "") disables indentation.
func (enc *Encoder) SetIndent(prefix, indent string) {
enc.prefix = prefix
enc.indent = indent
}
// Quote returns the JSON-quoted version of s.
func Quote(s string) string {
var n int
var p []byte
/* check for empty string */
if s == "" {
return `""`
}
/* allocate space for result */
n = len(s) + 2
p = make([]byte, 0, n)
/* call the encoder */
_ = encodeString(&p, s)
return rt.Mem2Str(p)
}
// Encode returns the JSON encoding of val, encoded with opts.
func Encode(val interface{}, opts Options) ([]byte, error) {
var ret []byte
buf := newBytes()
err := encodeInto(&buf, val, opts)
/* check for errors */
if err != nil {
freeBytes(buf)
return nil, err
}
/* htmlescape or correct UTF-8 if opts enable */
old := buf
buf = encodeFinish(old, opts)
pbuf := ((*rt.GoSlice)(unsafe.Pointer(&buf))).Ptr
pold := ((*rt.GoSlice)(unsafe.Pointer(&old))).Ptr
/* return when allocated a new buffer */
if pbuf != pold {
freeBytes(old)
return buf, nil
}
/* make a copy of the result */
ret = make([]byte, len(buf))
copy(ret, buf)
freeBytes(buf)
/* return the buffer into pool */
return ret, nil
}
// EncodeInto is like Encode but uses a user-supplied buffer instead of allocating
// a new one.
func EncodeInto(buf *[]byte, val interface{}, opts Options) error {
err := encodeInto(buf, val, opts)
if err != nil {
return err
}
*buf = encodeFinish(*buf, opts)
return err
}
func encodeInto(buf *[]byte, val interface{}, opts Options) error {
stk := newStack()
efv := rt.UnpackEface(val)
err := encodeTypedPointer(buf, efv.Type, &efv.Value, stk, uint64(opts))
/* return the stack into pool */
if err != nil {
resetStack(stk)
}
freeStack(stk)
/* avoid GC ahead */
runtime.KeepAlive(buf)
runtime.KeepAlive(efv)
return err
}
func encodeFinish(buf []byte, opts Options) []byte {
if opts & EscapeHTML != 0 {
buf = HTMLEscape(nil, buf)
}
if opts & ValidateString != 0 && !utf8.Validate(buf) {
buf = utf8.CorrectWith(nil, buf, `\ufffd`)
}
return buf
}
var typeByte = rt.UnpackType(reflect.TypeOf(byte(0)))
// HTMLEscape appends to dst the JSON-encoded src with <, >, &, U+2028 and U+2029
// characters inside string literals changed to \u003c, \u003e, \u0026, \u2028, \u2029
// so that the JSON will be safe to embed inside HTML <script> tags.
// For historical reasons, web browsers don't honor standard HTML
// escaping within <script> tags, so an alternative JSON encoding must
// be used.
func HTMLEscape(dst []byte, src []byte) []byte {
return htmlEscape(dst, src)
}
// EncodeIndented is like Encode but applies Indent to format the output.
// Each JSON element in the output will begin on a new line beginning with prefix
// followed by one or more copies of indent according to the indentation nesting.
func EncodeIndented(val interface{}, prefix string, indent string, opts Options) ([]byte, error) {
var err error
var out []byte
var buf *bytes.Buffer
/* encode into the buffer */
out = newBytes()
err = EncodeInto(&out, val, opts)
/* check for errors */
if err != nil {
freeBytes(out)
return nil, err
}
/* indent the JSON */
buf = newBuffer()
err = json.Indent(buf, out, prefix, indent)
/* check for errors */
if err != nil {
freeBytes(out)
freeBuffer(buf)
return nil, err
}
/* copy to the result buffer */
ret := make([]byte, buf.Len())
copy(ret, buf.Bytes())
/* return the buffers into pool */
freeBytes(out)
freeBuffer(buf)
return ret, nil
}
// Pretouch compiles vt ahead-of-time to avoid JIT compilation on-the-fly, in
// order to reduce the first-hit latency.
//
// Opts are the compile options, for example, "option.WithCompileRecursiveDepth" is
// a compile option to set the depth of recursive compile for the nested struct type.
func Pretouch(vt reflect.Type, opts ...option.CompileOption) error {
cfg := option.DefaultCompileOptions()
for _, opt := range opts {
opt(&cfg)
}
return pretouchRec(map[reflect.Type]uint8{vt: 0}, cfg)
}
// Valid validates json and returns first non-blank character position,
// if it is only one valid json value.
// Otherwise returns invalid character position using start.
//
// Note: it does not check for the invalid UTF-8 characters.
func Valid(data []byte) (ok bool, start int) {
n := len(data)
if n == 0 {
return false, -1
}
s := rt.Mem2Str(data)
p := 0
m := types.NewStateMachine()
ret := native.ValidateOne(&s, &p, m, types.F_VALIDATE_STRING)
types.FreeStateMachine(m)
if ret < 0 {
return false, p-1
}
/* check for trailing spaces */
for ;p < n; p++ {
if (types.SPACE_MASK & (1 << data[p])) == 0 {
return false, p
}
}
return true, ret
}
@@ -0,0 +1,65 @@
/*
* Copyright 2021 ByteDance Inc.
*
* 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.
*/
package encoder
import (
`encoding/json`
`fmt`
`reflect`
`strconv`
`unsafe`
`github.com/bytedance/sonic/internal/rt`
)
var _ERR_too_deep = &json.UnsupportedValueError {
Str : "Value nesting too deep",
Value : reflect.ValueOf("..."),
}
var _ERR_nan_or_infinite = &json.UnsupportedValueError {
Str : "NaN or ±Infinite",
Value : reflect.ValueOf("NaN or ±Infinite"),
}
func error_type(vtype reflect.Type) error {
return &json.UnsupportedTypeError{Type: vtype}
}
func error_number(number json.Number) error {
return &json.UnsupportedValueError {
Str : "invalid number literal: " + strconv.Quote(string(number)),
Value : reflect.ValueOf(number),
}
}
func error_marshaler(ret []byte, pos int) error {
return fmt.Errorf("invalid Marshaler output json syntax at %d: %q", pos, ret)
}
const (
panicNilPointerOfNonEmptyString int = 1 + iota
)
func goPanic(code int, val unsafe.Pointer) {
switch(code){
case panicNilPointerOfNonEmptyString:
panic(fmt.Sprintf("val: %#v has nil pointer while its length is not zero!", (*rt.GoString)(val)))
default:
panic("encoder error!")
}
}
@@ -0,0 +1,199 @@
/*
* Copyright 2021 ByteDance Inc.
*
* 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.
*/
package encoder
import (
"encoding"
"reflect"
"sync"
"unsafe"
"github.com/bytedance/sonic/internal/native"
"github.com/bytedance/sonic/internal/rt"
)
type _MapPair struct {
k string // when the map key is integer, k is pointed to m
v unsafe.Pointer
m [32]byte
}
type _MapIterator struct {
it rt.GoMapIterator // must be the first field
kv rt.GoSlice // slice of _MapPair
ki int
}
var (
iteratorPool = sync.Pool{}
iteratorPair = rt.UnpackType(reflect.TypeOf(_MapPair{}))
)
func init() {
if unsafe.Offsetof(_MapIterator{}.it) != 0 {
panic("_MapIterator.it is not the first field")
}
}
func newIterator() *_MapIterator {
if v := iteratorPool.Get(); v == nil {
return new(_MapIterator)
} else {
return resetIterator(v.(*_MapIterator))
}
}
func resetIterator(p *_MapIterator) *_MapIterator {
p.ki = 0
p.it = rt.GoMapIterator{}
p.kv.Len = 0
return p
}
func (self *_MapIterator) at(i int) *_MapPair {
return (*_MapPair)(unsafe.Pointer(uintptr(self.kv.Ptr) + uintptr(i) * unsafe.Sizeof(_MapPair{})))
}
func (self *_MapIterator) add() (p *_MapPair) {
p = self.at(self.kv.Len)
self.kv.Len++
return
}
func (self *_MapIterator) data() (p []_MapPair) {
*(*rt.GoSlice)(unsafe.Pointer(&p)) = self.kv
return
}
func (self *_MapIterator) append(t *rt.GoType, k unsafe.Pointer, v unsafe.Pointer) (err error) {
p := self.add()
p.v = v
/* check for strings */
if tk := t.Kind(); tk != reflect.String {
return self.appendGeneric(p, t, tk, k)
}
/* fast path for strings */
p.k = *(*string)(k)
return nil
}
func (self *_MapIterator) appendGeneric(p *_MapPair, t *rt.GoType, v reflect.Kind, k unsafe.Pointer) error {
switch v {
case reflect.Int : p.k = rt.Mem2Str(p.m[:native.I64toa(&p.m[0], int64(*(*int)(k)))]) ; return nil
case reflect.Int8 : p.k = rt.Mem2Str(p.m[:native.I64toa(&p.m[0], int64(*(*int8)(k)))]) ; return nil
case reflect.Int16 : p.k = rt.Mem2Str(p.m[:native.I64toa(&p.m[0], int64(*(*int16)(k)))]) ; return nil
case reflect.Int32 : p.k = rt.Mem2Str(p.m[:native.I64toa(&p.m[0], int64(*(*int32)(k)))]) ; return nil
case reflect.Int64 : p.k = rt.Mem2Str(p.m[:native.I64toa(&p.m[0], *(*int64)(k))]) ; return nil
case reflect.Uint : p.k = rt.Mem2Str(p.m[:native.U64toa(&p.m[0], uint64(*(*uint)(k)))]) ; return nil
case reflect.Uint8 : p.k = rt.Mem2Str(p.m[:native.U64toa(&p.m[0], uint64(*(*uint8)(k)))]) ; return nil
case reflect.Uint16 : p.k = rt.Mem2Str(p.m[:native.U64toa(&p.m[0], uint64(*(*uint16)(k)))]) ; return nil
case reflect.Uint32 : p.k = rt.Mem2Str(p.m[:native.U64toa(&p.m[0], uint64(*(*uint32)(k)))]) ; return nil
case reflect.Uint64 : p.k = rt.Mem2Str(p.m[:native.U64toa(&p.m[0], *(*uint64)(k))]) ; return nil
case reflect.Uintptr : p.k = rt.Mem2Str(p.m[:native.U64toa(&p.m[0], uint64(*(*uintptr)(k)))]) ; return nil
case reflect.Interface : return self.appendInterface(p, t, k)
case reflect.Struct, reflect.Ptr : return self.appendConcrete(p, t, k)
default : panic("unexpected map key type")
}
}
func (self *_MapIterator) appendConcrete(p *_MapPair, t *rt.GoType, k unsafe.Pointer) (err error) {
// compiler has already checked that the type implements the encoding.MarshalText interface
if !t.Indirect() {
k = *(*unsafe.Pointer)(k)
}
eface := rt.GoEface{Value: k, Type: t}.Pack()
out, err := eface.(encoding.TextMarshaler).MarshalText()
if err != nil {
return err
}
p.k = rt.Mem2Str(out)
return
}
func (self *_MapIterator) appendInterface(p *_MapPair, t *rt.GoType, k unsafe.Pointer) (err error) {
if len(rt.IfaceType(t).Methods) == 0 {
panic("unexpected map key type")
} else if p.k, err = asText(k); err == nil {
return nil
} else {
return
}
}
func iteratorStop(p *_MapIterator) {
iteratorPool.Put(p)
}
func iteratorNext(p *_MapIterator) {
i := p.ki
t := &p.it
/* check for unordered iteration */
if i < 0 {
mapiternext(t)
return
}
/* check for end of iteration */
if p.ki >= p.kv.Len {
t.K = nil
t.V = nil
return
}
/* update the key-value pair, and increase the pointer */
t.K = unsafe.Pointer(&p.at(p.ki).k)
t.V = p.at(p.ki).v
p.ki++
}
func iteratorStart(t *rt.GoMapType, m *rt.GoMap, fv uint64) (*_MapIterator, error) {
it := newIterator()
mapiterinit(t, m, &it.it)
/* check for key-sorting, empty map don't need sorting */
if m.Count == 0 || (fv & uint64(SortMapKeys)) == 0 {
it.ki = -1
return it, nil
}
/* pre-allocate space if needed */
if m.Count > it.kv.Cap {
it.kv = growslice(iteratorPair, it.kv, m.Count)
}
/* dump all the key-value pairs */
for ; it.it.K != nil; mapiternext(&it.it) {
if err := it.append(t.Key, it.it.K, it.it.V); err != nil {
iteratorStop(it)
return nil, err
}
}
/* sort the keys, map with only 1 item don't need sorting */
if it.ki = 1; m.Count > 1 {
radixQsort(it.data(), 0, maxDepth(it.kv.Len))
}
/* load the first pair into iterator */
it.it.V = it.at(0).v
it.it.K = unsafe.Pointer(&it.at(0).k)
return it, nil
}
@@ -0,0 +1,193 @@
/*
* Copyright 2021 ByteDance Inc.
*
* 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.
*/
package encoder
import (
`bytes`
`sync`
`unsafe`
`errors`
`reflect`
`github.com/bytedance/sonic/internal/caching`
`github.com/bytedance/sonic/option`
`github.com/bytedance/sonic/internal/rt`
)
const (
_MaxStack = 4096 // 4k states
_StackSize = unsafe.Sizeof(_Stack{})
)
var (
bytesPool = sync.Pool{}
stackPool = sync.Pool{}
bufferPool = sync.Pool{}
programCache = caching.CreateProgramCache()
)
type _State struct {
x int
f uint64
p unsafe.Pointer
q unsafe.Pointer
}
type _Stack struct {
sp uint64
sb [_MaxStack]_State
}
type _Encoder func(
rb *[]byte,
vp unsafe.Pointer,
sb *_Stack,
fv uint64,
) error
var _KeepAlive struct {
rb *[]byte
vp unsafe.Pointer
sb *_Stack
fv uint64
err error
frame [_FP_offs]byte
}
var errCallShadow = errors.New("DON'T CALL THIS!")
// Faker func of _Encoder, used to export its stackmap as _Encoder's
func _Encoder_Shadow(rb *[]byte, vp unsafe.Pointer, sb *_Stack, fv uint64) (err error) {
// align to assembler_amd64.go: _FP_offs
var frame [_FP_offs]byte
// must keep all args and frames noticeable to GC
_KeepAlive.rb = rb
_KeepAlive.vp = vp
_KeepAlive.sb = sb
_KeepAlive.fv = fv
_KeepAlive.err = err
_KeepAlive.frame = frame
return errCallShadow
}
func newBytes() []byte {
if ret := bytesPool.Get(); ret != nil {
return ret.([]byte)
} else {
return make([]byte, 0, option.DefaultEncoderBufferSize)
}
}
func newStack() *_Stack {
if ret := stackPool.Get(); ret == nil {
return new(_Stack)
} else {
return ret.(*_Stack)
}
}
func resetStack(p *_Stack) {
memclrNoHeapPointers(unsafe.Pointer(p), _StackSize)
}
func newBuffer() *bytes.Buffer {
if ret := bufferPool.Get(); ret != nil {
return ret.(*bytes.Buffer)
} else {
return bytes.NewBuffer(make([]byte, 0, option.DefaultEncoderBufferSize))
}
}
func freeBytes(p []byte) {
p = p[:0]
bytesPool.Put(p)
}
func freeStack(p *_Stack) {
p.sp = 0
stackPool.Put(p)
}
func freeBuffer(p *bytes.Buffer) {
p.Reset()
bufferPool.Put(p)
}
func makeEncoder(vt *rt.GoType, ex ...interface{}) (interface{}, error) {
if pp, err := newCompiler().compile(vt.Pack(), ex[0].(bool)); err != nil {
return nil, err
} else {
as := newAssembler(pp)
as.name = vt.String()
return as.Load(), nil
}
}
func findOrCompile(vt *rt.GoType, pv bool) (_Encoder, error) {
if val := programCache.Get(vt); val != nil {
return val.(_Encoder), nil
} else if ret, err := programCache.Compute(vt, makeEncoder, pv); err == nil {
return ret.(_Encoder), nil
} else {
return nil, err
}
}
func pretouchType(_vt reflect.Type, opts option.CompileOptions, v uint8) (map[reflect.Type]uint8, error) {
/* compile function */
compiler := newCompiler().apply(opts)
encoder := func(vt *rt.GoType, ex ...interface{}) (interface{}, error) {
if pp, err := compiler.compile(_vt, ex[0].(bool)); err != nil {
return nil, err
} else {
as := newAssembler(pp)
as.name = vt.String()
return as.Load(), nil
}
}
/* find or compile */
vt := rt.UnpackType(_vt)
if val := programCache.Get(vt); val != nil {
return nil, nil
} else if _, err := programCache.Compute(vt, encoder, v == 1); err == nil {
return compiler.rec, nil
} else {
return nil, err
}
}
func pretouchRec(vtm map[reflect.Type]uint8, opts option.CompileOptions) error {
if opts.RecursiveDepth < 0 || len(vtm) == 0 {
return nil
}
next := make(map[reflect.Type]uint8)
for vt, v := range vtm {
sub, err := pretouchType(vt, opts, v)
if err != nil {
return err
}
for svt, v := range sub {
next[svt] = v
}
}
opts.RecursiveDepth -= 1
return pretouchRec(next, opts)
}
@@ -0,0 +1,167 @@
/*
* Copyright 2021 ByteDance Inc.
*
* 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.
*/
package encoder
import (
`encoding`
`encoding/json`
`unsafe`
`github.com/bytedance/sonic/internal/jit`
`github.com/bytedance/sonic/internal/native`
`github.com/bytedance/sonic/internal/native/types`
`github.com/bytedance/sonic/internal/rt`
)
/** Encoder Primitives **/
func encodeNil(rb *[]byte) error {
*rb = append(*rb, 'n', 'u', 'l', 'l')
return nil
}
func encodeString(buf *[]byte, val string) error {
var sidx int
var pbuf *rt.GoSlice
var pstr *rt.GoString
/* opening quote */
*buf = append(*buf, '"')
pbuf = (*rt.GoSlice)(unsafe.Pointer(buf))
pstr = (*rt.GoString)(unsafe.Pointer(&val))
/* encode with native library */
for sidx < pstr.Len {
sn := pstr.Len - sidx
dn := pbuf.Cap - pbuf.Len
sp := padd(pstr.Ptr, sidx)
dp := padd(pbuf.Ptr, pbuf.Len)
nb := native.Quote(sp, sn, dp, &dn, 0)
/* check for errors */
if pbuf.Len += dn; nb >= 0 {
break
}
/* not enough space, grow the slice and try again */
sidx += ^nb
*pbuf = growslice(rt.UnpackType(byteType), *pbuf, pbuf.Cap * 2)
}
/* closing quote */
*buf = append(*buf, '"')
return nil
}
func encodeTypedPointer(buf *[]byte, vt *rt.GoType, vp *unsafe.Pointer, sb *_Stack, fv uint64) error {
if vt == nil {
return encodeNil(buf)
} else if fn, err := findOrCompile(vt, (fv&(1<<bitPointerValue)) != 0); err != nil {
return err
} else if vt.Indirect() {
rt.MoreStack(_FP_size + native.MaxFrameSize)
err := fn(buf, *vp, sb, fv)
return err
} else {
rt.MoreStack(_FP_size + native.MaxFrameSize)
err := fn(buf, unsafe.Pointer(vp), sb, fv)
return err
}
}
func encodeJsonMarshaler(buf *[]byte, val json.Marshaler, opt Options) error {
if ret, err := val.MarshalJSON(); err != nil {
return err
} else {
if opt & CompactMarshaler != 0 {
return compact(buf, ret)
}
if opt & NoValidateJSONMarshaler == 0 {
if ok, s := Valid(ret); !ok {
return error_marshaler(ret, s)
}
}
*buf = append(*buf, ret...)
return nil
}
}
func encodeTextMarshaler(buf *[]byte, val encoding.TextMarshaler, opt Options) error {
if ret, err := val.MarshalText(); err != nil {
return err
} else {
if opt & NoQuoteTextMarshaler != 0 {
*buf = append(*buf, ret...)
return nil
}
return encodeString(buf, rt.Mem2Str(ret) )
}
}
func htmlEscape(dst []byte, src []byte) []byte {
var sidx int
dst = append(dst, src[:0]...) // avoid check nil dst
sbuf := (*rt.GoSlice)(unsafe.Pointer(&src))
dbuf := (*rt.GoSlice)(unsafe.Pointer(&dst))
/* grow dst if it is shorter */
if cap(dst) - len(dst) < len(src) + types.BufPaddingSize {
cap := len(src) * 3 / 2 + types.BufPaddingSize
*dbuf = growslice(typeByte, *dbuf, cap)
}
for sidx < sbuf.Len {
sp := padd(sbuf.Ptr, sidx)
dp := padd(dbuf.Ptr, dbuf.Len)
sn := sbuf.Len - sidx
dn := dbuf.Cap - dbuf.Len
nb := native.HTMLEscape(sp, sn, dp, &dn)
/* check for errors */
if dbuf.Len += dn; nb >= 0 {
break
}
/* not enough space, grow the slice and try again */
sidx += ^nb
*dbuf = growslice(typeByte, *dbuf, dbuf.Cap * 2)
}
return dst
}
var (
argPtrs = []bool { true, true, true, false }
localPtrs = []bool{}
)
var (
_F_assertI2I = jit.Func(rt.AssertI2I2)
)
func asText(v unsafe.Pointer) (string, error) {
text := rt.AssertI2I2(_T_encoding_TextMarshaler, *(*rt.GoIface)(v))
r, e := (*(*encoding.TextMarshaler)(unsafe.Pointer(&text))).MarshalText()
return rt.Mem2Str(r), e
}
func asJson(v unsafe.Pointer) (string, error) {
text := rt.AssertI2I2(_T_json_Marshaler, *(*rt.GoIface)(v))
r, e := (*(*json.Marshaler)(unsafe.Pointer(&text))).MarshalJSON()
return rt.Mem2Str(r), e
}
@@ -0,0 +1,206 @@
/*
* Copyright 2021 ByteDance Inc.
*
* 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.
*/
package encoder
// Algorithm 3-way Radix Quicksort, d means the radix.
// Reference: https://algs4.cs.princeton.edu/51radix/Quick3string.java.html
func radixQsort(kvs []_MapPair, d, maxDepth int) {
for len(kvs) > 11 {
// To avoid the worst case of quickSort (time: O(n^2)), use introsort here.
// Reference: https://en.wikipedia.org/wiki/Introsort and
// https://github.com/golang/go/issues/467
if maxDepth == 0 {
heapSort(kvs, 0, len(kvs))
return
}
maxDepth--
p := pivot(kvs, d)
lt, i, gt := 0, 0, len(kvs)
for i < gt {
c := byteAt(kvs[i].k, d)
if c < p {
swap(kvs, lt, i)
i++
lt++
} else if c > p {
gt--
swap(kvs, i, gt)
} else {
i++
}
}
// kvs[0:lt] < v = kvs[lt:gt] < kvs[gt:len(kvs)]
// Native implemention:
// radixQsort(kvs[:lt], d, maxDepth)
// if p > -1 {
// radixQsort(kvs[lt:gt], d+1, maxDepth)
// }
// radixQsort(kvs[gt:], d, maxDepth)
// Optimize as follows: make recursive calls only for the smaller parts.
// Reference: https://www.geeksforgeeks.org/quicksort-tail-call-optimization-reducing-worst-case-space-log-n/
if p == -1 {
if lt > len(kvs) - gt {
radixQsort(kvs[gt:], d, maxDepth)
kvs = kvs[:lt]
} else {
radixQsort(kvs[:lt], d, maxDepth)
kvs = kvs[gt:]
}
} else {
ml := maxThree(lt, gt-lt, len(kvs)-gt)
if ml == lt {
radixQsort(kvs[lt:gt], d+1, maxDepth)
radixQsort(kvs[gt:], d, maxDepth)
kvs = kvs[:lt]
} else if ml == gt-lt {
radixQsort(kvs[:lt], d, maxDepth)
radixQsort(kvs[gt:], d, maxDepth)
kvs = kvs[lt:gt]
d += 1
} else {
radixQsort(kvs[:lt], d, maxDepth)
radixQsort(kvs[lt:gt], d+1, maxDepth)
kvs = kvs[gt:]
}
}
}
insertRadixSort(kvs, d)
}
func insertRadixSort(kvs []_MapPair, d int) {
for i := 1; i < len(kvs); i++ {
for j := i; j > 0 && lessFrom(kvs[j].k, kvs[j-1].k, d); j-- {
swap(kvs, j, j-1)
}
}
}
func pivot(kvs []_MapPair, d int) int {
m := len(kvs) >> 1
if len(kvs) > 40 {
// Tukey's ``Ninther,'' median of three mediankvs of three.
t := len(kvs) / 8
return medianThree(
medianThree(byteAt(kvs[0].k, d), byteAt(kvs[t].k, d), byteAt(kvs[2*t].k, d)),
medianThree(byteAt(kvs[m].k, d), byteAt(kvs[m-t].k, d), byteAt(kvs[m+t].k, d)),
medianThree(byteAt(kvs[len(kvs)-1].k, d),
byteAt(kvs[len(kvs)-1-t].k, d),
byteAt(kvs[len(kvs)-1-2*t].k, d)))
}
return medianThree(byteAt(kvs[0].k, d), byteAt(kvs[m].k, d), byteAt(kvs[len(kvs)-1].k, d))
}
func medianThree(i, j, k int) int {
if i > j {
i, j = j, i
} // i < j
if k < i {
return i
}
if k > j {
return j
}
return k
}
func maxThree(i, j, k int) int {
max := i
if max < j {
max = j
}
if max < k {
max = k
}
return max
}
// maxDepth returns a threshold at which quicksort should switch
// to heapsort. It returnkvs 2*ceil(lg(n+1)).
func maxDepth(n int) int {
var depth int
for i := n; i > 0; i >>= 1 {
depth++
}
return depth * 2
}
// siftDown implements the heap property on kvs[lo:hi].
// first is an offset into the array where the root of the heap lies.
func siftDown(kvs []_MapPair, lo, hi, first int) {
root := lo
for {
child := 2*root + 1
if child >= hi {
break
}
if child+1 < hi && kvs[first+child].k < kvs[first+child+1].k {
child++
}
if kvs[first+root].k >= kvs[first+child].k {
return
}
swap(kvs, first+root, first+child)
root = child
}
}
func heapSort(kvs []_MapPair, a, b int) {
first := a
lo := 0
hi := b - a
// Build heap with the greatest element at top.
for i := (hi - 1) / 2; i >= 0; i-- {
siftDown(kvs, i, hi, first)
}
// Pop elements, the largest first, into end of kvs.
for i := hi - 1; i >= 0; i-- {
swap(kvs, first, first+i)
siftDown(kvs, lo, i, first)
}
}
// Note that _MapPair.k is NOT pointed to _MapPair.m when map key is integer after swap
func swap(kvs []_MapPair, a, b int) {
kvs[a].k, kvs[b].k = kvs[b].k, kvs[a].k
kvs[a].v, kvs[b].v = kvs[b].v, kvs[a].v
}
// Compare two strings from the pos d.
func lessFrom(a, b string, d int) bool {
l := len(a)
if l > len(b) {
l = len(b)
}
for i := d; i < l; i++ {
if a[i] == b[i] {
continue
}
return a[i] < b[i]
}
return len(a) < len(b)
}
func byteAt(b string, p int) int {
if p < len(b) {
return int(b[p])
}
return -1
}
@@ -0,0 +1,89 @@
/*
* Copyright 2021 ByteDance Inc.
*
* 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.
*/
package encoder
import (
`encoding/json`
`io`
)
// StreamEncoder uses io.Writer as input.
type StreamEncoder struct {
w io.Writer
Encoder
}
// NewStreamEncoder adapts to encoding/json.NewDecoder API.
//
// NewStreamEncoder returns a new encoder that write to w.
func NewStreamEncoder(w io.Writer) *StreamEncoder {
return &StreamEncoder{w: w}
}
// Encode encodes interface{} as JSON to io.Writer
func (enc *StreamEncoder) Encode(val interface{}) (err error) {
buf := newBytes()
out := buf
/* encode into the buffer */
err = EncodeInto(&out, val, enc.Opts)
if err != nil {
goto free_bytes
}
if enc.indent != "" || enc.prefix != "" {
/* indent the JSON */
buf := newBuffer()
err = json.Indent(buf, out, enc.prefix, enc.indent)
if err != nil {
freeBuffer(buf)
goto free_bytes
}
// according to standard library, terminate each value with a newline...
if enc.Opts & NoEncoderNewline == 0 {
buf.WriteByte('\n')
}
/* copy into io.Writer */
_, err = io.Copy(enc.w, buf)
if err != nil {
freeBuffer(buf)
goto free_bytes
}
} else {
/* copy into io.Writer */
var n int
for len(out) > 0 {
n, err = enc.w.Write(out)
out = out[n:]
if err != nil {
goto free_bytes
}
}
// according to standard library, terminate each value with a newline...
if enc.Opts & NoEncoderNewline == 0 {
enc.w.Write([]byte{'\n'})
}
}
free_bytes:
freeBytes(buf)
return err
}
@@ -0,0 +1,61 @@
// +build go1.16,!go1.17
/*
* Copyright 2021 ByteDance Inc.
*
* 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.
*/
package encoder
import (
`unsafe`
_ `github.com/cloudwego/base64x`
`github.com/bytedance/sonic/internal/rt`
)
//go:linkname _subr__b64encode github.com/cloudwego/base64x._subr__b64encode
var _subr__b64encode uintptr
//go:noescape
//go:linkname memmove runtime.memmove
//goland:noinspection GoUnusedParameter
func memmove(to unsafe.Pointer, from unsafe.Pointer, n uintptr)
//go:linkname growslice runtime.growslice
//goland:noinspection GoUnusedParameter
func growslice(et *rt.GoType, old rt.GoSlice, cap int) rt.GoSlice
//go:linkname mapiternext runtime.mapiternext
//goland:noinspection GoUnusedParameter
func mapiternext(it *rt.GoMapIterator)
//go:linkname mapiterinit runtime.mapiterinit
//goland:noinspection GoUnusedParameter
func mapiterinit(t *rt.GoMapType, m *rt.GoMap, it *rt.GoMapIterator)
//go:linkname isValidNumber encoding/json.isValidNumber
//goland:noinspection GoUnusedParameter
func isValidNumber(s string) bool
//go:noescape
//go:linkname memclrNoHeapPointers runtime.memclrNoHeapPointers
//goland:noinspection GoUnusedParameter
func memclrNoHeapPointers(ptr unsafe.Pointer, n uintptr)
var _runtime_writeBarrier uintptr = rt.GcwbAddr()
//go:linkname gcWriteBarrierAX runtime.gcWriteBarrier
func gcWriteBarrierAX()
@@ -0,0 +1,62 @@
// +build go1.17,!go1.20
/*
* Copyright 2021 ByteDance Inc.
*
* 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.
*/
package encoder
import (
`unsafe`
_ `github.com/cloudwego/base64x`
`github.com/bytedance/sonic/internal/rt`
)
//go:linkname _subr__b64encode github.com/cloudwego/base64x._subr__b64encode
var _subr__b64encode uintptr
//go:noescape
//go:linkname memmove runtime.memmove
//goland:noinspection GoUnusedParameter
func memmove(to unsafe.Pointer, from unsafe.Pointer, n uintptr)
//go:linkname growslice runtime.growslice
//goland:noinspection GoUnusedParameter
func growslice(et *rt.GoType, old rt.GoSlice, cap int) rt.GoSlice
//go:linkname mapiternext runtime.mapiternext
//goland:noinspection GoUnusedParameter
func mapiternext(it *rt.GoMapIterator)
//go:linkname mapiterinit runtime.mapiterinit
//goland:noinspection GoUnusedParameter
func mapiterinit(t *rt.GoMapType, m *rt.GoMap, it *rt.GoMapIterator)
//go:linkname isValidNumber encoding/json.isValidNumber
//goland:noinspection GoUnusedParameter
func isValidNumber(s string) bool
//go:noescape
//go:linkname memclrNoHeapPointers runtime.memclrNoHeapPointers
//goland:noinspection GoUnusedParameter
func memclrNoHeapPointers(ptr unsafe.Pointer, n uintptr)
//go:linkname _runtime_writeBarrier runtime.writeBarrier
var _runtime_writeBarrier uintptr
//go:linkname gcWriteBarrierAX runtime.gcWriteBarrier
func gcWriteBarrierAX()

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