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