Skip to main content

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, &current_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//! Feature complete, `0.5.0`: the core, the web layer, links, Structured
77//! Field writing, and the email lexical layer. The public API is written
78//! down in `docs/API.md`, each field's grammar in `docs/GRAMMAR.md`, the
79//! design in `dev/PLAN.md`, and the milestone schedule in `dev/ROADMAP.md`
80//! (all in the [repository](https://github.com/jamesgober/parse-net)).
81
82#![doc(html_root_url = "https://docs.rs/parse-net")]
83#![no_std]
84#![cfg_attr(docsrs, feature(doc_cfg))]
85#![forbid(unsafe_code)]
86#![deny(missing_docs)]
87#![deny(unused_must_use, unused_results)]
88#![cfg_attr(
89    not(test),
90    deny(
91        clippy::unwrap_used,
92        clippy::expect_used,
93        clippy::panic,
94        clippy::todo,
95        clippy::unimplemented,
96        clippy::unreachable,
97        clippy::dbg_macro,
98        clippy::print_stdout,
99        clippy::print_stderr,
100        clippy::indexing_slicing
101    )
102)]
103
104#[cfg(any(feature = "std", test))]
105extern crate std;
106
107#[cfg(feature = "alloc")]
108extern crate alloc;
109
110pub mod auth;
111pub mod cache;
112pub mod class;
113pub mod date;
114pub mod disposition;
115mod error;
116pub mod etag;
117pub mod ext_value;
118pub mod forwarded;
119pub mod host;
120mod limits;
121pub mod link;
122pub mod list;
123pub mod mail;
124pub mod media;
125pub mod negotiate;
126pub mod num;
127pub mod origin;
128#[cfg(feature = "alloc")]
129mod owned;
130pub mod param;
131pub mod pct;
132pub mod range;
133pub mod scan;
134pub mod sfv;
135pub mod text;
136mod utf8;
137pub mod write;
138
139pub use auth::{AuthPayload, Challenge, Credentials, basic, bearer};
140pub use cache::{
141    Directive, RequestDirectives, ResponseDirectives, request_cache_control, response_cache_control,
142};
143#[cfg(feature = "std")]
144pub use date::http_date;
145pub use date::{Civil, DateFormat, HttpDate, MailDate, http_date_at};
146pub use disposition::{
147    ContentDisposition, DispositionType, Filename, parse_content_disposition, write_attachment,
148};
149pub use error::{ErrorKind, Full, ParseError, WriteError};
150pub use etag::{Condition, EntityTag, TagList, if_match, if_none_match, parse_condition};
151pub use ext_value::{Charset, ExtValue, write_ext_value};
152pub use forwarded::{ClientAddress, ForwardedSource, Trust, client_address};
153pub use host::{Host, HostPort, parse_host_port};
154pub use limits::{DuplicatePolicy, Limits};
155pub use link::{LinkValue, Links, parse_link};
156pub use list::{List, list};
157pub use media::{Boundary, MediaRange, MediaType, parse_media_type};
158pub use negotiate::{QValue, best_charset, best_encoding, best_language, best_media};
159pub use origin::{Origin, OriginValue, parse_origin};
160#[cfg(feature = "alloc")]
161#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
162pub use owned::{OwnedEntityTag, OwnedHostPort, OwnedMediaType};
163pub use param::{Param, Params};
164pub use range::{
165    ByteRange, ContentRange, IfRange, RangeHeader, RangeOutcome, RangeSpec, byte_ranges,
166};
167pub use text::{Comment, Quoted, Word};
168pub use write::{CountSink, Sink, SliceSink};
169
170/// Crate version string, populated by Cargo at build time.
171///
172/// # Examples
173///
174/// ```
175/// assert_eq!(parse_net::VERSION.split('.').count(), 3);
176/// ```
177pub const VERSION: &str = env!("CARGO_PKG_VERSION");
178
179#[cfg(test)]
180mod tests {
181    use super::VERSION;
182
183    #[test]
184    fn test_version_matches_manifest() {
185        assert_eq!(VERSION, env!("CARGO_PKG_VERSION"));
186        assert_eq!(VERSION.split('.').count(), 3, "expected MAJOR.MINOR.PATCH");
187    }
188}