51 Commits

Author SHA1 Message Date
f4f560b164 Let's call this 0.0.14. Cut some apps to squeeze in under 5MB.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4702 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-29 19:33:46 +00:00
14b7f9237b A uv_connect_t is not a handle that can be closed. Fixes a crash.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4701 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-29 18:58:58 +00:00
f3518b3d0f Fix some broken tooltips.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4700 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-29 18:12:14 +00:00
7964524e0a Fix websocket unmasking issues. Autotest works with C httpd, now.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4699 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-29 17:45:07 +00:00
8ab8335baa This is exchanging some websocket messages, now.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4698 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-25 23:50:55 +00:00
cd43bf9dfa Bugs galore, but this is sending and receiving some websocket messages.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4697 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-25 23:39:16 +00:00
ccebf831e7 A bit closer to websockets.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4696 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-25 22:53:05 +00:00
9f2f9bd8b0 Fixed some package math.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4695 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-24 22:09:09 +00:00
adf8c14536 Saw a websocket message go across the wire with this.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4694 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-24 22:06:11 +00:00
606e82d718 Saw a websocket message go across the wire with this.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4693 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-24 21:39:51 +00:00
1621f1753a WebSocket request/response header dance. Feels like the loop is getting close to closed, but I want to refactor everything.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4692 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-24 17:43:33 +00:00
196ab66e14 Treat the ?query string and body the same as httpd.js does. Now I can auth.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4691 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-23 19:52:59 +00:00
38ab32dad9 Fix some http request lifetime issues, and follow the same lowercase convention for headers.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4690 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-21 17:45:06 +00:00
86046e52f0 One less dynamic http allocation. Also one less crash.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4689 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-21 17:15:59 +00:00
9e7c860414 Compile fix.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4688 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-21 02:06:17 +00:00
7dc8b86ee2 Return legit responses for some static files.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4687 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-21 02:04:20 +00:00
6ecbfe3de6 Sort of barely starting to call httpd callbacks with the new implementation.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4686 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-21 01:27:57 +00:00
f9940fc436 Add a runtime switch between httpd implementions. One of which is totally not hooked up yet.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4685 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-21 00:56:16 +00:00
58e75ee276 I think we did some keep-alive.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4684 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-21 00:13:03 +00:00
e7771f539d Now we're uploading some data.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4683 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-21 00:00:15 +00:00
c2f62cd8e0 Now we're uploading some data.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4682 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-20 23:58:28 +00:00
f4b6812675 Auto-add a content-length header.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4681 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-20 23:13:03 +00:00
03e4b37c04 Make the http test complete successfully.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4680 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-18 17:51:15 +00:00
7b3a9e0f63 Send a valid HTTP response and shutdown the connection.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4679 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-17 17:44:54 +00:00
067f546580 Send a canned HTTP response.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4678 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-14 01:59:23 +00:00
2f7697b7ec These colors were bugging me.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4677 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-14 00:02:18 +00:00
1d214f89ed Work in progress HTTP server in C.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4676 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-13 23:59:11 +00:00
0b47207949 Show selected and hovered items in the wiki table of contents.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4675 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-12 17:38:10 +00:00
94dd573a81 Show a tree of wikis and docs in the wiki app.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4674 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-11 17:48:08 +00:00
6fa4896155 Fix OpenBSD.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4673 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-10 23:16:00 +00:00
28c99f9d8b We often have alloca.h.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4672 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-10 23:07:05 +00:00
88fbb5f73b Unused code.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4671 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-10 01:50:33 +00:00
402c185dd4 Let's be clear about our C standard.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4670 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-10 00:49:53 +00:00
ae2015a604 Add the blog app.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4669 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-09 20:25:49 +00:00
023731fc3f Make the wiki app produce the blog title.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4668 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-09 19:35:41 +00:00
99998aac8a Fixed some publicWebHostring UI issues in the ssb app.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4667 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-09 19:26:33 +00:00
360d0bc110 Let the wiki app post blog messages.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4666 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-09 19:13:06 +00:00
817838e522 Call out a summary and thumbnail for wiki pages, for the purpose of using in blog posts.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4665 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-09 18:35:42 +00:00
deb3cfb4b6 quickjs-2023-12-09.tar.xz with Haiku+OpenBSD tweaks.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4664 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-09 15:18:26 +00:00
af61519632 Compile fix.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4663 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-07 02:29:30 +00:00
b1714cf554 I think I fixed following calculations, again. Revived the test, though it's still very not thorough.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4662 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-07 02:28:49 +00:00
f0984b19f2 Encrypt wiki docs to all editors..
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4661 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-06 17:48:44 +00:00
eb3c9cd6f3 Compile fix.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4660 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-06 00:52:47 +00:00
e677b0ac3c Fix tf_min and some crashes in trace. Wow.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4659 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-06 00:40:34 +00:00
dd909bfe53 Give some feedback about encrypted wiki pages.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4658 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-05 23:39:45 +00:00
b13b111614 Make privateness of wiki pages sticky.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4657 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-04 17:50:13 +00:00
5511530926 Fix a mention of a renamed make target.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4656 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-03 18:06:50 +00:00
5e1ef01bc0 Support pasting and showing images in the wiki.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4655 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-03 18:03:42 +00:00
a060eadab7 ios build is part of the makefile.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4654 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-03 17:12:52 +00:00
70db31bb8f Add an index_map which can be used to redirect different hostnames to different app paths so that I can host multiple domains of the same device.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4653 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-03 17:03:17 +00:00
1292775a75 Now we're 0.0.14-wip.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4652 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-03 16:29:49 +00:00
54 changed files with 5293 additions and 3722 deletions

View File

@ -3,9 +3,9 @@
MAKEFLAGS += --warn-undefined-variables MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules MAKEFLAGS += --no-builtin-rules
VERSION_CODE := 13 VERSION_CODE := 14
VERSION_NUMBER := 0.0.13 VERSION_NUMBER := 0.0.14
VERSION_NAME := Served on grilled naan or gluten free sweet potato flatbread. VERSION_NAME := Served on apple cider dressed winter greens.
PROJECT = tildefriends PROJECT = tildefriends
BUILD_DIR ?= out BUILD_DIR ?= out
@ -42,6 +42,7 @@ $(error Unexpected host platform $(UNAME_S).)
endif endif
CFLAGS += \ CFLAGS += \
-std=gnu11 \
-Wall \ -Wall \
-Wextra \ -Wextra \
-Wno-unused-parameter \ -Wno-unused-parameter \
@ -448,8 +449,10 @@ $(SODIUM_OBJS): CFLAGS += \
-Wno-attributes \ -Wno-attributes \
-Ideps/libsodium/builds/msvc \ -Ideps/libsodium/builds/msvc \
-Ideps/libsodium/src/libsodium/include/sodium -Ideps/libsodium/src/libsodium/include/sodium
$(SODIUM_OBJS_unix): CFLAGS += \ ifneq ($(UNAME_S),OpenBSD)
$(filter-out $(BUILD_DIR)/win%,$(SODIUM_OBJS)): CFLAGS += \
-DHAVE_ALLOCA_H -DHAVE_ALLOCA_H
endif
SQLITE_SOURCES := deps/sqlite/sqlite3.c SQLITE_SOURCES := deps/sqlite/sqlite3.c
SQLITE_OBJS := $(call get_objs,SQLITE_SOURCES) SQLITE_OBJS := $(call get_objs,SQLITE_SOURCES)
@ -700,7 +703,7 @@ PACKAGE_DIRS := \
deps/codemirror/ \ deps/codemirror/ \
deps/lit/ deps/lit/
RAW_FILES := $(filter-out apps/gg% apps/welcome% %.map, $(shell find $(PACKAGE_DIRS) -type f)) RAW_FILES := $(filter-out apps/blog% apps/gg% apps/issues% apps/welcome% apps/journal% %.map, $(shell find $(PACKAGE_DIRS) -type f))
out/apk/TildeFriends-arm-debug.unsigned.apk: BUILD_TYPE := debug out/apk/TildeFriends-arm-debug.unsigned.apk: BUILD_TYPE := debug
out/apk/TildeFriends-arm-release.unsigned.apk: BUILD_TYPE := release out/apk/TildeFriends-arm-release.unsigned.apk: BUILD_TYPE := release
@ -722,19 +725,19 @@ out/apk/TildeFriends-arm-%.unsigned.apk:
@cp out/apk/res.apk $@ @cp out/apk/res.apk $@
@cp out/apk/classes.dex out/apk-arm-$(BUILD_TYPE)/ @cp out/apk/classes.dex out/apk-arm-$(BUILD_TYPE)/
@cd out/apk-arm-$(BUILD_TYPE) && zip -u ../../$@ -q -9 -r . && cd ../../ @cd out/apk-arm-$(BUILD_TYPE) && zip -u ../../$@ -q -9 -r . && cd ../../
@zip -u $@ -q -9 -x '*.map' --exclude=apps/gg* --exclude=apps/welcome* -r $(PACKAGE_DIRS) $(RAW_FILES) @zip -u $@ -q -9 $(RAW_FILES)
out/apk/TildeFriends-x86-%.unsigned.apk: out/apk/TildeFriends-x86-%.unsigned.apk:
@mkdir -p $(dir $@) out/apk-x86-$(BUILD_TYPE)/lib/x86_64/ out/apk-x86-$(BUILD_TYPE)/lib/x86/ @mkdir -p $(dir $@) out/apk-x86-$(BUILD_TYPE)/lib/x86_64/ out/apk-x86-$(BUILD_TYPE)/lib/x86/
@echo [aapt] $@ @echo [aapt] $@
@cp out/android$(BUILD_TYPE)-x86_64/tildefriends out/apk-x86-$(BUILD_TYPE)/lib/x86_64/ @cp out/android$(BUILD_TYPE)-x86_64/tildefriends out/apk-x86-$(BUILD_TYPE)/lib/x86_64/tildefriends.so
@cp out/android$(BUILD_TYPE)-x86/tildefriends out/apk-x86-$(BUILD_TYPE)/lib/x86/ @cp out/android$(BUILD_TYPE)-x86/tildefriends out/apk-x86-$(BUILD_TYPE)/lib/x86/tildefriends.so
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-x86-$(BUILD_TYPE)/lib/x86_64/tildefriends @$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-x86-$(BUILD_TYPE)/lib/x86_64/tildefriends.so
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-x86-$(BUILD_TYPE)/lib/x86/tildefriends @$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-x86-$(BUILD_TYPE)/lib/x86/tildefriends.so
@cp out/apk/res.apk $@ @cp out/apk/res.apk $@
@cp out/apk/classes.dex out/apk-x86-$(BUILD_TYPE)/ @cp out/apk/classes.dex out/apk-x86-$(BUILD_TYPE)/
@cd out/apk-x86-$(BUILD_TYPE) && zip -u ../../$@ -q -9 -r . && cd ../../ @cd out/apk-x86-$(BUILD_TYPE) && zip -u ../../$@ -q -9 -r . && cd ../../
@zip -u $@ -q -9 -x '*.map' --exclude=apps/gg* --exclude=apps/welcome* -r $(PACKAGE_DIRS) $(RAW_FILES) @zip -u $@ -q -9 $(RAW_FILES)
out/%.apk: out/apk/%.unsigned.apk out/%.apk: out/apk/%.unsigned.apk
@echo [apksigner] $(notdir $@) @echo [apksigner] $(notdir $@)

View File

@ -17,7 +17,7 @@ Scuttlebutt, as well as a platform for writing and running web applications.
generated in a subdirectory of `out/`. generated in a subdirectory of `out/`.
3. It's possible to build for Android, iOS, and Windows on Linux, if you have 3. It's possible to build for Android, iOS, and Windows on Linux, if you have
the right dependencies in the right places. `make windebug winrelease the right dependencies in the right places. `make windebug winrelease
iosdebug-ipa iosrelease-ipa apk`. iosdebug-ipa iosrelease-ipa release-apk`.
4. To build in docker, `docker build .`. 4. To build in docker, `docker build .`.
## Running ## Running

5
apps/blog.json Normal file
View File

@ -0,0 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "🪵",
"previous": "&YHBylHM7DlDDiGfuNj+g95Bf7NFxTZs/IKG14TbWnhs=.sha256"
}

8
apps/blog/app.js Normal file
View File

@ -0,0 +1,8 @@
import * as blog from './blog.js';
async function main() {
let blogs = await blog.get_posts();
await app.setDocument(blog.render_html(blogs));
}
main();

149
apps/blog/blog.js Normal file
View File

@ -0,0 +1,149 @@
import * as commonmark from './commonmark.min.js';
function escape(text) {
return (text ?? '').replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');
}
function escapeAttribute(text) {
return (text ?? '').replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;').replaceAll("'", '&#39;');
}
function markdown(md) {
let reader = new commonmark.Parser({safe: true});
let writer = new commonmark.HtmlRenderer();
let parsed = reader.parse(md || '');
let walker = parsed.walker();
let event, node;
while ((event = walker.next())) {
node = event.node;
if (event.entering) {
if (node.type == 'image') {
if (node.destination.startsWith('&')) {
node.destination = '/' + node.destination + '/view';
}
}
}
}
return writer.render(parsed);
}
export async function render_blog_post_html(blog_post) {
let blob = utf8Decode(await ssb.blobGet(blog_post.blog));
return `<!DOCTYPE html>
<html>
<body>
<div>
<div><a href="../ssb/#${escapeAttribute(blog_post.author)}">${escape(blog_post.name)}</a> ${escape(new Date(blog_post.timestamp).toString())}</div>
<div>${markdown(blob)}</div>
</div>
</body>
</html>
`;
}
function render_blog_post(blog_post) {
return `
<div>
<h2><a href="../ssb/#${escapeAttribute(blog_post.id)}">${escape(blog_post.title)}</a></h2>
<div><a href="../ssb/#${escapeAttribute(blog_post.author)}">${escape(blog_post.name)}</a> ${escape(new Date(blog_post.timestamp).toString())}</div>
<div>${markdown(blog_post.summary)}</div>
</div>
`;
}
export function render_html(blogs) {
return `<!DOCTYPE html>
<html>
<head>
<title>🪵Tilde Blog</title>
<link href="./atom" type="application/atom+xml" rel="alternate" title="🪵Tilde Blog"/>
<style>
html {
background-color: #ccc;
}
</style>
<base target="_blank">
</head>
<body>
<div style="display: flex; flex-direction: row; align-items: center; gap: 1em">
<h1>🪵Tilde Blog</h1>
<div style="font-size: xx-small; vertical-align: middle"><a href="/~cory/blog/atom">atom feed</a></div>
</div>
${blogs.map(blog_post => render_blog_post(blog_post)).join('\n')}
</body>
</html>`;
}
function render_blog_post_atom(blog_post) {
return `<entry>
<title>${escape(blog_post.title)}</title>
<link href="https://tildefriends.net/~cory/ssb/#${blog_post.id}" />
<id>${blog_post.id}</id>
<published>${escape(new Date(blog_post.timestamp).toString())}</published>
<summary>${escape(blog_post.summary)}</summary>
<author>
<name>${escape(blog_post.name)}</name>
<feed>${escape(blog_post.author)}</feed>
</author>
</entry>`;
}
export function render_atom(blogs) {
return `<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>🪵Tilde Blog</title>
<subtitle>A subtitle.</subtitle>
<link href="https://tildefriends.net/~cory/blog/atom" rel="self"/>
<link href="https://tildefriends.net/~cory/blog/"/>
<id>https://www.tildefriends.net/~cory/blog/</id>
<updated>${new Date().toString()}</updated>
${blogs.map(blog_post => render_blog_post_atom(blog_post)).join('\n')}
</feed>`;
}
export async function get_posts() {
let blogs = [];
await ssb.sqlAsync(`
WITH
blogs AS (
SELECT
messages.author,
messages.id,
json_extract(messages.content, '$.title') AS title,
json_extract(messages.content, '$.summary') AS summary,
json_extract(messages.content, '$.blog') AS blog,
messages.timestamp
FROM messages_fts('blog')
JOIN messages ON messages.rowid = messages_fts.rowid
WHERE json_extract(messages.content, '$.type') = 'blog'),
public AS (
SELECT author FROM (
SELECT
messages.author,
RANK() OVER (PARTITION BY messages.author ORDER BY messages.sequence DESC) AS author_rank,
json_extract(messages.content, '$.publicWebHosting') AS is_public
FROM messages_fts('about')
JOIN messages ON messages.rowid = messages_fts.rowid
WHERE json_extract(messages.content, '$.type') = 'about' AND is_public IS NOT NULL)
WHERE author_rank = 1 AND is_public),
names AS (
SELECT author, name FROM (
SELECT
messages.author,
RANK() OVER (PARTITION BY messages.author ORDER BY messages.sequence DESC) AS author_rank,
json_extract(messages.content, '$.name') AS name
FROM messages_fts('about')
JOIN messages ON messages.rowid = messages_fts.rowid
WHERE json_extract(messages.content, '$.type') = 'about' AND
json_extract(messages.content, '$.about') = messages.author AND
name IS NOT NULL)
WHERE author_rank = 1)
SELECT blogs.*, names.name FROM blogs
JOIN public ON public.author = blogs.author
LEFT OUTER JOIN names ON names.author = blogs.author
ORDER BY blogs.timestamp DESC LIMIT 20
`, [], function(row) {
blogs.push(row);
});
return blogs;
}

1
apps/blog/commonmark.min.js vendored Normal file

File diff suppressed because one or more lines are too long

22
apps/blog/handler.js Normal file
View File

@ -0,0 +1,22 @@
import * as blog from './blog.js';
async function main() {
let blogs = await blog.get_posts();
for (let blog_post of blogs) {
let title = (blog_post.title || '').replaceAll(/\W/g, '_').toLowerCase();
if (request.path === title) {
respond({data: await blog.render_blog_post_html(blog_post), content_type: 'text/html; charset=utf-8'});
return;
}
}
if (request.path == 'atom') {
respond({data: blog.render_atom(blogs), content_type: 'application/atom+xml'});
} else {
respond({data: blog.render_html(blogs), content_type: 'text/html; charset=utf-8'});
}
}
main().catch(function(error) {
respond({data: `<!DOCTYPE html>
<pre style="color: #f00">${error.message}\n${error.stack}</pre>`, content_type: 'text/html'});
});

126
apps/blog/lit-all.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
{ {
"type": "tildefriends-app", "type": "tildefriends-app",
"emoji": "🐌", "emoji": "🐌",
"previous": "&vIYnoUkbz97WRvyunV+ETe+Y6tJk7tTEVvgYuwkoDiM=.sha256" "previous": "&dO6ckMIPVv9QvSc+0TOg0S59qe+rirPo2a6p9xSHj9M=.sha256"
} }

