Files
idapython-src/idapyswitch.cpp
T
2019-11-14 15:58:44 +01:00

720 lines
20 KiB
C++

/*
* This utility is meant to be used to let IDA switch between
* installed versions of Python3.
*
* See the documentation placed in 'opts.epilog', near the bottom
* of this file.
*/
#ifdef __NT__
# include <windows.h>
#endif
#include <algorithm>
//lint -esym(1788, iinc) is referenced only by its constructor or destructor
#include <pro.h>
#include <fpro.h>
#include <prodir.h>
#include <diskio.hpp>
#include <network.hpp>
#define EXIT_CODE_FORCE_PATH_FAILED 110
#define EXIT_CODE_NO_INSTALLS 120
#define EXIT_CODE_APPLY_FAILED 121
#ifdef __LINUX__
# define EXIT_CODE_SPLIT_DEBUG_EXPAND_DT_NEEDED_ROOM_FAILED 140
#endif
#ifdef __GNUC__ // gcc defines those macros, that are in our way
# undef major
# undef minor
#endif
//-------------------------------------------------------------------------
struct user_args_t
{
qstring force_path;
bool verbose;
bool auto_apply;
bool dry_run;
#ifdef __UNIX__
bool ignore_python_config;
uint32 major_version;
#endif
#ifdef __LINUX__
qstring split_debug_expand_libpython3_dtneeded_room; //lint !e958 padding of 4 bytes needed to align member on a 8 byte boundary
#endif
user_args_t()
: verbose(false),
auto_apply(false),
dry_run(false)
#ifdef __UNIX__
, ignore_python_config(false),
major_version(3)
#endif
{}
};
static user_args_t args;
//-------------------------------------------------------------------------
static int out_ident = 0;
struct out_ident_inc_t
{
out_ident_inc_t() { ++out_ident; }
~out_ident_inc_t() { --out_ident; }
};
//-------------------------------------------------------------------------
AS_PRINTF(1, 0) int vout(const char *format, va_list va)
{
for ( int i = 0; i < out_ident; ++i )
printf(" ");
return vprintf(format, va);
}
//-------------------------------------------------------------------------
AS_PRINTF(1, 2) int out(const char *format, ...)
{
va_list va;
va_start(va, format);
int rc = vout(format, va);
va_end(va);
return rc;
}
//-------------------------------------------------------------------------
AS_PRINTF(1, 2) int out_verb(const char *format, ...)
{
int rc = 0;
if ( args.verbose )
{
out("V: ");
va_list va;
va_start(va, format);
rc = vout(format, va);
va_end(va);
}
return rc;
}
//-------------------------------------------------------------------------
AS_PRINTF(2, 3) void error(int exit_code, const char *format, ...)
{
va_list va;
va_start(va, format);
vout(format, va);
va_end(va);
qexit(exit_code);
}
//-------------------------------------------------------------------------
struct pylib_version_t
{
int major;
int minor;
int revision;
qstring modifiers; //lint !e958 padding of 4 bytes needed to align member on a 8 byte boundary
qstring raw;
pylib_version_t(
int _major=0,
int _minor=0,
int _revision=0,
const char *_modifiers=nullptr,
const char *_raw=nullptr)
: major(_major),
minor(_minor),
revision(_revision),
modifiers(_modifiers),
raw(_raw) {}
bool valid() const { return major > 0; }
const char *str(qstring *out) const
{
out->sprnt("%d.%d.%d%s ('%s')",
major, minor, revision,
modifiers.c_str(), raw.c_str());
return out->c_str();
}
DECLARE_COMPARISONS(pylib_version_t)
{
if ( major != r.major )
return major - r.major;
if ( minor != r.minor )
return minor - r.minor;
if ( revision != r.revision )
return revision - r.revision;
// when it comes to modifiers, we'll consider:
// - that a debug version is lesser than a non-debug one
// - that a version with more modifiers is "greater" than
// one with fewer modifiers.
const int has_debug = qstrstr(modifiers.c_str(), "d") != nullptr;
const int r_has_debug = qstrstr(r.modifiers.c_str(), "d") != nullptr;
if ( has_debug != r_has_debug )
return r_has_debug - has_debug;
return modifiers.length() - r.modifiers.length();
}
};
DECLARE_TYPE_AS_MOVABLE(pylib_version_t);
//-------------------------------------------------------------------------
struct pylib_entry_t
{
pylib_version_t version;
#ifdef __MAC__
pylib_version_t compatibility_version; // only for OSX
#endif
#ifdef __NT__
qstring display_name;
#endif
qstrvec_t paths;
bool preferred;
pylib_entry_t(const pylib_version_t &_version)
: version(_version), preferred(false) {}
const char *str(qstring *out) const
{
qstring pbuf, vbuf;
for ( auto const &p : paths )
{
if ( !pbuf.empty() )
pbuf.append(", ", 2);
pbuf.append(p);
}
out->sprnt("Version: %s; paths: %s", version.str(&vbuf), pbuf.c_str());
if ( preferred )
out->append(" (PREFERRED)");
return out->c_str();
}
bool operator ==(const pylib_entry_t &r) const { return paths == r.paths; }
bool operator !=(const pylib_entry_t &r) const { return !(*this == r); }
};
DECLARE_TYPE_AS_MOVABLE(pylib_entry_t);
typedef qvector<pylib_entry_t> pylib_entry_vec_t;
//-------------------------------------------------------------------------
struct pylib_entries_t
{
pylib_entry_vec_t entries;
pylib_entry_t *get_entry_for_version(const pylib_version_t &version)
{
for ( auto &e : entries )
if ( e.version == version )
return &e;
return nullptr;
}
pylib_entry_t &get_or_create_entry_for_version(const pylib_version_t &version)
{
pylib_entry_t *e = get_entry_for_version(version);
if ( e == nullptr )
{
pylib_entry_t ne(version);
entries.push_back(ne);
e = &entries.back();
}
return *e;
}
};
#ifdef __UNIX__
static void set_preferred_pylib_version(pylib_entries_t *result);
#endif
//-------------------------------------------------------------------------
struct pyver_tool_t
{
static bool reverse_compare_entries(
const pylib_entry_t &e0,
const pylib_entry_t &e1)
{
if ( e0.preferred )
return true;
if ( e1.preferred )
return false;
int rc = e0.version.compare(e1.version);
if ( rc > 0 )
return true;
return false;
}
bool path_to_pylib_entry(
pylib_entry_t *out,
const char *path,
qstring *errbuf) const
{
return do_path_to_pylib_entry(out, path, errbuf);
}
void find_python_libs(
pylib_entries_t *result) const
{
do_find_python_libs(result);
#ifdef __UNIX__
set_preferred_pylib_version(result);
#endif
std::sort(result->entries.begin(), result->entries.end(), reverse_compare_entries);
qstring buf;
if ( args.verbose )
{
out_ident_inc_t iinc;
for ( auto const &e : result->entries )
out_verb("%s\n", e.str(&buf));
}
}
bool apply_version(
const pylib_entry_t &entry,
qstring *errbuf) const
{
return do_apply_version(entry, errbuf);
}
private:
// These three need to be implemented in different
// ways on __NT__, __LINUX__ and __MAC__
// Look on the filesystem (or the registry on Windows)
// for available Python3 installations.
void do_find_python_libs(pylib_entries_t *out) const;
// Given a path to a .so, .dll or .dylib, try and parse the
// Python3 version and produce an entry with it.
bool do_path_to_pylib_entry(pylib_entry_t *entry, const char *path, qstring *errbuf) const;
// Do patch the idapython.[so|dylib] binary (or the
// registry on Windows) so that they refer to the right
// Python3 version.
bool do_apply_version(const pylib_entry_t &entry, qstring *errbuf) const;
};
//-------------------------------------------------------------------------
// Accepts:
// "3.7"
// "3.7.1"
// "3.7m"
// "3.7.1dm"
//lint -esym(528, parse_python_version_str) not referenced
static bool parse_python_version_str(pylib_version_t *out, const char *raw)
{
int major = 0;
int minor = 0;
int revision = 0;
int nchars_read;
const char *p = raw;
if ( qsscanf(raw, "%d.%d%n", &major, &minor, &nchars_read) != 2 )
return false;
p += nchars_read;
if ( p[0] == '.' && qisdigit(p[1]) )
{
if ( qsscanf(p, ".%d%n", &revision, &nchars_read) != 1 )
return false;
p += nchars_read;
}
*out = pylib_version_t(major, minor, revision, p, raw);
return true;
}
#ifdef __UNIX__
//-------------------------------------------------------------------------
static bool extract_version_from_str(
pylib_version_t *out,
const char *p,
const char *stem,
const char *end_delimiter)
{
const size_t stemlen = qstrlen(stem);
if ( !strneq(p, stem, stemlen) )
return false;
p += stemlen;
const char *p2 = qstrstr(p, end_delimiter);
if ( p2 == nullptr )
p2 = tail(p);
size_t nbytes = p2 - p;
char raw[MAXSTR];
if ( nbytes > sizeof(raw) - 1 )
nbytes = sizeof(raw) - 1;
memmove(raw, p, nbytes);
raw[nbytes] = '\0';
parse_python_version_str(out, raw);
return true;
}
//-------------------------------------------------------------------------
static void set_preferred_pylib_version(pylib_entries_t *result)
{
// If python(3)-config exists, use it to determine the preferred version
if ( args.ignore_python_config )
return;
const char *config_util = args.major_version == 3 ? "python3-config" : "python-config";
qstring cmd;
cmd.sprnt("%s --libs", config_util);
qstring error;
qstring verbuf;
FILE *fp = popen(cmd.c_str(), "r");
if ( fp != nullptr )
{
char outbuf[MAXSTR];
/*ssize_t nread =*/ qfread(fp, outbuf, sizeof(outbuf));
int rc = pclose(fp);
if ( rc == 0 )
{
pylib_version_t version;
const char *p = qstrstr(outbuf, "-lpython");
if ( p != nullptr
&& extract_version_from_str(&version, p, "-lpython", " ") )
{
#ifdef __MAC__
// python3-config output on OSX will contain modifiers, i.e. "-lpython3.7m", but when detecting pylibs on OSX
// we extract the python version from the LC_ID_DYLIB load command, which does not contain modifiers.
// it seems safe to ignore the modifiers on OSX, they appear to be only used for compatibility purposes.
version.modifiers.clear();
#endif
out_verb("Preferred version, as reported by \"%s\": %s\n", cmd.c_str(), version.str(&verbuf));
pylib_entry_t *e = result->get_entry_for_version(version);
if ( e != nullptr )
e->preferred = true;
else
error.sprnt("\"%s\" reports preferred "
"version \"%s\", but no corresponding library file "
"was found.\n", cmd.c_str(), version.str(&verbuf));
}
else
{
error.sprnt("Error parsing \"%s\" output", cmd.c_str());
}
}
else
{
error.sprnt("Error calling \"%s\"", cmd.c_str());
}
}
else
{
error.sprnt("\"%s\" is not available", config_util);
}
if ( !error.empty() )
out_verb("%s. Cannot determine preferred version this way.\n",
error.c_str());
}
#endif // __UNIX__
#if defined(__LINUX__) || defined(__MAC__)
# ifdef __LINUX__
# define SOSFX "so"
# else
# define SOSFX "dylib"
# endif
//-------------------------------------------------------------------------
struct file_visitor_t
{
virtual int visit_file(const char *path) = 0;
};
//-------------------------------------------------------------------------
static int visit_files(
file_visitor_t &dv,
const char *dir,
const char *pattern,
int attr = 0)
{
char path[QMAXPATH];
qmakepath(path, sizeof(path), dir, pattern, nullptr);
qffblk64_t fb;
for ( int code = qfindfirst(path, &fb, attr);
code == 0;
code = qfindnext(&fb) )
{
qmakepath(path, sizeof(path), dir, fb.ff_name, nullptr);
int ret = dv.visit_file(path);
if ( ret != 0 )
return ret;
}
return 0;
}
//-------------------------------------------------------------------------
//lint -esym(528, for_all_plugin_files) not referenced
static int for_all_plugin_files(file_visitor_t &dv, qstring *errbuf)
{
char path[QMAXPATH];
char found = 0;
for ( int is_ea64 = 0; is_ea64 < 2; ++is_ea64 )
{
// Only patch for actually available IDAs (e.g. don't fail in std edition)
qmakepath(path, sizeof(path), idadir(""), is_ea64 ? "ida64" : "ida", nullptr);
if ( qfileexist(path) )
{
found++;
qmakepath(path, sizeof(path), idadir(""), "plugins", is_ea64 ? "idapython64." SOSFX : "idapython." SOSFX, nullptr);
int ret = dv.visit_file(path);
if ( ret != 0 )
return ret;
qmakepath(path, sizeof(path), idadir(""), "python", args.major_version == 3 ? "3" : "2", is_ea64 ? "ida_64" : "ida_32", nullptr);
ret = visit_files(dv, path, "_ida_*.so");
if ( ret != 0 )
return ret;
}
}
if ( !found )
{
errbuf->sprnt("Nothing to patch in %s", idadir(""));
return -1;
}
return 0;
}
#endif
#ifdef __LINUX__
# include "idapyswitch_linux.cpp"
#else
# ifdef __NT__
# include "idapyswitch_win.cpp"
# else
# include "idapyswitch_mac.cpp"
# endif
#endif
//-------------------------------------------------------------------------
static void set_verbose(const char *, void *) { args.verbose = true; }
static void set_auto_apply(const char *, void *) { args.auto_apply = true; }
static void set_dry_run(const char *, void *) { args.dry_run = true; }
static void set_force_path(const char *arg, void *) { args.force_path = arg; }
#ifdef __UNIX__
static void set_ignore_python_config(const char *, void *) { args.ignore_python_config = true; }
#endif
#ifdef __LINUX__
static void set_split_debug_expand_libpython3_dtneeded_room(const char *arg, void *)
{
args.split_debug_expand_libpython3_dtneeded_room = arg;
}
#endif
#ifdef __MAC__
static void set_use_python2(const char *, void *) { args.major_version = 2; }
#endif
//-------------------------------------------------------------------------
static const cliopt_t _opts[] =
{
{ 'v', "verbose", "Verbose mode", set_verbose, 0 },
{ 'a', "auto-apply", "Run non-interactively; automatically apply the preferred version (if found)", set_auto_apply, 0 },
{ 'r', "dry-run", "Only report what would happen; don't do it", set_dry_run, 0 },
{ 's', "force-path",
#ifdef __LINUX__
"Have IDAPython use the specified \"/path/to/libpython[...]so\" shared object",
#else
# ifdef __NT__
"Have IDAPython use the specified \"\\path\\to\\python3.dll\" DLL",
# else
"Have IDAPython use the specified \"/path/to/libpython[...]dylib\" dylib",
# endif
#endif
set_force_path,
1
},
#ifdef __UNIX__
{ 'k', "ignore-python-config", "Don't use python-config to find out the preferred version number", set_ignore_python_config, 0 },
#endif
#ifdef __LINUX__
{ 'x', "split-debug-and-expand-libpython3-dtneeded-room", "Expand the DT_NEEDED room to N bytes, using the local `patchelf` (needed at build-time only)", set_split_debug_expand_libpython3_dtneeded_room, 1 },
#endif
#ifdef __MAC__
{ 't', "use-python2", "Search for alternate Python2 installations, and patch the Python2 version of idapython", set_use_python2, 0 },
#endif
};
//-------------------------------------------------------------------------
static const char usage_epilog[] =
"Switch between available installations of Python3\n"
"\n"
"Because Python3 does not systematically install a single,\n"
"always-available \"python3.dll\", \"libpython3.so\" or \"python3.dylib\",\n"
"but rather allows for multiple versions of Python3 to be\n"
"installed in parallel on a given system, many tools\n"
"provide a way to switch between those versions.\n"
"\n"
"IDA is no exception, and this tool is one such Python3 'switcher'.\n"
"\n"
"It can be run in 3 ways:\n"
"\n"
" 1) The default, interactive way\n"
" -------------------------------\n"
" > $ idapyswitch\n"
" will look on the filesystem for available Python3 installations,\n"
" present the user with a list of found versions (sorted according\n"
" to preferability), and let the user pick which one IDA should use.\n"
"\n"
" 2) The 'automatic' way\n"
" ----------------------\n"
" > $ idapyswitch --auto\n"
" will look on the filesystem for available Python3 installations,\n"
" and automatically pick the one it deemed the most preferable.\n"
"\n"
" 3) The 'manual' way\n"
" -------------------\n"
#ifdef __NT__
" > $ idapyswitch --force-path C:\\Python37\\python3.dll\n"
#else
# ifdef __LINUX__
" > $ idapyswitch --force-path /path/to/libpython3.7dm.so.1.2\n"
# else
" > $ idapyswitch --force-path /path/to/Python.framework/Versions/3.7/Python\n"
# endif
#endif
" will pick the path that the user provided.\n"
"\n"
"Once a version is picked, this tool will do the following:\n"
"\n"
#ifdef __NT__
" * place the path to the directory containing python3.dll\n"
" into the registry. IDA will pick it up at launch-time,\n"
" and add it to the list of paths that have to be looked\n"
" into by the DLL loader.\n"
#else
# ifdef __LINUX__
" * patch 'idapython.so' and 'idapython64.so' so that they\n"
" have a DT_NEEDED corresponding to the DT_SONAME of the\n"
" library that was selected.\n"
# else
" * patch 'idapython.dylib' and 'idapython64.dylib' so that\n"
" they refer to the right Python3 dylib.\n"
# endif
#endif
;
//-------------------------------------------------------------------------
int main(int argc, const char **argv)
{
cliopts_t opts(out);
opts.epilog = usage_epilog;
opts.add(_opts, qnumber(_opts));
opts.apply(argc, argv);
qstring errbuf;
#ifdef __LINUX__
if ( !args.split_debug_expand_libpython3_dtneeded_room.empty() )
{
if ( !split_debug_expand_libpython3_dtneeded_room(
args.split_debug_expand_libpython3_dtneeded_room.c_str(),
&errbuf) )
{
error(EXIT_CODE_SPLIT_DEBUG_EXPAND_DT_NEEDED_ROOM_FAILED,
"Cannot split debug/expand DT_NEEDED room: %s\n",
errbuf.c_str());
}
return 0;
}
#endif
pyver_tool_t tool;
if ( !args.force_path.empty() )
{
const char *path = args.force_path.c_str();
pylib_version_t dummy_version;
pylib_entry_t entry(dummy_version);
if ( !tool.path_to_pylib_entry(&entry, path, &errbuf) )
{
error(EXIT_CODE_FORCE_PATH_FAILED,
"Cannot determine python library version for \"%s\": %s\n",
path, errbuf.c_str());
}
if ( !tool.apply_version(entry, &errbuf) )
{
qstring buf;
error(EXIT_CODE_FORCE_PATH_FAILED,
"Applying \"%s\" (extracted from path \"%s\") failed: %s\n",
entry.str(&buf), path, errbuf.c_str());
}
}
else
{
pylib_entries_t entries;
tool.find_python_libs(&entries);
const size_t nentries = entries.entries.size();
if ( nentries > 0 )
{
qstring buf;
const pylib_entry_t *preferred = nullptr;
if ( args.auto_apply )
{
preferred = &entries.entries[0];
}
else
{
out("The following Python installations were found:\n");
for ( size_t i = 0; i < nentries; ++i )
{
out_ident_inc_t iinc;
const pylib_entry_t &e = entries.entries[i];
out("#%" FMT_Z ": %s (%s)\n",
i,
e.version.str(&buf),
!e.paths.empty() ? e.paths[0].c_str() : "<unavailable path>");
}
size_t picked = size_t(-1);
while ( picked >= nentries )
{
out("Please pick a number between 0 and %" FMT_Z " (default: 0)\n", nentries-1);
char numbuf[MAXSTR];
qfgets(numbuf, sizeof(numbuf), stdin);
qstring qnumbuf(numbuf);
qnumbuf.rtrim('\n');
if ( qnumbuf.empty() )
picked = 0;
else
picked = qatoll(qnumbuf.c_str());
}
preferred = &entries.entries[picked];
}
out("Applying version %s\n", preferred->version.str(&buf));
if ( !tool.apply_version(*preferred, &errbuf) )
{
error(EXIT_CODE_APPLY_FAILED,
"Apply failed: %s\n",
errbuf.c_str());
}
}
else
{
error(EXIT_CODE_NO_INSTALLS,
"No Python installations were found\n");
}
}
return 0;
}