diff --git a/.github/workflows/regression.yml b/.github/workflows/regression.yml index f792e2a..53f568a 100644 --- a/.github/workflows/regression.yml +++ b/.github/workflows/regression.yml @@ -515,12 +515,21 @@ jobs: path: src\prj\fun\ffi.dll key: win64-ffi-fpc-3.2.2-v1 + # The helper's exports are part of the test interface: hash the source so + # adding a function (ffi-var-ref's setout/addout) rebuilds the DLL + # instead of restoring one that cannot resolve the new symbol. + - name: Compute libffitest input hash (win64) + id: ffitest-hash + shell: pwsh + run: | + "value=${{ hashFiles('fun/test/ffi/libffitest.c') }}" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 + - name: Restore libffitest.dll (win64) id: cache-libffitest-win uses: actions/cache/restore@v4 with: path: fun\test\ffi\libffitest.dll - key: win64-libffitest-v1 + key: win64-libffitest-v1-${{ steps.ffitest-hash.outputs.value }} - name: Mark cached win64 helper DLLs shell: pwsh @@ -834,7 +843,7 @@ jobs: # is readable from outside (the job log is not) $need = 'big64','passll','addll','applyll','summ','ident','addi','five','fsum', 'fdbl','dfromf','mixsum','apply1','dapply','sumvia','cb2','twice_cb', - 'walk','acc','fc','fire0i','fire0' + 'walk','acc','fc','fire0i','fire0','setout','addout' $objdump = Join-Path (Split-Path $gcc) 'objdump.exe' $sym = @() if (Test-Path $objdump) { @@ -876,7 +885,7 @@ jobs: uses: actions/cache/save@v4 with: path: fun\test\ffi\libffitest.dll - key: win64-libffitest-v1 + key: win64-libffitest-v1-${{ steps.ffitest-hash.outputs.value }} - name: Build win64 runtime DLLs (tcc + zlib, best effort) if: steps.cache-win64-runtime.outputs.cache-hit != 'true' diff --git a/README.md b/README.md index eb8095c..564f10c 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ In one sentence: **Fun is a scripting language that goes all the way from JSON to machine code.** - One runtime spans the whole spectrum, from high-level data work (JSON/FD) down to low-level system programming (FFI, C at runtime, JIT, machine code); -- 78 standard-library modules written in Fun itself ship together as one embeddable `fun.dll`; +- 79 standard-library modules written in Fun itself ship together as one embeddable `fun.dll`; - in AI-agent and domestic-IT scenarios, it is the lightweight glue connecting OSes, databases, chips, and business logic. Fun is designed around a simple premise: **data and code should be equally first-class, and a script should be able to reach down to the machine without leaving the language.** @@ -71,7 +71,7 @@ Fun is written in Pascal (Delphi / Free Pascal). The core is a small tree-walkin - **Keyword aliasing** — a canonical English keyword set with pluggable alias packs (Chinese, French, custom) selected at runtime (see [below](#keyword-aliasing)). - **Cross-platform** — Windows (32/64-bit), Linux, ARM, and Windows CE. - **Dual distribution** — command-line `fun.exe` and embeddable runtime `fun.dll` exporting a simple `Run` interface. -- **Self-hosted standard library** — 78 modules written in Fun itself, from JSON/YAML/XML to ORM, async, UI, and native bindings. +- **Self-hosted standard library** — 79 modules written in Fun itself, from JSON/YAML/XML to ORM, async, UI, and native bindings. --- @@ -423,6 +423,8 @@ The signature encodes each argument's type, then `:`, then the return type (`i` `getapi` is per-platform: `winapi.pas` on 32-bit Windows, and `lffi.pas` - backed by FPC's bundled libffi - on Linux, macOS and win64 (there it resolves against `ffi.dll`). +A bare `var x` argument for a `p` parameter is an OUT/INOUT pointer on both backends: the engine passes a pointer-width cell and copies the result back into `x`. For explicit control (or for a caller-owned buffer) pass the address yourself with `buf.toNum(-1)`. + --- ## Keyword aliasing @@ -467,7 +469,7 @@ fun/ │ ├── utils/ # Utility functions │ └── prj/fun/ # Projects & build scripts (funcmd.dpr, Delphi/FPC) ├── fun/ -│ ├── lib/ # Standard library (pure Fun, 78 modules) +│ ├── lib/ # Standard library (pure Fun, 79 modules) │ ├── demo/ # Language syntax demos │ ├── demos/ # Full applications & benchmarks │ └── key_*.ini # Keyword alias packs (CN / FR / PUA) @@ -641,16 +643,16 @@ and the Windows job builds and runs the same strict capability test. The ## Standard library -The standard library in [`fun/lib`](fun/lib) is written in pure Fun (78 modules): +The standard library in [`fun/lib`](fun/lib) is written in pure Fun (79 modules): -> In total roughly 6,000 lines of logical code (excluding comments) — all 78 modules written in Fun itself. +> In total roughly 6,000 lines of logical code (excluding comments) — all 79 modules written in Fun itself. - **Data**: `lib-json`, `csv`, `toml`, `jsonschema`, `lib-yaml`, `lib-xml`, `lib-base64`, `lib-cstruct`, `lib-md5`, `lib-crypt`, `lib-hmac`, `lib-url` - **Utility**: `uuid` - **Collections / algorithms**: `lib-set`, `lib-tree`, `lib-stack`, `lib-dyns`, `lib-math` - **Text**: `lib-string`, `lib-regex`, `lib-match`, `lib-unicode` - **System / IO**: `lib-file`, `lib-os`, `lib-host`, `lib-path`, `env`, `cli`, `fs`, `io`, `process`, `logging`, `lib-time`, `lib-cmdline`, `lib-proc` -- **Network**: `lib-winsock`, `lib-ajax`, `lib-jsonrpc`, `http-server`, `http-client`, `http-auth`, `http-bearer`, `http-cors`, `http-multipart`, `http-session` +- **Network**: `lib-winsock`, `lib-ajax`, `lib-jsonrpc`, `http-server`, `http-client`, `http-auth`, `http-bearer`, `http-cors`, `http-multipart`, `http-session`, `jwt` - **Databases**: `lib-orm`, `lib-orm-pro`, `lib-orm-gen`, `lib-orm-rpc`, `lib-orm-cte`, `lib-ado`, `lib-ado-schema` - **Native / low-level**: `lib-winapi`, `lib-winole`, `lib-tcc` (C compile), `lib-jit`, `lib-asm`, `lib-asm-pro` - **UI**: `lib-ui`, `lib-ui-base`, `lib-dialog`, `lib-trayicon` @@ -701,6 +703,16 @@ var r = req.signedCookie('uid', secret); if r.ok then ?. r.value; end if; ``` +JWT tokens use the same auth helpers: + +```fun +use 'jwt.fun'; + +var token = jwt.sign([sub: 'u1', exp: jwt.now() + 3600], secret); +var r = jwt.verify(token, secret, [issuer: 'app']); +if r.ok then ?. r.claims['sub']; end if; +``` + Environment variables are available without platform-specific FFI: ```fun diff --git a/README.zh.md b/README.zh.md index 7dcd452..ceee4c1 100644 --- a/README.zh.md +++ b/README.zh.md @@ -40,7 +40,7 @@ 一句话定位:**Fun 是一门从 JSON 一路写到机器码的脚本语言。** - 用同一个运行时,覆盖从高层数据处理(JSON/FD)到底层系统编程(FFI、运行时 C、JIT、机器码)的全谱系; -- 自带 78 个用 Fun 自身编写的标准库模块,全栈能力打包进单个可嵌入的 `fun.dll`; +- 自带 79 个用 Fun 自身编写的标准库模块,全栈能力打包进单个可嵌入的 `fun.dll`; - 在信创与 AI Agent 场景中,作为连接国产 OS、数据库、芯片与业务逻辑的轻量级胶水。 Fun 的设计出发点很简单:**数据与代码都应该是头等公民,而且一段脚本应该能在不离开语言的情况下一路触达机器底层。** @@ -71,7 +71,7 @@ Fun 使用 Pascal(Delphi / Free Pascal)编写。核心是一个小巧的树 - **关键字别名** —— 规范英文关键字 + 可插拔的别名包(中文、法文、自定义),运行时选择(见[下文](#关键字别名))。 - **跨平台** —— Windows(32/64 位)、Linux、ARM 与 Windows CE。 - **双形态分发** —— 命令行 `fun.exe` 与嵌入运行时 `fun.dll`(导出简单的 `Run` 接口)。 -- **自托管的标准库** —— 78 个用 Fun 自身编写的模块,覆盖 JSON/YAML/XML 到 ORM、异步、UI 与原生绑定。 +- **自托管的标准库** —— 79 个用 Fun 自身编写的模块,覆盖 JSON/YAML/XML 到 ORM、异步、UI 与原生绑定。 --- @@ -447,7 +447,7 @@ fun/ │ ├── utils/ # 工具函数 │ └── prj/fun/ # 工程与构建脚本(funcmd.dpr,Delphi/FPC) ├── fun/ -│ ├── lib/ # 标准库(纯 Fun 编写,78 个模块) +│ ├── lib/ # 标准库(纯 Fun 编写,79 个模块) │ ├── demo/ # 语言语法演示 │ ├── demos/ # 完整应用与基准测试 │ └── key_*.ini # 关键字别名包(CN / FR / PUA) @@ -601,16 +601,16 @@ COM)、任意地址读写、文件访问、以及从文件或 `inline` 加载 ## 标准库 -标准库位于 [`fun/lib`](fun/lib),以纯 Fun 编写(78 个模块): +标准库位于 [`fun/lib`](fun/lib),以纯 Fun 编写(79 个模块): -> 总代码量约 6,000 行逻辑代码(不含注释)——78 个模块全部用 Fun 自身写成。 +> 总代码量约 6,000 行逻辑代码(不含注释)——79 个模块全部用 Fun 自身写成。 - **数据**:`lib-json`、`csv`、`toml`、`jsonschema`、`lib-yaml`、`lib-xml`、`lib-base64`、`lib-cstruct`、`lib-md5`、`lib-crypt`、`lib-hmac`、`lib-url` - **工具**:`uuid` - **集合 / 算法**:`lib-set`、`lib-tree`、`lib-stack`、`lib-dyns`、`lib-math` - **文本**:`lib-string`、`lib-regex`、`lib-match`、`lib-unicode` - **系统 / IO**:`lib-file`、`lib-os`、`lib-host`、`lib-path`、`env`、`cli`、`fs`、`io`、`process`、`logging`、`lib-time`、`lib-cmdline`、`lib-proc` -- **网络**:`lib-winsock`、`lib-ajax`、`lib-jsonrpc`、`http-server`、`http-client`、`http-auth`、`http-bearer`、`http-cors`、`http-multipart`、`http-session` +- **网络**:`lib-winsock`、`lib-ajax`、`lib-jsonrpc`、`http-server`、`http-client`、`http-auth`、`http-bearer`、`http-cors`、`http-multipart`、`http-session`、`jwt` - **数据库**:`lib-orm`、`lib-orm-pro`、`lib-orm-gen`、`lib-orm-rpc`、`lib-orm-cte`、`lib-ado`、`lib-ado-schema` - **原生 / 底层**:`lib-winapi`、`lib-winole`、`lib-tcc`(C 编译)、`lib-jit`、`lib-asm`、`lib-asm-pro` - **UI**:`lib-ui`、`lib-ui-base`、`lib-dialog`、`lib-trayicon` @@ -660,6 +660,16 @@ var r = req.signedCookie('uid', secret); if r.ok then ?. r.value; end if; ``` +JWT 复用同一套鉴权工具: + +```fun +use 'jwt.fun'; + +var token = jwt.sign([sub: 'u1', exp: jwt.now() + 3600], secret); +var r = jwt.verify(token, secret, [issuer: 'app']); +if r.ok then ?. r.claims['sub']; end if; +``` + 环境变量也可以直接读取,不需要自己写平台 FFI: ```fun diff --git a/fun/lib/auth/util.fun b/fun/lib/auth/util.fun index 54a2ce6..833e73e 100644 --- a/fun/lib/auth/util.fun +++ b/fun/lib/auth/util.fun @@ -7,14 +7,128 @@ // builds fall back to the pure Fun implementation. use '../lib-base64.fun'; +use '../lib-host.fun'; use '../lib-md5.fun'; use '../lib-utils.fun'; var _AUTH_B64URL = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'; +var _AUTH_SHA256 = 32780; +var _AUTH_SHA384 = 32781; +var _AUTH_SHA512 = 32782; +var _authCr = nil; +var _authTime = nil; -fun AuthHmacSha256Raw(key, msg) +fun _authCrypt() + if _authCr <> nil then result = _authCr; return; end if; + var host = 'host'.arg().getJson(fd: true); + try + if host.os = 'linux' or host.os = 'macos' then + use '../lib-crypt-unx.fun' as libCr; + _authCr = libCr; + else + use '../lib-crypt.fun' as libCr; + _authCr = libCr; + end if; + except + raise 'auth: crypto backend is not available'; + end try; + result = _authCr; +end fun; + +fun _authHashHex(msg, alg) + if alg = 'sha256' then result = SHA256(msg); return; end if; + // CNG is the win64 path only: win64 getapi() uses the libffi backend, where + // a 'p' argument is a raw pointer value. 32-bit Windows uses winapi.pas, + // whose 'p' means "address of this value", so keep it on CryptoAPI below. + var host = 'host'.arg().getJson(fd: true); + if host.os = 'windows' and host.bits >= 64 then + result = _authCngHashHex(msg, alg); + return; + end if; + var c = _authCrypt(); + if alg = 'sha384' then + result = c.Crypt(msg, _AUTH_SHA384); + elsif alg = 'sha512' then + result = c.Crypt(msg, _AUTH_SHA512); + else + raise 'auth: unsupported hash: ' & alg; + end if; +end fun; + +var _authCngOpen = nil; +var _authCngClose = nil; +var _authCngHash = nil; + +fun _authCngName(name) + var host = 'host'.arg().getJson(fd: true); + if host.chars = 2 then + result = name & 0.toChar(); + return; + end if; + var out = ''; + var i = 0; + while i < name.length() do + out &= name[i] & 0.toChar(); + i += 1; + end do; + result = out & 0.toChar() & 0.toChar(); +end fun; + +// Pointer-width little-endian read: Fun's >> is 32-bit and win64 addresses +// live above 4GB. Same idiom as lib-ado-unx.fun's _zr. +fun _authCngReadPtr(s) + result = 0; + var m = 1; + var i = 0; + while i < 8 do + result += s.toByte(i) * m; + m *= 256; + i += 1; + end do; +end fun; + +fun _authCngHashHex(msg, alg) + if _authCngOpen = nil then + _authCngOpen = 'bcrypt.dll'.getapi('BCryptOpenAlgorithmProvider', 'pppi:i'); + _authCngClose = 'bcrypt.dll'.getapi('BCryptCloseAlgorithmProvider', 'pi:i'); + _authCngHash = 'bcrypt.dll'.getapi('BCryptHash', 'ppipipi:i'); + end if; + var name = 'SHA384'; + var len = 48; + if alg = 'sha512' then name = 'SHA512'; len = 64; end if; + var wn = _authCngName(name); + // An OUT handle needs a writable buffer: lffi passes a 'p' argument's value, + // so `var h` would hand CNG a NULL phAlgorithm and it returns + // STATUS_INVALID_PARAMETER (0xC000000D). + var hbuf = 0.toChar().x(8); + var st = _authCngOpen(hbuf.toNum(-1), wn.toNum(-1), 0, 0); + if st <> 0 then raise 'auth: CNG open failed (' & st & ')'; end if; + var h = _authCngReadPtr(hbuf); + try + var buf = ' '.x(len); + // CNG rejects a NULL input pointer even for a zero-length message, so + // hash one harmless byte when msg is empty. + var empty = 0.toChar(); + var mptr = msg.toNum(-1); + if msg.length() = 0 then mptr = empty.toNum(-1); end if; + st = _authCngHash(h, 0, 0, mptr, msg.length(), buf.toNum(-1), len); + if st <> 0 then raise 'auth: CNG hash failed (' & st & ')'; end if; + var raw = 0.toChar().x(len); + var i = 0; + while i < len do + raw.fromByte(i, buf.toByte(i)); + i += 1; + end do; + result = str2hex(raw); + finally + _authCngClose(h, 0); + end try; +end fun; + +fun AuthHmacRaw(key, msg, alg) var block = 64; - if key.length() > block then key = hex2str(SHA256(key)); end if; + if alg = 'sha384' or alg = 'sha512' then block = 128; end if; + if key.length() > block then key = hex2str(_authHashHex(key, alg)); end if; if key.length() < block then key &= 0.toChar().x(block - key.length()); end if; var ipad = ''; var opad = ''; @@ -25,13 +139,36 @@ fun AuthHmacSha256Raw(key, msg) opad &= (b bit xor 0x5c).toChar(); i += 1; end do; - result = hex2str(SHA256(opad & hex2str(SHA256(ipad & msg)))); + result = hex2str(_authHashHex(opad & hex2str(_authHashHex(ipad & msg, alg)), alg)); +end fun; + +fun AuthHmacHex(key, msg, alg) + result = str2hex(AuthHmacRaw(key, msg, alg)); +end fun; + +fun AuthHmacSha256Raw(key, msg) + result = AuthHmacRaw(key, msg, 'sha256'); end fun; fun AuthHmacSha256Hex(key, msg) result = str2hex(AuthHmacSha256Raw(key, msg)); end fun; +fun AuthUnixNow() + if _authTime <> nil then result = _authTime(0); return; end if; + var host = 'host'.arg().getJson(fd: true); + if host.os = 'windows' then + try + _authTime = LibC().getapi('_time64', 'p:l'); + except + _authTime = LibC().getapi('time', 'p:l'); + end try; + else + _authTime = LibC().getapi('time', 'p:l'); + end if; + result = _authTime(0); +end fun; + fun AuthB64UrlEncode(s) var b = toBase64(s); var out = ''; diff --git a/fun/lib/jwt.fun b/fun/lib/jwt.fun new file mode 100644 index 0000000..d831099 --- /dev/null +++ b/fun/lib/jwt.fun @@ -0,0 +1,443 @@ +// Copyright (c) 2010-2026 Zhang Weidong +// SPDX-License-Identifier: MIT +// +// jwt: HS256/HS384/HS512 JSON Web Tokens (RFC 7519 / RFC 7515). +// +// use 'jwt.fun'; +// var token = jwt.sign([sub: 'u1', exp: jwt.now() + 3600], key); +// var r = jwt.verify(token, key, [issuer: 'app']); +// if r.ok then ?. r.claims['sub']; end if; +// +// verify returns [ok, error, header, claims]; malformed tokens and claim +// failures are normal results, not exceptions. Algorithm none is rejected. + +use 'auth/util.fun'; + +var _JWT_SIGN_OPTS = '|alg|headers|expiresIn|notBefore|now|'; +var _JWT_VERIFY_OPTS = '|algorithms|issuer|audience|subject|jwtid|leeway|requireExp|now|'; + +fun _jwtKey(key) + if key = nil or key.type() <> 8 or key.length() = 0 then + raise 'jwt: key must be a non-empty string'; + end if; + result = true; +end fun; + +fun _jwtHas(opts, name) + if opts = nil then result = false; return; end if; + result = opts.@has(name); +end fun; + +fun _jwtCheckOpts(opts, allowed) + if opts = nil then result = true; return; end if; + var bad = ''; + var listish = false; + try + opts.@each((v, k){ + if k = nil or k = '' then + listish = true; + elsif allowed.subpos('|' & k & '|') < 0 then + bad = k; + end if; + }); + except + raise 'jwt: opts must be a set'; + end try; + if listish then raise 'jwt: opts must be a set'; end if; + if bad <> '' then raise 'jwt: unknown option: ' & bad; end if; + result = true; +end fun; + +fun _jwtIsNum(v) + result = v.type() = 3 or v.type() = 5; +end fun; + +fun _jwtOptNum(opts, name, def) + if not _jwtHas(opts, name) then result = def; return; end if; + var v = opts[name]; + if not _jwtIsNum(v) or v < 0 then + raise 'jwt: ' & name & ' must be a non-negative number'; + end if; + result = v; +end fun; + +fun _jwtAlg(a) + if a = nil or a.type() <> 8 then raise 'jwt: alg must be a string'; end if; + if not a.eq('HS256') and not a.eq('HS384') and not a.eq('HS512') then + raise 'jwt: unsupported alg: ' & a; + end if; + result = a; +end fun; + +fun _jwtHeaderCheck(h) + if h.@has('crit') then raise 'jwt: unsupported header: crit'; end if; + if h.@has('b64') then raise 'jwt: unsupported header: b64'; end if; + if h.@has('zip') then raise 'jwt: unsupported header: zip'; end if; + result = true; +end fun; + +fun _jwtHashName(alg) + if alg.eq('HS256') then result = 'sha256'; + elsif alg.eq('HS384') then result = 'sha384'; + else result = 'sha512'; end if; +end fun; + +fun _jwtObj(v, label) + var j = ''; + try + j = v.@toJson(json: true); + except + raise 'jwt: ' & label & ' must be an object'; + end try; + if j = '' or j.substr(0, 1) <> '{' then + raise 'jwt: ' & label & ' must be an object'; + end if; + result = true; +end fun; + +fun _jwtCopySet(s) + var out = new []; + s.@each((v, k){ if k.type() = 8 then out[k] = v; end if; }); + result = out; +end fun; + +fun _jwtHeader(alg, headers) + var h = new []; + h['alg'] = alg; + h['typ'] = 'JWT'; + if headers <> nil then + var bad = false; + try + headers.@each((v, k){ + if k = nil then + bad = true; + elsif k <> 'alg' and k <> 'typ' then + h[k] = v; + end if; + }); + except + bad = true; + end try; + if bad then raise 'jwt: headers must be a set'; end if; + _jwtHeaderCheck(h); + end if; + result = h; +end fun; + +fun _jwtSplit(token) + var r = new []; + r.ok = false; + r.error = 'jwt: token must have three parts'; + r.a = ''; + r.b = ''; + r.c = ''; + var p1 = token.subpos('.'); + if p1 <= 0 then result = r; return; end if; + var rest = token.substr(p1 + 1); + var p2 = rest.subpos('.'); + if p2 <= 0 then result = r; return; end if; + var sig = rest.substr(p2 + 1); + if sig = '' or sig.subpos('.') >= 0 then result = r; return; end if; + r.a = token.substr(0, p1); + r.b = rest.substr(0, p2); + r.c = sig; + r.ok = true; + r.error = nil; + result = r; +end fun; + +fun _jwtDecodePart(seg, label) + try + var text = AuthB64UrlDecode(seg); + var v = text.getJson(json: true); + _jwtObj(v, label); + result = v; + except + raise 'jwt: invalid ' & label; + end try; +end fun; + +fun _jwtValues(v) + var out = new []; + if v.type() = 8 then + out.@add(v); + result = out; + return; + end if; + if v = nil then result = out; return; end if; + var n = 0; + try + n = v.@count(); + except + result = out; + return; + end try; + var i = 0; + while i < n do + var x = v[i]; + if x <> nil and x.type() = 8 then out.@add(x); end if; + i += 1; + end do; + result = out; +end fun; + +fun _jwtAudMatch(got, want) + var g = _jwtValues(got); + var w = _jwtValues(want); + var i = 0; + while i < g.@count() do + var j = 0; + while j < w.@count() do + if g[i].eq(w[j]) then result = true; return; end if; + j += 1; + end do; + i += 1; + end do; + result = false; +end fun; + +fun _jwtCheckClaims(claims, opts, now) + var leeway = _jwtOptNum(opts, 'leeway', 0); + if claims.@has('exp') then + var e = claims['exp']; + if not _jwtIsNum(e) then result = 'jwt: invalid exp'; return; end if; + if now >= e + leeway then result = 'jwt: token expired'; return; end if; + elsif _jwtHas(opts, 'requireExp') and opts['requireExp'] = true then + result = 'jwt: missing exp'; + return; + end if; + if claims.@has('nbf') then + var n = claims['nbf']; + if not _jwtIsNum(n) then result = 'jwt: invalid nbf'; return; end if; + if now + leeway < n then result = 'jwt: token not yet valid'; return; end if; + end if; + if claims.@has('iat') then + var i = claims['iat']; + if not _jwtIsNum(i) then result = 'jwt: invalid iat'; return; end if; + if i > now + leeway then result = 'jwt: token issued in the future'; return; end if; + end if; + if _jwtHas(opts, 'issuer') then + var iss = claims['iss']; + var wantIss = opts['issuer']; + if iss.type() <> 8 or wantIss.type() <> 8 or not iss.eq(wantIss) then + result = 'jwt: issuer mismatch'; + return; + end if; + end if; + if _jwtHas(opts, 'audience') then + if not _jwtAudMatch(claims['aud'], opts['audience']) then + result = 'jwt: audience mismatch'; + return; + end if; + end if; + if _jwtHas(opts, 'subject') then + var sub = claims['sub']; + var wantSub = opts['subject']; + if sub.type() <> 8 or wantSub.type() <> 8 or not sub.eq(wantSub) then + result = 'jwt: subject mismatch'; + return; + end if; + end if; + if _jwtHas(opts, 'jwtid') then + var jti = claims['jti']; + var wantJti = opts['jwtid']; + if jti.type() <> 8 or wantJti.type() <> 8 or not jti.eq(wantJti) then + result = 'jwt: jwtid mismatch'; + return; + end if; + end if; + result = ''; +end fun; + +fun _jwtAlgorithms(opts) + var out = new []; + if _jwtHas(opts, 'algorithms') then + var vs = _jwtValues(opts['algorithms']); + var i = 0; + while i < vs.@count() do + out.@add(_jwtAlg(vs[i])); + i += 1; + end do; + else + out.@add('HS256'); + out.@add('HS384'); + out.@add('HS512'); + end if; + result = out; +end fun; + +fun _jwtAlgAllowed(list, alg) + var i = 0; + while i < list.@count() do + if list[i].eq(alg) then result = true; return; end if; + i += 1; + end do; + result = false; +end fun; + +class _JwtApi() + fun now() + result = AuthUnixNow(); + end fun; + + fun sign(claims, key, opts) + _jwtKey(key); + _jwtCheckOpts(opts, _JWT_SIGN_OPTS); + _jwtObj(claims, 'claims'); + var alg = 'HS256'; + if _jwtHas(opts, 'alg') then alg = _jwtAlg(opts['alg']); end if; + var now = 0; + if _jwtHas(opts, 'expiresIn') or _jwtHas(opts, 'notBefore') then + if _jwtHas(opts, 'now') then + now = _jwtOptNum(opts, 'now', 0); + else + now = AuthUnixNow(); + end if; + end if; + var c = _jwtCopySet(claims); + if _jwtHas(opts, 'expiresIn') and not c.@has('exp') then + c['exp'] = now + _jwtOptNum(opts, 'expiresIn', 0); + end if; + if _jwtHas(opts, 'notBefore') and not c.@has('nbf') then + c['nbf'] = now + _jwtOptNum(opts, 'notBefore', 0); + end if; + var h = _jwtHeader(alg, _jwtGetHeaders(opts)); + var hb = AuthB64UrlEncode(h.@toJson(json: true)); + var pb = AuthB64UrlEncode(c.@toJson(json: true)); + var input = hb & '.' & pb; + var sig = AuthB64UrlEncode(AuthHmacRaw(key, input, _jwtHashName(alg))); + result = input & '.' & sig; + end fun; + + fun decode(token) + var r = new []; + r.ok = false; + r.error = ''; + r.header = nil; + r.claims = nil; + if token.type() <> 8 then + r.error = 'jwt: token must be a string'; + result = r; + return; + end if; + var sp = _jwtSplit(token); + if not sp['ok'] then + r.error = sp['error']; + result = r; + return; + end if; + try + r.header = _jwtDecodePart(sp['a'], 'header'); + except + r.error = 'jwt: invalid header'; + result = r; + return; + end try; + try + r.claims = _jwtDecodePart(sp['b'], 'claims'); + except + r.error = 'jwt: invalid claims'; + result = r; + return; + end try; + r.ok = true; + r.error = nil; + result = r; + end fun; + + fun verify(token, key, opts) + _jwtKey(key); + _jwtCheckOpts(opts, _JWT_VERIFY_OPTS); + var r = new []; + r.ok = false; + r.error = ''; + r.header = nil; + r.claims = nil; + if token.type() <> 8 then + r.error = 'jwt: token must be a string'; + result = r; + return; + end if; + var sp = _jwtSplit(token); + if not sp['ok'] then + r.error = sp['error']; + result = r; + return; + end if; + var hdr = nil; + try + hdr = _jwtDecodePart(sp['a'], 'header'); + except + r.error = 'jwt: invalid header'; + result = r; + return; + end try; + try + _jwtHeaderCheck(hdr); + except + r.error = 'jwt: unsupported header'; + result = r; + return; + end try; + var alg = ''; + try + alg = _jwtAlg(hdr['alg']); + except + r.error = 'jwt: unsupported alg'; + result = r; + return; + end try; + if not _jwtAlgAllowed(_jwtAlgorithms(opts), alg) then + r.error = 'jwt: alg not allowed'; + result = r; + return; + end if; + var want = AuthHmacRaw(key, sp['a'] & '.' & sp['b'], _jwtHashName(alg)); + var got = ''; + try + got = AuthB64UrlDecode(sp['c']); + except + r.error = 'jwt: invalid signature'; + result = r; + return; + end try; + if not AuthBytesEqual(want, got) then + r.error = 'jwt: signature mismatch'; + result = r; + return; + end if; + r.header = hdr; + try + r.claims = _jwtDecodePart(sp['b'], 'claims'); + except + r.error = 'jwt: invalid claims'; + result = r; + return; + end try; + var needNow = r.claims.@has('exp') or r.claims.@has('nbf') or r.claims.@has('iat') or + (_jwtHas(opts, 'requireExp') and opts['requireExp'] = true); + var now = 0; + if needNow then + if _jwtHas(opts, 'now') then + now = _jwtOptNum(opts, 'now', 0); + else + now = AuthUnixNow(); + end if; + end if; + var err = _jwtCheckClaims(r.claims, opts, now); + if err <> '' then + r.error = err; + result = r; + return; + end if; + r.ok = true; + r.error = nil; + result = r; + end fun; +end class; + +fun _jwtGetHeaders(opts) + if _jwtHas(opts, 'headers') then result = opts['headers']; else result = nil; end if; +end fun; + +var jwt = _JwtApi(); diff --git a/fun/test/expected/ffi-var-ref.fun.txt b/fun/test/expected/ffi-var-ref.fun.txt new file mode 100644 index 0000000..b204025 --- /dev/null +++ b/fun/test/expected/ffi-var-ref.fun.txt @@ -0,0 +1,7 @@ +0 +42 +0 +50 +0 +7 +-1 diff --git a/fun/test/expected/lib-jwt-test.fun.txt b/fun/test/expected/lib-jwt-test.fun.txt new file mode 100644 index 0000000..795d310 --- /dev/null +++ b/fun/test/expected/lib-jwt-test.fun.txt @@ -0,0 +1,81 @@ +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True +True diff --git a/fun/test/expected/lib-loadcheck.sh.txt b/fun/test/expected/lib-loadcheck.sh.txt index 40001f3..0f3dee9 100644 --- a/fun/test/expected/lib-loadcheck.sh.txt +++ b/fun/test/expected/lib-loadcheck.sh.txt @@ -11,6 +11,7 @@ LOAD http-server LOAD http-session LOAD io LOAD jsonschema +LOAD jwt LOAD lib-ado-schema LOAD lib-ado LOAD lib-ajax @@ -83,4 +84,4 @@ LOAD test LOAD toml LOAD uuid -loaded=78 failed=6 +loaded=79 failed=6 diff --git a/fun/test/expected/macos/lib-loadcheck.sh.txt b/fun/test/expected/macos/lib-loadcheck.sh.txt index a51a891..2cc8607 100644 --- a/fun/test/expected/macos/lib-loadcheck.sh.txt +++ b/fun/test/expected/macos/lib-loadcheck.sh.txt @@ -11,6 +11,7 @@ LOAD http-server LOAD http-session LOAD io LOAD jsonschema +LOAD jwt LOAD lib-ado-schema LOAD lib-ado LOAD lib-ajax @@ -83,4 +84,4 @@ LOAD test LOAD toml LOAD uuid -loaded=78 failed=6 +loaded=79 failed=6 diff --git a/fun/test/ffi/README.md b/fun/test/ffi/README.md index 9edfec5..1a15c61 100644 --- a/fun/test/ffi/README.md +++ b/fun/test/ffi/README.md @@ -72,4 +72,6 @@ src/prj/fun/funcmd fun/test/ffi/ffi-float-cb.fun # float cb, void cb, nested c - Struct / array args and C `long double` / complex returns are not covered. - The `p` pointer parameter type is passed to Fun as a raw numeric address (no automatic dereference); use `ptr=true` on `@toCallback` to hand C a raw - address instead of a managed closure when needed. + address instead of a managed closure when needed. A bare `var x` argument is + the exception: it passes the address of a pointer-width cell and copies the + result back into `x`, matching win32 `getapi` (`ffi-var-ref.fun` covers it). diff --git a/fun/test/ffi/ffi-var-ref.fun b/fun/test/ffi/ffi-var-ref.fun new file mode 100644 index 0000000..abdd510 --- /dev/null +++ b/fun/test/ffi/ffi-var-ref.fun @@ -0,0 +1,23 @@ +use 'fun/lib/lib-host.fun'; + +// A bare `var x` passed for a 'p' parameter is an OUT/INOUT pointer, the same +// convention win32's winapi.pas has always had. The lffi backend passes the +// address of a pointer-width scratch cell and copies the result back. An +// explicit lffi buffer address keeps working unchanged, and a NULL stays NULL. +var FFITEST = DyLibWin('./fun/test/ffi/libffitest.so', './fun/test/ffi/libffitest.dylib', + './fun/test/ffi/libffitest.dll'); + +var setout = FFITEST.getapi('setout', 'pii:i'); +var addout = FFITEST.getapi('addout', 'pii:i'); + +var h = 0; +?. setout(var h, 42); +?. h; +?. addout(var h, 8); +?. h; + +var buf = 0.toChar().x(8); +?. setout(buf.toNum(-1), 7); +?. buf.toByte(0); + +?. setout(0, 9); diff --git a/fun/test/ffi/libffitest.c b/fun/test/ffi/libffitest.c index eb7bbeb..367f173 100644 --- a/fun/test/ffi/libffitest.c +++ b/fun/test/ffi/libffitest.c @@ -53,3 +53,7 @@ float fc(float x, float(*cb)(float)){ return cb(x); } /* zero-arg callbacks */ int fire0i(int(*cb)(void)){ return cb(); } void fire0(void(*cb)(void)){ cb(); } + +/* ---- OUT / INOUT pointer args ('p' with a bare `var`) ------------------- */ +int setout(int *p, int v){ if(!p) return -1; *p = v; return 0; } +int addout(int *p, int v){ if(!p) return -2; *p += v; return 0; } diff --git a/fun/test/lib-jwt-test.fun b/fun/test/lib-jwt-test.fun new file mode 100644 index 0000000..a8671d0 --- /dev/null +++ b/fun/test/lib-jwt-test.fun @@ -0,0 +1,165 @@ +// jwt: HS256/384/512, claims, malformed tokens. Booleans only. +use 'fun/lib/jwt.fun'; +use 'fun/lib/auth/util.fun'; + +fun errHas(r, s) + result = not r['ok'] and ('' & (r['error'] or '')).subpos(s) >= 0; +end fun; + +fun raises(fn) + var bad = false; + try + fn(); + except + bad = true; + end try; + result = bad; +end fun; + +fun tamperLast(s) + var c = s[s.length() - 1]; + var d = 'X'; + if c = 'X' then d = 'Y'; end if; + result = s.substr(0, s.length() - 1) & d; +end fun; + +var secret = 'secret'; + +// --- HMAC-SHA256/384/512: RFC 4231 vectors ------------------------------- +var k = 0x0b.toChar().x(20); +?. AuthHmacHex(k, 'Hi There', 'sha256') = + 'b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7'; +?. AuthHmacHex(k, 'Hi There', 'sha384') = + 'afd03944d84895626b0825f4ab46907f15f9dadbe4101ec682aa034c7cebc59cfaea9ea9076ede7f4af152e8b2fa9cb6'; +?. AuthHmacHex(k, 'Hi There', 'sha512') = + '87aa7cdea5ef619d4ff0b4241a1d6cb02379f4e2ce4ec2787ad0b30545e17cdedaa833b7d6b8a702038b274eaea3f4e4be9d914eeb61f1702e696c203a126854'; + +// --- known HS256 token ---------------------------------------------------- +var known = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'; +var kr = jwt.verify(known, 'your-256-bit-secret', [now: 1516239023]); +?. kr['ok']; +?. kr['claims']['sub'] = '1234567890'; +?. kr['claims']['name'] = 'John Doe'; +?. not jwt.verify(known, 'wrong-secret', [now: 1516239023])['ok']; + +// --- sign/verify round-trip for all HMAC algorithms ---------------------- +var t256 = jwt.sign([sub: 'u1', exp: 2000000000], secret); +?. jwt.verify(t256, secret)['ok']; +?. jwt.verify(t256, secret)['claims']['sub'] = 'u1'; +var t384 = jwt.sign([sub: 'u1', exp: 2000000000], secret, [alg: 'HS384']); +?. jwt.verify(t384, secret)['ok']; +var t512 = jwt.sign([sub: 'u1', exp: 2000000000], secret, [alg: 'HS512']); +?. jwt.verify(t512, secret)['ok']; +?. not jwt.verify(t512, secret, [algorithms: ['HS256']])['ok']; +?. jwt.verify(t256, secret, [algorithms: 'HS256'])['ok']; + +// --- tamper and decode ---------------------------------------------------- +var badSig = tamperLast(t256); +?. not jwt.verify(badSig, secret)['ok']; +var p1 = t256.subpos('.'); +var badPayload = t256.substr(0, p1 + 1) & 'x' & t256.substr(p1 + 2); +?. not jwt.verify(badPayload, secret)['ok']; +?. jwt.decode(badPayload)['ok']; +?. jwt.decode(t256)['claims']['sub'] = 'u1'; +?. jwt.decode(t256)['header']['typ'] = 'JWT'; + +// --- malformed tokens ----------------------------------------------------- +?. errHas(jwt.verify('', secret), 'three parts'); +?. errHas(jwt.verify('abc', secret), 'three parts'); +?. errHas(jwt.verify('a.b', secret), 'three parts'); +?. errHas(jwt.verify('a.b.c.d', secret), 'three parts'); +?. errHas(jwt.verify('a..c', secret), 'three parts'); +?. errHas(jwt.verify('a.b.', secret), 'three parts'); +?. errHas(jwt.verify('%' & t256.substr(t256.subpos('.')), secret), 'invalid header'); +var h = AuthB64UrlEncode('{"alg":"HS256","typ":"JWT"}'); +var p = AuthB64UrlEncode('{"sub":"u1"}'); +var s = AuthB64UrlEncode(AuthHmacRaw(secret, h & '.' & p, 'sha256')); +?. jwt.verify(h & '.' & p & '.' & s, secret)['ok']; +?. errHas(jwt.verify(AuthB64UrlEncode('not json') & '.' & p & '.' & s, secret), 'invalid header'); +var badP = AuthB64UrlEncode('not json'); +var badPs = AuthB64UrlEncode(AuthHmacRaw(secret, h & '.' & badP, 'sha256')); +?. errHas(jwt.verify(h & '.' & badP & '.' & badPs, secret), 'invalid claims'); +?. errHas(jwt.verify('%' & '.' & p & '.' & s, secret), 'invalid header'); +var hn = AuthB64UrlEncode('{"alg":"none","typ":"JWT"}'); +?. not jwt.verify(hn & '.' & AuthB64UrlEncode('{}') & '.x', secret)['ok']; +var hu = AuthB64UrlEncode('{"alg":"HS999","typ":"JWT"}'); +?. not jwt.verify(hu & '.' & AuthB64UrlEncode('{}') & '.x', secret)['ok']; +var lc = AuthB64UrlEncode('{"alg":"hs256","typ":"JWT"}'); +var ls = AuthB64UrlEncode(AuthHmacRaw(secret, lc & '.' & p, 'sha256')); +?. errHas(jwt.verify(lc & '.' & p & '.' & ls, secret), 'unsupported alg'); +var hc = AuthB64UrlEncode('{"alg":"HS256","typ":"JWT","crit":["exp"]}'); +var cs = AuthB64UrlEncode(AuthHmacRaw(secret, hc & '.' & p, 'sha256')); +?. errHas(jwt.verify(hc & '.' & p & '.' & cs, secret), 'unsupported header'); +var hb = AuthB64UrlEncode('{"alg":"HS256","typ":"JWT","b64":false}'); +var bs = AuthB64UrlEncode(AuthHmacRaw(secret, hb & '.' & p, 'sha256')); +?. errHas(jwt.verify(hb & '.' & p & '.' & bs, secret), 'unsupported header'); +var br = jwt.verify(tamperLast(t256), secret); +?. br['claims'] = nil; +?. br['header'] = nil; +?. raises(((){ jwt.sign([sub: 'u1'], secret, [alg: 'hs256']); })); +?. raises(((){ jwt.sign([sub: 'u1'], secret, [headers: [crit: ['exp']]]); })); + +// --- exp / nbf / iat / leeway -------------------------------------------- +var expired = jwt.sign([exp: 1000], secret); +?. not jwt.verify(expired, secret, [now: 1000])['ok']; +?. errHas(jwt.verify(expired, secret, [now: 1000]), 'expired'); +?. jwt.verify(expired, secret, [now: 1001, leeway: 10])['ok']; +?. jwt.verify(expired, secret, [now: 999])['ok']; +var noExp = jwt.sign([sub: 'u1'], secret); +?. jwt.verify(noExp, secret)['ok']; +?. errHas(jwt.verify(noExp, secret, [requireExp: true]), 'missing exp'); +?. errHas(jwt.verify(jwt.sign([exp: 'soon'], secret), secret, [now: 1000]), 'invalid exp'); +var early = jwt.sign([nbf: 2000], secret); +?. errHas(jwt.verify(early, secret, [now: 1999]), 'not yet valid'); +?. jwt.verify(early, secret, [now: 1999, leeway: 2])['ok']; +var future = jwt.sign([iat: 2000], secret); +?. errHas(jwt.verify(future, secret, [now: 1999]), 'issued in the future'); +?. jwt.verify(future, secret, [now: 1999, leeway: 2])['ok']; + +// --- iss / aud / sub / jti ---------------------------------------------- +var claims = jwt.sign([iss: 'app', aud: ['a', 'b'], sub: 'u1', jti: 'j1'], secret); +?. jwt.verify(claims, secret, [issuer: 'app', audience: 'b', subject: 'u1', jwtid: 'j1'])['ok']; +?. jwt.verify(claims, secret, [audience: ['c', 'a']])['ok']; +?. errHas(jwt.verify(claims, secret, [issuer: 'other']), 'issuer'); +?. errHas(jwt.verify(claims, secret, [audience: 'c']), 'audience'); +?. errHas(jwt.verify(claims, secret, [subject: 'u2']), 'subject'); +?. errHas(jwt.verify(claims, secret, [jwtid: 'j2']), 'jwtid'); +?. errHas(jwt.verify(jwt.sign([aud: 7], secret), secret, [audience: 'a']), 'audience'); +var caseClaims = jwt.sign([iss: 'App', aud: 'A', sub: 'U', jti: 'J'], secret); +?. errHas(jwt.verify(caseClaims, secret, [issuer: 'app']), 'issuer'); +?. errHas(jwt.verify(caseClaims, secret, [audience: 'a']), 'audience'); +?. errHas(jwt.verify(caseClaims, secret, [subject: 'u']), 'subject'); +?. errHas(jwt.verify(caseClaims, secret, [jwtid: 'j']), 'jwtid'); +var uni = jwt.sign([name: '张三', aud: ['测试', 'a']], secret); +var unir = jwt.verify(uni, secret, [audience: '测试']); +?. unir['ok']; +?. unir['claims']['name'] = '张三'; +var emptyAud = jwt.sign([aud: ''], secret); +?. jwt.verify(emptyAud, secret, [audience: ''])['ok']; + +// --- relative options and custom headers --------------------------------- +var rel = jwt.sign([sub: 'u1'], secret, [expiresIn: 60, notBefore: 10, now: 1000]); +var rr = jwt.verify(rel, secret, [now: 1010]); +?. rr['ok']; +?. rr['claims']['exp'] = 1060; +?. rr['claims']['nbf'] = 1010; +?. not jwt.verify(rel, secret, [now: 1009])['ok']; +var hk = jwt.sign([sub: 'u1'], secret, [headers: [kid: 'k1']]); +?. jwt.decode(hk)['header']['kid'] = 'k1'; + +// --- parameter misuse raises -------------------------------------------- +?. raises(((){ jwt.sign([sub: 'u1'], ''); })); +?. raises(((){ jwt.sign([sub: 'u1'], 7); })); +?. raises(((){ jwt.sign([1, 2], secret); })); +?. raises(((){ jwt.sign([sub: 'u1'], secret, [alg: 'none']); })); +?. raises(((){ jwt.sign([sub: 'u1'], secret, [nope: 1]); })); +?. raises(((){ jwt.sign([sub: 'u1'], secret, [expiresIn: -1]); })); +?. raises(((){ jwt.verify(t256, ''); })); +?. raises(((){ jwt.verify(t256, secret, [nope: 1]); })); +?. raises(((){ jwt.verify(t256, secret, [algorithms: ['RS256']]); })); +?. raises(((){ jwt.verify(t256, secret, [algorithms: ['hs256']]); })); +?. raises(((){ jwt.verify(t256, secret, [leeway: -1]); })); +?. not jwt.verify(7, secret)['ok']; + +// --- clock --------------------------------------------------------------- +?. jwt.now() > 1700000000; diff --git a/fun/test/regression-list.txt b/fun/test/regression-list.txt index 3d3ba87..9ef6151 100644 --- a/fun/test/regression-list.txt +++ b/fun/test/regression-list.txt @@ -54,6 +54,7 @@ lib-csv-test.fun test/lib-csv-test.fun snap lib-path-test.fun test/lib-path-test.fun snap lib-logging-test.fun test/lib-logging-test.fun snap lib-jsonschema-test.fun test/lib-jsonschema-test.fun snap +lib-jwt-test.fun test/lib-jwt-test.fun snap lib-cmdline-linux.fun test/lib-cmdline-linux.fun snap lib-loadcheck.sh test/lib-loadcheck.sh shell run-benchmarks.sh test/run-benchmarks.sh shell @@ -90,6 +91,7 @@ ffi-int64.fun test/ffi/ffi-int64.fun snap ffi-float.fun test/ffi/ffi-float.fun snap ffi-float-cb.fun test/ffi/ffi-float-cb.fun snap ffi-callbacks.fun test/ffi/ffi-callbacks.fun snap +ffi-var-ref.fun test/ffi/ffi-var-ref.fun snap lib-zlib-test.fun test/lib-zlib-test.fun snap lib-crypt-lnx.fun test/lib-crypt-lnx.fun snap lib-cmdline-lnx-test.fun test/lib-cmdline-lnx-test.fun snap diff --git a/fun/test/run-windows-regression.bat b/fun/test/run-windows-regression.bat index 109d2ec..f6e83f0 100644 --- a/fun/test/run-windows-regression.bat +++ b/fun/test/run-windows-regression.bat @@ -119,7 +119,7 @@ rem ffi-*.fun load fun/test/ffi/libffitest.(so|dylib|dll) (built on demand, see rem fun/test/ffi/build.sh / the CI) and call it through getapi(). On Windows rem getapi needs -dWinFFI plus ffi.dll next to funcmd.exe. Probe getapi rem unconditionally: the stdio/pipe snaps need only getapi, not libffitest.dll. -set "FFITESTS= ffi-int64.fun ffi-float.fun ffi-float-cb.fun ffi-callbacks.fun " +set "FFITESTS= ffi-int64.fun ffi-float.fun ffi-float-cb.fun ffi-callbacks.fun ffi-var-ref.fun " set "NATIVEIOTESTS= lib-io-stdio-out.fun lib-io-stdio-err.fun lib-io-pipe-test.fun lib-process-test.fun " set "GETAPIOK=0" set "PROBEFILE=fun\test\.getapi-probe.fun" diff --git a/src/lib/lffi.pas b/src/lib/lffi.pas index b647d2e..2aa7827 100644 --- a/src/lib/lffi.pas +++ b/src/lib/lffi.pas @@ -236,6 +236,8 @@ function CFFIFun.call(env: CEnv; exps: CExps): PValue; at: array of Pffi_type; av: array of Pointer; slots: array of QWord; + refs: array of QWord; + byref: array of Boolean; strs: array of AnsiString; ch, rt: Char; ret: QWord; @@ -249,6 +251,7 @@ function CFFIFun.call(env: CEnv; exps: CExps): PValue; n := CExps.count(exps); SetLength(at, n); SetLength(av, n); SetLength(slots, n); + SetLength(refs, n); SetLength(byref, n); SetLength(strs, n); // Build one 8-byte slot per argument; libffi reads from it. Integer/pointer @@ -262,6 +265,19 @@ function CFFIFun.call(env: CEnv; exps: CExps): PValue; av[i] := @slots[i]; e := CExp(exps.Item[i]); ch := flag[i + 1]; + // A bare `var x` with a pointer type is an OUT/INOUT parameter. win32's + // winapi.pas passes the address of the value for 'p'; do the same through + // a pointer-width scratch cell and copy the result back after the call. + // Without this, lffi sent x's value (often 0 = NULL) and the callee + // reported STATUS_INVALID_PARAMETER (e.g. BCryptOpenAlgorithmProvider). + byref[i] := (e is CRef) and (ch = 'p') + and not isStr(e.value) and (e.asObj = nil); + if byref[i] then + begin + if isNum(e.value) then refs[i] := QWord(e.value^) else refs[i] := 0; + slots[i] := QWord(fun.uintptr(@refs[i])); + Continue; + end; case ch of 's', 'a', 'w': // 'w' degrades to narrow char* on Linux (wchar_t is 4-byte UTF-32, not @@ -332,6 +348,11 @@ function CFFIFun.call(env: CEnv; exps: CExps): PValue; if rt = 'v' then ffi_call(@cif, TFnProc(hfun), nil, @av[0]) else ffi_call(@cif, TFnProc(hfun), @ret, @av[0]); // 8-byte resp buffer holds f/d too + // Write pointer-sized OUT/INOUT values back into their `var` targets. + for i := 0 to n - 1 do + if byref[i] then + CRef(CExp(exps.Item[i])).value^ := Int64(refs[i]); + case rt of 'f': p_val^ := PSingle(@ret)^; 'd': p_val^ := PDouble(@ret)^;