Browse Source

Implement gmp_import() and gmp_export()

pull/807/head
Leigh 12 years ago
committed by Nikita Popov
parent
commit
47d630e7f0
  1. 1
      NEWS
  2. 2
      UPGRADING
  3. 178
      ext/gmp/gmp.c
  4. 2
      ext/gmp/php_gmp.h
  5. 151
      ext/gmp/tests/import-export.phpt

1
NEWS

@ -22,6 +22,7 @@ PHP NEWS
- GMP:
. Fixed bug #67917 (Using GMP objects with overloaded operators can cause
memory exhaustion). (Nikita)
. Implemented gmp_import() and gmp_export(). (Leigh, Nikita)
- MySQLi:
. Fixed bug #67839 (mysqli does not handle 4-byte floats correctly). (Keyur)

2
UPGRADING

@ -267,6 +267,8 @@ PHP 5.6 UPGRADE NOTES
- GMP:
Added gmp_root($a, $nth) and gmp_rootrem($a, $nth) for calculating nth roots.
Added gmp_import($data, $word_size = 1, $options = GMP_MSW_FIRST | GMP_NATIVE_ENDIAN) in PHP 5.6.1.
Added gmp_export($gmpnumber, $word_size = 1, $options = GMP_MSW_FIRST | GMP_NATIVE_ENDIAN) in PHP 5.6.1.
- Hash
Added hash_equals($known_string, $user_string)

178
ext/gmp/gmp.c

