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)
        }
    }
}