diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml new file mode 100644 index 00000000..c933911e --- /dev/null +++ b/.github/workflows/docs.yaml @@ -0,0 +1,59 @@ +name: documentation + +permissions: + contents: read + +on: + workflow_dispatch: + push: + branches: + - main + schedule: + - cron: '0 18 * * *' + +jobs: + generate: + name: Generate API documentation + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Install rust toolchain + uses: dtolnay/rust-toolchain@nightly + + - name: Install zola + uses: taiki-e/install-action@v2 + with: + tool: zola@0.19.1 # Matched to rustls repo + + - name: Generate API JSON data + run: + cargo run -p rustls-ffi-tools --bin docgen > website/static/api.json + + - name: Generate site pages + run: | + cd website && zola build --output-dir ../target/website/ + + - name: Package and upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: ./target/website/ + + deploy: + name: Deploy + runs-on: ubuntu-latest + if: github.repository == 'rustls/rustls-ffi' + needs: generate + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index cdb92b1e..7bfcff02 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ librustls/cmake-build* .vs debian/usr debian/DEBIAN +/website/static/api.json +/website/public diff --git a/Cargo.lock b/Cargo.lock index c94346ae..59ce8433 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1241,7 +1241,12 @@ version = "0.1.0" dependencies = [ "hickory-resolver", "rustls", + "serde", + "serde_json", "tokio", + "tree-sitter", + "tree-sitter-c", + "tree-sitter-md", ] [[package]] @@ -1307,6 +1312,12 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + [[package]] name = "same-file" version = "1.0.6" @@ -1380,6 +1391,18 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.133" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + [[package]] name = "serde_spanned" version = "0.6.8" @@ -1646,6 +1669,45 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tree-sitter" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0203df02a3b6dd63575cc1d6e609edc2181c9a11867a271b25cfd2abff3ec5ca" +dependencies = [ + "cc", + "regex", + "regex-syntax", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-c" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afd2b1bf1585dc2ef6d69e87d01db8adb059006649dd5f96f31aa789ee6e9c71" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-language" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c199356c799a8945965bb5f2c55b2ad9d9aa7c4b4f6e587fe9dea0bc715e5f9c" + +[[package]] +name = "tree-sitter-md" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17f968c22a01010b83fc960455ae729db08dbeb6388617d9113897cb9204b030" +dependencies = [ + "cc", + "tree-sitter", + "tree-sitter-language", +] + [[package]] name = "triomphe" version = "0.1.11" diff --git a/Cargo.toml b/Cargo.toml index 6215128d..456634ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,6 @@ members = [ default-members = [ "librustls", - "tools" ] resolver = "2" @@ -23,3 +22,8 @@ regex = "1.9.6" toml = { version = "0.6.0", default-features = false, features = ["parse"] } hickory-resolver = { version = "=0.25.0-alpha.4", features = ["dns-over-https-rustls", "webpki-roots"] } tokio = { version = "1.42.0", features = ["io-util", "macros", "net", "rt"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tree-sitter = "0.23" # TODO(@cpu): handle breaking API changes for 0.24 +tree-sitter-c = "0.23" +tree-sitter-md = "0.3" diff --git a/librustls/cmake/rust.cmake b/librustls/cmake/rust.cmake index ca905ac0..2f2a08d2 100644 --- a/librustls/cmake/rust.cmake +++ b/librustls/cmake/rust.cmake @@ -91,7 +91,7 @@ endif() add_custom_command( TARGET ech-test POST_BUILD - COMMAND cargo run --manifest-path=../Cargo.toml --bin ech_fetch + COMMAND cargo run -p rustls-ffi-tools --bin ech_fetch COMMAND ${CMAKE_COMMAND} -E env RUSTLS_PLATFORM_VERIFIER=1 ${CMAKE_COMMAND} -E env ECH_CONFIG_LIST="research.cloudflare.com.ech.configs.bin" diff --git a/librustls/src/client.rs b/librustls/src/client.rs index 0dee7cd9..3de52ee8 100644 --- a/librustls/src/client.rs +++ b/librustls/src/client.rs @@ -570,6 +570,7 @@ pub struct rustls_verify_server_cert_params<'a> { #[allow(non_camel_case_types)] pub type rustls_verify_server_cert_user_data = *mut libc::c_void; +/// A callback that is invoked to verify a server certificate. // According to the nomicon https://doc.rust-lang.org/nomicon/ffi.html#the-nullable-pointer-optimization): // > Option c_int> is a correct way to represent a // > nullable function pointer using the C ABI (corresponding to the C type int (*)(int)). diff --git a/librustls/src/connection.rs b/librustls/src/connection.rs index ff627701..e161242c 100644 --- a/librustls/src/connection.rs +++ b/librustls/src/connection.rs @@ -93,6 +93,7 @@ impl std::ops::DerefMut for Connection { } box_castable! { + /// A C representation of a Rustls `Connection`. pub struct rustls_connection(Connection); } @@ -288,6 +289,7 @@ impl rustls_connection { } } + /// Returns a `rustls_handshake_kind` describing the `rustls_connection`. #[no_mangle] pub extern "C" fn rustls_connection_handshake_kind( conn: *const rustls_connection, diff --git a/librustls/src/error.rs b/librustls/src/error.rs index 3524cc0c..88ee75bf 100644 --- a/librustls/src/error.rs +++ b/librustls/src/error.rs @@ -46,6 +46,7 @@ macro_rules! u32_enum_builder { } u32_enum_builder! { + /// Numeric error codes returned from rustls-ffi API functions. EnumName: rustls_result; EnumDefault: InvalidParameter; EnumVal{ @@ -242,6 +243,7 @@ impl rustls_result { } } + /// Returns true if the `result` is a certificate related error. #[no_mangle] pub extern "C" fn rustls_result_is_cert_error(result: c_uint) -> bool { use rustls_result::*; diff --git a/librustls/src/log.rs b/librustls/src/log.rs index 27bfce7a..7968acde 100644 --- a/librustls/src/log.rs +++ b/librustls/src/log.rs @@ -42,6 +42,10 @@ pub(crate) fn ensure_log_registered() { log::set_max_level(log::LevelFilter::Debug) } +/// Numeric representation of a log level. +/// +/// Passed as a field of the `rustls_log_params` passed to a log callback. +/// Use with `rustls_log_level_str` to convert to a string label. pub type rustls_log_level = usize; /// Return a rustls_str containing the stringified version of a log level. @@ -58,12 +62,16 @@ pub extern "C" fn rustls_log_level_str(level: rustls_log_level) -> rustls_str<'s rustls_str::from_str_unchecked(s) } +/// Parameter structure passed to a `rustls_log_callback`. #[repr(C)] pub struct rustls_log_params<'a> { + /// The log level the message was logged at. pub level: rustls_log_level, + /// The message that was logged. pub message: rustls_str<'a>, } +/// A callback that is invoked for messages logged by rustls. #[allow(non_camel_case_types)] pub type rustls_log_callback = Option; diff --git a/librustls/src/rustls.h b/librustls/src/rustls.h index 41619f01..ae91726b 100644 --- a/librustls/src/rustls.h +++ b/librustls/src/rustls.h @@ -42,6 +42,9 @@ typedef enum rustls_handshake_kind { RUSTLS_HANDSHAKE_KIND_RESUMED = 3, } rustls_handshake_kind; +/** + * Numeric error codes returned from rustls-ffi API functions. + */ enum rustls_result { RUSTLS_RESULT_OK = 7000, RUSTLS_RESULT_IO = 7001, @@ -272,6 +275,9 @@ typedef struct rustls_client_config rustls_client_config; */ typedef struct rustls_client_config_builder rustls_client_config_builder; +/** + * A C representation of a Rustls `Connection`. + */ typedef struct rustls_connection rustls_connection; /** @@ -550,6 +556,9 @@ typedef struct rustls_verify_server_cert_params { struct rustls_slice_bytes ocsp_response; } rustls_verify_server_cert_params; +/** + * A callback that is invoked to verify a server certificate. + */ typedef uint32_t (*rustls_verify_server_cert_callback)(rustls_verify_server_cert_user_data userdata, const struct rustls_verify_server_cert_params *params); @@ -575,13 +584,31 @@ typedef void (*rustls_keylog_log_callback)(struct rustls_str label, */ typedef int (*rustls_keylog_will_log_callback)(struct rustls_str label); +/** + * Numeric representation of a log level. + * + * Passed as a field of the `rustls_log_params` passed to a log callback. + * Use with `rustls_log_level_str` to convert to a string label. + */ typedef size_t rustls_log_level; +/** + * Parameter structure passed to a `rustls_log_callback`. + */ typedef struct rustls_log_params { + /** + * The log level the message was logged at. + */ rustls_log_level level; + /** + * The message that was logged. + */ struct rustls_str message; } rustls_log_params; +/** + * A callback that is invoked for messages logged by rustls. + */ typedef void (*rustls_log_callback)(void *userdata, const struct rustls_log_params *params); /** @@ -1666,6 +1693,9 @@ bool rustls_connection_wants_write(const struct rustls_connection *conn); */ bool rustls_connection_is_handshaking(const struct rustls_connection *conn); +/** + * Returns a `rustls_handshake_kind` describing the `rustls_connection`. + */ enum rustls_handshake_kind rustls_connection_handshake_kind(const struct rustls_connection *conn); /** @@ -2128,6 +2158,9 @@ struct rustls_str rustls_handshake_kind_str(enum rustls_handshake_kind kind); */ void rustls_error(unsigned int result, char *buf, size_t len, size_t *out_n); +/** + * Returns true if the `result` is a certificate related error. + */ bool rustls_result_is_cert_error(unsigned int result); /** diff --git a/tools/Cargo.toml b/tools/Cargo.toml index 4f49f6ae..91c30572 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -8,7 +8,8 @@ publish = false rustls = { workspace = true } hickory-resolver = { workspace = true } tokio = { workspace = true } - -[[bin]] -name = "ech_fetch" -path = "src/ech_fetch.rs" +serde = { workspace = true } +serde_json = { workspace = true } +tree-sitter = { workspace = true } +tree-sitter-c = { workspace = true } +tree-sitter-md = { workspace = true, features = ["parser"] } diff --git a/tools/src/bin/docgen/main.rs b/tools/src/bin/docgen/main.rs new file mode 100644 index 00000000..269229f3 --- /dev/null +++ b/tools/src/bin/docgen/main.rs @@ -0,0 +1,819 @@ +use std::collections::HashSet; +use std::error::Error; +use std::fmt::{Display, Formatter}; +use std::fs; + +use serde::Serialize; +use tree_sitter::{Node, Parser, Query, QueryCursor}; +use tree_sitter_md::MarkdownParser; + +fn main() -> Result<(), Box> { + // Create a C parser. + let mut parser = Parser::new(); + let language = tree_sitter_c::LANGUAGE; + parser.set_language(&language.into())?; + + // Parse the .h into an AST. + let header_file = fs::read_to_string("librustls/src/rustls.h")?; + //let header_file = fs::read_to_string("test.h")?; + let header_file_bytes = header_file.as_bytes(); + let tree = parser + .parse(&header_file, None) + .ok_or("no tree parsed from input")?; + + // Make sure we have the root node we expect. + let root = tree.root_node(); + require_kind("translation_unit", root, header_file_bytes)?; + + // Collect up all items we want to document. Returns an error if any + // items we expect to be documented are missing associated block comments. + let mut docs = find_doc_items(root, header_file_bytes)?; + + // Cross-link items in comments. + docs.crosslink_comments()?; + + // Render JSON data. + println!("{}", serde_json::to_string_pretty(&docs)?); + + Ok(()) +} + +#[derive(Debug, Default, Serialize)] +struct ApiDocs { + structs: Vec, + functions: Vec, + callbacks: Vec, + enums: Vec, + externs: Vec, + aliases: Vec, +} + +impl ApiDocs { + fn crosslink_comments(&mut self) -> Result<(), Box> { + // Put all anchors into a set. Error for any duplicates. + let mut anchor_set = HashSet::new(); + for a in self.all_anchors() { + if !anchor_set.insert(a.to_string()) { + return Err(format!("duplicate anchor: {a}").into()); + } + } + + // For each item of each type, crosslink its comment. + for s in &mut self.structs { + s.comment.crosslink(&anchor_set)?; + } + for f in &mut self.functions { + f.comment.crosslink(&anchor_set)?; + } + for cb in &mut self.callbacks { + cb.comment.crosslink(&anchor_set)?; + } + for e in &mut self.enums { + e.comment.crosslink(&anchor_set)?; + for v in &mut e.variants { + if let Some(comment) = &mut v.comment { + comment.crosslink(&anchor_set)?; + } + } + } + for e in &mut self.externs { + e.comment.crosslink(&anchor_set)?; + } + for a in &mut self.aliases { + a.comment.crosslink(&anchor_set)?; + } + + Ok(()) + } + + fn all_anchors(&self) -> impl Iterator { + // return all item anchors as a chained iterator + self.structs + .iter() + .map(|s| s.anchor.as_str()) + .chain(self.functions.iter().map(|f| f.anchor.as_str())) + .chain(self.callbacks.iter().map(|cb| cb.anchor.as_str())) + .chain(self.enums.iter().map(|e| e.anchor.as_str())) + .chain(self.externs.iter().map(|e| e.anchor.as_str())) + .chain(self.aliases.iter().map(|a| a.anchor.as_str())) + } +} + +fn find_doc_items(root: Node, source_code: &[u8]) -> Result> { + // We document all: + // * type definitions + // * enum specifiers with an enumerator list (enums outside an inline typedef) + // * declarations (incls. externs) + // + // For bare enums we have to make sure we don't match on just (enum_specifier) because + // this will match the node in a function decl returning an enum. We want an enum + // specifier with an enumerator list. + let query = r#" + ( + [ + (type_definition) + (enum_specifier (type_identifier) (enumerator_list)) + (declaration) + ] + @doc_item + ) + "#; + let language = tree_sitter_c::LANGUAGE; + let query = Query::new(&language.into(), query)?; + + let mut cursor = QueryCursor::new(); + let matches = cursor.matches(&query, root, source_code); + + let mut items = Vec::default(); + let mut errors = 0; + for query_match in matches { + for doc_item_node in query_match.nodes_for_capture_index(0) { + match process_doc_item(doc_item_node, source_code) { + Ok(Some(item)) => items.push(item), + Err(err) => { + eprintln!("{err}"); + errors += 1; + } + _ => {} + } + } + } + if errors > 0 { + return Err(format!("{} errors produced while documenting header file", errors).into()); + } + + let mut api = ApiDocs::default(); + for item in items { + match item { + Item::Enum(e) => api.enums.push(e), + Item::Struct(s) => api.structs.push(s), + Item::TypeAlias(a) => api.aliases.push(a), + Item::Callback(cb) => api.callbacks.push(cb), + Item::Function(f) => api.functions.push(f), + Item::Extern(e) => api.externs.push(e), + } + } + + Ok(api) +} + +fn process_doc_item(item: Node, src: &[u8]) -> Result, Box> { + // Get the item's previous sibling in the tree. + let Some(prev) = item.prev_sibling() else { + return Err(node_error("to-be-documented item without previous item", item, src).into()); + }; + + // If we're looking at an enum node, but it's after a typedef, skip. + // We'll document this enum when we process the typedef. + if item.kind() == "enum_specifier" && prev.kind() == "typedef" { + return Ok(None); + } + + // Try to turn the previous sibling into a comment node. Some items may + // require this to be Some(_) while others may allow None. + let (comment, feat_requirement) = comment_and_requirement(prev, src)?; + + let kind = item.kind(); + // Based on the node kind, convert it to an appropriate Item. + Ok(Some(match kind { + "type_definition" => process_typedef_item(comment, feat_requirement, item, src)?, + "enum_specifier" => Item::from(EnumItem::new(comment, feat_requirement, item, src)?), + "declaration" => process_declaration_item(comment, feat_requirement, item, src)?, + _ => return Err(format!("unexpected item kind: {}", item.kind()).into()), + })) +} + +fn comment_and_requirement( + node: Node, + src: &[u8], +) -> Result<(Option, Option), Box> { + let maybe_comment = Comment::new(node, src).ok(); + + // If prev wasn't a comment, see if it was a feature requirement. + if maybe_comment.is_none() { + return Ok(match Feature::new(node, src).ok() { + Some(feat_req) => (None, Some(feat_req)), + None => (None, None), + }); + } + + // Otherwise, check the prev of the comment for a feature requirement + let Some(prev) = node.prev_named_sibling() else { + return Ok((maybe_comment, None)); + }; + + Ok((maybe_comment, Feature::new(prev, src).ok())) +} + +#[derive(Debug, Default, Serialize)] +struct Feature(String); + +impl Feature { + fn new(node: Node, src: &[u8]) -> Result> { + // Verify we're looking at a preproc_defined node preceded by a preproc_if node. + require_kind("preproc_defined", node, src)?; + if let Some(prev) = node.prev_sibling() { + require_kind("#if", prev, src)?; + } else { + return Err(node_error("feature requirement without previous item", node, src).into()); + } + + let Some(required_define) = node.named_child(0).map(|n| node_text(n, src)) else { + return Err(node_error("feature requirement without identifier", node, src).into()); + }; + + // We assume features have cbindgen defines like "DEFINE_$FEATURE_NAME" and we want to + // extract "$feature_name". + Ok(Self( + required_define + .strip_prefix("DEFINE_") + .unwrap_or(&required_define) + .to_ascii_lowercase(), + )) + } +} + +impl Display for Feature { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0) + } +} + +/// A comment from the header file. +/// +/// The comment text is cleaned up to remove leading/trailing C block comment syntax. +/// The remaining text is unaltered with respect to indentation and newlines within the +/// content. +#[derive(Debug, Default, Serialize)] +struct Comment(String); + +impl Comment { + fn new(node: Node, src: &[u8]) -> Result> { + require_kind("comment", node, src)?; + + // Convert the node to UTF8 text and then strip the block comment syntax and any + // leading/trailing newlines. + let text = node + .utf8_text(src) + .unwrap_or_default() + .lines() + .map(|line| { + line.trim() + .trim_start_matches("/**") + .trim_end_matches("*/") + .trim_start_matches('*') + }) + .collect::>() + .join("\n") + .trim() + .to_string(); + + Ok(Self(text)) + } + + fn crosslink(&mut self, anchors: &HashSet) -> Result<(), Box> { + let Some(parser) = MarkdownParser::default().parse(self.0.as_bytes(), None) else { + return Ok(()); + }; + + // Find all the "code_span" items from each inline tree, potentially replacing some content. + let mut replacements = Vec::new(); + for t in parser.inline_trees() { + let mut cursor = t.walk(); + if !cursor.goto_first_child() { + break; + } + loop { + let node = cursor.node(); + if node.kind() != "code_span" { + if !cursor.goto_next_sibling() { + break; + } + continue; + } + let start = node.start_byte(); + let end = node.end_byte(); + let content = &self.0[start..end].trim_matches('`'); + let anchor = content.trim_end_matches("()").replace('_', "-"); + + // If we found an anchor in backticks, make it a link to that anchor. + if anchors.contains(&anchor) { + replacements.push((start, end, format!("[`{content}`](#{anchor})"))); + } + + if !cursor.goto_next_sibling() { + break; + } + } + } + + // Apply replacements to comment in reverse to maintain correct byte offsets. + for (start, end, replacement) in replacements.into_iter().rev() { + self.0.replace_range(start..end, &replacement); + } + + Ok(()) + } +} + +impl Display for Comment { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0) + } +} + +fn process_typedef_item( + maybe_comment: Option, + maybe_feature: Option, + item: Node, + src: &[u8], +) -> Result> { + require_kind("type_definition", item, src)?; + + let typedef_node = item.child_by_field_name("type").unwrap(); + let typedef_kind = typedef_node.kind(); + + let comment = match (&maybe_comment, item.prev_named_sibling()) { + // We allow an uncommented type_definition if the previous node was a bare enum_specifier. + // This happens when an enum has a primitive type repr, like rustls_result. The enum + // appears without typedef (but with comment), and then a typedef uint32_t appears (without + // preceding comment). This is OK and doesn't count as an undocumented error. + // + // It's important we use prev_named_sibling() for finding the enum_specifier that precedes + // the typedef. Using prev_sibling() would return an anonymous ';' node. + (None, Some(sib)) if sib.kind() == "enum_specifier" => Comment::default(), + _ => require_documented(maybe_comment, item, src)?, + }; + + // Convert the particular item being typedef'd based on kind(). + // We treat function typedefs differently - we want those to be considered callbacks. + let func_declarator = item + .child_by_field_name("declarator") + .map(|n| { + n.kind() == "function_declarator" + || (n.kind() == "pointer_declarator" + && n.child_by_field_name("declarator") + .map(|n| n.kind() == "function_declarator") + .unwrap_or_default()) + }) + .unwrap_or_default(); + Ok(match typedef_kind { + // e.g. `typedef enum rustls_handshake_kind { ... } rustls_handshake_kind;` + "enum_specifier" => Item::from(EnumItem::new( + Some(comment), + maybe_feature, + typedef_node, + src, + )?), + + // e.g. `typedef uint32_t (*rustls_verify_server_cert_callback)(...);` + "primitive_type" if func_declarator => { + Item::from(CallbackItem::new(comment, maybe_feature, item, src)?) + } + + // e.g. `typedef rustls_io_result (*rustls_read_callback)(...);` + "type_identifier" if func_declarator => { + Item::from(CallbackItem::new(comment, maybe_feature, item, src)?) + } + + // e.g. `typedef const struct rustls_certified_key *(*rustls_client_hello_callback)(...);` + "struct_specifier" if func_declarator => { + Item::from(CallbackItem::new(comment, maybe_feature, item, src)?) + } + + // e.g. `typedef struct rustls_accepted rustls_accepted;` + "struct_specifier" => { + Item::from(StructItem::new(comment, maybe_feature, typedef_node, src)?) + } + + // e.g. `typedef int rustls_io_result;` + "primitive_type" if !func_declarator => { + Item::from(TypeAliasItem::new(comment, maybe_feature, item, src)) + } + + // e.g. ... well, none so far - but something like `typedef rustls_io_result rustls_funtime_io_result;`. + "type_identifier" if !func_declarator => { + Item::from(TypeAliasItem::new(comment, maybe_feature, item, src)) + } + + _ => return Err(format!("unknown typedef kind: {typedef_kind:?}").into()), + }) +} + +fn process_declaration_item( + comment: Option, + maybe_feature: Option, + item: Node, + src: &[u8], +) -> Result> { + require_kind("declaration", item, src)?; + + let comment = require_documented(comment, item, src)?; + if item.child(0).unwrap().kind() == "storage_class_specifier" { + // extern is a storage_class_specifier. + Ok(Item::from(ExternItem::new( + comment, + maybe_feature, + item, + src, + )?)) + } else { + // other non-extern declarations are functions. + Ok(Item::from(FunctionItem::new( + comment, + maybe_feature, + item, + src, + )?)) + } +} + +/// An item to be documented from a C header file. +#[derive(Debug)] +enum Item { + Enum(EnumItem), + Struct(StructItem), + TypeAlias(TypeAliasItem), + Callback(CallbackItem), + Function(FunctionItem), + Extern(ExternItem), +} + +impl From for Item { + fn from(item: EnumItem) -> Self { + Self::Enum(item) + } +} + +impl From for Item { + fn from(item: StructItem) -> Self { + Self::Struct(item) + } +} + +impl From for Item { + fn from(item: TypeAliasItem) -> Self { + Self::TypeAlias(item) + } +} + +impl From for Item { + fn from(item: CallbackItem) -> Self { + Self::Callback(item) + } +} + +impl From for Item { + fn from(item: FunctionItem) -> Self { + Self::Function(item) + } +} + +impl From for Item { + fn from(item: ExternItem) -> Self { + Self::Extern(item) + } +} + +/// An enum declaration. +/// +/// E.g. `typedef enum rustls_handshake_kind { ... variants ... };` +#[derive(Debug, Serialize)] +struct EnumItem { + anchor: String, + comment: Comment, + feature: Option, + name: String, + variants: Vec, +} + +impl EnumItem { + fn new( + comment: Option, + feature: Option, + enum_spec: Node, + src: &[u8], + ) -> Result> { + let comment = require_documented(comment, enum_spec, src)?; + + let name = enum_spec + .child_by_field_name("name") + .map(|n| node_text(n, src)) + .unwrap(); + + // Get the enumerator_list and walk its children, converting each variant to an + // EnumVariantItem. + let enumeraor_list = enum_spec.child_by_field_name("body").unwrap(); + let mut cursor = enumeraor_list.walk(); + let variants = enumeraor_list + .children(&mut cursor) + .filter_map(|n| EnumVariantItem::new(n, src).ok()) + .collect(); + + Ok(Self { + anchor: name.replace('_', "-").to_ascii_lowercase(), + comment, + feature, + name, + variants, + }) + } +} + +/// A variant of an Enum. +/// +/// E.g. RUSTLS_RESULT_ALERT_UNKNOWN = 7234 +#[derive(Debug, Default, Serialize)] +struct EnumVariantItem { + anchor: String, + comment: Option, // We don't require all enum variants have comments. + name: String, + value: String, +} + +impl EnumVariantItem { + fn new(variant_node: Node, src: &[u8]) -> Result> { + require_kind("enumerator", variant_node, src)?; + + let name = node_text(variant_node.child_by_field_name("name").unwrap(), src); + Ok(Self { + anchor: name.replace('_', "-").to_ascii_lowercase(), + comment: variant_node + .prev_sibling() + .and_then(|n| Comment::new(n, src).ok()), + name, + value: node_text(variant_node.child_by_field_name("value").unwrap(), src), + }) + } +} + +/// A structure typedef. +/// +/// May have fields (not presently parsed) or no fields (e.g. an opaque struct). +/// +/// E.g. `typedef struct rustls_client_config_builder rustls_client_config_builder;` +#[derive(Debug, Serialize)] +struct StructItem { + anchor: String, + comment: Comment, + feature: Option, + name: String, + text: String, +} + +impl StructItem { + fn new( + comment: Comment, + feature: Option, + struct_node: Node, + src: &[u8], + ) -> Result> { + require_kind("struct_specifier", struct_node, src)?; + + let name = node_text(struct_node.child_by_field_name("name").unwrap(), src); + Ok(Self { + anchor: name.replace('_', "-").to_ascii_lowercase(), + comment, + feature, + name, + text: markup_text(struct_node, src), + }) + } +} + +/// A simple typedef type alias. +/// +/// E.g. `typedef int rustls_io_result;` +#[derive(Debug, Serialize)] +struct TypeAliasItem { + anchor: String, + comment: Comment, + feature: Option, + name: String, + text: String, +} + +impl TypeAliasItem { + fn new(comment: Comment, feature: Option, item: Node, src: &[u8]) -> Self { + let language = tree_sitter_c::LANGUAGE; + let query = Query::new(&language.into(), "(type_identifier) @name").unwrap(); + let mut cursor = QueryCursor::new(); + let name = cursor + .matches(&query, item, src) + .next() + .map(|m| node_text(m.captures[0].node, src)) + .unwrap(); + + Self { + // Note: we add a 'alias-' prefix for simple type aliases anchors. + // We do this because otherwise we end up with two 'rustls-result' + // anchors. One for the bare enum, and one for the typedef'd type. + anchor: format!("alias-{}", name.replace("_", "-").to_ascii_lowercase()), + name, + comment, + feature, + text: markup_text(item, src), + } + } +} + +/// A function pointer typedef for a callback function. +/// +/// E.g. `typedef rustls_io_result (*rustls_read_callback)(void *userdata, ...);` +#[derive(Debug, Serialize)] +struct CallbackItem { + anchor: String, + comment: Comment, + feature: Option, + name: String, + text: String, +} + +impl CallbackItem { + fn new( + comment: Comment, + feature: Option, + typedef: Node, + src: &[u8], + ) -> Result> { + require_kind("type_definition", typedef, src)?; + + let name = function_identifier(typedef, src); + Ok(Self { + anchor: name.replace("_'", "-").to_ascii_lowercase(), + comment, + feature, + name, + text: markup_text(typedef, src), + }) + } +} + +/// A function prototype definition. +/// +/// E.g. `void rustls_acceptor_free(struct rustls_acceptor *acceptor);` +#[derive(Debug, Serialize)] +struct FunctionItem { + anchor: String, + comment: Comment, + feature: Option, + name: String, + text: String, +} + +impl FunctionItem { + fn new( + comment: Comment, + feature: Option, + decl_node: Node, + src: &[u8], + ) -> Result> { + require_kind("declaration", decl_node, src)?; + + let name = function_identifier(decl_node, src); + Ok(Self { + anchor: name.replace('_', "-").to_ascii_lowercase(), + comment, + feature, + name, + text: markup_text(decl_node, src), + }) + } +} + +/// An extern constant declaration. +/// +/// E.g. `extern const uint16_t RUSTLS_ALL_VERSIONS[2];` +#[derive(Debug, Serialize)] +struct ExternItem { + anchor: String, + comment: Comment, + feature: Option, + name: String, + text: String, +} + +impl ExternItem { + fn new( + comment: Comment, + feature: Option, + decl_node: Node, + src: &[u8], + ) -> Result> { + require_kind("declaration", decl_node, src)?; + + // Query for the first identifier kind child node. + let language = tree_sitter_c::LANGUAGE; + let query = Query::new(&language.into(), "(identifier) @name").unwrap(); + let mut cursor = QueryCursor::new(); + let name = cursor + .matches(&query, decl_node, src) + .next() + .map(|m| node_text(m.captures[0].node, src)) + .unwrap(); + + Ok(Self { + anchor: name.replace('_', "-").to_ascii_lowercase(), + comment, + feature, + name, + text: markup_text(decl_node, src), + }) + } +} + +/// Return a function's name. +/// +/// Queries for function names from simple function declarators like +/// `rustls_io_result rustls_acceptor_read_tls(...)` +/// or, pointers to functions that have a parenthesized name like +/// `typedef rustls_io_result (*rustls_read_callback)(...)` +/// +/// Panics if there is no match. +fn function_identifier(node: Node, src: &[u8]) -> String { + let language = tree_sitter_c::LANGUAGE; + let query = Query::new( + &language.into(), + r#" + [ + (function_declarator + declarator: (identifier) @name + ) + (function_declarator + declarator: (parenthesized_declarator + (pointer_declarator + (type_identifier) @name + ) + ) + ) + ]"#, + ) + .unwrap(); + let mut cursor = QueryCursor::new(); + let res = cursor + .matches(&query, node, src) + .next() + .map(|m| node_text(m.captures[0].node, src)); + + if res.is_none() { + dbg!(&node.start_position().row); + dbg!(&node); + } + res.unwrap() +} + +/// Require that a `Node` correspond to a specific kind of grammar rule. +/// +/// Returns an error describing the node's position and the expected vs actual +/// kind if there is a mismatch. +/// +/// Once the kind is verified we can lean on the grammar to unwrap() elements +/// we know must exist. +fn require_kind(kind: &str, node: Node, src: &[u8]) -> Result<(), Box> { + let found_kind = node.kind(); + match found_kind == kind { + true => Ok(()), + false => Err(node_error(format!("expected {kind}, found {found_kind}"), node, src).into()), + } +} + +/// Unwrap a required CommentNode or return an error. +/// +/// The error will describe the kind of node that was missing a documentation comment, as well +/// as its location (line/col) in the source code. +fn require_documented( + comment: Option, + item: Node, + src: &[u8], +) -> Result> { + comment.ok_or( + node_error( + format!("undocumented {kind}", kind = item.kind()), + item, + src, + ) + .into(), + ) +} + +fn node_error(prefix: impl Display, n: Node, src: &[u8]) -> String { + format!( + "{prefix} on L{line}:{col}: item: {:?}", + node_text(n, src), + line = n.start_position().row + 1, + col = n.start_position().column, + ) +} + +/// Convert the node to its textual representation in the source code. +/// +/// Returns an empty string if the content isn't valid UTF-8. +fn node_text(node: Node, src: &[u8]) -> String { + node.utf8_text(src).unwrap_or_default().to_string() +} + +/// Convert the node to its textual representation, then decorate it as C +/// markdown code block. +fn markup_text(node: Node, src: &[u8]) -> String { + format!("```c\n{}\n```", node_text(node, src)) +} diff --git a/tools/src/ech_fetch.rs b/tools/src/bin/ech_fetch/main.rs similarity index 100% rename from tools/src/ech_fetch.rs rename to tools/src/bin/ech_fetch/main.rs diff --git a/website/README.md b/website/README.md new file mode 100644 index 00000000..7e944229 --- /dev/null +++ b/website/README.md @@ -0,0 +1,5 @@ +Source for the website at `https://ffi.rustls.dev/` + +This uses [Zola](https://www.getzola.org/). + +Run `zola serve` in this directory to run a local copy. diff --git a/website/config.toml b/website/config.toml new file mode 100644 index 00000000..6498c85b --- /dev/null +++ b/website/config.toml @@ -0,0 +1,18 @@ +# The URL the site will be built for +base_url = "https://ffi.rustls.dev/" + +# Whether to automatically compile all Sass files in the sass directory +compile_sass = false + +# Whether to build a search index to be used later on by a JavaScript library +build_search_index = false + +[markdown] +# Whether to do syntax highlighting +# Theme can be customised by setting the `highlight_theme` variable to a theme supported by Zola +highlight_code = true + +[slugify] +paths_keep_dates = true + +[extra] diff --git a/website/content/_index.md b/website/content/_index.md new file mode 100644 index 00000000..20c3eed3 --- /dev/null +++ b/website/content/_index.md @@ -0,0 +1,4 @@ ++++ +title = "rustls-ffi API docs" +template = "index.html" ++++ diff --git a/website/static/GeneralSans-Variable.woff2 b/website/static/GeneralSans-Variable.woff2 new file mode 100644 index 00000000..55e906ba Binary files /dev/null and b/website/static/GeneralSans-Variable.woff2 differ diff --git a/website/static/rustls-ferris.png b/website/static/rustls-ferris.png new file mode 100644 index 00000000..be48c92d Binary files /dev/null and b/website/static/rustls-ferris.png differ diff --git a/website/static/style.css b/website/static/style.css new file mode 100644 index 00000000..41a3df06 --- /dev/null +++ b/website/static/style.css @@ -0,0 +1,176 @@ +/* Base styles */ +body { + background-image: linear-gradient(to right top, #2f4858, #2e4e6c, #3a517f, #54528e, #744e95); + font-family: sans-serif; + margin: 0; +} + +/* logo image */ +img#baby-logo { + filter: drop-shadow(0px 0.1em 0.2em rgba(0, 0, 0, 0.2)); + max-height: 10%; + max-width: 20%; + display: block; + margin-left: 1em; + margin-top: 1em; +} + +/* Text colors */ +* { color: white; } + +h1, h2, h3 { color: #c5d8ff; } + +.toc a { color: #c5d8ff; } + +/* Layout */ +.container { + box-sizing: border-box; + margin: 0 auto; + max-width: 1200px; + padding: 0 1rem; + width: 95%; +} + +/* Typography and Links */ +h2 .header-link, +h3 .header-link { + color: inherit; + display: block; + text-decoration: none; +} + +.back-to-top a { + background: rgba(255, 255, 255, 0.1); + border-radius: 1em; + color: rgba(255, 255, 255, 0.7); + font-size: 0.85rem; + padding: 0.4em 0.8em; + text-decoration: none; + transition: all 0.2s ease; +} + +.back-to-top a:hover { + background: rgba(255, 255, 255, 0.15); + color: rgba(255, 255, 255, 0.9); +} + +/* Code and Pre blocks */ +p > code { + background: rgba(0, 0, 0, 0.1); + border-radius: 1em; + padding: 0.2em; +} + +pre { + background: rgba(0, 0, 0, 0.1); + border-radius: 1em; + max-width: 100%; + padding: 1em; + white-space: pre-wrap; + margin: 1em; +} + +.variant pre { + margin: 0.5em; + background-color: #2b303b; + color: #b48ead; +} + +code { + word-break: break-all; + word-wrap: break-word; +} + +/* Components */ +.toc { + background-image: linear-gradient(to right top, #2f4858, #2e4e6c, #3a517f, #54528e, #744e95); + border-radius: 1rem; + box-sizing: border-box; + filter: drop-shadow(0px 0px 1em rgba(0, 0, 0, 0.25)); + margin: 1.5rem 0; + max-width: 1200px; + overflow-x: auto; + padding: 1.5rem; +} + +.item { + background: rgba(255, 255, 255, 0.07); + backdrop-filter: blur(2px); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 1rem; + margin: 1.5rem 0; + padding: 1.5rem; + transition: background 0.2s ease; +} + +section { + background-image: linear-gradient(to right top, #2f4858, #2e4e6c, #3a517f, #54528e, #744e95); + border-radius: 1rem; + filter: drop-shadow(0px 0px 1em rgba(0, 0, 0, 0.25)); + margin: 2em 0; + max-width: 100%; + padding: 2em; +} + +/* Anchors */ +h2, h3, .variant { + position: relative; +} + +.variant a { + text-decoration: none; +} + +h2 .anchor, +h3 .anchor, +.variant .anchor { + color: #ac66e6; + left: -1em; + opacity: 0; + position: absolute; + text-decoration: none; + transition: opacity 0.2s ease-in-out; +} + +h2:hover .anchor, +h3:hover .anchor, +.variant:hover .anchor { + opacity: 1; +} + +/* Media Queries */ +@media (max-width: 768px) { + .container { + padding: 0 0.5rem; + width: 100%; + } + .variant { + margin-left: 1rem; + } +} + +.feature-box { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem 1rem; + margin: 1rem 0; + background: #744e95; + border-left: 5px solid #3b82f6; + border-right: 1px solid #3b82f6; + border-top: 1px solid #3b82f6; + border-bottom: 1px solid #3b82f6; + border-radius: 0.5em; +} + +a:has(code) { + text-decoration: none; +} +a:has(code):hover { + text-decoration: #ac66e6 overline underline; + text-decoration-thickness: 2px; +} + +a code { + color: #c5d8ff; +} diff --git a/website/templates/_api_section.html b/website/templates/_api_section.html new file mode 100644 index 00000000..153ee945 --- /dev/null +++ b/website/templates/_api_section.html @@ -0,0 +1,43 @@ +{% macro render_section(section_id, title, items) %} +
+ {{ macros::section_header(id=section_id, title=title) }} + {{ macros::toc_list(items=items) }} + + {% for item in items %} +
+ {{ macros::item_header(anchor=item.anchor, name=item.name) }} + + {% if item.feature %} +
+ + + + + + Requires feature: {{ item.feature }} +
+ {% endif %} + + {{ item.comment | markdown | safe }} + + {% if section_id == "enums" %} + {% for variant in item.variants %} +
+ {% if variant.comment %} + {{ variant.comment | markdown | safe }} + {% endif %} + # + +
{{ variant.name }} = {{ variant.value }}
+
+
+ {% endfor %} + {% else %} + {{ item.text | markdown | safe }} + {% endif %} + + {{ macros::back_to_top() }} +
+ {% endfor %} +
+{% endmacro render_section %} diff --git a/website/templates/base.html b/website/templates/base.html new file mode 100644 index 00000000..ba04bd01 --- /dev/null +++ b/website/templates/base.html @@ -0,0 +1,15 @@ + + + rustls: {{ page.title | default (value="C FFI bindings for Rustls") }} + + + + + + + + + {% block lede %} {% endblock %} + {% block content %} {% endblock %} + + diff --git a/website/templates/index.html b/website/templates/index.html new file mode 100644 index 00000000..7fed53e9 --- /dev/null +++ b/website/templates/index.html @@ -0,0 +1,34 @@ +{% extends "base.html" %} +{% import "macros.html" as macros %} +{% import "_api_section.html" as api_section %} + +{% block content %} +{% set data = load_data(path="static/api.json") %} +{% set section_ids = ["structs", "functions", "callbacks", "enums", "externs", "aliases"] %} +{% set section_titles = ["Structs", "Functions", "Callbacks", "Enums", "Externs", "Type Aliases"] %} + +
+

rustls-ffi API Documentation

+ +
+

Table of Contents

+ +
+ + {% for id in section_ids %} + {% if data[id] %} + {{ api_section::render_section( + section_id=id, + title=section_titles[loop.index0], + items=data[id]) + }} + {% endif %} + {% endfor %} +
+{% endblock %} diff --git a/website/templates/macros.html b/website/templates/macros.html new file mode 100644 index 00000000..ef50f1c4 --- /dev/null +++ b/website/templates/macros.html @@ -0,0 +1,27 @@ +{% macro section_header(id, title) %} +

+ # + {{ title }} +

+{% endmacro section_header %} + +{% macro item_header(anchor, name) %} +

+ # + {{ name }} +

+{% endmacro item_header %} + +{% macro toc_list(items) %} +
+ +
+{% endmacro toc_list %} + +{% macro back_to_top() %} +

Back to top ↑

+{% endmacro back_to_top %}