1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228
//! Simple EntityTag implementation. //! //! # Usage //! //! ```rust //! extern crate etag; //! //! use etag::EntityTag; //! //! fn main() { //! let my_tag = EntityTag::strong("lolka".to_owned()); //! let text_etag = my_tag.to_string(); //! let parse_tag = text_etag.parse::<EntityTag>().unwrap(); //! //! assert!(my_tag.strong_eq(&parse_tag)); //! } //! ``` use std::fs; use std::fmt; use std::time; use std::error; use std::hash::{ Hash, Hasher }; use std::collections::hash_map::DefaultHasher; /// An entity tag, defined in [RFC7232](https://tools.ietf.org/html/rfc7232#section-2.3) /// /// The ETag HTTP response header is an identifier for a specific version of a resource. It allows /// caches to be more efficient, and saves bandwidth, as a web server does not need to send a full /// response if the content has not changed. On the other side, if the content has changed, etags /// are useful to help prevent simultaneous updates of a resource from overwriting each other /// ("mid-air collisions"). /// /// If the resource at a given URL changes, a new Etag value must be generated. Etags are therefore /// similar to fingerprints and might also be used for tracking purposes by some servers. A /// comparison of them allows to quickly determine whether two representations of a resource are the /// same, but they might also be set to persist indefinitely by a tracking server. /// /// /// # Format `W/"<etag_value>"` /// /// - 'W/' (case-sensitive) indicates that a weak validator is used. Weak validators are easy to /// generate but are far less useful for comparisons. Strong validators are ideal for comparisons /// but can be very difficult to generate efficiently. Weak Etag values of two representations of /// the same resources might be semantically equivalent, but not byte-for-byte identical. /// /// - "<etag_value>" Entity tags uniquely representing the requested resources. They are a string of ASCII /// characters placed between double quotes (Like "675af34563dc-tr34"). The method by which ETag /// values are generated is not specified. Oftentimes, a hash of the content, a hash of the last /// modification timestamp, or just a revision number is used. For example, MDN uses a hash of /// hexadecimal digits of the wiki content. /// /// # Comparison /// To check if two entity tags are equivalent in an application always use the /// `strong_eq` or `weak_eq` methods based on the context of the Tag. Only use /// `==` to check if two tags are identical. /// /// The example below shows the results for a set of entity-tag pairs and /// both the weak and strong comparison function results: /// /// | `ETag 1`| `ETag 2`| Strong Comparison | Weak Comparison | /// |---------|---------|-------------------|-----------------| /// | `W/"1"` | `W/"1"` | no match | match | /// | `W/"1"` | `W/"2"` | no match | no match | /// | `W/"1"` | `"1"` | no match | match | /// | `"1"` | `"1"` | match | match | #[derive(Clone, Debug, Eq, PartialEq)] pub struct EntityTag { /// Weakness indicator for the tag pub weak: bool, /// The opaque string in between the DQUOTEs tag: String, } impl EntityTag { /// Constructs a new EntityTag. /// /// As tag characters must be in ASCII assert /// is included to check for it. pub fn new(weak: bool, tag: String) -> Self { debug_assert!(tag.is_ascii()); EntityTag { weak, tag } } /// Constructs a new weak EntityTag. pub fn weak(tag: String) -> Self { Self::new(true, tag) } /// Constructs a new strong EntityTag. pub fn strong(tag: String) -> Self { Self::new(false, tag) } /// Creates weak EntityTag from file metadata using modified time and len. /// /// ## Format: /// /// `[modified-]<len>` pub fn from_file_meta(metadata: &fs::Metadata) -> Self { let tag = match metadata.modified().map(|modified| modified.duration_since(time::UNIX_EPOCH).expect("Modified is earlier than time::UNIX_EPOCH!")) { Ok(modified) => format!("{}.{}-{}", modified.as_secs(), modified.subsec_nanos(), metadata.len()), _ => format!("{}", metadata.len()) }; Self { weak: true, tag } } /// Creates strong EntityTag by hashing provided bytes. /// /// ## Format: /// /// `<len>-<hash>` pub fn from_hash(bytes: &[u8]) -> Self { let mut hasher = DefaultHasher::default(); bytes.hash(&mut hasher); let tag = format!("{}-{}", bytes.len(), hasher.finish()); Self { weak: false, tag } } /// Get the tag. pub fn tag(&self) -> &str { self.tag.as_ref() } /// Set the tag. pub fn set_tag(&mut self, tag: String) { debug_assert!(tag.is_ascii()); self.tag = tag } /// For strong comparison two entity-tags are equivalent if both are not /// weak and their opaque-tags match character-by-character. pub fn strong_eq(&self, other: &EntityTag) -> bool { !self.weak && !other.weak && self.tag == other.tag } /// For weak comparison two entity-tags are equivalent if their /// opaque-tags match character-by-character, regardless of either or /// both being tagged as "weak". pub fn weak_eq(&self, other: &EntityTag) -> bool { self.tag == other.tag } /// The inverse of `EntityTag.strong_eq()`. pub fn strong_ne(&self, other: &EntityTag) -> bool { !self.strong_eq(other) } /// The inverse of `EntityTag.weak_eq()`. pub fn weak_ne(&self, other: &EntityTag) -> bool { !self.weak_eq(other) } } impl fmt::Display for EntityTag { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self.weak { true => write!(f, "W/\"{}\"", self.tag), false => write!(f, "\"{}\"", self.tag), } } } ///Describes possible errors for EntityTag #[derive(Debug)] pub enum ParseError { ///Format of EntityTag is invalid InvalidFormat, ///Tag contains non-ASCII characters NotAscii } impl fmt::Display for ParseError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { ParseError::InvalidFormat => write!(f, "EntityTag uses invalid format"), ParseError::NotAscii => write!(f, "EntityTag uses non-ASCII characters") } } } impl error::Error for ParseError { fn description(&self) -> &str { match self { ParseError::InvalidFormat => "EntityTag uses invalid format", ParseError::NotAscii => "EntityTag uses non-ASCII characters" } } } impl std::str::FromStr for EntityTag { type Err = ParseError; fn from_str(text: &str) -> Result<EntityTag, ParseError> { let len = text.len(); let slice = &text[..]; if !slice.ends_with('"') || len < 2 { return Err(ParseError::InvalidFormat); } if slice.starts_with('"') { let slice = &slice[1..len-1]; match slice.is_ascii() { true => Ok(EntityTag::strong(slice.to_string())), false => Err(ParseError::NotAscii) } } else if len >= 4 && slice.starts_with("W/\"") { let slice = &slice[3..len-1]; match slice.is_ascii() { true => Ok(EntityTag::weak(slice.to_string())), false => Err(ParseError::NotAscii) } } else { Err(ParseError::InvalidFormat) } } }