aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/design/008-module-format.md9
-rw-r--r--prototypes/connections/module.c66
-rw-r--r--prototypes/connections/module.h9
-rw-r--r--prototypes/connections/test-module.c51
4 files changed, 110 insertions, 25 deletions
diff --git a/docs/design/008-module-format.md b/docs/design/008-module-format.md
index b77ec9c..0f0a6d8 100644
--- a/docs/design/008-module-format.md
+++ b/docs/design/008-module-format.md
@@ -30,7 +30,7 @@ v5에는 뒤에 집계(FOLD), 걸러진 표, 마크, 그리고 **키 있는 리�
| 40 | 상태 × 16 B: u32 이름, u32 타입(0 수, 1 text, 2 커서, 3 리스트, 4 인덱스), 8 B 값: 수는 f64 초기값, text는 u32 초기 상수, 커서는 u32 대상 text 상태와 u32 플래그(1이면 마크: 그 자리에 넣은 글 앞에 남는다), 리스트는 u32 레코드와 u32 키(0이면 없음, 아니면 키 필드 + 1), 인덱스는 u32 대상 리스트 상태 |
| | 컬렉션 × 12 B: u32 이름, u32 1(바이트), u32 1(키별) |
| | 입력 × 16 B: u32 이름, u8 종류(1 키, 2 메시지, 3 text, 4 타이머), u8 키별 여부, u16 0, u32 키 이름(키 입력)·주기(타이머, 1 ms–24시간을 ms로) 또는 0, u16 첫 필드, u16 필드 수 |
-| | 필드 × 8 B: u32 이름, u32 타입(1 u32le, 2 바이트, 3 수(binary64), 4 text). 페이로드는 접두부와 가변 필드의 바이트다. 접두부에는 선언 순서로 고정 필드의 값과 가변 필드(바이트·text)의 u32 길이가 들어가는데, 마지막 가변 필드는 길이 없이 나머지를 차지한다. 가변 필드의 바이트는 접두부 뒤에 선언 순서로 이어진다(가변 필드가 하나면 예전의 "바이트 꼬리"와 같다) |
+| | 필드 × 8 B: u32 이름(식별자, 또는 그룹 안의 필드면 점으로 이은 경로 `user.name`), u32 타입(1 u32le, 2 바이트, 3 수(binary64), 4 text). 페이로드는 접두부와 가변 필드의 바이트다. 접두부에는 선언 순서로 고정 필드의 값과 가변 필드(바이트·text)의 u32 길이가 들어가는데, 마지막 가변 필드는 길이 없이 나머지를 차지한다. 가변 필드의 바이트는 접두부 뒤에 선언 순서로 이어진다(가변 필드가 하나면 예전의 "바이트 꼬리"와 같다) |
| | 상수 × 8 B: u32 블롭 오프셋, u32 길이 (UTF-8) |
| | 레코드 × 8 B: u32 이름, u16 첫 레코드 필드, u16 필드 수(1–16) |
| | 레코드 필드 × 16 B: u32 이름, u32 타입(0 수, 1 text), 8 B 기본값: 수는 f64, text는 u32 상수 |
@@ -112,11 +112,12 @@ v5에는 뒤에 집계(FOLD), 걸러진 표, 마크, 그리고 **키 있는 리�
## 로드할 때 검증하는 것
- 머리말, 예약 바이트, 명령 패딩이 0인지. 한도(상태 32, 입력 32, 보기 64(열 포함), 핸들러 64, 명령 65,535,
- text 상수 4,096개·1 MiB, 필드 32, 컬렉션 1, 이름 문자열 32 KiB)와 정확한 파일 크기. 명령과 상수는 모듈 크기에 맞춘
- 블록 하나에 담긴다(`PureModule`의 다른 표는 고정 크기로 12 KiB). 그래서 한도를 올려도 작은 앱의 메모리는 그대로다.
+ text 상수 4,096개·1 MiB, 메시지 필드 64, 컬렉션 1, 이름 문자열 32 KiB)와 정확한 파일 크기. 명령과 상수는 모듈 크기에 맞춘
+ 블록 하나에 담긴다(`PureModule`의 다른 표는 고정 크기로 13 KiB). 그래서 한도를 올려도 작은 앱의 메모리는 그대로다.
호스트는 모듈 파일을 크기대로 읽는다(`pure_module_read`).
- 이름은 ASCII 식별자 1–31바이트이고, 상태·레코드·컬렉션·입력·보기가 한 이름공간을 쓴다. 필드 이름은 메시지나
- 레코드 안에서 유일하다. 레코드(최대 8)의 필드 범위는 차례로 이어지고 모두 합쳐 64개 이하다.
+ 레코드 안에서 유일하다. 메시지 필드의 이름은 식별자를 점으로 이은 경로일 수 있고(그룹 `user` 안의 `user.name`), 한
+ 이름이 필드이면서 그룹일 수는 없다(`user`와 `user.name`). 레코드(최대 8)의 필드 범위는 차례로 이어지고 모두 합쳐 64개 이하다.
키 입력의 키는 정해진 목록(`Space Enter` 화살표 `Backspace Delete Home End Tab Escape PageUp PageDown KeyA–Z Digit0–9`)
안에 있고 겹치지 않는다. 메시지 입력은 키 이름이 없고 필드 범위가 차례로 이어진다. text 입력은 필드가 없다.
- 상수는 블롭 안에 있고 유효한 UTF-8이다. text 상태의 초기 상수, 커서의 대상 text, 리스트의 레코드, 인덱스의
diff --git a/prototypes/connections/module.c b/prototypes/connections/module.c
index c1a71d5..b1f58fe 100644
--- a/prototypes/connections/module.c
+++ b/prototypes/connections/module.c
@@ -138,16 +138,21 @@ static bool document_key(const char *key) {
return true;
}
-/* Strings are [u8 length][bytes]; a name is an ASCII identifier of 1..31 bytes. */
+/* Strings are [u8 length][bytes]; a name is 1..31 bytes: an ASCII identifier,
+ * a path of identifiers joined by dots (a message field inside a group, as
+ * user.name), or any printable ASCII (a key's name). */
+enum { NAME_IDENTIFIER, NAME_PATH, NAME_PRINTABLE };
PURE_COLD static bool name_at(const unsigned char *strings, uint32_t size, uint32_t offset,
- bool identifier, char out[PURE_MODULE_NAME]) {
+ int kind, char out[PURE_MODULE_NAME]) {
if (offset >= size) return false;
uint32_t length = strings[offset];
if (!length || length >= PURE_MODULE_NAME || length > size - offset - 1) return false;
- for (uint32_t i = 0; i < length; ++i) {
+ for (uint32_t i = 0, start = 0; i < length; ++i) {
unsigned char c = strings[offset + 1 + i];
bool letter = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_';
- if (!letter && !(i && c >= '0' && c <= '9') && (identifier || c < 32 || c > 126)) return false;
+ if (kind == NAME_PATH && c == '.' && i > start && i + 1 < length) { start = i + 1; continue; }
+ if (!letter && !(i > start && c >= '0' && c <= '9') && (kind != NAME_PRINTABLE || c < 32 || c > 126))
+ return false;
}
memcpy(out, strings + offset + 1, length);
out[length] = 0;
@@ -461,7 +466,7 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule
if (constant_bytes) memcpy(out->constant_data, p + size - constant_bytes, constant_bytes);
for (uint32_t i = 0; i < out->states; ++i, at += STATE_BYTES) {
out->state_type[i] = (uint8_t)read32(at + 4);
- if (!name_at(text, strings, read32(at), true, out->state_names[i])) REFUSE("state name", i);
+ if (!name_at(text, strings, read32(at), NAME_IDENTIFIER, out->state_names[i])) REFUSE("state name", i);
if (read32(at + 4) > PURE_STATE_INDEX) REFUSE("state type", i);
if (out->state_type[i] == PURE_STATE_NUM) {
out->initial[i] = read_double(at + 8);
@@ -478,7 +483,7 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule
}
}
for (uint32_t i = 0; i < out->collections; ++i, at += COLLECTION_BYTES) {
- if (!name_at(text, strings, read32(at), true, out->collection_names[i])) REFUSE("collection name", i);
+ if (!name_at(text, strings, read32(at), NAME_IDENTIFIER, out->collection_names[i])) REFUSE("collection name", i);
if (read32(at + 4) != 1 || read32(at + 8) != 1) REFUSE("collection must hold bytes by key", i);
}
uint32_t next_field = 0;
@@ -489,11 +494,11 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule
shape->keyed = at[5];
shape->field_start = (uint16_t)read16(at + 12);
shape->field_count = (uint16_t)read16(at + 14);
- if (!name_at(text, strings, read32(at), true, out->input_names[i])) REFUSE("input name", i);
+ if (!name_at(text, strings, read32(at), NAME_IDENTIFIER, out->input_names[i])) REFUSE("input name", i);
if (read16(at + 6) || at[5] > 1) REFUSE("input flags", i);
if (shape->kind == PURE_KIND_KEY) {
if (shape->keyed || shape->field_start || shape->field_count ||
- !name_at(text, strings, read32(at + 8), false, out->input_keys[i]) ||
+ !name_at(text, strings, read32(at + 8), NAME_PRINTABLE, out->input_keys[i]) ||
!key_name(out->input_keys[i])) REFUSE("input key", i);
for (uint32_t k = 0; k < i; ++k)
if (out->input[k].kind == PURE_KIND_KEY && !strcmp(out->input_keys[k], out->input_keys[i]))
@@ -524,11 +529,17 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule
const unsigned char *entry = fields + (size_t)(next_field + f) * FIELD_BYTES;
PureField *field = &out->field[next_field + f];
field->type = (uint8_t)read32(entry + 4);
- if (!name_at(text, strings, read32(entry), true, field->name) || field->type < PURE_FIELD_U32 ||
+ if (!name_at(text, strings, read32(entry), NAME_PATH, field->name) || field->type < PURE_FIELD_U32 ||
read32(entry + 4) > PURE_FIELD_TEXT)
REFUSE("message field", next_field + f);
- for (uint32_t k = 0; k < f; ++k)
- if (!strcmp(out->field[next_field + k].name, field->name)) REFUSE("duplicate field", next_field + f);
+ /* A name is a field or a group, never both (user and user.name). */
+ for (uint32_t k = 0; k < f; ++k) {
+ const char *a = out->field[next_field + k].name, *b = field->name;
+ size_t la = strlen(a), lb = strlen(b);
+ if (!strcmp(a, b)) REFUSE("duplicate field", next_field + f);
+ if ((la < lb && !memcmp(a, b, la) && b[la] == '.') || (lb < la && !memcmp(a, b, lb) && a[lb] == '.'))
+ REFUSE("field and group", next_field + f);
+ }
field->offset = f == last ? PURE_FIELD_REST : shape->prefix;
if (f != last) shape->prefix += field->type == PURE_FIELD_NUMBER ? 8 : 4;
}
@@ -550,14 +561,14 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule
PureRecord *record = &out->record[i];
record->first = (uint16_t)read16(at + 4);
record->count = (uint16_t)read16(at + 6);
- if (!name_at(text, strings, read32(at), true, out->record_names[i]) || record->first != next_record_field ||
+ if (!name_at(text, strings, read32(at), NAME_IDENTIFIER, out->record_names[i]) || record->first != next_record_field ||
!record->count || record->count > PURE_RECORD_FIELDS || record->count > out->record_fields - next_record_field)
REFUSE("record", i);
for (uint32_t f = record->first; f < record->first + record->count; ++f) {
const unsigned char *entry = fields_at + (size_t)f * RECORD_FIELD_BYTES;
uint32_t type = read32(entry + 4);
out->record_field_text[f] = type == 1;
- if (!name_at(text, strings, read32(entry), true, out->record_field_names[f]) || type > 1)
+ if (!name_at(text, strings, read32(entry), NAME_IDENTIFIER, out->record_field_names[f]) || type > 1)
REFUSE("record field", f);
for (uint32_t k = record->first; k < f; ++k)
if (!strcmp(out->record_field_names[k], out->record_field_names[f])) REFUSE("duplicate record field", f);
@@ -578,7 +589,7 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule
REFUSE("state reference", i);
/* A table names its list in place of code; its columns follow it. */
for (uint32_t i = 0; i < out->views; ++i, at += VIEW_BYTES) {
- if (!name_at(text, strings, read32(at), true, out->view_names[i])) REFUSE("view name", i);
+ if (!name_at(text, strings, read32(at), NAME_IDENTIFIER, out->view_names[i])) REFUSE("view name", i);
out->view_type[i] = at[4];
out->view_table[i] = (uint16_t)read16(at + 6);
out->view_code[i] = (PureBlock){read32(at + 8), read32(at + 12)};
@@ -1435,7 +1446,10 @@ static PureResult keep_reply(PureApp *app) {
return PURE_OK;
}
-PureResult pure_app_message(PureApp *app, uint32_t input, const char *key, int64_t base, PureSpan payload) {
+/* A message whose variable fields are where placed says (start and size in
+ * the payload), or, without it, one after another after the prefix. */
+static PureResult message(PureApp *app, uint32_t input, const char *key, int64_t base, PureSpan payload,
+ const uint32_t *placed) {
pure_app_finish(app);
const PureModule *m = app->module;
app->fault = PURE_FAULT_NONE;
@@ -1455,13 +1469,16 @@ PureResult pure_app_message(PureApp *app, uint32_t input, const char *key, int64
if (field->type == PURE_FIELD_NUMBER && !isfinite(read_double(payload.data + field->offset)))
return fault(app, PURE_FAULT_NON_FINITE, UINT32_MAX);
if (field->type != PURE_FIELD_BYTES && field->type != PURE_FIELD_TEXT) continue;
- size_t size = field->offset == PURE_FIELD_REST ? payload.size - at : read32(payload.data + field->offset);
- if (size > payload.size - at) return fault(app, PURE_FAULT_LIMIT, UINT32_MAX);
- if (field->type == PURE_FIELD_TEXT && !pure_utf8_valid((PureSpan){size ? payload.data + at : NULL, size}))
+ size_t start = placed ? placed[2 * f] : at;
+ if (start > payload.size) return fault(app, PURE_FAULT_LIMIT, UINT32_MAX);
+ size_t size = placed ? placed[2 * f + 1] : field->offset == PURE_FIELD_REST ? payload.size - at :
+ read32(payload.data + field->offset);
+ if (size > payload.size - start) return fault(app, PURE_FAULT_LIMIT, UINT32_MAX);
+ if (field->type == PURE_FIELD_TEXT && !pure_utf8_valid((PureSpan){size ? payload.data + start : NULL, size}))
return fault(app, PURE_FAULT_ENCODING, UINT32_MAX);
- spans[f][0] = (uint32_t)at;
+ spans[f][0] = (uint32_t)start;
spans[f][1] = (uint32_t)size;
- at += size;
+ at = start + size;
}
if (shape->keyed && m->collections) {
if (!app->data.open) return PURE_INVALID;
@@ -1491,6 +1508,15 @@ PureResult pure_app_message(PureApp *app, uint32_t input, const char *key, int64
return result;
}
+PureResult pure_app_message(PureApp *app, uint32_t input, const char *key, int64_t base, PureSpan payload) {
+ return message(app, input, key, base, payload, NULL);
+}
+
+PureResult pure_app_message_fields(PureApp *app, uint32_t input, const char *key, int64_t base, PureSpan payload,
+ const uint32_t *placed) {
+ return message(app, input, key, base, payload, placed);
+}
+
PureResult pure_app_reply(const PureApp *app, bool (*visit)(void *, PureSpan), void *context) {
if (!app->has_reply) return PURE_OK;
return visit_value(app, &app->arena, app->reply, visit, context);
diff --git a/prototypes/connections/module.h b/prototypes/connections/module.h
index 3a75fc1..430ccae 100644
--- a/prototypes/connections/module.h
+++ b/prototypes/connections/module.h
@@ -17,7 +17,7 @@ enum {
PURE_MODULE_STATES = 32, PURE_MODULE_INPUTS = 32, PURE_MODULE_VIEWS = 64,
PURE_MODULE_HANDLERS = 64, PURE_MODULE_CODE = 65535, PURE_MODULE_STACK = 64,
PURE_MODULE_NAME = 32, PURE_MODULE_STRINGS = 32768, PURE_MODULE_HEADER = 40,
- PURE_MODULE_FIELDS = 32, PURE_MODULE_COLLECTIONS = 1, PURE_MODULE_CONSTANTS = 4096,
+ PURE_MODULE_FIELDS = 64, PURE_MODULE_COLLECTIONS = 1, PURE_MODULE_CONSTANTS = 4096,
PURE_MODULE_CONSTANT_BYTES = 1 << 20, PURE_MODULE_RECORDS = 8, PURE_MODULE_RECORD_FIELDS = 64,
PURE_RECORD_FIELDS = 16, PURE_MODULE_JOINS = 8, PURE_APP_PENDING = 256
};
@@ -256,6 +256,13 @@ PureResult pure_app_text(PureApp *, uint32_t input, PureSpan text);
* pure_app_finish. On return app->revision is the document's revision
* (deferred: the revision the next save gives it). */
PureResult pure_app_message(PureApp *, uint32_t input, const char *key, int64_t base, PureSpan payload);
+/* The same, with the variable fields where the host found them: field f
+ * starts at placed[2f] in payload and has placed[2f + 1] bytes (entries of
+ * fixed fields are not read, nor are the variable fields' lengths in the
+ * prefix). A host that parsed a request in place passes the fields' bytes
+ * where they are. */
+PureResult pure_app_message_fields(PureApp *, uint32_t input, const char *key, int64_t base, PureSpan payload,
+ const uint32_t *placed);
/* Visit the reply of the last message in order, without copying, until the
* next event (deferred: copy it before a later save too). */
PureResult pure_app_reply(const PureApp *, bool (*visit)(void *, PureSpan), void *context);
diff --git a/prototypes/connections/test-module.c b/prototypes/connections/test-module.c
index d885c92..ae82c69 100644
--- a/prototypes/connections/test-module.c
+++ b/prototypes/connections/test-module.c
@@ -2486,6 +2486,57 @@ static void message_fields(void) {
CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "operand type"));
bad.o = bytes_as_text; bad.no = 2; bad.h = two;
CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "operand type"));
+
+ /* Placed by the host: the variable fields wherever they are in the
+ * payload (here a JSON body after the prefix), in any order; the
+ * prefix's lengths are not read. */
+ CHECK(pure_app_start(&app, &module_a, (PureData){0}) == PURE_OK);
+ static const char body[] = "{\"note\":\"xyz\",\"title\":\"\xea\xb0\x80\xeb\x82\x98\"}";
+ points = 2.5;
+ title_size = 999;
+ memcpy(payload, &title_size, 4);
+ memcpy(payload + 4, &points, 8);
+ memcpy(payload + 12, &count, 4);
+ memcpy(payload + 16, body, sizeof(body) - 1);
+ size = 16 + sizeof(body) - 1;
+ uint32_t placed[8] = {16 + 23, 6, 0, 0, 16 + 9, 3, 0, 0};
+ CHECK(pure_app_message_fields(&app, 0, NULL, -1, (PureSpan){payload, size}, placed) == PURE_OK);
+ CHECK(app.values[0] == 5.5 && !strcmp(state_text(&app, 1), "\xea\xb0\x80\xeb\x82\x98") && replied(&app, "xyz"));
+ pure_app_finish(&app);
+ /* A field that starts or ends past the payload, or a title that is not UTF-8 there. */
+ placed[0] = (uint32_t)size + 1;
+ CHECK(pure_app_message_fields(&app, 0, NULL, -1, (PureSpan){payload, size}, placed) == PURE_LIMIT);
+ placed[0] = (uint32_t)size - 2;
+ placed[1] = 3;
+ CHECK(pure_app_message_fields(&app, 0, NULL, -1, (PureSpan){payload, size}, placed) == PURE_LIMIT);
+ placed[0] = 16 + 24;
+ placed[1] = 2;
+ CHECK(pure_app_message_fields(&app, 0, NULL, -1, (PureSpan){payload, size}, placed) == PURE_INVALID &&
+ app.fault == PURE_FAULT_ENCODING);
+ CHECK(app.values[0] == 5.5);
+ pure_app_stop(&app);
+
+ /* Fields in groups: a name is a path of identifiers (user.name), and
+ * never both a field and a group. */
+ static const F grouped[] = {{"user.name", PURE_FIELD_TEXT}, {"user.age", PURE_FIELD_U32}, {"plan", PURE_FIELD_TEXT}};
+ static const I grouped_inputs[] = {{"post", NULL, PURE_KIND_MESSAGE, false, grouped, 3}};
+ static const O grouped_code[] = {{FIELD, 1, 0}, {ST, 0, 0}, {FIELD, 0, 0}, {TSET, 1, 0}};
+ static const H grouped_handlers[] = {{1, 0, 4}};
+ Spec groups = spec;
+ groups.in = grouped_inputs; groups.o = grouped_code; groups.no = LEN(grouped_code); groups.h = grouped_handlers;
+ CHECK(load(groups, &module_b) == PURE_OK && !strcmp(module_b.field[0].name, "user.name"));
+ static const char *const refused_paths[] = {"user..name", ".user", "user.", "user.1st", "user name"};
+ for (size_t i = 0; i < LEN(refused_paths); ++i) {
+ F renamed[3] = {grouped[0], grouped[1], grouped[2]};
+ renamed[0].name = refused_paths[i];
+ I renamed_inputs[1] = {{"post", NULL, PURE_KIND_MESSAGE, false, renamed, 3}};
+ groups.in = renamed_inputs;
+ CHECK(load(groups, &module_b) == PURE_INVALID && strstr(error, "message field"));
+ }
+ F both[3] = {{"user", PURE_FIELD_TEXT}, grouped[1], grouped[2]};
+ I both_inputs[1] = {{"post", NULL, PURE_KIND_MESSAGE, false, both, 3}};
+ groups.in = both_inputs;
+ CHECK(load(groups, &module_b) == PURE_INVALID && strstr(error, "field and group"));
}
/* A timer's tick sees the time the host set; floor and a floored modulo