Run cargo fmt

pull/1/head
Trinity Pointard 6 years ago
parent e16eb40fd9
commit f3ec5bd6dc

@ -1,5 +1,4 @@
use csrf::{AesGcmCsrfProtection, CsrfProtection,
CSRF_COOKIE_NAME, CSRF_FORM_FIELD};
use csrf::{AesGcmCsrfProtection, CsrfProtection, CSRF_COOKIE_NAME, CSRF_FORM_FIELD};
use data_encoding::{BASE64, BASE64URL_NOPAD};
use rand::prelude::thread_rng;
use rand::Rng;
@ -19,7 +18,8 @@ use csrf_token::CsrfToken;
use path::Path;
use utils::parse_args;
const CSRF_FORM_FIELD_MULTIPART: &[u8] = "Content-Disposition: form-data; name=\"csrf-token\"".as_bytes();
const CSRF_FORM_FIELD_MULTIPART: &[u8] =
"Content-Disposition: form-data; name=\"csrf-token\"".as_bytes();
/// Builder for [CsrfFairing](struct.CsrfFairing.html)
///
@ -321,10 +321,12 @@ impl Fairing for CsrfFairing {
let _ = request.guard::<CsrfToken>(); //force regeneration of csrf cookies
let token = if request.content_type()
let token = if request
.content_type()
.map(|c| c.media_type())
.filter(|m| m.top() == "multipart" && m.sub() == "form-data")
.is_some() {
.is_some()
{
data.peek().split(|&c| c==0x0A || c==0x0D)//0x0A=='\n', 0x0D=='\r'
.filter(|l| l.len() > 0)
.skip_while(|&l| l != CSRF_FORM_FIELD_MULTIPART && l != &CSRF_FORM_FIELD_MULTIPART[..CSRF_FORM_FIELD_MULTIPART.len()-2])
@ -333,7 +335,13 @@ impl Fairing for CsrfFairing {
.next().unwrap_or(None)
} else {
parse_args(from_utf8(data.peek()).unwrap_or(""))
.filter_map(|(key, token)| if key == CSRF_FORM_FIELD {Some(token.as_bytes())} else {None})
.filter_map(|(key, token)| {
if key == CSRF_FORM_FIELD {
Some(token.as_bytes())
} else {
None
}
})
.next()
}.and_then(|token| BASE64URL_NOPAD.decode(&token).ok())
.and_then(|token| csrf_engine.parse_token(&token).ok());
@ -379,7 +387,8 @@ impl Fairing for CsrfFairing {
if self
.auto_insert_disable_prefix
.iter()
.any(|prefix| uri.starts_with(prefix)) {
.any(|prefix| uri.starts_with(prefix))
{
return;
} //if request is on an ignored prefix, ignore it

@ -1,8 +1,7 @@
use std::io::{Read, Error};
use csrf_proxy::ParseState::*;
use std::cmp;
use std::collections::VecDeque;
use csrf_proxy::ParseState::*;
use std::io::{Error, Read};
#[derive(Debug)]
struct Buffer {
@ -43,7 +42,8 @@ impl Buffer {
}
fn len(&self) -> usize {
self.buf.iter().fold(0, |size, buf| size+buf.len()) - self.pos.iter().fold(0, |size, pos| size + pos)
self.buf.iter().fold(0, |size, buf| size + buf.len())
- self.pos.iter().fold(0, |size, pos| size + pos)
}
fn is_empty(&self) -> bool {
@ -57,7 +57,6 @@ impl Default for Buffer {
}
}
#[derive(Debug)]
enum ParseState {
Init, //default state
@ -100,17 +99,15 @@ impl<'a> CsrfProxy<'a> {
impl<'a> Read for CsrfProxy<'a> {
fn read(&mut self, buf: &mut [u8]) -> Result<usize, Error> {
while self.buf.len() < buf.len() && !(self.eof && self.unparsed.is_empty()) {
let len = if !self.eof {
let unparsed_len = self.unparsed.len();
self.unparsed.resize(4096, 0);
unparsed_len +
match self.underlying.read(&mut self.unparsed[unparsed_len..]) {
unparsed_len + match self.underlying.read(&mut self.unparsed[unparsed_len..]) {
Ok(0) => {
self.eof = true;
0
},
}
Ok(len) => len,
Err(e) => return Err(e),
}
@ -137,9 +134,11 @@ impl<'a> Read for CsrfProxy<'a> {
consumed += buf.len();
Init
}
},
}
PartialFormMatch => {
if let Some(lower_begin) = buf.get(1..5).map(|slice| slice.to_ascii_lowercase()) {
if let Some(lower_begin) =
buf.get(1..5).map(|slice| slice.to_ascii_lowercase())
{
buf = &buf[5..];
consumed += 5;
if lower_begin == "form".as_bytes() {
@ -151,7 +150,7 @@ impl<'a> Read for CsrfProxy<'a> {
leave = true;
PartialFormMatch
}
},
}
SearchFormElem => {
if let Some(tag_pos) = buf.iter().position(|&c| c as char == '<') {
buf = &buf[tag_pos..];
@ -161,13 +160,17 @@ impl<'a> Read for CsrfProxy<'a> {
leave = true;
consumed += buf.len();
SearchFormElem
}},
}
}
PartialFormElemMatch => {
if let Some(lower_begin) = buf.get(1..9).map(|slice| slice.to_ascii_lowercase()) {
if let Some(lower_begin) =
buf.get(1..9).map(|slice| slice.to_ascii_lowercase())
{
if lower_begin.starts_with("/form".as_bytes())
|| lower_begin.starts_with("textarea".as_bytes())
|| lower_begin.starts_with("button".as_bytes())
|| lower_begin.starts_with("select".as_bytes()) {
|| lower_begin.starts_with("select".as_bytes())
{
insert_token = true;
leave = true;
Init
@ -182,12 +185,16 @@ impl<'a> Read for CsrfProxy<'a> {
leave = true;
SearchFormElem
}
},
}
SearchMethod(pos) => {
if let Some(meth_pos) = buf[pos..].iter().position(|&c| c as char == ' ' || c as char == '>') {
if let Some(meth_pos) = buf[pos..]
.iter()
.position(|&c| c as char == ' ' || c as char == '>')
{
if buf[meth_pos + pos] as char == ' ' {
PartialNameMatch(meth_pos + pos + 1)
} else { //reached '>'
} else {
//reached '>'
insert_token = true;
leave = true;
Init
@ -196,11 +203,15 @@ impl<'a> Read for CsrfProxy<'a> {
leave = true;
SearchMethod(buf.len())
}
},
}
PartialNameMatch(pos) => {
if let Some(lower_begin) = buf.get(pos..pos+14).map(|slice| slice.to_ascii_lowercase()) {
if let Some(lower_begin) = buf
.get(pos..pos + 14)
.map(|slice| slice.to_ascii_lowercase())
{
if lower_begin.starts_with("name=\"_method\"".as_bytes())
|| lower_begin.starts_with("name='_method'".as_bytes()) {
|| lower_begin.starts_with("name='_method'".as_bytes())
{
buf = &buf[pos + 14..];
consumed += pos + 14;
CloseInputTag
@ -215,7 +226,7 @@ impl<'a> Read for CsrfProxy<'a> {
leave = true;
PartialNameMatch(pos)
}
},
}
CloseInputTag => {
leave = true;
if let Some(tag_pos) = buf.iter().position(|&c| c as char == '>') {
@ -227,7 +238,7 @@ impl<'a> Read for CsrfProxy<'a> {
consumed += buf.len();
CloseInputTag
}
},
}
}
}
(consumed, insert_token)
@ -305,7 +316,8 @@ mod tests{
<body>
Body of this simple doc
</body>
</html>".as_bytes();
</html>"
.as_bytes();
let mut proxy = CsrfProxy::from(Box::new(Cursor::new(data)), "abcd".as_bytes());
let mut pr_data = Vec::new();
let read = proxy.read_to_end(&mut pr_data);
@ -327,7 +339,8 @@ mod tests{
</p>
</form>
</body>
</html>".as_bytes();
</html>"
.as_bytes();
let expected = "<!DOCTYPE html>
<html>
<head>
@ -340,11 +353,15 @@ mod tests{
</p>
<input type=\"hidden\" name=\"csrf-token\" value=\"abcd\"/></form>
</body>
</html>".as_bytes();
</html>"
.as_bytes();
let mut proxy = CsrfProxy::from(Box::new(Cursor::new(data)), "abcd".as_bytes());
let mut pr_data = Vec::new();
let read = proxy.read_to_end(&mut pr_data);
assert_eq!(read.unwrap(), data.len() + "<input type=\"hidden\" name=\"csrf-token\" value=\"abcd\"/>".len());
assert_eq!(
read.unwrap(),
data.len() + "<input type=\"hidden\" name=\"csrf-token\" value=\"abcd\"/>".len()
);
assert_eq!(&pr_data, &expected)
}
@ -360,7 +377,8 @@ mod tests{
<input name=\"name\"/>
</form>
</body>
</html>".as_bytes();
</html>"
.as_bytes();
let expected = "<!DOCTYPE html>
<html>
<head>
@ -371,11 +389,15 @@ mod tests{
<input type=\"hidden\" name=\"csrf-token\" value=\"abcd\"/><input name=\"name\"/>
</form>
</body>
</html>".as_bytes();
</html>"
.as_bytes();
let mut proxy = CsrfProxy::from(Box::new(Cursor::new(data)), "abcd".as_bytes());
let mut pr_data = Vec::new();
let read = proxy.read_to_end(&mut pr_data);
assert_eq!(read.unwrap(), data.len() + "<input type=\"hidden\" name=\"csrf-token\" value=\"abcd\"/>".len());
assert_eq!(
read.unwrap(),
data.len() + "<input type=\"hidden\" name=\"csrf-token\" value=\"abcd\"/>".len()
);
assert_eq!(&pr_data, &expected)
}
@ -391,7 +413,8 @@ mod tests{
<input name=\"_method\"/>
</form>
</body>
</html>".as_bytes();
</html>"
.as_bytes();
let expected = "<!DOCTYPE html>
<html>
<head>
@ -402,11 +425,15 @@ mod tests{
<input name=\"_method\"/><input type=\"hidden\" name=\"csrf-token\" value=\"abcd\"/>
</form>
</body>
</html>".as_bytes();
</html>"
.as_bytes();
let mut proxy = CsrfProxy::from(Box::new(Cursor::new(data)), "abcd".as_bytes());
let mut pr_data = Vec::new();
let read = proxy.read_to_end(&mut pr_data);
assert_eq!(read.unwrap(), data.len() + "<input type=\"hidden\" name=\"csrf-token\" value=\"abcd\"/>".len());
assert_eq!(
read.unwrap(),
data.len() + "<input type=\"hidden\" name=\"csrf-token\" value=\"abcd\"/>".len()
);
assert_eq!(&pr_data, &expected)
}
@ -424,7 +451,10 @@ mod tests{
let err = ErrorReader {};
let mut proxy_err = CsrfProxy::from(Box::new(err), &[0]);
let read = proxy_err.read(buf).unwrap_err();
assert_eq!(read.kind(), ::std::io::Error::new(::std::io::ErrorKind::Other, "").kind());
assert_eq!(
read.kind(),
::std::io::Error::new(::std::io::ErrorKind::Other, "").kind()
);
}
struct SlowReader<'a> {
@ -444,7 +474,8 @@ mod tests{
}
#[test]
fn test_difficult_cut() {//this basically re-test the parser, using short reads so it encounter rare code paths
fn test_difficult_cut() {
//this basically re-test the parser, using short reads so it encounter rare code paths
let data = "<!DOCTYPE html>
<html>
<head>
@ -457,7 +488,8 @@ mod tests{
</p>
</form>
</body>
</html>".as_bytes();
</html>"
.as_bytes();
let expected = "<!DOCTYPE html>
<html>
<head>
@ -470,11 +502,15 @@ mod tests{
</p>
<input type=\"hidden\" name=\"csrf-token\" value=\"abcd\"/></form>
</body>
</html>".as_bytes();
</html>"
.as_bytes();
let mut proxy = CsrfProxy::from(Box::new(SlowReader { content: data }), "abcd".as_bytes());
let mut pr_data = Vec::new();
let read = proxy.read_to_end(&mut pr_data);
assert_eq!(read.unwrap(), data.len() + "<input type=\"hidden\" name=\"csrf-token\" value=\"abcd\"/>".len());
assert_eq!(
read.unwrap(),
data.len() + "<input type=\"hidden\" name=\"csrf-token\" value=\"abcd\"/>".len()
);
assert_eq!(&pr_data, &expected);
let data = "<!DOCTYPE html>
@ -487,7 +523,8 @@ mod tests{
<input name=\"name\"/>
</form>
</body>
</html>".as_bytes();
</html>"
.as_bytes();
let expected = "<!DOCTYPE html>
<html>
<head>
@ -498,11 +535,15 @@ mod tests{
<input type=\"hidden\" name=\"csrf-token\" value=\"abcd\"/><input name=\"name\"/>
</form>
</body>
</html>".as_bytes();
</html>"
.as_bytes();
let mut proxy = CsrfProxy::from(Box::new(SlowReader { content: data }), "abcd".as_bytes());
let mut pr_data = Vec::new();
let read = proxy.read_to_end(&mut pr_data);
assert_eq!(read.unwrap(), data.len() + "<input type=\"hidden\" name=\"csrf-token\" value=\"abcd\"/>".len());
assert_eq!(
read.unwrap(),
data.len() + "<input type=\"hidden\" name=\"csrf-token\" value=\"abcd\"/>".len()
);
assert_eq!(&pr_data, &expected);
let data = "<!DOCTYPE html>
@ -515,7 +556,8 @@ mod tests{
<input name=\"_method\"/>
</form>
</body>
</html>".as_bytes();
</html>"
.as_bytes();
let expected = "<!DOCTYPE html>
<html>
<head>
@ -526,11 +568,15 @@ mod tests{
<input name=\"_method\"/><input type=\"hidden\" name=\"csrf-token\" value=\"abcd\"/>
</form>
</body>
</html>".as_bytes();
</html>"
.as_bytes();
let mut proxy = CsrfProxy::from(Box::new(SlowReader { content: data }), "abcd".as_bytes());
let mut pr_data = Vec::new();
let read = proxy.read_to_end(&mut pr_data);
assert_eq!(read.unwrap(), data.len() + "<input type=\"hidden\" name=\"csrf-token\" value=\"abcd\"/>".len());
assert_eq!(
read.unwrap(),
data.len() + "<input type=\"hidden\" name=\"csrf-token\" value=\"abcd\"/>".len()
);
assert_eq!(&pr_data, &expected)
}
}

@ -1,9 +1,9 @@
use csrf::{AesGcmCsrfProtection, CsrfProtection, CSRF_COOKIE_NAME};
use data_encoding::{BASE64, BASE64URL_NOPAD};
use rocket::{Request, State};
use rocket::http::{Cookie, Status};
use rocket::outcome::Outcome;
use rocket::request::{self, FromRequest};
use rocket::{Request, State};
use serde::{Serialize, Serializer};
use time::Duration;

@ -49,11 +49,11 @@ extern crate rocket;
extern crate serde;
extern crate time;
mod csrf_proxy;
mod csrf_fairing;
mod csrf_proxy;
mod csrf_token;
mod path;
mod utils;
pub use self::csrf_fairing::{CsrfFairingBuilder, CsrfFairing};
pub use self::csrf_fairing::{CsrfFairing, CsrfFairingBuilder};
pub use self::csrf_token::CsrfToken;

@ -30,15 +30,20 @@ impl Path {
}
})
.collect();
if path[0..path.len()-1].iter().any(|a|
if let PathPart::MultiDynamic(_) = a {true} else {false}
) {
if path[0..path.len() - 1].iter().any(|a| {
if let PathPart::MultiDynamic(_) = a {
true
} else {
false
}
}) {
panic!("PathPart::MultiDynamic can only be found at end of path"); //TODO return error instead of panic
}
let param = query.map(|query| {
parse_args(query)
.map(|(k, v)| {(
.map(|(k, v)| {
(
k.to_owned(),
if v.starts_with('<') && v.ends_with("..>") {
panic!("PathPart::MultiDynamic is invalid in query part");
@ -52,10 +57,7 @@ impl Path {
})
.collect()
});
Path {
path,
param,
}
Path { path, param }
}
pub fn extract<'a>(&self, uri: &'a str) -> Option<HashMap<&str, String>> {
@ -78,18 +80,18 @@ impl Path {
//static, but not the same, fail to parse
if let Some(val) = path.next() {
if val != reference {
return None
return None;
}
} else {
return None
return None;
}
}
},
PathPart::Dynamic(key) => {
//dynamic, store to hashmap
if let Some(val) = path.next() {
res.insert(key, val.to_owned());
} else {
return None
return None;
}
}
PathPart::MultiDynamic(key) => {
@ -120,7 +122,9 @@ impl Path {
//dynamic, store to hashmap
res.insert(key, hm.get::<&str>(&(k as &str))?.to_string());
}
PathPart::MultiDynamic(_) => panic!("PathPart::MultiDynamic is invalid in query part"),
PathPart::MultiDynamic(_) => {
panic!("PathPart::MultiDynamic is invalid in query part")
}
}
}
} else {
@ -143,7 +147,9 @@ impl Path {
res.push('/');
match seg {
PathPart::Static(val) => res.push_str(val),
PathPart::Dynamic(val) | PathPart::MultiDynamic(val) => res.push_str(param.get::<str>(val)?),
PathPart::Dynamic(val) | PathPart::MultiDynamic(val) => {
res.push_str(param.get::<str>(val)?)
}
}
}
if let Some(ref keymap) = self.param {
@ -155,7 +161,9 @@ impl Path {
match v {
PathPart::Static(val) => res.push_str(val),
PathPart::Dynamic(val) => res.push_str(param.get::<str>(val)?),
PathPart::MultiDynamic(_) => panic!("PathPart::MultiDynamic is invalid in query part"),
PathPart::MultiDynamic(_) => {
panic!("PathPart::MultiDynamic is invalid in query part")
}
}
res.push('&');
}
@ -197,33 +205,58 @@ mod tests{
fn test_static_path_with_query() {
let query = Path::from("/path/query?param=value&param2=value2");
assert!(query.extract("/path/query").is_none());
assert!(query.extract("/path/other?param=value&param2=value2").is_none());
assert!(query.extract("/path/query/longer?param=value&param2=value2").is_none());
assert!(
query
.extract("/path/other?param=value&param2=value2")
.is_none()
);
assert!(
query
.extract("/path/query/longer?param=value&param2=value2")
.is_none()
);
assert!(query.extract("/path?param=value&param2=value2").is_none());
let hashmap = query.extract("/path/query?param=value&param2=value2").unwrap();
let hashmap = query
.extract("/path/query?param=value&param2=value2")
.unwrap();
assert_eq!(hashmap.len(), 0);
let hashmap = query.extract("/path/query?param2=value2&param=value").unwrap();
let hashmap = query
.extract("/path/query?param2=value2&param=value")
.unwrap();
assert_eq!(hashmap.len(), 0);
let uri = query.map(&HashMap::new()).unwrap();
assert!(uri=="/path/query?param=value&param2=value2" || uri=="/path/query?param2=value2&param=value");
assert!(
uri == "/path/query?param=value&param2=value2"
|| uri == "/path/query?param2=value2&param=value"
);
let mut hashmap = HashMap::new();
hashmap.insert("key", "value".to_owned());
let uri = query.map(&hashmap).unwrap();
assert!(uri=="/path/query?param=value&param2=value2" || uri=="/path/query?param2=value2&param=value");
assert!(
uri == "/path/query?param=value&param2=value2"
|| uri == "/path/query?param2=value2&param=value"
);
}
#[test]
fn test_dynamic_path_without_query() {
let no_query = Path::from("/path/<with>/<dynamic>/values");
assert!(no_query.extract("/path/with/dynamic/values/longer").is_none());
assert!(
no_query
.extract("/path/with/dynamic/values/longer")
.is_none()
);
assert!(no_query.extract("/path/with/dynamic").is_none());
assert!(no_query.extract("/path/with/dynamic/non_value").is_none());
assert!(no_query.extract("/path/with/dynamic/values?and=query").is_none());
assert!(
no_query
.extract("/path/with/dynamic/values?and=query")
.is_none()
);
let hashmap = no_query.extract("/path/containing/moving/values").unwrap();
assert_eq!(hashmap.len(), 2);
@ -235,23 +268,37 @@ mod tests{
let mut hashmap = HashMap::new();
hashmap.insert("with", "with".to_owned());
hashmap.insert("dynamic", "non_static".to_owned());
assert_eq!(no_query.map(&hashmap).unwrap(), "/path/with/non_static/values");
assert_eq!(
no_query.map(&hashmap).unwrap(),
"/path/with/non_static/values"
);
hashmap.insert("random", "value".to_owned());
assert_eq!(no_query.map(&hashmap).unwrap(), "/path/with/non_static/values");
assert_eq!(
no_query.map(&hashmap).unwrap(),
"/path/with/non_static/values"
);
}
#[test]
fn test_dynamic_path_with_query() {
let query = Path::from("/path/<with>/<dynamic>/values?key=<value>&static=static");
assert!(query.extract("/path/with/dynamic/values?key=something&static=error").is_none());
let hashmap = query.extract("/path/containing/moving/values?key=val&static=static").unwrap();
assert!(
query
.extract("/path/with/dynamic/values?key=something&static=error")
.is_none()
);
let hashmap = query
.extract("/path/containing/moving/values?key=val&static=static")
.unwrap();
assert_eq!(hashmap.len(), 3);
assert_eq!(hashmap.get("with").unwrap(), "containing");
assert_eq!(hashmap.get("dynamic").unwrap(), "moving");
assert_eq!(hashmap.get("value").unwrap(), "val");
let hashmap = query.extract("/path/containing/moving/values?static=static&key=val").unwrap();
let hashmap = query
.extract("/path/containing/moving/values?static=static&key=val")
.unwrap();
assert_eq!(hashmap.len(), 3);
assert_eq!(hashmap.get("with").unwrap(), "containing");
assert_eq!(hashmap.get("dynamic").unwrap(), "moving");
@ -263,9 +310,19 @@ mod tests{
hashmap.insert("with", "with".to_owned());
hashmap.insert("dynamic", "non_static".to_owned());
hashmap.insert("value", "something".to_owned());
assert!(query.map(&hashmap).unwrap()=="/path/with/non_static/values?key=something&static=static" || query.map(&hashmap).unwrap()=="/path/with/non_static/values?static=static&key=something");
assert!(
query.map(&hashmap).unwrap()
== "/path/with/non_static/values?key=something&static=static"
|| query.map(&hashmap).unwrap()
== "/path/with/non_static/values?static=static&key=something"
);
hashmap.insert("random", "value".to_owned());
assert!(query.map(&hashmap).unwrap()=="/path/with/non_static/values?key=something&static=static" || query.map(&hashmap).unwrap()=="/path/with/non_static/values?static=static&key=something");
assert!(
query.map(&hashmap).unwrap()
== "/path/with/non_static/values?key=something&static=static"
|| query.map(&hashmap).unwrap()
== "/path/with/non_static/values?static=static&key=something"
);
}
#[test]
@ -288,12 +345,17 @@ mod tests{
assert_eq!(hashmap.len(), 1);
assert_eq!(hashmap.get("multidyn").unwrap(), "");
let hashmap = query.extract("/path/longer/than/before?static=static").unwrap();
let hashmap = query
.extract("/path/longer/than/before?static=static")
.unwrap();
assert_eq!(hashmap.len(), 1);
assert_eq!(hashmap.get("multidyn").unwrap(), "longer/than/before");
let mut hashmap = HashMap::new();
hashmap.insert("multidyn", "something".to_owned());
assert_eq!(query.map(&hashmap).unwrap(), "/path/something?static=static");
assert_eq!(
query.map(&hashmap).unwrap(),
"/path/something?static=static"
);
}
}

@ -15,16 +15,22 @@ fn parse_keyvalue(kv: &str) -> Option<(&str, &str)> {
#[cfg(test)]
mod tests {
use utils::{parse_keyvalue, parse_args};
use utils::{parse_args, parse_keyvalue};
#[test]
fn test_parse_keyvalue() {
assert_eq!(parse_keyvalue("a_key=a_value").unwrap(),("a_key", "a_value"));
assert_eq!(
parse_keyvalue("a_key=a_value").unwrap(),
("a_key", "a_value")
);
assert_eq!(parse_keyvalue("=a_value").unwrap(), ("", "a_value"));
assert_eq!(parse_keyvalue("a_key=").unwrap(), ("a_key", ""));
assert_eq!(parse_keyvalue("a_key=a=value").unwrap(),("a_key", "a=value"));
assert_eq!(
parse_keyvalue("a_key=a=value").unwrap(),
("a_key", "a=value")
);
assert_eq!(parse_keyvalue("=").unwrap(), ("", ""));

Loading…
Cancel
Save