From fa1ba2776518cb134c7bfbb300d58bb28ad93157 Mon Sep 17 00:00:00 2001 From: Dominik Maier Date: Thu, 17 Oct 2024 21:28:25 +0200 Subject: [PATCH] `no_std`support for the `url` crate --- .github/workflows/main.yml | 14 +++++ data-url/Cargo.toml | 2 +- idna/src/uts46.rs | 3 + url/Cargo.toml | 12 ++-- url/src/host.rs | 10 +++- url/src/lib.rs | 116 +++++++++++++++++++++++++++++-------- url/src/origin.rs | 5 +- url/src/parser.rs | 13 +++-- url/src/path_segments.rs | 23 +++++++- url/src/quirks.rs | 2 + url/src/slicing.rs | 3 +- url/tests/unit.rs | 55 +++++++++++++++--- 12 files changed, 209 insertions(+), 49 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 28ff922ac..44be99ed6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -35,6 +35,13 @@ jobs: - uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.rust }} + # Add toolchain for no_std tests + - run: rustup toolchain install nightly + - name: Add `aarch64-unknown-none` toolchain for `no_std` tests + if: | + matrix.os == 'ubuntu-latest' && + matrix.rust == 'nightly' + run: rustup target add aarch64-unknown-none && rustup component add rust-src --toolchain nightly-x86_64-unknown-linux-gnu - run: cargo build --all-targets # Run tests - name: Run tests @@ -52,6 +59,13 @@ jobs: continue-on-error: true # Fails on GH actions, but not locally. - name: Test `no_std` support run: cargo test --no-default-features --features=alloc + - name: Build `url` crate for `aarch64-unknown-none` with `no_std` + if: | + matrix.os == 'ubuntu-latest' && + matrix.rust == 'nightly' + run: > + cd url + && cargo check --target aarch64-unknown-none -v --no-default-features WASM: runs-on: ubuntu-latest diff --git a/data-url/Cargo.toml b/data-url/Cargo.toml index cdde4ac9f..a831f1e39 100644 --- a/data-url/Cargo.toml +++ b/data-url/Cargo.toml @@ -19,7 +19,7 @@ alloc = [] tester = "0.9" # We pin this transitive dev dep so that MSRV CI can continue to run. unicode-width = "=0.1.12" -serde = {version = "1.0", features = ["derive"]} +serde = { version = "1.0", default-features = false, features = ["alloc", "derive"] } serde_json = "1.0" [lib] diff --git a/idna/src/uts46.rs b/idna/src/uts46.rs index d52a4d0fb..8630d0e2e 100644 --- a/idna/src/uts46.rs +++ b/idna/src/uts46.rs @@ -714,6 +714,9 @@ impl From for Result<(), Errors> { #[cfg(feature = "std")] impl std::error::Error for Errors {} +#[cfg(not(feature = "std"))] +impl core::error::Error for Errors {} + impl fmt::Display for Errors { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Debug::fmt(self, f) diff --git a/url/Cargo.toml b/url/Cargo.toml index 6fd4b994a..ce887411e 100644 --- a/url/Cargo.toml +++ b/url/Cargo.toml @@ -10,7 +10,7 @@ documentation = "https://docs.rs/url" repository = "https://github.com/servo/rust-url" readme = "../README.md" keywords = ["url", "parser"] -categories = ["parser-implementations", "web-programming", "encoding"] +categories = ["parser-implementations", "web-programming", "encoding", "no_std"] license = "MIT OR Apache-2.0" include = ["src/**/*", "LICENSE-*", "README.md", "tests/**"] edition = "2018" @@ -25,13 +25,15 @@ bencher = "0.1" wasm-bindgen-test = "0.3" [dependencies] -form_urlencoded = { version = "1.2.1", path = "../form_urlencoded" } -idna = { version = "0.5.0", path = "../idna" } -percent-encoding = { version = "2.3.1", path = "../percent_encoding" } +form_urlencoded = { version = "1.2.1", path = "../form_urlencoded", default-features = false, features = ["alloc"] } +idna = { version = "0.5.0", path = "../idna", default-features = false, features = ["alloc"] } +percent-encoding = { version = "2.3.1", path = "../percent_encoding", default-features = false, features = ["alloc"] } serde = { version = "1.0", optional = true, features = ["derive"] } [features] -default = [] +default = ["std"] +std = ["idna/std", "percent-encoding/std", "form_urlencoded/std"] + # Enable to use the #[debugger_visualizer] attribute. This feature requires Rust >= 1.71. debugger_visualizer = [] # Expose internal offsets of the URL. diff --git a/url/src/host.rs b/url/src/host.rs index 7ec356326..8b9e507dd 100644 --- a/url/src/host.rs +++ b/url/src/host.rs @@ -6,9 +6,13 @@ // option. This file may not be copied, modified, or distributed // except according to those terms. -use std::cmp; -use std::fmt::{self, Formatter}; -use std::net::{Ipv4Addr, Ipv6Addr}; +use crate::net::{Ipv4Addr, Ipv6Addr}; +use alloc::borrow::ToOwned; +use alloc::string::String; +use alloc::string::ToString; +use alloc::vec::Vec; +use core::cmp; +use core::fmt::{self, Formatter}; use percent_encoding::{percent_decode, utf8_percent_encode, CONTROLS}; #[cfg(feature = "serde")] diff --git a/url/src/lib.rs b/url/src/lib.rs index 3cbf9ee8b..33dc322ec 100644 --- a/url/src/lib.rs +++ b/url/src/lib.rs @@ -73,6 +73,14 @@ assert!(data_url.fragment() == Some("")); # run().unwrap(); ``` +## Default Features + +Versions `<= 2.5.2` of the crate have no default features. Versions `> 2.5.2` have the default feature 'std'. +If you are upgrading across this boundary and you have specified `default-features = false`, then +you will need to add the 'std' feature or the 'alloc' feature to your dependency. +The 'std' feature has the same behavior as the previous versions. The 'alloc' feature +provides no_std support. + ## Serde Enable the `serde` feature to include `Deserialize` and `Serialize` implementations for `url::Url`. @@ -134,6 +142,7 @@ url = { version = "2", features = ["debugger_visualizer"] } */ +#![no_std] #![doc(html_root_url = "https://docs.rs/url/2.5.2")] #![cfg_attr( feature = "debugger_visualizer", @@ -145,29 +154,48 @@ url = { version = "2", features = ["debugger_visualizer"] } pub use form_urlencoded; +// For forwards compatibility +#[cfg(feature = "std")] +extern crate std; + +#[macro_use] +extern crate alloc; + #[cfg(feature = "serde")] extern crate serde; use crate::host::HostInternal; -use crate::parser::{ - to_u32, Context, Parser, SchemeType, PATH_SEGMENT, SPECIAL_PATH_SEGMENT, USERINFO, -}; -use percent_encoding::{percent_decode, percent_encode, utf8_percent_encode}; -use std::borrow::Borrow; -use std::cmp; -use std::fmt::{self, Write}; -use std::hash; + +use crate::net::IpAddr; +#[cfg(feature = "std")] #[cfg(any(unix, windows, target_os = "redox", target_os = "wasi"))] -use std::io; -use std::mem; -use std::net::IpAddr; +use crate::net::{SocketAddr, ToSocketAddrs}; +use crate::parser::{to_u32, Context, Parser, SchemeType, USERINFO}; +use alloc::borrow::ToOwned; +use alloc::str; +use alloc::string::{String, ToString}; +use core::borrow::Borrow; +use core::convert::TryFrom; +use core::fmt::Write; +use core::ops::{Range, RangeFrom, RangeTo}; +use core::{cmp, fmt, hash, mem}; +use percent_encoding::utf8_percent_encode; +#[cfg(feature = "std")] #[cfg(any(unix, windows, target_os = "redox", target_os = "wasi"))] -use std::net::{SocketAddr, ToSocketAddrs}; -use std::ops::{Range, RangeFrom, RangeTo}; +use std::io; +#[cfg(feature = "std")] use std::path::{Path, PathBuf}; -use std::str; -use std::convert::TryFrom; +/// `std` version of `net` +#[cfg(feature = "std")] +pub(crate) mod net { + pub use std::net::*; +} +/// `no_std` nightly version of `net` +#[cfg(not(feature = "std"))] +pub(crate) mod net { + pub use core::net::*; +} pub use crate::host::Host; pub use crate::origin::{OpaqueOrigin, Origin}; @@ -1279,11 +1307,12 @@ impl Url { /// }) /// } /// ``` + #[cfg(feature = "std")] #[cfg(any(unix, windows, target_os = "redox", target_os = "wasi"))] pub fn socket_addrs( &self, default_port_number: impl Fn() -> Option, - ) -> io::Result> { + ) -> io::Result> { // Note: trying to avoid the Vec allocation by returning `impl AsRef<[SocketAddr]>` // causes borrowck issues because the return value borrows `default_port_number`: // @@ -1353,7 +1382,11 @@ impl Url { /// /// ``` /// use url::Url; + /// + /// # #[cfg(feature = "std")] /// # use std::error::Error; + /// # #[cfg(not(feature = "std"))] + /// # use core::error::Error; /// /// # fn run() -> Result<(), Box> { /// let url = Url::parse("https://example.com/foo/bar")?; @@ -1767,7 +1800,11 @@ impl Url { /// /// ``` /// use url::Url; + /// + /// # #[cfg(feature = "std")] /// # use std::error::Error; + /// # #[cfg(not(feature = "std"))] + /// # use core::error::Error; /// /// # fn run() -> Result<(), Box> { /// let mut url = Url::parse("ssh://example.net:2048/")?; @@ -1786,7 +1823,11 @@ impl Url { /// /// ```rust /// use url::Url; + /// + /// # #[cfg(feature = "std")] /// # use std::error::Error; + /// # #[cfg(not(feature = "std"))] + /// # use core::error::Error; /// /// # fn run() -> Result<(), Box> { /// let mut url = Url::parse("https://example.org/")?; @@ -2469,9 +2510,14 @@ impl Url { /// # run().unwrap(); /// # } /// ``` - #[cfg(any(unix, windows, target_os = "redox", target_os = "wasi"))] + /// + /// This method is only available if the `std` Cargo feature is enabled. + #[cfg(all( + feature = "std", + any(unix, windows, target_os = "redox", target_os = "wasi") + ))] #[allow(clippy::result_unit_err)] - pub fn from_file_path>(path: P) -> Result { + pub fn from_file_path>(path: P) -> Result { let mut serialization = "file://".to_owned(); let host_start = serialization.len() as u32; let (host_end, host) = path_to_file_url_segments(path.as_ref(), &mut serialization)?; @@ -2506,9 +2552,14 @@ impl Url { /// /// Note that `std::path` does not consider trailing slashes significant /// and usually does not include them (e.g. in `Path::parent()`). - #[cfg(any(unix, windows, target_os = "redox", target_os = "wasi"))] + /// + /// This method is only available if the `std` Cargo feature is enabled. + #[cfg(all( + feature = "std", + any(unix, windows, target_os = "redox", target_os = "wasi") + ))] #[allow(clippy::result_unit_err)] - pub fn from_directory_path>(path: P) -> Result { + pub fn from_directory_path>(path: P) -> Result { let mut url = Url::from_file_path(path)?; if !url.serialization.ends_with('/') { url.serialization.push('/') @@ -2622,8 +2673,13 @@ impl Url { /// or if `Path::new_opt()` returns `None`. /// (That is, if the percent-decoded path contains a NUL byte or, /// for a Windows path, is not UTF-8.) + /// + /// This method is only available if the `std` Cargo feature is enabled. #[inline] - #[cfg(any(unix, windows, target_os = "redox", target_os = "wasi"))] + #[cfg(all( + feature = "std", + any(unix, windows, target_os = "redox", target_os = "wasi") + ))] #[allow(clippy::result_unit_err)] pub fn to_file_path(&self) -> Result { if let Some(segments) = self.path_segments() { @@ -2827,11 +2883,13 @@ impl<'de> serde::Deserialize<'de> for Url { } } -#[cfg(any(unix, target_os = "redox", target_os = "wasi"))] +#[cfg(all(feature = "std", any(unix, target_os = "redox", target_os = "wasi")))] fn path_to_file_url_segments( path: &Path, serialization: &mut String, ) -> Result<(u32, HostInternal), ()> { + use parser::SPECIAL_PATH_SEGMENT; + use percent_encoding::percent_encode; #[cfg(any(unix, target_os = "redox"))] use std::os::unix::prelude::OsStrExt; #[cfg(target_os = "wasi")] @@ -2857,7 +2915,7 @@ fn path_to_file_url_segments( Ok((host_end, HostInternal::None)) } -#[cfg(windows)] +#[cfg(all(feature = "std", windows))] fn path_to_file_url_segments( path: &Path, serialization: &mut String, @@ -2866,11 +2924,14 @@ fn path_to_file_url_segments( } // Build this unconditionally to alleviate https://github.com/servo/rust-url/issues/102 +#[cfg(feature = "std")] #[cfg_attr(not(windows), allow(dead_code))] fn path_to_file_url_segments_windows( path: &Path, serialization: &mut String, ) -> Result<(u32, HostInternal), ()> { + use crate::parser::PATH_SEGMENT; + use percent_encoding::percent_encode; use std::path::{Component, Prefix}; if !path.is_absolute() { return Err(()); @@ -2929,16 +2990,19 @@ fn path_to_file_url_segments_windows( Ok((host_end, host_internal)) } -#[cfg(any(unix, target_os = "redox", target_os = "wasi"))] +#[cfg(all(feature = "std", any(unix, target_os = "redox", target_os = "wasi")))] fn file_url_segments_to_pathbuf( host: Option<&str>, segments: str::Split<'_, char>, ) -> Result { + use alloc::vec::Vec; + use percent_encoding::percent_decode; use std::ffi::OsStr; #[cfg(any(unix, target_os = "redox"))] use std::os::unix::prelude::OsStrExt; #[cfg(target_os = "wasi")] use std::os::wasi::prelude::OsStrExt; + use std::path::PathBuf; if host.is_some() { return Err(()); @@ -2974,7 +3038,7 @@ fn file_url_segments_to_pathbuf( Ok(path) } -#[cfg(windows)] +#[cfg(all(feature = "std", windows))] fn file_url_segments_to_pathbuf( host: Option<&str>, segments: str::Split, @@ -2983,11 +3047,13 @@ fn file_url_segments_to_pathbuf( } // Build this unconditionally to alleviate https://github.com/servo/rust-url/issues/102 +#[cfg(feature = "std")] #[cfg_attr(not(windows), allow(dead_code))] fn file_url_segments_to_pathbuf_windows( host: Option<&str>, mut segments: str::Split<'_, char>, ) -> Result { + use percent_encoding::percent_decode; let mut string = if let Some(host) = host { r"\\".to_owned() + host } else { diff --git a/url/src/origin.rs b/url/src/origin.rs index 81193f510..a039f4529 100644 --- a/url/src/origin.rs +++ b/url/src/origin.rs @@ -9,7 +9,10 @@ use crate::host::Host; use crate::parser::default_port; use crate::Url; -use std::sync::atomic::{AtomicUsize, Ordering}; +use alloc::borrow::ToOwned; +use alloc::format; +use alloc::string::String; +use core::sync::atomic::{AtomicUsize, Ordering}; pub fn url_origin(url: &Url) -> Origin { let scheme = url.scheme(); diff --git a/url/src/parser.rs b/url/src/parser.rs index 181a02e86..e26b50281 100644 --- a/url/src/parser.rs +++ b/url/src/parser.rs @@ -6,9 +6,10 @@ // option. This file may not be copied, modified, or distributed // except according to those terms. -use std::error::Error; -use std::fmt::{self, Formatter, Write}; -use std::str; +use alloc::string::String; +use alloc::string::ToString; +use core::fmt::{self, Formatter, Write}; +use core::str; use crate::host::{Host, HostInternal}; use crate::Url; @@ -72,7 +73,11 @@ macro_rules! simple_enum_error { } } -impl Error for ParseError {} +#[cfg(feature = "std")] +impl std::error::Error for ParseError {} + +#[cfg(not(feature = "std"))] +impl core::error::Error for ParseError {} simple_enum_error! { EmptyHost => "empty host", diff --git a/url/src/path_segments.rs b/url/src/path_segments.rs index d8a78d785..5cc8e7758 100644 --- a/url/src/path_segments.rs +++ b/url/src/path_segments.rs @@ -8,7 +8,8 @@ use crate::parser::{self, to_u32, SchemeType}; use crate::Url; -use std::str; +use alloc::string::String; +use core::str; /// Exposes methods to manipulate the path of an URL that is not cannot-be-base. /// @@ -19,7 +20,11 @@ use std::str; /// /// ```rust /// use url::Url; +/// +/// # #[cfg(feature = "std")] /// # use std::error::Error; +/// # #[cfg(not(feature = "std"))] +/// # use core::error::Error; /// /// # fn run() -> Result<(), Box> { /// let mut url = Url::parse("mailto:me@example.com")?; @@ -78,7 +83,11 @@ impl<'a> PathSegmentsMut<'a> { /// /// ```rust /// use url::Url; + /// + /// # #[cfg(feature = "std")] /// # use std::error::Error; + /// # #[cfg(not(feature = "std"))] + /// # use core::error::Error; /// /// # fn run() -> Result<(), Box> { /// let mut url = Url::parse("https://github.com/servo/rust-url/")?; @@ -106,7 +115,11 @@ impl<'a> PathSegmentsMut<'a> { /// /// ```rust /// use url::Url; + /// + /// # #[cfg(feature = "std")] /// # use std::error::Error; + /// # #[cfg(not(feature = "std"))] + /// # use core::error::Error; /// /// # fn run() -> Result<(), Box> { /// let mut url = Url::parse("https://github.com/servo/rust-url/")?; @@ -181,7 +194,11 @@ impl<'a> PathSegmentsMut<'a> { /// /// ```rust /// use url::Url; + /// + /// # #[cfg(feature = "std")] /// # use std::error::Error; + /// # #[cfg(not(feature = "std"))] + /// # use core::error::Error; /// /// # fn run() -> Result<(), Box> { /// let mut url = Url::parse("https://github.com/")?; @@ -201,7 +218,11 @@ impl<'a> PathSegmentsMut<'a> { /// /// ```rust /// use url::Url; + /// + /// # #[cfg(feature = "std")] /// # use std::error::Error; + /// # #[cfg(not(feature = "std"))] + /// # use core::error::Error; /// /// # fn run() -> Result<(), Box> { /// let mut url = Url::parse("https://github.com/servo")?; diff --git a/url/src/quirks.rs b/url/src/quirks.rs index 391a50dbe..8626f64ca 100644 --- a/url/src/quirks.rs +++ b/url/src/quirks.rs @@ -13,6 +13,8 @@ use crate::parser::{default_port, Context, Input, Parser, SchemeType}; use crate::{Host, ParseError, Position, Url}; +use alloc::string::String; +use alloc::string::ToString; /// Internal components / offsets of a URL. /// diff --git a/url/src/slicing.rs b/url/src/slicing.rs index 13829575d..4544509dc 100644 --- a/url/src/slicing.rs +++ b/url/src/slicing.rs @@ -6,8 +6,9 @@ // option. This file may not be copied, modified, or distributed // except according to those terms. +use core::ops::{Index, Range, RangeFrom, RangeFull, RangeTo}; + use crate::Url; -use std::ops::{Index, Range, RangeFrom, RangeFull, RangeTo}; impl Index for Url { type Output = str; diff --git a/url/tests/unit.rs b/url/tests/unit.rs index fd9a3ce70..c2cd3ef3d 100644 --- a/url/tests/unit.rs +++ b/url/tests/unit.rs @@ -7,13 +7,39 @@ // except according to those terms. //! Unit tests +#![no_std] -use std::borrow::Cow; -use std::cell::{Cell, RefCell}; -use std::net::{Ipv4Addr, Ipv6Addr}; +#[cfg(feature = "std")] +extern crate std; + +#[macro_use] +extern crate alloc; + +use alloc::borrow::Cow; +use alloc::borrow::ToOwned; +use alloc::string::{String, ToString}; +use alloc::vec::Vec; +use core::cell::{Cell, RefCell}; +#[cfg(feature = "std")] +use std::dbg; +use url::{form_urlencoded, Host, Origin, Url}; + +/// `std` version of `net` +#[cfg(feature = "std")] +pub(crate) mod net { + pub use std::net::*; +} +/// `no_std` nightly version of `net` +#[cfg(not(feature = "std"))] +pub(crate) mod net { + pub use core::net::*; +} + +use crate::net::{Ipv4Addr, Ipv6Addr}; + +#[cfg(feature = "std")] #[cfg(any(unix, windows, target_os = "redox", target_os = "wasi"))] use std::path::{Path, PathBuf}; -use url::{form_urlencoded, Host, Origin, Url}; // https://rustwasm.github.io/wasm-bindgen/wasm-bindgen-test/usage.html #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] @@ -23,7 +49,7 @@ wasm_bindgen_test_configure!(run_in_browser); #[test] fn size() { - use std::mem::size_of; + use core::mem::size_of; assert_eq!(size_of::(), size_of::>()); } @@ -124,6 +150,7 @@ fn test_set_empty_query() { assert_eq!(base.as_str(), "moz://example.com/path"); } +#[cfg(feature = "std")] #[cfg(any(unix, windows, target_os = "redox", target_os = "wasi"))] macro_rules! assert_from_file_path { ($path: expr) => { @@ -138,6 +165,7 @@ macro_rules! assert_from_file_path { } #[test] +#[cfg(feature = "std")] #[cfg(any(unix, windows))] fn new_file_paths() { if cfg!(unix) { @@ -160,7 +188,7 @@ fn new_file_paths() { } #[test] -#[cfg(unix)] +#[cfg(all(feature = "std", unix))] fn new_path_bad_utf8() { use std::ffi::OsStr; use std::os::unix::prelude::*; @@ -171,7 +199,7 @@ fn new_path_bad_utf8() { } #[test] -#[cfg(windows)] +#[cfg(all(feature = "std", windows))] fn new_path_windows_fun() { assert_from_file_path!(r"C:\foo\bar", "/C:/foo/bar"); assert_from_file_path!("C:\\foo\\ba\0r", "/C:/foo/ba%00r"); @@ -192,6 +220,10 @@ fn new_path_windows_fun() { } #[test] +#[cfg(all( + feature = "std", + any(unix, windows, target_os = "redox", target_os = "wasi") +))] #[cfg(any(unix, windows))] fn new_directory_paths() { if cfg!(unix) { @@ -258,6 +290,7 @@ fn issue_124() { } #[test] +#[cfg(feature = "std")] fn test_equality() { use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; @@ -448,6 +481,7 @@ fn issue_61() { } #[test] +#[cfg(feature = "std")] #[cfg(any(unix, target_os = "redox", target_os = "wasi"))] #[cfg(not(windows))] /// https://github.com/servo/rust-url/issues/197 @@ -539,6 +573,7 @@ fn test_leading_dots() { } #[test] +#[cfg(feature = "std")] /// https://github.com/servo/rust-url/issues/302 fn test_origin_hash() { use std::collections::hash_map::DefaultHasher; @@ -633,6 +668,7 @@ fn test_origin_unicode_serialization() { } #[test] +#[cfg(feature = "std")] #[cfg(any(unix, windows, target_os = "redox", target_os = "wasi"))] fn test_socket_addrs() { use std::net::ToSocketAddrs; @@ -815,6 +851,7 @@ fn test_expose_internals() { } #[test] +#[cfg(feature = "std")] #[cfg(windows)] fn test_windows_unc_path() { let url = Url::from_file_path(Path::new(r"\\host\share\path\file.txt")).unwrap(); @@ -923,8 +960,8 @@ fn test_options_reuse() { } /// https://github.com/servo/rust-url/issues/505 -#[cfg(windows)] #[test] +#[cfg(all(feature = "std", windows))] fn test_url_from_file_path() { use std::path::PathBuf; use url::Url; @@ -936,6 +973,7 @@ fn test_url_from_file_path() { } /// https://github.com/servo/rust-url/issues/505 +#[cfg(feature = "std")] #[cfg(any(unix, target_os = "redox", target_os = "wasi"))] #[cfg(not(windows))] #[test] @@ -1309,6 +1347,7 @@ fn test_file_with_drive_and_path() { assert_eq!(url2.to_string(), "file:///p:/a"); } +#[cfg(feature = "std")] #[test] fn issue_864() { let mut url = url::Url::parse("file://").unwrap();