parse_net/lib.rs
1//! # parse_net
2//!
3//! The grammar layer for Internet message fields: RFC 9110 field syntax,
4//! media types, RFC 9651 Structured Field Values, HTTP and RFC 5322 dates,
5//! content negotiation, entity tags, ranges, `Content-Disposition` with
6//! RFC 8187, `Cache-Control`, `Link`, `Forwarded`, authentication challenges,
7//! `Host`, `Origin`, and the email lexical layer, with safe writers for each.
8//!
9//! Protocol engines (http-net, ws-net, h2-net) frame messages; parse-net reads
10//! what is inside a field value and writes field values back out without ever
11//! emitting CR, LF, or NUL. It is `no_std`, needs no allocator for any parse,
12//! does no I/O, and has no dependencies.
13//!
14//! ## Tier 1
15//!
16//! The convention in every example is `use parse_net as pn;`. One plain call
17//! per field:
18//!
19//! ```rust
20//! use parse_net as pn;
21//!
22//! // Media types: one strict view, no copy.
23//! let mt = pn::parse_media_type(b"text/html; charset=UTF-8")?;
24//! assert!(mt.essence_is("text/html"));
25//! assert!(mt.charset().is_some_and(|c| c.eq_ignore_ascii_case(b"utf-8")));
26//!
27//! // Lists: commas inside quoted strings and comments do not split.
28//! let codings: Vec<&[u8]> = pn::list(b"gzip, deflate;q=0.5, x=\"a,b\"")?.iter().collect();
29//! assert_eq!(codings, [&b"gzip"[..], b"deflate;q=0.5", b"x=\"a,b\""]);
30//!
31//! // HTTP-date: all three formats in, IMF-fixdate out.
32//! let now = 1_792_540_800; // the caller's clock, Unix seconds
33//! let d = pn::http_date_at(b"Sunday, 06-Nov-94 08:49:37 GMT", now)?;
34//! assert_eq!(&d.format(), b"Sun, 06 Nov 1994 08:49:37 GMT");
35//!
36//! // Host: real IPv4 and IPv6 validation, ports up to 65535.
37//! let hp = pn::parse_host_port(b"[2001:db8::1]:8443")?;
38//! assert_eq!(hp.port, Some(8443));
39//! assert!(pn::parse_host_port(b"127.1").is_err()); // read as IPv4 by browsers: refused
40//!
41//! // Writers cannot emit CR, LF, or NUL.
42//! let mut buf = [0u8; 64];
43//! let mut out = pn::SliceSink::new(&mut buf);
44//! pn::media::write("text", "plain", &[("charset", "utf-8")], &mut out)?;
45//! assert_eq!(out.written(), b"text/plain; charset=utf-8");
46//! assert!(pn::write::write_field_value(b"x\r\nSet-Cookie: y", &mut out).is_err());
47//! # Ok::<(), Box<dyn std::error::Error>>(())
48//! ```
49//!
50//! Errors carry a kind and an offset, never input bytes:
51//!
52//! ```rust
53//! use parse_net as pn;
54//!
55//! let err = pn::parse_media_type(b"text/ html").unwrap_err();
56//! assert_eq!(err.kind(), pn::ErrorKind::InvalidMediaType);
57//! assert_eq!(err.to_string(), "invalid media type at offset 5");
58//! ```
59//!
60//! The rest of Tier 1 arrives with `0.4.0` and `0.5.0` (see the
61//! [roadmap](https://github.com/jamesgober/parse-net/blob/main/dev/ROADMAP.md)):
62//!
63//! ```text
64//! pn::best_media(accept, &["text/html", "application/json"]) // Option<usize>
65//! pn::if_none_match(if_none_match, ¤t_etag)? // bool: answer 304
66//! pn::byte_ranges(range, file_len, &mut buf)? // RangeOutcome
67//! pn::parse_content_disposition(value)?.filename() // prefers filename*
68//! pn::response_cache_control(value)? // ResponseDirectives
69//! pn::client_address(peer, pn::ForwardedSource::XForwardedFor(&lines), &trusted, 16)?
70//! pn::sfv::parse_item(b"same-origin")? // Sec-Fetch-Site
71//! pn::bearer(authorization)? // Option<&[u8]>
72//! ```
73//!
74//! ## Status
75//!
76//! Beta, `0.9.0`: every planned field, measured against the Rust crates that
77//! do the same work (`docs/PERFORMANCE.md`), reviewed and hardened
78//! (`docs/SECURITY.md`), and in use by published consumers (http-net,
79//! cookie-net, form-net). The public API is written down in `docs/API.md`,
80//! each field's grammar in `docs/GRAMMAR.md`, the design in `dev/PLAN.md`,
81//! and the milestone schedule in `dev/ROADMAP.md`
82//! (all in the [repository](https://github.com/jamesgober/parse-net)).
83
84#![doc(html_root_url = "https://docs.rs/parse-net")]
85#![no_std]
86#![cfg_attr(docsrs, feature(doc_cfg))]
87#![forbid(unsafe_code)]
88#![deny(missing_docs)]
89#![deny(unused_must_use, unused_results)]
90#![cfg_attr(
91 not(test),
92 deny(
93 clippy::unwrap_used,
94 clippy::expect_used,
95 clippy::panic,
96 clippy::todo,
97 clippy::unimplemented,
98 clippy::unreachable,
99 clippy::dbg_macro,
100 clippy::print_stdout,
101 clippy::print_stderr,
102 clippy::indexing_slicing
103 )
104)]
105
106#[cfg(any(feature = "std", test))]
107extern crate std;
108
109#[cfg(feature = "alloc")]
110extern crate alloc;
111
112pub mod auth;
113pub mod cache;
114pub mod class;
115pub mod date;
116pub mod disposition;
117mod error;
118pub mod etag;
119pub mod ext_value;
120pub mod forwarded;
121pub mod host;
122mod limits;
123pub mod link;
124pub mod list;
125pub mod mail;
126pub mod media;
127pub mod negotiate;
128pub mod num;
129pub mod origin;
130#[cfg(feature = "alloc")]
131mod owned;
132pub mod param;
133pub mod pct;
134pub mod range;
135pub mod scan;
136pub mod sfv;
137pub mod text;
138mod utf8;
139pub mod write;
140
141pub use auth::{AuthPayload, Challenge, Credentials, basic, bearer};
142pub use cache::{
143 Directive, RequestDirectives, ResponseDirectives, request_cache_control, response_cache_control,
144};
145#[cfg(feature = "std")]
146pub use date::http_date;
147pub use date::{Civil, DateFormat, HttpDate, MailDate, http_date_at};
148pub use disposition::{
149 ContentDisposition, DispositionType, Filename, parse_content_disposition, write_attachment,
150};
151pub use error::{ErrorKind, Full, ParseError, WriteError};
152pub use etag::{Condition, EntityTag, TagList, if_match, if_none_match, parse_condition};
153pub use ext_value::{Charset, ExtValue, write_ext_value};
154pub use forwarded::{ClientAddress, ForwardedSource, Trust, client_address};
155pub use host::{Host, HostPort, parse_host_port};
156pub use limits::{DuplicatePolicy, Limits};
157pub use link::{LinkValue, Links, parse_link};
158pub use list::{List, list};
159pub use media::{Boundary, MediaRange, MediaType, parse_media_type};
160pub use negotiate::{QValue, best_charset, best_encoding, best_language, best_media};
161pub use origin::{Origin, OriginValue, parse_origin};
162#[cfg(feature = "alloc")]
163#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
164pub use owned::{OwnedEntityTag, OwnedHostPort, OwnedMediaType};
165pub use param::{Param, Params};
166pub use range::{
167 ByteRange, ContentRange, IfRange, RangeHeader, RangeOutcome, RangeSpec, byte_ranges,
168};
169pub use text::{Comment, Quoted, Word};
170pub use write::{CountSink, Sink, SliceSink};
171
172/// Crate version string, populated by Cargo at build time.
173///
174/// # Examples
175///
176/// ```
177/// assert_eq!(parse_net::VERSION.split('.').count(), 3);
178/// ```
179pub const VERSION: &str = env!("CARGO_PKG_VERSION");
180
181#[cfg(test)]
182mod tests {
183 use super::VERSION;
184
185 #[test]
186 fn test_version_matches_manifest() {
187 assert_eq!(VERSION, env!("CARGO_PKG_VERSION"));
188 assert_eq!(VERSION.split('.').count(), 3, "expected MAJOR.MINOR.PATCH");
189 }
190}