View File

@ -106,6 +106,7 @@ class TfProfileElement extends LitElement {
name: original.name, name: original.name,
description: original.description, description: original.description,
image: original.image, image: original.image,
publicWebHosting: original.publicWebHosting,
}; };
console.log(this.editing); console.log(this.editing);
} }
@ -220,7 +221,7 @@ class TfProfileElement extends LitElement {
<textarea style="flex: 1 0" id="description" @input=${event => this.editing = Object.assign({}, this.editing, {description: event.srcElement.value})}>${this.editing.description}</textarea> <textarea style="flex: 1 0" id="description" @input=${event => this.editing = Object.assign({}, this.editing, {description: event.srcElement.value})}>${this.editing.description}</textarea>
<div> <div>
<label for="public_web_hosting">Public Web Hosting:</label> <label for="public_web_hosting">Public Web Hosting:</label>
<input type="checkbox" id="public_web_hosting" value=${this.editing.public_web_hosting} @input=${event => this.editing = Object.assign({}, this.editing, {publicWebHosting: event.srcElement.checked})}></input> <input type="checkbox" id="public_web_hosting" ?checked=${this.editing.publicWebHosting} @input=${event => self.editing = Object.assign({}, self.editing, {publicWebHosting: event.srcElement.checked})}></input>
</div> </div>
<div> <div>
<input type="button" value="Attach Image" @click=${this.attach_image}></input> <input type="button" value="Attach Image" @click=${this.attach_image}></input>

View File

@ -1,5 +1,5 @@
{ {
"type": "tildefriends-app", "type": "tildefriends-app",
"emoji": "📝", "emoji": "📝",
"previous": "&r+JXDhWclHwMbeZHYjueOnSPaYCAXUlnl1Gyjgw6TxM=.sha256" "previous": "&JHopifgZn2TsiMCQY8HUTlDqHEDDviiu2ifvr8HHNwo=.sha256"
} }

View File

@ -240,6 +240,14 @@ class TfCollectionsAppElement extends LitElement {
render() { render() {
let self = this; let self = this;
return html` return html`
<style>
.toc:hover {
background-color: #0cc;
}
.toc.selected {
background-color: #088;
}
</style>
<div> <div>
<tf-id-picker .ids=${this.ids} selected=${this.whoami} @change=${this.on_whoami_changed} ?hidden=${!this.ids?.length}></tf-id-picker> <tf-id-picker .ids=${this.ids} selected=${this.whoami} @change=${this.on_whoami_changed} ?hidden=${!this.ids?.length}></tf-id-picker>
</div> </div>
@ -278,12 +286,24 @@ class TfCollectionsAppElement extends LitElement {
</div> </div>
</div> </div>
</div> </div>
<div style="display: flex; flex-direction: row">
<div style="flex: 0 0">
${Object.values(this.wikis || {}).sort((x, y) => x.name.localeCompare(y.name)).map(wiki => html`
<div class="toc ${self.wiki?.id === wiki.id ? 'selected' : ''}" style="white-space: nowrap; cursor: pointer" @click=${() => self.on_wiki_changed({detail: {value: wiki}})}>${wiki.name}</div>
<ul>
${Object.values(self.wiki_docs || {}).filter(doc => doc.parent === wiki?.id).sort((x, y) => x.name.localeCompare(y.name)).map(doc => html`
<li class="toc ${self.wiki_doc?.id === doc.id ? 'selected' : ''}" style="white-space: nowrap; cursor: pointer" @click=${() => self.on_wiki_doc_changed({detail: {value: doc}})}>${doc.name}</li>
`)}
</ul>
`)}
</div>
${this.wiki_doc && this.wiki_doc.parent === this.wiki?.id ? html` ${this.wiki_doc && this.wiki_doc.parent === this.wiki?.id ? html`
<tf-wiki-doc <tf-wiki-doc
whoami=${this.whoami} whoami=${this.whoami}
.wiki=${this.wiki} .wiki=${this.wiki}
.value=${this.wiki_doc}></tf-wiki-doc> .value=${this.wiki_doc}></tf-wiki-doc>
` : undefined} ` : undefined}
</div>
`; `;
} }
} }

View File

@ -6,7 +6,7 @@ class TfWikiDocElement extends LitElement {
static get properties() { static get properties() {
return { return {
whoami: {type: String}, whoami: {type: String},
wiki: {type: String}, wiki: {type: Object},
value: {type: Object}, value: {type: Object},
blob: {type: String}, blob: {type: String},
blob_original: {type: String}, blob_original: {type: String},
@ -20,9 +20,9 @@ class TfWikiDocElement extends LitElement {
} }
markdown(md) { markdown(md) {
var reader = new commonmark.Parser({safe: true}); let reader = new commonmark.Parser({safe: true});
var writer = new commonmark.HtmlRenderer(); let writer = new commonmark.HtmlRenderer();
var parsed = reader.parse(md || ''); let parsed = reader.parse(md || '');
let walker = parsed.walker(); let walker = parsed.walker();
let event; let event;
while ((event = walker.next())) { while ((event = walker.next())) {
@ -33,12 +33,51 @@ class TfWikiDocElement extends LitElement {
node.destination.indexOf('/') == -1) { node.destination.indexOf('/') == -1) {
node.destination = `#${this.wiki?.name}/${node.destination}`; node.destination = `#${this.wiki?.name}/${node.destination}`;
} }
} else if (node.type == 'image') {
if (node.destination.startsWith('&')) {
node.destination = '/' + node.destination + '/view';
}
} }
} }
} }
return writer.render(parsed); return writer.render(parsed);
} }
title(md) {
let lines = (md || '').split('\n');
for (let line of lines) {
let m = line.match(/#+ (.*)/);
if (m) {
return m[1];
}
}
}
summary(md) {
let lines = (md || '').split('\n');
let result = [];
let have_content = false;
for (let line of lines) {
if (have_content && !line.trim().length) {
return result.join('\n');
}
if (!line.startsWith('#') && line.trim().length) {
have_content = true;
}
if (!line.startsWith('#')) {
result.push(line);
}
}
return result.join('\n');
}
thumbnail(md) {
//let m = md ? md.match(/\!\[image:[^\]]+]\((\&.{44}\.sha256)\)/) : undefined;
let m = md ? md.match(/.*\((\&.{44}\.sha256)\).*/) : undefined;
console.log('thumb', m);
return m ? m[1] : undefined;
}
async load_blob() { async load_blob() {
let blob = await tfrpc.rpc.get_blob(this.value?.blob); let blob = await tfrpc.rpc.get_blob(this.value?.blob);
if (blob.endsWith('.box')) { if (blob.endsWith('.box')) {
@ -62,8 +101,8 @@ class TfWikiDocElement extends LitElement {
async append_message(draft) { async append_message(draft) {
let blob = this.blob; let blob = this.blob;
if (draft) { if (draft || this.value?.private) {
blob = await tfrpc.rpc.encrypt(this.whoami, this.value.editors, blob); blob = await tfrpc.rpc.encrypt(this.whoami, this.wiki.editors, blob);
} }
let id = await tfrpc.rpc.store_blob(blob); let id = await tfrpc.rpc.store_blob(blob);
let message = { let message = {
@ -71,13 +110,13 @@ class TfWikiDocElement extends LitElement {
key: this.value.id, key: this.value.id,
parent: this.value.parent, parent: this.value.parent,
blob: id, blob: id,
mentions: this.blob.match(/(&.{44}.sha256)/g)?.map(x => ({link: x})),
private: this.value?.private,
}; };
if (draft) { if (draft) {
message.recps = this.value.editors; message.recps = this.value.editors;
print(message);
message = await tfrpc.rpc.encrypt(this.whoami, this.value.editors, JSON.stringify(message)); message = await tfrpc.rpc.encrypt(this.whoami, this.value.editors, JSON.stringify(message));
} }
print(message);
await tfrpc.rpc.appendMessage(this.whoami, message); await tfrpc.rpc.appendMessage(this.whoami, message);
this.is_editing = false; this.is_editing = false;
} }
@ -90,6 +129,91 @@ class TfWikiDocElement extends LitElement {
return this.append_message(false); return this.append_message(false);
} }
async on_blog_publish() {
let blob = this.blob;
let id = await tfrpc.rpc.store_blob(blob);
let message = {
type: 'blog',
key: this.value.id,
parent: this.value.parent,
title: this.title(blob),
summary: this.summary(blob),
thumbnail: this.thumbnail(blob),
blog: id,
mentions: this.blob.match(/(&.{44}.sha256)/g)?.map(x => ({link: x})),
};
await tfrpc.rpc.appendMessage(this.whoami, message);
this.is_editing = false;
}
convert_to_format(buffer, type, mime_type) {
return new Promise(function(resolve, reject) {
let img = new Image();
img.onload = function() {
let canvas = document.createElement('canvas');
let width_scale = Math.min(img.width, 1024) / img.width;
let height_scale = Math.min(img.height, 1024) / img.height;
let scale = Math.min(width_scale, height_scale);
canvas.width = img.width * scale;
canvas.height = img.height * scale;
let context = canvas.getContext('2d');
context.drawImage(img, 0, 0, canvas.width, canvas.height);
let data_url = canvas.toDataURL(mime_type);
let result = atob(data_url.split(',')[1]).split('').map(x => x.charCodeAt(0));
resolve(result);
};
img.onerror = function(event) {
reject(new Error('Failed to load image.'));
};
let raw = Array.from(new Uint8Array(buffer)).map(b => String.fromCharCode(b)).join('');
let original = `data:${type};base64,${btoa(raw)}`;
img.src = original;
});
}
async add_file(editor, file) {
try {
let self = this;
let buffer = await file.arrayBuffer();
let type = file.type;
if (type.startsWith('image/')) {
let best_buffer;
let best_type;
for (let format of ['image/png', 'image/jpeg', 'image/webp']) {
let test_buffer = await self.convert_to_format(buffer, file.type, format);
if (!best_buffer || test_buffer.length < best_buffer.length) {
best_buffer = test_buffer;
best_type = format;
}
}
buffer = best_buffer;
type = best_type;
} else {
buffer = Array.from(new Uint8Array(buffer));
}
let id = await tfrpc.rpc.store_blob(buffer);
let name = type.split('/')[0] + ':' + file.name;
editor.value += `\n![${name}](${id})`;
self.on_edit({srcElement: editor});
} catch(e) {
alert(e?.message);
}
}
paste(event) {
let self = this;
for (let item of event.clipboardData.items) {
if (item.type?.startsWith('image/')) {
let file = item.getAsFile();
if (!file) {
continue;
}
self.add_file(event.srcElement, file);
break;
}
}
}
render() { render() {
let value = JSON.stringify(this.value); let value = JSON.stringify(this.value);
if (this.blob_for_value != value) { if (this.blob_for_value != value) {
@ -99,19 +223,32 @@ class TfWikiDocElement extends LitElement {
this.load_blob(); this.load_blob();
} }
let self = this; let self = this;
let thumbnail_ref = this.thumbnail(this.blob);
return html` return html`
<div style="display: inline-flex; flex-direction: row"> <div style="display: inline-flex; flex-direction: row">
<button ?disabled=${!this.whoami || this.is_editing} @click=${() => self.is_editing = true}>Edit</button> <button ?disabled=${!this.whoami || this.is_editing} @click=${() => self.is_editing = true}>Edit</button>
<button ?disabled=${this.blob == this.blob_original} @click=${this.on_save_draft}>Save Draft</button> <button ?disabled=${this.blob == this.blob_original} @click=${this.on_save_draft}>Save Draft</button>
<button ?disabled=${this.blob == this.blob_original && !this.value?.draft} @click=${this.on_publish}>Publish</button> <button ?disabled=${this.blob == this.blob_original && !this.value?.draft} @click=${this.on_publish}>Publish</button>
<button ?disabled=${!this.is_editing} @click=${this.on_discard}>Discard</button> <button ?disabled=${!this.is_editing} @click=${this.on_discard}>Discard</button>
<button ?disabled=${!this.is_editing} @click=${() => self.value = Object.assign({}, self.value, {private: !self.value.private})}>${this.value?.private ? 'Make Public' : 'Make Private'}</button>
<button ?disabled=${!this.is_editing} @click=${this.on_blog_publish}>Publish Blog</button>
</div> </div>
<div style="display: flex; flex-direction: row"> <div ?hidden=${!this.value?.private} style="color: #800">🔒 document is private</div>
<div style="display: flex; flex-direction: row; ${this.value?.private ? 'border-top: 4px solid #800' : ''}">
<textarea <textarea
?hidden=${!this.is_editing} ?hidden=${!this.is_editing}
style="flex: 1 1; min-height: 10em" style="flex: 1 1; min-height: 10em; ${this.value?.private ? 'border: 4px solid #800' : ''}"
@input=${this.on_edit} .value=${this.blob ?? ''}></textarea> @input=${this.on_edit}
<div style="flex: 1 1">${unsafeHTML(this.markdown(this.blob))}</div> @paste=${this.paste}
.value=${this.blob ?? ''}></textarea>
<div style="flex: 1 1">
<div ?hidden=${!this.is_editing} style="border: 1px solid #fff; border-radius: 1em; padding: 0.5em">
<img ?hidden=${!thumbnail_ref} style="max-width: 128px; max-height: 128px; float: right" src="/${thumbnail_ref}/view">
<h1 ?hidden=${!this.title(this.blob)}>${unsafeHTML(this.markdown(this.title(this.blob)))}</h1>
${unsafeHTML(this.markdown(this.summary(this.blob)))}
</div>
${unsafeHTML(this.markdown(this.blob))}
</div>
</div> </div>
`; `;
} }

View File

@ -120,19 +120,38 @@ class TfNavigationElement extends LitElement {
} }
} }
set_access_key_title(event) {
if (!event.srcElement.title) {
event.srcElement.title = `${event.srcElement.dataset.tip} [${(event.srcElement.accessKeyLabel || event.srcElement.accessKey).toUpperCase()}]`;
}
}
render() { render() {
let self = this; let self = this;
return html` return html`
<style> <style>
${k_global_style} ${k_global_style}
.tooltip {
position: absolute;
z-index: 1;
display: none;
border: 1px solid black;
padding: 4px;
color: black;
background: white;
}
.tooltip_parent:hover .tooltip {
display: inline-block;
}
</style> </style>
<div style="margin: 4px; display: flex; flex-direction: row; flex-wrap: nowrap; gap: 3px; align-items: center"> <div style="margin: 4px; display: flex; flex-direction: row; flex-wrap: nowrap; gap: 3px; align-items: center">
<span style="cursor: pointer" @click=${() => this.show_version = !this.show_version}>😎</span> <span style="cursor: pointer" @click=${() => this.show_version = !this.show_version}>😎</span>
<span ?hidden=${!this.show_version} style="flex: 0 0; white-space: nowrap" title=${this.version?.name + ' ' + Object.entries(this.version || {}).filter(x => ['name', 'number'].indexOf(x[0]) == -1).map(x => `\n* ${x[0]}: ${x[1]}`)}>${this.version?.number}</span> <span ?hidden=${!this.show_version} style="flex: 0 0; white-space: nowrap" title=${this.version?.name + ' ' + Object.entries(this.version || {}).filter(x => ['name', 'number'].indexOf(x[0]) == -1).map(x => `\n* ${x[0]}: ${x[1]}`)}>${this.version?.number}</span>
<a accesskey="h" data-tip="Open home app." href="/" style="color: #fff; white-space: nowrap">TF</a> <a accesskey="h" @mouseover=${this.set_access_key_title} data-tip="Open home app." href="/" style="color: #fff; white-space: nowrap">TF</a>
<a accesskey="a" data-tip="Open apps list." href="/~core/apps/">apps</a> <a accesskey="a" @mouseover=${this.set_access_key_title} data-tip="Open apps list." href="/~core/apps/">apps</a>
<a accesskey="e" data-tip="Toggle the app editor." href="#" @click=${this.toggle_edit}>edit</a> <a accesskey="e" @mouseover=${this.set_access_key_title} data-tip="Toggle the app editor." href="#" @click=${this.toggle_edit}>edit</a>
<a accesskey="p" data-tip="View and change permissions." href="#" @click=${() => self.show_permissions = !self.show_permissions}>🎛️</a> <a accesskey="p" @mouseover=${this.set_access_key_title} data-tip="View and change permissions." href="#" @click=${() => self.show_permissions = !self.show_permissions}>🎛️</a>
<span style="display: inline-block; vertical-align: top; white-space: pre; color: ${this.status.color ?? kErrorColor}">${this.status.message}</span> <span style="display: inline-block; vertical-align: top; white-space: pre; color: ${this.status.color ?? kErrorColor}">${this.status.message}</span>
<span id="requests"></span> <span id="requests"></span>
${this.render_permissions()} ${this.render_permissions()}
@ -1077,30 +1096,6 @@ window.addEventListener("load", function() {
event.preventDefault(); event.preventDefault();
trace(); trace();
}); });
for (let tag of document.getElementsByTagName('a')) {
if (tag.accessKey) {
tag.classList.add('tooltip_parent');
let tooltip = document.createElement('div');
tooltip.classList.add('tooltip');
if (tag.dataset.tip) {
let description = document.createElement('div');
description.innerText = tag.dataset.tip;
tooltip.appendChild(description);
}
let parts = tag.accessKeyLabel ? tag.accessKeyLabel.split('+') : [];
for (let i = 0; i < parts.length; i++)
{
let key = parts[i];
let kbd = document.createElement('kbd');
kbd.innerText = key;
tooltip.appendChild(kbd);
if (i < parts.length - 1) {
tooltip.appendChild(document.createTextNode('+'));
}
}
tag.appendChild(tooltip);
}
}
connectSocket(window.location.pathname); connectSocket(window.location.pathname);
if (window.localStorage.getItem('editing') == '1') { if (window.localStorage.getItem('editing') == '1') {

View File

@ -47,6 +47,11 @@ const k_global_settings = {
default_value: '/~core/apps/', default_value: '/~core/apps/',
description: 'Default path.', description: 'Default path.',
}, },
index_map: {
type: 'textarea',
default_value: undefined,
description: 'Mappings from hostname to redirect path, one per line, as in: "www.tildefriends.net=/~core/index/"',
},
room: { room: {
type: 'boolean', type: 'boolean',
default_value: true, default_value: true,
@ -947,10 +952,22 @@ function stringResponse(response, data) {
} }
loadSettings().then(function() { loadSettings().then(function() {
httpd.all("/login", auth.handler); let httpd_impl = (tildefriends.args.httpdc ? httpdc : httpd);
httpd.all("", function(request, response) { httpd_impl.all("/login", auth.handler);
httpd_impl.all("", function(request, response) {
let match; let match;
if (request.uri === "/" || request.uri === "") { if (request.uri === "/" || request.uri === "") {
try {
for (let line of (gGlobalSettings.index_map || '').split('\n')) {
let parts = line.split('=');
if (parts.length == 2 && request.headers.host.match(new RegExp(parts[0], 'i'))) {
response.writeHead(303, {"Location": (request.client.tls ? 'https://' : 'http://') + request.headers.host + parts[1], "Content-Length": "0"});
return response.end();
}
}
} catch (e) {
print(e);
}
response.writeHead(303, {"Location": (request.client.tls ? 'https://' : 'http://') + request.headers.host + gGlobalSettings.index, "Content-Length": "0"}); response.writeHead(303, {"Location": (request.client.tls ? 'https://' : 'http://') + request.headers.host + gGlobalSettings.index, "Content-Length": "0"});
return response.end(); return response.end();
} else if (match = /^(\/~[^\/]+\/[^\/]+)(\/?.*)$/.exec(request.uri)) { } else if (match = /^(\/~[^\/]+\/[^\/]+)(\/?.*)$/.exec(request.uri)) {
@ -987,7 +1004,8 @@ loadSettings().then(function() {
return response.end(data); return response.end(data);
} }
}); });
httpd.registerSocketHandler("/app/socket", app.socket); httpd_impl.registerSocketHandler("/app/socket", app.socket);
httpd_impl.start(tildefriends.http_port);
}).catch(function(error) { }).catch(function(error) {
print('Failed to load settings.'); print('Failed to load settings.');
printError({print: print}, error); printError({print: print}, error);

View File

@ -203,6 +203,7 @@ function handleWebSocketRequest(request, response, client) {
return; return;
} }
if (client) {
response.send = function(message, opCode) { response.send = function(message, opCode) {
if (opCode === undefined) { if (opCode === undefined) {
opCode = 0x2; opCode = 0x2;
@ -243,10 +244,12 @@ function handleWebSocketRequest(request, response, client) {
throw error; throw error;
} }
} }
}
response.onMessage = null; response.onMessage = null;
let extra_headers = handler.invoke(request, response); let extra_headers = handler.invoke(request, response);
if (client) {
client.read(function(data) { client.read(function(data) {
if (data) { if (data) {
let newBuffer = new Uint8Array(buffer.length + data.length); let newBuffer = new Uint8Array(buffer.length + data.length);
@ -328,6 +331,7 @@ function handleWebSocketRequest(request, response, client) {
logError(client.peerName + " - - [" + new Date() + "] " + error); logError(client.peerName + " - - [" + new Date() + "] " + error);
response.onError(error); response.onError(error);
}); });
}
let headers = { let headers = {
"Upgrade": "websocket", "Upgrade": "websocket",
@ -342,7 +346,6 @@ function handleWebSocketRequest(request, response, client) {
function webSocketAcceptResponse(key) { function webSocketAcceptResponse(key) {
let kMagic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; let kMagic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
let kAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
return base64Encode(sha1Digest(key + kMagic)); return base64Encode(sha1Digest(key + kMagic));
} }
@ -521,8 +524,9 @@ function handleConnection(client) {
let kBacklog = 8; let kBacklog = 8;
let kHost = platform() == 'haiku' ? 'localhost' : '::'; let kHost = platform() == 'haiku' ? 'localhost' : '::';
let socket = new Socket(); function start() {
socket.bind(kHost, tildefriends.http_port).then(function(port) { let socket = new Socket();
socket.bind(kHost, tildefriends.http_port).then(function(port) {
print("bound to", port); print("bound to", port);
print("checking", tildefriends.args.out_http_port_file); print("checking", tildefriends.args.out_http_port_file);
if (tildefriends.args.out_http_port_file) { if (tildefriends.args.out_http_port_file) {
@ -542,11 +546,11 @@ socket.bind(kHost, tildefriends.http_port).then(function(port) {
logError("[" + new Date() + "] accept error " + error); logError("[" + new Date() + "] accept error " + error);
} }
}); });
}).catch(function(error) { }).catch(function(error) {
logError("[" + new Date() + "] bind error " + error); logError("[" + new Date() + "] bind error " + error);
}); });
if (tildefriends.https_port) { if (tildefriends.https_port) {
let tls = {}; let tls = {};
let secureSocket = new Socket(); let secureSocket = new Socket();
secureSocket.bind(kHost, tildefriends.https_port).then(function() { secureSocket.bind(kHost, tildefriends.https_port).then(function() {
@ -588,6 +592,7 @@ if (tildefriends.https_port) {
}).catch(function(error) { }).catch(function(error) {
logError("[" + new Date() + "] bind error " + error); logError("[" + new Date() + "] bind error " + error);
}); });
}
} }
export { all, registerSocketHandler }; export { all, start, registerSocketHandler };

View File

@ -125,34 +125,6 @@ a:active {
.cyan { color: #2aa198; } .cyan { color: #2aa198; }
.green { color: #859900; } .green { color: #859900; }
.tooltip {
position: absolute;
z-index: 1;
display: none;
border: 1px solid black;
padding: 4px;
color: black;
background: white;
}
.tooltip_parent:hover .tooltip {
display: inline-block;
}
kbd {
background-color: #eee;
border-radius: 3px;
border: 1px solid #b4b4b4;
box-shadow: 0 1px 1px rgba(0, 0, 0, .2), 0 2px 0 0 rgba(255, 255, 255, .7) inset;
color: #333;
display: inline-block;
font-size: .85em;
font-weight: 700;
line-height: 1;
padding: 2px 4px;
white-space: nowrap;
}
.permissions { .permissions {
position: absolute; position: absolute;
display: block; display: block;

View File

@ -1,3 +1,11 @@
2023-12-09:
- added Object.hasOwn, {String|Array|TypedArray}.prototype.at,
{Array|TypedArray}.prototype.findLast{Index}
- BigInt support is enabled even if CONFIG_BIGNUM disabled
- updated to Unicode 15.0.0
- misc bug fixes
2021-03-27: 2021-03-27:
- faster Array.prototype.push and Array.prototype.unshift - faster Array.prototype.push and Array.prototype.unshift

10
deps/quickjs/Makefile vendored
View File

@ -47,7 +47,7 @@ prefix=/usr/local
#CONFIG_PROFILE=y #CONFIG_PROFILE=y
# use address sanitizer # use address sanitizer
#CONFIG_ASAN=y #CONFIG_ASAN=y
# include the code for BigInt/BigFloat/BigDecimal and math mode # include the code for BigFloat/BigDecimal, math mode and faster large integers
CONFIG_BIGNUM=y CONFIG_BIGNUM=y
OBJDIR=.obj OBJDIR=.obj
@ -166,11 +166,10 @@ endif
all: $(OBJDIR) $(OBJDIR)/quickjs.check.o $(OBJDIR)/qjs.check.o $(PROGS) all: $(OBJDIR) $(OBJDIR)/quickjs.check.o $(OBJDIR)/qjs.check.o $(PROGS)
QJS_LIB_OBJS=$(OBJDIR)/quickjs.o $(OBJDIR)/libregexp.o $(OBJDIR)/libunicode.o $(OBJDIR)/cutils.o $(OBJDIR)/quickjs-libc.o QJS_LIB_OBJS=$(OBJDIR)/quickjs.o $(OBJDIR)/libregexp.o $(OBJDIR)/libunicode.o $(OBJDIR)/cutils.o $(OBJDIR)/quickjs-libc.o $(OBJDIR)/libbf.o
QJS_OBJS=$(OBJDIR)/qjs.o $(OBJDIR)/repl.o $(QJS_LIB_OBJS) QJS_OBJS=$(OBJDIR)/qjs.o $(OBJDIR)/repl.o $(QJS_LIB_OBJS)
ifdef CONFIG_BIGNUM ifdef CONFIG_BIGNUM
QJS_LIB_OBJS+=$(OBJDIR)/libbf.o
QJS_OBJS+=$(OBJDIR)/qjscalc.o QJS_OBJS+=$(OBJDIR)/qjscalc.o
endif endif
@ -317,10 +316,7 @@ endif
HELLO_SRCS=examples/hello.js HELLO_SRCS=examples/hello.js
HELLO_OPTS=-fno-string-normalize -fno-map -fno-promise -fno-typedarray \ HELLO_OPTS=-fno-string-normalize -fno-map -fno-promise -fno-typedarray \
-fno-typedarray -fno-regexp -fno-json -fno-eval -fno-proxy \ -fno-typedarray -fno-regexp -fno-json -fno-eval -fno-proxy \
-fno-date -fno-module-loader -fno-date -fno-module-loader -fno-bigint
ifdef CONFIG_BIGNUM
HELLO_OPTS+=-fno-bigint
endif
hello.c: $(QJSC) $(HELLO_SRCS) hello.c: $(QJSC) $(HELLO_SRCS)
$(QJSC) -e $(HELLO_OPTS) -o $@ $(HELLO_SRCS) $(QJSC) -e $(HELLO_OPTS) -o $@ $(HELLO_SRCS)

4
deps/quickjs/TODO vendored
View File

@ -66,5 +66,5 @@ Optimization ideas:
Test262o: 0/11262 errors, 463 excluded Test262o: 0/11262 errors, 463 excluded
Test262o commit: 7da91bceb9ce7613f87db47ddd1292a2dda58b42 (es5-tests branch) Test262o commit: 7da91bceb9ce7613f87db47ddd1292a2dda58b42 (es5-tests branch)
Result: 35/75280 errors, 909 excluded, 585 skipped Result: 41/76133 errors, 1497 excluded, 8650 skipped
Test262 commit: 31126581e7290f9233c29cefd93f66c6ac78f1c9 Test262 commit: 6cbb6da9473c56d95358d8e679c5a6d2b4574efb

View File

@ -1 +1 @@
2021-03-27 2023-12-09

Binary file not shown.

41
deps/quickjs/libbf.c vendored
View File

@ -37,10 +37,12 @@
/* enable it to check the multiplication result */ /* enable it to check the multiplication result */
//#define USE_MUL_CHECK //#define USE_MUL_CHECK
#ifdef CONFIG_BIGNUM
/* enable it to use FFT/NTT multiplication */ /* enable it to use FFT/NTT multiplication */
#define USE_FFT_MUL #define USE_FFT_MUL
/* enable decimal floating point support */ /* enable decimal floating point support */
#define USE_BF_DEC #define USE_BF_DEC
#endif
//#define inline __attribute__((always_inline)) //#define inline __attribute__((always_inline))
@ -164,6 +166,21 @@ static inline slimb_t sat_add(slimb_t a, slimb_t b)
return r; return r;
} }
static inline __maybe_unused limb_t shrd(limb_t low, limb_t high, long shift)
{
if (shift != 0)
low = (low >> shift) | (high << (LIMB_BITS - shift));
return low;
}
static inline __maybe_unused limb_t shld(limb_t a1, limb_t a0, long shift)
{
if (shift != 0)
return (a1 << shift) | (a0 >> (LIMB_BITS - shift));
else
return a1;
}
#define malloc(s) malloc_is_forbidden(s) #define malloc(s) malloc_is_forbidden(s)
#define free(p) free_is_forbidden(p) #define free(p) free_is_forbidden(p)
#define realloc(p, s) realloc_is_forbidden(p, s) #define realloc(p, s) realloc_is_forbidden(p, s)
@ -236,7 +253,7 @@ int bf_set_ui(bf_t *r, uint64_t a)
a1 = a >> 32; a1 = a >> 32;
shift = clz(a1); shift = clz(a1);
r->tab[0] = a0 << shift; r->tab[0] = a0 << shift;
r->tab[1] = (a1 << shift) | (a0 >> (LIMB_BITS - shift)); r->tab[1] = shld(a1, a0, shift);
r->expn = 2 * LIMB_BITS - shift; r->expn = 2 * LIMB_BITS - shift;
} }
#endif #endif
@ -1585,7 +1602,9 @@ int bf_mul(bf_t *r, const bf_t *a, const bf_t *b, limb_t prec,
r = &tmp; r = &tmp;
} }
if (bf_resize(r, a_len + b_len)) { if (bf_resize(r, a_len + b_len)) {
#ifdef USE_FFT_MUL
fail: fail:
#endif
bf_set_nan(r); bf_set_nan(r);
ret = BF_ST_MEM_ERROR; ret = BF_ST_MEM_ERROR;
goto done; goto done;
@ -2282,11 +2301,14 @@ static int bf_pow_ui_ui(bf_t *r, limb_t a1, limb_t b,
bf_t a; bf_t a;
int ret; int ret;
#ifdef USE_BF_DEC
if (a1 == 10 && b <= LIMB_DIGITS) { if (a1 == 10 && b <= LIMB_DIGITS) {
/* use precomputed powers. We do not round at this point /* use precomputed powers. We do not round at this point
because we expect the caller to do it */ because we expect the caller to do it */
ret = bf_set_ui(r, mp_pow_dec[b]); ret = bf_set_ui(r, mp_pow_dec[b]);
} else { } else
#endif
{
bf_init(r->ctx, &a); bf_init(r->ctx, &a);
ret = bf_set_ui(&a, a1); ret = bf_set_ui(&a, a1);
ret |= bf_pow_ui(r, &a, b, prec, flags); ret |= bf_pow_ui(r, &a, b, prec, flags);
@ -5392,21 +5414,6 @@ int bf_acos(bf_t *r, const bf_t *a, limb_t prec, bf_flags_t flags)
#endif /* LIMB_BITS != 64 */ #endif /* LIMB_BITS != 64 */
static inline __maybe_unused limb_t shrd(limb_t low, limb_t high, long shift)
{
if (shift != 0)
low = (low >> shift) | (high << (LIMB_BITS - shift));
return low;
}
static inline __maybe_unused limb_t shld(limb_t a1, limb_t a0, long shift)
{
if (shift != 0)
return (a1 << shift) | (a0 >> (LIMB_BITS - shift));
else
return a1;
}
#if LIMB_DIGITS == 19 #if LIMB_DIGITS == 19
/* WARNING: hardcoded for b = 1e19. It is assumed that: /* WARNING: hardcoded for b = 1e19. It is assumed that:

View File

@ -1071,11 +1071,10 @@ static int re_is_simple_quantifier(const uint8_t *bc_buf, int bc_buf_len)
} }
/* '*pp' is the first char after '<' */ /* '*pp' is the first char after '<' */
static int re_parse_group_name(char *buf, int buf_size, static int re_parse_group_name(char *buf, int buf_size, const uint8_t **pp)
const uint8_t **pp, BOOL is_utf16)
{ {
const uint8_t *p; const uint8_t *p, *p1;
uint32_t c; uint32_t c, d;
char *q; char *q;
p = *pp; p = *pp;
@ -1086,11 +1085,18 @@ static int re_parse_group_name(char *buf, int buf_size,
p++; p++;
if (*p != 'u') if (*p != 'u')
return -1; return -1;
c = lre_parse_escape(&p, is_utf16 * 2); c = lre_parse_escape(&p, 2); // accept surrogate pairs
} else if (c == '>') { } else if (c == '>') {
break; break;
} else if (c >= 128) { } else if (c >= 128) {
c = unicode_from_utf8(p, UTF8_CHAR_LEN_MAX, &p); c = unicode_from_utf8(p, UTF8_CHAR_LEN_MAX, &p);
if (c >= 0xD800 && c <= 0xDBFF) {
d = unicode_from_utf8(p, UTF8_CHAR_LEN_MAX, &p1);
if (d >= 0xDC00 && d <= 0xDFFF) {
c = 0x10000 + 0x400 * (c - 0xD800) + (d - 0xDC00);
p = p1;
}
}
} else { } else {
p++; p++;
} }
@ -1140,8 +1146,7 @@ static int re_parse_captures(REParseState *s, int *phas_named_captures,
/* potential named capture */ /* potential named capture */
if (capture_name) { if (capture_name) {
p += 3; p += 3;
if (re_parse_group_name(name, sizeof(name), &p, if (re_parse_group_name(name, sizeof(name), &p) == 0) {
s->is_utf16) == 0) {
if (!strcmp(name, capture_name)) if (!strcmp(name, capture_name))
return capture_index; return capture_index;
} }
@ -1314,7 +1319,7 @@ static int re_parse_term(REParseState *s, BOOL is_backward_dir)
} else if (p[2] == '<') { } else if (p[2] == '<') {
p += 3; p += 3;
if (re_parse_group_name(s->u.tmp_buf, sizeof(s->u.tmp_buf), if (re_parse_group_name(s->u.tmp_buf, sizeof(s->u.tmp_buf),
&p, s->is_utf16)) { &p)) {
return re_parse_error(s, "invalid group name"); return re_parse_error(s, "invalid group name");
} }
if (find_group_name(s, s->u.tmp_buf) > 0) { if (find_group_name(s, s->u.tmp_buf) > 0) {
@ -1378,7 +1383,7 @@ static int re_parse_term(REParseState *s, BOOL is_backward_dir)
} }
p1 += 3; p1 += 3;
if (re_parse_group_name(s->u.tmp_buf, sizeof(s->u.tmp_buf), if (re_parse_group_name(s->u.tmp_buf, sizeof(s->u.tmp_buf),
&p1, s->is_utf16)) { &p1)) {
if (s->is_utf16 || re_has_named_captures(s)) if (s->is_utf16 || re_has_named_captures(s))
return re_parse_error(s, "invalid group name"); return re_parse_error(s, "invalid group name");
else else

File diff suppressed because it is too large Load Diff

2
deps/quickjs/qjs.c vendored
View File

@ -454,8 +454,10 @@ int main(int argc, char **argv)
} }
} }
#ifdef CONFIG_BIGNUM
if (load_jscalc) if (load_jscalc)
bignum_ext = 1; bignum_ext = 1;
#endif
if (trace_memory) { if (trace_memory) {
js_trace_malloc_init(&trace_data); js_trace_malloc_init(&trace_data);

2
deps/quickjs/qjsc.c vendored
View File

@ -76,9 +76,7 @@ static const FeatureEntry feature_list[] = {
{ "promise", "Promise" }, { "promise", "Promise" },
#define FE_MODULE_LOADER 9 #define FE_MODULE_LOADER 9
{ "module-loader", NULL }, { "module-loader", NULL },
#ifdef CONFIG_BIGNUM
{ "bigint", "BigInt" }, { "bigint", "BigInt" },
#endif
}; };
void namelist_add(namelist_t *lp, const char *name, const char *short_name, void namelist_add(namelist_t *lp, const char *name, const char *short_name,

View File

@ -169,8 +169,8 @@ DEF(groups, "groups")
DEF(status, "status") DEF(status, "status")
DEF(reason, "reason") DEF(reason, "reason")
DEF(globalThis, "globalThis") DEF(globalThis, "globalThis")
#ifdef CONFIG_BIGNUM
DEF(bigint, "bigint") DEF(bigint, "bigint")
#ifdef CONFIG_BIGNUM
DEF(bigfloat, "bigfloat") DEF(bigfloat, "bigfloat")
DEF(bigdecimal, "bigdecimal") DEF(bigdecimal, "bigdecimal")
DEF(roundingMode, "roundingMode") DEF(roundingMode, "roundingMode")
@ -209,15 +209,13 @@ DEF(Int16Array, "Int16Array")
DEF(Uint16Array, "Uint16Array") DEF(Uint16Array, "Uint16Array")
DEF(Int32Array, "Int32Array") DEF(Int32Array, "Int32Array")
DEF(Uint32Array, "Uint32Array") DEF(Uint32Array, "Uint32Array")
#ifdef CONFIG_BIGNUM
DEF(BigInt64Array, "BigInt64Array") DEF(BigInt64Array, "BigInt64Array")
DEF(BigUint64Array, "BigUint64Array") DEF(BigUint64Array, "BigUint64Array")
#endif
DEF(Float32Array, "Float32Array") DEF(Float32Array, "Float32Array")
DEF(Float64Array, "Float64Array") DEF(Float64Array, "Float64Array")
DEF(DataView, "DataView") DEF(DataView, "DataView")
#ifdef CONFIG_BIGNUM
DEF(BigInt, "BigInt") DEF(BigInt, "BigInt")
#ifdef CONFIG_BIGNUM
DEF(BigFloat, "BigFloat") DEF(BigFloat, "BigFloat")
DEF(BigFloatEnv, "BigFloatEnv") DEF(BigFloatEnv, "BigFloatEnv")
DEF(BigDecimal, "BigDecimal") DEF(BigDecimal, "BigDecimal")

View File

@ -279,7 +279,7 @@ def( scope_get_ref, 7, 0, 2, atom_u16) /* emitted in phase 1, removed in phase
def(scope_put_var_init, 7, 0, 2, atom_u16) /* emitted in phase 1, removed in phase 2 */ def(scope_put_var_init, 7, 0, 2, atom_u16) /* emitted in phase 1, removed in phase 2 */
def(scope_get_private_field, 7, 1, 1, atom_u16) /* obj -> value, emitted in phase 1, removed in phase 2 */ def(scope_get_private_field, 7, 1, 1, atom_u16) /* obj -> value, emitted in phase 1, removed in phase 2 */
def(scope_get_private_field2, 7, 1, 2, atom_u16) /* obj -> obj value, emitted in phase 1, removed in phase 2 */ def(scope_get_private_field2, 7, 1, 2, atom_u16) /* obj -> obj value, emitted in phase 1, removed in phase 2 */
def(scope_put_private_field, 7, 1, 1, atom_u16) /* obj value ->, emitted in phase 1, removed in phase 2 */ def(scope_put_private_field, 7, 2, 0, atom_u16) /* obj value ->, emitted in phase 1, removed in phase 2 */
def( set_class_name, 5, 1, 1, u32) /* emitted in phase 1, removed in phase 2 */ def( set_class_name, 5, 1, 1, u32) /* emitted in phase 1, removed in phase 2 */

1930
deps/quickjs/quickjs.c vendored

File diff suppressed because it is too large Load Diff

View File

@ -733,13 +733,13 @@ JSValue JS_GetPropertyStr(JSContext *ctx, JSValueConst this_obj,
JSValue JS_GetPropertyUint32(JSContext *ctx, JSValueConst this_obj, JSValue JS_GetPropertyUint32(JSContext *ctx, JSValueConst this_obj,
uint32_t idx); uint32_t idx);
int JS_SetPropertyInternal(JSContext *ctx, JSValueConst this_obj, int JS_SetPropertyInternal(JSContext *ctx, JSValueConst obj,
JSAtom prop, JSValue val, JSAtom prop, JSValue val, JSValueConst this_obj,
int flags); int flags);
static inline int JS_SetProperty(JSContext *ctx, JSValueConst this_obj, static inline int JS_SetProperty(JSContext *ctx, JSValueConst this_obj,
JSAtom prop, JSValue val) JSAtom prop, JSValue val)
{ {
return JS_SetPropertyInternal(ctx, this_obj, prop, val, JS_PROP_THROW); return JS_SetPropertyInternal(ctx, this_obj, prop, val, this_obj, JS_PROP_THROW);
} }
int JS_SetPropertyUint32(JSContext *ctx, JSValueConst this_obj, int JS_SetPropertyUint32(JSContext *ctx, JSValueConst this_obj,
uint32_t idx, JSValue val); uint32_t idx, JSValue val);

View File

@ -47,16 +47,25 @@ testdir=test262/test
# Standard language features and proposed extensions # Standard language features and proposed extensions
# list the features that are included # list the features that are included
# skipped features are tagged as such to avoid warnings # skipped features are tagged as such to avoid warnings
# Keep this list alpha-sorted (:sort i in vim)
__getter__
__proto__
__setter__
AggregateError AggregateError
align-detached-buffer-semantics-with-web-reality align-detached-buffer-semantics-with-web-reality
arbitrary-module-namespace-names=skip arbitrary-module-namespace-names=skip
Array.prototype.at=skip array-find-from-last
array-grouping=skip
Array.fromAsync=skip
Array.prototype.at
Array.prototype.flat Array.prototype.flat
Array.prototype.flatMap Array.prototype.flatMap
Array.prototype.flatten Array.prototype.flatten
Array.prototype.includes
Array.prototype.values Array.prototype.values
ArrayBuffer ArrayBuffer
arraybuffer-transfer=skip
arrow-function arrow-function
async-functions async-functions
async-iteration async-iteration
@ -64,12 +73,15 @@ Atomics
Atomics.waitAsync=skip Atomics.waitAsync=skip
BigInt BigInt
caller caller
change-array-by-copy=skip
class class
class-fields-private class-fields-private
class-fields-private-in=skip
class-fields-public class-fields-public
class-methods-private class-methods-private
class-static-fields-public class-static-block=skip
class-static-fields-private class-static-fields-private
class-static-fields-public
class-static-methods-private class-static-methods-private
cleanupSome=skip cleanupSome=skip
coalesce-expression coalesce-expression
@ -85,14 +97,17 @@ DataView.prototype.getInt8
DataView.prototype.getUint16 DataView.prototype.getUint16
DataView.prototype.getUint32 DataView.prototype.getUint32
DataView.prototype.setUint8 DataView.prototype.setUint8
decorators=skip
default-parameters default-parameters
destructuring-assignment destructuring-assignment
destructuring-binding destructuring-binding
dynamic-import dynamic-import
error-cause=skip
exponentiation
export-star-as-namespace-from-module export-star-as-namespace-from-module
FinalizationGroup=skip FinalizationGroup=skip
FinalizationRegistry=skip
FinalizationRegistry.prototype.cleanupSome=skip FinalizationRegistry.prototype.cleanupSome=skip
FinalizationRegistry=skip
Float32Array Float32Array
Float64Array Float64Array
for-in-order for-in-order
@ -101,11 +116,16 @@ generators
globalThis globalThis
hashbang hashbang
host-gc-required=skip host-gc-required=skip
import-assertions=skip
import-attributes=skip
import.meta import.meta
Int16Array Int16Array
Int32Array Int32Array
Int8Array Int8Array
IsHTMLDDA IsHTMLDDA
iterator-helpers=skip
json-modules=skip
json-parse-with-source=skip
json-superset json-superset
legacy-regexp=skip legacy-regexp=skip
let let
@ -116,10 +136,12 @@ numeric-separator-literal
object-rest object-rest
object-spread object-spread
Object.fromEntries Object.fromEntries
Object.hasOwn
Object.is Object.is
optional-catch-binding optional-catch-binding
optional-chaining optional-chaining
Promise Promise
promise-with-resolvers=skip
Promise.allSettled Promise.allSettled
Promise.any Promise.any
Promise.prototype.finally Promise.prototype.finally
@ -130,20 +152,27 @@ Reflect.construct
Reflect.set Reflect.set
Reflect.setPrototypeOf Reflect.setPrototypeOf
regexp-dotall regexp-dotall
regexp-duplicate-named-groups=skip
regexp-lookbehind regexp-lookbehind
regexp-match-indices=skip regexp-match-indices=skip
regexp-named-groups regexp-named-groups
regexp-unicode-property-escapes regexp-unicode-property-escapes
regexp-v-flag=skip
resizable-arraybuffer=skip
rest-parameters rest-parameters
Set Set
set-methods=skip
ShadowRealm=skip
SharedArrayBuffer SharedArrayBuffer
string-trimming string-trimming
String.fromCodePoint String.fromCodePoint
String.prototype.at
String.prototype.endsWith String.prototype.endsWith
String.prototype.includes String.prototype.includes
String.prototype.at=skip String.prototype.isWellFormed=skip
String.prototype.matchAll String.prototype.matchAll
String.prototype.replaceAll String.prototype.replaceAll
String.prototype.toWellFormed=skip
String.prototype.trimEnd String.prototype.trimEnd
String.prototype.trimStart String.prototype.trimStart
super super
@ -162,11 +191,13 @@ Symbol.split
Symbol.toPrimitive Symbol.toPrimitive
Symbol.toStringTag Symbol.toStringTag
Symbol.unscopables Symbol.unscopables
symbols-as-weakmap-keys=skip
tail-call-optimization=skip tail-call-optimization=skip
template template
Temporal=skip
top-level-await=skip top-level-await=skip
TypedArray TypedArray
TypedArray.prototype.at=skip TypedArray.prototype.at
u180e u180e
Uint16Array Uint16Array
Uint32Array Uint32Array
@ -176,9 +207,6 @@ WeakMap
WeakRef=skip WeakRef=skip
WeakSet WeakSet
well-formed-json-stringify well-formed-json-stringify
__getter__
__proto__
__setter__
[exclude] [exclude]
# list excluded tests and directories here # list excluded tests and directories here

View File

@ -1,35 +1,41 @@
test262/test/built-ins/Function/internals/Construct/derived-this-uninitialized-realm.js:20: Test262Error: Expected a ReferenceError but got a ReferenceError test262/test/annexB/language/eval-code/direct/script-decl-lex-collision-in-sloppy-mode.js:13: Test262Error: Expected a SyntaxError to be thrown but no exception was thrown at all
test262/test/built-ins/Function/internals/Construct/derived-this-uninitialized-realm.js:20: strict mode: Test262Error: Expected a ReferenceError but got a ReferenceError test262/test/built-ins/AsyncGeneratorPrototype/return/return-state-completed-broken-promise.js:53: TypeError: $DONE() not called
test262/test/built-ins/RegExp/named-groups/non-unicode-property-names-valid.js:46: SyntaxError: invalid group name test262/test/built-ins/AsyncGeneratorPrototype/return/return-state-completed-broken-promise.js:53: strict mode: TypeError: $DONE() not called
test262/test/built-ins/RegExp/named-groups/non-unicode-property-names-valid.js:46: strict mode: SyntaxError: invalid group name test262/test/built-ins/AsyncGeneratorPrototype/return/return-suspendedStart-broken-promise.js:34: TypeError: $DONE() not called
test262/test/built-ins/TypedArrayConstructors/internals/DefineOwnProperty/BigInt/detached-buffer.js:46: Test262Error: (Testing with BigInt64Array.) test262/test/built-ins/AsyncGeneratorPrototype/return/return-suspendedStart-broken-promise.js:34: strict mode: TypeError: $DONE() not called
test262/test/built-ins/TypedArrayConstructors/internals/DefineOwnProperty/BigInt/detached-buffer.js:46: strict mode: Test262Error: (Testing with BigInt64Array.) test262/test/built-ins/AsyncGeneratorPrototype/return/return-suspendedYield-broken-promise-try-catch.js:39: TypeError: $DONE() not called
test262/test/built-ins/TypedArrayConstructors/internals/DefineOwnProperty/detached-buffer.js:47: Test262Error: (Testing with Float64Array.) test262/test/built-ins/AsyncGeneratorPrototype/return/return-suspendedYield-broken-promise-try-catch.js:39: strict mode: TypeError: $DONE() not called
test262/test/built-ins/TypedArrayConstructors/internals/DefineOwnProperty/detached-buffer.js:47: strict mode: Test262Error: (Testing with Float64Array.) test262/test/built-ins/Function/internals/Construct/derived-this-uninitialized-realm.js:20: Test262Error: Expected a ReferenceError but got a different error constructor with the same name
test262/test/built-ins/TypedArrayConstructors/internals/Set/BigInt/detached-buffer-realm.js:37: strict mode: TypeError: out-of-bound numeric index (Testing with BigInt64Array.) test262/test/built-ins/Function/internals/Construct/derived-this-uninitialized-realm.js:20: strict mode: Test262Error: Expected a ReferenceError but got a different error constructor with the same name
test262/test/built-ins/TypedArrayConstructors/internals/Set/BigInt/detached-buffer.js:34: TypeError: cannot convert bigint to number (Testing with BigInt64Array.) test262/test/built-ins/RegExp/lookahead-quantifier-match-groups.js:27: Test262Error: Expected [a, abc] and [a, undefined] to have the same contents. ? quantifier
test262/test/built-ins/TypedArrayConstructors/internals/Set/BigInt/detached-buffer.js:32: strict mode: TypeError: out-of-bound numeric index (Testing with BigInt64Array.) test262/test/built-ins/RegExp/lookahead-quantifier-match-groups.js:27: strict mode: Test262Error: Expected [a, abc] and [a, undefined] to have the same contents. ? quantifier
test262/test/built-ins/TypedArrayConstructors/internals/Set/BigInt/key-is-minus-zero.js:20: Test262Error: Reflect.set("new TA([42n])", "-0", 1n) must return true Expected SameValue(«false», «true») to be true (Testing with BigInt64Array.) test262/test/built-ins/RegExp/unicode_full_case_folding.js:20: Test262Error: \u0390 does not match \u1fd3
test262/test/built-ins/TypedArrayConstructors/internals/Set/BigInt/key-is-minus-zero.js:20: strict mode: Test262Error: Reflect.set("new TA([42n])", "-0", 1n) must return true Expected SameValue(«false», «true») to be true (Testing with BigInt64Array.) test262/test/built-ins/RegExp/unicode_full_case_folding.js:20: strict mode: Test262Error: \u0390 does not match \u1fd3
test262/test/built-ins/TypedArrayConstructors/internals/Set/BigInt/key-is-not-integer.js:21: Test262Error: Reflect.set("new TA([42n])", "1.1", 1n) must return true Expected SameValue(«false», «true») to be true (Testing with BigInt64Array.) test262/test/built-ins/String/prototype/localeCompare/15.5.4.9_CE.js:62: Test262Error: String.prototype.localeCompare considers ö (\u006f\u0308) ≠ ö (\u00f6).
test262/test/built-ins/TypedArrayConstructors/internals/Set/BigInt/key-is-not-integer.js:21: strict mode: Test262Error: Reflect.set("new TA([42n])", "1.1", 1n) must return true Expected SameValue(«false», «true») to be true (Testing with BigInt64Array.) test262/test/built-ins/String/prototype/localeCompare/15.5.4.9_CE.js:62: strict mode: Test262Error: String.prototype.localeCompare considers ö (\u006f\u0308) ≠ ö (\u00f6).
test262/test/built-ins/TypedArrayConstructors/internals/Set/BigInt/key-is-out-of-bounds.js:27: Test262Error: Reflect.set("new TA([42n])", "-1", 1n) must return false Expected SameValue(«false», «true») to be true (Testing with BigInt64Array.) test262/test/built-ins/TypedArray/prototype/sort/sort-tonumber.js:30: TypeError: ArrayBuffer is detached (Testing with Float64Array.)
test262/test/built-ins/TypedArrayConstructors/internals/Set/BigInt/key-is-out-of-bounds.js:27: strict mode: Test262Error: Reflect.set("new TA([42n])", "-1", 1n) must return false Expected SameValue(«false», «true») to be true (Testing with BigInt64Array.) test262/test/built-ins/TypedArray/prototype/sort/sort-tonumber.js:30: strict mode: TypeError: ArrayBuffer is detached (Testing with Float64Array.)
test262/test/built-ins/TypedArrayConstructors/internals/Set/BigInt/tonumber-value-detached-buffer.js:24: Test262Error: Expected SameValue(«false», «true») to be true (Testing with BigInt64Array.) test262/test/language/expressions/assignment/target-member-computed-reference-null.js:32: Test262Error: Expected a DummyError but got a TypeError
test262/test/built-ins/TypedArrayConstructors/internals/Set/BigInt/tonumber-value-detached-buffer.js:24: strict mode: Test262Error: Expected SameValue(«false», «true») to be true (Testing with BigInt64Array.) test262/test/language/expressions/assignment/target-member-computed-reference-null.js:32: strict mode: Test262Error: Expected a DummyError but got a TypeError
test262/test/built-ins/TypedArrayConstructors/internals/Set/detached-buffer-realm.js:37: strict mode: TypeError: out-of-bound numeric index (Testing with Float64Array.) test262/test/language/expressions/assignment/target-member-computed-reference-undefined.js:32: Test262Error: Expected a DummyError but got a TypeError
test262/test/built-ins/TypedArrayConstructors/internals/Set/detached-buffer.js:32: strict mode: TypeError: out-of-bound numeric index (Testing with Float64Array.) test262/test/language/expressions/assignment/target-member-computed-reference-undefined.js:32: strict mode: Test262Error: Expected a DummyError but got a TypeError
test262/test/built-ins/TypedArrayConstructors/internals/Set/key-is-minus-zero.js:22: Test262Error: Reflect.set(sample, "-0", 1) must return true Expected SameValue(«false», «true») to be true (Testing with Float64Array.)
test262/test/built-ins/TypedArrayConstructors/internals/Set/key-is-minus-zero.js:22: strict mode: Test262Error: Reflect.set(sample, "-0", 1) must return true Expected SameValue(«false», «true») to be true (Testing with Float64Array.)
test262/test/built-ins/TypedArrayConstructors/internals/Set/key-is-not-integer.js:22: Test262Error: Reflect.set(sample, "1.1", 1) must return true Expected SameValue(«false», «true») to be true (Testing with Float64Array.)
test262/test/built-ins/TypedArrayConstructors/internals/Set/key-is-not-integer.js:22: strict mode: Test262Error: Reflect.set(sample, "1.1", 1) must return true Expected SameValue(«false», «true») to be true (Testing with Float64Array.)
test262/test/built-ins/TypedArrayConstructors/internals/Set/key-is-out-of-bounds.js:22: Test262Error: Reflect.set(sample, "-1", 1) must return true Expected SameValue(«false», «true») to be true (Testing with Float64Array.)
test262/test/built-ins/TypedArrayConstructors/internals/Set/key-is-out-of-bounds.js:22: strict mode: Test262Error: Reflect.set(sample, "-1", 1) must return true Expected SameValue(«false», «true») to be true (Testing with Float64Array.)
test262/test/built-ins/TypedArrayConstructors/internals/Set/tonumber-value-detached-buffer.js:39: Test262Error: Expected SameValue(«false», «true») to be true (Testing with Float64Array.)
test262/test/built-ins/TypedArrayConstructors/internals/Set/tonumber-value-detached-buffer.js:39: strict mode: Test262Error: Expected SameValue(«false», «true») to be true (Testing with Float64Array.)
test262/test/language/expressions/dynamic-import/usage-from-eval.js:26: TypeError: $DONE() not called test262/test/language/expressions/dynamic-import/usage-from-eval.js:26: TypeError: $DONE() not called
test262/test/language/expressions/dynamic-import/usage-from-eval.js:26: strict mode: TypeError: $DONE() not called test262/test/language/expressions/dynamic-import/usage-from-eval.js:26: strict mode: TypeError: $DONE() not called
test262/test/language/expressions/optional-chaining/optional-call-preserves-this.js:21: TypeError: cannot read property 'c' of undefined test262/test/language/expressions/optional-chaining/optional-call-preserves-this.js:21: TypeError: cannot read property 'c' of undefined
test262/test/language/expressions/optional-chaining/optional-call-preserves-this.js:15: strict mode: TypeError: cannot read property '_b' of undefined test262/test/language/expressions/optional-chaining/optional-call-preserves-this.js:15: strict mode: TypeError: cannot read property '_b' of undefined
test262/test/language/global-code/script-decl-lex-var-declared-via-eval-sloppy.js:13: Test262Error: variable Expected a SyntaxError to be thrown but no exception was thrown at all
test262/test/language/module-code/namespace/internals/define-own-property.js:30: Test262Error: Object.freeze: 1 Expected a TypeError to be thrown but no exception was thrown at all
test262/test/language/statements/async-generator/yield-star-promise-not-unwrapped.js:25: TypeError: $DONE() not called
test262/test/language/statements/async-generator/yield-star-promise-not-unwrapped.js:25: strict mode: TypeError: $DONE() not called
test262/test/language/statements/async-generator/yield-star-return-then-getter-ticks.js:131: TypeError: $DONE() not called
test262/test/language/statements/async-generator/yield-star-return-then-getter-ticks.js:131: strict mode: TypeError: $DONE() not called
test262/test/language/statements/class/elements/private-method-double-initialisation-get-and-set.js:33: Test262Error: Expected a TypeError to be thrown but no exception was thrown at all
test262/test/language/statements/class/elements/private-method-double-initialisation-get-and-set.js:33: strict mode: Test262Error: Expected a TypeError to be thrown but no exception was thrown at all
test262/test/language/statements/class/elements/private-method-double-initialisation-get.js:32: Test262Error: Expected a TypeError to be thrown but no exception was thrown at all
test262/test/language/statements/class/elements/private-method-double-initialisation-get.js:32: strict mode: Test262Error: Expected a TypeError to be thrown but no exception was thrown at all
test262/test/language/statements/class/elements/private-method-double-initialisation-set.js:32: Test262Error: Expected a TypeError to be thrown but no exception was thrown at all
test262/test/language/statements/class/elements/private-method-double-initialisation-set.js:32: strict mode: Test262Error: Expected a TypeError to be thrown but no exception was thrown at all
test262/test/language/statements/class/elements/private-method-double-initialisation.js:32: Test262Error: Expected a TypeError to be thrown but no exception was thrown at all
test262/test/language/statements/class/elements/private-method-double-initialisation.js:32: strict mode: Test262Error: Expected a TypeError to be thrown but no exception was thrown at all
test262/test/language/statements/for-of/head-lhs-async-invalid.js:14: unexpected error type: Test262: This statement should not be evaluated. test262/test/language/statements/for-of/head-lhs-async-invalid.js:14: unexpected error type: Test262: This statement should not be evaluated.
test262/test/language/statements/for-of/head-lhs-async-invalid.js:14: strict mode: unexpected error type: Test262: This statement should not be evaluated. test262/test/language/statements/for-of/head-lhs-async-invalid.js:14: strict mode: unexpected error type: Test262: This statement should not be evaluated.

View File

@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
set -e set -e
url="ftp://ftp.unicode.org/Public/13.0.0/ucd" url="ftp://ftp.unicode.org/Public/15.0.0/ucd"
emoji_url="${url}/emoji/emoji-data.txt" emoji_url="${url}/emoji/emoji-data.txt"
files="CaseFolding.txt DerivedNormalizationProps.txt PropList.txt \ files="CaseFolding.txt DerivedNormalizationProps.txt PropList.txt \
@ -11,9 +11,9 @@ PropertyValueAliases.txt"
mkdir -p unicode mkdir -p unicode
#for f in $files; do for f in $files; do
# g="${url}/${f}" g="${url}/${f}"
# wget $g -O unicode/$f wget $g -O unicode/$f
#done done
wget $emoji_url -O unicode/emoji-data.txt wget $emoji_url -O unicode/emoji-data.txt

View File

@ -72,6 +72,7 @@ DEF(Coptic, "Copt,Qaac")
DEF(Cuneiform, "Xsux") DEF(Cuneiform, "Xsux")
DEF(Cypriot, "Cprt") DEF(Cypriot, "Cprt")
DEF(Cyrillic, "Cyrl") DEF(Cyrillic, "Cyrl")
DEF(Cypro_Minoan, "Cpmn")
DEF(Deseret, "Dsrt") DEF(Deseret, "Dsrt")
DEF(Devanagari, "Deva") DEF(Devanagari, "Deva")
DEF(Dives_Akuru, "Diak") DEF(Dives_Akuru, "Diak")
@ -104,6 +105,7 @@ DEF(Javanese, "Java")
DEF(Kaithi, "Kthi") DEF(Kaithi, "Kthi")
DEF(Kannada, "Knda") DEF(Kannada, "Knda")
DEF(Katakana, "Kana") DEF(Katakana, "Kana")
DEF(Kawi, "Kawi")
DEF(Kayah_Li, "Kali") DEF(Kayah_Li, "Kali")
DEF(Kharoshthi, "Khar") DEF(Kharoshthi, "Khar")
DEF(Khmer, "Khmr") DEF(Khmer, "Khmr")
@ -138,6 +140,7 @@ DEF(Mro, "Mroo")
DEF(Multani, "Mult") DEF(Multani, "Mult")
DEF(Myanmar, "Mymr") DEF(Myanmar, "Mymr")
DEF(Nabataean, "Nbat") DEF(Nabataean, "Nbat")
DEF(Nag_Mundari, "Nagm")
DEF(Nandinagari, "Nand") DEF(Nandinagari, "Nand")
DEF(New_Tai_Lue, "Talu") DEF(New_Tai_Lue, "Talu")
DEF(Newa, "Newa") DEF(Newa, "Newa")
@ -154,6 +157,7 @@ DEF(Old_Persian, "Xpeo")
DEF(Old_Sogdian, "Sogo") DEF(Old_Sogdian, "Sogo")
DEF(Old_South_Arabian, "Sarb") DEF(Old_South_Arabian, "Sarb")
DEF(Old_Turkic, "Orkh") DEF(Old_Turkic, "Orkh")
DEF(Old_Uyghur, "Ougr")
DEF(Oriya, "Orya") DEF(Oriya, "Orya")
DEF(Osage, "Osge") DEF(Osage, "Osge")
DEF(Osmanya, "Osma") DEF(Osmanya, "Osma")
@ -192,8 +196,11 @@ DEF(Thai, "Thai")
DEF(Tibetan, "Tibt") DEF(Tibetan, "Tibt")
DEF(Tifinagh, "Tfng") DEF(Tifinagh, "Tfng")
DEF(Tirhuta, "Tirh") DEF(Tirhuta, "Tirh")
DEF(Tangsa, "Tnsa")
DEF(Toto, "Toto")
DEF(Ugaritic, "Ugar") DEF(Ugaritic, "Ugar")
DEF(Vai, "Vaii") DEF(Vai, "Vaii")
DEF(Vithkuqi, "Vith")
DEF(Wancho, "Wcho") DEF(Wancho, "Wcho")
DEF(Warang_Citi, "Wara") DEF(Warang_Citi, "Wara")
DEF(Yezidi, "Yezi") DEF(Yezidi, "Yezi")

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.unprompted.tildefriends" package="com.unprompted.tildefriends"
android:versionCode="13" android:versionCode="14"
android:versionName="0.0.13"> android:versionName="0.0.14">
<uses-sdk android:minSdkVersion="24" android:targetSdkVersion="34"/> <uses-sdk android:minSdkVersion="24" android:targetSdkVersion="34"/>
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<application <application

666
src/http.c Normal file
View File

@ -0,0 +1,666 @@
#include "http.h"
#include "log.h"
#include "mem.h"
#include "util.js.h"
#include "picohttpparser.h"
#include "uv.h"
#include <assert.h>
#include <stdalign.h>
#include <stdlib.h>
#include <string.h>
#if !defined(__APPLE__) && !defined(__OpenBSD__) && !defined(_WIN32)
#include <alloca.h>
#endif
typedef struct _tf_http_connection_t
{
tf_http_t* http;
uv_tcp_t tcp;
uv_shutdown_t shutdown;
int ref_count;
const char* method;
const char* path;
const char* query;
int minor_version;
char headers_buffer[8192];
size_t headers_buffer_length;
int parsed_length;
char buffer[8192];
size_t buffer_length;
struct phr_header headers[32];
int headers_length;
bool headers_done;
int flags;
tf_http_callback_t* callback;
tf_http_request_t* request;
void* user_data;
bool is_websocket;
int websocket_message_index;
void* body;
size_t body_length;
size_t content_length;
bool connection_close;
} tf_http_connection_t;
typedef struct _tf_http_handler_t
{
const char* pattern;
int flags;
tf_http_callback_t* callback;
void* user_data;
} tf_http_handler_t;
typedef struct _tf_http_t
{
uv_tcp_t** listeners;
int listeners_count;
tf_http_connection_t** connections;
int connections_count;
tf_http_handler_t* handlers;
int handlers_count;
int pending_closes;
uv_loop_t* loop;
} tf_http_t;
static const char* _http_connection_get_header(const tf_http_connection_t* connection, const char* name);
static void _http_connection_destroy(tf_http_connection_t* connection, const char* reason);
static const char* _http_status_text(int status);
tf_http_t* tf_http_create(uv_loop_t* loop)
{
tf_http_t* http = tf_malloc(sizeof(tf_http_t));
*http = (tf_http_t)
{
.loop = loop,
};
return http;
}
void _http_allocate_buffer(uv_handle_t* handle, size_t suggestedSize, uv_buf_t* buf)
{
tf_http_connection_t* connection = handle->data;
if (!connection->headers_done)
{
*buf = uv_buf_init(connection->headers_buffer + connection->headers_buffer_length, sizeof(connection->headers_buffer) - connection->headers_buffer_length);
}
else
{
*buf = uv_buf_init(connection->buffer + connection->buffer_length, sizeof(connection->buffer) - connection->buffer_length);
}
}
bool _http_find_handler(tf_http_t* http, const char* path, int flags, tf_http_callback_t** out_callback, void** out_user_data)
{
for (int i = 0; i < http->handlers_count; i++)
{
if (http->handlers[i].flags == flags &&
(!http->handlers[i].pattern ||
strcmp(path, http->handlers[i].pattern) == 0 ||
(strncmp(path, http->handlers[i].pattern, strlen(http->handlers[i].pattern)) == 0 && path[strlen(http->handlers[i].pattern)] == '/')))
{
*out_callback = http->handlers[i].callback;
*out_user_data = http->handlers[i].user_data;
return true;
}
}
return false;
}
static void _http_on_write(uv_write_t* write, int status)
{
tf_free(write);
}
static void _http_connection_on_close(uv_handle_t* handle)
{
tf_http_connection_t* connection = handle->data;
handle->data = NULL;
_http_connection_destroy(connection, "handle closed");
}
static void _http_connection_destroy(tf_http_connection_t* connection, const char* reason)
{
if (connection->tcp.data)
{
uv_close((uv_handle_t*)&connection->tcp, _http_connection_on_close);
}
else if (connection->ref_count == 0)
{
tf_http_t* http = connection->http;
for (int i = 0; i < http->connections_count; i++)
{
if (http->connections[i] == connection)
{
http->connections[i] = http->connections[--http->connections_count];
}
}
if (connection->body)
{
tf_free(connection->body);
connection->body = NULL;
}
tf_free(connection);
}
}
static void _http_builtin_404_handler(tf_http_request_t* request)
{
const char* k_payload = _http_status_text(404);
tf_http_respond(request, 404, NULL, 0, k_payload, strlen(k_payload));
}
static void _http_reset_connection(tf_http_connection_t* connection)
{
connection->headers_done = false;
connection->headers_buffer_length = 0;
connection->body_length = 0;
connection->content_length = 0;
connection->parsed_length = 0;
}
static void _http_websocket_mask_in_place(uint8_t* p, uint32_t mask, size_t size)
{
int i = 0;
for (; ((intptr_t)(p + i)) % alignof(uint32_t); i++)
{
p[i] ^= (mask & 0xff);
mask = ((mask >> 8) & 0xffffff) | ((mask & 0xff) << 24);
}
int aligned_start = i;
for (; i + 4 < (int)size; i += 4)
{
*(uint32_t*)(p + i) ^= mask;
}
for (; i < (int)size; i++)
{
p[i] ^= ((mask >> (8 * ((i - aligned_start) % 4))) & 0xff);
}
}
static void _http_add_body_bytes(tf_http_connection_t* connection, const void* data, size_t size)
{
if (connection->is_websocket)
{
connection->body = tf_realloc(connection->body, connection->body_length + size);
memcpy((char*)connection->body + connection->body_length, data, size);
connection->body_length += size;
while (connection->body_length >= 2)
{
uint8_t* p = connection->body;
uint8_t bits0 = p[0];
uint8_t bits1 = p[1];
if ((bits1 & (1 << 7)) == 0)
{
/* Unmasked message. */
_http_connection_destroy(connection, "websocket server received unmasked bytes");
return;
}
uint8_t op_code = bits0 & 0xf;
bool fin = (bits0 & (1 << 7)) != 0;
size_t length = bits1 & 0x7f;
int mask_start = 2;
if (length == 126)
{
length = 0;
for (int i = 0; i < 2; i++)
{
length <<= 8;
length |= p[2 + i];
}
mask_start = 4;
}
else if (length == 127)
{
length = 0;
for (int i = 0; i < 8; i++)
{
length <<= 8;
length |= p[2 + i];
}
mask_start = 10;
}
if (connection->body_length >= mask_start + length + 4)
{
uint32_t mask =
(uint32_t)p[mask_start + 0] |
(uint32_t)p[mask_start + 1] << 8 |
(uint32_t)p[mask_start + 2] << 16 |
(uint32_t)p[mask_start + 3] << 24;
_http_websocket_mask_in_place(p + mask_start + 4, mask, length);
if (fin)
{
if (connection->request->on_message)
{
connection->request->on_message(connection->request, op_code, p + mask_start + 4, length);
}
else
{
tf_printf("MESSAGE %d [%.*s]\n", op_code, (int)length, p + mask_start + 4);
}
}
connection->websocket_message_index++;
size_t total_length = mask_start + 4 + length;
memmove(connection->body, (char*)connection->body + total_length, connection->body_length - total_length);
connection->body_length -= total_length;
}
}
}
else
{
size_t fit = tf_min(connection->content_length - connection->body_length, size);
if (fit > 0)
{
memcpy((char*)connection->body + connection->body_length, data, fit);
connection->body_length += fit;
}
if (connection->body_length == connection->content_length)
{
if (connection->flags & k_tf_http_handler_flag_websocket)
{
connection->is_websocket = true;
}
tf_http_request_t* request = tf_malloc(sizeof(tf_http_request_t));
*request = (tf_http_request_t)
{
.connection = connection,
.phase = k_http_callback_phase_headers_received,
.method = connection->method,
.path = connection->path,
.flags = connection->flags,
.query = connection->query,
.body = connection->body,
.content_length = connection->content_length,
.headers = connection->headers,
.headers_count = connection->headers_length,
.user_data = connection->user_data,
};
connection->request = request;
tf_http_request_ref(request);
connection->callback(request);
tf_http_request_release(request);
if (!connection->is_websocket)
{
_http_reset_connection(connection);
}
}
}
}
static void _http_on_read(uv_stream_t* stream, ssize_t read_size, const uv_buf_t* buffer)
{
tf_http_connection_t* connection = stream->data;
if (read_size > 0)
{
if (!connection->headers_done)
{
connection->headers_buffer_length += read_size;
const char* method = NULL;
size_t method_length = 0;
const char* path = NULL;
size_t path_length = 0;
size_t header_count = sizeof(connection->headers) / sizeof(*connection->headers);
int parse_result = phr_parse_request(connection->headers_buffer, connection->headers_buffer_length, &method, &method_length, &path, &path_length, &connection->minor_version, connection->headers, &header_count, connection->parsed_length);
connection->parsed_length = connection->headers_buffer_length;
if (parse_result > 0)
{
connection->headers_done = true;
connection->headers_length = header_count;
connection->method = method;
((char*)connection->method)[method_length] = '\0';
connection->path = path;
((char*)connection->path)[path_length] = '\0';
char* q = strchr(connection->path, '?');
if (q)
{
*q = '\0';
connection->query = q + 1;
}
connection->connection_close = connection->minor_version == 0;
for (int i = 0; i < (int)header_count; i++)
{
for (size_t j = 0; j < connection->headers[i].name_len; j++)
{
if (connection->headers[i].name[j] >= 'A' &&
connection->headers[i].name[j] <= 'Z')
{
((char*)connection->headers[i].name)[j] += 'a' - 'A';
}
}
((char*)connection->headers[i].name)[connection->headers[i].name_len] = '\0';
((char*)connection->headers[i].value)[connection->headers[i].value_len] = '\0';
if (strcasecmp(connection->headers[i].name, "content-length") == 0)
{
connection->content_length = strtoull(connection->headers[i].value, NULL, 10);
}
else if (strcasecmp(connection->headers[i].name, "connection") == 0)
{
if (strcasecmp(connection->headers[i].value, "close") == 0)
{
connection->connection_close = true;
}
}
}
if (connection->content_length)
{
connection->body = tf_malloc(connection->content_length);
}
int flags = _http_connection_get_header(connection, "upgrade") ? k_tf_http_handler_flag_websocket : 0;
connection->flags = flags;
if (!_http_find_handler(connection->http, connection->path, flags, &connection->callback, &connection->user_data) || !connection->callback)
{
connection->callback = _http_builtin_404_handler;
}
_http_add_body_bytes(connection, connection->headers_buffer + parse_result, connection->headers_buffer_length - parse_result);
}
else if (parse_result == -2)
{
/* Incomplete. Will try again next time. */
}
else
{
tf_printf("phr_parse_request: %d\n", parse_result);
_http_connection_destroy(connection, "failed to parse request headers");
}
}
else
{
connection->buffer_length += read_size;
_http_add_body_bytes(connection, connection->buffer, connection->buffer_length);
connection->buffer_length = 0;
}
}
else if (read_size < 0)
{
_http_connection_destroy(connection, uv_strerror(read_size));
}
}
static void _http_on_connection(uv_stream_t* stream, int status)
{
tf_http_t* http = stream->data;
tf_http_connection_t* connection = tf_malloc(sizeof(tf_http_connection_t));
*connection = (tf_http_connection_t) { .http = http, .tcp = { .data = connection } };
int r = uv_tcp_init(connection->http->loop, &connection->tcp);
if (r)
{
tf_printf("uv_tcp_init: %s\n", uv_strerror(r));
tf_free(connection);
return;
}
r = uv_accept(stream, (uv_stream_t*)&connection->tcp);
if (r)
{
tf_printf("uv_accept: %s\n", uv_strerror(r));
uv_close((uv_handle_t*)&connection->tcp, NULL);
tf_free(connection);
return;
}
r = uv_read_start((uv_stream_t*)&connection->tcp, _http_allocate_buffer, _http_on_read);
if (r)
{
tf_printf("uv_read-start: %s\n", uv_strerror(r));
uv_close((uv_handle_t*)&connection->tcp, NULL);
tf_free(connection);
return;
}
http->connections = tf_realloc(http->connections, sizeof(tf_http_connection_t*) * (http->connections_count + 1));
http->connections[http->connections_count++] = connection;
}
void tf_http_listen(tf_http_t* http, int port)
{
uv_tcp_t* tcp = tf_malloc(sizeof(uv_tcp_t));
*tcp = (uv_tcp_t) { .data = http };
int r = uv_tcp_init(http->loop, tcp);
if (r)
{
tf_printf("uv_tcp_init: %s\n", uv_strerror(r));
}
if (r == 0)
{
struct sockaddr_in addr =
{
.sin_family = AF_INET,
.sin_addr = { .s_addr = INADDR_ANY },
.sin_port = ntohs(port),
};
r = uv_tcp_bind(tcp, (struct sockaddr*)&addr, 0);
if (r)
{
tf_printf("uv_tcp_bind: %s\n", uv_strerror(r));
}
}
if (r == 0)
{
r = uv_listen((uv_stream_t*)tcp, 16, _http_on_connection);
if (r)
{
tf_printf("uv_listen: %s\n", uv_strerror(r));
}
}
if (r == 0)
{
http->listeners = tf_realloc(http->listeners, sizeof(uv_tcp_t*) * (http->listeners_count + 1));
http->listeners[http->listeners_count++] = tcp;
}
}
void tf_http_add_handler(tf_http_t* http, const char* pattern, int flags, tf_http_callback_t* callback, void* user_data)
{
http->handlers = tf_realloc(http->handlers, sizeof(tf_http_handler_t) * (http->handlers_count + 1));
http->handlers[http->handlers_count++] = (tf_http_handler_t)
{
.pattern = tf_strdup(pattern),
.flags = flags,
.callback = callback,
.user_data = user_data,
};
}
static void _http_free_on_close(uv_handle_t* handle)
{
handle->data = NULL;
tf_free(handle);
}
void tf_http_destroy(tf_http_t* http)
{
for (int i = 0; i < http->listeners_count; i++)
{
uv_close((uv_handle_t*)http->listeners[i], _http_free_on_close);
}
tf_free(http->listeners);
http->listeners = NULL;
http->listeners_count = 0;
for (int i = 0; i < http->handlers_count; i++)
{
if (http->handlers[i].pattern)
{
tf_free((void*)http->handlers[i].pattern);
http->handlers[i].pattern = NULL;
}
}
tf_free(http->handlers);
http->handlers_count = 0;
tf_free(http->connections);
http->connections_count = 0;
tf_free(http);
}
static const char* _http_status_text(int status)
{
switch (status)
{
case 101: return "Switching Protocols";
case 200: return "OK";
case 303: return "See other";
case 304: return "Not Modified";
case 400: return "Bad Request";
case 401: return "Unauthorized";
case 403: return "Forbidden";
case 404: return "File not found";
case 500: return "Internal server error";
default: return "Unknown";
}
}
static void _http_on_shutdown(uv_shutdown_t* request, int status)
{
request->data = NULL;
}
static void _http_write(tf_http_connection_t* connection, const void* data, size_t size)
{
uv_write_t* write = tf_malloc(sizeof(uv_write_t) + size);
*write = (uv_write_t) { .data = connection };
memcpy(write + 1, data, size);
int r = uv_write(write, (uv_stream_t*)&connection->tcp, &(uv_buf_t) { .base = (void*)(write + 1), .len = size }, 1, _http_on_write);
if (r)
{
tf_printf("uv_write: %s\n", uv_strerror(r));
}
}
void tf_http_request_send(tf_http_request_t* request, const void* data, size_t size)
{
_http_write(request->connection, data, size);
}
void tf_http_respond(tf_http_request_t* request, int status, const char** headers, int headers_count, const void* body, size_t content_length)
{
const char* status_text = _http_status_text(status);
/* HTTP/1.x 200 OK\r\n */
bool sent_content_length = false;
int headers_length = 8 + 1 + 3 + 1 + strlen(status_text) + 2;
if (headers)
{
for (int i = 0; i < headers_count * 2; i += 2)
{
/* Key: Value\r\n */
headers_length += strlen(headers[i]) + 2 + strlen(headers[i + 1]) + 2;
if (strcasecmp(headers[i], "content-length") == 0)
{
sent_content_length = true;
}
}
}
/* \r\n */
headers_length += 2;
char content_length_buffer[32] = { 0 };
int content_length_buffer_length = 0;
if (!sent_content_length && status != 101)
{
content_length_buffer_length = snprintf(content_length_buffer, sizeof(content_length_buffer), "Content-Length: %zd\r\n", content_length);
headers_length += content_length_buffer_length;
}
uv_write_t* write = tf_malloc(sizeof(uv_write_t) + headers_length + content_length + 1);
*write = (uv_write_t) { .data = request->connection };
char* buffer = (char*)(write + 1);
int offset = snprintf(buffer, headers_length + 1, "HTTP/1.%d %03d %s\r\n", request->connection->minor_version, status, status_text);
if (headers)
{
for (int i = 0; i < headers_count * 2; i += 2)
{
offset += snprintf(buffer + offset, headers_length + 1 - offset, "%s: %s\r\n", headers[i], headers[i + 1]);
}
}
if (!sent_content_length)
{
memcpy(buffer + offset, content_length_buffer, content_length_buffer_length);
offset += content_length_buffer_length;
}
offset += snprintf(buffer + offset, headers_length + 1 - offset, "\r\n");
assert(offset == headers_length);
if (content_length)
{
memcpy(buffer + offset, body, content_length);
}
int r = uv_write(write, (uv_stream_t*)&request->connection->tcp, &(uv_buf_t) { .base = buffer, .len = headers_length + content_length }, 1, _http_on_write);
if (r)
{
tf_printf("uv_write: %s\n", uv_strerror(r));
}
if (request->connection->connection_close &&
!request->connection->shutdown.data)
{
request->connection->shutdown.data = request->connection;
uv_shutdown(&request->connection->shutdown, (uv_stream_t*)&request->connection->tcp, _http_on_shutdown);
}
}
size_t tf_http_get_body(const tf_http_request_t* request, const void** out_data)
{
*out_data = request->connection->body;
return request->connection->content_length;
}
void tf_http_request_ref(tf_http_request_t* request)
{
request->ref_count++;
request->connection->ref_count++;
}
void tf_http_request_release(tf_http_request_t* request)
{
if (--request->connection->ref_count == 0)
{
/* Reset the connection? */
}
if (--request->ref_count == 0)
{
tf_free(request);
}
}
static const char* _http_connection_get_header(const tf_http_connection_t* connection, const char* name)
{
for (int i = 0; i < connection->headers_length; i++)
{
if (strcasecmp(connection->headers[i].name, name) == 0)
{
return connection->headers[i].value;
}
}
return NULL;
}
const char* tf_http_request_get_header(tf_http_request_t* request, const char* name)
{
return _http_connection_get_header(request->connection, name);
}

54
src/http.h Normal file
View File

@ -0,0 +1,54 @@
#pragma once
#include <stddef.h>
typedef struct uv_loop_s uv_loop_t;
typedef struct _tf_http_t tf_http_t;
typedef struct _tf_http_connection_t tf_http_connection_t;
typedef struct _tf_http_request_t tf_http_request_t;
typedef enum _tf_http_callback_phase_t
{
k_http_callback_phase_headers_received,
k_http_callback_phase_request_done,
} tf_http_callback_phase_t;
typedef void (tf_http_message_callback)(tf_http_request_t* request, int op_code, const void* data, size_t size);
typedef struct _tf_http_request_t
{
tf_http_callback_phase_t phase;
tf_http_connection_t* connection;
const char* method;
const char* path;
int flags;
const char* query;
void* body;
size_t content_length;
struct phr_header* headers;
int headers_count;
tf_http_message_callback* on_message;
void* context;
void* user_data;
int ref_count;
} tf_http_request_t;
typedef enum _tf_http_handler_flags_t
{
k_tf_http_handler_flag_none = 0,
k_tf_http_handler_flag_websocket = 1,
} tf_http_handler_flags_t;
typedef void (tf_http_callback_t)(tf_http_request_t* request);
tf_http_t* tf_http_create(uv_loop_t* loop);
void tf_http_listen(tf_http_t* http, int port);
void tf_http_add_handler(tf_http_t* http, const char* pattern, int flags, tf_http_callback_t* callback, void* user_data);
void tf_http_respond(tf_http_request_t* request, int status, const char** headers, int headers_count, const void* body, size_t content_length);
size_t tf_http_get_body(const tf_http_request_t* request, const void** out_data);
void tf_http_destroy(tf_http_t* http);
void tf_http_request_ref(tf_http_request_t* request);
void tf_http_request_release(tf_http_request_t* request);
const char* tf_http_request_get_header(tf_http_request_t* request, const char* name);
void tf_http_request_send(tf_http_request_t* request, const void* data, size_t size);

327
src/httpd.js.c Normal file
View File

@ -0,0 +1,327 @@
#include "httpd.js.h"
#include "http.h"
#include "log.h"
#include "mem.h"
#include "task.h"
#include "util.js.h"
#include "picohttpparser.h"
#include <stdlib.h>
#include <string.h>
#include <openssl/sha.h>
#if !defined(__APPLE__) && !defined(__OpenBSD__) && !defined(_WIN32)
#include <alloca.h>
#endif
static JSClassID _httpd_class_id;
static JSClassID _httpd_request_class_id;
typedef struct _http_handler_data_t
{
JSContext* context;
JSValue callback;
} http_handler_data_t;
static JSValue _httpd_response_write_head(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
JS_SetPropertyStr(context, this_val, "response_status", JS_DupValue(context, argv[0]));
JS_SetPropertyStr(context, this_val, "response_headers", JS_DupValue(context, argv[1]));
return JS_UNDEFINED;
}
static JSValue _httpd_response_end(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
tf_http_request_t* request = JS_GetOpaque(this_val, _httpd_request_class_id);
size_t length = 0;
const void* data = NULL;
JSValue buffer;
if (JS_IsString(argv[0]))
{
data = JS_ToCStringLen(context, &length, argv[0]);
}
else if ((data = tf_util_try_get_array_buffer(context, &length, argv[0])) != 0)
{
}
else
{
size_t offset;
size_t size;
size_t element_size;
buffer = tf_util_try_get_typed_array_buffer(context, argv[0], &offset, &size, &element_size);
if (!JS_IsException(buffer))
{
data = tf_util_try_get_array_buffer(context, &length, buffer);
}
}
JSValue response_status = JS_GetPropertyStr(context, this_val, "response_status");
int status = 0;
JS_ToInt32(context, &status, response_status);
JS_FreeValue(context, response_status);
const char** headers = NULL;
int headers_length = 0;
JSValue response_headers = JS_GetPropertyStr(context, this_val, "response_headers");
JSPropertyEnum* ptab;
uint32_t plen;
JS_GetOwnPropertyNames(context, &ptab, &plen, response_headers, JS_GPN_STRING_MASK);
headers = alloca(sizeof(const char*) * plen * 2);
headers_length = plen;
for (uint32_t i = 0; i < plen; ++i)
{
JSValue key = JS_AtomToString(context, ptab[i].atom);
JSPropertyDescriptor desc;
JSValue key_value = JS_NULL;
if (JS_GetOwnProperty(context, &desc, response_headers, ptab[i].atom) == 1)
{
key_value = desc.value;
JS_FreeValue(context, desc.setter);
JS_FreeValue(context, desc.getter);
}
headers[i * 2 + 0] = JS_ToCString(context, key);
headers[i * 2 + 1] = JS_ToCString(context, key_value);
}
tf_http_respond(request, status, headers, headers_length, data, length);
for (int i = 0; i < headers_length * 2; i++)
{
JS_FreeCString(context, headers[i]);
}
for (uint32_t i = 0; i < plen; ++i)
{
JS_FreeAtom(context, ptab[i].atom);
}
js_free(context, ptab);
JS_FreeValue(context, buffer);
return JS_UNDEFINED;
}
static JSValue _httpd_response_send(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
tf_http_request_t* request = JS_GetOpaque(this_val, _httpd_request_class_id);
int opcode = 0x1;
JS_ToInt32(context, &opcode, argv[1]);
uint64_t length = 0;
size_t length_size = 0;
const char* message = JS_ToCStringLen(context, &length_size, argv[0]);
length = length_size;
uint8_t* copy = tf_malloc(length + 16);
bool fin = true;
size_t header = 1;
copy[0] = (fin ? (1 << 7) : 0) | (opcode & 0xf);
if (length < 126)
{
copy[1] = length;
header += 1;
}
else if (length < (1 << 16))
{
copy[1] = 126;
copy[2] = (length >> 8) & 0xff;
copy[3] = (length >> 0) & 0xff;
header += 3;
}
else
{
uint32_t high = (length >> 32) & 0xffffffff;
uint32_t low = (length >> 0) & 0xffffffff;
copy[1] = 127;
copy[2] = (high >> 24) & 0xff;
copy[3] = (high >> 16) & 0xff;
copy[4] = (high >> 8) & 0xff;
copy[5] = (high >> 0) & 0xff;
copy[6] = (low >> 24) & 0xff;
copy[7] = (low >> 16) & 0xff;
copy[8] = (low >> 8) & 0xff;
copy[9] = (low >> 0) & 0xff;
header += 9;
}
memcpy(copy + header, message, length);
tf_http_request_send(request, copy, header + length);
tf_free(copy);
JS_FreeCString(context, message);
return JS_UNDEFINED;
}
static void _httpd_message_callback(tf_http_request_t* request, int op_code, const void* data, size_t size)
{
JSContext* context = request->context;
JSValue response_object = JS_MKPTR(JS_TAG_OBJECT, request->user_data);
JSValue on_message = JS_GetPropertyStr(context, response_object, "onMessage");
JSValue event = JS_NewObject(context);
JS_SetPropertyStr(context, event, "opCode", JS_NewInt32(context, op_code));
JS_SetPropertyStr(context, event, "data", JS_NewStringLen(context, data, size));
JSValue response = JS_Call(context, on_message, JS_UNDEFINED, 1, &event);
tf_util_report_error(context, response);
JS_FreeValue(context, event);
JS_FreeValue(context, on_message);
}
static void _httpd_callback(tf_http_request_t* request)
{
if (request->flags & k_tf_http_handler_flag_websocket)
{
const char* header_connection = tf_http_request_get_header(request, "connection");
const char* header_upgrade = tf_http_request_get_header(request, "upgrade");
const char* header_sec_websocket_key = tf_http_request_get_header(request, "sec-websocket-key");
if (header_connection &&
header_upgrade &&
header_sec_websocket_key &&
strstr(header_connection, "Upgrade") &&
strcasecmp(header_upgrade, "websocket") == 0)
{
static const char* k_magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
size_t key_length = strlen(header_sec_websocket_key);
size_t size = key_length + 36;
uint8_t* key_magic = alloca(size);
memcpy(key_magic, header_sec_websocket_key, key_length);
memcpy(key_magic + key_length, k_magic, 36);
uint8_t digest[20];
SHA1(key_magic, size, digest);
char key[41] = { 0 };
tf_base64_encode(digest, sizeof(digest), key, sizeof(key));
enum { k_headers_count = 4 };
const char* headers[k_headers_count * 2] =
{
"Upgrade", "websocket",
"Connection", "Upgrade",
"Sec-WebSocket-Accept", key,
"Sec-WebSocket-Version", "13",
};
bool send_version =
!tf_http_request_get_header(request, "sec-websocket-version") ||
strcmp(tf_http_request_get_header(request, "sec-websocket-version"), "13") != 0;
tf_http_respond(request, 101, headers, send_version ? k_headers_count : (k_headers_count - 1), NULL, 0);
}
}
http_handler_data_t* data = request->user_data;
JSContext* context = data->context;
JSValue request_object = JS_NewObject(context);
JS_SetPropertyStr(context, request_object, "method", JS_NewString(context, request->method));
JS_SetPropertyStr(context, request_object, "uri", JS_NewString(context, request->path));
JSValue headers = JS_NewObject(context);
for (int i = 0; i < request->headers_count; i++)
{
JS_SetPropertyStr(context, headers, request->headers[i].name, JS_NewString(context, request->headers[i].value));
}
JS_SetPropertyStr(context, request_object, "headers", headers);
if (request->query)
{
JS_SetPropertyStr(context, request_object, "query", JS_NewString(context, request->query));
}
if (request->body)
{
JS_SetPropertyStr(context, request_object, "body", tf_util_new_uint8_array(context, request->body, request->content_length));
}
JSValue client = JS_NewObject(context);
JS_SetPropertyStr(context, client, "tls", JS_FALSE);
JS_SetPropertyStr(context, request_object, "client", client);
JSValue response_object = JS_NewObjectClass(context, _httpd_request_class_id);
tf_http_request_ref(request);
JS_SetOpaque(response_object, request);
JS_SetPropertyStr(context, response_object, "writeHead", JS_NewCFunction(context, _httpd_response_write_head, "writeHead", 2));
JS_SetPropertyStr(context, response_object, "end", JS_NewCFunction(context, _httpd_response_end, "end", 1));
JS_SetPropertyStr(context, response_object, "send", JS_NewCFunction(context, _httpd_response_send, "send", 2));
JSValue args[] =
{
request_object,
response_object,
};
JSValue response = JS_Call(context, data->callback, JS_UNDEFINED, 2, args);
tf_util_report_error(context, response);
JS_FreeValue(context, request_object);
//JS_FreeValue(context, response_object);
JS_FreeValue(context, response);
request->on_message = _httpd_message_callback;
request->context = context;
request->user_data = JS_VALUE_GET_PTR(response_object);
}
static JSValue _httpd_all(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
tf_http_t* http = JS_GetOpaque(this_val, _httpd_class_id);
const char* pattern = JS_ToCString(context, argv[0]);
http_handler_data_t* data = tf_malloc(sizeof(http_handler_data_t));
*data = (http_handler_data_t) { .context = context, .callback = JS_DupValue(context, argv[1]) };
tf_http_add_handler(http, pattern, k_tf_http_handler_flag_none, _httpd_callback, data);
JS_FreeCString(context, pattern);
return JS_UNDEFINED;
}
static JSValue _httpd_register_socket_handler(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
tf_http_t* http = JS_GetOpaque(this_val, _httpd_class_id);
const char* pattern = JS_ToCString(context, argv[0]);
http_handler_data_t* data = tf_malloc(sizeof(http_handler_data_t));
*data = (http_handler_data_t) { .context = context, .callback = JS_DupValue(context, argv[1]) };
tf_http_add_handler(http, pattern, k_tf_http_handler_flag_websocket, _httpd_callback, data);
JS_FreeCString(context, pattern);
return JS_UNDEFINED;
}
static JSValue _httpd_start(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
tf_http_t* http = JS_GetOpaque(this_val, _httpd_class_id);
int port = 0;
JS_ToInt32(context, &port, argv[0]);
tf_http_listen(http, port);
return JS_UNDEFINED;
}
void _httpd_finalizer(JSRuntime* runtime, JSValue value)
{
tf_http_t* http = JS_GetOpaque(value, _httpd_class_id);
tf_http_destroy(http);
}
void _httpd_request_finalizer(JSRuntime* runtime, JSValue value)
{
tf_http_request_t* request = JS_GetOpaque(value, _httpd_request_class_id);
tf_http_request_release(request);
}
void tf_httpd_register(JSContext* context)
{
JS_NewClassID(&_httpd_class_id);
JS_NewClassID(&_httpd_request_class_id);
JSClassDef httpd_def =
{
.class_name = "Httpd",
.finalizer = &_httpd_finalizer,
};
if (JS_NewClass(JS_GetRuntime(context), _httpd_class_id, &httpd_def) != 0)
{
fprintf(stderr, "Failed to register Httpd.\n");
}
JSClassDef request_def =
{
.class_name = "Request",
.finalizer = &_httpd_request_finalizer,
};
if (JS_NewClass(JS_GetRuntime(context), _httpd_request_class_id, &request_def) != 0)
{
fprintf(stderr, "Failed to register Request.\n");
}
JSValue global = JS_GetGlobalObject(context);
JSValue httpd = JS_NewObjectClass(context, _httpd_class_id);
tf_task_t* task = tf_task_get(context);
uv_loop_t* loop = tf_task_get_loop(task);
tf_http_t* http = tf_http_create(loop);
JS_SetOpaque(httpd, http);
JS_SetPropertyStr(context, httpd, "handlers", JS_NewObject(context));
JS_SetPropertyStr(context, httpd, "all", JS_NewCFunction(context, _httpd_all, "all", 2));
JS_SetPropertyStr(context, httpd, "registerSocketHandler", JS_NewCFunction(context, _httpd_register_socket_handler, "register_socket_handler", 2));
JS_SetPropertyStr(context, httpd, "start", JS_NewCFunction(context, _httpd_start, "start", 1));
JS_SetPropertyStr(context, global, "httpdc", httpd);
JS_FreeValue(context, global);
}

5
src/httpd.js.h Normal file
View File

@ -0,0 +1,5 @@
#pragma once
#include "quickjs.h"
void tf_httpd_register(JSContext* context);

View File

@ -688,15 +688,6 @@ void tf_run_thread_start(const char* zip_path)
.args = args, .args = args,
}; };
uv_thread_create(thread, _tf_run_task_thread, data); uv_thread_create(thread, _tf_run_task_thread, data);
#if 0
uv_thread_join(&threads[i]);
if (data[i].result != 0)
{
result = data[i].result;
}
tf_free(data);
tf_free(threads);
#endif
} }
#else #else
int main(int argc, char* argv[]) int main(int argc, char* argv[])

View File

@ -130,7 +130,8 @@ static void _socket_gc_mark(JSRuntime* runtime, JSValueConst value, JS_MarkFunc
JSValue tf_socket_register(JSContext* context) JSValue tf_socket_register(JSContext* context)
{ {
JS_NewClassID(&_classId); JS_NewClassID(&_classId);
JSClassDef def = { JSClassDef def =
{
.class_name = "Socket", .class_name = "Socket",
.finalizer = &_socket_finalizer, .finalizer = &_socket_finalizer,
.gc_mark = _socket_gc_mark, .gc_mark = _socket_gc_mark,
@ -269,7 +270,7 @@ void _socket_close_internal(socket_t* socket)
} }
} }
void _socket_finalizer(JSRuntime *runtime, JSValue value) void _socket_finalizer(JSRuntime* runtime, JSValue value)
{ {
socket_t* socket = JS_GetOpaque(value, _classId); socket_t* socket = JS_GetOpaque(value, _classId);
socket->_object = JS_UNDEFINED; socket->_object = JS_UNDEFINED;

View File

@ -1881,10 +1881,6 @@ static void _tf_ssb_connection_destroy(tf_ssb_connection_t* connection, const ch
{ {
uv_close((uv_handle_t*)&connection->tcp, _tf_ssb_connection_on_close); uv_close((uv_handle_t*)&connection->tcp, _tf_ssb_connection_on_close);
} }
if (connection->connect.data && !uv_is_closing((uv_handle_t*)&connection->connect))
{
uv_close((uv_handle_t*)&connection->connect, _tf_ssb_connection_on_close);
}
if (JS_IsUndefined(connection->object) && if (JS_IsUndefined(connection->object) &&
!connection->async.data && !connection->async.data &&

View File

@ -1301,6 +1301,7 @@ typedef struct _following_t
int depth; int depth;
int ref_count; int ref_count;
int block_ref_count; int block_ref_count;
bool populated;
} following_t; } following_t;
static int _following_compare(const void* a, const void* b) static int _following_compare(const void* a, const void* b)
@ -1310,6 +1311,11 @@ static int _following_compare(const void* a, const void* b)
return strcmp(ida, (*followingb)->id); return strcmp(ida, (*followingb)->id);
} }
static bool _has_following_entry(const char* id, following_t** list, int count)
{
return count ? bsearch(id, list, count, sizeof(following_t*), _following_compare) != 0 : false;
}
static bool _add_following_entry(following_t*** list, int* count, following_t* add) static bool _add_following_entry(following_t*** list, int* count, following_t* add)
{ {
int index = tf_util_insert_index(add->id, *list, *count, sizeof(following_t*), _following_compare); int index = tf_util_insert_index(add->id, *list, *count, sizeof(following_t*), _following_compare);
@ -1343,19 +1349,37 @@ static bool _remove_following_entry(following_t*** list, int* count, following_t
return false; return false;
} }
static following_t* _get_following(tf_ssb_t* ssb, const char* id, following_t*** following, int* following_count, int depth, int max_depth) typedef struct _block_node_t block_node_t;
typedef struct _block_node_t
{ {
following_t* entry;
block_node_t* parent;
} block_node_t;
static bool _is_blocked_by_active_blocks(const char* id, const block_node_t* blocks)
{
for (const block_node_t* b = blocks; b; b = b->parent)
{
if (_has_following_entry(id, b->entry->blocking, b->entry->blocking_count))
{
return true;
}
}
return false;
}
static following_t* _make_following_node(const char* id, following_t*** following, int* following_count, block_node_t* blocks)
{
if (_is_blocked_by_active_blocks(id, blocks))
{
return NULL;
}
int index = tf_util_insert_index(id, *following, *following_count, sizeof(following_t*), _following_compare); int index = tf_util_insert_index(id, *following, *following_count, sizeof(following_t*), _following_compare);
following_t* entry = NULL; following_t* entry = NULL;
bool already_populated = false;
if (index < *following_count && strcmp(id, (*following)[index]->id) == 0) if (index < *following_count && strcmp(id, (*following)[index]->id) == 0)
{ {
entry = (*following)[index]; entry = (*following)[index];
already_populated = entry->depth < max_depth;
if (depth < entry->depth)
{
entry->depth = depth;
}
} }
else else
{ {
@ -1368,29 +1392,34 @@ static following_t* _get_following(tf_ssb_t* ssb, const char* id, following_t***
(*following)[index] = entry; (*following)[index] = entry;
(*following_count)++; (*following_count)++;
memset(entry, 0, sizeof(*entry)); memset(entry, 0, sizeof(*entry));
entry->depth = INT_MAX;
snprintf(entry->id, sizeof(entry->id), "%s", id); snprintf(entry->id, sizeof(entry->id), "%s", id);
entry->depth = depth;
} }
return entry;
}
if (depth < max_depth && !already_populated) static void _populate_follows_and_blocks(tf_ssb_t* ssb, following_t* entry, following_t*** following, int* following_count, block_node_t* active_blocks)
{ {
sqlite3* db = tf_ssb_acquire_db_reader(ssb); sqlite3* db = tf_ssb_acquire_db_reader(ssb);
sqlite3_stmt* statement = NULL; sqlite3_stmt* statement = NULL;
if (sqlite3_prepare(db, "SELECT json_extract(content, '$.contact'), json_extract(content, '$.following'), json_extract(content, '$.blocking') FROM messages WHERE author = ? AND json_extract(content, '$.type') = 'contact' ORDER BY sequence", -1, &statement, NULL) == SQLITE_OK) if (sqlite3_prepare(db,
"SELECT json_extract(content, '$.contact') AS contact, json_extract(content, '$.following'), json_extract(content, '$.blocking') "
"FROM messages "
"WHERE contact IS NOT NULL AND author = ? AND json_extract(content, '$.type') = 'contact' "
"ORDER BY sequence",
-1, &statement, NULL) == SQLITE_OK)
{ {
if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK) if (sqlite3_bind_text(statement, 1, entry->id, -1, NULL) == SQLITE_OK)
{ {
while (sqlite3_step(statement) == SQLITE_ROW) while (sqlite3_step(statement) == SQLITE_ROW)
{ {
const char* contact = (const char*)sqlite3_column_text(statement, 0); const char* contact = (const char*)sqlite3_column_text(statement, 0);
if (!contact)
{
continue;
}
if (sqlite3_column_type(statement, 1) != SQLITE_NULL) if (sqlite3_column_type(statement, 1) != SQLITE_NULL)
{ {
bool is_following = sqlite3_column_int(statement, 1) != 0; bool is_following = sqlite3_column_int(statement, 1) != 0;
following_t* next = _get_following(ssb, contact, following, following_count, depth + 1, max_depth); following_t* next = _make_following_node(contact, following, following_count, active_blocks);
if (next)
{
if (is_following) if (is_following)
{ {
if (_add_following_entry(&entry->following, &entry->following_count, next)) if (_add_following_entry(&entry->following, &entry->following_count, next))
@ -1406,10 +1435,13 @@ static following_t* _get_following(tf_ssb_t* ssb, const char* id, following_t***
} }
} }
} }
}
if (sqlite3_column_type(statement, 2) != SQLITE_NULL) if (sqlite3_column_type(statement, 2) != SQLITE_NULL)
{ {
bool is_blocking = sqlite3_column_int(statement, 2) != 0; bool is_blocking = sqlite3_column_int(statement, 2) != 0;
following_t* next = _get_following(ssb, contact, following, following_count, depth + 1, 0 /* don't dig deeper into blocked users */); following_t* next = _make_following_node(contact, following, following_count, active_blocks);
if (next)
{
if (is_blocking) if (is_blocking)
{ {
if (_add_following_entry(&entry->blocking, &entry->blocking_count, next)) if (_add_following_entry(&entry->blocking, &entry->blocking_count, next))
@ -1427,11 +1459,32 @@ static following_t* _get_following(tf_ssb_t* ssb, const char* id, following_t***
} }
} }
} }
}
sqlite3_finalize(statement); sqlite3_finalize(statement);
} }
tf_ssb_release_db_reader(ssb, db); tf_ssb_release_db_reader(ssb, db);
}
static void _get_following(tf_ssb_t* ssb, following_t* entry, following_t*** following, int* following_count, int depth, int max_depth, block_node_t* active_blocks)
{
entry->depth = tf_min(depth, entry->depth);
if (depth < max_depth && !entry->populated && !_is_blocked_by_active_blocks(entry->id, active_blocks))
{
entry->populated = true;
_populate_follows_and_blocks(ssb, entry, following, following_count, active_blocks);
if (depth < max_depth)
{
block_node_t blocks = { .entry = entry, .parent = active_blocks };
for (int i = 0; i < entry->following_count; i++)
{
if (!_has_following_entry(entry->following[i]->id, entry->blocking, entry->blocking_count))
{
_get_following(ssb, entry->following[i], following, following_count, depth + 1, max_depth, &blocks);
}
}
}
} }
return entry;
} }
tf_ssb_following_t* tf_ssb_db_following_deep(tf_ssb_t* ssb, const char** ids, int count, int depth) tf_ssb_following_t* tf_ssb_db_following_deep(tf_ssb_t* ssb, const char** ids, int count, int depth)
@ -1440,7 +1493,8 @@ tf_ssb_following_t* tf_ssb_db_following_deep(tf_ssb_t* ssb, const char** ids, in
int following_count = 0; int following_count = 0;
for (int i = 0; i < count; i++) for (int i = 0; i < count; i++)
{ {
following_t* entry = _get_following(ssb, ids[i], &following, &following_count, 0, depth); following_t* entry = _make_following_node(ids[i], &following, &following_count, NULL);
_get_following(ssb, entry, &following, &following_count, 0, depth, NULL);
entry->ref_count++; entry->ref_count++;
} }
@ -1487,7 +1541,8 @@ const char** tf_ssb_db_following_deep_ids(tf_ssb_t* ssb, const char** ids, int c
int following_count = 0; int following_count = 0;
for (int i = 0; i < count; i++) for (int i = 0; i < count; i++)
{ {
following_t* entry = _get_following(ssb, ids[i], &following, &following_count, 0, depth); following_t* entry = _make_following_node(ids[i], &following, &following_count, NULL);
_get_following(ssb, entry, &following, &following_count, 0, depth, NULL);
entry->ref_count++; entry->ref_count++;
} }

View File

@ -496,6 +496,23 @@ void tf_ssb_test_rooms(const tf_test_options_t* options)
uv_loop_close(&loop); uv_loop_close(&loop);
} }
static void _assert_visible(tf_ssb_t* ssb, const char* id, const char* contact, bool visible)
{
const char** ids = tf_ssb_db_following_deep_ids(ssb, &id, 1, 2);
bool found = false;
(void)found;
for (int i = 0; ids[i]; i++)
{
if (strcmp(ids[i], contact) == 0)
{
found = true;
break;
}
}
tf_free(ids);
assert(found == visible);
}
void tf_ssb_test_following(const tf_test_options_t* options) void tf_ssb_test_following(const tf_test_options_t* options)
{ {
tf_printf("Testing following.\n"); tf_printf("Testing following.\n");
@ -503,91 +520,68 @@ void tf_ssb_test_following(const tf_test_options_t* options)
uv_loop_t loop = { 0 }; uv_loop_t loop = { 0 };
uv_loop_init(&loop); uv_loop_init(&loop);
tf_ssb_t* ssb0 = tf_ssb_create(&loop, NULL, ":memory:"); unlink("out/test_db0.sqlite");
tf_ssb_t* ssb0 = tf_ssb_create(&loop, NULL, "file:out/test_db0.sqlite");
tf_ssb_generate_keys(ssb0); tf_ssb_generate_keys(ssb0);
tf_ssb_t* ssb1 = tf_ssb_create(&loop, NULL, ":memory:"); char id0[k_id_base64_len] = { "@" };
tf_ssb_generate_keys(ssb1); char id1[k_id_base64_len] = { "@" };
char id2[k_id_base64_len] = { "@" };
tf_ssb_t* ssb2 = tf_ssb_create(&loop, NULL, ":memory:"); char id3[k_id_base64_len] = { "@" };
tf_ssb_generate_keys(ssb2); char priv0b[512] = { 0 };
char priv1b[512] = { 0 };
char id0[k_id_base64_len] = { 0 }; char priv2b[512] = { 0 };
char id1[k_id_base64_len] = { 0 }; char priv3b[512] = { 0 };
char id2[k_id_base64_len] = { 0 };
tf_ssb_whoami(ssb0, id0, sizeof(id0));
tf_ssb_whoami(ssb1, id1, sizeof(id1));
tf_ssb_whoami(ssb2, id2, sizeof(id2));
uint8_t priv0[512] = { 0 }; uint8_t priv0[512] = { 0 };
uint8_t priv1[512] = { 0 }; uint8_t priv1[512] = { 0 };
uint8_t priv2[512] = { 0 }; uint8_t priv2[512] = { 0 };
tf_ssb_get_private_key(ssb0, priv0, sizeof(priv0)); uint8_t priv3[512] = { 0 };
tf_ssb_get_private_key(ssb1, priv1, sizeof(priv1));
tf_ssb_get_private_key(ssb2, priv2, sizeof(priv2));
JSContext* context = NULL; tf_ssb_generate_keys_buffer(id0 + 1, sizeof(id0) - 1, priv0b, sizeof(priv0b));
tf_ssb_generate_keys_buffer(id1 + 1, sizeof(id1) - 1, priv1b, sizeof(priv1b));
tf_ssb_generate_keys_buffer(id2 + 1, sizeof(id2) - 1, priv2b, sizeof(priv2b));
tf_ssb_generate_keys_buffer(id3 + 1, sizeof(id3) - 1, priv3b, sizeof(priv3b));
tf_base64_decode(priv0b, strlen(priv0b), priv0, sizeof(priv0));
tf_base64_decode(priv1b, strlen(priv1b), priv1, sizeof(priv1));
tf_base64_decode(priv2b, strlen(priv2b), priv2, sizeof(priv2));
tf_base64_decode(priv3b, strlen(priv3b), priv3, sizeof(priv3));
JSContext* context = tf_ssb_get_context(ssb0);
JSValue message; JSValue message;
JSValue signed_message; JSValue signed_message;
bool stored; bool stored;
#define FOLLOW(ssb, id, priv, follow) \ #define FOLLOW_BLOCK(id, priv, contact, follow, block) \
context = tf_ssb_get_context(ssb); \
message = JS_NewObject(context); \ message = JS_NewObject(context); \
JS_SetPropertyStr(context, message, "type", JS_NewString(context, "contact")); \ JS_SetPropertyStr(context, message, "type", JS_NewString(context, "contact")); \
JS_SetPropertyStr(context, message, "contact", JS_NewString(context, id)); \ JS_SetPropertyStr(context, message, "contact", JS_NewString(context, contact)); \
JS_SetPropertyStr(context, message, "following", follow ? JS_TRUE : JS_FALSE); \ JS_SetPropertyStr(context, message, "following", follow ? JS_TRUE : JS_FALSE); \
signed_message = tf_ssb_sign_message(ssb, id, priv, message); \ JS_SetPropertyStr(context, message, "blocking", block ? JS_TRUE : JS_FALSE); \
signed_message = tf_ssb_sign_message(ssb0, id, priv, message); \
stored = false; \ stored = false; \
tf_ssb_verify_strip_and_store_message(ssb, signed_message, _message_stored, &stored); \ tf_ssb_verify_strip_and_store_message(ssb0, signed_message, _message_stored, &stored); \
_wait_stored(ssb, &stored); \ _wait_stored(ssb0, &stored); \
JS_FreeValue(context, signed_message); \ JS_FreeValue(context, signed_message); \
JS_FreeValue(context, message); \ JS_FreeValue(context, message); \
context = NULL
#if 1 FOLLOW_BLOCK(id0, priv0, id1, true, false);
/* TODO: This test doesn't actually really test anything anymore. */ FOLLOW_BLOCK(id1, priv1, id2, true, false);
#define DUMP(id, depth) FOLLOW_BLOCK(id1, priv1, id3, true, false);
#else _assert_visible(ssb0, id0, id0, true);
#define DUMP(id, depth) \ _assert_visible(ssb0, id0, id1, true);
do \ _assert_visible(ssb0, id0, id2, true);
{ \ _assert_visible(ssb0, id0, id3, true);
tf_printf("following %d:\n", depth); \ FOLLOW_BLOCK(id0, priv0, id3, false, true);
const char** tf_ssb_get_following_deep(tf_ssb_t* ssb_param, const char** ids, int depth_param); \ _assert_visible(ssb0, id0, id0, true);
const char** f = tf_ssb_get_following_deep(ssb0, (const char*[]) { id, NULL }, depth); \ _assert_visible(ssb0, id0, id1, true);
for (const char** p = f; p && *p; p++) \ _assert_visible(ssb0, id0, id2, true);
{ \ _assert_visible(ssb0, id0, id3, false);
tf_printf("* %s\n", *p); \
} \
tf_printf("\n"); \
tf_free(f); \
} \
while (0)
#endif
FOLLOW(ssb0, id1, priv1, true); #undef FOLLOW_BLOCK
FOLLOW(ssb1, id2, priv2, true);
FOLLOW(ssb2, id0, priv0, true);
DUMP(id0, 2);
DUMP(id1, 2);
DUMP(id2, 2);
FOLLOW(ssb0, id1, priv1, false);
//FOLLOW(ssb0, id1, priv1, true);
//FOLLOW(ssb0, id1, priv1, true);
DUMP(id0, 1);
DUMP(id1, 2);
//FOLLOW(ssb0, id1, priv1, false);
//DUMP(1);
//DUMP(1);
#undef FOLLOW
#undef DUMP
uv_run(&loop, UV_RUN_DEFAULT); uv_run(&loop, UV_RUN_DEFAULT);
tf_ssb_destroy(ssb0); tf_ssb_destroy(ssb0);
tf_ssb_destroy(ssb1);
tf_ssb_destroy(ssb2);
uv_loop_close(&loop); uv_loop_close(&loop);
} }

View File

@ -3,6 +3,7 @@
#include "bcrypt.js.h" #include "bcrypt.js.h"
#include "database.js.h" #include "database.js.h"
#include "file.js.h" #include "file.js.h"
#include "httpd.js.h"
#include "log.h" #include "log.h"
#include "mem.h" #include "mem.h"
#include "packetstream.h" #include "packetstream.h"
@ -1731,6 +1732,7 @@ void tf_task_activate(tf_task_t* task)
JS_SetPropertyStr(context, global, "TlsContext", tf_tls_context_register(context)); JS_SetPropertyStr(context, global, "TlsContext", tf_tls_context_register(context));
tf_file_register(context); tf_file_register(context);
tf_database_register(context); tf_database_register(context);
tf_httpd_register(context);
task->_ssb = tf_ssb_create(&task->_loop, task->_context, task->_db_path); task->_ssb = tf_ssb_create(&task->_loop, task->_context, task->_db_path);
tf_ssb_set_trace(task->_ssb, task->_trace); tf_ssb_set_trace(task->_ssb, task->_trace);

View File

@ -1,5 +1,6 @@
#include "tests.h" #include "tests.h"
#include "http.h"
#include "log.h" #include "log.h"
#include "mem.h" #include "mem.h"
#include "ssb.tests.h" #include "ssb.tests.h"
@ -11,6 +12,8 @@
#include <string.h> #include <string.h>
#include <unistd.h> #include <unistd.h>
#include <uv.h>
#if defined(_WIN32) #if defined(_WIN32)
#define WIFEXITED(x) 1 #define WIFEXITED(x) 1
#define WEXITSTATUS(x) (x) #define WEXITSTATUS(x) (x)
@ -667,6 +670,95 @@ static void _test_b64(const tf_test_options_t* options)
unlink("out/test.js"); unlink("out/test.js");
} }
typedef struct _test_http_t
{
uv_loop_t* loop;
uv_async_t async;
bool done;
} test_http_t;
static void _test_http_async(uv_async_t* async)
{
}
static void _test_http_thread(void* data)
{
#if defined(__linux__)
test_http_t* test = data;
int r = system("curl -v http://localhost:23456/404");
assert(WEXITSTATUS(r) == 0);
tf_printf("curl returned %d\n", WEXITSTATUS(r));
r = system("curl -v http://localhost:23456/hello");
assert(WEXITSTATUS(r) == 0);
tf_printf("curl returned %d\n", WEXITSTATUS(r));
r = system("curl -v --data 'hello world' http://localhost:23456/post");
assert(WEXITSTATUS(r) == 0);
tf_printf("curl returned %d\n", WEXITSTATUS(r));
r = system("curl -v http://localhost:23456/hello http://localhost:23456/hello http://localhost:23456/hello");
assert(WEXITSTATUS(r) == 0);
tf_printf("curl returned %d\n", WEXITSTATUS(r));
test->done = true;
/* All to wake up the loop. */
uv_async_send(&test->async);
#endif
}
static void _test_http_handler(tf_http_request_t* request)
{
const char* headers[] =
{
"User-Agent", "TildeFriends/1.0",
};
const char* k_payload = "Hello, world!\n";
tf_http_respond(request, 200, headers, 1, k_payload, strlen(k_payload));
}
static void _test_http_handler_post(tf_http_request_t* request)
{
const void* body = NULL;
size_t size = tf_http_get_body(request, &body);
tf_printf("size = %zd body=%.*s\n", size, (int)size, (const char*)body);
const char* headers[] =
{
"Connection", "close",
};
const char* k_payload = "Hello, world!\n";
tf_http_respond(request, 200, headers, 1, k_payload, strlen(k_payload));
}
static void _test_http(const tf_test_options_t* options)
{
tf_printf("Starting http.\n");
uv_loop_t loop = { 0 };
uv_loop_init(&loop);
tf_http_t* http = tf_http_create(&loop);
tf_http_add_handler(http, "/hello", k_tf_http_handler_flag_none, _test_http_handler, NULL);
tf_http_add_handler(http, "/post", k_tf_http_handler_flag_none, _test_http_handler_post, NULL);
tf_http_listen(http, 23456);
test_http_t test = { .loop = &loop };
uv_async_init(&loop, &test.async, _test_http_async);
uv_thread_t thread = { 0 };
uv_thread_create(&thread, _test_http_thread, &test);
while (!test.done)
{
uv_run(&loop, UV_RUN_ONCE);
}
uv_close((uv_handle_t*)&test.async, NULL);
tf_printf("Done running.\n");
tf_http_destroy(http);
uv_run(&loop, UV_RUN_DEFAULT);
uv_loop_close(&loop);
uv_thread_join(&thread);
}
static void _tf_test_run(const tf_test_options_t* options, const char* name, void (*test)(const tf_test_options_t* options), bool opt_in) static void _tf_test_run(const tf_test_options_t* options, const char* name, void (*test)(const tf_test_options_t* options), bool opt_in)
{ {
bool specified = false; bool specified = false;
@ -706,6 +798,7 @@ static void _tf_test_run(const tf_test_options_t* options, const char* name, voi
void tf_tests(const tf_test_options_t* options) void tf_tests(const tf_test_options_t* options)
{ {
#if !TARGET_OS_IPHONE #if !TARGET_OS_IPHONE
_tf_test_run(options, "http", _test_http, false);
_tf_test_run(options, "ssb", tf_ssb_test_ssb, false); _tf_test_run(options, "ssb", tf_ssb_test_ssb, false);
_tf_test_run(options, "ssb_id", tf_ssb_test_id_conversion, false); _tf_test_run(options, "ssb_id", tf_ssb_test_id_conversion, false);
_tf_test_run(options, "ssb_following", tf_ssb_test_following, false); _tf_test_run(options, "ssb_following", tf_ssb_test_following, false);

View File

@ -5,6 +5,7 @@
#include "trace.h" #include "trace.h"
#include "mem.h" #include "mem.h"
#include "util.js.h"
#include "sqlite3.h" #include "sqlite3.h"
#include "uv.h" #include "uv.h"
@ -15,9 +16,7 @@
#include <time.h> #include <time.h>
#include <unistd.h> #include <unistd.h>
#if !defined(_countof) #define tf_countof(a) ((int)(sizeof((a)) / sizeof(*(a))))
#define _countof(a) ((int)(sizeof((a)) / sizeof(*(a))))
#endif
enum enum
{ {
@ -138,7 +137,7 @@ void tf_trace_counter(tf_trace_t* trace, const char* name, int argc, const char*
{ {
p += snprintf(line + p, sizeof(line) - p, "\"%s\": %" PRId64 "%s", arg_names[i], arg_values[i], i == argc - 1 ? "}}," : ", "); p += snprintf(line + p, sizeof(line) - p, "\"%s\": %" PRId64 "%s", arg_names[i], arg_values[i], i == argc - 1 ? "}}," : ", ");
} }
p = tf_min(p, tf_countof(line));
trace->callback(trace, line, p, trace->user_data); trace->callback(trace, line, p, trace->user_data);
} }
@ -228,7 +227,7 @@ static tf_trace_thread_t* _tf_trace_get_thread(tf_trace_t* trace, pthread_t self
static void _tf_push_stack(tf_trace_t* trace, pthread_t self, const char* name, void* tag) static void _tf_push_stack(tf_trace_t* trace, pthread_t self, const char* name, void* tag)
{ {
tf_trace_thread_t* thread = _tf_trace_get_thread(trace, self); tf_trace_thread_t* thread = _tf_trace_get_thread(trace, self);
if (!thread->stack || thread->stack->count + 1 > (int)_countof(thread->stack->names)) if (!thread->stack || thread->stack->count + 1 > tf_countof(thread->stack->names))
{ {
tf_trace_stack_t* stack = tf_malloc(sizeof(tf_trace_stack_t)); tf_trace_stack_t* stack = tf_malloc(sizeof(tf_trace_stack_t));
memset(stack, 0, sizeof(*stack)); memset(stack, 0, sizeof(*stack));
@ -236,7 +235,7 @@ static void _tf_push_stack(tf_trace_t* trace, pthread_t self, const char* name,
thread->stack = stack; thread->stack = stack;
} }
tf_trace_stack_t* stack = thread->stack; tf_trace_stack_t* stack = thread->stack;
while (stack->count == 0 && stack->next && stack->next->count + 1 <= (int)_countof(thread->stack->names)) while (stack->count == 0 && stack->next && stack->next->count + 1 <= tf_countof(thread->stack->names))
{ {
stack = stack->next; stack = stack->next;
} }
@ -279,6 +278,7 @@ static void _tf_trace_begin_tagged(tf_trace_t* trace, const char* name, void* ta
int p = snprintf(line, sizeof(line), "{\"ph\": \"B\", \"pid\": %d, \"tid\": \"0x%" PRIx64 "\", \"ts\": %" PRId64 ", \"name\": \"", getpid(), (int64_t)self, _trace_ts()); int p = snprintf(line, sizeof(line), "{\"ph\": \"B\", \"pid\": %d, \"tid\": \"0x%" PRIx64 "\", \"ts\": %" PRId64 ", \"name\": \"", getpid(), (int64_t)self, _trace_ts());
p += _tf_trace_escape_name(line + p, sizeof(line) - p, name); p += _tf_trace_escape_name(line + p, sizeof(line) - p, name);
p += snprintf(line + p, sizeof(line) - p, "\"},"); p += snprintf(line + p, sizeof(line) - p, "\"},");
p = tf_min(p, tf_countof(line));
trace->callback(trace, line, p, trace->user_data); trace->callback(trace, line, p, trace->user_data);
} }
@ -305,6 +305,7 @@ static void _tf_trace_end_tagged(tf_trace_t* trace, void* tag)
int p = snprintf(line, sizeof(line), "{\"ph\": \"E\", \"pid\": %d, \"tid\": \"0x%" PRIx64 "\", \"ts\": %" PRId64 ", \"name\": \"", getpid(), (int64_t)pthread_self(), _trace_ts()); int p = snprintf(line, sizeof(line), "{\"ph\": \"E\", \"pid\": %d, \"tid\": \"0x%" PRIx64 "\", \"ts\": %" PRId64 ", \"name\": \"", getpid(), (int64_t)pthread_self(), _trace_ts());
p += _tf_trace_escape_name(line + p, sizeof(line) - p, name); p += _tf_trace_escape_name(line + p, sizeof(line) - p, name);
p += snprintf(line + p, sizeof(line) - p, "\"},"); p += snprintf(line + p, sizeof(line) - p, "\"},");
p = tf_min(p, tf_countof(line));
trace->callback(trace, line, p, trace->user_data); trace->callback(trace, line, p, trace->user_data);
} }

View File

@ -180,7 +180,12 @@ bool tf_util_report_error(JSContext* context, JSValue value)
else if (JS_IsException(value)) else if (JS_IsException(value))
{ {
tf_task_t* task = tf_task_get(context); tf_task_t* task = tf_task_get(context);
tf_task_send_error_to_parent(task, value); if (!tf_task_send_error_to_parent(task, value))
{
JSValue exception = JS_GetException(context);
tf_util_report_error(context, exception);
JS_FreeValue(context, exception);
}
is_error = true; is_error = true;
} }
return is_error; return is_error;

View File

@ -22,4 +22,4 @@ const char* tf_util_backtrace_string();
const char* tf_util_function_to_string(void* function); const char* tf_util_function_to_string(void* function);
#define tf_min(a, b) ({ __typeof__ (a) _a = (a); __typeof__ (b) _b = (b); _a > _b ? _a : _b; }) #define tf_min(a, b) ({ __typeof__ (a) _a = (a); __typeof__ (b) _b = (b); _a > _b ? _b : _a; })

View File

@ -1,2 +1,2 @@
#define VERSION_NUMBER "0.0.13" #define VERSION_NUMBER "0.0.14"
#define VERSION_NAME "Served on grilled naan or gluten free sweet potato flatbread." #define VERSION_NAME "Served on apple cider dressed winter greens."

View File

@ -7,6 +7,7 @@ from selenium.webdriver.support.ui import WebDriverWait
import os import os
import subprocess import subprocess
import sys
import time import time
for path in ('out/selenium.sqlite', 'out/selenium.sqlite-shm', 'out/selenium.sqlite-wal'): for path in ('out/selenium.sqlite', 'out/selenium.sqlite-shm', 'out/selenium.sqlite-wal'):
@ -14,7 +15,7 @@ for path in ('out/selenium.sqlite', 'out/selenium.sqlite-shm', 'out/selenium.sql
os.unlink(path) os.unlink(path)
except: except:
pass pass
tf = subprocess.Popen(['out/debug/tildefriends', 'run', '-d', 'out/selenium.sqlite', '-b', '0', '-p', '8888']) tf = subprocess.Popen(['out/debug/tildefriends', 'run', '-d', 'out/selenium.sqlite', '-b', '0', '-p', '8888'] + sys.argv[1:])
def exists_in_shadow_root(shadow_root, by, value): def exists_in_shadow_root(shadow_root, by, value):
return lambda driver: shadow_root.find_element(by, value) return lambda driver: shadow_root.find_element(by, value)

View File

@ -1,6 +0,0 @@
#!/usr/bin/env bash -e
mkdir -p out/cory.app
cp src/ios/Info.plist out/cory.app
xcrun --sdk iphonesimulator clang src/ios.m -arch x86_64 -o out/cory.app/cory -framework CoreFoundation -framework UIKit -framework WebKit -framework Foundation
xcrun simctl install booted out/cory.app
xcrun simctl launch booted cory