diff --git a/backend/.dockerignore b/.dockerignore similarity index 100% rename from backend/.dockerignore rename to .dockerignore diff --git a/.gitmodules b/.gitmodules index 5f80c0f..6f7a96b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,10 +1,10 @@ [submodule "backend/vendor/hare-http"] - path = backend/vendor/hare-http + path = vendor/hare-http url = https://git.fnordig.de/jer/hare-http.git branch = host-in-uri [submodule "backend/vendor/hare-logfmt"] - path = backend/vendor/hare-logfmt + path = vendor/hare-logfmt url = https://git.sr.ht/~blainsmith/hare-logfmt [submodule "backend/vendor/hare-json"] - path = backend/vendor/hare-json + path = vendor/hare-json url = https://git.sr.ht/~sircmpwn/hare-json diff --git a/backend/Cargo.lock b/Cargo.lock similarity index 100% rename from backend/Cargo.lock rename to Cargo.lock diff --git a/backend/Cargo.toml b/Cargo.toml similarity index 100% rename from backend/Cargo.toml rename to Cargo.toml diff --git a/backend/Dockerfile b/Dockerfile similarity index 100% rename from backend/Dockerfile rename to Dockerfile diff --git a/backend/Makefile b/Makefile similarity index 100% rename from backend/Makefile rename to Makefile diff --git a/backend/vendor/hare-http b/backend/vendor/hare-http deleted file mode 160000 index f3a9625..0000000 --- a/backend/vendor/hare-http +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f3a96257c7fc3594a1ec2cde37c248730a174e6f diff --git a/backend/vendor/hare-json b/backend/vendor/hare-json deleted file mode 160000 index b6aeae9..0000000 --- a/backend/vendor/hare-json +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b6aeae96199607a1f3b4d437d5c99f821bd6a6b6 diff --git a/backend/vendor/hare-logfmt b/backend/vendor/hare-logfmt deleted file mode 160000 index 2b4a374..0000000 --- a/backend/vendor/hare-logfmt +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 2b4a37459be54c83ac6ac6354cddec3e9fa796bf diff --git a/backend/cmd/httpd/main.ha b/cmd/httpd/main.ha similarity index 100% rename from backend/cmd/httpd/main.ha rename to cmd/httpd/main.ha diff --git a/backend/cmd/httpd/sandbox.ha b/cmd/httpd/sandbox.ha similarity index 100% rename from backend/cmd/httpd/sandbox.ha rename to cmd/httpd/sandbox.ha diff --git a/backend/cmd/proctest/main.ha b/cmd/proctest/main.ha similarity index 100% rename from backend/cmd/proctest/main.ha rename to cmd/proctest/main.ha diff --git a/backend/cmd/threads/main.ha b/cmd/threads/main.ha similarity index 100% rename from backend/cmd/threads/main.ha rename to cmd/threads/main.ha diff --git a/backend/debug.sh b/debug.sh similarity index 100% rename from backend/debug.sh rename to debug.sh diff --git a/backend/fly.toml b/fly.toml similarity index 100% rename from backend/fly.toml rename to fly.toml diff --git a/backend/frontend/Makefile b/frontend/Makefile similarity index 100% rename from backend/frontend/Makefile rename to frontend/Makefile diff --git a/backend/frontend/app.js b/frontend/app.js similarity index 100% rename from backend/frontend/app.js rename to frontend/app.js diff --git a/backend/frontend/codapi-settings.js b/frontend/codapi-settings.js similarity index 100% rename from backend/frontend/codapi-settings.js rename to frontend/codapi-settings.js diff --git a/backend/frontend/codapi.css b/frontend/codapi.css similarity index 100% rename from backend/frontend/codapi.css rename to frontend/codapi.css diff --git a/backend/frontend/codapi.js b/frontend/codapi.js similarity index 100% rename from backend/frontend/codapi.js rename to frontend/codapi.js diff --git a/backend/frontend/codejar.js b/frontend/codejar.js similarity index 100% rename from backend/frontend/codejar.js rename to frontend/codejar.js diff --git a/backend/frontend/github.min.css b/frontend/github.min.css similarity index 100% rename from backend/frontend/github.min.css rename to frontend/github.min.css diff --git a/backend/frontend/highlight.min.js b/frontend/highlight.min.js similarity index 100% rename from backend/frontend/highlight.min.js rename to frontend/highlight.min.js diff --git a/backend/frontend/index.html b/frontend/index.html similarity index 100% rename from backend/frontend/index.html rename to frontend/index.html diff --git a/backend/frontend/style.css b/frontend/style.css similarity index 100% rename from backend/frontend/style.css rename to frontend/style.css diff --git a/backend/frontend/writ.min.css b/frontend/writ.min.css similarity index 100% rename from backend/frontend/writ.min.css rename to frontend/writ.min.css diff --git a/backend/live.hurl b/live.hurl similarity index 100% rename from backend/live.hurl rename to live.hurl diff --git a/backend/out.txt b/out.txt similarity index 100% rename from backend/out.txt rename to out.txt diff --git a/proctest b/proctest new file mode 100755 index 0000000..39a8bf4 Binary files /dev/null and b/proctest differ diff --git a/backend/request.hurl b/request.hurl similarity index 100% rename from backend/request.hurl rename to request.hurl diff --git a/backend/run.sh b/run.sh similarity index 100% rename from backend/run.sh rename to run.sh diff --git a/backend/src/main.rs b/src/main.rs similarity index 100% rename from backend/src/main.rs rename to src/main.rs diff --git a/backend/src/sandbox.rs b/src/sandbox.rs similarity index 100% rename from backend/src/sandbox.rs rename to src/sandbox.rs diff --git a/threads b/threads new file mode 100755 index 0000000..691bcc0 Binary files /dev/null and b/threads differ diff --git a/vendor/hare-http/COPYING b/vendor/hare-http/COPYING new file mode 100644 index 0000000..c257317 --- /dev/null +++ b/vendor/hare-http/COPYING @@ -0,0 +1,367 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. diff --git a/vendor/hare-http/cmd/http/main.ha b/vendor/hare-http/cmd/http/main.ha new file mode 100644 index 0000000..e266c29 --- /dev/null +++ b/vendor/hare-http/cmd/http/main.ha @@ -0,0 +1,75 @@ +use getopt; +use io; +use log; +use net::dial; +use net::http; +use net::uri; +use os; +use strings; + +const usage: [_]getopt::help = [ + "HTTP client", + ('H', "Name:value", "Sets an HTTP header"), + ('X', "method", "Sets the HTTP method verb"), + "url" +]; + +export fn main() void = { + const client = http::newclient("Hare net::http test client"); + defer http::client_finish(&client); + + const cmd = getopt::parse(os::args, usage...); + defer getopt::finish(&cmd); + + if (len(cmd.args) != 1) { + getopt::printusage(os::stderr, "http", usage)!; + os::exit(os::status::FAILURE); + }; + + const targ = match (uri::parse(cmd.args[0])) { + case let u: uri::uri => + yield u; + case uri::invalid => + log::fatal("Invalid URI"); + }; + defer uri::finish(&targ); + + let req = http::new_request(&client, "GET", &targ)!; + for (let i = 0z; i < len(cmd.opts); i += 1) { + const (opt, val) = cmd.opts[i]; + switch (opt) { + case 'H' => + const (name, val) = strings::cut(val, ":"); + http::header_add(&req.header, name, val); + case 'X' => + req.method = val; + case => abort(); + }; + }; + + const resp = match (http::do(&client, &req)) { + case let err: http::error => + log::fatal("HTTP error:", http::strerror(err)); + case let resp: http::response => + yield resp; + }; + defer http::response_finish(&resp); + + log::printfln("HTTP/{}.{}: {} {}", + resp.version.0, resp.version.1, + resp.status, resp.reason); + + for (let i = 0z; i < len(resp.header); i += 1) { + const (name, val) = resp.header[i]; + log::printfln("{}: {}", name, val); + }; + + const body = match (resp.body) { + case let st: *io::stream => + yield st; + case null => + return; + }; + io::copy(os::stdout, body)!; + io::close(body)!; +}; diff --git a/vendor/hare-http/cmd/httpd/main.ha b/vendor/hare-http/cmd/httpd/main.ha new file mode 100644 index 0000000..819a2f5 --- /dev/null +++ b/vendor/hare-http/cmd/httpd/main.ha @@ -0,0 +1,92 @@ +use getopt; +use net; +use net::ip; +use net::http; +use net::dial; +use os; +use memio; +use io; +use fmt; +use bufio; +use strings; + +const usage: [_]getopt::help = [ + "HTTP server", + ('a', "address", "listened address (ex: 127.0.0.1:8080)") +]; + +export fn main() void = { + const cmd = getopt::parse(os::args, usage...); + defer getopt::finish(&cmd); + + let port: u16 = 8080; + let ip_addr: ip::addr4 = [127, 0, 0, 1]; + + for (let i = 0z; i < len(cmd.opts); i += 1) { + const opt = cmd.opts[i]; + switch (opt.0) { + case 'a' => + match (dial::splitaddr(opt.1, "")) { + case let value: (str, u16) => + ip_addr = ip::parsev4(value.0)!; + port = value.1; + case dial::invalid_address => + abort("Invalid address"); + }; + case => abort(); // unreachable + }; + }; + + const server = match (http::listen(ip_addr, port)) { + case let this: *http::server => + yield this; + case net::error => abort("failure while listening"); + }; + defer http::server_finish(server); + + for (true) { + const serv_req = match (http::serve(server)) { + case let this: *http::server_request => + yield this; + case net::error => abort("failure while serving"); + }; + defer http::serve_finish(serv_req); + + let buf = memio::dynamic(); + defer io::close(&buf)!; + handlereq(&buf, &serv_req.request); + + http::response_write( + serv_req.socket, + http::STATUS_OK, + &buf, + ("Content-Type", "text/plain") + )!; + }; +}; + +export fn handlereq(buf: *io::stream, request: *http::request) void = { + fmt::fprintfln(buf, "Method: {}", request.method)!; + fmt::fprintfln(buf, "Path: {}", request.target.path)!; + fmt::fprintfln(buf, "Fragment: {}", request.target.fragment)!; + fmt::fprintfln(buf, "Query: {}", request.target.query)!; + fmt::fprintfln(buf, "Headers: < void; + case let body: io::handle => + fmt::fprintfln(buf, "Body: < + fmt::fprintln(buf, strings::fromutf8(line)!)!; + break; + case io::EOF => + break; + }; + }; + fmt::fprintfln(buf, "EOF")!; + }; +}; diff --git a/vendor/hare-http/net/http/README b/vendor/hare-http/net/http/README new file mode 100644 index 0000000..4180b4a --- /dev/null +++ b/vendor/hare-http/net/http/README @@ -0,0 +1,17 @@ +net::http provides an implementation of an HTTP 1.1 client and server as defined +by RFC 9110 et al. + +TODO: Flesh me out + +Caveats: + +- No attempt is made to validate that the input for client requests or responses + are valid according to the HTTP grammar; such cases will fail when rejected by + the other party. +- Details indicated by RFC 7230 et al as "obsolete" are not implemented +- Max header length including "name: value" is 4KiB + +TODO: + +- Server stuff +- TLS diff --git a/vendor/hare-http/net/http/client.ha b/vendor/hare-http/net/http/client.ha new file mode 100644 index 0000000..26d3b68 --- /dev/null +++ b/vendor/hare-http/net/http/client.ha @@ -0,0 +1,95 @@ +use io; +use net::uri; + +export type client = struct { + default_header: header, + default_transport: transport, +}; + +// Creates a new HTTP [[client]] with the provided User-Agent string. +// +// The HTTP client implements a number of sane defaults, which may be tuned. The +// set of default headers is configured with [[client_default_header]], and the +// default transport behavior with [[client_default_transport]]. +// +// TODO: Implement and document the connection pool +// +// The caller must pass the client object to [[client_finish]] to free resources +// associated with this client after use. +export fn newclient(ua: str) client = { + let client = client { ... }; + header_add(&client, "User-Agent", ua); + return client; +}; + +// Frees resources associated with an HTTP [[client]]. +export fn client_finish(client: *client) void = { + header_free(&client.default_header); +}; + +// Returns the default headers used by this HTTP client, so that the user can +// examine or modify the net::http defaults (such as User-Agent or +// Accept-Encoding), or add their own. +export fn client_default_header(client: *client) *header = { + return &client.default_header; +}; + +// Returns the default [[transport]] configuration used by this HTTP client. +export fn client_default_transport(client: *client) *transport = { + return &client.default_transport; +}; + +fn uri_origin_form(target: *uri::uri) uri::uri = { + let target = *target; + target.scheme = ""; + target.host = ""; + target.fragment = ""; + target.userinfo = ""; + target.port = 0; + if (target.path == "") { + target.path = "/"; + }; + return target; +}; + +// Performs a synchronous HTTP GET request with the given client. +export fn get(client: *client, target: *uri::uri) (response | error) = { + const req = new_request(client, GET, target)?; + defer request_finish(&req); + return do(client, &req); +}; + +// Performs a synchronous HTTP HEAD request with the given client. +export fn head(client: *client, target: *uri::uri) (response | error) = { + const req = new_request(client, HEAD, target)?; + defer request_finish(&req); + return do(client, &req); +}; + +// Performs a synchronous HTTP POST request with the given client. +// +// If the provided I/O handle is seekable, the Content-Length header is added +// automatically. Otherwise, Transfer-Encoding: chunked will be used. +export fn post( + client: *client, + target: *uri::uri, + body: io::handle, +) (response | error) = { + const req = new_request_body(client, POST, target, body)?; + defer request_finish(&req); + return do(client, &req); +}; + +// Performs a synchronous HTTP PUT request with the given client. +// +// If the provided I/O handle is seekable, the Content-Length header is added +// automatically. Otherwise, Transfer-Encoding: chunked will be used. +export fn put( + client: *client, + target: *uri::uri, + body: io::handle, +) (response | error) = { + const req = new_request_body(client, PUT, target, body)?; + defer request_finish(&req); + return do(client, &req); +}; diff --git a/vendor/hare-http/net/http/constants.ha b/vendor/hare-http/net/http/constants.ha new file mode 100644 index 0000000..da8bb1e --- /dev/null +++ b/vendor/hare-http/net/http/constants.ha @@ -0,0 +1,112 @@ +// HTTP "GET" method. +export def GET: str = "GET"; +// HTTP "HEAD" method. +export def HEAD: str = "HEAD"; +// HTTP "POST" method. +export def POST: str = "POST"; +// HTTP "PUT" method. +export def PUT: str = "PUT"; +// HTTP "DELETE" method. +export def DELETE: str = "DELETE"; +// HTTP "OPTIONS" method. +export def OPTIONS: str = "OPTIONS"; +// HTTP "PATCH" method. +export def PATCH: str = "PATCH"; +// HTTP "CONNECT" method. +export def CONNECT: str = "CONNECT"; + +// HTTP "Continue" response status (100). +export def STATUS_CONTINUE: uint = 100; +// HTTP "Switching Protocols" response status (101). +export def STATUS_SWITCHING_PROTOCOLS: uint = 101; + +// HTTP "OK" response status (200). +export def STATUS_OK: uint = 200; +// HTTP "Created" response status (201). +export def STATUS_CREATED: uint = 201; +// HTTP "Accepted" response status (202). +export def STATUS_ACCEPTED: uint = 202; +// HTTP "Non-authoritative Info" response status (203). +export def STATUS_NONAUTHORITATIVE_INFO: uint = 203; +// HTTP "No Content" response status (204). +export def STATUS_NO_CONTENT: uint = 204; +// HTTP "Reset Content" response status (205). +export def STATUS_RESET_CONTENT: uint = 205; +// HTTP "Partial Content" response status (206). +export def STATUS_PARTIAL_CONTENT: uint = 206; + +// HTTP "Multiple Choices" response status (300). +export def STATUS_MULTIPLE_CHOICES: uint = 300; +// HTTP "Moved Permanently" response status (301). +export def STATUS_MOVED_PERMANENTLY: uint = 301; +// HTTP "Found" response status (302). +export def STATUS_FOUND: uint = 302; +// HTTP "See Other" response status (303). +export def STATUS_SEE_OTHER: uint = 303; +// HTTP "Not Modified" response status (304). +export def STATUS_NOT_MODIFIED: uint = 304; +// HTTP "Use Proxy" response status (305). +export def STATUS_USE_PROXY: uint = 305; + +// HTTP "Temporary Redirect" response status (307). +export def STATUS_TEMPORARY_REDIRECT: uint = 307; +// HTTP "Permanent Redirect" response status (308). +export def STATUS_PERMANENT_REDIRECT: uint = 308; + +// HTTP "Bad Request" response status (400). +export def STATUS_BAD_REQUEST: uint = 400; +// HTTP "Unauthorized" response status (401). +export def STATUS_UNAUTHORIZED: uint = 401; +// HTTP "Payment Required" response status (402). +export def STATUS_PAYMENT_REQUIRED: uint = 402; +// HTTP "Forbidden" response status (403). +export def STATUS_FORBIDDEN: uint = 403; +// HTTP "Not Found" response status (404). +export def STATUS_NOT_FOUND: uint = 404; +// HTTP "Method Not Allowed" response status (405). +export def STATUS_METHOD_NOT_ALLOWED: uint = 405; +// HTTP "Not Acceptable" response status (406). +export def STATUS_NOT_ACCEPTABLE: uint = 406; +// HTTP "Proxy Authentication Required" response status (407). +export def STATUS_PROXY_AUTH_REQUIRED: uint = 407; +// HTTP "Request Timeout" response status (408). +export def STATUS_REQUEST_TIMEOUT: uint = 408; +// HTTP "Conflict" response status (409). +export def STATUS_CONFLICT: uint = 409; +// HTTP "Gone" response status (410). +export def STATUS_GONE: uint = 410; +// HTTP "Length Required" response status (411). +export def STATUS_LENGTH_REQUIRED: uint = 411; +// HTTP "Precondition Failed" response status (412). +export def STATUS_PRECONDITION_FAILED: uint = 412; +// HTTP "Request Entity Too Large" response status (413). +export def STATUS_REQUEST_ENTITY_TOO_LARGE: uint = 413; +// HTTP "Request URI Too Long" response status (414). +export def STATUS_REQUEST_URI_TOO_LONG: uint = 414; +// HTTP "Unsupported Media Type" response status (415). +export def STATUS_UNSUPPORTED_MEDIA_TYPE: uint = 415; +// HTTP "Requested Range Not Satisfiable" response status (416). +export def STATUS_REQUESTED_RANGE_NOT_SATISFIABLE: uint = 416; +// HTTP "Expectation Failed" response status (417). +export def STATUS_EXPECTATION_FAILED: uint = 417; +// HTTP "I'm a Teapot" response status (418). +export def STATUS_TEAPOT: uint = 418; +// HTTP "Misdirected Request" response status (421). +export def STATUS_MISDIRECTED_REQUEST: uint = 421; +// HTTP "Unprocessable Entity" response status (422). +export def STATUS_UNPROCESSABLE_ENTITY: uint = 422; +// HTTP "Upgrade Required" response status (426). +export def STATUS_UPGRADE_REQUIRED: uint = 426; + +// HTTP "Internal Server Error" response status (500). +export def STATUS_INTERNAL_SERVER_ERROR: uint = 500; +// HTTP "Not Implemented" response status (501). +export def STATUS_NOT_IMPLEMENTED: uint = 501; +// HTTP "Bad Gateway" response status (502). +export def STATUS_BAD_GATEWAY: uint = 502; +// HTTP "Service Unavailable" response status (503). +export def STATUS_SERVICE_UNAVAILABLE: uint = 503; +// HTTP "Gateway Timeout" response status (504). +export def STATUS_GATEWAY_TIMEOUT: uint = 504; +// HTTP "HTTP Version Not Supported" response status (505). +export def STATUS_HTTP_VERSION_NOT_SUPPORTED: uint = 505; diff --git a/vendor/hare-http/net/http/do.ha b/vendor/hare-http/net/http/do.ha new file mode 100644 index 0000000..9b5905b --- /dev/null +++ b/vendor/hare-http/net/http/do.ha @@ -0,0 +1,144 @@ +use bufio; +use encoding::utf8; +use errors; +use fmt; +use io; +use net::dial; +use net::uri; +use net; +use os; +use strconv; +use strings; +use types; + +// Performs an HTTP [[request]] with the given [[client]]. The request is +// performed synchronously; this function blocks until the server has returned +// the response status and all HTTP headers associated with the response. +// +// If the provided [[response]] has a non-null body, the user must pass it to +// [[io::close]] before calling [[response_finish]]. +export fn do(client: *client, req: *request) (response | error) = { + assert(req.target.scheme == "http"); // TODO: https + const conn = dial::dial_uri("tcp", req.target)?; + + let buf: [os::BUFSZ]u8 = [0...]; + let file = bufio::init(conn, [], buf); + bufio::setflush(&file, []); + + fmt::fprintf(&file, "{} ", req.method)?; + + // TODO: Support other request-targets than origin-form + const target = uri_origin_form(req.target); + uri::fmt(&file, &target)?; + fmt::fprintf(&file, " HTTP/1.1\r\n")?; + + write_header(&file, &req.header)?; + fmt::fprintf(&file, "\r\n")?; + bufio::flush(&file)?; + + const trans = match (req.transport) { + case let t: *transport => + yield t; + case => + yield &client.default_transport; + }; + // TODO: Implement None + assert(trans.request_transport == transport_mode::AUTO); + assert(trans.response_transport == transport_mode::AUTO); + assert(trans.request_content == content_mode::AUTO); + assert(trans.response_content == content_mode::AUTO); + + match (req.body) { + case let body: io::handle => + io::copy(conn, body)?; + case void => + yield; + }; + + let resp = response { ... }; + const scan = bufio::newscanner(conn, 512); + read_statusline(&resp, &scan)?; + read_header(&resp.header, &scan)?; + + const response_complete = + req.method == "HEAD" || + resp.status == STATUS_NO_CONTENT || + resp.status == STATUS_NOT_MODIFIED || + (resp.status >= 100 && resp.status < 200) || + (req.method == "CONNECT" && resp.status >= 200 && resp.status < 300); + if (!response_complete) { + resp.body = new_reader(conn, &resp, &scan)?; + } else if (req.method != "CONNECT") { + io::close(conn)!; + }; + return resp; +}; + +fn read_statusline( + resp: *response, + scan: *bufio::scanner, +) (void | error) = { + const status = match (bufio::scan_string(scan, "\r\n")) { + case let line: const str => + yield line; + case let err: io::error => + return err; + case utf8::invalid => + return protoerr; + case io::EOF => + return protoerr; + }; + + const tok = strings::tokenize(status, " "); + + const version = match (strings::next_token(&tok)) { + case let ver: str => + yield ver; + case done => + return protoerr; + }; + + const status = match (strings::next_token(&tok)) { + case let status: str => + yield status; + case done => + return protoerr; + }; + + const reason = match (strings::next_token(&tok)) { + case let reason: str => + yield reason; + case done => + return protoerr; + }; + + const (_, version) = strings::cut(version, "/"); + const (major, minor) = strings::cut(version, "."); + + const major = match (strconv::stou(major)) { + case let u: uint => + yield u; + case => + return protoerr; + }; + const minor = match (strconv::stou(minor)) { + case let u: uint => + yield u; + case => + return protoerr; + }; + resp.version = (major, minor); + + if (resp.version.0 > 1) { + return errors::unsupported; + }; + + resp.status = match (strconv::stou(status)) { + case let u: uint => + yield u; + case => + return protoerr; + }; + + resp.reason = strings::dup(reason); +}; diff --git a/vendor/hare-http/net/http/error.ha b/vendor/hare-http/net/http/error.ha new file mode 100644 index 0000000..e0dbd1a --- /dev/null +++ b/vendor/hare-http/net/http/error.ha @@ -0,0 +1,26 @@ +use errors; +use io; +use net::dial; + +// Errors possible while servicing HTTP requests. Note that these errors are for +// errors related to the processing of the HTTP connection; semantic HTTP errors +// such as [[STATUS_NOT_FOUND]] are not handled by this type. +export type error = !(dial::error | io::error | errors::unsupported | protoerr); + +// An HTTP protocol error occurred, indicating that the remote party is not +// conformant with HTTP semantics. +export type protoerr = !void; + +// Converts an [[error]] to a string. +export fn strerror(err: error) const str = { + match (err) { + case let err: dial::error => + return dial::strerror(err); + case let err: io::error => + return io::strerror(err); + case errors::unsupported => + return "Unsupported HTTP feature"; + case protoerr => + return "HTTP protocol error"; + }; +}; diff --git a/vendor/hare-http/net/http/header.ha b/vendor/hare-http/net/http/header.ha new file mode 100644 index 0000000..c615484 --- /dev/null +++ b/vendor/hare-http/net/http/header.ha @@ -0,0 +1,105 @@ +use bufio; +use encoding::utf8; +use fmt; +use io; +use strings; + +// List of HTTP headers. +// TODO: [](str, []str) +export type header = [](str, str); + +// Adds a given HTTP header, which may be added more than once. The name should +// be canonicalized by the caller. +export fn header_add(head: *header, name: str, val: str) void = { + assert(len(name) >= 1 && len(val) >= 1); + append(head, (strings::dup(name), strings::dup(val))); +}; + +// Sets the value of a given HTTP header, removing any previous values. The name +// should be canonicalized by the caller. +export fn header_set(head: *header, name: str, val: str) void = { + header_del(head, name); + header_add(head, name, val); +}; + +// Removes an HTTP header from a list of [[header]]. If multiple headers match +// the given name, all matching headers are removed. +export fn header_del(head: *header, name: str) void = { + for (let i = 0z; i < len(head); i += 1) { + if (head[i].0 == name) { + free(head[i].0); + free(head[i].1); + delete(head[i]); + i -= 1; + }; + }; +}; + +// Retrieves a value, or values, from a header. An empty string indicates the +// absence of a header. +export fn header_get(head: *header, name: str) str = { + for (let i = 0z; i < len(head); i += 1) { + const (key, val) = head[i]; + if (key == name) { + return val; + }; + }; + return ""; +}; + +// Frees state associated with an HTTP [[header]]. +export fn header_free(head: *header) void = { + for (let i = 0z; i < len(head); i += 1) { + free(head[i].0); + free(head[i].1); + }; + free(*head); +}; + +// Duplicates a set of HTTP headers. +export fn header_dup(head: *header) header = { + let new: header = []; + for (let i = 0z; i < len(head); i += 1) { + const (key, val) = head[i]; + header_add(&new, key, val); + }; + return new; +}; + +// Writes a list of HTTP headers to the provided I/O handle in the HTTP wire +// format. +export fn write_header(sink: io::handle, head: *header) (size | io::error) = { + let z = 0z; + for (let i = 0z; i < len(head); i += 1) { + const (name, val) = head[i]; + z += fmt::fprintf(sink, "{}: {}\r\n", name, val)?; + }; + return z; +}; + +fn read_header(head: *header, scan: *bufio::scanner) (void | io::error | protoerr) = { + for (true) { + const item = match (bufio::scan_string(scan, "\r\n")) { + case let line: const str => + yield line; + case io::EOF => + break; + case let err: io::error => + return err; + case utf8::invalid => + return protoerr; + }; + if (item == "") { + break; + }; + + let (name, val) = strings::cut(item, ":"); + val = strings::trim(val); + if (val == "") { + return protoerr; + }; + // TODO: validate field-name + + header_add(head, name, val); + }; +}; diff --git a/vendor/hare-http/net/http/request.ha b/vendor/hare-http/net/http/request.ha new file mode 100644 index 0000000..acc56ae --- /dev/null +++ b/vendor/hare-http/net/http/request.ha @@ -0,0 +1,278 @@ +use errors; +use fmt; +use io; +use net::ip; +use net::uri; +use strconv; +use strings; +use bufio; +use memio; +use encoding::utf8; +use types; + +// Stores state related to an HTTP request. +// +// For a request to be processable by an HTTP [[client]], i.e. via [[do]], the +// method and target must be filled in appropriately. The target must include at +// least a host, port, and scheme. The default values for other fields are +// suitable if appropriate for the request you wish to perform. +export type request = struct { + // HTTP request method, e.g. GET + method: str, + // Request target URI. + // + // Note that the normal constraints for [[uri::parse]] are not upheld in + // the case of a request using the origin-form (e.g. /index.html), i.e. + // the scheme field may be empty. + target: *uri::uri, + + // List of HTTP request headers. + header: header, + + // Transport configuration, or null to use the [[client]] default. + transport: nullable *transport, + + // I/O reader for the request body, or void if there is no body. + body: (io::handle | void), +}; + +// Frees state associated with an HTTP [[request]]. +export fn request_finish(req: *request) void = { + header_free(&req.header); + uri::finish(req.target); + free(req.target); +}; + +// Creates a new HTTP [[request]] using the given HTTP [[client]] defaults. +export fn new_request( + client: *client, + method: str, + target: *uri::uri, +) (request | errors::unsupported) = { + let req = request { + method = method, + target = alloc(uri::dup(target)), + header = header_dup(&client.default_header), + transport = null, + body = void, + }; + switch (req.target.scheme) { + case "http" => + if (req.target.port == 0) { + req.target.port = 80; + }; + case "https" => + if (req.target.port == 0) { + req.target.port = 443; + }; + case => + return errors::unsupported; + }; + + let host = match (req.target.host) { + case let host: str => + yield host; + case let ip: ip::addr4 => + yield ip::string(ip); + case let ip: ip::addr6 => + static let buf: [64 + 2]u8 = [0...]; + yield fmt::bsprintf(buf, "[{}]", ip::string(ip)); + }; + + if (req.target.scheme == "http" && req.target.port != 80) { + host = fmt::asprintf("{}:{}", host, req.target.port); + } else if (target.scheme == "https" && target.port != 443) { + host = fmt::asprintf("{}:{}", host, req.target.port); + } else { + host = strings::dup(host); + }; + defer free(host); + header_add(&req.header, "Host", host); + return req; +}; + +// Creates a new HTTP [[request]] using the given HTTP [[client]] defaults and +// the provided request body. +// +// If the provided I/O handle is seekable, the Content-Length header is added +// automatically. Otherwise, Transfer-Encoding: chunked will be used. +export fn new_request_body( + client: *client, + method: str, + target: *uri::uri, + body: io::handle, +) (request | errors::unsupported) = { + let req = new_request(client, method, target)?; + req.body = body; + + const offs = match (io::seek(body, 0, io::whence::CUR)) { + case let off: io::off => + yield off; + case io::error => + header_add(&req.header, "Transfer-Encoding", "chunked"); + return req; + }; + const ln = io::seek(body, 0, io::whence::END)!; + io::seek(body, offs, io::whence::SET)!; + header_add(&req.header, "Content-Length", strconv::ztos(ln: size)); + return req; +}; + +export type request_server = void; +export type authority = void; + +export type request_uri = ( + request_server | + authority | + *uri::uri | + str | +); + +export type request_line = struct { + method: str, + uri: request_uri, + version: version, +}; + +export fn request_parse(file: io::handle) (request | protoerr | io::error) = { + const scan = bufio::newscanner(file, types::SIZE_MAX); + defer bufio::finish(&scan); + + const req_line = request_line_parse(&scan)?; + defer request_line_finish(&req_line); + + let header: header = []; + read_header(&header, &scan)?; + + const target = match (req_line.uri) { + case let uri: request_server => return errors::unsupported; + case let uri: authority => return errors::unsupported; + case let uri: *uri::uri => yield uri; + case let path: str => + const host = header_get(&header, "Host"); + const uri = fmt::asprintf("http://{}{}", host, path); + defer free(uri); + yield alloc(uri::parse(uri)!); + }; + + const length: (void | size) = void; + const head_length = header_get(&header, "Content-Length"); + if ("" != head_length) { + match (strconv::stoz(head_length)) { + case let s: size => length = s; + case strconv::invalid => return protoerr; + case strconv::overflow => return protoerr; + }; + }; + + let body: (io::handle | void) = void; + if (length is size) { + const limit = io::limitreader(&scan, length as size); + let _body = alloc(memio::dynamic()); + io::copy(_body, &limit)!; + io::seek(_body, 0, io::whence::SET)!; + body = _body; + }; + + return request { + method = req_line.method, + target = target, + header = header, + body = body, + ... + }; +}; + +export fn parsed_request_finish(request: *request) void = { + uri::finish(request.target); + free(request.target); + + match (request.body) { + case void => yield; + case let body: io::handle => + io::close(body)!; + free(body: *memio::stream); + }; + + header_free(&request.header); +}; + +export fn request_line_parse(scan: *bufio::scanner) (request_line | protoerr | io::error) = { + const line = match (bufio::scan_string(scan, "\r\n")) { + case let line: const str => + yield line; + case let err: io::error => + return err; + case utf8::invalid => + return protoerr; + case io::EOF => + return protoerr; + }; + + const tok = strings::tokenize(line, " "); + + const method = match (strings::next_token(&tok)) { + case let method: str => + yield strings::dup(method); + case done => + return protoerr; + }; + + const uri: request_uri = match (strings::next_token(&tok)) { + case let req_uri: str => + if ("*" == req_uri) { + yield request_server; + }; + + yield match (uri::parse(req_uri)) { + case let uri: uri::uri => yield alloc(uri); + case => yield strings::dup(req_uri); // as path + }; + case done => + return protoerr; + }; + + const version = match (strings::next_token(&tok)) { + case let ver: str => + yield ver; + case done => + return protoerr; + }; + + const (_, version) = strings::cut(version, "/"); + const (major, minor) = strings::cut(version, "."); + + const major = match (strconv::stou(major)) { + case let u: uint => + yield u; + case => + return protoerr; + }; + const minor = match (strconv::stou(minor)) { + case let u: uint => + yield u; + case => + return protoerr; + }; + + if (major > 1) { + return errors::unsupported; + }; + + if (uri is request_server && method != OPTIONS) { + return protoerr; + }; + + return request_line { + method = method, + uri = uri, + version = (major, minor), + }; +}; + +export fn request_line_finish(line: *request_line) void = { + match (line.uri) { + case let path: str => free(path); + case => yield; + }; +}; diff --git a/vendor/hare-http/net/http/response.ha b/vendor/hare-http/net/http/response.ha new file mode 100644 index 0000000..84f0c81 --- /dev/null +++ b/vendor/hare-http/net/http/response.ha @@ -0,0 +1,63 @@ +use io; +use os; +use fmt; +use strconv; + +export type version = (uint, uint); + +// Stores state related to an HTTP response. +export type response = struct { + // HTTP protocol version (major, minor) + version: version, + // The HTTP status for this request as an integer. + status: uint, + // The HTTP status reason phrase. + reason: str, + // The HTTP headers provided by the server. + header: header, + // The response body, if any. + body: nullable *io::stream, +}; + +// Frees state associated with an HTTP [[response]]. If the response has a +// non-null body, the user must call [[io::close]] prior to calling this +// function. +export fn response_finish(resp: *response) void = { + header_free(&resp.header); + free(resp.reason); + free(resp.body); +}; + +export fn response_write( + rw: io::handle, + status: uint, + body: (void | io::handle), + header: (str, str)... +) (void | io::error) = { + fmt::fprintfln(rw, "HTTP/1.1 {} {}", status, status_reason(status))?; + + let header = header_dup(&header); + defer header_free(&header); + + match (body) { + case void => void; + case let body: io::handle => + match (io::tell(body)) { + case io::error => void; + case let off: io::off => + header_add(&header, "Content-Length", strconv::i64tos(off)); + io::seek(body, 0, io::whence::SET)!; + body = &io::limitreader(body, off: size); + }; + }; + + write_header(rw, &header)?; + + fmt::fprintln(rw)!; + + match (body) { + case void => void; + case let body: io::handle => + io::copy(rw, body)!; + }; +}; diff --git a/vendor/hare-http/net/http/server.ha b/vendor/hare-http/net/http/server.ha new file mode 100644 index 0000000..42e21ac --- /dev/null +++ b/vendor/hare-http/net/http/server.ha @@ -0,0 +1,44 @@ +use net; +use net::ip; +use net::tcp; +use net::tcp::{listen_option}; + +export type server = struct { + socket: net::socket, +}; + +export type server_request = struct { + socket: net::socket, + request: request, +}; + +export fn listen(ip: ip::addr, port: u16, options: listen_option...) (*server | net::error) = { + return alloc(server { + socket = tcp::listen(ip, port, options...)?, + }); +}; + +export fn server_finish(server: *server) void = { + net::close(server.socket)!; + free(server); +}; + +export fn accept(server: *server) (net::socket | net::error) = { + return net::accept(server.socket)?; +}; + +export fn serve(server: *server) (*server_request | net::error) = { + const socket = accept(server)?; + const request = request_parse(socket)!; + + return alloc(server_request { + request = request, + socket = socket, + }); +}; + +export fn serve_finish(serv_req: *server_request) void = { + parsed_request_finish(&serv_req.request); + net::close(serv_req.socket)!; + free(serv_req); +}; diff --git a/vendor/hare-http/net/http/status.ha b/vendor/hare-http/net/http/status.ha new file mode 100644 index 0000000..bbb418a --- /dev/null +++ b/vendor/hare-http/net/http/status.ha @@ -0,0 +1,111 @@ +// A semantic HTTP error and its status code. +export type httperror = !uint; + +// Checks if an HTTP status code is semantically considered an error, returning +// [[httperror]] if so, or otherwise returning the original status code. +export fn check(status: uint) (uint | httperror) = { + if (status >= 400 && status < 600) { + return status: httperror; + }; + return status; +}; + +// Converts a standard HTTP status code into the reason text typically +// associated with this status code (or "Unknown Status" if the status code is +// not known to net::http). +export fn status_reason(status: uint) const str = { + switch (status) { + case STATUS_CONTINUE => + return "Continue"; + case STATUS_SWITCHING_PROTOCOLS => + return "Switching Protocols"; + case STATUS_OK => + return "Continue"; + case STATUS_CREATED => + return "Continue"; + case STATUS_ACCEPTED => + return "Accepted"; + case STATUS_NONAUTHORITATIVE_INFO => + return "Non-Authoritative Information"; + case STATUS_NO_CONTENT => + return "No Content"; + case STATUS_RESET_CONTENT => + return "Reset Content"; + case STATUS_PARTIAL_CONTENT => + return "Partial Content"; + case STATUS_MULTIPLE_CHOICES => + return "Multiple Choices"; + case STATUS_MOVED_PERMANENTLY => + return "Moved Permanently"; + case STATUS_FOUND => + return "Found"; + case STATUS_SEE_OTHER => + return "See Other"; + case STATUS_NOT_MODIFIED => + return "Not Modified"; + case STATUS_USE_PROXY => + return "Use Proxy"; + case STATUS_TEMPORARY_REDIRECT => + return "Temporary Redirect"; + case STATUS_PERMANENT_REDIRECT => + return "Permanent Redirect"; + case STATUS_BAD_REQUEST => + return "Bad Request"; + case STATUS_UNAUTHORIZED => + return "Unauthorized"; + case STATUS_PAYMENT_REQUIRED => + return "Payment Required"; + case STATUS_FORBIDDEN => + return "Forbidden"; + case STATUS_NOT_FOUND => + return "Not Found"; + case STATUS_METHOD_NOT_ALLOWED => + return "Method Not Allowed"; + case STATUS_NOT_ACCEPTABLE => + return "Not Acceptable"; + case STATUS_PROXY_AUTH_REQUIRED => + return "Proxy Authentication Required"; + case STATUS_REQUEST_TIMEOUT => + return "Request Timeout"; + case STATUS_CONFLICT => + return "Conflict"; + case STATUS_GONE => + return "Gone"; + case STATUS_LENGTH_REQUIRED => + return "Length Required"; + case STATUS_PRECONDITION_FAILED => + return "Precondition Failed"; + case STATUS_REQUEST_ENTITY_TOO_LARGE => + return "Request Entity Too Large"; + case STATUS_REQUEST_URI_TOO_LONG => + return "Request URI Too Long"; + case STATUS_UNSUPPORTED_MEDIA_TYPE => + return "Unsupported Media Type"; + case STATUS_REQUESTED_RANGE_NOT_SATISFIABLE => + return "Requested Range Not Satisfiable"; + case STATUS_EXPECTATION_FAILED => + return "Expectation Failed"; + case STATUS_TEAPOT => + return "I'm A Teapot"; + case STATUS_MISDIRECTED_REQUEST => + return "Misdirected Request"; + case STATUS_UNPROCESSABLE_ENTITY => + return "Unprocessable Entity"; + case STATUS_UPGRADE_REQUIRED => + return "Upgrade Required"; + case STATUS_INTERNAL_SERVER_ERROR => + return "Internal Server Error"; + case STATUS_NOT_IMPLEMENTED => + return "Not Implemented"; + case STATUS_BAD_GATEWAY => + return "Bad Gateway"; + case STATUS_SERVICE_UNAVAILABLE => + return "Service Unavailable"; + case STATUS_GATEWAY_TIMEOUT => + return "Gateway Timeout"; + case STATUS_HTTP_VERSION_NOT_SUPPORTED => + return "HTTP Version Not Supported"; + case => + return "Unknown status"; + }; +}; diff --git a/vendor/hare-http/net/http/transport.ha b/vendor/hare-http/net/http/transport.ha new file mode 100644 index 0000000..6343ccf --- /dev/null +++ b/vendor/hare-http/net/http/transport.ha @@ -0,0 +1,296 @@ +use errors; +use bufio; +use bytes; +use io; +use os; +use strconv; +use strings; +use types; + +// Configures the Transport-Encoding behavior. +// +// If set to NONE, no transport decoding or encoding is performed on the message +// body, irrespective of the value of the Transport-Encoding header. The user +// must perform any required encoding or decoding themselves in this mode. If +// set to AUTO, the implementation will examine the Transport-Encoding header +// and encode the message body appropriately. +// +// Most users will want this to be set to auto. +export type transport_mode = enum { + AUTO = 0, + NONE, +}; + +// Configures the Content-Encoding behavior. +// +// If set to NONE, no transport decoding or encoding is performed on the message +// body, irrespective of the value of the Content-Encoding header. The user must +// perform any required encoding or decoding themselves in this mode. If set to +// AUTO, the implementation will examine the Content-Encoding header and encode +// the message body appropriately. +// +// Most users will want this to be set to AUTO. +export type content_mode = enum { + AUTO = 0, + NONE, +}; + +// Describes an HTTP [[client]]'s transport configuration for a given request. +// +// The default value of this type sets all parameters to "auto". +export type transport = struct { + // Desired Transport-Encoding configuration, see [[transport_mode]] for + // details. + request_transport: transport_mode, + response_transport: transport_mode, + // Desired Content-Encoding configuration, see [[content_mode]] for + // details. + request_content: content_mode, + response_content: content_mode, +}; + +fn new_reader( + conn: io::file, + resp: *response, + scan: *bufio::scanner, +) (*io::stream | errors::unsupported | protoerr) = { + // TODO: Content-Encoding support + const cl = header_get(&resp.header, "Content-Length"); + const te = header_get(&resp.header, "Transfer-Encoding"); + + if (cl != "" || te == "") { + let length = types::SIZE_MAX; + if (cl != "") { + length = match (strconv::stoz(cl)) { + case let z: size => + yield z; + case => + return protoerr; + }; + }; + return new_identity_reader(conn, scan, length); + }; + + // TODO: Figure out the semantics for closing the stream + // The caller should probably be required to close it + // It should close/free any intermediate transport/content decoders + // And it should not close the actual connection if it's still in the + // connection pool + // Unless it isn't in the pool, then it should! + let stream: io::handle = conn; + let buffer: []u8 = bufio::scan_buffer(scan); + const iter = strings::tokenize(te, ","); + for (const tok => strings::next_token(&iter)) { + const te = strings::trim(tok); + + // XXX: We could add lzw support if someone added it to + // hare-compress + const next = switch (te) { + case "chunked" => + yield new_chunked_reader(stream, buffer); + case "deflate" => + abort(); // TODO + case "gzip" => + abort(); // TODO + case => + return errors::unsupported; + }; + stream = next; + + buffer = []; + }; + + if (!(stream is *io::stream)) { + // Empty Transfer-Encoding header + return protoerr; + }; + return stream as *io::stream; +}; + +type identity_reader = struct { + vtable: io::stream, + conn: io::file, + scan: *bufio::scanner, + src: io::limitstream, +}; + +const identity_reader_vtable = io::vtable { + reader = &identity_read, + closer = &identity_close, + ... +}; + +// Creates a new reader that reads data until the response's Content-Length is +// reached; i.e. the null Transport-Encoding. +fn new_identity_reader( + conn: io::file, + scan: *bufio::scanner, + content_length: size, +) *io::stream = { + const scan = alloc(*scan); + return alloc(identity_reader { + vtable = &identity_reader_vtable, + conn = conn, + scan = scan, + src = io::limitreader(scan, content_length), + ... + }); +}; + +fn identity_read( + s: *io::stream, + buf: []u8, +) (size | io::EOF | io::error) = { + let rd = s: *identity_reader; + assert(rd.vtable == &identity_reader_vtable); + return io::read(&rd.src, buf)?; +}; + +fn identity_close(s: *io::stream) (void | io::error) = { + let rd = s: *identity_reader; + assert(rd.vtable == &identity_reader_vtable); + + // Flush the remainder of the response in case the caller did not read + // it out entirely + io::copy(io::empty, &rd.src)?; + + bufio::finish(rd.scan); + free(rd.scan); + io::close(rd.conn)?; +}; + +type chunk_state = enum { + HEADER, + DATA, + FOOTER, +}; + +type chunked_reader = struct { + vtable: io::stream, + conn: io::handle, + buffer: [os::BUFSZ]u8, + state: chunk_state, + // Amount of read-ahead data in buffer + pending: size, + // Length of current chunk + length: size, +}; + +fn new_chunked_reader( + conn: io::handle, + buffer: []u8, +) *io::stream = { + let rd = alloc(chunked_reader { + vtable = &chunked_reader_vtable, + conn = conn, + ... + }); + rd.buffer[..len(buffer)] = buffer[..]; + rd.pending = len(buffer); + return rd; +}; + +const chunked_reader_vtable = io::vtable { + reader = &chunked_read, + ... +}; + +fn chunked_read( + s: *io::stream, + buf: []u8, +) (size | io::EOF | io::error) = { + // XXX: I am not satisfied with this code + let rd = s: *chunked_reader; + assert(rd.vtable == &chunked_reader_vtable); + + for (true) switch (rd.state) { + case chunk_state::HEADER => + let crlf = 0z; + for (true) { + const n = rd.pending; + match (bytes::index(rd.buffer[..n], ['\r', '\n'])) { + case let z: size => + crlf = z; + break; + case void => + yield; + }; + if (rd.pending >= len(rd.buffer)) { + // Chunk header exceeds buffer size + return errors::overflow; + }; + + match (io::read(rd.conn, rd.buffer[rd.pending..])?) { + case let n: size => + rd.pending += n; + case io::EOF => + if (rd.pending > 0) { + return errors::invalid; + }; + return io::EOF; + }; + }; + + // XXX: Should we do anything with chunk-ext? + const header = rd.buffer[..crlf]; + const (ln, _) = bytes::cut(header, ';'); + const ln = match (strings::fromutf8(ln)) { + case let s: str => + yield s; + case => + return errors::invalid; + }; + + match (strconv::stoz(ln, strconv::base::HEX)) { + case let z: size => + rd.length = z; + case => + return errors::invalid; + }; + if (rd.length == 0) { + return io::EOF; + }; + + const n = crlf + 2; + rd.buffer[..rd.pending - n] = rd.buffer[n..rd.pending]; + rd.pending -= n; + rd.state = chunk_state::DATA; + case chunk_state::DATA => + if (rd.pending == 0) { + match (io::read(rd.conn, rd.buffer)?) { + case let n: size => + rd.pending += n; + case io::EOF => + return io::EOF; + }; + }; + let n = len(buf); + if (n > rd.pending) { + n = rd.pending; + }; + if (n > rd.length) { + n = rd.length; + }; + buf[..n] = rd.buffer[..n]; + rd.buffer[..rd.pending - n] = rd.buffer[n..rd.pending]; + rd.pending -= n; + rd.length -= n; + rd.state = chunk_state::FOOTER; + return n; + case chunk_state::FOOTER => + for (rd.pending < 2) { + match (io::read(rd.conn, rd.buffer[rd.pending..])?) { + case let n: size => + rd.pending += n; + case io::EOF => + return io::EOF; + }; + }; + if (!bytes::equal(rd.buffer[..2], ['\r', '\n'])) { + return errors::invalid; + }; + rd.buffer[..rd.pending - 2] = rd.buffer[2..rd.pending]; + rd.pending -= 2; + rd.state = chunk_state::HEADER; + }; +}; diff --git a/vendor/hare-json/COPYING b/vendor/hare-json/COPYING new file mode 100644 index 0000000..c257317 --- /dev/null +++ b/vendor/hare-json/COPYING @@ -0,0 +1,367 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. diff --git a/vendor/hare-json/Makefile b/vendor/hare-json/Makefile new file mode 100644 index 0000000..f9bcca5 --- /dev/null +++ b/vendor/hare-json/Makefile @@ -0,0 +1,36 @@ +.POSIX: +.SUFFIXES: +HARE=hare +HAREFLAGS= +HAREDOC=haredoc + +DESTDIR= +PREFIX=/usr/local +SRCDIR=$(PREFIX)/src +HARESRCDIR=$(SRCDIR)/hare +THIRDPARTYDIR=$(HARESRCDIR)/third-party + +all: + @true # no-op + +check: + $(HARE) test + +clean: + rm -rf docs + +docs: + mkdir -p docs/encoding/json + $(HAREDOC) -Fhtml encoding > docs/encoding/index.html + $(HAREDOC) -Fhtml encoding::json > docs/encoding/json/index.html + +install: + mkdir -p "$(DESTDIR)$(THIRDPARTYDIR)"/encoding + mkdir -p "$(DESTDIR)$(THIRDPARTYDIR)"/encoding/json + install -m644 encoding/json/README "$(DESTDIR)$(THIRDPARTYDIR)"/encoding/json/README + install -m644 encoding/json/*.ha "$(DESTDIR)$(THIRDPARTYDIR)"/encoding/json + +uninstall: + rm -rf $(DESTDIR)$(THIRDPARTYDIR)/encoding/json + +.PHONY: all docs clean check install uninstall diff --git a/vendor/hare-json/README.md b/vendor/hare-json/README.md new file mode 100644 index 0000000..36cac8b --- /dev/null +++ b/vendor/hare-json/README.md @@ -0,0 +1,23 @@ +# hare-json + +This package provides JSON support for Hare. + +## Installation + +### From your distribution + +The recommended name for this package is "hare-json". Look for this, or +something similar, in your local package manager. This is the preferred way to +install this package. + +### System-wide installation + +``` +make install +``` + +### Vendoring + +``` +git subtree -P vendor/hare-json/ add https://git.sr.ht/~sircmpwn/hare-json master +``` diff --git a/vendor/hare-json/encoding/json/+test/lexer.ha b/vendor/hare-json/encoding/json/+test/lexer.ha new file mode 100644 index 0000000..b4c098e --- /dev/null +++ b/vendor/hare-json/encoding/json/+test/lexer.ha @@ -0,0 +1,62 @@ +use io; +use memio; +use strings; + +@test fn lex() void = { + const cases: [_](str, []token) = [ + ("true", [true]), + ("false", [false]), + ("null", [_null]), + ("1234", [1234.0]), + ("12.34", [12.34]), + ("12.34e5", [12.34e5]), + ("12.34E5", [12.34e5]), + ("12.34e+5", [12.34e5]), + ("12.34e-5", [12.34e-5]), + ("12e5", [12.0e5]), + ("-1234", [-1234.0]), + (`"hello world"`, ["hello world"]), + (`"\"\\\/\b\f\n\r\t\u0020"`, ["\"\\/\b\f\n\r\t\u0020"]), + ("[ null, null ]", [arraystart, _null, comma, _null, arrayend]), + ]; + + for (let i = 0z; i < len(cases); i += 1) { + const src = strings::toutf8(cases[i].0); + const src = memio::fixed(src); + const lexer = newlexer(&src); + defer close(&lexer); + + for (let j = 0z; j < len(cases[i].1); j += 1) { + const want = cases[i].1[j]; + const have = lex(&lexer)! as token; + assert(tokeq(want, have)); + }; + + assert(lex(&lexer) is io::EOF); + }; +}; + +fn tokeq(want: token, have: token) bool = { + match (want) { + case _null => + return have is _null; + case comma => + return have is comma; + case colon => + return have is colon; + case arraystart => + return have is arraystart; + case arrayend => + return have is arrayend; + case objstart => + return have is objstart; + case objend => + return have is objend; + case let b: bool => + return have as bool == b; + case let f: f64 => + return have as f64 == f; + case let s: str => + return have as str == s; + }; +}; diff --git a/vendor/hare-json/encoding/json/+test/test_load.ha b/vendor/hare-json/encoding/json/+test/test_load.ha new file mode 100644 index 0000000..bf53777 --- /dev/null +++ b/vendor/hare-json/encoding/json/+test/test_load.ha @@ -0,0 +1,164 @@ +use fmt; + +fn roundtrip(input: str, expected: value) void = { + const val = loadstr(input)!; + defer finish(val); + assert(equal(val, expected)); + const s = dumpstr(val); + defer free(s); + const val = loadstr(s)!; + defer finish(val); + assert(equal(val, expected)); +}; + +fn errassert(input: str, expected_loc: (uint, uint)) void = { + const loc = loadstr(input) as invalid; + if (loc.0 != expected_loc.0 || loc.1 != expected_loc.1) { + fmt::errorfln("=== JSON:\n{}", input)!; + fmt::errorfln("=== expected error location:\n({}, {})", + expected_loc.0, expected_loc.1)!; + fmt::errorfln("=== actual error location:\n({}, {})", + loc.0, loc.1)!; + abort(); + }; +}; + +@test fn load() void = { + let obj = newobject(); + defer finish(obj); + let obj2 = newobject(); + defer finish(obj2); + + roundtrip(`1234`, 1234.0); + roundtrip(`[]`, []); + roundtrip(`[1, 2, 3, null]`, [1.0, 2.0, 3.0, _null]); + roundtrip(`{}`, obj); + set(&obj, "hello", "world"); + set(&obj, "answer", 42.0); + roundtrip(`{ "hello": "world", "answer": 42 }`, obj); + reset(&obj); + roundtrip(`[[] ]`, [[]]); + roundtrip(`[""]`, [""]); + roundtrip(`["a"]`, ["a"]); + roundtrip(`[false]`, [false]); + roundtrip(`[null, 1, "1", {}]`, [_null, 1.0, "1", obj]); + roundtrip(`[null]`, [_null]); + roundtrip("[1\n]", [1.0]); + roundtrip(`[1,null,null,null,2]`, [1.0, _null, _null, _null, 2.0]); + set(&obj, "", 0.0); + roundtrip(`{"":0}`, obj); + reset(&obj); + set(&obj, "foo\0bar", 42.0); + roundtrip(`{"foo\u0000bar": 42}`, obj); + reset(&obj); + set(&obj, "min", -1.0e+28); + set(&obj, "max", 1.0e+28); + roundtrip(`{"min": -1.0e+28, "max": 1.0e+28}`, obj); + reset(&obj); + set(&obj, "id", "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"); + set(&obj2, "id", "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"); + set(&obj, "x", [obj2]); + roundtrip(`{"x":[{"id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}], "id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}`, obj); + reset(&obj); + reset(&obj2); + set(&obj, "a", []); + roundtrip(`{"a":[]}`, obj); + roundtrip("{\n" `"a": []` "\n}", obj); + reset(&obj); + roundtrip(`"\u0060\u012a\u12AB"`, "\u0060\u012a\u12AB"); + roundtrip(`"\"\\\/\b\f\n\r\t"`, "\"\\/\b\f\n\r\t"); + roundtrip(`"\\u0000"`, `\u0000`); + roundtrip(`"\""`, `"`); + roundtrip(`"a/*b*/c/*d//e"`, "a/*b*/c/*d//e"); + roundtrip(`"\\a"`, `\a`); + roundtrip(`"\\n"`, `\n`); + roundtrip(`"\u0012"`, "\u0012"); + roundtrip(`[ "asd"]`, ["asd"]); + roundtrip(`"new\u000Aline"`, "new\nline"); + roundtrip(`"\u0000"`, "\0"); + roundtrip(`"\u002c"`, "\u002c"); + roundtrip(`"asd "`, "asd "); + roundtrip(`" "`, " "); + roundtrip(`"\u0821"`, "\u0821"); + roundtrip(`"\u0123"`, "\u0123"); + roundtrip(`"\u0061\u30af\u30EA\u30b9"`, "\u0061\u30af\u30EA\u30b9"); + roundtrip(`"\uA66D"`, "\uA66D"); + roundtrip(`"\u005C"`, `\`); + roundtrip(`"\u0022"`, `"`); + roundtrip(`""`, ""); + roundtrip(` [] `, []); + + errassert(`[1,,]`, (1, 4)); + errassert(`[1 true]`, (1, 7)); + errassert(`["": 1]`, (1, 4)); + errassert(`[,1]`, (1, 2)); + errassert(`[1,,2]`, (1, 4)); + errassert(`["",]`, (1, 5)); + errassert(`["x"`, (1, 5)); + errassert(`[x`, (1, 2)); + errassert(`[3[4]]`, (1, 3)); + errassert(`[1:2]`, (1, 3)); + errassert(`[,]`, (1, 2)); + errassert(`[-]`, (1, 3)); + errassert(`[ , ""]`, (1, 5)); + errassert("[\"a\",\n4\n,1,", (3, 4)); + errassert(`[1,]`, (1, 4)); + errassert("[\"\va\"\\f", (1, 3)); + errassert(`[*]`, (1, 2)); + errassert(`[1,`, (1, 4)); + errassert("[1,\n1\n,1", (3, 3)); + errassert(`[{}`, (1, 4)); + errassert(`["x", truth]`, (1, 11)); + errassert(`{[: "x"}`, (1, 2)); + errassert(`{"x", null}`, (1, 5)); + errassert(`{"x"::"b"}`, (1, 6)); + errassert(`{"a":"a" 123}`, (1, 12)); + errassert(`{"a" b}`, (1, 6)); + errassert(`{:"b"}`, (1, 2)); + errassert(`{"a" "b"}`, (1, 8)); + errassert(`{"a":`, (1, 6)); + errassert(`{"a"`, (1, 5)); + errassert(`{1:1}`, (1, 2)); + errassert(`{9999E9999:1}`, (1, 10)); + errassert(`{null:null,null:null}`, (1, 5)); + errassert(`{"id":0,,,,,}`, (1, 9)); + errassert(`{'a':0}`, (1, 2)); + errassert(`{"id":0,}`, (1, 9)); + errassert(`{"a":"b",,"c":"d"}`, (1, 10)); + errassert(`{true: false}`, (1, 5)); + errassert(`{"a":"a`, (1, 8)); + errassert(`{ "foo" : "bar", "a" }`, (1, 22)); + errassert(` `, (1, 2)); + errassert(``, (1, 1)); + errassert(`["asd]`, (1, 7)); + errassert(`True`, (1, 4)); + errassert(`]`, (1, 1)); + errassert(`}`, (1, 1)); + errassert(`{"x": true,`, (1, 12)); + errassert(`[`, (1, 2)); + errassert(`{`, (1, 2)); + errassert(``, (1, 1)); + errassert("\0", (1, 1)); + errassert(`{"":`, (1, 5)); + errassert(`['`, (1, 2)); + errassert(`["`, (1, 3)); + errassert(`[,`, (1, 2)); + errassert(`[{`, (1, 3)); + errassert(`{[`, (1, 2)); + errassert(`{]`, (1, 2)); + errassert(`[}`, (1, 2)); + errassert(`{'`, (1, 2)); + errassert(`{"`, (1, 3)); + errassert(`{,`, (1, 2)); + errassert(`["\{["\{["\{["\{`, (1, 4)); + errassert(`*`, (1, 1)); + errassert(`\u000A""`, (1, 1)); + errassert("\f", (1, 1)); +}; + +@test fn nestlimit() void = { + const s = `{ "foo": [[[{"bar": ["baz"]}]]] }`; + const val = loadstr(s, 6: nestlimit)!; + finish(val); + assert(loadstr(s, 5: nestlimit) is limitreached); +}; diff --git a/vendor/hare-json/encoding/json/+test/test_value.ha b/vendor/hare-json/encoding/json/+test/test_value.ha new file mode 100644 index 0000000..eca7dcf --- /dev/null +++ b/vendor/hare-json/encoding/json/+test/test_value.ha @@ -0,0 +1,49 @@ +// License: MPL-2.0 +// (c) 2022 Drew DeVault + +@test fn object() void = { + let obj = newobject(); + defer finish(obj); + + set(&obj, "hello", "world"); + set(&obj, "foo", "bar"); + set(&obj, "the answer", 42.0); + + // XXX: Match overhaul? + assert(*(get(&obj, "hello") as *value) as str == "world"); + assert(*(get(&obj, "foo") as *value) as str == "bar"); + assert(*(get(&obj, "the answer") as *value) as f64 == 42.0); + assert(get(&obj, "nonexistent") is void); + + del(&obj, "hello"); + assert(get(&obj, "hello") is void); +}; + +@test fn iterator() void = { + let obj = newobject(); + defer finish(obj); + + set(&obj, "hello", "world"); + set(&obj, "foo", "bar"); + set(&obj, "the answer", 42.0); + + let it = iter(&obj); + assert(next(&it) is (const str, const *value)); + assert(next(&it) is (const str, const *value)); + assert(next(&it) is (const str, const *value)); + assert(next(&it) is void); +}; + +@test fn equal() void = { + let a = newobject(); + defer finish(a); + set(&a, "a", 42.0); + set(&a, "A", "hello"); + + let b = newobject(); + defer finish(b); + set(&b, "A", "hello"); + set(&b, "a", 42.0); + + assert(equal(a, b)); +}; diff --git a/vendor/hare-json/encoding/json/README b/vendor/hare-json/encoding/json/README new file mode 100644 index 0000000..fa917d5 --- /dev/null +++ b/vendor/hare-json/encoding/json/README @@ -0,0 +1,15 @@ +This module provides an implementation of the JavaScript Object Notation (JSON) +format, as defined by RFC 8259. Note that several other, incompatible +specifications exist. This implementation does not include any extensions; only +features which are strictly required by the spec are implemented. + +A lexer for JSON values is provided, which may be initialized with [[lex]] and +provides tokens via [[next]], and which uses a relatively small amount of memory +and provides relatively few guarantees regarding the compliance of the input with +the JSON grammar. + +Additionally, the [[value]] type is provided to store any value JSON value, as +well as helpers like [[newobject]], [[get]], and [[set]]. One can load a JSON +value from an input stream into a heap-allocated [[value]] via [[load]], which +enforces all of JSON's grammar constraints and returns an object which must be +freed with [[finish]]. diff --git a/vendor/hare-json/encoding/json/dump.ha b/vendor/hare-json/encoding/json/dump.ha new file mode 100644 index 0000000..7e7dd8d --- /dev/null +++ b/vendor/hare-json/encoding/json/dump.ha @@ -0,0 +1,81 @@ +// License: MPL-2.0 +// (c) 2022 Sebastian +use fmt; +use io; +use strings; +use memio; + +// Dumps a [[value]] into an [[io::handle]] as a string without any additional +// formatting. +export fn dump(out: io::handle, val: value) (size | io::error) = { + let z = 0z; + match (val) { + case let v: (f64 | bool) => + z += fmt::fprint(out, v)?; + case let s: str => + z += fmt::fprint(out, `"`)?; + let it = strings::iter(s); + for (const r => strings::next(&it)) { + switch (r) { + case '\b' => + z += fmt::fprint(out, `\b`)?; + case '\f' => + z += fmt::fprint(out, `\f`)?; + case '\n' => + z += fmt::fprint(out, `\n`)?; + case '\r' => + z += fmt::fprint(out, `\r`)?; + case '\t' => + z += fmt::fprint(out, `\t`)?; + case '\"' => + z += fmt::fprint(out, `\"`)?; + case '\\' => + z += fmt::fprint(out, `\\`)?; + case => + if (iscntrl(r)) { + z += fmt::fprintf(out, `\u{:.4x}`, + r: u32)?; + } else { + z += fmt::fprint(out, r)?; + }; + }; + }; + z += fmt::fprint(out, `"`)?; + case _null => + z += fmt::fprint(out, "null")?; + case let a: []value => + z += fmt::fprint(out, "[")?; + for (let i = 0z; i < len(a); i += 1) { + z += dump(out, a[i])?; + if (i < len(a) - 1) { + z += fmt::fprint(out, ",")?; + }; + }; + z += fmt::fprint(out, "]")?; + case let o: object => + z += fmt::fprint(out, "{")?; + let comma = false; + let it = iter(&o); + for (true) match (next(&it)) { + case void => break; + case let pair: (const str, const *value) => + if (comma) { + z += fmt::fprint(out, ",")?; + }; + comma = true; + z += dump(out, pair.0)?; + z += fmt::fprint(out, ":")?; + z += dump(out, *pair.1)?; + }; + z += fmt::fprint(out, "}")?; + }; + return z; +}; + +// Dumps a [[value]] into a string without any additional formatting. The caller +// must free the return value. +export fn dumpstr(val: value) str = { + let s = memio::dynamic(); + dump(&s, val)!; + return memio::string(&s)!; +}; diff --git a/vendor/hare-json/encoding/json/lex.ha b/vendor/hare-json/encoding/json/lex.ha new file mode 100644 index 0000000..0f14a0c --- /dev/null +++ b/vendor/hare-json/encoding/json/lex.ha @@ -0,0 +1,377 @@ +// License: MPL-2.0 +// (c) 2022 Drew DeVault +use ascii; +use bufio; +use encoding::utf8; +use io; +use os; +use strconv; +use strings; +use memio; + +export type lexer = struct { + src: io::handle, + strbuf: memio::stream, + un: (token | void), + rb: (rune | void), + loc: (uint, uint), + prevloc: (uint, uint), + nextloc: (uint, uint), + prevrloc: (uint, uint), +}; + +// Creates a new JSON lexer. The caller may obtain tokens with [[lex]] and +// should pass the result to [[close]] when they're done with it. +export fn newlexer(src: io::handle) lexer = lexer { + src = src, + strbuf = memio::dynamic(), + un = void, + rb = void, + loc = (1, 0), + ... +}; + +// Frees state associated with a JSON lexer. +export fn close(lex: *lexer) void = { + io::close(&lex.strbuf)!; +}; + +// Returns the next token from a JSON lexer. The return value is borrowed from +// the lexer and will be overwritten on subsequent calls. +export fn lex(lex: *lexer) (token | io::EOF | error) = { + match (lex.un) { + case void => + lex.prevloc = lex.loc; + case let tok: token => + lex.un = void; + lex.prevloc = lex.loc; + lex.loc = lex.nextloc; + return tok; + }; + + const rn = match (nextrunews(lex)?) { + case io::EOF => + return io::EOF; + case let rn: rune => + yield rn; + }; + + switch (rn) { + case '[' => + return arraystart; + case ']' => + return arrayend; + case '{' => + return objstart; + case '}' => + return objend; + case ',' => + return comma; + case ':' => + return colon; + case '"' => + return scan_str(lex)?; + case => + yield; + }; + + if (ascii::isdigit(rn) || rn == '-') { + unget(lex, rn); + return scan_number(lex)?; + }; + + if (!ascii::isalpha(rn)) { + return lex.loc: invalid; + }; + + unget(lex, rn); + const word = scan_word(lex)?; + switch (word) { + case "true" => + return true; + case "false" => + return false; + case "null" => + return _null; + case => + return lex.loc: invalid; + }; +}; + +// "Unlexes" a token from the lexer, such that the next call to [[lex]] will +// return that token again. Only one token can be unlexed at a time, otherwise +// the program will abort. +export fn unlex(lex: *lexer, tok: token) void = { + assert(lex.un is void, "encoding::json::unlex called twice in a row"); + lex.un = tok; + lex.nextloc = lex.loc; + lex.loc = lex.prevloc; +}; + +// Scans until encountering a non-alphabetical character, returning the +// resulting word. +fn scan_word(lex: *lexer) (str | error) = { + memio::reset(&lex.strbuf); + + for (true) { + const rn = match (nextrune(lex)?) { + case let rn: rune => + yield rn; + case io::EOF => + break; + }; + if (!ascii::isalpha(rn)) { + unget(lex, rn); + break; + }; + memio::appendrune(&lex.strbuf, rn)!; + }; + + return memio::string(&lex.strbuf)!; +}; + +type numstate = enum { + SIGN, + START, + ZERO, + INTEGER, + FRACSTART, + FRACTION, + EXPSIGN, + EXPSTART, + EXPONENT, +}; + +fn scan_number(lex: *lexer) (token | error) = { + memio::reset(&lex.strbuf); + + let state = numstate::SIGN; + for (true) { + const rn = match (nextrune(lex)?) { + case let rn: rune => + yield rn; + case io::EOF => + break; + }; + + switch (state) { + case numstate::SIGN => + state = numstate::START; + if (rn != '-') { + unget(lex, rn); + continue; + }; + case numstate::START => + switch (rn) { + case '0' => + state = numstate::ZERO; + case => + if (!ascii::isdigit(rn)) { + return lex.loc: invalid; + }; + state = numstate::INTEGER; + }; + case numstate::ZERO => + switch (rn) { + case '.' => + state = numstate::FRACSTART; + case 'e', 'E' => + state = numstate::EXPSIGN; + case => + if (ascii::isdigit(rn)) { + return lex.loc: invalid; + }; + unget(lex, rn); + break; + }; + case numstate::INTEGER => + switch (rn) { + case '.' => + state = numstate::FRACSTART; + case 'e', 'E' => + state = numstate::EXPSIGN; + case => + if (!ascii::isdigit(rn)) { + unget(lex, rn); + break; + }; + }; + case numstate::FRACSTART => + if (!ascii::isdigit(rn)) { + return lex.loc: invalid; + }; + state = numstate::FRACTION; + case numstate::FRACTION => + switch (rn) { + case 'e', 'E' => + state = numstate::EXPSIGN; + case => + if (!ascii::isdigit(rn)) { + unget(lex, rn); + break; + }; + }; + case numstate::EXPSIGN => + state = numstate::EXPSTART; + if (rn != '+' && rn != '-') { + unget(lex, rn); + continue; + }; + case numstate::EXPSTART => + if (!ascii::isdigit(rn)) { + return lex.loc: invalid; + }; + state = numstate::EXPONENT; + case numstate::EXPONENT => + if (!ascii::isdigit(rn)) { + unget(lex, rn); + break; + }; + }; + + memio::appendrune(&lex.strbuf, rn)!; + }; + + match (strconv::stof64(memio::string(&lex.strbuf)!)) { + case let f: f64 => + return f; + case => + return lex.loc: invalid; + }; +}; + +fn scan_str(lex: *lexer) (token | error) = { + memio::reset(&lex.strbuf); + + for (true) { + const rn = match (nextrune(lex)?) { + case let rn: rune => + yield rn; + case io::EOF => + lex.loc.1 += 1; + return lex.loc: invalid; + }; + + switch (rn) { + case '"' => + break; + case '\\' => + const rn = scan_escape(lex)?; + memio::appendrune(&lex.strbuf, rn)!; + case => + if (iscntrl(rn)) { + return lex.loc: invalid; + }; + memio::appendrune(&lex.strbuf, rn)!; + }; + }; + + return memio::string(&lex.strbuf)!; +}; + +fn scan_escape(lex: *lexer) (rune | error) = { + const rn = match (nextrune(lex)?) { + case let rn: rune => + yield rn; + case io::EOF => + return lex.loc: invalid; + }; + + switch (rn) { + case '\"' => + return '\"'; + case '\\' => + return '\\'; + case '/' => + return '/'; + case 'b' => + return '\b'; + case 'f' => + return '\f'; + case 'n' => + return '\n'; + case 'r' => + return '\r'; + case 't' => + return '\t'; + case 'u' => + let buf: [4]u8 = [0...]; + match (io::readall(lex.src, buf)?) { + case io::EOF => + return lex.loc: invalid; + case size => + yield; + }; + const s = match (strings::fromutf8(buf)) { + case let s: str => + yield s; + case => + return lex.loc: invalid; + }; + match (strconv::stou32(s, strconv::base::HEX)) { + case let u: u32 => + lex.loc.1 += 4; + return u: rune; + case => + return lex.loc: invalid; + }; + case => + return lex.loc: invalid; + }; +}; + +// Gets the next rune from the lexer. +fn nextrune(lex: *lexer) (rune | io::EOF | error) = { + if (lex.rb is rune) { + lex.prevrloc = lex.loc; + const r = lex.rb as rune; + lex.rb = void; + if (r == '\n') { + lex.loc = (lex.loc.0 + 1, 0); + } else { + lex.loc.1 += 1; + }; + return r; + }; + match (bufio::read_rune(lex.src)) { + case let err: io::error => + return err; + case utf8::invalid => + return lex.loc: invalid; + case io::EOF => + return io::EOF; + case let rn: rune => + lex.prevrloc = lex.loc; + if (rn == '\n') { + lex.loc = (lex.loc.0 + 1, 0); + } else { + lex.loc.1 += 1; + }; + return rn; + }; +}; + +// Like nextrune but skips whitespace. +fn nextrunews(lex: *lexer) (rune | io::EOF | error) = { + for (true) { + match (nextrune(lex)?) { + case let rn: rune => + if (isspace(rn)) { + continue; + }; + return rn; + case io::EOF => + return io::EOF; + }; + }; +}; + +fn unget(lex: *lexer, r: rune) void = { + assert(lex.rb is void); + lex.rb = r; + lex.loc = lex.prevrloc; +}; + +fn iscntrl(r: rune) bool = r: u32 < 0x20; + +fn isspace(r: rune) bool = ascii::isspace(r) && r != '\f'; diff --git a/vendor/hare-json/encoding/json/load.ha b/vendor/hare-json/encoding/json/load.ha new file mode 100644 index 0000000..8dc2b56 --- /dev/null +++ b/vendor/hare-json/encoding/json/load.ha @@ -0,0 +1,148 @@ +use memio; +use io; +use strings; +use types; + +// Options for [[load]]. +export type load_option = nestlimit; + +// The maximum number of nested objects or arrays that can be entered before +// erroring out. +export type nestlimit = uint; + +// Parses a JSON value from the given [[io::handle]], returning the value or an +// error. The return value is allocated on the heap; use [[finish]] to free it +// up when you're done using it. +// +// By default, this function assumes non-antagonistic inputs, and does not limit +// recursion depth or memory usage. You may want to set a custom [[nestlimit]], +// or incorporate an [[io::limitreader]] or similar. Alternatively, you can use +// the JSON lexer ([[lex]]) directly if dealing with potentially malicious +// inputs. +export fn load(src: io::handle, opts: load_option...) (value | error) = { + let limit = types::UINT_MAX; + for (let i = 0z; i < len(opts); i += 1) { + limit = opts[i]: nestlimit: uint; + }; + const lex = newlexer(src); + defer close(&lex); + return _load(&lex, 0, limit); +}; + +// Parses a JSON value from the given string, returning the value or an error. +// The return value is allocated on the heap; use [[finish]] to free it up when +// you're done using it. +// +// See the documentation for [[load]] for information on dealing with +// potentially malicious inputs. +export fn loadstr(input: str, opts: load_option...) (value | error) = { + let src = memio::fixed(strings::toutf8(input)); + return load(&src, opts...); +}; + +fn _load(lexer: *lexer, level: uint, limit: uint) (value | error) = { + const tok = mustscan(lexer)?; + match (tok) { + case _null => + return _null; + case let b: bool => + return b; + case let f: f64 => + return f; + case let s: str => + return strings::dup(s); + case arraystart => + if (level == limit) { + return limitreached; + }; + return _load_array(lexer, level + 1, limit); + case objstart => + if (level == limit) { + return limitreached; + }; + return _load_obj(lexer, level + 1, limit); + case (arrayend | objend | colon | comma) => + return lexer.loc: invalid; + }; +}; + +fn _load_array(lexer: *lexer, level: uint, limit: uint) (value | error) = { + let success = false; + let array: []value = []; + defer if (!success) finish(array); + let tok = mustscan(lexer)?; + match (tok) { + case arrayend => + success = true; + return array; + case => + unlex(lexer, tok); + }; + + for (true) { + append(array, _load(lexer, level, limit)?); + + tok = mustscan(lexer)?; + match (tok) { + case comma => void; + case arrayend => break; + case => + return lexer.loc: invalid; + }; + }; + success = true; + return array; +}; + +fn _load_obj(lexer: *lexer, level: uint, limit: uint) (value | error) = { + let success = false; + let obj = newobject(); + defer if (!success) finish(obj); + let tok = mustscan(lexer)?; + match (tok) { + case objend => + success = true; + return obj; + case => + unlex(lexer, tok); + }; + + for (true) { + let tok = mustscan(lexer)?; + const key = match (tok) { + case let s: str => + yield strings::dup(s); + case => + return lexer.loc: invalid; + }; + defer free(key); + + tok = mustscan(lexer)?; + if (!(tok is colon)) { + return lexer.loc: invalid; + }; + + put(&obj, key, _load(lexer, level, limit)?); + + tok = mustscan(lexer)?; + match (tok) { + case comma => void; + case objend => break; + case => + return lexer.loc: invalid; + }; + }; + + success = true; + return obj; +}; + +fn mustscan(lexer: *lexer) (token | error) = { + match (lex(lexer)?) { + case io::EOF => + lexer.loc.1 += 1; + return lexer.loc: invalid; + case let tok: token => + return tok; + }; +}; diff --git a/vendor/hare-json/encoding/json/path/path.ha b/vendor/hare-json/encoding/json/path/path.ha new file mode 100644 index 0000000..819e9f5 --- /dev/null +++ b/vendor/hare-json/encoding/json/path/path.ha @@ -0,0 +1,26 @@ +// A compiled JSONPath query. +export type query = []segment; + +export type segment_type = enum { + CHILD, + DESCENDANT, +}; + +export type segment = struct { + stype: segment_type, + selector: selector, +}; + +export type selector = (str | wild | index | slice | filter); + +export type wild = void; + +export type index = int; + +export type slice = struct { + start: (int | void), + end: (int | void), + step: (int | void), +}; + +export type filter = void; // TODO diff --git a/vendor/hare-json/encoding/json/types.ha b/vendor/hare-json/encoding/json/types.ha new file mode 100644 index 0000000..1e1b433 --- /dev/null +++ b/vendor/hare-json/encoding/json/types.ha @@ -0,0 +1,50 @@ +// License: MPL-2.0 +// (c) 2022 Drew DeVault +use fmt; +use io; + +// An invalid JSON token was encountered at this location (line, column). +export type invalid = !(uint, uint); + +// The maximum nesting limit was reached. +export type limitreached = !void; + +// A tagged union of all possible errors returned from this module. +export type error = !(invalid | limitreached | io::error); + +// The JSON null value. +export type _null = void; + +// The '[' token, signaling the start of a JSON array. +export type arraystart = void; + +// The ']' token, signaling the end of a JSON array. +export type arrayend = void; + +// The '{' token, signaling the start of a JSON object. +export type objstart = void; + +// The '}' token, signaling the end of a JSON object. +export type objend = void; + +// The ':' token. +export type colon = void; + +// The ',' token. +export type comma = void; + +// All tokens which can be returned from the JSON tokenizer. +export type token = (arraystart | arrayend | objstart | + objend | colon | comma | str | f64 | bool | _null); + +// Converts an [[error]] into a human-friendly string. +export fn strerror(err: error) const str = { + static let buf: [53]u8 = [0...]; + match (err) { + case let err: invalid => + return fmt::bsprintf(buf, + "{}:{}: Invalid JSON token encountered", err.0, err.1); + case let err: io::error => + return io::strerror(err); + }; +}; diff --git a/vendor/hare-json/encoding/json/value.ha b/vendor/hare-json/encoding/json/value.ha new file mode 100644 index 0000000..793915d --- /dev/null +++ b/vendor/hare-json/encoding/json/value.ha @@ -0,0 +1,219 @@ +// License: MPL-2.0 +// (c) 2022 Drew DeVault +use hash::fnv; +use strings; + +// TODO: Resize table as appropriate +export def OBJECT_BUCKETS: size = 32; + +export type object = struct { + buckets: [OBJECT_BUCKETS][](str, value), + count: size, +}; + +// A JSON value. +export type value = (f64 | str | bool | _null | []value | object); + +// Initializes a new (empty) JSON object. Call [[finish]] to free associated +// resources when you're done using it. +export fn newobject() object = { + return object { ... }; +}; + +// Gets a value from a JSON object. The return value is borrowed from the +// object. +export fn get(obj: *object, key: str) (*value | void) = { + const hash = fnv::string(key); + const bucket = &obj.buckets[hash % len(obj.buckets)]; + for (let i = 0z; i < len(bucket); i += 1) { + if (bucket[i].0 == key) { + return &bucket[i].1; + }; + }; +}; + +// Sets a value in a JSON object. The key and value will be duplicated. +export fn set(obj: *object, key: const str, val: const value) void = { + put(obj, key, dup(val)); +}; + +// Sets a value in a JSON object. The key will be duplicated. The object will +// assume ownership over the value, without duplicating it. +export fn put(obj: *object, key: const str, val: const value) void = { + const hash = fnv::string(key); + const bucket = &obj.buckets[hash % len(obj.buckets)]; + for (let i = 0z; i < len(bucket); i += 1) { + if (bucket[i].0 == key) { + finish(bucket[i].1); + bucket[i].1 = val; + return; + }; + }; + obj.count += 1; + append(bucket, (strings::dup(key), val)); +}; + +// Deletes values from a JSON object, if they are present. +export fn del(obj: *object, keys: const str...) void = { + for (let i = 0z; i < len(keys); i += 1) { + match (take(obj, keys[i])) { + case let val: value => + finish(val); + case void => void; + }; + }; +}; + +// Deletes a key from a JSON object, returning its previous value, if any. +// The caller is responsible for freeing the value. +export fn take(obj: *object, key: const str) (value | void) = { + const hash = fnv::string(key); + const bucket = &obj.buckets[hash % len(obj.buckets)]; + for (let i = 0z; i < len(bucket); i += 1) { + if (bucket[i].0 == key) { + obj.count -= 1; + free(bucket[i].0); + const val = bucket[i].1; + delete(bucket[i]); + return val; + }; + }; +}; + +// Clears all values from a JSON object, leaving it empty. +export fn reset(obj: *object) void = { + let it = iter(obj); + for (true) match (next(&it)) { + case void => + break; + case let v: (const str, const *value) => + del(obj, v.0); + }; +}; + +// Returns the number of key/value pairs in a JSON object. +export fn count(obj: *object) size = { + return obj.count; +}; + +export type iterator = struct { + obj: *object, + i: size, + j: size, +}; + +// Creates an iterator that enumerates over the key/value pairs in an +// [[object]]. +export fn iter(obj: *object) iterator = { + return iterator { obj = obj, ... }; +}; + +// Returns the next key/value pair from this iterator, or void if none remain. +export fn next(iter: *iterator) ((const str, const *value) | void) = { + for (iter.i < len(iter.obj.buckets); iter.i += 1) { + const bucket = &iter.obj.buckets[iter.i]; + for (iter.j < len(bucket)) { + const key = bucket[iter.j].0; + const val = &bucket[iter.j].1; + iter.j += 1; + return (key, val); + }; + iter.j = 0; + }; +}; + +// Duplicates a JSON value. The caller must pass the return value to [[finish]] +// to free associated resources when they're done using it. +export fn dup(val: value) value = { + match (val) { + case let s: str => + return strings::dup(s); + case let v: []value => + let new: []value = alloc([], len(v)); + for (let i = 0z; i < len(v); i += 1) { + append(new, dup(v[i])); + }; + return new; + case let o: object => + let new = newobject(); + const i = iter(&o); + for (true) { + const pair = match (next(&i)) { + case void => + break; + case let pair: (const str, const *value) => + yield pair; + }; + set(&new, pair.0, *pair.1); + }; + return new; + case => + return val; + }; +}; + +// Checks two JSON values for equality. +export fn equal(a: value, b: value) bool = { + match (a) { + case _null => + return b is _null; + case let a: bool => + return b is bool && a == b as bool; + case let a: f64 => + return b is f64 && a == b as f64; + case let a: str => + return b is str && a == b as str; + case let a: []value => + if (!(b is []value)) return false; + const b = b as []value; + if (len(a) != len(b)) return false; + for (let i = 0z; i < len(a); i += 1) { + if (!equal(a[i], b[i])) { + return false; + }; + }; + return true; + case let a: object => + if (!(b is object)) return false; + let b = b as object; + if (count(&a) != count(&b)) { + return false; + }; + let a = iter(&a); + for (true) match (next(&a)) { + case let a: (const str, const *value) => + match (get(&b, a.0)) { + case let b: *value => + if (!equal(*a.1, *b)) { + return false; + }; + case void => return false; + }; + case void => break; + }; + return true; + }; +}; + +// Frees state associated with a JSON value. +export fn finish(val: value) void = { + match (val) { + case let s: str => + free(s); + case let v: []value => + for (let i = 0z; i < len(v); i += 1) { + finish(v[i]); + }; + free(v); + case let o: object => + for (let i = 0z; i < len(o.buckets); i += 1) { + const bucket = &o.buckets[i]; + for (let j = 0z; j < len(bucket); j += 1) { + free(bucket[j].0); + finish(bucket[j].1); + }; + free(*bucket); + }; + case => void; + }; +}; diff --git a/vendor/hare-logfmt/LICENSE b/vendor/hare-logfmt/LICENSE new file mode 100644 index 0000000..64e9fec --- /dev/null +++ b/vendor/hare-logfmt/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Blain Smith + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/vendor/hare-logfmt/Makefile b/vendor/hare-logfmt/Makefile new file mode 100644 index 0000000..1267a8b --- /dev/null +++ b/vendor/hare-logfmt/Makefile @@ -0,0 +1,28 @@ +.POSIX: +.SUFFIXES: +HARE=hare +HAREFLAGS= + +DESTDIR= +PREFIX=/usr/local +SRCDIR=$(PREFIX)/src +HARESRCDIR=$(SRCDIR)/hare +THIRDPARTYDIR=$(HARESRCDIR)/third-party + +all: + # no-op + +clean: + # no-op + +check: + $(HARE) test + +install: + mkdir -p $(DESTDIR)$(THIRDPARTYDIR)/log/logfmt + install -m644 * $(DESTDIR)$(THIRDPARTYDIR) + +uninstall: + rm -rf $(DESTDIR)$(THIRDPARTYDIR)/log/logfmt + +.PHONY: all clean check install uninstall \ No newline at end of file diff --git a/vendor/hare-logfmt/README.md b/vendor/hare-logfmt/README.md new file mode 100644 index 0000000..0c02d90 --- /dev/null +++ b/vendor/hare-logfmt/README.md @@ -0,0 +1,45 @@ +# hare-logfmt + +A logfmt logger that can be used in [`log::setlogger(*logger) void`](https://docs.harelang.org/log#setlogger) in Hare. + +## Usage + +```hare +use logfmt; +use log; + +export fn main() void = { + // create an instance of the logger + let l = logfmt::new(os::stderr); + + // set the global logger to the logfmt logger + log::setlogger(&l); + + // use the normal log::println function + log::println("request_uri", "/", "method", "POST", "user_id", 123); + log::println("request_uri", "/sign-in", "method", "GET"); + log::println("request_uri", "/dashboard", "method", "GET", "user_id", 123); +}; +``` + +**Output** + +```console +ts=2022-05-12T09:36:27-0400 request_uri=/ method=POST user_id=123 +ts=2022-05-12T09:42:27-0400 request_uri=/sign-in method=GET +ts=2022-05-12T09:52:10-0400 request_uri=/dashboard method=GET user_id=123 +``` + +You can also run `haredoc` to read the module documentation. + +```console +> haredoc +// Implements the log::logger for outputting logs in Logfmt format. +type logfmtlogger = struct { + log::logger, + handle: io::handle, +}; + +// creates a new instace of logfmtlogger to be use with [[log::setlogger]]. +fn new(handle: io::handle) logfmtlogger; +``` \ No newline at end of file diff --git a/vendor/hare-logfmt/log/logfmt/+test.ha b/vendor/hare-logfmt/log/logfmt/+test.ha new file mode 100644 index 0000000..be6d39f --- /dev/null +++ b/vendor/hare-logfmt/log/logfmt/+test.ha @@ -0,0 +1,48 @@ +use log; +use io; +use os; +use strings; +use fmt; + +@test fn logfmt() void = { + let s = teststream_open(); + + let l = new(&s); + + log::setlogger(&l); + log::println("request_uri", "/", "method", "POST", "user_id", 123); + + let sbuf = strings::fromutf8(s.buf)!; + + assert(strings::contains(sbuf, "request_uri=/ method=POST user_id=123")); + + free(sbuf); +}; + +const teststream_vtable: io::vtable = io::vtable { + reader = &teststream_read, + writer = &teststream_write, + ... +}; + +type teststream = struct { + stream: io::stream, + buf: []u8, +}; + +fn teststream_open() teststream = teststream { + stream = &teststream_vtable, + ... +}; + +fn teststream_read(s: *io::stream, buf: []u8) (size | io::EOF | io::error) = { + let stream = s: *teststream; + buf = stream.buf; + return len(buf); +}; + +fn teststream_write(s: *io::stream, buf: const []u8) (size | io::error) = { + let stream = s: *teststream; + append(stream.buf, buf...); + return len(buf); +}; \ No newline at end of file diff --git a/vendor/hare-logfmt/log/logfmt/logfmt.ha b/vendor/hare-logfmt/log/logfmt/logfmt.ha new file mode 100644 index 0000000..496f6de --- /dev/null +++ b/vendor/hare-logfmt/log/logfmt/logfmt.ha @@ -0,0 +1,64 @@ +use io; +use log; +use fmt; +use time::date; +use os; +use encoding::utf8; +use strings; + +// Implements the log::logger for outputting logs in Logfmt format. +export type logfmtlogger = struct { + log::logger, + handle: io::handle, +}; + +// creates a new instace of logfmtlogger to be use with [[log::setlogger]]. +export fn new(handle: io::handle) logfmtlogger = { + return logfmtlogger { + println = &log_println, + printfln = &log_printfln, + handle = handle, + }; +}; + +fn log_println(logger: *log::logger, fields: fmt::formattable...) void = { + const logger = logger: *logfmtlogger; + assert(logger.println == &log_println); + + const now = date::now(); + fmt::fprint(logger.handle, "ts="): void; + date::format(logger.handle, date::RFC3339, &now): void; + fmt::fprint(logger.handle, " "): void; + + for (let i = 0z; i < len(fields); i+= 1) { + if (i % 2 == 0) { + fmt::fprint(logger.handle, fields[i]): void; + fmt::fprint(logger.handle, "="): void; + } else { + fmt::fprint(logger.handle, fields[i]): void; + fmt::fprint(logger.handle, " "): void; + }; + }; + fmt::fprintln(logger.handle, ""): void; +}; + +fn log_printfln(logger: *log::logger, fmt: str, fields: fmt::field...) void = { + const logger = logger: *logfmtlogger; + assert(logger.printfln == &log_printfln); + + const now = date::now(); + fmt::fprint(logger.handle, "ts="): void; + date::format(logger.handle, date::RFC3339, &now): void; + fmt::fprint(logger.handle, " "): void; + + for (let i = 0z; i < len(fields); i+= 1) { + if (i % 2 == 0) { + fmt::fprintf(logger.handle, "{}", fields[i]): void; + fmt::fprint(logger.handle, "="): void; + } else { + fmt::fprintf(logger.handle, "{}", fields[i]): void; + fmt::fprint(logger.handle, " "): void; + }; + }; + fmt::fprintln(logger.handle, ""): void; +}; \ No newline at end of file diff --git a/backend/vendor/hare-thread/thread/thread.ha b/vendor/hare-thread/thread/thread.ha similarity index 100% rename from backend/vendor/hare-thread/thread/thread.ha rename to vendor/hare-thread/thread/thread.ha