mirror of
https://github.com/chatmail/core.git
synced 2026-04-27 18:36:30 +03:00
571 lines
19 KiB
Rust
571 lines
19 KiB
Rust
use libc;
|
|
|
|
use crate::dc_context::dc_context_t;
|
|
use crate::dc_log::*;
|
|
use crate::dc_pgp::*;
|
|
use crate::dc_sqlite3::*;
|
|
use crate::dc_strbuilder::*;
|
|
use crate::dc_tools::*;
|
|
use crate::types::*;
|
|
use crate::x::*;
|
|
/* *
|
|
* Library-internal.
|
|
*/
|
|
#[derive(Copy, Clone)]
|
|
#[repr(C)]
|
|
pub struct dc_key_t {
|
|
pub binary: *mut libc::c_void,
|
|
pub bytes: libc::c_int,
|
|
pub type_0: libc::c_int,
|
|
pub _m_heap_refcnt: libc::c_int,
|
|
}
|
|
|
|
#[inline]
|
|
pub unsafe fn toupper(mut _c: libc::c_int) -> libc::c_int {
|
|
return __toupper(_c);
|
|
}
|
|
|
|
pub unsafe fn dc_key_new() -> *mut dc_key_t {
|
|
let mut key: *mut dc_key_t = 0 as *mut dc_key_t;
|
|
key = calloc(1, ::std::mem::size_of::<dc_key_t>()) as *mut dc_key_t;
|
|
if key.is_null() {
|
|
exit(44i32);
|
|
}
|
|
(*key)._m_heap_refcnt = 1i32;
|
|
return key;
|
|
}
|
|
pub unsafe fn dc_key_ref(mut key: *mut dc_key_t) -> *mut dc_key_t {
|
|
if key.is_null() {
|
|
return 0 as *mut dc_key_t;
|
|
}
|
|
(*key)._m_heap_refcnt += 1;
|
|
return key;
|
|
}
|
|
pub unsafe fn dc_key_unref(mut key: *mut dc_key_t) {
|
|
if key.is_null() {
|
|
return;
|
|
}
|
|
(*key)._m_heap_refcnt -= 1;
|
|
if (*key)._m_heap_refcnt != 0i32 {
|
|
return;
|
|
}
|
|
dc_key_empty(key);
|
|
free(key as *mut libc::c_void);
|
|
}
|
|
unsafe fn dc_key_empty(mut key: *mut dc_key_t) {
|
|
if key.is_null() {
|
|
return;
|
|
}
|
|
if (*key).type_0 == 1i32 {
|
|
dc_wipe_secret_mem((*key).binary, (*key).bytes as size_t);
|
|
}
|
|
free((*key).binary);
|
|
(*key).binary = 0 as *mut libc::c_void;
|
|
(*key).bytes = 0i32;
|
|
(*key).type_0 = 0i32;
|
|
}
|
|
pub unsafe fn dc_wipe_secret_mem(mut buf: *mut libc::c_void, mut buf_bytes: size_t) {
|
|
if buf.is_null() || buf_bytes <= 0 {
|
|
return;
|
|
}
|
|
memset(buf, 0i32, buf_bytes);
|
|
}
|
|
pub unsafe fn dc_key_set_from_binary(
|
|
mut key: *mut dc_key_t,
|
|
mut data: *const libc::c_void,
|
|
mut bytes: libc::c_int,
|
|
mut type_0: libc::c_int,
|
|
) -> libc::c_int {
|
|
dc_key_empty(key);
|
|
if key.is_null() || data == 0 as *mut libc::c_void || bytes <= 0i32 {
|
|
return 0i32;
|
|
}
|
|
(*key).binary = malloc(bytes as size_t);
|
|
if (*key).binary.is_null() {
|
|
exit(40i32);
|
|
}
|
|
memcpy((*key).binary, data, bytes as size_t);
|
|
(*key).bytes = bytes;
|
|
(*key).type_0 = type_0;
|
|
return 1i32;
|
|
}
|
|
pub unsafe fn dc_key_set_from_key(mut key: *mut dc_key_t, mut o: *const dc_key_t) -> libc::c_int {
|
|
dc_key_empty(key);
|
|
if key.is_null() || o.is_null() {
|
|
return 0i32;
|
|
}
|
|
return dc_key_set_from_binary(key, (*o).binary, (*o).bytes, (*o).type_0);
|
|
}
|
|
pub unsafe extern "C" fn dc_key_set_from_stmt(
|
|
mut key: *mut dc_key_t,
|
|
mut stmt: *mut sqlite3_stmt,
|
|
mut index: libc::c_int,
|
|
mut type_0: libc::c_int,
|
|
) -> libc::c_int {
|
|
dc_key_empty(key);
|
|
if key.is_null() || stmt.is_null() {
|
|
return 0i32;
|
|
}
|
|
return dc_key_set_from_binary(
|
|
key,
|
|
sqlite3_column_blob(stmt, index) as *mut libc::c_uchar as *const libc::c_void,
|
|
sqlite3_column_bytes(stmt, index),
|
|
type_0,
|
|
);
|
|
}
|
|
pub unsafe fn dc_key_set_from_base64(
|
|
mut key: *mut dc_key_t,
|
|
mut base64: *const libc::c_char,
|
|
mut type_0: libc::c_int,
|
|
) -> libc::c_int {
|
|
let mut indx: size_t = 0i32 as size_t;
|
|
let mut result_len: size_t = 0i32 as size_t;
|
|
let mut result: *mut libc::c_char = 0 as *mut libc::c_char;
|
|
dc_key_empty(key);
|
|
if key.is_null() || base64.is_null() {
|
|
return 0i32;
|
|
}
|
|
if mailmime_base64_body_parse(
|
|
base64,
|
|
strlen(base64),
|
|
&mut indx,
|
|
&mut result,
|
|
&mut result_len,
|
|
) != MAILIMF_NO_ERROR as libc::c_int
|
|
|| result.is_null()
|
|
|| result_len == 0
|
|
{
|
|
return 0;
|
|
}
|
|
dc_key_set_from_binary(
|
|
key,
|
|
result as *const libc::c_void,
|
|
result_len as libc::c_int,
|
|
type_0,
|
|
);
|
|
mmap_string_unref(result);
|
|
return 1i32;
|
|
}
|
|
pub unsafe fn dc_key_set_from_file(
|
|
mut key: *mut dc_key_t,
|
|
mut pathNfilename: *const libc::c_char,
|
|
mut context: &dc_context_t,
|
|
) -> libc::c_int {
|
|
let mut current_block: u64;
|
|
let mut buf: *mut libc::c_char = 0 as *mut libc::c_char;
|
|
// just pointer inside buf, must not be freed
|
|
let mut headerline: *const libc::c_char = 0 as *const libc::c_char;
|
|
// - " -
|
|
let mut base64: *const libc::c_char = 0 as *const libc::c_char;
|
|
let mut buf_bytes: size_t = 0i32 as size_t;
|
|
let mut type_0: libc::c_int = -1i32;
|
|
let mut success: libc::c_int = 0i32;
|
|
dc_key_empty(key);
|
|
if !(key.is_null() || pathNfilename.is_null()) {
|
|
if !(0
|
|
== dc_read_file(
|
|
context,
|
|
pathNfilename,
|
|
&mut buf as *mut *mut libc::c_char as *mut *mut libc::c_void,
|
|
&mut buf_bytes,
|
|
)
|
|
|| buf_bytes < 50)
|
|
{
|
|
/* error is already loged */
|
|
if !(0
|
|
== dc_split_armored_data(
|
|
buf,
|
|
&mut headerline,
|
|
0 as *mut *const libc::c_char,
|
|
0 as *mut *const libc::c_char,
|
|
&mut base64,
|
|
)
|
|
|| headerline.is_null()
|
|
|| base64.is_null())
|
|
{
|
|
if strcmp(
|
|
headerline,
|
|
b"-----BEGIN PGP PUBLIC KEY BLOCK-----\x00" as *const u8 as *const libc::c_char,
|
|
) == 0i32
|
|
{
|
|
type_0 = 0i32;
|
|
current_block = 7149356873433890176;
|
|
} else if strcmp(
|
|
headerline,
|
|
b"-----BEGIN PGP PRIVATE KEY BLOCK-----\x00" as *const u8
|
|
as *const libc::c_char,
|
|
) == 0i32
|
|
{
|
|
type_0 = 1i32;
|
|
current_block = 7149356873433890176;
|
|
} else {
|
|
dc_log_warning(
|
|
context,
|
|
0i32,
|
|
b"Header missing for key \"%s\".\x00" as *const u8 as *const libc::c_char,
|
|
pathNfilename,
|
|
);
|
|
current_block = 7704194852291245876;
|
|
}
|
|
match current_block {
|
|
7704194852291245876 => {}
|
|
_ => {
|
|
if 0 == dc_key_set_from_base64(key, base64, type_0) {
|
|
dc_log_warning(
|
|
context,
|
|
0i32,
|
|
b"Bad data in key \"%s\".\x00" as *const u8 as *const libc::c_char,
|
|
pathNfilename,
|
|
);
|
|
} else {
|
|
success = 1i32
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
free(buf as *mut libc::c_void);
|
|
return success;
|
|
}
|
|
pub unsafe fn dc_key_equals(mut key: *const dc_key_t, mut o: *const dc_key_t) -> libc::c_int {
|
|
if key.is_null()
|
|
|| o.is_null()
|
|
|| (*key).binary.is_null()
|
|
|| (*key).bytes <= 0i32
|
|
|| (*o).binary.is_null()
|
|
|| (*o).bytes <= 0i32
|
|
{
|
|
return 0;
|
|
}
|
|
if (*key).bytes != (*o).bytes {
|
|
return 0;
|
|
}
|
|
if (*key).type_0 != (*o).type_0 {
|
|
return 0;
|
|
}
|
|
|
|
if memcmp((*key).binary, (*o).binary, (*o).bytes as size_t) == 0 {
|
|
1
|
|
} else {
|
|
0
|
|
}
|
|
}
|
|
pub unsafe fn dc_key_save_self_keypair(
|
|
mut public_key: *const dc_key_t,
|
|
mut private_key: *const dc_key_t,
|
|
mut addr: *const libc::c_char,
|
|
mut is_default: libc::c_int,
|
|
mut sql: *mut dc_sqlite3_t,
|
|
) -> libc::c_int {
|
|
let mut success: libc::c_int = 0i32;
|
|
let mut stmt: *mut sqlite3_stmt = 0 as *mut sqlite3_stmt;
|
|
if !(public_key.is_null()
|
|
|| private_key.is_null()
|
|
|| addr.is_null()
|
|
|| sql.is_null()
|
|
|| (*public_key).binary.is_null()
|
|
|| (*private_key).binary.is_null())
|
|
{
|
|
stmt =
|
|
dc_sqlite3_prepare(sql,
|
|
b"INSERT INTO keypairs (addr, is_default, public_key, private_key, created) VALUES (?,?,?,?,?);\x00"
|
|
as *const u8 as *const libc::c_char);
|
|
sqlite3_bind_text(stmt, 1i32, addr, -1i32, None);
|
|
sqlite3_bind_int(stmt, 2i32, is_default);
|
|
sqlite3_bind_blob(stmt, 3i32, (*public_key).binary, (*public_key).bytes, None);
|
|
sqlite3_bind_blob(
|
|
stmt,
|
|
4i32,
|
|
(*private_key).binary,
|
|
(*private_key).bytes,
|
|
None,
|
|
);
|
|
sqlite3_bind_int64(stmt, 5i32, time(0 as *mut time_t) as sqlite3_int64);
|
|
if !(sqlite3_step(stmt) != 101i32) {
|
|
success = 1i32
|
|
}
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
return success;
|
|
}
|
|
pub unsafe fn dc_key_load_self_public(
|
|
mut key: *mut dc_key_t,
|
|
mut self_addr: *const libc::c_char,
|
|
mut sql: *mut dc_sqlite3_t,
|
|
) -> libc::c_int {
|
|
let mut success: libc::c_int = 0i32;
|
|
let mut stmt: *mut sqlite3_stmt = 0 as *mut sqlite3_stmt;
|
|
if !(key.is_null() || self_addr.is_null() || sql.is_null()) {
|
|
dc_key_empty(key);
|
|
stmt = dc_sqlite3_prepare(
|
|
sql,
|
|
b"SELECT public_key FROM keypairs WHERE addr=? AND is_default=1;\x00" as *const u8
|
|
as *const libc::c_char,
|
|
);
|
|
sqlite3_bind_text(stmt, 1i32, self_addr, -1i32, None);
|
|
if !(sqlite3_step(stmt) != 100i32) {
|
|
dc_key_set_from_stmt(key, stmt, 0i32, 0i32);
|
|
success = 1i32
|
|
}
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
return success;
|
|
}
|
|
pub unsafe fn dc_key_load_self_private(
|
|
mut key: *mut dc_key_t,
|
|
mut self_addr: *const libc::c_char,
|
|
mut sql: *mut dc_sqlite3_t,
|
|
) -> libc::c_int {
|
|
let mut success: libc::c_int = 0i32;
|
|
let mut stmt: *mut sqlite3_stmt = 0 as *mut sqlite3_stmt;
|
|
if !(key.is_null() || self_addr.is_null() || sql.is_null()) {
|
|
dc_key_empty(key);
|
|
stmt = dc_sqlite3_prepare(
|
|
sql,
|
|
b"SELECT private_key FROM keypairs WHERE addr=? AND is_default=1;\x00" as *const u8
|
|
as *const libc::c_char,
|
|
);
|
|
sqlite3_bind_text(stmt, 1i32, self_addr, -1i32, None);
|
|
if !(sqlite3_step(stmt) != 100i32) {
|
|
dc_key_set_from_stmt(key, stmt, 0i32, 1i32);
|
|
success = 1i32
|
|
}
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
return success;
|
|
}
|
|
/* the result must be freed */
|
|
pub unsafe fn dc_render_base64(
|
|
mut buf: *const libc::c_void,
|
|
mut buf_bytes: size_t,
|
|
mut break_every: libc::c_int,
|
|
mut break_chars: *const libc::c_char,
|
|
mut add_checksum: libc::c_int,
|
|
) -> *mut libc::c_char {
|
|
let mut ret: *mut libc::c_char = 0 as *mut libc::c_char;
|
|
if !(buf == 0 as *mut libc::c_void || buf_bytes <= 0) {
|
|
ret = encode_base64(buf as *const libc::c_char, buf_bytes as libc::c_int);
|
|
if !ret.is_null() {
|
|
if break_every > 0i32 {
|
|
let mut temp: *mut libc::c_char = ret;
|
|
ret = dc_insert_breaks(temp, break_every, break_chars);
|
|
free(temp as *mut libc::c_void);
|
|
}
|
|
if add_checksum == 2i32 {
|
|
let mut checksum: libc::c_long = crc_octets(buf as *const libc::c_uchar, buf_bytes);
|
|
let mut c: [uint8_t; 3] = [0; 3];
|
|
c[0usize] = (checksum >> 16i32 & 0xffi32 as libc::c_long) as uint8_t;
|
|
c[1usize] = (checksum >> 8i32 & 0xffi32 as libc::c_long) as uint8_t;
|
|
c[2usize] = (checksum & 0xffi32 as libc::c_long) as uint8_t;
|
|
let mut c64: *mut libc::c_char =
|
|
encode_base64(c.as_mut_ptr() as *const libc::c_char, 3i32);
|
|
let mut temp_0: *mut libc::c_char = ret;
|
|
ret = dc_mprintf(
|
|
b"%s%s=%s\x00" as *const u8 as *const libc::c_char,
|
|
temp_0,
|
|
break_chars,
|
|
c64,
|
|
);
|
|
free(temp_0 as *mut libc::c_void);
|
|
free(c64 as *mut libc::c_void);
|
|
}
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
/* ******************************************************************************
|
|
* Render keys
|
|
******************************************************************************/
|
|
unsafe fn crc_octets(mut octets: *const libc::c_uchar, mut len: size_t) -> libc::c_long {
|
|
let mut crc: libc::c_long = 0xb704ce;
|
|
loop {
|
|
let fresh0 = len;
|
|
len = len.wrapping_sub(1);
|
|
if !(0 != fresh0) {
|
|
break;
|
|
}
|
|
let fresh1 = octets;
|
|
octets = octets.offset(1);
|
|
crc ^= ((*fresh1 as libc::c_int) << 16i32) as libc::c_long;
|
|
let mut i: libc::c_int = 0i32;
|
|
while i < 8i32 {
|
|
crc <<= 1i32;
|
|
if 0 != crc & 0x1000000 as libc::c_long {
|
|
crc ^= 0x1864cfb
|
|
}
|
|
i += 1
|
|
}
|
|
}
|
|
return crc & 0xffffff;
|
|
}
|
|
/* the result must be freed */
|
|
pub unsafe fn dc_key_render_base64(
|
|
mut key: *const dc_key_t,
|
|
mut break_every: libc::c_int,
|
|
mut break_chars: *const libc::c_char,
|
|
mut add_checksum: libc::c_int,
|
|
) -> *mut libc::c_char {
|
|
if key.is_null() {
|
|
return 0 as *mut libc::c_char;
|
|
}
|
|
return dc_render_base64(
|
|
(*key).binary,
|
|
(*key).bytes as size_t,
|
|
break_every,
|
|
break_chars,
|
|
add_checksum,
|
|
);
|
|
}
|
|
/* each header line must be terminated by \r\n, the result must be freed */
|
|
pub unsafe fn dc_key_render_asc(
|
|
mut key: *const dc_key_t,
|
|
mut add_header_lines: *const libc::c_char,
|
|
) -> *mut libc::c_char {
|
|
/* see RFC 4880, 6.2. Forming ASCII Armor, https://tools.ietf.org/html/rfc4880#section-6.2 */
|
|
let mut base64: *mut libc::c_char = 0 as *mut libc::c_char;
|
|
let mut ret: *mut libc::c_char = 0 as *mut libc::c_char;
|
|
if !key.is_null() {
|
|
base64 = dc_key_render_base64(
|
|
key,
|
|
76i32,
|
|
b"\r\n\x00" as *const u8 as *const libc::c_char,
|
|
2i32,
|
|
);
|
|
if !base64.is_null() {
|
|
/*checksum in new line*/
|
|
/* RFC: The encoded output stream must be represented in lines of no more than 76 characters each. */
|
|
ret =
|
|
dc_mprintf(b"-----BEGIN PGP %s KEY BLOCK-----\r\n%s\r\n%s\r\n-----END PGP %s KEY BLOCK-----\r\n\x00"
|
|
as *const u8 as *const libc::c_char,
|
|
if (*key).type_0 == 0i32 {
|
|
b"PUBLIC\x00" as *const u8 as
|
|
*const libc::c_char
|
|
} else {
|
|
b"PRIVATE\x00" as *const u8 as
|
|
*const libc::c_char
|
|
},
|
|
if !add_header_lines.is_null() {
|
|
add_header_lines
|
|
} else {
|
|
b"\x00" as *const u8 as *const libc::c_char
|
|
}, base64,
|
|
if (*key).type_0 == 0i32 {
|
|
b"PUBLIC\x00" as *const u8 as
|
|
*const libc::c_char
|
|
} else {
|
|
b"PRIVATE\x00" as *const u8 as
|
|
*const libc::c_char
|
|
})
|
|
}
|
|
}
|
|
free(base64 as *mut libc::c_void);
|
|
return ret;
|
|
}
|
|
pub unsafe fn dc_key_render_asc_to_file(
|
|
mut key: *const dc_key_t,
|
|
mut file: *const libc::c_char,
|
|
mut context: &dc_context_t,
|
|
) -> libc::c_int {
|
|
let mut success: libc::c_int = 0i32;
|
|
let mut file_content: *mut libc::c_char = 0 as *mut libc::c_char;
|
|
if !(key.is_null() || file.is_null() || context.is_null()) {
|
|
file_content = dc_key_render_asc(key, 0 as *const libc::c_char);
|
|
if !file_content.is_null() {
|
|
if 0 == dc_write_file(
|
|
context,
|
|
file,
|
|
file_content as *const libc::c_void,
|
|
strlen(file_content),
|
|
) {
|
|
dc_log_error(
|
|
context,
|
|
0i32,
|
|
b"Cannot write key to %s\x00" as *const u8 as *const libc::c_char,
|
|
file,
|
|
);
|
|
} else {
|
|
success = 1i32
|
|
}
|
|
}
|
|
}
|
|
free(file_content as *mut libc::c_void);
|
|
return success;
|
|
}
|
|
pub unsafe fn dc_format_fingerprint(mut fingerprint: *const libc::c_char) -> *mut libc::c_char {
|
|
let mut i: libc::c_int = 0i32;
|
|
let mut fingerprint_len: libc::c_int = strlen(fingerprint) as libc::c_int;
|
|
let mut ret: dc_strbuilder_t = dc_strbuilder_t {
|
|
buf: 0 as *mut libc::c_char,
|
|
allocated: 0,
|
|
free: 0,
|
|
eos: 0 as *mut libc::c_char,
|
|
};
|
|
dc_strbuilder_init(&mut ret, 0i32);
|
|
while 0 != *fingerprint.offset(i as isize) {
|
|
dc_strbuilder_catf(
|
|
&mut ret as *mut dc_strbuilder_t,
|
|
b"%c\x00" as *const u8 as *const libc::c_char,
|
|
*fingerprint.offset(i as isize) as libc::c_int,
|
|
);
|
|
i += 1;
|
|
if i != fingerprint_len {
|
|
if i % 20i32 == 0i32 {
|
|
dc_strbuilder_cat(&mut ret, b"\n\x00" as *const u8 as *const libc::c_char);
|
|
} else if i % 4i32 == 0i32 {
|
|
dc_strbuilder_cat(&mut ret, b" \x00" as *const u8 as *const libc::c_char);
|
|
}
|
|
}
|
|
}
|
|
return ret.buf;
|
|
}
|
|
pub unsafe fn dc_normalize_fingerprint(mut in_0: *const libc::c_char) -> *mut libc::c_char {
|
|
if in_0.is_null() {
|
|
return 0 as *mut libc::c_char;
|
|
}
|
|
let mut out: dc_strbuilder_t = dc_strbuilder_t {
|
|
buf: 0 as *mut libc::c_char,
|
|
allocated: 0,
|
|
free: 0,
|
|
eos: 0 as *mut libc::c_char,
|
|
};
|
|
dc_strbuilder_init(&mut out, 0i32);
|
|
let mut p1: *const libc::c_char = in_0;
|
|
while 0 != *p1 {
|
|
if *p1 as libc::c_int >= '0' as i32 && *p1 as libc::c_int <= '9' as i32
|
|
|| *p1 as libc::c_int >= 'A' as i32 && *p1 as libc::c_int <= 'F' as i32
|
|
|| *p1 as libc::c_int >= 'a' as i32 && *p1 as libc::c_int <= 'f' as i32
|
|
{
|
|
dc_strbuilder_catf(
|
|
&mut out as *mut dc_strbuilder_t,
|
|
b"%c\x00" as *const u8 as *const libc::c_char,
|
|
toupper(*p1 as libc::c_int),
|
|
);
|
|
}
|
|
p1 = p1.offset(1isize)
|
|
}
|
|
return out.buf;
|
|
}
|
|
pub unsafe fn dc_key_get_fingerprint(mut key: *const dc_key_t) -> *mut libc::c_char {
|
|
let mut fingerprint_buf: *mut uint8_t = 0 as *mut uint8_t;
|
|
let mut fingerprint_bytes: size_t = 0i32 as size_t;
|
|
let mut fingerprint_hex: *mut libc::c_char = 0 as *mut libc::c_char;
|
|
if !key.is_null() {
|
|
if !(0 == dc_pgp_calc_fingerprint(key, &mut fingerprint_buf, &mut fingerprint_bytes)) {
|
|
fingerprint_hex = dc_binary_to_uc_hex(fingerprint_buf, fingerprint_bytes)
|
|
}
|
|
}
|
|
free(fingerprint_buf as *mut libc::c_void);
|
|
return if !fingerprint_hex.is_null() {
|
|
fingerprint_hex
|
|
} else {
|
|
dc_strdup(0 as *const libc::c_char)
|
|
};
|
|
}
|
|
pub unsafe fn dc_key_get_formatted_fingerprint(mut key: *const dc_key_t) -> *mut libc::c_char {
|
|
let mut rawhex: *mut libc::c_char = dc_key_get_fingerprint(key);
|
|
let mut formatted: *mut libc::c_char = dc_format_fingerprint(rawhex);
|
|
free(rawhex as *mut libc::c_void);
|
|
return formatted;
|
|
}
|