2020-10-10 16:35:14 +01:00
/*
* nso . c
*
2022-03-17 12:58:40 +00:00
* Copyright ( c ) 2020 - 2022 , DarkMatterCore < pabloacurielz @ gmail . com > .
2020-10-10 16:35:14 +01:00
*
* This file is part of nxdumptool ( https : //github.com/DarkMatterCore/nxdumptool).
*
2021-03-25 19:26:58 +00:00
* nxdumptool is free software : you can redistribute it and / or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation , either version 3 of the License , or
* ( at your option ) any later version .
2020-10-10 16:35:14 +01:00
*
2021-03-25 19:26:58 +00:00
* nxdumptool is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU General Public License for more details .
2020-10-10 16:35:14 +01:00
*
* You should have received a copy of the GNU General Public License
2021-03-25 19:26:58 +00:00
* along with this program . If not , see < https : //www.gnu.org/licenses/>.
2020-10-10 16:35:14 +01:00
*/
2021-03-26 04:35:14 +00:00
# include "nxdt_utils.h"
2020-10-10 16:35:14 +01:00
# include "nso.h"
2020-10-11 16:22:26 +01:00
/* Function prototypes. */
static bool nsoGetModuleName ( NsoContext * nso_ctx ) ;
static u8 * nsoGetRodataSegment ( NsoContext * nso_ctx ) ;
static bool nsoGetModuleInfoName ( NsoContext * nso_ctx , u8 * rodata_buf ) ;
static bool nsoGetSectionFromRodataSegment ( NsoContext * nso_ctx , u8 * rodata_buf , u8 * * section_ptr , u64 section_offset , u64 section_size ) ;
bool nsoInitializeContext ( NsoContext * out , PartitionFileSystemContext * pfs_ctx , PartitionFileSystemEntry * pfs_entry )
{
NcaContext * nca_ctx = NULL ;
u8 * rodata_buf = NULL ;
2021-03-07 23:22:49 +00:00
bool success = false , dump_nso_header = false ;
2022-07-05 02:04:28 +01:00
2022-07-04 01:20:51 +01:00
if ( ! out | | ! pfs_ctx | | ! ncaStorageIsValidContext ( & ( pfs_ctx - > storage_ctx ) ) | | ! ( nca_ctx = ( NcaContext * ) pfs_ctx - > nca_fs_ctx - > nca_ctx ) | | \
2022-07-04 00:36:01 +01:00
nca_ctx - > content_type ! = NcmContentType_Program | | ! pfs_ctx - > offset | | ! pfs_ctx - > size | | ! pfs_ctx - > is_exefs | | \
pfs_ctx - > header_size < = sizeof ( PartitionFileSystemHeader ) | | ! pfs_ctx - > header | | ! pfs_entry )
2020-10-11 16:22:26 +01:00
{
2021-03-07 23:22:49 +00:00
LOG_MSG ( " Invalid parameters! " ) ;
2020-10-11 16:22:26 +01:00
return false ;
}
2022-07-05 02:04:28 +01:00
2020-10-11 16:22:26 +01:00
/* Free output context beforehand. */
nsoFreeContext ( out ) ;
2022-07-05 02:04:28 +01:00
2020-10-11 16:22:26 +01:00
/* Update output context. */
out - > pfs_ctx = pfs_ctx ;
out - > pfs_entry = pfs_entry ;
2022-07-05 02:04:28 +01:00
2020-10-11 16:22:26 +01:00
/* Get entry filename. */
2020-10-15 01:06:53 +01:00
if ( ! ( out - > nso_filename = pfsGetEntryName ( pfs_ctx , pfs_entry ) ) | | ! * ( out - > nso_filename ) )
2020-10-11 16:22:26 +01:00
{
2021-03-07 23:22:49 +00:00
LOG_MSG ( " Invalid Partition FS entry filename! " ) ;
2020-10-11 16:22:26 +01:00
goto end ;
}
2022-07-05 02:04:28 +01:00
2020-10-11 16:22:26 +01:00
/* Read NSO header. */
if ( ! pfsReadEntryData ( pfs_ctx , pfs_entry , & ( out - > nso_header ) , sizeof ( NsoHeader ) , 0 ) )
{
2021-03-07 23:22:49 +00:00
LOG_MSG ( " Failed to read NSO \" %s \" header! " , out - > nso_filename ) ; ;
2020-10-11 16:22:26 +01:00
goto end ;
}
2022-07-05 02:04:28 +01:00
2020-10-11 16:22:26 +01:00
/* Verify NSO header. */
if ( __builtin_bswap32 ( out - > nso_header . magic ) ! = NSO_HEADER_MAGIC )
{
2021-03-07 23:22:49 +00:00
LOG_MSG ( " Invalid NSO \" %s \" header magic word! (0x%08X != 0x%08X). " , out - > nso_filename , __builtin_bswap32 ( out - > nso_header . magic ) , __builtin_bswap32 ( NSO_HEADER_MAGIC ) ) ;
dump_nso_header = true ;
2020-10-11 16:22:26 +01:00
goto end ;
}
2022-07-05 02:04:28 +01:00
2020-11-08 19:08:30 +00:00
if ( out - > nso_header . text_segment_info . file_offset < sizeof ( NsoHeader ) | | ! out - > nso_header . text_segment_info . size | | \
( ( out - > nso_header . flags & NsoFlags_TextCompress ) & & ( ! out - > nso_header . text_file_size | | out - > nso_header . text_file_size > out - > nso_header . text_segment_info . size ) ) | | \
( ! ( out - > nso_header . flags & NsoFlags_TextCompress ) & & out - > nso_header . text_file_size ! = out - > nso_header . text_segment_info . size ) | | \
( out - > nso_header . text_segment_info . file_offset + out - > nso_header . text_file_size ) > pfs_entry - > size )
2020-10-11 16:22:26 +01:00
{
2021-03-07 23:22:49 +00:00
LOG_MSG ( " Invalid .text segment offset/size for NSO \" %s \" ! (0x%08X, 0x%08X, 0x%08X). " , out - > nso_filename , out - > nso_header . text_segment_info . file_offset , out - > nso_header . text_file_size , \
2020-11-08 19:08:30 +00:00
out - > nso_header . text_segment_info . size ) ;
2021-03-07 23:22:49 +00:00
dump_nso_header = true ;
2020-10-11 16:22:26 +01:00
goto end ;
}
2022-07-05 02:04:28 +01:00
2020-11-08 19:08:30 +00:00
if ( out - > nso_header . rodata_segment_info . file_offset < sizeof ( NsoHeader ) | | ! out - > nso_header . rodata_segment_info . size | | \
( ( out - > nso_header . flags & NsoFlags_RoCompress ) & & ( ! out - > nso_header . rodata_file_size | | out - > nso_header . rodata_file_size > out - > nso_header . rodata_segment_info . size ) ) | | \
( ! ( out - > nso_header . flags & NsoFlags_RoCompress ) & & out - > nso_header . rodata_file_size ! = out - > nso_header . rodata_segment_info . size ) | | \
( out - > nso_header . rodata_segment_info . file_offset + out - > nso_header . rodata_file_size ) > pfs_entry - > size )
2020-10-11 16:22:26 +01:00
{
2021-03-07 23:22:49 +00:00
LOG_MSG ( " Invalid .rodata segment offset/size for NSO \" %s \" ! (0x%08X, 0x%08X, 0x%08X). " , out - > nso_filename , out - > nso_header . rodata_segment_info . file_offset , out - > nso_header . rodata_file_size , \
2020-11-08 19:08:30 +00:00
out - > nso_header . rodata_segment_info . size ) ;
2021-03-07 23:22:49 +00:00
dump_nso_header = true ;
2020-10-11 16:22:26 +01:00
goto end ;
}
2022-07-05 02:04:28 +01:00
2020-11-08 19:08:30 +00:00
if ( out - > nso_header . data_segment_info . file_offset < sizeof ( NsoHeader ) | | ! out - > nso_header . data_segment_info . size | | \
( ( out - > nso_header . flags & NsoFlags_DataCompress ) & & ( ! out - > nso_header . data_file_size | | out - > nso_header . data_file_size > out - > nso_header . data_segment_info . size ) ) | | \
( ! ( out - > nso_header . flags & NsoFlags_DataCompress ) & & out - > nso_header . data_file_size ! = out - > nso_header . data_segment_info . size ) | | \
( out - > nso_header . data_segment_info . file_offset + out - > nso_header . data_file_size ) > pfs_entry - > size )
2020-10-11 16:22:26 +01:00
{
2021-03-07 23:22:49 +00:00
LOG_MSG ( " Invalid .data segment offset/size for NSO \" %s \" ! (0x%08X, 0x%08X, 0x%08X). " , out - > nso_filename , out - > nso_header . data_segment_info . file_offset , out - > nso_header . data_file_size , \
2020-11-08 19:08:30 +00:00
out - > nso_header . data_segment_info . size ) ;
2021-03-07 23:22:49 +00:00
dump_nso_header = true ;
2020-10-11 16:22:26 +01:00
goto end ;
}
2022-07-05 02:04:28 +01:00
2021-03-07 23:22:49 +00:00
if ( out - > nso_header . module_name_size > 1 & & ( out - > nso_header . module_name_offset < sizeof ( NsoHeader ) | | ( out - > nso_header . module_name_offset + out - > nso_header . module_name_size ) > pfs_entry - > size ) )
2020-10-11 16:22:26 +01:00
{
2021-03-07 23:22:49 +00:00
LOG_MSG ( " Invalid module name offset/size for NSO \" %s \" ! (0x%08X, 0x%08X). " , out - > nso_filename , out - > nso_header . module_name_offset , out - > nso_header . module_name_size ) ;
dump_nso_header = true ;
goto end ;
2020-10-11 16:22:26 +01:00
}
2022-07-05 02:04:28 +01:00
2020-11-08 19:08:30 +00:00
if ( out - > nso_header . api_info_section_info . size & & ( out - > nso_header . api_info_section_info . offset + out - > nso_header . api_info_section_info . size ) > out - > nso_header . rodata_segment_info . size )
2020-10-11 16:22:26 +01:00
{
2021-03-07 23:22:49 +00:00
LOG_MSG ( " Invalid .api_info section offset/size for NSO \" %s \" ! (0x%08X, 0x%08X). " , out - > nso_filename , out - > nso_header . api_info_section_info . offset , out - > nso_header . api_info_section_info . size ) ;
dump_nso_header = true ;
goto end ;
2020-10-11 16:22:26 +01:00
}
2022-07-05 02:04:28 +01:00
2021-03-07 23:22:49 +00:00
if ( out - > nso_header . dynstr_section_info . size & & ( out - > nso_header . dynstr_section_info . offset + out - > nso_header . dynstr_section_info . size ) > out - > nso_header . rodata_segment_info . size )
2020-10-11 16:22:26 +01:00
{
2021-03-07 23:22:49 +00:00
LOG_MSG ( " Invalid .dynstr section offset/size for NSO \" %s \" ! (0x%08X, 0x%08X). " , out - > nso_filename , out - > nso_header . dynstr_section_info . offset , out - > nso_header . dynstr_section_info . size ) ;
dump_nso_header = true ;
goto end ;
2020-10-11 16:22:26 +01:00
}
2022-07-05 02:04:28 +01:00
2021-03-07 23:22:49 +00:00
if ( out - > nso_header . dynsym_section_info . size & & ( out - > nso_header . dynsym_section_info . offset + out - > nso_header . dynsym_section_info . size ) > out - > nso_header . rodata_segment_info . size )
2020-10-11 16:22:26 +01:00
{
2021-03-07 23:22:49 +00:00
LOG_MSG ( " Invalid .dynsym section offset/size for NSO \" %s \" ! (0x%08X, 0x%08X). " , out - > nso_filename , out - > nso_header . dynsym_section_info . offset , out - > nso_header . dynsym_section_info . size ) ;
dump_nso_header = true ;
goto end ;
2020-10-11 16:22:26 +01:00
}
2022-07-05 02:04:28 +01:00
2020-10-11 16:22:26 +01:00
/* Get module name. */
if ( ! nsoGetModuleName ( out ) ) goto end ;
2022-07-05 02:04:28 +01:00
2020-10-11 16:22:26 +01:00
/* Get .rodata segment. */
if ( ! ( rodata_buf = nsoGetRodataSegment ( out ) ) ) goto end ;
2022-07-05 02:04:28 +01:00
2020-10-11 16:22:26 +01:00
/* Get module info name. */
if ( ! nsoGetModuleInfoName ( out , rodata_buf ) ) goto end ;
2022-07-05 02:04:28 +01:00
2020-10-11 16:22:26 +01:00
/* Get .api_info section data. */
2020-11-08 19:08:30 +00:00
if ( ! nsoGetSectionFromRodataSegment ( out , rodata_buf , ( u8 * * ) & ( out - > rodata_api_info_section ) , out - > nso_header . api_info_section_info . offset , out - > nso_header . api_info_section_info . size ) ) goto end ;
out - > rodata_api_info_section_size = out - > nso_header . api_info_section_info . size ;
2022-07-05 02:04:28 +01:00
2020-10-11 16:22:26 +01:00
/* Get .dynstr section data. */
2020-11-08 19:08:30 +00:00
if ( ! nsoGetSectionFromRodataSegment ( out , rodata_buf , ( u8 * * ) & ( out - > rodata_dynstr_section ) , out - > nso_header . dynstr_section_info . offset , out - > nso_header . dynstr_section_info . size ) ) goto end ;
out - > rodata_dynstr_section_size = out - > nso_header . dynstr_section_info . size ;
2022-07-05 02:04:28 +01:00
2020-10-11 16:22:26 +01:00
/* Get .dynsym section data. */
2020-11-08 19:08:30 +00:00
if ( ! nsoGetSectionFromRodataSegment ( out , rodata_buf , & ( out - > rodata_dynsym_section ) , out - > nso_header . dynsym_section_info . offset , out - > nso_header . dynsym_section_info . size ) ) goto end ;
out - > rodata_dynsym_section_size = out - > nso_header . dynsym_section_info . size ;
2022-07-05 02:04:28 +01:00
2020-10-11 16:22:26 +01:00
success = true ;
2022-07-05 02:04:28 +01:00
2020-10-11 16:22:26 +01:00
end :
if ( rodata_buf ) free ( rodata_buf ) ;
2022-07-05 02:04:28 +01:00
2021-03-07 23:22:49 +00:00
if ( ! success )
{
if ( dump_nso_header ) LOG_DATA ( & ( out - > nso_header ) , sizeof ( NsoHeader ) , " NSO header dump: " ) ;
2022-07-05 02:04:28 +01:00
2021-03-07 23:22:49 +00:00
nsoFreeContext ( out ) ;
}
2022-07-05 02:04:28 +01:00
2020-10-11 16:22:26 +01:00
return success ;
}
static bool nsoGetModuleName ( NsoContext * nso_ctx )
{
2020-10-12 21:35:47 +01:00
if ( nso_ctx - > nso_header . module_name_offset < sizeof ( NsoHeader ) | | nso_ctx - > nso_header . module_name_size < = 1 ) return true ;
2022-07-05 02:04:28 +01:00
2020-10-11 16:22:26 +01:00
NsoModuleName module_name = { 0 } ;
2022-07-05 02:04:28 +01:00
2020-10-11 16:22:26 +01:00
/* Get module name. */
if ( ! pfsReadEntryData ( nso_ctx - > pfs_ctx , nso_ctx - > pfs_entry , & module_name , sizeof ( NsoModuleName ) , nso_ctx - > nso_header . module_name_offset ) )
{
2021-03-07 23:22:49 +00:00
LOG_MSG ( " Failed to read NSO \" %s \" module name length! " , nso_ctx - > nso_filename ) ;
2020-10-11 16:22:26 +01:00
return false ;
}
2022-07-05 02:04:28 +01:00
2020-10-11 16:22:26 +01:00
/* Verify module name length. */
if ( module_name . name_length ! = ( ( u8 ) nso_ctx - > nso_header . module_name_size - 1 ) )
{
2021-03-07 23:22:49 +00:00
LOG_MSG ( " NSO \" %s \" module name length mismatch! (0x%02X != 0x%02X). " , nso_ctx - > nso_filename , module_name . name_length , ( u8 ) nso_ctx - > nso_header . module_name_size - 1 ) ;
2020-10-11 16:22:26 +01:00
return false ;
}
2022-07-05 02:04:28 +01:00
2020-10-11 16:22:26 +01:00
/* Allocate memory for the module name. */
nso_ctx - > module_name = calloc ( nso_ctx - > nso_header . module_name_size , sizeof ( char ) ) ;
if ( ! nso_ctx - > module_name )
{
2021-03-07 23:22:49 +00:00
LOG_MSG ( " Failed to allocate memory for NSO \" %s \" module name! " , nso_ctx - > nso_filename ) ;
2020-10-11 16:22:26 +01:00
return false ;
}
2022-07-05 02:04:28 +01:00
2020-10-11 16:22:26 +01:00
/* Read module name string. */
if ( ! pfsReadEntryData ( nso_ctx - > pfs_ctx , nso_ctx - > pfs_entry , nso_ctx - > module_name , module_name . name_length , nso_ctx - > nso_header . module_name_offset + 1 ) )
{
2021-03-07 23:22:49 +00:00
LOG_MSG ( " Failed to read NSO \" %s \" module name string! " , nso_ctx - > nso_filename ) ;
2020-10-11 16:22:26 +01:00
return false ;
}
2022-07-05 02:04:28 +01:00
2020-10-11 16:22:26 +01:00
return true ;
}
static u8 * nsoGetRodataSegment ( NsoContext * nso_ctx )
{
2020-10-11 18:23:58 +01:00
int lz4_res = 0 ;
bool compressed = ( nso_ctx - > nso_header . flags & NsoFlags_RoCompress ) , verify = ( nso_ctx - > nso_header . flags & NsoFlags_RoHash ) ;
2022-07-05 02:04:28 +01:00
2020-10-11 16:22:26 +01:00
u8 * rodata_buf = NULL ;
2020-11-08 19:08:30 +00:00
u64 rodata_buf_size = ( compressed ? LZ4_DECOMPRESS_INPLACE_BUFFER_SIZE ( nso_ctx - > nso_header . rodata_segment_info . size ) : nso_ctx - > nso_header . rodata_segment_info . size ) ;
2022-07-05 02:04:28 +01:00
2020-10-12 01:40:54 +01:00
u8 * rodata_read_ptr = NULL ;
2020-11-08 19:08:30 +00:00
u64 rodata_read_size = ( compressed ? nso_ctx - > nso_header . rodata_file_size : nso_ctx - > nso_header . rodata_segment_info . size ) ;
2022-07-05 02:04:28 +01:00
2020-10-11 18:23:58 +01:00
u8 rodata_hash [ SHA256_HASH_SIZE ] = { 0 } ;
2022-07-05 02:04:28 +01:00
2020-10-11 16:22:26 +01:00
bool success = false ;
2022-07-05 02:04:28 +01:00
2020-10-11 16:22:26 +01:00
/* Allocate memory for the .rodata buffer. */
if ( ! ( rodata_buf = calloc ( rodata_buf_size , sizeof ( u8 ) ) ) )
{
2021-03-07 23:22:49 +00:00
LOG_MSG ( " Failed to allocate 0x%lX bytes for the .rodata segment in NSO \" %s \" ! " , rodata_buf_size , nso_ctx - > nso_filename ) ;
2020-10-11 16:22:26 +01:00
return NULL ;
}
2022-07-05 02:04:28 +01:00
2020-10-12 01:40:54 +01:00
rodata_read_ptr = ( compressed ? ( rodata_buf + ( rodata_buf_size - nso_ctx - > nso_header . rodata_file_size ) ) : rodata_buf ) ;
2022-07-05 02:04:28 +01:00
2020-10-11 16:22:26 +01:00
/* Read .rodata segment data. */
2020-11-08 19:08:30 +00:00
if ( ! pfsReadEntryData ( nso_ctx - > pfs_ctx , nso_ctx - > pfs_entry , rodata_read_ptr , rodata_read_size , nso_ctx - > nso_header . rodata_segment_info . file_offset ) )
2020-10-11 16:22:26 +01:00
{
2021-03-29 19:27:35 +01:00
LOG_MSG ( " Failed to read .rodata segment in NRO \" %s \" ! " , nso_ctx - > nso_filename ) ;
2020-10-11 16:22:26 +01:00
goto end ;
}
2022-07-05 02:04:28 +01:00
2020-10-11 16:22:26 +01:00
if ( compressed )
{
/* Decompress .rodata segment in-place. */
if ( ( lz4_res = LZ4_decompress_safe ( ( char * ) rodata_read_ptr , ( char * ) rodata_buf , ( int ) nso_ctx - > nso_header . rodata_file_size , ( int ) rodata_buf_size ) ) ! = \
2020-11-08 19:08:30 +00:00
( int ) nso_ctx - > nso_header . rodata_segment_info . size )
2020-10-11 16:22:26 +01:00
{
2022-07-09 13:56:44 +01:00
LOG_MSG ( " LZ4 decompression failed for NRO \" %s \" ! (%d). " , nso_ctx - > nso_filename , lz4_res ) ;
2020-10-11 16:22:26 +01:00
goto end ;
}
}
2022-07-05 02:04:28 +01:00
2020-10-11 18:23:58 +01:00
if ( verify )
{
/* Verify .rodata segment hash. */
2020-11-08 19:08:30 +00:00
sha256CalculateHash ( rodata_hash , rodata_buf , nso_ctx - > nso_header . rodata_segment_info . size ) ;
2020-10-11 18:23:58 +01:00
if ( memcmp ( rodata_hash , nso_ctx - > nso_header . rodata_segment_hash , SHA256_HASH_SIZE ) ! = 0 )
{
2021-03-07 23:22:49 +00:00
LOG_MSG ( " .rodata segment checksum mismatch for NRO \" %s \" ! " , nso_ctx - > nso_filename ) ;
2020-10-11 18:23:58 +01:00
goto end ;
}
}
2022-07-05 02:04:28 +01:00
2020-10-11 16:22:26 +01:00
success = true ;
2022-07-05 02:04:28 +01:00
2020-10-11 16:22:26 +01:00
end :
if ( ! success & & rodata_buf )
{
free ( rodata_buf ) ;
rodata_buf = NULL ;
}
2022-07-05 02:04:28 +01:00
2020-10-11 16:22:26 +01:00
return rodata_buf ;
}
static bool nsoGetModuleInfoName ( NsoContext * nso_ctx , u8 * rodata_buf )
{
NsoModuleInfo * module_info = ( NsoModuleInfo * ) ( rodata_buf + 0x4 ) ;
if ( ! module_info - > name_length ) return true ;
2022-07-05 02:04:28 +01:00
2020-10-11 16:22:26 +01:00
/* Allocate memory for the module info name. */
nso_ctx - > module_info_name = calloc ( module_info - > name_length + 1 , sizeof ( char ) ) ;
if ( ! nso_ctx - > module_info_name )
{
2021-03-07 23:22:49 +00:00
LOG_MSG ( " Failed to allocate memory for NSO \" %s \" module info name! " , nso_ctx - > nso_filename ) ;
2020-10-11 16:22:26 +01:00
return false ;
}
2022-07-05 02:04:28 +01:00
2020-10-11 16:22:26 +01:00
/* Copy module info name. */
sprintf ( nso_ctx - > module_info_name , " %.*s " , ( int ) module_info - > name_length , module_info - > name ) ;
2022-07-05 02:04:28 +01:00
2020-10-11 16:22:26 +01:00
return true ;
}
2020-10-10 16:35:14 +01:00
2020-10-11 16:22:26 +01:00
static bool nsoGetSectionFromRodataSegment ( NsoContext * nso_ctx , u8 * rodata_buf , u8 * * section_ptr , u64 section_offset , u64 section_size )
{
2020-11-08 19:08:30 +00:00
if ( ! section_size | | ( section_offset + section_size ) > nso_ctx - > nso_header . rodata_segment_info . size ) return true ;
2022-07-05 02:04:28 +01:00
2020-10-11 16:22:26 +01:00
/* Allocate memory for the desired .rodata section. */
if ( ! ( * section_ptr = malloc ( section_size ) ) )
{
2021-03-07 23:22:49 +00:00
LOG_MSG ( " Failed to allocate 0x%lX bytes for section at .rodata offset 0x%lX in NSO \" %s \" ! " , section_size , section_offset , nso_ctx - > nso_filename ) ;
2020-10-11 16:22:26 +01:00
return false ;
}
2022-07-05 02:04:28 +01:00
2020-10-11 16:22:26 +01:00
/* Copy .rodata section data. */
memcpy ( * section_ptr , rodata_buf + section_offset , section_size ) ;
2022-07-05 02:04:28 +01:00
2020-10-11 16:22:26 +01:00
return true ;
}