@ -43,6 +43,18 @@ ZEND_BEGIN_ARG_INFO_EX(arginfo_gmp_init, 0, 0, 1)
ZEND_ARG_INFO(0, base)
ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_INFO_EX(arginfo_gmp_import, 0, 0, 1)
ZEND_ARG_INFO(0, data)
ZEND_ARG_INFO(0, word_size)
ZEND_ARG_INFO(0, options)
ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_INFO_EX(arginfo_gmp_export, 0, 0, 1)
ZEND_ARG_INFO(0, gmpnumber)
ZEND_ARG_INFO(0, word_size)
ZEND_ARG_INFO(0, options)
ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_INFO_EX(arginfo_gmp_intval, 0, 0, 1)
ZEND_ARG_INFO(0, gmpnumber)
ZEND_END_ARG_INFO()
@ -117,6 +129,8 @@ static ZEND_GINIT_FUNCTION(gmp);
*/
const zend_function_entry gmp_functions[] = {
ZEND_FE(gmp_init, arginfo_gmp_init)
ZEND_FE(gmp_import, arginfo_gmp_import)
ZEND_FE(gmp_export, arginfo_gmp_export)
ZEND_FE(gmp_intval, arginfo_gmp_intval)
ZEND_FE(gmp_strval, arginfo_gmp_strval)
ZEND_FE(gmp_add, arginfo_gmp_binary)
@ -204,6 +218,12 @@ typedef struct _gmp_temp {
#define GMP_ROUND_PLUSINF 1
#define GMP_ROUND_MINUSINF 2
#define GMP_MSW_FIRST (1 << 0)
#define GMP_LSW_FIRST (1 << 1)
#define GMP_LITTLE_ENDIAN (1 << 2)
#define GMP_BIG_ENDIAN (1 << 3)
#define GMP_NATIVE_ENDIAN (1 << 4)
#define GMP_42_OR_NEWER \
((__GNU_MP_VERSION >= 5) || (__GNU_MP_VERSION >= 4 && __GNU_MP_VERSION_MINOR >= 2))
@ -297,7 +317,7 @@ static void gmp_strval(zval *result, mpz_t gmpnum, long base);
static int convert_to_gmp(mpz_t gmpnumber, zval *val, int base TSRMLS_DC);
static void gmp_cmp(zval *return_value, zval *a_arg, zval *b_arg TSRMLS_DC);
/*
/*
* The gmp_*_op functions provide an implementation for several common types
* of GMP functions. The gmp_zval_(unary|binary)_*_op functions have to be manually
* passed zvals to work on, whereas the gmp_(unary|binary)_*_op macros already
@ -599,7 +619,7 @@ static int gmp_serialize(zval *object, unsigned char **buffer, zend_uint *buf_le
PHP_VAR_SERIALIZE_INIT(serialize_data);
INIT_PZVAL(zv_ptr);
gmp_strval(zv_ptr, gmpnum, 10);
php_var_serialize(&buf, &zv_ptr, &serialize_data TSRMLS_CC);
zval_dtor(zv_ptr);
@ -697,6 +717,12 @@ ZEND_MINIT_FUNCTION(gmp)
#endif
REGISTER_STRING_CONSTANT("GMP_VERSION", (char *)gmp_version, CONST_CS | CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("GMP_MSW_FIRST", GMP_MSW_FIRST, CONST_CS | CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("GMP_LSW_FIRST", GMP_LSW_FIRST, CONST_CS | CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("GMP_LITTLE_ENDIAN", GMP_LITTLE_ENDIAN, CONST_CS | CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("GMP_BIG_ENDIAN", GMP_BIG_ENDIAN, CONST_CS | CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("GMP_NATIVE_ENDIAN", GMP_NATIVE_ENDIAN, CONST_CS | CONST_PERSISTENT);
mp_set_memory_functions(gmp_emalloc, gmp_erealloc, gmp_efree);
return SUCCESS;
@ -734,7 +760,7 @@ ZEND_MODULE_INFO_D(gmp)
/* {{{ convert_to_gmp
* Convert zval to be gmp number */
static int convert_to_gmp(mpz_t gmpnumber, zval *val, int base TSRMLS_DC)
static int convert_to_gmp(mpz_t gmpnumber, zval *val, int base TSRMLS_DC)
{
switch (Z_TYPE_P(val)) {
case IS_LONG:
@ -788,8 +814,8 @@ static void gmp_strval(zval *result, mpz_t gmpnum, long base) /* {{{ */
out_string = emalloc(num_len + 1);
mpz_get_str(out_string, base, gmpnum);
/*
/*
* From GMP documentation for mpz_sizeinbase():
* The returned value will be exact or 1 too big. If base is a power of
* 2, the returned value will always be exact.
@ -831,7 +857,7 @@ static void gmp_cmp(zval *return_value, zval *a_arg, zval *b_arg TSRMLS_DC) /* {
FREE_GMP_TEMP(temp_a);
FREE_GMP_TEMP(temp_b);
RETURN_LONG(res);
}
/* }}} */
@ -839,14 +865,14 @@ static void gmp_cmp(zval *return_value, zval *a_arg, zval *b_arg TSRMLS_DC) /* {
/* {{{ gmp_zval_binary_ui_op
Execute GMP binary operation.
*/
static inline void gmp_zval_binary_ui_op(zval *return_value, zval *a_arg, zval *b_arg, gmp_binary_op_t gmp_op, gmp_binary_ui_op_t gmp_ui_op, int check_b_zero TSRMLS_DC)
static inline void gmp_zval_binary_ui_op(zval *return_value, zval *a_arg, zval *b_arg, gmp_binary_op_t gmp_op, gmp_binary_ui_op_t gmp_ui_op, int check_b_zero TSRMLS_DC)
{
mpz_ptr gmpnum_a, gmpnum_b, gmpnum_result;
int use_ui = 0;
gmp_temp_t temp_a, temp_b;
FETCH_GMP_ZVAL(gmpnum_a, a_arg, temp_a);
if (gmp_ui_op && Z_TYPE_P(b_arg) == IS_LONG && Z_LVAL_P(b_arg) >= 0) {
use_ui = 1;
temp_b.is_used = 0;
@ -942,7 +968,7 @@ static inline void _gmp_binary_ui_op(INTERNAL_FUNCTION_PARAMETERS, gmp_binary_op
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "zz", &a_arg, &b_arg) == FAILURE){
return;
}
gmp_zval_binary_ui_op(return_value, a_arg, b_arg, gmp_op, gmp_ui_op, check_b_zero TSRMLS_CC);
}
/* }}} */
@ -951,11 +977,11 @@ static inline void _gmp_binary_ui_op(INTERNAL_FUNCTION_PARAMETERS, gmp_binary_op
/* {{{ gmp_zval_unary_op
*/
static inline void gmp_zval_unary_op(zval *return_value, zval *a_arg, gmp_unary_op_t gmp_op TSRMLS_DC)
static inline void gmp_zval_unary_op(zval *return_value, zval *a_arg, gmp_unary_op_t gmp_op TSRMLS_DC)
{
mpz_ptr gmpnum_a, gmpnum_result;
gmp_temp_t temp_a;
FETCH_GMP_ZVAL(gmpnum_a, a_arg, temp_a);
INIT_GMP_RETVAL(gmpnum_result);
@ -1000,7 +1026,7 @@ static inline void _gmp_unary_op(INTERNAL_FUNCTION_PARAMETERS, gmp_unary_op_t gm
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", &a_arg) == FAILURE){
return;
}
gmp_zval_unary_op(return_value, a_arg, gmp_op TSRMLS_CC);
}
/* }}} */
@ -1016,7 +1042,7 @@ static inline void _gmp_unary_opl(INTERNAL_FUNCTION_PARAMETERS, gmp_unary_opl_t
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", &a_arg) == FAILURE){
return;
}
FETCH_GMP_ZVAL(gmpnum_a, a_arg, temp_a);
RETVAL_LONG(gmp_op(gmpnum_a));
FREE_GMP_TEMP(temp_a);
@ -1070,6 +1096,118 @@ ZEND_FUNCTION(gmp_init)
}
/* }}} */
int gmp_import_export_validate(long size, long options, int *order, int *endian TSRMLS_DC)
{
if (size < 1) {
php_error_docref(NULL TSRMLS_CC, E_WARNING,
"Word size must be positive, %ld given", size);
return FAILURE;
}
switch (options & (GMP_LSW_FIRST | GMP_MSW_FIRST)) {
case GMP_LSW_FIRST:
*order = -1;
break;
case GMP_MSW_FIRST:
case 0: /* default */
*order = 1;
break;
default:
php_error_docref(NULL TSRMLS_CC, E_WARNING,
"Invalid options: Conflicting word orders");
return FAILURE;
}
switch (options & (GMP_LITTLE_ENDIAN | GMP_BIG_ENDIAN | GMP_NATIVE_ENDIAN)) {
case GMP_LITTLE_ENDIAN:
*endian = -1;
break;
case GMP_BIG_ENDIAN:
*endian = 1;
break;
case GMP_NATIVE_ENDIAN:
case 0: /* default */
*endian = 0;
break;
default:
php_error_docref(NULL TSRMLS_CC, E_WARNING,
"Invalid options: Conflicting word endianness");
return FAILURE;
}
return SUCCESS;
}
/* {{{ proto GMP gmp_import(string data [, int word_size = 1, int options = GMP_MSW_FIRST | GMP_NATIVE_ENDIAN])
Imports a GMP number from a binary string */
ZEND_FUNCTION(gmp_import)
{
char *data;
int data_len;
long size = 1;
long options = GMP_MSW_FIRST | GMP_NATIVE_ENDIAN;;
int order, endian;
mpz_ptr gmpnumber;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s|ll", &data, &data_len, &size, &options) == FAILURE) {
return;
}
if (gmp_import_export_validate(size, options, &order, &endian TSRMLS_CC) == FAILURE) {
RETURN_FALSE;
}
if ((data_len % size) != 0) {
php_error_docref(NULL TSRMLS_CC, E_WARNING,
"Input length must be a multiple of word size");
RETURN_FALSE;
}
INIT_GMP_RETVAL(gmpnumber);
mpz_import(gmpnumber, data_len / size, order, size, endian, 0, data);
}
/* }}} */
/* {{{ proto string gmp_export(GMP gmpnumber [, int word_size = 1, int options = GMP_MSW_FIRST | GMP_NATIVE_ENDIAN])
Exports a GMP number to a binary string */
ZEND_FUNCTION(gmp_export)
{
zval *gmpnumber_arg;
long size = 1;
long options = GMP_MSW_FIRST | GMP_NATIVE_ENDIAN;;
int order, endian;
mpz_ptr gmpnumber;
gmp_temp_t temp_a;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z|ll", &gmpnumber_arg, &size, &options) == FAILURE) {
return;
}
if (gmp_import_export_validate(size, options, &order, &endian TSRMLS_CC) == FAILURE) {
RETURN_FALSE;
}
FETCH_GMP_ZVAL(gmpnumber, gmpnumber_arg, temp_a);
if (mpz_sgn(gmpnumber) == 0) {
ZVAL_STRING(return_value, "", 1);
} else {
size_t bits_per_word = size * 8;
size_t count = (mpz_sizeinbase(gmpnumber, 2) + bits_per_word - 1) / bits_per_word;
size_t out_len = count * size;
char *out_string = emalloc(out_len + 1);
mpz_export(out_string, NULL, order, size, endian, 0, gmpnumber);
out_string[out_len] = '\0';
ZVAL_STRINGL(return_value, out_string, out_len, 0);
}
FREE_GMP_TEMP(temp_a);
}
/* }}} */
/* {{{ proto int gmp_intval(mixed gmpnumber)
Gets signed long value of GMP number */
ZEND_FUNCTION(gmp_intval)
@ -1079,7 +1217,7 @@ ZEND_FUNCTION(gmp_intval)
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", &gmpnumber_arg) == FAILURE){
return;
}
if (IS_GMP(gmpnumber_arg)) {
RETVAL_LONG(mpz_get_si(GET_GMP_FROM_ZVAL(gmpnumber_arg)));
} else {
@ -1226,7 +1364,7 @@ ZEND_FUNCTION(gmp_div_q)
php_error_docref(NULL TSRMLS_CC, E_WARNING, "Invalid rounding mode");
RETURN_FALSE;
}
}
/* }}} */
@ -1306,7 +1444,7 @@ ZEND_FUNCTION(gmp_pow)
php_error_docref(NULL TSRMLS_CC, E_WARNING, "Negative exponent not supported");
RETURN_FALSE;
}
INIT_GMP_RETVAL(gmpnum_result);
if (Z_TYPE_P(base_arg) == IS_LONG && Z_LVAL_P(base_arg) >= 0) {
mpz_ui_pow_ui(gmpnum_result, Z_LVAL_P(base_arg), exp);
@ -1380,14 +1518,14 @@ ZEND_FUNCTION(gmp_sqrt)
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", &a_arg) == FAILURE){
return;
}
FETCH_GMP_ZVAL(gmpnum_a, a_arg, temp_a);
if (mpz_sgn(gmpnum_a) < 0) {
php_error_docref(NULL TSRMLS_CC, E_WARNING, "Number has to be greater than or equal to 0");
FREE_GMP_TEMP(temp_a);
RETURN_FALSE;
}
}
INIT_GMP_RETVAL(gmpnum_result);
mpz_sqrt(gmpnum_result, gmpnum_a);
@ -1414,7 +1552,7 @@ ZEND_FUNCTION(gmp_sqrtrem)
FREE_GMP_TEMP(temp_a);
RETURN_FALSE;
}
array_init(return_value);
add_index_zval(return_value, 0, gmp_create(&gmpnum_result1 TSRMLS_CC));
add_index_zval(return_value, 1, gmp_create(&gmpnum_result2 TSRMLS_CC));
@ -1494,7 +1632,7 @@ ZEND_FUNCTION(gmp_rootrem)
mpz_sub(gmpnum_result2, gmpnum_a, gmpnum_result2);
mpz_abs(gmpnum_result2, gmpnum_result2);
#endif
FREE_GMP_TEMP(temp_a);
}
/* }}} */

