mirror of
https://github.com/jakcron/nstool
synced 2024-11-22 21:49:30 +00:00
Merge pull request #26 from jakcron/codesym-development
Refactor NSO/NRO code symbol processing.
This commit is contained in:
commit
ee1531d40d
16 changed files with 578 additions and 546 deletions
4
.vscode/c_cpp_properties.json
vendored
4
.vscode/c_cpp_properties.json
vendored
|
@ -10,9 +10,11 @@
|
|||
"/usr/include",
|
||||
"${workspaceRoot}",
|
||||
"${workspaceRoot}/lib/libcrypto/include",
|
||||
"${workspaceRoot}/lib/libcompress/include",
|
||||
"${workspaceRoot}/lib/libes/include",
|
||||
"${workspaceRoot}/lib/libfnd/include",
|
||||
"${workspaceRoot}/lib/libnx/include"
|
||||
"${workspaceRoot}/lib/libnx/include",
|
||||
"${workspaceRoot}/lib/libnx-hb/include"
|
||||
],
|
||||
"defines": [],
|
||||
"intelliSenseMode": "clang-x64",
|
||||
|
|
|
@ -3,12 +3,11 @@
|
|||
|
||||
namespace nx
|
||||
{
|
||||
namespace dynsym
|
||||
namespace elf
|
||||
{
|
||||
enum SpecialSectionIndex
|
||||
{
|
||||
SHN_UNDEF,
|
||||
SHN_EXPORT = 1,
|
||||
SHN_LORESERVE = 0xFF00,
|
||||
SHN_LOPROC = 0xFF00,
|
||||
SHN_HIPROC = 0xFF1F,
|
||||
|
@ -31,10 +30,21 @@ namespace nx
|
|||
STT_LOPROC,
|
||||
STT_HIPROC = 0xF
|
||||
};
|
||||
|
||||
enum SymbolBinding
|
||||
{
|
||||
STB_LOCAL,
|
||||
STB_GLOBAL,
|
||||
STB_WEAK,
|
||||
STB_LOOS = 10,
|
||||
STB_HIOS = 12,
|
||||
STB_LOPROC,
|
||||
STB_HIPROC = 0xF
|
||||
};
|
||||
}
|
||||
|
||||
#pragma pack(push,1)
|
||||
struct sDynSymbol32Bit
|
||||
struct sElfSymbol32Bit
|
||||
{
|
||||
le_uint32_t name;
|
||||
le_uint32_t value;
|
||||
|
@ -44,7 +54,7 @@ namespace nx
|
|||
le_uint32_t special_section_index;
|
||||
};
|
||||
|
||||
struct sDynSymbol64Bit
|
||||
struct sElfSymbol64Bit
|
||||
{
|
||||
le_uint32_t name;
|
||||
byte_t info;
|
|
@ -28,7 +28,7 @@
|
|||
<ClInclude Include="include\nx\ApplicationControlPropertyUtils.h" />
|
||||
<ClInclude Include="include\nx\cnmt.h" />
|
||||
<ClInclude Include="include\nx\ContentMetaBinary.h" />
|
||||
<ClInclude Include="include\nx\dynamic_symbol.h" />
|
||||
<ClInclude Include="include\nx\elf.h" />
|
||||
<ClInclude Include="include\nx\FacBinary.h" />
|
||||
<ClInclude Include="include\nx\FacHeader.h" />
|
||||
<ClInclude Include="include\nx\HandleTableSizeEntry.h" />
|
||||
|
|
|
@ -162,9 +162,6 @@
|
|||
<ClInclude Include="include\nx\NsoHeader.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="include\nx\dynamic_symbol.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="include\nx\macro.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
|
@ -186,6 +183,9 @@
|
|||
<ClInclude Include="include\nx\nacp.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="include\nx\elf.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="makefile" />
|
||||
|
|
|
@ -171,7 +171,7 @@
|
|||
<ClInclude Include="source\AesCtrWrappedIFile.h" />
|
||||
<ClInclude Include="source\AssetProcess.h" />
|
||||
<ClInclude Include="source\CnmtProcess.h" />
|
||||
<ClInclude Include="source\DynamicSymbolParser.h" />
|
||||
<ClInclude Include="source\ElfSymbolParser.h" />
|
||||
<ClInclude Include="source\HashTreeMeta.h" />
|
||||
<ClInclude Include="source\HashTreeWrappedIFile.h" />
|
||||
<ClInclude Include="source\NacpProcess.h" />
|
||||
|
@ -182,6 +182,7 @@
|
|||
<ClInclude Include="source\nstool.h" />
|
||||
<ClInclude Include="source\OffsetAdjustedIFile.h" />
|
||||
<ClInclude Include="source\PfsProcess.h" />
|
||||
<ClInclude Include="source\RoMetadataProcess.h" />
|
||||
<ClInclude Include="source\RomfsProcess.h" />
|
||||
<ClInclude Include="source\SdkApiString.h" />
|
||||
<ClInclude Include="source\UserSettings.h" />
|
||||
|
@ -192,7 +193,7 @@
|
|||
<ClCompile Include="source\AesCtrWrappedIFile.cpp" />
|
||||
<ClCompile Include="source\AssetProcess.cpp" />
|
||||
<ClCompile Include="source\CnmtProcess.cpp" />
|
||||
<ClCompile Include="source\DynamicSymbolParser.cpp" />
|
||||
<ClCompile Include="source\ElfSymbolParser.cpp" />
|
||||
<ClCompile Include="source\HashTreeMeta.cpp" />
|
||||
<ClCompile Include="source\HashTreeWrappedIFile.cpp" />
|
||||
<ClCompile Include="source\main.cpp" />
|
||||
|
@ -203,6 +204,7 @@
|
|||
<ClCompile Include="source\NsoProcess.cpp" />
|
||||
<ClCompile Include="source\OffsetAdjustedIFile.cpp" />
|
||||
<ClCompile Include="source\PfsProcess.cpp" />
|
||||
<ClCompile Include="source\RoMetadataProcess.cpp" />
|
||||
<ClCompile Include="source\RomfsProcess.cpp" />
|
||||
<ClCompile Include="source\SdkApiString.cpp" />
|
||||
<ClCompile Include="source\UserSettings.cpp" />
|
||||
|
|
|
@ -57,9 +57,6 @@
|
|||
<ClInclude Include="source\NsoProcess.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="source\DynamicSymbolParser.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="source\AssetProcess.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
|
@ -72,6 +69,12 @@
|
|||
<ClInclude Include="source\NacpProcess.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="source\ElfSymbolParser.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="source\RoMetadataProcess.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="source\main.cpp">
|
||||
|
@ -113,9 +116,6 @@
|
|||
<ClCompile Include="source\NsoProcess.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="source\DynamicSymbolParser.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="source\AssetProcess.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
|
@ -128,6 +128,12 @@
|
|||
<ClCompile Include="source\NacpProcess.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="source\ElfSymbolParser.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="source\RoMetadataProcess.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="makefile" />
|
||||
|
|
|
@ -1,70 +0,0 @@
|
|||
#include "DynamicSymbolParser.h"
|
||||
|
||||
DynamicSymbolParser::DynamicSymbolParser()
|
||||
{
|
||||
mDynSymbolList.clear();
|
||||
}
|
||||
|
||||
bool DynamicSymbolParser::operator==(const DynamicSymbolParser& other) const
|
||||
{
|
||||
return isEqual(other);
|
||||
}
|
||||
|
||||
bool DynamicSymbolParser::operator!=(const DynamicSymbolParser& other) const
|
||||
{
|
||||
return !isEqual(other);
|
||||
}
|
||||
|
||||
void DynamicSymbolParser::operator=(const DynamicSymbolParser& other)
|
||||
{
|
||||
copyFrom(other);
|
||||
}
|
||||
|
||||
void DynamicSymbolParser::parseData(const byte_t *dyn_sym, size_t dyn_sym_size, const byte_t *dyn_str, size_t dyn_str_size, bool is64Bit)
|
||||
{
|
||||
//printf("DynamicSymbolParser::parseData()");
|
||||
size_t dynSymSize = is64Bit ? sizeof(nx::sDynSymbol64Bit) : sizeof(nx::sDynSymbol32Bit);
|
||||
|
||||
sDynSymbol symbol;
|
||||
for (size_t i = 0; i < dyn_sym_size; i += dynSymSize)
|
||||
{
|
||||
//printf("pos %x\n", i);
|
||||
|
||||
uint32_t name_pos;
|
||||
|
||||
if (is64Bit)
|
||||
{
|
||||
name_pos = ((nx::sDynSymbol64Bit*)(dyn_sym + i))->name.get();
|
||||
symbol.shn_index = (nx::dynsym::SpecialSectionIndex)((nx::sDynSymbol64Bit*)(dyn_sym + i))->special_section_index.get();
|
||||
symbol.symbol_type = (nx::dynsym::SymbolType)((((nx::sDynSymbol64Bit*)(dyn_sym + i))->info) & nx::dynsym::STT_HIPROC);
|
||||
}
|
||||
else
|
||||
{
|
||||
name_pos = ((nx::sDynSymbol64Bit*)(dyn_sym + i))->name.get();
|
||||
symbol.shn_index = (nx::dynsym::SpecialSectionIndex)((nx::sDynSymbol32Bit*)(dyn_sym + i))->special_section_index.get();
|
||||
symbol.symbol_type = (nx::dynsym::SymbolType)((((nx::sDynSymbol32Bit*)(dyn_sym + i))->info.get()) & nx::dynsym::STT_HIPROC);
|
||||
}
|
||||
|
||||
for (; dyn_str[name_pos] == 0x00 && name_pos < dyn_str_size; name_pos++);
|
||||
|
||||
//printf("name_pos = 0x%x\n", name_pos);
|
||||
symbol.name = std::string((char*)&dyn_str[name_pos]);
|
||||
mDynSymbolList.addElement(symbol);
|
||||
}
|
||||
//printf("DynamicSymbolParser::parseData() end\n");
|
||||
}
|
||||
|
||||
const fnd::List<DynamicSymbolParser::sDynSymbol>& DynamicSymbolParser::getDynamicSymbolList() const
|
||||
{
|
||||
return mDynSymbolList;
|
||||
}
|
||||
|
||||
bool DynamicSymbolParser::isEqual(const DynamicSymbolParser& other) const
|
||||
{
|
||||
return mDynSymbolList == other.mDynSymbolList;
|
||||
}
|
||||
|
||||
void DynamicSymbolParser::copyFrom(const DynamicSymbolParser& other)
|
||||
{
|
||||
mDynSymbolList = other.mDynSymbolList;
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
#pragma once
|
||||
#include <string>
|
||||
#include <fnd/List.h>
|
||||
#include <nx/dynamic_symbol.h>
|
||||
|
||||
class DynamicSymbolParser
|
||||
{
|
||||
public:
|
||||
struct sDynSymbol
|
||||
{
|
||||
nx::dynsym::SpecialSectionIndex shn_index;
|
||||
nx::dynsym::SymbolType symbol_type;
|
||||
std::string name;
|
||||
|
||||
void operator=(const sDynSymbol& other)
|
||||
{
|
||||
shn_index = other.shn_index;
|
||||
symbol_type = other.symbol_type;
|
||||
name = other.name;
|
||||
}
|
||||
|
||||
bool operator==(const sDynSymbol& other) const
|
||||
{
|
||||
return (shn_index == other.shn_index && symbol_type == other.symbol_type && name == other.name);
|
||||
}
|
||||
|
||||
bool operator!=(const sDynSymbol& other) const
|
||||
{
|
||||
return !(*this == other);
|
||||
}
|
||||
};
|
||||
|
||||
DynamicSymbolParser();
|
||||
|
||||
bool operator==(const DynamicSymbolParser& other) const;
|
||||
bool operator!=(const DynamicSymbolParser& other) const;
|
||||
void operator=(const DynamicSymbolParser& other);
|
||||
|
||||
void parseData(const byte_t *dyn_sym, size_t dyn_sym_size, const byte_t *dyn_str, size_t dyn_str_size, bool is64Bit);
|
||||
|
||||
const fnd::List<sDynSymbol>& getDynamicSymbolList() const;
|
||||
private:
|
||||
|
||||
// data
|
||||
fnd::List<sDynSymbol> mDynSymbolList;
|
||||
|
||||
bool isEqual(const DynamicSymbolParser& other) const;
|
||||
void copyFrom(const DynamicSymbolParser& other);
|
||||
};
|
72
programs/nstool/source/ElfSymbolParser.cpp
Normal file
72
programs/nstool/source/ElfSymbolParser.cpp
Normal file
|
@ -0,0 +1,72 @@
|
|||
#include "ElfSymbolParser.h"
|
||||
|
||||
ElfSymbolParser::ElfSymbolParser()
|
||||
{
|
||||
mSymbolList.clear();
|
||||
}
|
||||
|
||||
bool ElfSymbolParser::operator==(const ElfSymbolParser& other) const
|
||||
{
|
||||
return isEqual(other);
|
||||
}
|
||||
|
||||
bool ElfSymbolParser::operator!=(const ElfSymbolParser& other) const
|
||||
{
|
||||
return !isEqual(other);
|
||||
}
|
||||
|
||||
void ElfSymbolParser::operator=(const ElfSymbolParser& other)
|
||||
{
|
||||
copyFrom(other);
|
||||
}
|
||||
|
||||
void ElfSymbolParser::parseData(const byte_t *dyn_sym, size_t dyn_sym_size, const byte_t *dyn_str, size_t dyn_str_size, bool is64Bit)
|
||||
{
|
||||
//printf("ElfSymbolParser::parseData()");
|
||||
size_t dynSymSize = is64Bit ? sizeof(nx::sElfSymbol64Bit) : sizeof(nx::sElfSymbol32Bit);
|
||||
|
||||
sElfSymbol symbol;
|
||||
for (size_t i = 0; i < dyn_sym_size; i += dynSymSize)
|
||||
{
|
||||
//printf("pos %x\n", i);
|
||||
|
||||
uint32_t name_pos;
|
||||
|
||||
if (is64Bit)
|
||||
{
|
||||
name_pos = ((nx::sElfSymbol64Bit*)(dyn_sym + i))->name.get();
|
||||
symbol.shn_index = (nx::elf::SpecialSectionIndex)((nx::sElfSymbol64Bit*)(dyn_sym + i))->special_section_index.get();
|
||||
symbol.symbol_type = (nx::elf::SymbolType)((((nx::sElfSymbol64Bit*)(dyn_sym + i))->info) & nx::elf::STT_HIPROC);
|
||||
symbol.symbol_binding = (nx::elf::SymbolBinding)(((((nx::sElfSymbol64Bit*)(dyn_sym + i))->info) >> 4) & nx::elf::STB_HIPROC);
|
||||
}
|
||||
else
|
||||
{
|
||||
name_pos = ((nx::sElfSymbol64Bit*)(dyn_sym + i))->name.get();
|
||||
symbol.shn_index = (nx::elf::SpecialSectionIndex)((nx::sElfSymbol32Bit*)(dyn_sym + i))->special_section_index.get();
|
||||
symbol.symbol_type = (nx::elf::SymbolType)((((nx::sElfSymbol32Bit*)(dyn_sym + i))->info.get()) & nx::elf::STT_HIPROC);
|
||||
symbol.symbol_binding = (nx::elf::SymbolBinding)(((((nx::sElfSymbol32Bit*)(dyn_sym + i))->info.get()) >> 4) & nx::elf::STB_HIPROC);
|
||||
}
|
||||
|
||||
for (; dyn_str[name_pos] == 0x00 && name_pos < dyn_str_size; name_pos++);
|
||||
|
||||
//printf("name_pos = 0x%x\n", name_pos);
|
||||
symbol.name = std::string((char*)&dyn_str[name_pos]);
|
||||
mSymbolList.addElement(symbol);
|
||||
}
|
||||
//printf("ElfSymbolParser::parseData() end\n");
|
||||
}
|
||||
|
||||
const fnd::List<ElfSymbolParser::sElfSymbol>& ElfSymbolParser::getSymbolList() const
|
||||
{
|
||||
return mSymbolList;
|
||||
}
|
||||
|
||||
bool ElfSymbolParser::isEqual(const ElfSymbolParser& other) const
|
||||
{
|
||||
return mSymbolList == other.mSymbolList;
|
||||
}
|
||||
|
||||
void ElfSymbolParser::copyFrom(const ElfSymbolParser& other)
|
||||
{
|
||||
mSymbolList = other.mSymbolList;
|
||||
}
|
51
programs/nstool/source/ElfSymbolParser.h
Normal file
51
programs/nstool/source/ElfSymbolParser.h
Normal file
|
@ -0,0 +1,51 @@
|
|||
#pragma once
|
||||
#include <string>
|
||||
#include <fnd/List.h>
|
||||
#include <nx/elf.h>
|
||||
|
||||
class ElfSymbolParser
|
||||
{
|
||||
public:
|
||||
struct sElfSymbol
|
||||
{
|
||||
nx::elf::SpecialSectionIndex shn_index;
|
||||
nx::elf::SymbolType symbol_type;
|
||||
nx::elf::SymbolBinding symbol_binding;
|
||||
std::string name;
|
||||
|
||||
void operator=(const sElfSymbol& other)
|
||||
{
|
||||
shn_index = other.shn_index;
|
||||
symbol_type = other.symbol_type;
|
||||
symbol_binding = other.symbol_binding;
|
||||
name = other.name;
|
||||
}
|
||||
|
||||
bool operator==(const sElfSymbol& other) const
|
||||
{
|
||||
return (shn_index == other.shn_index && symbol_type == other.symbol_type && symbol_binding == other.symbol_binding && name == other.name);
|
||||
}
|
||||
|
||||
bool operator!=(const sElfSymbol& other) const
|
||||
{
|
||||
return !(*this == other);
|
||||
}
|
||||
};
|
||||
|
||||
ElfSymbolParser();
|
||||
|
||||
bool operator==(const ElfSymbolParser& other) const;
|
||||
bool operator!=(const ElfSymbolParser& other) const;
|
||||
void operator=(const ElfSymbolParser& other);
|
||||
|
||||
void parseData(const byte_t *dyn_sym, size_t dyn_sym_size, const byte_t *dyn_str, size_t dyn_str_size, bool is64Bit);
|
||||
|
||||
const fnd::List<sElfSymbol>& getSymbolList() const;
|
||||
private:
|
||||
|
||||
// data
|
||||
fnd::List<sElfSymbol> mSymbolList;
|
||||
|
||||
bool isEqual(const ElfSymbolParser& other) const;
|
||||
void copyFrom(const ElfSymbolParser& other);
|
||||
};
|
|
@ -1,4 +1,3 @@
|
|||
#include <sstream>
|
||||
#include <fnd/SimpleTextOutput.h>
|
||||
#include <fnd/MemoryBlob.h>
|
||||
#include <compress/lz4.h>
|
||||
|
@ -10,10 +9,7 @@ NroProcess::NroProcess():
|
|||
mFile(nullptr),
|
||||
mOwnIFile(false),
|
||||
mCliOutputMode(_BIT(OUTPUT_BASIC)),
|
||||
mVerify(false),
|
||||
mInstructionType(nx::npdm::INSTR_64BIT),
|
||||
mListApi(false),
|
||||
mListSymbols(false)
|
||||
mVerify(false)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -34,12 +30,12 @@ void NroProcess::process()
|
|||
|
||||
importHeader();
|
||||
importCodeSegments();
|
||||
importApiList();
|
||||
|
||||
if (_HAS_BIT(mCliOutputMode, OUTPUT_BASIC))
|
||||
{
|
||||
displayHeader();
|
||||
displayRoMetaData();
|
||||
}
|
||||
|
||||
processRoMeta();
|
||||
|
||||
if (mIsHomebrewNro)
|
||||
mAssetProc.process();
|
||||
}
|
||||
|
@ -62,17 +58,17 @@ void NroProcess::setVerifyMode(bool verify)
|
|||
|
||||
void NroProcess::setInstructionType(nx::npdm::InstructionType type)
|
||||
{
|
||||
mInstructionType = type;
|
||||
mRoMeta.setInstructionType(type);
|
||||
}
|
||||
|
||||
void NroProcess::setListApi(bool listApi)
|
||||
{
|
||||
mListApi = listApi;
|
||||
mRoMeta.setListApi(listApi);
|
||||
}
|
||||
|
||||
void NroProcess::setListSymbols(bool listSymbols)
|
||||
{
|
||||
mListSymbols = listSymbols;
|
||||
mRoMeta.setListSymbols(listSymbols);
|
||||
}
|
||||
|
||||
void NroProcess::setAssetListFs(bool list)
|
||||
|
@ -108,8 +104,8 @@ void NroProcess::importHeader()
|
|||
|
||||
mHdr.importBinary(scratch.getBytes(), scratch.getSize());
|
||||
|
||||
// setup homebrew extension
|
||||
nx::sNroHeader* raw_hdr = (nx::sNroHeader*)scratch.getBytes();
|
||||
|
||||
if (((le_uint64_t*)raw_hdr->reserved_0)->get() == nx::nro::kNroHomebrewSig && mFile->size() > mHdr.getNroSize())
|
||||
{
|
||||
mIsHomebrewNro = true;
|
||||
|
@ -119,7 +115,6 @@ void NroProcess::importHeader()
|
|||
}
|
||||
else
|
||||
mIsHomebrewNro = false;
|
||||
|
||||
}
|
||||
|
||||
void NroProcess::importCodeSegments()
|
||||
|
@ -132,38 +127,6 @@ void NroProcess::importCodeSegments()
|
|||
mFile->read(mDataBlob.getBytes(), mHdr.getDataInfo().memory_offset, mDataBlob.getSize());
|
||||
}
|
||||
|
||||
void NroProcess::importApiList()
|
||||
{
|
||||
struct sLayout { size_t offset; size_t size; } api_info, dyn_str, dyn_sym;
|
||||
|
||||
api_info.offset = mHdr.getRoEmbeddedInfo().memory_offset;
|
||||
api_info.size = mHdr.getRoEmbeddedInfo().size;
|
||||
dyn_str.offset = mHdr.getRoDynStrInfo().memory_offset;
|
||||
dyn_str.size = mHdr.getRoDynStrInfo().size;
|
||||
dyn_sym.offset = mHdr.getRoDynSymInfo().memory_offset;
|
||||
dyn_sym.size = mHdr.getRoDynSymInfo().size;
|
||||
|
||||
if (api_info.size > 0)
|
||||
{
|
||||
std::stringstream list_stream(std::string((char*)mRoBlob.getBytes() + api_info.offset, api_info.size));
|
||||
std::string api;
|
||||
|
||||
while(std::getline(list_stream, api, (char)0x00))
|
||||
{
|
||||
mApiList.push_back(api);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mApiList.clear();
|
||||
}
|
||||
|
||||
if (dyn_sym.size > 0)
|
||||
{
|
||||
mDynSymbolList.parseData(mRoBlob.getBytes() + dyn_sym.offset, dyn_sym.size, mRoBlob.getBytes() + dyn_str.offset, dyn_str.size, mInstructionType == nx::npdm::INSTR_64BIT);
|
||||
}
|
||||
}
|
||||
|
||||
void NroProcess::displayHeader()
|
||||
{
|
||||
#define _HEXDUMP_L(var, len) do { for (size_t a__a__A = 0; a__a__A < len; a__a__A++) printf("%02x", var[a__a__A]); } while(0)
|
||||
|
@ -203,125 +166,16 @@ void NroProcess::displayHeader()
|
|||
#undef _HEXDUMP_L
|
||||
}
|
||||
|
||||
void NroProcess::displayRoMetaData()
|
||||
void NroProcess::processRoMeta()
|
||||
{
|
||||
if (mApiList.size() > 0 && (mListApi || _HAS_BIT(mCliOutputMode, OUTPUT_EXTENDED)))
|
||||
if (mRoBlob.getSize())
|
||||
{
|
||||
printf("[SDK API List]\n");
|
||||
for (size_t i = 0; i < mApiList.size(); i++)
|
||||
{
|
||||
printf(" API %d:\n", (int)i);
|
||||
printf(" Type: %s\n", getApiTypeStr(mApiList[i].getApiType()));
|
||||
printf(" Vender: %s\n", mApiList[i].getVenderName().c_str());
|
||||
printf(" Module: %s\n", mApiList[i].getModuleName().c_str());
|
||||
// setup ro metadata
|
||||
mRoMeta.setApiInfo(mHdr.getRoEmbeddedInfo().memory_offset, mHdr.getRoEmbeddedInfo().size);
|
||||
mRoMeta.setDynSym(mHdr.getRoDynSymInfo().memory_offset, mHdr.getRoDynSymInfo().size);
|
||||
mRoMeta.setDynStr(mHdr.getRoDynStrInfo().memory_offset, mHdr.getRoDynStrInfo().size);
|
||||
mRoMeta.setRoBinary(mRoBlob);
|
||||
mRoMeta.setCliOutputMode(mCliOutputMode);
|
||||
mRoMeta.process();
|
||||
}
|
||||
}
|
||||
if (mDynSymbolList.getDynamicSymbolList().getSize() > 0 && (mListSymbols || _HAS_BIT(mCliOutputMode, OUTPUT_EXTENDED)))
|
||||
{
|
||||
printf("[Symbol List]\n");
|
||||
for (size_t i = 0; i < mDynSymbolList.getDynamicSymbolList().getSize(); i++)
|
||||
{
|
||||
const DynamicSymbolParser::sDynSymbol& symbol = mDynSymbolList.getDynamicSymbolList()[i];
|
||||
printf(" %s [SHN=%s (%04x)][STT=%s]\n", symbol.name.c_str(), getSectionIndexStr(symbol.shn_index), symbol.shn_index, getSymbolTypeStr(symbol.symbol_type));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const char* NroProcess::getApiTypeStr(SdkApiString::ApiType type) const
|
||||
{
|
||||
const char* str;
|
||||
switch (type)
|
||||
{
|
||||
case (SdkApiString::API_MIDDLEWARE):
|
||||
str = "Middleware";
|
||||
break;
|
||||
case (SdkApiString::API_DEBUG):
|
||||
str = "Debug";
|
||||
break;
|
||||
case (SdkApiString::API_PRIVATE):
|
||||
str = "Private";
|
||||
break;
|
||||
case (SdkApiString::API_SDK_VERSION):
|
||||
str = "SDK Version";
|
||||
break;
|
||||
default:
|
||||
str = "UNKNOWN";
|
||||
break;
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
const char* NroProcess::getSectionIndexStr(nx::dynsym::SpecialSectionIndex shn_index) const
|
||||
{
|
||||
const char* str;
|
||||
switch (shn_index)
|
||||
{
|
||||
case (nx::dynsym::SHN_UNDEF):
|
||||
str = "UNDEF";
|
||||
break;
|
||||
case (nx::dynsym::SHN_EXPORT):
|
||||
str = "EXPORT";
|
||||
break;
|
||||
case (nx::dynsym::SHN_LOPROC):
|
||||
str = "LOPROC";
|
||||
break;
|
||||
case (nx::dynsym::SHN_HIPROC):
|
||||
str = "HIPROC";
|
||||
break;
|
||||
case (nx::dynsym::SHN_LOOS):
|
||||
str = "LOOS";
|
||||
break;
|
||||
case (nx::dynsym::SHN_HIOS):
|
||||
str = "HIOS";
|
||||
break;
|
||||
case (nx::dynsym::SHN_ABS):
|
||||
str = "ABS";
|
||||
break;
|
||||
case (nx::dynsym::SHN_COMMON):
|
||||
str = "COMMON";
|
||||
break;
|
||||
default:
|
||||
str = "UNKNOWN";
|
||||
break;
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
const char* NroProcess::getSymbolTypeStr(nx::dynsym::SymbolType symbol_type) const
|
||||
{
|
||||
const char* str;
|
||||
switch (symbol_type)
|
||||
{
|
||||
case (nx::dynsym::STT_NOTYPE):
|
||||
str = "NOTYPE";
|
||||
break;
|
||||
case (nx::dynsym::STT_OBJECT):
|
||||
str = "OBJECT";
|
||||
break;
|
||||
case (nx::dynsym::STT_FUNC):
|
||||
str = "FUNC";
|
||||
break;
|
||||
case (nx::dynsym::STT_SECTION):
|
||||
str = "SECTION";
|
||||
break;
|
||||
case (nx::dynsym::STT_FILE):
|
||||
str = "FILE";
|
||||
break;
|
||||
case (nx::dynsym::STT_LOOS):
|
||||
str = "LOOS";
|
||||
break;
|
||||
case (nx::dynsym::STT_HIOS):
|
||||
str = "HIOS";
|
||||
break;
|
||||
case (nx::dynsym::STT_LOPROC):
|
||||
str = "LOPROC";
|
||||
break;
|
||||
case (nx::dynsym::STT_HIPROC):
|
||||
str = "HIPROC";
|
||||
break;
|
||||
default:
|
||||
str = "UNKNOWN";
|
||||
break;
|
||||
}
|
||||
return str;
|
||||
}
|
|
@ -8,8 +8,7 @@
|
|||
#include "AssetProcess.h"
|
||||
|
||||
#include "nstool.h"
|
||||
#include "SdkApiString.h"
|
||||
#include "DynamicSymbolParser.h"
|
||||
#include "RoMetadataProcess.h"
|
||||
|
||||
class NroProcess
|
||||
{
|
||||
|
@ -40,24 +39,15 @@ private:
|
|||
|
||||
CliOutputMode mCliOutputMode;
|
||||
bool mVerify;
|
||||
nx::npdm::InstructionType mInstructionType;
|
||||
bool mListApi;
|
||||
bool mListSymbols;
|
||||
|
||||
nx::NroHeader mHdr;
|
||||
fnd::MemoryBlob mTextBlob, mRoBlob, mDataBlob;
|
||||
RoMetadataProcess mRoMeta;
|
||||
bool mIsHomebrewNro;
|
||||
AssetProcess mAssetProc;
|
||||
fnd::MemoryBlob mTextBlob, mRoBlob, mDataBlob;
|
||||
std::vector<SdkApiString> mApiList;
|
||||
DynamicSymbolParser mDynSymbolList;
|
||||
|
||||
void importHeader();
|
||||
void importCodeSegments();
|
||||
void importApiList();
|
||||
void displayHeader();
|
||||
void displayRoMetaData();
|
||||
|
||||
const char* getApiTypeStr(SdkApiString::ApiType type) const;
|
||||
const char* getSectionIndexStr(nx::dynsym::SpecialSectionIndex shn_index) const;
|
||||
const char* getSymbolTypeStr(nx::dynsym::SymbolType symbol_type) const;
|
||||
void processRoMeta();
|
||||
};
|
|
@ -1,4 +1,3 @@
|
|||
#include <sstream>
|
||||
#include <fnd/SimpleTextOutput.h>
|
||||
#include <fnd/MemoryBlob.h>
|
||||
#include <compress/lz4.h>
|
||||
|
@ -9,10 +8,7 @@ NsoProcess::NsoProcess():
|
|||
mFile(nullptr),
|
||||
mOwnIFile(false),
|
||||
mCliOutputMode(_BIT(OUTPUT_BASIC)),
|
||||
mVerify(false),
|
||||
mInstructionType(nx::npdm::INSTR_64BIT),
|
||||
mListApi(false),
|
||||
mListSymbols(false)
|
||||
mVerify(false)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -33,12 +29,10 @@ void NsoProcess::process()
|
|||
|
||||
importHeader();
|
||||
importCodeSegments();
|
||||
importApiList();
|
||||
if (_HAS_BIT(mCliOutputMode, OUTPUT_BASIC))
|
||||
{
|
||||
displayNsoHeader();
|
||||
displayRoMetaData();
|
||||
}
|
||||
|
||||
processRoMeta();
|
||||
}
|
||||
|
||||
void NsoProcess::setInputFile(fnd::IFile* file, bool ownIFile)
|
||||
|
@ -59,17 +53,17 @@ void NsoProcess::setVerifyMode(bool verify)
|
|||
|
||||
void NsoProcess::setInstructionType(nx::npdm::InstructionType type)
|
||||
{
|
||||
mInstructionType = type;
|
||||
mRoMeta.setInstructionType(type);
|
||||
}
|
||||
|
||||
void NsoProcess::setListApi(bool listApi)
|
||||
{
|
||||
mListApi = listApi;
|
||||
mRoMeta.setListApi(listApi);
|
||||
}
|
||||
|
||||
void NsoProcess::setListSymbols(bool listSymbols)
|
||||
{
|
||||
mListSymbols = listSymbols;
|
||||
mRoMeta.setListSymbols(listSymbols);
|
||||
}
|
||||
|
||||
void NsoProcess::importHeader()
|
||||
|
@ -83,7 +77,7 @@ void NsoProcess::importHeader()
|
|||
scratch.alloc(sizeof(nx::sNsoHeader));
|
||||
mFile->read(scratch.getBytes(), 0, scratch.getSize());
|
||||
|
||||
mNsoHdr.importBinary(scratch.getBytes(), scratch.getSize());
|
||||
mHdr.importBinary(scratch.getBytes(), scratch.getSize());
|
||||
}
|
||||
|
||||
void NsoProcess::importCodeSegments()
|
||||
|
@ -93,11 +87,11 @@ void NsoProcess::importCodeSegments()
|
|||
crypto::sha::sSha256Hash calc_hash;
|
||||
|
||||
// process text segment
|
||||
if (mNsoHdr.getTextSegmentInfo().is_compressed)
|
||||
if (mHdr.getTextSegmentInfo().is_compressed)
|
||||
{
|
||||
scratch.alloc(mNsoHdr.getTextSegmentInfo().file_layout.size);
|
||||
mFile->read(scratch.getBytes(), mNsoHdr.getTextSegmentInfo().file_layout.offset, scratch.getSize());
|
||||
mTextBlob.alloc(mNsoHdr.getTextSegmentInfo().memory_layout.size);
|
||||
scratch.alloc(mHdr.getTextSegmentInfo().file_layout.size);
|
||||
mFile->read(scratch.getBytes(), mHdr.getTextSegmentInfo().file_layout.offset, scratch.getSize());
|
||||
mTextBlob.alloc(mHdr.getTextSegmentInfo().memory_layout.size);
|
||||
compress::lz4::decompressData(scratch.getBytes(), (uint32_t)scratch.getSize(), mTextBlob.getBytes(), (uint32_t)mTextBlob.getSize(), decompressed_len);
|
||||
if (decompressed_len != mTextBlob.getSize())
|
||||
{
|
||||
|
@ -106,24 +100,24 @@ void NsoProcess::importCodeSegments()
|
|||
}
|
||||
else
|
||||
{
|
||||
mTextBlob.alloc(mNsoHdr.getTextSegmentInfo().file_layout.size);
|
||||
mFile->read(mTextBlob.getBytes(), mNsoHdr.getTextSegmentInfo().file_layout.offset, mTextBlob.getSize());
|
||||
mTextBlob.alloc(mHdr.getTextSegmentInfo().file_layout.size);
|
||||
mFile->read(mTextBlob.getBytes(), mHdr.getTextSegmentInfo().file_layout.offset, mTextBlob.getSize());
|
||||
}
|
||||
if (mNsoHdr.getTextSegmentInfo().is_hashed)
|
||||
if (mHdr.getTextSegmentInfo().is_hashed)
|
||||
{
|
||||
crypto::sha::Sha256(mTextBlob.getBytes(), mTextBlob.getSize(), calc_hash.bytes);
|
||||
if (calc_hash != mNsoHdr.getTextSegmentInfo().hash)
|
||||
if (calc_hash != mHdr.getTextSegmentInfo().hash)
|
||||
{
|
||||
throw fnd::Exception(kModuleName, "NSO text segment failed SHA256 verification");
|
||||
}
|
||||
}
|
||||
|
||||
// process ro segment
|
||||
if (mNsoHdr.getRoSegmentInfo().is_compressed)
|
||||
if (mHdr.getRoSegmentInfo().is_compressed)
|
||||
{
|
||||
scratch.alloc(mNsoHdr.getRoSegmentInfo().file_layout.size);
|
||||
mFile->read(scratch.getBytes(), mNsoHdr.getRoSegmentInfo().file_layout.offset, scratch.getSize());
|
||||
mRoBlob.alloc(mNsoHdr.getRoSegmentInfo().memory_layout.size);
|
||||
scratch.alloc(mHdr.getRoSegmentInfo().file_layout.size);
|
||||
mFile->read(scratch.getBytes(), mHdr.getRoSegmentInfo().file_layout.offset, scratch.getSize());
|
||||
mRoBlob.alloc(mHdr.getRoSegmentInfo().memory_layout.size);
|
||||
compress::lz4::decompressData(scratch.getBytes(), (uint32_t)scratch.getSize(), mRoBlob.getBytes(), (uint32_t)mRoBlob.getSize(), decompressed_len);
|
||||
if (decompressed_len != mRoBlob.getSize())
|
||||
{
|
||||
|
@ -132,24 +126,24 @@ void NsoProcess::importCodeSegments()
|
|||
}
|
||||
else
|
||||
{
|
||||
mRoBlob.alloc(mNsoHdr.getRoSegmentInfo().file_layout.size);
|
||||
mFile->read(mRoBlob.getBytes(), mNsoHdr.getRoSegmentInfo().file_layout.offset, mRoBlob.getSize());
|
||||
mRoBlob.alloc(mHdr.getRoSegmentInfo().file_layout.size);
|
||||
mFile->read(mRoBlob.getBytes(), mHdr.getRoSegmentInfo().file_layout.offset, mRoBlob.getSize());
|
||||
}
|
||||
if (mNsoHdr.getRoSegmentInfo().is_hashed)
|
||||
if (mHdr.getRoSegmentInfo().is_hashed)
|
||||
{
|
||||
crypto::sha::Sha256(mRoBlob.getBytes(), mRoBlob.getSize(), calc_hash.bytes);
|
||||
if (calc_hash != mNsoHdr.getRoSegmentInfo().hash)
|
||||
if (calc_hash != mHdr.getRoSegmentInfo().hash)
|
||||
{
|
||||
throw fnd::Exception(kModuleName, "NSO ro segment failed SHA256 verification");
|
||||
}
|
||||
}
|
||||
|
||||
// process data segment
|
||||
if (mNsoHdr.getDataSegmentInfo().is_compressed)
|
||||
if (mHdr.getDataSegmentInfo().is_compressed)
|
||||
{
|
||||
scratch.alloc(mNsoHdr.getDataSegmentInfo().file_layout.size);
|
||||
mFile->read(scratch.getBytes(), mNsoHdr.getDataSegmentInfo().file_layout.offset, scratch.getSize());
|
||||
mDataBlob.alloc(mNsoHdr.getDataSegmentInfo().memory_layout.size);
|
||||
scratch.alloc(mHdr.getDataSegmentInfo().file_layout.size);
|
||||
mFile->read(scratch.getBytes(), mHdr.getDataSegmentInfo().file_layout.offset, scratch.getSize());
|
||||
mDataBlob.alloc(mHdr.getDataSegmentInfo().memory_layout.size);
|
||||
compress::lz4::decompressData(scratch.getBytes(), (uint32_t)scratch.getSize(), mDataBlob.getBytes(), (uint32_t)mDataBlob.getSize(), decompressed_len);
|
||||
if (decompressed_len != mDataBlob.getSize())
|
||||
{
|
||||
|
@ -158,239 +152,99 @@ void NsoProcess::importCodeSegments()
|
|||
}
|
||||
else
|
||||
{
|
||||
mDataBlob.alloc(mNsoHdr.getDataSegmentInfo().file_layout.size);
|
||||
mFile->read(mDataBlob.getBytes(), mNsoHdr.getDataSegmentInfo().file_layout.offset, mDataBlob.getSize());
|
||||
mDataBlob.alloc(mHdr.getDataSegmentInfo().file_layout.size);
|
||||
mFile->read(mDataBlob.getBytes(), mHdr.getDataSegmentInfo().file_layout.offset, mDataBlob.getSize());
|
||||
}
|
||||
if (mNsoHdr.getDataSegmentInfo().is_hashed)
|
||||
if (mHdr.getDataSegmentInfo().is_hashed)
|
||||
{
|
||||
crypto::sha::Sha256(mDataBlob.getBytes(), mDataBlob.getSize(), calc_hash.bytes);
|
||||
if (calc_hash != mNsoHdr.getDataSegmentInfo().hash)
|
||||
if (calc_hash != mHdr.getDataSegmentInfo().hash)
|
||||
{
|
||||
throw fnd::Exception(kModuleName, "NSO data segment failed SHA256 verification");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void NsoProcess::importApiList()
|
||||
{
|
||||
struct sLayout { size_t offset; size_t size; } api_info, dyn_str, dyn_sym;
|
||||
|
||||
api_info.offset = mNsoHdr.getRoEmbeddedInfo().offset;
|
||||
api_info.size = mNsoHdr.getRoEmbeddedInfo().size;
|
||||
dyn_str.offset = mNsoHdr.getRoDynStrInfo().offset;
|
||||
dyn_str.size = mNsoHdr.getRoDynStrInfo().size;
|
||||
dyn_sym.offset = mNsoHdr.getRoDynSymInfo().offset;
|
||||
dyn_sym.size = mNsoHdr.getRoDynSymInfo().size;
|
||||
|
||||
if (api_info.size > 0)
|
||||
{
|
||||
std::stringstream list_stream(std::string((char*)mRoBlob.getBytes() + api_info.offset, api_info.size));
|
||||
std::string api;
|
||||
|
||||
while(std::getline(list_stream, api, (char)0x00))
|
||||
{
|
||||
mApiList.push_back(api);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mApiList.clear();
|
||||
}
|
||||
|
||||
if (dyn_sym.size > 0)
|
||||
{
|
||||
mDynSymbolList.parseData(mRoBlob.getBytes() + dyn_sym.offset, dyn_sym.size, mRoBlob.getBytes() + dyn_str.offset, dyn_str.size, mInstructionType == nx::npdm::INSTR_64BIT);
|
||||
}
|
||||
}
|
||||
|
||||
void NsoProcess::displayNsoHeader()
|
||||
{
|
||||
#define _HEXDUMP_L(var, len) do { for (size_t a__a__A = 0; a__a__A < len; a__a__A++) printf("%02x", var[a__a__A]); } while(0)
|
||||
|
||||
printf("[NSO Header]\n");
|
||||
printf(" ModuleId: ");
|
||||
_HEXDUMP_L(mNsoHdr.getModuleId().data, nx::nso::kModuleIdSize);
|
||||
_HEXDUMP_L(mHdr.getModuleId().data, nx::nso::kModuleIdSize);
|
||||
printf("\n");
|
||||
if (_HAS_BIT(mCliOutputMode, OUTPUT_LAYOUT))
|
||||
{
|
||||
printf(" Program Segments:\n");
|
||||
printf(" .module_name:\n");
|
||||
printf(" FileOffset: 0x%" PRIx32 "\n", mNsoHdr.getModuleNameInfo().offset);
|
||||
printf(" FileSize: 0x%" PRIx32 "\n", mNsoHdr.getModuleNameInfo().size);
|
||||
printf(" FileOffset: 0x%" PRIx32 "\n", mHdr.getModuleNameInfo().offset);
|
||||
printf(" FileSize: 0x%" PRIx32 "\n", mHdr.getModuleNameInfo().size);
|
||||
printf(" .text:\n");
|
||||
printf(" FileOffset: 0x%" PRIx32 "\n", mNsoHdr.getTextSegmentInfo().file_layout.offset);
|
||||
printf(" FileSize: 0x%" PRIx32 "%s\n", mNsoHdr.getTextSegmentInfo().file_layout.size, mNsoHdr.getTextSegmentInfo().is_compressed? " (COMPRESSED)" : "");
|
||||
printf(" FileOffset: 0x%" PRIx32 "\n", mHdr.getTextSegmentInfo().file_layout.offset);
|
||||
printf(" FileSize: 0x%" PRIx32 "%s\n", mHdr.getTextSegmentInfo().file_layout.size, mHdr.getTextSegmentInfo().is_compressed? " (COMPRESSED)" : "");
|
||||
printf(" .ro:\n");
|
||||
printf(" FileOffset: 0x%" PRIx32 "\n", mNsoHdr.getRoSegmentInfo().file_layout.offset);
|
||||
printf(" FileSize: 0x%" PRIx32 "%s\n", mNsoHdr.getRoSegmentInfo().file_layout.size, mNsoHdr.getRoSegmentInfo().is_compressed? " (COMPRESSED)" : "");
|
||||
printf(" FileOffset: 0x%" PRIx32 "\n", mHdr.getRoSegmentInfo().file_layout.offset);
|
||||
printf(" FileSize: 0x%" PRIx32 "%s\n", mHdr.getRoSegmentInfo().file_layout.size, mHdr.getRoSegmentInfo().is_compressed? " (COMPRESSED)" : "");
|
||||
printf(" .data:\n");
|
||||
printf(" FileOffset: 0x%" PRIx32 "\n", mNsoHdr.getDataSegmentInfo().file_layout.offset);
|
||||
printf(" FileSize: 0x%" PRIx32 "%s\n", mNsoHdr.getDataSegmentInfo().file_layout.size, mNsoHdr.getDataSegmentInfo().is_compressed? " (COMPRESSED)" : "");
|
||||
printf(" FileOffset: 0x%" PRIx32 "\n", mHdr.getDataSegmentInfo().file_layout.offset);
|
||||
printf(" FileSize: 0x%" PRIx32 "%s\n", mHdr.getDataSegmentInfo().file_layout.size, mHdr.getDataSegmentInfo().is_compressed? " (COMPRESSED)" : "");
|
||||
}
|
||||
printf(" Program Sections:\n");
|
||||
printf(" .text:\n");
|
||||
printf(" MemoryOffset: 0x%" PRIx32 "\n", mNsoHdr.getTextSegmentInfo().memory_layout.offset);
|
||||
printf(" MemorySize: 0x%" PRIx32 "\n", mNsoHdr.getTextSegmentInfo().memory_layout.size);
|
||||
if (mNsoHdr.getTextSegmentInfo().is_hashed && _HAS_BIT(mCliOutputMode, OUTPUT_EXTENDED))
|
||||
printf(" MemoryOffset: 0x%" PRIx32 "\n", mHdr.getTextSegmentInfo().memory_layout.offset);
|
||||
printf(" MemorySize: 0x%" PRIx32 "\n", mHdr.getTextSegmentInfo().memory_layout.size);
|
||||
if (mHdr.getTextSegmentInfo().is_hashed && _HAS_BIT(mCliOutputMode, OUTPUT_EXTENDED))
|
||||
{
|
||||
printf(" Hash: ");
|
||||
_HEXDUMP_L(mNsoHdr.getTextSegmentInfo().hash.bytes, 32);
|
||||
_HEXDUMP_L(mHdr.getTextSegmentInfo().hash.bytes, 32);
|
||||
printf("\n");
|
||||
}
|
||||
printf(" .ro:\n");
|
||||
printf(" MemoryOffset: 0x%" PRIx32 "\n", mNsoHdr.getRoSegmentInfo().memory_layout.offset);
|
||||
printf(" MemorySize: 0x%" PRIx32 "\n", mNsoHdr.getRoSegmentInfo().memory_layout.size);
|
||||
if (mNsoHdr.getRoSegmentInfo().is_hashed && _HAS_BIT(mCliOutputMode, OUTPUT_EXTENDED))
|
||||
printf(" MemoryOffset: 0x%" PRIx32 "\n", mHdr.getRoSegmentInfo().memory_layout.offset);
|
||||
printf(" MemorySize: 0x%" PRIx32 "\n", mHdr.getRoSegmentInfo().memory_layout.size);
|
||||
if (mHdr.getRoSegmentInfo().is_hashed && _HAS_BIT(mCliOutputMode, OUTPUT_EXTENDED))
|
||||
{
|
||||
printf(" Hash: ");
|
||||
_HEXDUMP_L(mNsoHdr.getRoSegmentInfo().hash.bytes, 32);
|
||||
_HEXDUMP_L(mHdr.getRoSegmentInfo().hash.bytes, 32);
|
||||
printf("\n");
|
||||
}
|
||||
if (_HAS_BIT(mCliOutputMode, OUTPUT_EXTENDED))
|
||||
{
|
||||
printf(" .api_info:\n");
|
||||
printf(" MemoryOffset: 0x%" PRIx32 "\n", mNsoHdr.getRoEmbeddedInfo().offset);
|
||||
printf(" MemorySize: 0x%" PRIx32 "\n", mNsoHdr.getRoEmbeddedInfo().size);
|
||||
printf(" MemoryOffset: 0x%" PRIx32 "\n", mHdr.getRoEmbeddedInfo().offset);
|
||||
printf(" MemorySize: 0x%" PRIx32 "\n", mHdr.getRoEmbeddedInfo().size);
|
||||
printf(" .dynstr:\n");
|
||||
printf(" MemoryOffset: 0x%" PRIx32 "\n", mNsoHdr.getRoDynStrInfo().offset);
|
||||
printf(" MemorySize: 0x%" PRIx32 "\n", mNsoHdr.getRoDynStrInfo().size);
|
||||
printf(" MemoryOffset: 0x%" PRIx32 "\n", mHdr.getRoDynStrInfo().offset);
|
||||
printf(" MemorySize: 0x%" PRIx32 "\n", mHdr.getRoDynStrInfo().size);
|
||||
printf(" .dynsym:\n");
|
||||
printf(" MemoryOffset: 0x%" PRIx32 "\n", mNsoHdr.getRoDynSymInfo().offset);
|
||||
printf(" MemorySize: 0x%" PRIx32 "\n", mNsoHdr.getRoDynSymInfo().size);
|
||||
printf(" MemoryOffset: 0x%" PRIx32 "\n", mHdr.getRoDynSymInfo().offset);
|
||||
printf(" MemorySize: 0x%" PRIx32 "\n", mHdr.getRoDynSymInfo().size);
|
||||
}
|
||||
|
||||
printf(" .data:\n");
|
||||
printf(" MemoryOffset: 0x%" PRIx32 "\n", mNsoHdr.getDataSegmentInfo().memory_layout.offset);
|
||||
printf(" MemorySize: 0x%" PRIx32 "\n", mNsoHdr.getDataSegmentInfo().memory_layout.size);
|
||||
if (mNsoHdr.getDataSegmentInfo().is_hashed && _HAS_BIT(mCliOutputMode, OUTPUT_EXTENDED))
|
||||
printf(" MemoryOffset: 0x%" PRIx32 "\n", mHdr.getDataSegmentInfo().memory_layout.offset);
|
||||
printf(" MemorySize: 0x%" PRIx32 "\n", mHdr.getDataSegmentInfo().memory_layout.size);
|
||||
if (mHdr.getDataSegmentInfo().is_hashed && _HAS_BIT(mCliOutputMode, OUTPUT_EXTENDED))
|
||||
{
|
||||
printf(" Hash: ");
|
||||
_HEXDUMP_L(mNsoHdr.getDataSegmentInfo().hash.bytes, 32);
|
||||
_HEXDUMP_L(mHdr.getDataSegmentInfo().hash.bytes, 32);
|
||||
printf("\n");
|
||||
}
|
||||
printf(" .bss:\n");
|
||||
printf(" MemorySize: 0x%" PRIx32 "\n", mNsoHdr.getBssSize());
|
||||
printf(" MemorySize: 0x%" PRIx32 "\n", mHdr.getBssSize());
|
||||
#undef _HEXDUMP_L
|
||||
}
|
||||
|
||||
void NsoProcess::displayRoMetaData()
|
||||
void NsoProcess::processRoMeta()
|
||||
{
|
||||
if (mApiList.size() > 0 && (mListApi || _HAS_BIT(mCliOutputMode, OUTPUT_EXTENDED)))
|
||||
if (mRoBlob.getSize())
|
||||
{
|
||||
printf("[SDK API List]\n");
|
||||
for (size_t i = 0; i < mApiList.size(); i++)
|
||||
{
|
||||
printf(" API %d:\n", (int)i);
|
||||
printf(" Type: %s\n", getApiTypeStr(mApiList[i].getApiType()));
|
||||
printf(" Vender: %s\n", mApiList[i].getVenderName().c_str());
|
||||
printf(" Module: %s\n", mApiList[i].getModuleName().c_str());
|
||||
// setup ro metadata
|
||||
mRoMeta.setApiInfo(mHdr.getRoEmbeddedInfo().offset, mHdr.getRoEmbeddedInfo().size);
|
||||
mRoMeta.setDynSym(mHdr.getRoDynSymInfo().offset, mHdr.getRoDynSymInfo().size);
|
||||
mRoMeta.setDynStr(mHdr.getRoDynStrInfo().offset, mHdr.getRoDynStrInfo().size);
|
||||
mRoMeta.setRoBinary(mRoBlob);
|
||||
mRoMeta.setCliOutputMode(mCliOutputMode);
|
||||
mRoMeta.process();
|
||||
}
|
||||
}
|
||||
if (mDynSymbolList.getDynamicSymbolList().getSize() > 0 && (mListSymbols || _HAS_BIT(mCliOutputMode, OUTPUT_EXTENDED)))
|
||||
{
|
||||
printf("[Symbol List]\n");
|
||||
for (size_t i = 0; i < mDynSymbolList.getDynamicSymbolList().getSize(); i++)
|
||||
{
|
||||
const DynamicSymbolParser::sDynSymbol& symbol = mDynSymbolList.getDynamicSymbolList()[i];
|
||||
printf(" %s [SHN=%s (%04x)][STT=%s]\n", symbol.name.c_str(), getSectionIndexStr(symbol.shn_index), symbol.shn_index, getSymbolTypeStr(symbol.symbol_type));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const char* NsoProcess::getApiTypeStr(SdkApiString::ApiType type) const
|
||||
{
|
||||
const char* str;
|
||||
switch (type)
|
||||
{
|
||||
case (SdkApiString::API_MIDDLEWARE):
|
||||
str = "Middleware";
|
||||
break;
|
||||
case (SdkApiString::API_DEBUG):
|
||||
str = "Debug";
|
||||
break;
|
||||
case (SdkApiString::API_PRIVATE):
|
||||
str = "Private";
|
||||
break;
|
||||
case (SdkApiString::API_SDK_VERSION):
|
||||
str = "SDK Version";
|
||||
break;
|
||||
default:
|
||||
str = "UNKNOWN";
|
||||
break;
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
const char* NsoProcess::getSectionIndexStr(nx::dynsym::SpecialSectionIndex shn_index) const
|
||||
{
|
||||
const char* str;
|
||||
switch (shn_index)
|
||||
{
|
||||
case (nx::dynsym::SHN_UNDEF):
|
||||
str = "UNDEF";
|
||||
break;
|
||||
case (nx::dynsym::SHN_EXPORT):
|
||||
str = "EXPORT";
|
||||
break;
|
||||
case (nx::dynsym::SHN_LOPROC):
|
||||
str = "LOPROC";
|
||||
break;
|
||||
case (nx::dynsym::SHN_HIPROC):
|
||||
str = "HIPROC";
|
||||
break;
|
||||
case (nx::dynsym::SHN_LOOS):
|
||||
str = "LOOS";
|
||||
break;
|
||||
case (nx::dynsym::SHN_HIOS):
|
||||
str = "HIOS";
|
||||
break;
|
||||
case (nx::dynsym::SHN_ABS):
|
||||
str = "ABS";
|
||||
break;
|
||||
case (nx::dynsym::SHN_COMMON):
|
||||
str = "COMMON";
|
||||
break;
|
||||
default:
|
||||
str = "UNKNOWN";
|
||||
break;
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
const char* NsoProcess::getSymbolTypeStr(nx::dynsym::SymbolType symbol_type) const
|
||||
{
|
||||
const char* str;
|
||||
switch (symbol_type)
|
||||
{
|
||||
case (nx::dynsym::STT_NOTYPE):
|
||||
str = "NOTYPE";
|
||||
break;
|
||||
case (nx::dynsym::STT_OBJECT):
|
||||
str = "OBJECT";
|
||||
break;
|
||||
case (nx::dynsym::STT_FUNC):
|
||||
str = "FUNC";
|
||||
break;
|
||||
case (nx::dynsym::STT_SECTION):
|
||||
str = "SECTION";
|
||||
break;
|
||||
case (nx::dynsym::STT_FILE):
|
||||
str = "FILE";
|
||||
break;
|
||||
case (nx::dynsym::STT_LOOS):
|
||||
str = "LOOS";
|
||||
break;
|
||||
case (nx::dynsym::STT_HIOS):
|
||||
str = "HIOS";
|
||||
break;
|
||||
case (nx::dynsym::STT_LOPROC):
|
||||
str = "LOPROC";
|
||||
break;
|
||||
case (nx::dynsym::STT_HIPROC):
|
||||
str = "HIPROC";
|
||||
break;
|
||||
default:
|
||||
str = "UNKNOWN";
|
||||
break;
|
||||
}
|
||||
return str;
|
||||
}
|
|
@ -7,8 +7,7 @@
|
|||
#include <nx/NsoHeader.h>
|
||||
|
||||
#include "nstool.h"
|
||||
#include "SdkApiString.h"
|
||||
#include "DynamicSymbolParser.h"
|
||||
#include "RoMetadataProcess.h"
|
||||
|
||||
class NsoProcess
|
||||
{
|
||||
|
@ -37,18 +36,12 @@ private:
|
|||
bool mListApi;
|
||||
bool mListSymbols;
|
||||
|
||||
nx::NsoHeader mNsoHdr;
|
||||
nx::NsoHeader mHdr;
|
||||
fnd::MemoryBlob mTextBlob, mRoBlob, mDataBlob;
|
||||
std::vector<SdkApiString> mApiList;
|
||||
DynamicSymbolParser mDynSymbolList;
|
||||
RoMetadataProcess mRoMeta;
|
||||
|
||||
void importHeader();
|
||||
void importCodeSegments();
|
||||
void importApiList();
|
||||
void displayNsoHeader();
|
||||
void displayRoMetaData();
|
||||
|
||||
const char* getApiTypeStr(SdkApiString::ApiType type) const;
|
||||
const char* getSectionIndexStr(nx::dynsym::SpecialSectionIndex shn_index) const;
|
||||
const char* getSymbolTypeStr(nx::dynsym::SymbolType symbol_type) const;
|
||||
void processRoMeta();
|
||||
};
|
255
programs/nstool/source/RoMetadataProcess.cpp
Normal file
255
programs/nstool/source/RoMetadataProcess.cpp
Normal file
|
@ -0,0 +1,255 @@
|
|||
#include <sstream>
|
||||
#include <fnd/types.h>
|
||||
|
||||
#include "RoMetadataProcess.h"
|
||||
|
||||
RoMetadataProcess::RoMetadataProcess() :
|
||||
mCliOutputMode(_BIT(OUTPUT_BASIC)),
|
||||
mInstructionType(nx::npdm::INSTR_64BIT),
|
||||
mListApi(false),
|
||||
mListSymbols(false),
|
||||
mApiInfo(),
|
||||
mDynSym(),
|
||||
mDynStr(),
|
||||
mRoBlob(),
|
||||
mSdkVerApiList(),
|
||||
mPublicApiList(),
|
||||
mDebugApiList(),
|
||||
mPrivateApiList(),
|
||||
mSymbolList()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
void RoMetadataProcess::process()
|
||||
{
|
||||
if (mRoBlob.getSize() == 0)
|
||||
{
|
||||
throw fnd::Exception(kModuleName, "No ro binary set.");
|
||||
}
|
||||
|
||||
importApiList();
|
||||
if (_HAS_BIT(mCliOutputMode, OUTPUT_BASIC))
|
||||
displayRoMetaData();
|
||||
}
|
||||
|
||||
void RoMetadataProcess::setRoBinary(const fnd::MemoryBlob& bin)
|
||||
{
|
||||
mRoBlob = bin;
|
||||
}
|
||||
|
||||
void RoMetadataProcess::setApiInfo(size_t offset, size_t size)
|
||||
{
|
||||
mApiInfo.offset = offset;
|
||||
mApiInfo.size = size;
|
||||
}
|
||||
void RoMetadataProcess::setDynSym(size_t offset, size_t size)
|
||||
{
|
||||
mDynSym.offset = offset;
|
||||
mDynSym.size = size;
|
||||
}
|
||||
void RoMetadataProcess::setDynStr(size_t offset, size_t size)
|
||||
{
|
||||
mDynStr.offset = offset;
|
||||
mDynStr.size = size;
|
||||
}
|
||||
|
||||
void RoMetadataProcess::setCliOutputMode(CliOutputMode type)
|
||||
{
|
||||
mCliOutputMode = type;
|
||||
}
|
||||
|
||||
void RoMetadataProcess::setInstructionType(nx::npdm::InstructionType type)
|
||||
{
|
||||
mInstructionType = type;
|
||||
}
|
||||
|
||||
void RoMetadataProcess::setListApi(bool listApi)
|
||||
{
|
||||
mListApi = listApi;
|
||||
}
|
||||
|
||||
void RoMetadataProcess::setListSymbols(bool listSymbols)
|
||||
{
|
||||
mListSymbols = listSymbols;
|
||||
}
|
||||
|
||||
void RoMetadataProcess::importApiList()
|
||||
{
|
||||
if (mApiInfo.size > 0)
|
||||
{
|
||||
std::stringstream list_stream(std::string((char*)mRoBlob.getBytes() + mApiInfo.offset, mApiInfo.size));
|
||||
std::string api_str;
|
||||
|
||||
while(std::getline(list_stream, api_str, (char)0x00))
|
||||
{
|
||||
SdkApiString api(api_str);
|
||||
|
||||
if (api.getApiType() == SdkApiString::API_SDK_VERSION)
|
||||
mSdkVerApiList.push_back(api);
|
||||
else if (api.getApiType() == SdkApiString::API_MIDDLEWARE)
|
||||
mPublicApiList.push_back(api);
|
||||
else if (api.getApiType() == SdkApiString::API_DEBUG)
|
||||
mDebugApiList.push_back(api);
|
||||
else if (api.getApiType() == SdkApiString::API_PRIVATE)
|
||||
mPrivateApiList.push_back(api);
|
||||
}
|
||||
}
|
||||
|
||||
if (mDynSym.size > 0)
|
||||
{
|
||||
mSymbolList.parseData(mRoBlob.getBytes() + mDynSym.offset, mDynSym.size, mRoBlob.getBytes() + mDynStr.offset, mDynStr.size, mInstructionType == nx::npdm::INSTR_64BIT);
|
||||
}
|
||||
}
|
||||
|
||||
void RoMetadataProcess::displayRoMetaData()
|
||||
{
|
||||
size_t api_num = mSdkVerApiList.size() + mPublicApiList.size() + mDebugApiList.size() + mPrivateApiList.size();
|
||||
|
||||
if (api_num > 0 && (mListApi || _HAS_BIT(mCliOutputMode, OUTPUT_EXTENDED)))
|
||||
{
|
||||
printf("[SDK API List]\n");
|
||||
if (mSdkVerApiList.size() > 0)
|
||||
{
|
||||
printf(" Sdk Revision: %s\n", mSdkVerApiList[0].getModuleName().c_str());
|
||||
}
|
||||
if (mPublicApiList.size() > 0)
|
||||
{
|
||||
printf(" Public APIs:\n");
|
||||
for (size_t i = 0; i < mPublicApiList.size(); i++)
|
||||
{
|
||||
printf(" %s (vender: %s)\n", mPublicApiList[i].getModuleName().c_str(), mPublicApiList[i].getVenderName().c_str());
|
||||
}
|
||||
}
|
||||
if (mDebugApiList.size() > 0)
|
||||
{
|
||||
printf(" Debug APIs:\n");
|
||||
for (size_t i = 0; i < mDebugApiList.size(); i++)
|
||||
{
|
||||
printf(" %s (vender: %s)\n", mDebugApiList[i].getModuleName().c_str(), mDebugApiList[i].getVenderName().c_str());
|
||||
}
|
||||
}
|
||||
if (mPrivateApiList.size() > 0)
|
||||
{
|
||||
printf(" Private APIs:\n");
|
||||
for (size_t i = 0; i < mPrivateApiList.size(); i++)
|
||||
{
|
||||
printf(" %s (vender: %s)\n", mPrivateApiList[i].getModuleName().c_str(), mPrivateApiList[i].getVenderName().c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mSymbolList.getSymbolList().getSize() > 0 && (mListSymbols || _HAS_BIT(mCliOutputMode, OUTPUT_EXTENDED)))
|
||||
{
|
||||
printf("[Symbol List]\n");
|
||||
for (size_t i = 0; i < mSymbolList.getSymbolList().getSize(); i++)
|
||||
{
|
||||
const ElfSymbolParser::sElfSymbol& symbol = mSymbolList.getSymbolList()[i];
|
||||
printf(" %s [SHN=%s (%04x)][STT=%s][STB=%s]\n", symbol.name.c_str(), getSectionIndexStr(symbol.shn_index), symbol.shn_index, getSymbolTypeStr(symbol.symbol_type), getSymbolBindingStr(symbol.symbol_binding));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const char* RoMetadataProcess::getSectionIndexStr(nx::elf::SpecialSectionIndex shn_index) const
|
||||
{
|
||||
const char* str;
|
||||
switch (shn_index)
|
||||
{
|
||||
case (nx::elf::SHN_UNDEF):
|
||||
str = "UNDEF";
|
||||
break;
|
||||
case (nx::elf::SHN_LOPROC):
|
||||
str = "LOPROC";
|
||||
break;
|
||||
case (nx::elf::SHN_HIPROC):
|
||||
str = "HIPROC";
|
||||
break;
|
||||
case (nx::elf::SHN_LOOS):
|
||||
str = "LOOS";
|
||||
break;
|
||||
case (nx::elf::SHN_HIOS):
|
||||
str = "HIOS";
|
||||
break;
|
||||
case (nx::elf::SHN_ABS):
|
||||
str = "ABS";
|
||||
break;
|
||||
case (nx::elf::SHN_COMMON):
|
||||
str = "COMMON";
|
||||
break;
|
||||
default:
|
||||
str = "UNKNOWN";
|
||||
break;
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
const char* RoMetadataProcess::getSymbolTypeStr(nx::elf::SymbolType symbol_type) const
|
||||
{
|
||||
const char* str;
|
||||
switch (symbol_type)
|
||||
{
|
||||
case (nx::elf::STT_NOTYPE):
|
||||
str = "NOTYPE";
|
||||
break;
|
||||
case (nx::elf::STT_OBJECT):
|
||||
str = "OBJECT";
|
||||
break;
|
||||
case (nx::elf::STT_FUNC):
|
||||
str = "FUNC";
|
||||
break;
|
||||
case (nx::elf::STT_SECTION):
|
||||
str = "SECTION";
|
||||
break;
|
||||
case (nx::elf::STT_FILE):
|
||||
str = "FILE";
|
||||
break;
|
||||
case (nx::elf::STT_LOOS):
|
||||
str = "LOOS";
|
||||
break;
|
||||
case (nx::elf::STT_HIOS):
|
||||
str = "HIOS";
|
||||
break;
|
||||
case (nx::elf::STT_LOPROC):
|
||||
str = "LOPROC";
|
||||
break;
|
||||
case (nx::elf::STT_HIPROC):
|
||||
str = "HIPROC";
|
||||
break;
|
||||
default:
|
||||
str = "UNKNOWN";
|
||||
break;
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
const char* RoMetadataProcess::getSymbolBindingStr(nx::elf::SymbolBinding symbol_binding) const
|
||||
{
|
||||
const char* str;
|
||||
switch (symbol_binding)
|
||||
{
|
||||
case (nx::elf::STB_LOCAL):
|
||||
str = "LOCAL";
|
||||
break;
|
||||
case (nx::elf::STB_GLOBAL):
|
||||
str = "GLOBAL";
|
||||
break;
|
||||
case (nx::elf::STB_WEAK):
|
||||
str = "WEAK";
|
||||
break;
|
||||
case (nx::elf::STB_LOOS):
|
||||
str = "LOOS";
|
||||
break;
|
||||
case (nx::elf::STB_HIOS):
|
||||
str = "HIOS";
|
||||
break;
|
||||
case (nx::elf::STB_LOPROC):
|
||||
str = "LOPROC";
|
||||
break;
|
||||
case (nx::elf::STB_HIPROC):
|
||||
str = "HIPROC";
|
||||
break;
|
||||
default:
|
||||
str = "UNKNOWN";
|
||||
break;
|
||||
}
|
||||
return str;
|
||||
}
|
62
programs/nstool/source/RoMetadataProcess.h
Normal file
62
programs/nstool/source/RoMetadataProcess.h
Normal file
|
@ -0,0 +1,62 @@
|
|||
#pragma once
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <fnd/types.h>
|
||||
#include <fnd/MemoryBlob.h>
|
||||
|
||||
#include <nx/npdm.h>
|
||||
|
||||
#include "nstool.h"
|
||||
#include "SdkApiString.h"
|
||||
#include "ElfSymbolParser.h"
|
||||
|
||||
class RoMetadataProcess
|
||||
{
|
||||
public:
|
||||
RoMetadataProcess();
|
||||
|
||||
void process();
|
||||
|
||||
void setRoBinary(const fnd::MemoryBlob& bin);
|
||||
void setApiInfo(size_t offset, size_t size);
|
||||
void setDynSym(size_t offset, size_t size);
|
||||
void setDynStr(size_t offset, size_t size);
|
||||
|
||||
void setCliOutputMode(CliOutputMode type);
|
||||
|
||||
void setInstructionType(nx::npdm::InstructionType type);
|
||||
void setListApi(bool listApi);
|
||||
void setListSymbols(bool listSymbols);
|
||||
private:
|
||||
const std::string kModuleName = "RoMetadataProcess";
|
||||
|
||||
CliOutputMode mCliOutputMode;
|
||||
nx::npdm::InstructionType mInstructionType;
|
||||
bool mListApi;
|
||||
bool mListSymbols;
|
||||
|
||||
struct sLayout
|
||||
{
|
||||
sLayout() : offset(0), size(0) {}
|
||||
size_t offset;
|
||||
size_t size;
|
||||
};
|
||||
|
||||
sLayout mApiInfo;
|
||||
sLayout mDynSym;
|
||||
sLayout mDynStr;
|
||||
fnd::MemoryBlob mRoBlob;
|
||||
std::vector<SdkApiString> mSdkVerApiList;
|
||||
std::vector<SdkApiString> mPublicApiList;
|
||||
std::vector<SdkApiString> mDebugApiList;
|
||||
std::vector<SdkApiString> mPrivateApiList;
|
||||
|
||||
ElfSymbolParser mSymbolList;
|
||||
|
||||
void importApiList();
|
||||
void displayRoMetaData();
|
||||
|
||||
const char* getSectionIndexStr(nx::elf::SpecialSectionIndex shn_index) const;
|
||||
const char* getSymbolTypeStr(nx::elf::SymbolType symbol_type) const;
|
||||
const char* getSymbolBindingStr(nx::elf::SymbolBinding symbol_binding) const;
|
||||
};
|
Loading…
Reference in a new issue