Repository navigation
Releases
Quillo v0.8.0
Upgrade from 0.7.0: see Security below. One visitor's data could reach another in 0.7.0,
in twelve ways, one of them a worker reading a finished request after it was freed. A key of
idempotent now runs at most once, whoever asks. Not in it, moved on: FEEDBACK43's database driver interface (Q-494 to Q-497), the
apps planned for 0.8 (the live dashboard and the others), and the studies FEEDBACK52, 54 and
56 to 59.
The fixes of FEEDBACK55.md's sweep, all of it: what a program writes into markup, the
files files() serves, HTTP's answers and cookies, highlighting and Markdown, the three
databases, components' fields, the server and the browser giving one answer, values that
are exact or an error, what runs at the same time, a program's files and Rust modules,
plurals, dates, numbers and images, and the command line: Q-700 to Q-852, and Q-875, found
while fixing (a map's order in the browser).
Security
- One viewer's
liveanswer could reach another, for up to two seconds, in 0.7.0 and
before: upgrade. Aliverun is shared between viewers unless its code read the request.
session()gave its kept copy without saying so, so the second loader of a page, or a
loader after a route that read the session, went to the next viewer: their page showed the
first viewer's data.t()andlocale()did the same with the language. Both mark the run
as the viewer's now; a loader reading nothing of the viewer's stays shared (Q-876). quillo checkwarns of aredirectorrespondnothing takes. In
if no_user { redirect("/login") }with more after it, the redirect is made and left, and
the route goes on: the visitor is shown the page. Writereply 303 redirect("/login").
Look for this in your code: two of the sample apps had it (Q-877).- Eleven more ways one visitor's data reached another are closed (an audit after
Q-876; in 0.7.0 and most in 0.6.0):- a view's cache kept a service's answer until restart, so one visitor saw another's
Authorization, and one tenant another's data (Q-878); - a tenant told by a function, or named with
with_tenant, was not in a run's key, so a
tenant saw another's rows (Q-879); requestin a parameter, pattern or lambda default went unseen, and the run was
shared (Q-880);- visitors were told apart by five fields, so
X-Api-Key,?token=orX-Tenant
reached the next visitor from the same address (Q-881); - mounted programs shared runs (Q-882);
- a worker kept a finished request, memory-unsafely: a job or a channel handler read
the last visitor's query, and a stream could go to the wrong visitor (Q-883); - kept values answered to a hash anyone could work out, so a visitor could guess another's
short secret offline (Q-884); - a kept run lost its marks, so a page lost
Cache-Control: private(Q-885); - a page showing the session with a status of 400 or more, one read through
get(request, ...)or a pattern, and a translated page had noprivateorVary
(Q-886); - an idempotency key was shared by every session without
.user(Q-887); - a run with
crypto.token,randomor a write was shared, so two visitors got one
token and a second write was skipped (Q-888).
- a view's cache kept a service's answer until restart, so one visitor saw another's
What to change in your code
- A run that read the visitor is shared with nobody, not even their other tabs, and one
that did anything but read is not shared either. A loader for everyone is one run, as
before, now per tenant too. Loaders calling a service, a site or a plugin stop sharing.
Builtins are classified: one not named is neither kept nor shared (Q-881, Q-888). - An idempotency key runs at most once, whoever asks, the user's decision: a double
charge is the worst outcome. With nothing in common the answer is 409, nothing run or
shown, also after signing out or losing a cookie. Two visitors with one key used to each
run; the second now gets 409: pass{ user }(a str or an int; a record is refused) or
use unique keys (Q-887). - Pages reading
request.headers,request.cookiesorrequest.ip, or passingrequest
on, are private, whatever their status, and so are those under middleware that reads the
visitor or cannot be read (Q-886).
Databases (each driver hides its database's quirks, so a value means the same on all three;
the user's decision, Q-736):
- NaN and the infinities are refused as values on every database, naming them (
value 1 is NaN, which cannot be stored): SQLite stored NaN as null, PostgreSQL kept them.-0.0
is stored and read as0.0everywhere (Q-735). last_idis the one inserted row's id, elsenull: after an insert of several rows
(SQLite gave the last, MySQL the first), an upsert that updated, anupdate(SQLite gave an
old one) or a read. It isnullon PostgreSQL as before:returning idthere (Q-753).changesis the rows matched on MySQL too, as on SQLite and PostgreSQL: anupdate
that sets the value a row has counts it, and an upsert's or areplace's row counts once
(?client_found_rows=falsegives MySQL's own count).changesis 0 for a read and a
schema statement, and a script's last statement's (Q-739, Q-753).- SQL with no statement (
"", spaces, a comment) is an error value, fromdb.exec,
db.queryanddb.one(Q-753). - An error value given as a parameter is given back, not stored as its JSON text, and a
function is refused (Q-727). Placeholders given no values are refused on SQLite, which
stored nulls (Q-729). A map given to SQL with no named placeholder is no value, and a
map for a lone?is that value, on all three (Q-747, Q-748). - On MySQL,
@namewith a map holdingnameis refused: it is the session's variable
(null); write:name. SQLite's@name,$nameand#nametake the map's plainname
(Q-752). - MySQL and MariaDB: a statement that commits by itself (
truncate,create table,
alter, ...) is refused insidedb.transactionanddb.exec_many, and one that could
not be seen beforehand ends the transaction with an error naming it, where everything
before it was committed (Q-728, the user's decision: MySQL only). Migrations take them as
before. - MySQL and MariaDB:
db.openof a database that is not there is an error, as on
PostgreSQL; it made an empty one. A tenant's is still made, by the driver (Q-736). - MySQL and MariaDB values:
BIT(n)reads as its digits ("00000101") and takes them
back (Q-741); ids and unsigned values past 2^63 are decimals, not negative or floats
(Q-740, Q-756);tinyint(1)is a bool only for 0 and 1 (Q-756);FLOATreads as the
shortest float that prints the same, and a value it would keep as 0 is refused (Q-744,
Q-734); a statement pastmax_allowed_packetis refused saying so (Q-751);||joins
text on MariaDB, in migrations and scripts too, where it wasor. - PostgreSQL values: whole numbers in JSON past an int are decimals, and a JSON value that
cannot be read is an error in its own cell, not a failed query (Q-732); a str written as a
json/jsonbcolumn's value is the JSON it holds, sojson.stringify(m)stores an object
(Q-733); a float pastreal's ordouble precision's range is refused (Q-734);real
reads as the shortest float (0.1) (Q-744); BC dates read as ISO 8601 (-0043-03-15) and
go back in (Q-743); a type with no reader is hex in every row, andtimetz,pg_lsn,
xid,xid8read as PostgreSQL writes them (Q-742);bit(n)keeps its length (Q-730); an
array not starting at 1 comes back as its text, keeping its bounds (Q-756); a value with no
type in the SQL is declaredint4,int8orfloat8, soselect ? as vwith 5 is5,
not"5", andgreatest(10, 9)is 10 (Q-731). - PostgreSQL:
set_config(..., false)is refused outside a transaction in every form
(with values, insidevalues (...), in anupdate, any case or quoting), and so are
pg_temptables;doblocks and calls of the database's own functions run on a connection
of their own, reset after; every transaction's connection is reset as it ends (Q-708). db.transactionrefuses an option it does not have, at run time and inquillo check
(has no option `isolaton` (did you mean `isolation`?)); a misspelt key ran with none
(Q-738).- The guide's column-list update,
set (${sql_name(cols)}) = (${sql_param(cols)}),
fails on MySQL and on PostgreSQL with one column: write the pairs,a = :a, b = :b,
built from the columns (the guide shows how) (Q-754).
Files and HTTP:
files()andquillo serveserve no SQLite database, whatever its name (by its
header,-wal,-shmor-journal): the program's, a tenant's, another's.file("x.db")
serves one on purpose, with ranges (Q-702, the user's decision).- A link inside
files()' folder is followed only through build-output and package-store
dot folders (.pnpm,.vitepress/dist,.output/public,.next/static,
.svelte-kit/output/client,.nuxt/dist/client,.vercel/output/static,.well-known,
andnode_modules'.pnpm,.deno,.bun,.store); a file is judged by where it leads,
and the program's own files (.env,--env-file,--tls-key, loaded sources, open
databases) by device and inode too, so a hard link to one is a miss (Q-701). write_file,save_upload,delete_fileandimage.convertrefuse the program's own
files in any case and Unicode form, and as Windows reads names, on every system (.ENV
was written, then read as the settings) (Q-704);delete_filerefuses a folder that holds
an open database (Q-705);check_programreads only.qwefiles (and.qwer/.rs
signatures), no dot paths, and its messages show no file's text (Q-703).- A header's
nullinrespond,streamandeventsis no such header, Quillo's own
default included (`{ "cache-control": null }...
Quillo v0.7.0
Quillo 0.7.0 is the release where programs grow up: files have modules, money is a type, a server can live on shared hosting, and services authenticate one another. It also carries the fixes from the review reports since 0.6.0, FEEDBACK27 to FEEDBACK53, among them a full security review (FEEDBACK51, 37 issues). The test suite is 2,689 tests; the tests run 98.69% of the Rust lines and 99.86% of the browser runtime's.
Read the full changelog for every change, and Upgrading from 0.6.0 below before you update a running program.
Highlights
Modules. A file sees its own names, the builtins, and what it takes: use lib/forum takes a file's public names, import lib/forum use latest, Feed only those. quillo fix main.qwe writes the imports a program needs. For now a name a file did not take still works, with a warning, so a program can move one file at a time.
Money. money is a type, held to money.of's rules wherever a value is read. a + b on two amounts adds them, and two currencies give an error value. money.parse(text, currency, locale) reads "1.234,56 €" as a German visitor writes it, and money.format writes what Intl.NumberFormat writes, for 122 locales, the same on the server and in the browser.
Shared hosting. quillo run --listen unix:PATH and quillo fcgi run behind Apache and cPanel. A policy file limits what a program may do. The Linux binaries are now linked against glibc 2.17, so they start on CentOS 7, CloudLinux 7 and 8, AlmaLinux 8 and anything newer.
Services. A service can be another program, possibly on another machine.
- A caller proves who it is with a key of its own, a bearer or OAuth token, a JWT or a client certificate.
server fn f() for ["shop"]takes calls from those callers only.- An app can call a service whose source it does not hold.
Pages and components.
value={state}is two-way, andonclick={...}is a handler, as in React.- Markup is read as JSX and HTML write it.
defer(...)streams a slow part of a page after the rest.use Chart from "https://..."brings a browser library in by name.- Prefetching is on for links.
- Live data holds a thousand open pages.
<form idempotent>runs a form sent twice once.
Also new.
- Phone apps, in preview: a program inside an iOS or Android app.
db.exec_many.- Query timings at
/_quillo/health/queries. --max-connections,--connections-per-clientand--allowed-hosts.- Editors: the VS Code extension, and a tree-sitter grammar for Zed, Neovim, Helix, Sublime and Emacs, in quillolang/editors.
quillo lspoffers a file's names as animportline is typed.
Security
A full review of Quillo found 37 issues (FEEDBACK51). Every one is fixed, a few of them in part, as the changelog says. The ones a running program is most likely to notice:
- Only a
sitedeclaration makes a site. A map with a$sitekey no longer turns a method call into a request. - Channels and media streams are each tenant's own. A stream is watched and sent only as its
media.onrules say. navigateandredirectrefusejavascript:URLs.<style>and<script>values cannot make markup inside<svg>or<math>. CSS values incsstemplates and style maps cannot add rules.http.*follows each redirect itself. The program's headers stay with their origin, andpublic_onlyis checked at every hop.- Middleware fails closed (below), and an
accept_callscheck that gives null refuses the caller. run_scriptgets Quillo's own builtins and gives back no function. JSON has a budget of values, and comparing, printing or writing a deeply nested value no longer overflows the stack.
Upgrading from 0.6.0
These change what an existing program does. The changelog lists them all.
- Middleware gives back the request to let a route go on.
nullnow means the route does not match, so the next route is tried, and with none it is a 404.falseanswers 403. Any other value, such astrue, text or a map like{ error: "..." }, is a 500 that names the middleware. A middleware that only logs ends withr. - Channels and media are per tenant. Pages on different tenants no longer share a room. Use
{ tenants: "shared" }for one room for every tenant. Addmedia.on(name, { watch })for every stream pages watch, an RTP camera's included, andpublishfor every stream pages send. db.exec("begin")and the like are refused. Writedb.transaction(() => ...).- Typed path, query and form values take one spelling.
05,+5andNaNno longer matchintorfloat. - Markup follows JSX and HTML.
- An attribute's
"text"is plain. onChangeruns on each keystroke.{true}shows nothing.- A number in a
stylemap is in pixels.
- An attribute's
- A Rust file says its effects. It needs
//! effects: ...to be called inside awithfunction. /_quillo/readyshows each check only with the health token (QUILLO_HEALTH_TOKEN). Without it, it says only whether the server is ready.- A static file from a visitor's path goes through
files("public", path).quillo checknow warns aboutfile("public/" + path). - Under
--watch, the server answers onlylocalhost, IP addresses and--allowed-hosts.
Downloads
| System | File |
|---|---|
| Linux, x86-64 (glibc 2.17 and newer) | quillo-v0.7.0-x86_64-unknown-linux-gnu.tar.gz |
| Linux, ARM (glibc 2.17 and newer) | quillo-v0.7.0-aarch64-unknown-linux-gnu.tar.gz |
| macOS, Apple silicon | quillo-v0.7.0-aarch64-apple-darwin.tar.gz |
| macOS, Intel | quillo-v0.7.0-x86_64-apple-darwin.tar.gz |
| Windows | quillo-v0.7.0-x86_64-pc-windows-msvc.zip |
Each has a .sha256 beside it: shasum -a 256 -c quillo-v0.7.0-aarch64-apple-darwin.tar.gz.sha256.
The macOS Apple silicon build was made on an Apple silicon Mac from this tag's source (git archive v0.7.0, cargo build --release --locked), as GitHub's macOS runners gave its job none three times; the other four are the release workflow's.
To start a new program, use the starter template. Its get-quillo.sh fetches this release and checks it.
Documentation
- The guide for 0.7.0, also at quillolang.org/docs.
- The function reference.
- For AI coding assistants: quillolang.org/v0.7.0/llms.txt.
Not in this release
These move to 0.8:
- Database drivers as plugins (FEEDBACK43, Q-494 to Q-497).
- The apps planned for 0.7, starting with the live dashboard.
Two security items stay partly open, as the changelog says:
- Islands are not authenticated (Q-667). Visitor HTML must not keep
data-qiand the other island attributes. - Very deep values (Q-661). Dropping a value nested about a million levels deep can still overflow the stack.
Quillo v0.6.0
Full Changelog: https://github.com/quillolang/quillo/commits/v0.6.0