2
ext/gmp/php_gmp.h

@ -31,6 +31,8 @@ ZEND_MODULE_DEACTIVATE_D(gmp);
ZEND_MODULE_INFO_D(gmp);
ZEND_FUNCTION(gmp_init);
ZEND_FUNCTION(gmp_import);
ZEND_FUNCTION(gmp_export);
ZEND_FUNCTION(gmp_intval);
ZEND_FUNCTION(gmp_strval);
ZEND_FUNCTION(gmp_add);

151
ext/gmp/tests/import-export.phpt

@ -0,0 +1,151 @@
--TEST--
Check gmp_import and gmp_export behave as intended
--SKIPIF--
<?php if (!extension_loaded("gmp")) echo "skip"; ?>
--FILE--
<?php
// Tests taken from GMPs own test suite.
// format is [expected, size, options, input]
$import = [
['0',1,GMP_BIG_ENDIAN,''],
['12345678',1,GMP_BIG_ENDIAN,'12345678'],
['12345678',4,GMP_BIG_ENDIAN,'12345678'],
['12345678',4,GMP_LSW_FIRST | GMP_BIG_ENDIAN,'12345678'],
['12345678',1,GMP_LSW_FIRST | GMP_LITTLE_ENDIAN,'78563412'],
['12345678',4,GMP_LITTLE_ENDIAN,'78563412'],
['12345678',4,GMP_LSW_FIRST | GMP_LITTLE_ENDIAN,'78563412'],
['123456789abc',2,GMP_BIG_ENDIAN,'123456789abc'],
['123456789abc',2,GMP_LSW_FIRST | GMP_BIG_ENDIAN,'9abc56781234'],
['123456789abc',2,GMP_LITTLE_ENDIAN,'34127856bc9a'],
['123456789abc',2,GMP_LSW_FIRST | GMP_LITTLE_ENDIAN,'bc9a78563412'],
['112233445566778899aabbcc',4,GMP_BIG_ENDIAN,'112233445566778899aabbcc'],
['112233445566778899aabbcc',4,GMP_LSW_FIRST | GMP_BIG_ENDIAN,'99aabbcc5566778811223344'],
['112233445566778899aabbcc',4,GMP_LITTLE_ENDIAN,'4433221188776655ccbbaa99'],
['112233445566778899aabbcc',4,GMP_LSW_FIRST | GMP_LITTLE_ENDIAN,'ccbbaa998877665544332211'],
['100120023003400450056006700780089009a00ab00bc00c',8,GMP_BIG_ENDIAN,'100120023003400450056006700780089009a00ab00bc00c'],
['100120023003400450056006700780089009a00ab00bc00c',8,GMP_LSW_FIRST | GMP_BIG_ENDIAN,'9009a00ab00bc00c50056006700780081001200230034004'],
['100120023003400450056006700780089009a00ab00bc00c',8,GMP_LITTLE_ENDIAN,'044003300220011008800770066005500cc00bb00aa00990'],
['100120023003400450056006700780089009a00ab00bc00c',8,GMP_LSW_FIRST | GMP_LITTLE_ENDIAN,'0cc00bb00aa0099008800770066005500440033002200110']
];
// format is [output, size, options, expected]
$export = [
['0',1,GMP_BIG_ENDIAN,''],
['0',2,GMP_BIG_ENDIAN,''],
['0',3,GMP_BIG_ENDIAN,''],
['12345678',1,GMP_BIG_ENDIAN,'12345678'],
['12345678',4,GMP_BIG_ENDIAN,'12345678'],
['12345678',4,GMP_LSW_FIRST | GMP_BIG_ENDIAN,'12345678'],
['12345678',1,GMP_LSW_FIRST | GMP_LITTLE_ENDIAN,'78563412'],
['12345678',4,GMP_LITTLE_ENDIAN,'78563412'],
['12345678',4,GMP_LSW_FIRST | GMP_LITTLE_ENDIAN,'78563412'],
['123456789ABC',2,GMP_BIG_ENDIAN,'123456789abc'],
['123456789ABC',2,GMP_LSW_FIRST | GMP_BIG_ENDIAN,'9abc56781234'],
['123456789ABC',2,GMP_LITTLE_ENDIAN,'34127856bc9a'],
['123456789ABC',2,GMP_LSW_FIRST | GMP_LITTLE_ENDIAN,'bc9a78563412'],
['112233445566778899AABBCC',4,GMP_BIG_ENDIAN,'112233445566778899aabbcc'],
['112233445566778899AABBCC',4,GMP_LSW_FIRST | GMP_BIG_ENDIAN,'99aabbcc5566778811223344'],
['112233445566778899AABBCC',4,GMP_LITTLE_ENDIAN,'4433221188776655ccbbaa99'],
['112233445566778899AABBCC',4,GMP_LSW_FIRST | GMP_LITTLE_ENDIAN,'ccbbaa998877665544332211'],
['100120023003400450056006700780089009A00AB00BC00C',8,GMP_BIG_ENDIAN,'100120023003400450056006700780089009a00ab00bc00c'],
['100120023003400450056006700780089009A00AB00BC00C',8,GMP_LSW_FIRST | GMP_BIG_ENDIAN,'9009a00ab00bc00c50056006700780081001200230034004'],
['100120023003400450056006700780089009A00AB00BC00C',8,GMP_LITTLE_ENDIAN,'044003300220011008800770066005500cc00bb00aa00990'],
['100120023003400450056006700780089009A00AB00BC00C',8,GMP_LSW_FIRST | GMP_LITTLE_ENDIAN,'0cc00bb00aa0099008800770066005500440033002200110']
];
echo "Import:\n";
$passed = true;
foreach ($import as $k => $test) {
$gmp = gmp_import(hex2bin($test[3]), $test[1], $test[2]);
if ($gmp instanceof GMP) {
$result = gmp_strval($gmp, 16);
if ($result !== $test[0]) {
echo "$k: '$result' !== '{$test[0]}'\n";
$passed = false;
}
} else {
$type = gettype($gmp);
echo "$k: $type !== '{$test[0]}'\n";
}
}
var_dump($passed);
echo "\nExport:\n";
$passed = true;
foreach ($export as $k => $test) {
$gmp = gmp_init($test[0], 16);
$str = gmp_export($gmp, $test[1], $test[2]);
if (is_string($str)) {
$result = bin2hex($str);
if ($result !== $test[3]) {
echo "$k: '$result' !== '{$test[3]}'\n";
$passed = false;
}
} else {
$type = gettype($str);
echo "$k: $type !== '{$test[3]}'\n";
}
}
var_dump($passed);
// Invalid word sizes
var_dump(gmp_import('a', -1));
var_dump(gmp_import('a', 0));
var_dump(gmp_export(123, -1));
var_dump(gmp_export(123, 0));
// Invalid data lengths
var_dump(gmp_import('a', 2));
var_dump(gmp_import('aa', 3));
var_dump(gmp_import(str_repeat('a', 100), 64));
// Invalid options
var_dump(gmp_import('a', 1, GMP_MSW_FIRST | GMP_LSW_FIRST));
var_dump(gmp_import('a', 1, GMP_BIG_ENDIAN | GMP_LITTLE_ENDIAN));
var_dump(gmp_export(123, 1, GMP_MSW_FIRST | GMP_LSW_FIRST));
var_dump(gmp_export(123, 1, GMP_BIG_ENDIAN | GMP_LITTLE_ENDIAN));
--EXPECTF--
Import:
bool(true)
Export:
bool(true)
Warning: gmp_import(): Word size must be positive, -1 given in %s on line %d
bool(false)
Warning: gmp_import(): Word size must be positive, 0 given in %s on line %d
bool(false)
Warning: gmp_export(): Word size must be positive, -1 given in %s on line %d
bool(false)
Warning: gmp_export(): Word size must be positive, 0 given in %s on line %d
bool(false)
Warning: gmp_import(): Input length must be a multiple of word size in %s on line %d
bool(false)
Warning: gmp_import(): Input length must be a multiple of word size in %s on line %d
bool(false)
Warning: gmp_import(): Input length must be a multiple of word size in %s on line %d
bool(false)
Warning: gmp_import(): Invalid options: Conflicting word orders in %s on line %d
bool(false)
Warning: gmp_import(): Invalid options: Conflicting word endianness in %s on line %d
bool(false)
Warning: gmp_export(): Invalid options: Conflicting word orders in %s on line %d
bool(false)
Warning: gmp_export(): Invalid options: Conflicting word endianness in %s on line %d
bool(false)
Loading…
Cancel
Save