Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions .github/workflows/regression.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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'
Expand Down
24 changes: 18 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.**
Expand Down Expand Up @@ -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.

---

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -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
Expand Down
22 changes: 16 additions & 6 deletions README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
一句话定位:**Fun 是一门从 JSON 一路写到机器码的脚本语言。**

- 用同一个运行时,覆盖从高层数据处理(JSON/FD)到底层系统编程(FFI、运行时 C、JIT、机器码)的全谱系;
- 自带 78 个用 Fun 自身编写的标准库模块,全栈能力打包进单个可嵌入的 `fun.dll`;
- 自带 79 个用 Fun 自身编写的标准库模块,全栈能力打包进单个可嵌入的 `fun.dll`;
- 在信创与 AI Agent 场景中,作为连接国产 OS、数据库、芯片与业务逻辑的轻量级胶水。

Fun 的设计出发点很简单:**数据与代码都应该是头等公民,而且一段脚本应该能在不离开语言的情况下一路触达机器底层。**
Expand Down Expand Up @@ -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 与原生绑定。

---

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -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
Expand Down
143 changes: 140 additions & 3 deletions fun/lib/auth/util.fun
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '';
Expand All @@ -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 = '';
Expand Down
Loading
Loading