Browse Source

separate prototype from test

heck-rework
heck 3 years ago
parent
commit
b463d27c5d
  1. 468
      src/prototype.hh
  2. 540
      test/test_nr1.cc

468
src/prototype.hh

@ -0,0 +1,468 @@
#include <iostream>
#include <cctype>
#include <utility>
#include <pEp/utils.hh>
#include <pEp/pEpLog.hh>
#include <pEp/inspect.hh>
#include <type_traits>
#include "../examples/libc99/libc99.h"
namespace pEp {
//---------------------------------------------------------------------------------------------
#define EXSTR(msg) std::string(__FUNCTION__) + "() - " + msg
template<class T>
class POD {
static_assert(
std::is_pod<T>::value && !std::is_pointer<T>::value,
"only POD value types supported");
public:
POD()
{
pEpLogClass("called");
};
explicit POD(T* pod_p, const T& init_val)
{
bind(pod_p, init_val);
}
// Best to use this constructor, as the object is in a valid state when the wrapped
// c_str (char*) is known
explicit POD(T* pod_p)
{
bind(pod_p);
}
void bind(T* pod_p, const T& init_val)
{
bind(pod_p);
this->operator=(init_val);
}
void bind(T* pod_p)
{
if (pod_p == nullptr) {
throw Exception{ EXSTR("cant bind on a nullptr") };
}
if (_is_initialized) {
throw Exception{ EXSTR("can only bind once") };
}
// init
_pod_p = pod_p;
_is_initialized = true;
pEpLogClass(to_string());
}
// make a copy
POD& operator=(T value)
{
pEpLogClass("Before: " + to_string() + " - new val: '" + CXX::Inspect::val(value) + "'");
*_pod_p = value;
// pEpLogClass("After: " + to_string());
return *this;
}
// return a copy
operator T() const
{
return *_pod_p;
}
// return address of the wrappee
T* data()
{
return _pod_p;
}
// return address of the wrappee (const)
const T* c_data() const
{
return _pod_p;
}
std::string to_string() const
{
std::string ret{ "[" + CXX::Inspect::all(_pod_p) + "]" };
return ret;
}
static bool log_enabled;
private:
bool _is_initialized{ false };
T* _pod_p{ nullptr };
Adapter::pEpLog::pEpLogger logger{ "pEp::POD", log_enabled };
Adapter::pEpLog::pEpLogger& m4gic_logger_n4me = logger;
class Exception : public std::runtime_error {
public:
explicit Exception(const std::string& msg) : std::runtime_error(msg) {}
};
};
template<class T>
bool pEp::POD<T>::log_enabled{ false };
template<class T>
std::ostream& operator<<(std::ostream& o, pEp::POD<T> pEpPOD)
{
return o << (T)pEpPOD;
}
//---------------------------------------------------------------------------------------------
// Manages a char* to appear like a std::string
// char* c_str
// c_str MUST point to either:
// * NULL - means there is no value/mem alloc yet
// * dyn allocated memory
// in case the mem is already allocated, there must be 2 modes
// * take ownership (and free it)
// * dont take ownership, only provide a C++ interface
// There must be the option to construct the object before the wrapped c_str (char*)
// is known. This leads to a pEp::String object in invalid state, and it must be initialized using
// bind(). Otherwise all(most) functions will throw.
// bind() can only be called once, will throw otherwise.
// NON-owning mode, just does not free the string when it dies, owning mode does.
// Thats the only difference.
class String {
public:
String()
{
pEpLogClass("called");
}
// Best to use this constructor, as the object is in a valid state when the wrapped
// c_str (char*) is known
String(bool is_owner, char** c_str_pp)
{
bind(is_owner, c_str_pp);
}
String(bool is_owner, char** c_str_pp, const std::string& init_val)
{
bind(is_owner, c_str_pp, init_val);
}
void bind(bool is_owner, char** c_str_pp, const std::string& init_val)
{
bind(is_owner, c_str_pp);
this->operator=(init_val);
}
// bind and set ownership
void bind(bool is_owner, char** c_str_pp)
{
if (c_str_pp == nullptr) {
throw Exception{ EXSTR("cant bind on a nullptr") };
}
if (_is_bound) {
throw Exception{ EXSTR("can only be bound once") };
}
// init
_c_str_pp = c_str_pp;
_is_bound = true;
pEpLogClass(to_string());
set_owner(is_owner);
//if the c_str_p is nullptr then init with ""
if (*_c_str_pp == nullptr) {
this->operator=("");
}
}
~String()
{
if (_is_owner) {
_free(*_c_str_pp);
}
}
// make a copy
String& operator=(const std::string& str)
{
pEpLogClass("Before: " + to_string() + " - new val: '" + pEp::Utils::clip(str, 30) + "'");
if (_c_str_pp == nullptr) {
throw Exception{ EXSTR("invalid state") };
}
// DEALLOCATION
_free(*_c_str_pp);
// ALLOCATION
*_c_str_pp = _copy(str);
pEpLogClass("After: " + to_string());
return *this;
}
// return a copy of whatever c_str currently is (maybe created by us, maybe changed meanwhile)
operator std::string() const
{
if (_c_str_pp == nullptr) {
throw Exception{ EXSTR("invalid state") };
}
if (*_c_str_pp != nullptr) {
return { *_c_str_pp };
}
return {};
}
char** data()
{
return _c_str_pp;
}
const char* c_data() const
{
if (_c_str_pp == nullptr) {
throw Exception{ EXSTR("invalid state") };
}
return *_c_str_pp;
}
bool operator==(const pEp::String& pstr) const
{
return *(pstr.c_data()) == (*c_data());
}
bool operator!=(const pEp::String& pstr) const
{
return !(*this == pstr);
}
std::string to_string() const
{
if (_c_str_pp == nullptr) {
throw Exception{ EXSTR("invalid state") };
}
std::string ret{ "[" + CXX::Inspect::all(_c_str_pp) + " / " +
CXX::Inspect::all(*_c_str_pp) + "]" };
return ret;
}
void set_owner(bool is_owner)
{
pEpLogClass(std::to_string(is_owner));
_is_owner = is_owner;
}
bool is_owner() const
{
return _is_owner;
}
static bool log_enabled;
private:
char* _copy(const std::string& str)
{
char* ret = strdup(str.c_str());
pEpLogClass(CXX::Inspect::all(ret));
return ret;
}
void _free(char* ptr_type)
{
pEpLogClass(CXX::Inspect::all(ptr_type));
::pEp_free(ptr_type);
}
bool _is_bound{ false };
bool _is_owner{ false };
char** _c_str_pp{ nullptr };
Adapter::pEpLog::pEpLogger logger{ "pEp::String", log_enabled };
Adapter::pEpLog::pEpLogger& m4gic_logger_n4me = logger;
class Exception : public std::runtime_error {
public:
explicit Exception(const std::string& msg) : std::runtime_error(msg) {}
};
};
bool pEp::String::log_enabled{ false };
std::ostream& operator<<(std::ostream& o, const pEp::String& pEpStr)
{
return o << std::string(pEpStr);
}
//---------------------------------------------------------------------------------------------
// TOOD:
// ctor not exception safe
// You can create an instance of a c-struct or you can wrap an already existing c-struct
// In both cases, you can define if the wrapper owns the instance, or not.
// If if owns it, it will free() it when the wrapper dies, otherwise it doesnt.
template<typename T>
class PODStruct {
public:
PODStruct() = delete;
// Creates a new instance or binds an existing one
PODStruct(bool is_owner, T** c_struct_pp = nullptr)
{
pEpLogClass("called");
_init(is_owner, c_struct_pp);
}
// Creates a new instance or binds an existing one
// but takes custom alloc/free functions
PODStruct(
bool is_owner,
std::function<T*()> alloc,
std::function<void(T*)> free,
T** c_struct_pp = nullptr) :
_effective_alloc(std::move(alloc)),
_effective_free(std::move(free))
{
_init(is_owner, c_struct_pp);
}
void bind(bool is_owner, T** c_struct_pp)
{
if (c_struct_pp == nullptr) {
throw Exception{ EXSTR("cant bind on a nullptr") };
}
if (_is_bound) {
throw Exception{ EXSTR("can only bind once") };
}
// init
_c_struct_pp = c_struct_pp;
_c_struct_p = *_c_struct_pp;
_is_bound = true;
pEpLogClass(to_string());
set_owner(is_owner);
}
~PODStruct()
{
if (_is_owner) {
_effective_free(_c_struct_p);
}
}
T** data()
{
return _c_struct_pp;
}
const T* c_data() const
{
if (_c_struct_pp == nullptr) {
throw Exception{ EXSTR("invalid state") };
}
return *_c_struct_pp;
}
operator T*()
{
return _c_struct_p;
}
std::string to_string() const
{
std::string ret{ "[" + CXX::Inspect::all(_c_struct_p) + "]" };
return ret;
}
void set_owner(bool is_owner)
{
pEpLogClass(std::to_string(is_owner));
_is_owner = is_owner;
}
bool is_owner() const
{
return _is_owner;
}
static bool log_enabled;
Adapter::pEpLog::pEpLogger logger{ typeid(T).name(), log_enabled };
protected:
Adapter::pEpLog::pEpLogger& m4gic_logger_n4me = logger;
private:
const std::function<T*()> _effective_alloc{ std::bind(&PODStruct::_alloc, this) };
const std::function<void(T*)> _effective_free{
std::bind(&PODStruct::_free, this, std::placeholders::_1)
};
bool _is_bound{ false };
bool _is_owner{ false };
T** _c_struct_pp{ nullptr };
T* _c_struct_p{ nullptr };
void _init(bool is_owner, T** c_struct_pp)
{
// if no pp is given, alloc new,
// otherwise bind to it
if (!c_struct_pp) {
_c_struct_p = _effective_alloc();
bind(is_owner, &_c_struct_p);
} else {
bind(is_owner, c_struct_pp);
}
}
// default alloc/free
T* _alloc()
{
T* ret = (T*)calloc(1, sizeof(T));
pEpLogClass(CXX::Inspect::all(ret));
return ret;
}
void _free(T* ptr)
{
pEpLogClass(CXX::Inspect::all(ptr));
free(ptr);
}
class Exception : public std::runtime_error {
public:
explicit Exception(const std::string& msg) : std::runtime_error(msg) {}
};
};
//---------------------------------------------------------------------------------------------
// TOOD:
// ctor not exception safe
// You can create an instance of a c-struct or you can wrap an already existing c-struct
// In both cases, you can define if the wrapper owns the instance, or not.
// If if owns it, it will free() it when the wrapper dies, otherwise it doesnt.
//
// alloc/free:
// the alloc() and free() functions HAVE to use calloc/malloc NOT 'new', because we are interfacing
// c99 code that could possibly get ownership of the struct and can only free memory that has
// been allocated using malloc/calloc. (malloc/free - new/delete are NOT compatible)
class TestStruct1 : public PODStruct<::Test_struct1> {
public:
TestStruct1(bool is_owner, ::Test_struct1** test_struct1_p = nullptr) :
PODStruct<::Test_struct1>(is_owner, test_struct1_p)
{
}
// fields of the struct as 'properties' ;)
pEp::POD<int> c_int{ &(*data())->c_int };
pEp::POD<::Test_enum> c_enum{ &(*data())->c_enum };
pEp::String c_str{ is_owner(), &(*data())->c_str };
};
template<>
bool PODStruct<::Test_struct1>::log_enabled{ false };
} // namespace pEp

540
test/test_nr1.cc

@ -9,299 +9,8 @@
#include <pEp/pEpLog.hh>
#include <pEp/inspect.hh>
#include <type_traits>
#include "../examples/libc99/libc99.h"
namespace pEp {
//---------------------------------------------------------------------------------------------
#define EXSTR(msg) std::string(__FUNCTION__) + "() - " + msg
template<class T>
class POD {
static_assert(
std::is_pod<T>::value && !std::is_pointer<T>::value,
"only POD value types supported");
public:
POD()
{
pEpLogClass("called");
};
explicit POD(T* pod_p, const T& init_val)
{
bind(pod_p, init_val);
}
// Best to use this constructor, as the object is in a valid state when the wrapped
// c_str (char*) is known
explicit POD(T* pod_p)
{
bind(pod_p);
}
void bind(T* pod_p, const T& init_val)
{
bind(pod_p);
this->operator=(init_val);
}
void bind(T* pod_p)
{
if (pod_p == nullptr) {
throw Exception{ EXSTR("cant bind on a nullptr") };
}
if (_is_initialized) {
throw Exception{ EXSTR("can only bind once") };
}
// init
_pod_p = pod_p;
_is_initialized = true;
pEpLogClass(to_string());
}
// make a copy
POD& operator=(T value)
{
pEpLogClass("Before: " + to_string() + " - new val: '" + CXX::Inspect::val(value) + "'");
*_pod_p = value;
// pEpLogClass("After: " + to_string());
return *this;
}
// return a copy
operator T() const
{
return *_pod_p;
}
// return address of the wrappee
T* data()
{
return _pod_p;
}
// return address of the wrappee (const)
const T* c_data() const
{
return _pod_p;
}
std::string to_string() const
{
std::string ret{ "[" + CXX::Inspect::all(_pod_p) + "]" };
return ret;
}
static bool log_enabled;
private:
bool _is_initialized{ false };
T* _pod_p{ nullptr };
Adapter::pEpLog::pEpLogger logger{ "pEp::POD", log_enabled };
Adapter::pEpLog::pEpLogger& m4gic_logger_n4me = logger;
class Exception : public std::runtime_error {
public:
explicit Exception(const std::string& msg) : std::runtime_error(msg) {}
};
};
template<class T>
bool pEp::POD<T>::log_enabled{ true };
template<class T>
std::ostream& operator<<(std::ostream& o, pEp::POD<T> pEpPOD)
{
return o << (T)pEpPOD;
}
//---------------------------------------------------------------------------------------------
// Manages a char* to appear like a std::string
// char* c_str
// c_str MUST point to either:
// * NULL - means there is no value/mem alloc yet
// * dyn allocated memory
// in case the mem is already allocated, there must be 2 modes
// * take ownership (and free it)
// * dont take ownership, only provide a C++ interface
// There must be the option to construct the object before the wrapped c_str (char*)
// is known. This leads to a pEp::String object in invalid state, and it must be initialized using
// bind(). Otherwise all(most) functions will throw.
// bind() can only be called once, will throw otherwise.
// NON-owning mode, just does not free the string when it dies, owning mode does.
// Thats the only difference.
class String {
public:
String()
{
pEpLogClass("called");
}
// Best to use this constructor, as the object is in a valid state when the wrapped
// c_str (char*) is known
String(bool is_owner, char** c_str_pp)
{
bind(is_owner, c_str_pp);
}
String(bool is_owner, char** c_str_pp, const std::string& init_val)
{
bind(is_owner, c_str_pp, init_val);
}
void bind(bool is_owner, char** c_str_pp, const std::string& init_val)
{
bind(is_owner, c_str_pp);
this->operator=(init_val);
}
// bind and set ownership
void bind(bool is_owner, char** c_str_pp)
{
if (c_str_pp == nullptr) {
throw Exception{ EXSTR("cant bind on a nullptr") };
}
if (_is_bound) {
throw Exception{ EXSTR("can only be bound once") };
}
// init
_c_str_pp = c_str_pp;
_is_bound = true;
pEpLogClass(to_string());
set_owner(is_owner);
//if the c_str_p is nullptr then init with ""
if (*_c_str_pp == nullptr) {
this->operator=("");
}
}
~String()
{
if (_is_owner) {
_free(*_c_str_pp);
}
}
// make a copy
String& operator=(const std::string& str)
{
pEpLogClass("Before: " + to_string() + " - new val: '" + pEp::Utils::clip(str, 30) + "'");
if (_c_str_pp == nullptr) {
throw Exception{ EXSTR("invalid state") };
}
// DEALLOCATION
_free(*_c_str_pp);
// ALLOCATION
*_c_str_pp = _copy(str);
pEpLogClass("After: " + to_string());
return *this;
}
// return a copy of whatever c_str currently is (maybe created by us, maybe changed meanwhile)
operator std::string() const
{
if (_c_str_pp == nullptr) {
throw Exception{ EXSTR("invalid state") };
}
if (*_c_str_pp != nullptr) {
return { *_c_str_pp };
}
return {};
}
char** data()
{
return _c_str_pp;
}
const char* c_data() const
{
if (_c_str_pp == nullptr) {
throw Exception{ EXSTR("invalid state") };
}
return *_c_str_pp;
}
bool operator==(const pEp::String& pstr) const
{
return *(pstr.c_data()) == (*c_data());
}
bool operator!=(const pEp::String& pstr) const
{
return !(*this == pstr);
}
std::string to_string() const
{
if (_c_str_pp == nullptr) {
throw Exception{ EXSTR("invalid state") };
}
std::string ret{ "[" + CXX::Inspect::all(_c_str_pp) + " / " +
CXX::Inspect::all(*_c_str_pp) + "]" };
return ret;
}
void set_owner(bool is_owner)
{
pEpLogClass(std::to_string(is_owner));
_is_owner = is_owner;
}
bool is_owner() const
{
return _is_owner;
}
static bool log_enabled;
private:
static char* _copy(const std::string& str)
{
char* ret = strdup(str.c_str());
pEpLog(CXX::Inspect::all(ret));
return ret;
}
static void _free(char* ptr_type)
{
pEpLog(CXX::Inspect::all(ptr_type));
::pEp_free(ptr_type);
}
bool _is_bound{ false };
bool _is_owner{ false };
char** _c_str_pp{ nullptr };
Adapter::pEpLog::pEpLogger logger{ "pEp::String", log_enabled };
Adapter::pEpLog::pEpLogger& m4gic_logger_n4me = logger;
class Exception : public std::runtime_error {
public:
explicit Exception(const std::string& msg) : std::runtime_error(msg) {}
};
};
bool pEp::String::log_enabled{ true };
std::ostream& operator<<(std::ostream& o, const pEp::String& pEpStr)
{
return o << std::string(pEpStr);
}
} // namespace pEp
//#include "../examples/libc99/libc99.h"
#include "../src/prototype.hh"
void test_getters(int* c_int_p, pEp::POD<int>& pint, int expected)
{
@ -392,171 +101,6 @@ void test_assign(char** c_str_p, pEp::String& pstr)
}
namespace pEp {
// TOOD:
// ctor not exception safe
// You can create an instance of a c-struct or you can wrap an already existing c-struct
// In both cases, you can define if the wrapper owns the instance, or not.
// If if owns it, it will free() it when the wrapper dies, otherwise it doesnt.
template<typename T>
class PODStruct {
public:
PODStruct() = delete;
// Creates a new instance or binds an existing one
PODStruct(bool is_owner, T** c_struct_pp = nullptr)
{
pEpLogClass("called");
_init(is_owner, c_struct_pp);
}
// Creates a new instance or binds an existing one
// but takes custom alloc/free functions
PODStruct(
bool is_owner,
std::function<T*()> alloc,
std::function<void(T*)> free,
T** c_struct_pp = nullptr) :
_effective_alloc(std::move(alloc)),
_effective_free(std::move(free))
{
_init(is_owner, c_struct_pp);
}
void bind(bool is_owner, T** c_struct_pp)
{
if (c_struct_pp == nullptr) {
throw Exception{ EXSTR("cant bind on a nullptr") };
}
if (_is_bound) {
throw Exception{ EXSTR("can only bind once") };
}
// init
_c_struct_pp = c_struct_pp;
_c_struct_p = *_c_struct_pp;
_is_bound = true;
pEpLogClass(to_string());
set_owner(is_owner);
}
~PODStruct()
{
if (_is_owner) {
_effective_free(_c_struct_p);
}
}
T** data()
{
return _c_struct_pp;
}
const T* c_data() const
{
if (_c_struct_pp == nullptr) {
throw Exception{ EXSTR("invalid state") };
}
return *_c_struct_pp;
}
operator T*()
{
return _c_struct_p;
}
std::string to_string() const
{
std::string ret{ "[" + CXX::Inspect::all(_c_struct_p) + "]" };
return ret;
}
void set_owner(bool is_owner)
{
pEpLogClass(std::to_string(is_owner));
_is_owner = is_owner;
}
bool is_owner() const
{
return _is_owner;
}
static bool log_enabled;
private:
const std::function<T*()> _effective_alloc{ _alloc };
const std::function<void(T*)> _effective_free{ _free };
bool _is_bound{ false };
bool _is_owner{ false };
T** _c_struct_pp{ nullptr };
T* _c_struct_p{ nullptr };
Adapter::pEpLog::pEpLogger logger{ typeid(T).name(), log_enabled };
Adapter::pEpLog::pEpLogger& m4gic_logger_n4me = logger;
class Exception : public std::runtime_error {
public:
explicit Exception(const std::string& msg) : std::runtime_error(msg) {}
};
void _init(bool is_owner, T** c_struct_pp)
{
// if no pp is given, alloc new,
// otherwise bind to it
if (!c_struct_pp) {
_c_struct_p = _effective_alloc();
bind(is_owner, &_c_struct_p);
} else {
bind(is_owner, c_struct_pp);
}
}
// default alloc/free
static T* _alloc()
{
T* ret = (T*)calloc(1, sizeof(T));
pEpLog(CXX::Inspect::all(ret));
return ret;
}
static void _free(T* ptr)
{
pEpLog(CXX::Inspect::all(ptr));
free(ptr);
}
};
// TOOD:
// ctor not exception safe
// You can create an instance of a c-struct or you can wrap an already existing c-struct
// In both cases, you can define if the wrapper owns the instance, or not.
// If if owns it, it will free() it when the wrapper dies, otherwise it doesnt.
//
// alloc/free:
// the alloc() and free() functions HAVE to use calloc/malloc NOT 'new', because we are interfacing
// c99 code that could possibly get ownership of the struct and can only free memory that has
// been allocated using malloc/calloc. (malloc/free - new/delete are NOT compatible)
class TestStruct1 : public PODStruct<::Test_struct1> {
public:
TestStruct1(bool is_owner, ::Test_struct1** test_struct1_p = nullptr) :
PODStruct<::Test_struct1>(is_owner, test_struct1_p)
{
}
// fields of the struct as 'properties' ;)
pEp::POD<int> c_int{ &(*data())->c_int };
pEp::POD<::Test_enum> c_enum{ &(*data())->c_enum };
pEp::String c_str{ true, &(*data())->c_str };
};
template<>
bool PODStruct<::Test_struct1>::log_enabled{ true };
} // namespace pEp
int main()
{
// c-types are always POD
@ -575,7 +119,7 @@ int main()
pEp::Adapter::pEpLog::set_enabled(true);
// POD
if (0) {
if (1) {
// VALID USAGE
int init_val = 0;
// new pEp::POD on int
@ -602,7 +146,7 @@ int main()
// String
// pEp::String::log_enabled = false;
if (0) {
if (1) {
//TODO: Test non-owning mode
// INVALID USAGE
@ -700,80 +244,4 @@ int main()
pEpLog("DONE");
}
}
// if (0) {
// ::PEP_SESSION session;
// setenv("HOME", ".", 1);
// ::init(&session, nullptr, nullptr, nullptr);
//
// // create identity
// pEp::Identity id1{ "wrong@entry.lol", "wrong", "23", "INVA_FPR" };
//
// pEpLog(id1);
// id1.username = "alice";
// id1.address = "alice@peptest.org";
//
// pEpLog(id1.address);
// pEpLog(id1.username);
// ::myself(session, id1);
// pEpLog(id1);
//
// pEp::Identity id2{ "bob" };
// ::update_identity(session, id2);
// pEpLog(id2);
// }
}
//---------------------------------------------------------------------------------------------
//
//// TOOD:
//// ctor not exception safe
//class Identity {
//public:
// Identity(
// const std::string& address = "",
// const std::string& username = "",
// const std::string& user_id = "",
// const std::string& fpr = "")
// {
// pEpLogClass("called");
// _wrappee = ::new_identity(nullptr, nullptr, nullptr, nullptr);
//
// // set the pEp::String wrapper underlying c_str
// this->address.bind(true, &_wrappee->address, address);
// this->username.bind(true, &_wrappee->username, username);
// this->user_id.bind(true, &_wrappee->user_id, user_id);
// this->fpr.bind(true, &_wrappee->fpr, fpr);
// }
//
// ~Identity()
// {
// _free();
// }
//
//
// pEp::String address{};
// pEp::String username{};
// pEp::String user_id{};
// pEp::String fpr{};
//
// operator ::pEp_identity*()
// {
// return _wrappee;
// }
//
// static bool log_enabled;
//
//private:
// void _free()
// {
// // pEp::free(_wrappee);
// }
//
// ::pEp_identity* _wrappee{ nullptr };
// Adapter::pEpLog::pEpLogger logger{ "IdentWrappySP", log_enabled };
// Adapter::pEpLog::pEpLogger& m4gic_logger_n4me = logger;
//};
//
//bool Identity::log_enabled{ true };
//

Loading…
Cancel
Save