rizin/librz/core/project_migrate.c
Quentin Minster 700c7d898b
Serialize seek history (#3838)
* Save/load seek history ##projects

* Add unit test for seek history serialization

* Bump project version to 15 for seek history serialization
2023-09-10 21:30:30 +08:00

640 lines
21 KiB
C

// SPDX-FileCopyrightText: 2021 Florian Märkl <info@florianmaerkl.de>
// SPDX-License-Identifier: LGPL-3.0-only
#include "sdb.h"
#include <rz_project.h>
#include <rz_util/rz_pj.h>
/**
* \file
* About Project Migrations:
* The project format is versioned, which means that a project saved with a certain version
* will always follow the exact same format.
* The current version is defined as `RZ_PROJECT_VERSION` and is completely independent of
* Rizin release versions. Do not confuse them!
* When any change in Rizin happens that changes the project format, this version must be raised
* and a migration must be implemented, which converts a given project from the previous version
* to the current and will be executed when loading an old project with new Rizin.
* These migrations always bring a project from version x to version x+1 and they may be executed
* sequentially if multiple versions are skipped. For example loading a project with version 1 in
* Rizin with version 3 will execute (1->2), then (2->3) and finally load the project just like
* any regular project of version 3.
*
* After introducing format changes in Rizin, do the following:
* * Raise RZ_PROJECT_VERSION by exactly 1.
* * Implement a function like `bool rz_project_migrate_migrate_v1_v2(RzProject *prj, RzSerializeResultInfo *res)`
* which edits prj in-place and converts it from the previous to the current version.
* * Append this function to the `migrations` array below.
* * Implement tests in `test/unit/test_project_migrate.c` that cover all changes (see the documentation there).
*/
// --
// Migration 1 -> 2
//
// Changes from 788fdb3d8ef98f50d61cdee72e0b57c74c814022:
// Information which addresses are marked as noreturn was previosly saved in "/core/analysis/types" as records like:
// addr.1337.noreturn=true
// Now it is available in "/core/analysis/noreturn" while actual type-related info stays in "/core/analysis/types".
typedef struct {
RzList /*<char *>*/ *moved_keys; ///< deferred for deletion from the old sdb
Sdb *noreturn_db;
} V1V2TypesCtx;
bool v1_v2_types_foreach_cb(void *user, const char *k, const char *v) {
if (!rz_str_startswith(k, "addr.") || !rz_str_endswith(k, ".noreturn")) {
return true;
}
V1V2TypesCtx *ctx = user;
sdb_set(ctx->noreturn_db, k, v, 0);
rz_list_push(ctx->moved_keys, strdup(k));
return true;
}
RZ_API bool rz_project_migrate_v1_v2(RzProject *prj, RzSerializeResultInfo *res) {
Sdb *core_db;
RZ_SERIALIZE_SUB(prj, core_db, res, "core", return false;);
Sdb *analysis_db;
RZ_SERIALIZE_SUB(core_db, analysis_db, res, "analysis", return false;);
Sdb *types_db;
RZ_SERIALIZE_SUB(analysis_db, types_db, res, "types", return false;);
V1V2TypesCtx ctx = {
.moved_keys = rz_list_newf(free),
.noreturn_db = sdb_ns(analysis_db, "noreturn", true)
};
if (!ctx.moved_keys || !ctx.noreturn_db) {
return false;
}
sdb_foreach(types_db, v1_v2_types_foreach_cb, &ctx);
RzListIter *it;
char *s;
rz_list_foreach (ctx.moved_keys, it, s) {
sdb_unset(types_db, s, 0);
}
rz_list_free(ctx.moved_keys);
return true;
}
// --
// Migration 2 -> 3
//
// Changes from 788fdb3d8ef98f50d61cdee72e0b57c74c814022:
// Types database "analysis/types" was converted into two
// separate SDBs - types and callables
// Thus all "func.*" keys from "/core/analysis/types" should be moved to
// "/core/analysis/callables"
// Type links information is also separated now from
// "/core/analysis/types" to "/core/analysis/typelinks"
typedef struct {
RzList /*<char *>*/ *moved_keys; ///< deferred for deletion from the old sdb
Sdb *callables_db;
Sdb *typelinks_db;
} V2V3TypesCtx;
bool v2_v3_types_foreach_cb(void *user, const char *k, const char *v) {
V2V3TypesCtx *ctx = user;
if (rz_str_startswith(k, "func.") || !strcmp(v, "func")) {
sdb_set(ctx->callables_db, k, v, 0);
rz_list_push(ctx->moved_keys, strdup(k));
} else if (rz_str_startswith(k, "link.")) {
// Old addresses were stored as hexadecimal numbers without `0x` part
// New addresses have them
char *tl_key = rz_str_newf("0x%s", k + strlen("link."));
sdb_set(ctx->typelinks_db, tl_key, v, 0);
free(tl_key);
rz_list_push(ctx->moved_keys, strdup(k));
}
return true;
}
RZ_API bool rz_project_migrate_v2_v3(RzProject *prj, RzSerializeResultInfo *res) {
Sdb *core_db;
RZ_SERIALIZE_SUB(prj, core_db, res, "core", return false;);
Sdb *analysis_db;
RZ_SERIALIZE_SUB(core_db, analysis_db, res, "analysis", return false;);
Sdb *types_db;
RZ_SERIALIZE_SUB(analysis_db, types_db, res, "types", return false;);
V2V3TypesCtx ctx = {
.moved_keys = rz_list_newf(free),
.callables_db = sdb_ns(analysis_db, "callables", true),
.typelinks_db = sdb_ns(analysis_db, "typelinks", true)
};
if (!ctx.moved_keys || !ctx.callables_db || !ctx.typelinks_db) {
return false;
}
sdb_foreach(types_db, v2_v3_types_foreach_cb, &ctx);
RzListIter *it;
char *s;
rz_list_foreach (ctx.moved_keys, it, s) {
sdb_unset(types_db, s, 0);
}
rz_list_free(ctx.moved_keys);
return true;
}
// --
// Migration 3 -> 4
//
// Changes from 8d54707ccf8b3492b999dfa42057da0847acb952:
// Added new global variables in "/core/analysis/vars"
RZ_API bool rz_project_migrate_v3_v4(RzProject *prj, RzSerializeResultInfo *res) {
Sdb *core_db;
RZ_SERIALIZE_SUB(prj, core_db, res, "core", return false;);
Sdb *analysis_db;
RZ_SERIALIZE_SUB(core_db, analysis_db, res, "analysis", return false;);
sdb_ns(analysis_db, "vars", true);
// Typelinks currently still exist. When they will be removed, the following
// code can be enabled and moved to the respective migration.
#if 0
V3V4TypesCtx ctx = {
.moved_keys = rz_list_newf(free),
.global_vars_db = sdb_ns(analysis_db, "vars", true)
};
if (!ctx.moved_keys || !ctx.global_vars_db) {
return false;
}
Sdb *typelinks_db = sdb_ns(analysis_db, "typelinks", true);
sdb_foreach(typelinks_db, v3_v4_types_foreach_cb, &ctx);
RzListIter *it;
char *s;
rz_list_foreach (ctx.moved_keys, it, s) {
sdb_unset(typelinks_db, s, 0);
}
rz_list_free(ctx.moved_keys);
#endif
return true;
}
// --
// Migration 4 -> 5
//
// Changes from a523314fa7c8fde0c2c0d116e82aa77f62991d37
// Added new type called `unknown_t` in "/core/analysis/types"
// It is used for all unknown types during the project loading and saving
RZ_API bool rz_project_migrate_v4_v5(RzProject *prj, RzSerializeResultInfo *res) {
Sdb *core_db;
RZ_SERIALIZE_SUB(prj, core_db, res, "core", return false;);
Sdb *analysis_db;
RZ_SERIALIZE_SUB(core_db, analysis_db, res, "analysis", return false;);
Sdb *config_db;
RZ_SERIALIZE_SUB(core_db, config_db, res, "config", return false;);
Sdb *types_db;
RZ_SERIALIZE_SUB(analysis_db, types_db, res, "types", return false;);
// Common keys:
// unknown_t=type
// type.unknown_t.typeclass=Integral
sdb_set(types_db, "unknown_t", "type", 0);
sdb_set(types_db, "type.unknown_t.typeclass", "Integral", 0);
// Now we read the bits value from "asm.bits=XX" in "/core/config"
int bits = sdb_num_get(config_db, "asm.bits", 0);
switch (bits) {
case 16:
// type.unknown_t=w
// type.unknown_t.size=16
sdb_set(types_db, "type.unknown_t", "w", 0);
sdb_set(types_db, "type.unknown_t.size", "16", 0);
break;
case 64:
// type.unknown_t=q
// type.unknown_t.size=64
sdb_set(types_db, "type.unknown_t", "q", 0);
sdb_set(types_db, "type.unknown_t.size", "64", 0);
break;
case 32:
default:
// type.unknown_t=d
// type.unknown_t.size=32
sdb_set(types_db, "type.unknown_t", "d", 0);
sdb_set(types_db, "type.unknown_t.size", "32", 0);
break;
}
return true;
}
// --
// Migration 5 -> 6
//
// Changes from 2c48a91d1332daede8d0640ce407c3abcf0abfb4
// Added serialization functionality for debug (only for breakpoints as of now)
// Used to save and load current RzDebug instance (only breakpoints) using serialization
// New namespaces: /core/debug, /core/debug/breakpoints
RZ_API bool rz_project_migrate_v5_v6(RzProject *prj, RzSerializeResultInfo *res) {
Sdb *core_db;
RZ_SERIALIZE_SUB(prj, core_db, res, "core", return false;);
Sdb *debug_db = sdb_ns(core_db, "debug", true);
sdb_ns(debug_db, "breakpoints", true);
return true;
}
// --
// Migration 6 -> 7
//
// Changes from 96a85e573e766e3870a01482f36c92df403cc4cd
// Removed esil pin feature. Namespace deleted: /core/analysis/pins
RZ_API bool rz_project_migrate_v6_v7(RzProject *prj, RzSerializeResultInfo *res) {
Sdb *core_db;
RZ_SERIALIZE_SUB(prj, core_db, res, "core", return false;);
Sdb *analysis_db;
RZ_SERIALIZE_SUB(core_db, analysis_db, res, "analysis", return false;);
sdb_ns_unset(analysis_db, "pins", NULL);
return true;
}
// --
// Migration 7 -> 8
//
// Changes from ea02b0d25f48bb17bc6578259485549cf5c74a20
// Removed zignature feature. Namespace deleted: /core/analysis/zigns
// Also removed configs zign.* options.
RZ_API bool rz_project_migrate_v7_v8(RzProject *prj, RzSerializeResultInfo *res) {
Sdb *core_db;
RZ_SERIALIZE_SUB(prj, core_db, res, "core", return false;);
Sdb *analysis_db;
RZ_SERIALIZE_SUB(core_db, analysis_db, res, "analysis", return false;);
sdb_ns_unset(analysis_db, "zigns", NULL);
Sdb *config_db;
RZ_SERIALIZE_SUB(core_db, config_db, res, "config", return false;);
sdb_unset(config_db, "zign.autoload", 0);
sdb_unset(config_db, "zign.diff.bthresh", 0);
sdb_unset(config_db, "zign.diff.gthresh", 0);
sdb_unset(config_db, "zign.match.bytes", 0);
sdb_unset(config_db, "zign.match.graph", 0);
sdb_unset(config_db, "zign.match.hash", 0);
sdb_unset(config_db, "zign.match.offset", 0);
sdb_unset(config_db, "zign.match.refs", 0);
sdb_unset(config_db, "zign.match.types", 0);
sdb_unset(config_db, "zign.maxsz", 0);
sdb_unset(config_db, "zign.mincc", 0);
sdb_unset(config_db, "zign.minsz", 0);
sdb_unset(config_db, "zign.prefix", 0);
sdb_unset(config_db, "zign.threshold", 0);
return true;
}
// --
// Migration 8 -> 9
//
// Changes from fbad0b4859802a62dcc96002c2710e696809a0c3
// Removed fingerprint from the serialized RzAnalysisFunction & RzAnalysisBlock
RZ_API bool rz_project_migrate_v8_v9(RzProject *prj, RzSerializeResultInfo *res) {
// there is nothing to be done since the deserializer will ignore the original serialized data
return true;
}
// --
// Migration 9 -> 10
//
// Changes from 13cd3942d12b61911e27ab82baef045adf57d77c
// Removed stackptr and parent_stackptr from the serialized RzAnalysisBlock
// Added sp_entry and sp_delta to serialized RzAnalysisBlock
RZ_API bool rz_project_migrate_v9_v10(RzProject *prj, RzSerializeResultInfo *res) {
// There is nothing to be done since the deserializer will ignore the original serialized data
// and missing sp_entry and sp_delta are valid for unknown values.
// The previous stackptr/parent_stackptr are too nonsensical to be converted to sp_entry/sp_delta
// unfortunately.
return true;
}
// --
// Migration 10 -> 11
//
// Changes from d9950f74792c1dfb565ac491cc7ef706b80e6044
// - Removed analysis.vars.stackname config var
// - In RzAnalysisVar JSON, "kind", "arg" and "delta" are removed. Instead, there is either a "stack"
// or a "reg" key, but never both.
// - {name:<str>, type:<str>, kind:"s|b|r", arg?:<bool>, delta?:<st64>, reg?:<str>, cmt?:<str>,...
// + {name:<str>, type:<str>, stack?:<st64>, reg?:<str>, cmt?:<str>,...
// - In RzAnalysisVar.accs JSON, "sp" value is now actually signed in json instead of being casted
// to and from its unsigned representation (pj_kn before, pj_kN now). The loader should be able
// to handle both just fine, but we still convert it so we do not have to make this assumption.
//
typedef struct {
Sdb *db_new;
RzSerializeResultInfo *res;
} V10V11FunctionsCtx;
bool v10_v11_migrate_variable(const RzJson *var, st64 maxstack, PJ *pj, RzSerializeResultInfo *res) {
if (var->type != RZ_JSON_OBJECT) {
goto invalid;
}
// read necessary info
const RzJson *kind = rz_json_get(var, "kind");
if (!kind || kind->type != RZ_JSON_STRING) {
goto invalid;
}
bool is_reg;
st64 stack_addr;
if (!strcmp(kind->str_value, "r")) {
is_reg = true;
// reg vars did not change, they just don't have the "delta" anymore because it was redundant.
} else if (!strcmp(kind->str_value, "b") || !strcmp(kind->str_value, "s")) {
is_reg = false;
const RzJson *delta = rz_json_get(var, "delta");
if (delta && delta->type != RZ_JSON_INTEGER) {
goto invalid;
}
// Despite variables being represented as sp/bp+offset, the delta value
// already matches our notion of stack addresses.
stack_addr = delta ? delta->num.s_value : 0;
} else {
goto invalid;
}
// write new json
pj_o(pj);
for (const RzJson *var_member = var->children.first; var_member; var_member = var_member->next) {
if (!strcmp(var_member->key, "arg") || !strcmp(var_member->key, "delta") || !strcmp(var_member->key, "kind")) {
// removed keys
continue;
}
// The accesses' "sp" key will be converted to sgned in the call below
// as a somewhat unexpected side effect. But this is exactly what we want.
rz_json_to_pj(var_member, pj, true);
}
if (!is_reg) {
pj_kN(pj, "stack", stack_addr);
}
pj_end(pj);
return true;
invalid:
RZ_SERIALIZE_ERR(res, "invalid json contents for variable");
return false;
}
bool v10_v11_functions_foreach_cb(void *user, const char *k, const char *v) {
V10V11FunctionsCtx *ctx = user;
char *json_str = strdup(v);
RzJson *j = rz_json_parse(json_str);
bool ret = false;
if (!j || j->type != RZ_JSON_OBJECT) {
RZ_SERIALIZE_ERR(ctx->res, "invalid json in function key %s", k);
goto end;
}
const RzJson *tmp = rz_json_get(j, "maxstack"); // maxstack is mandatory in v10
if (!tmp || tmp->type != RZ_JSON_INTEGER) {
RZ_SERIALIZE_ERR(ctx->res, "missing or invalid maxstack in function key %s", k);
goto end;
}
st64 maxstack = tmp->num.s_value;
PJ *pj = pj_new();
if (!pj) {
goto end;
}
pj_o(pj);
for (RzJson *func_member = j->children.first; func_member; func_member = func_member->next) {
if (!strcmp(func_member->key, "vars")) {
if (func_member->type != RZ_JSON_ARRAY) {
RZ_SERIALIZE_ERR(ctx->res, "invalid json contents for function -> vars");
pj_free(pj);
goto end;
}
pj_ka(pj, "vars");
for (RzJson *var = func_member->children.first; var; var = var->next) {
v10_v11_migrate_variable(var, maxstack, pj, ctx->res);
}
pj_end(pj);
continue;
}
rz_json_to_pj(func_member, pj, true);
}
pj_end(pj);
char *res = pj_drain(pj);
if (!res) {
goto end;
}
sdb_set_owned(ctx->db_new, k, res, 0);
ret = true;
end:
rz_json_free(j);
free(json_str);
return ret;
}
RZ_API bool rz_project_migrate_v10_v11(RzProject *prj, RzSerializeResultInfo *res) {
Sdb *core_db;
RZ_SERIALIZE_SUB(prj, core_db, res, "core", return false;);
Sdb *config_db;
RZ_SERIALIZE_SUB(core_db, config_db, res, "config", return false;);
sdb_unset(config_db, "analysis.vars.stackname", 0);
Sdb *analysis_db;
RZ_SERIALIZE_SUB(core_db, analysis_db, res, "analysis", return false;);
Sdb *functions_db_old;
RZ_SERIALIZE_SUB(analysis_db, functions_db_old, res, "functions", return false;);
functions_db_old->refs++;
sdb_ns_unset(analysis_db, "functions", NULL);
V10V11FunctionsCtx ctx = {
.db_new = sdb_ns(analysis_db, "functions", true),
.res = res
};
bool ret = sdb_foreach(functions_db_old, v10_v11_functions_foreach_cb, &ctx);
sdb_free(functions_db_old);
return ret;
}
// --
// Migration 11 -> 12
//
// Changes from 59f32b6db89c09c16fadbda6a098e326b73e03d8
// - Rename config var `asm.dwarf` to `asm.debuginfo`
// - Rename config var `asm.dwarf.abspath` to `asm.debuginfo.abspath`
// - Rename config var `asm.dwarf.file` to `asm.debuginfo.file`
// - Rename config var `asm.dwarf.lines` to `asm.debuginfo.lines`
//
static inline bool sdb_rename(Sdb *db, const char *old_key, const char *new_key) {
char *val = sdb_get(db, old_key, 0);
if (!val) {
return false;
}
sdb_unset(db, old_key, 0);
sdb_set_owned(db, new_key, val, 0);
return true;
}
RZ_API bool rz_project_migrate_v11_v12(RzProject *prj, RzSerializeResultInfo *res) {
Sdb *core_db;
RZ_SERIALIZE_SUB(prj, core_db, res, "core", return false;);
Sdb *config_db;
RZ_SERIALIZE_SUB(core_db, config_db, res, "config", return false;);
sdb_rename(config_db, "asm.dwarf.abspath", "asm.debuginfo.abspath");
sdb_rename(config_db, "asm.dwarf.file", "asm.debuginfo.file");
sdb_rename(config_db, "asm.dwarf.lines", "asm.debuginfo.lines");
sdb_rename(config_db, "asm.dwarf", "asm.debuginfo");
return true;
}
// --
// Migration 12 -> 13
//
// Changes from 366dfcfbf0ac2eb4c09c49b0a9e43117864750b7:
// Removed typelinks from "/core/analysis/typelinks"
// and converted into global variables in "/core/analysis/vars"
typedef struct {
RzList /*<char *>*/ *moved_keys; ///< deferred for deletion from the old sdb
Sdb *global_vars_db;
} V12V13TypesCtx;
bool v12_v13_types_foreach_cb(void *user, const char *k, const char *v) {
V12V13TypesCtx *ctx = user;
if (rz_str_startswith(k, "0x")) {
char name[32];
PJ *j = pj_new();
pj_o(j);
pj_ks(j, "name", rz_strf(name, "gvar_%s", k));
pj_ks(j, "type", v);
pj_ks(j, "addr", k);
// We don't have constraints for typelink here.
pj_end(j);
sdb_set(ctx->global_vars_db, k, pj_string(j), 0);
pj_free(j);
rz_list_push(ctx->moved_keys, strdup(k));
}
return true;
}
RZ_API bool rz_project_migrate_v12_v13(RzProject *prj, RzSerializeResultInfo *res) {
Sdb *core_db;
RZ_SERIALIZE_SUB(prj, core_db, res, "core", return false;);
Sdb *analysis_db;
RZ_SERIALIZE_SUB(core_db, analysis_db, res, "analysis", return false;);
sdb_ns(analysis_db, "vars", true);
V12V13TypesCtx ctx = {
.moved_keys = rz_list_newf(free),
.global_vars_db = sdb_ns(analysis_db, "vars", true)
};
if (!ctx.moved_keys || !ctx.global_vars_db) {
return false;
}
Sdb *typelinks_db = sdb_ns(analysis_db, "typelinks", true);
sdb_foreach(typelinks_db, v12_v13_types_foreach_cb, &ctx);
RzListIter *it;
char *s;
rz_list_foreach (ctx.moved_keys, it, s) {
sdb_unset(typelinks_db, s, 0);
}
rz_list_free(ctx.moved_keys);
return true;
}
// --
// Migration 13 -> 14
//
// Changes from 8e29b959b86a35bbbfed599989f077dba6e0ebd5:
// Removed {stack,reg} from "/core/analysis/functions/vars"
// and converted into storage object { ..., storage: { type: ... } }
bool v13_v14_foreach_cb(void *user, const char *k, const char *v) {
static const char *types[] = { "stack", "reg" };
Sdb *fn_db = user;
if (rz_str_startswith(k, "0x")) {
RzJson *fn_j = rz_json_parse((char *)v);
assert(fn_j->type == RZ_JSON_OBJECT);
PJ *j = pj_new();
pj_o(j);
for (RzJson *body = fn_j->children.first; body; body = body->next) {
bool filtered = false;
for (int i = 0; i < RZ_ARRAY_SIZE(types); ++i) {
const char *type = types[i];
if (rz_str_cmp(body->key, type, -1) != 0) {
continue;
}
filtered = true;
pj_ko(j, "storage");
pj_ks(j, "type", types[i]);
switch (body->type) {
case RZ_JSON_INTEGER:
pj_kn(j, types[i], body->num.s_value);
break;
case RZ_JSON_STRING:
pj_ks(j, types[i], body->str_value);
break;
default: rz_warn_if_reached();
}
pj_end(j);
}
if (!filtered) {
rz_json_to_pj(body, j, true);
}
}
pj_end(j);
sdb_set(fn_db, k, pj_string(j), 0);
pj_free(j);
rz_json_free(fn_j);
}
return true;
}
RZ_API bool rz_project_migrate_v13_v14(RzProject *prj, RzSerializeResultInfo *res) {
Sdb *core_db;
RZ_SERIALIZE_SUB(prj, core_db, res, "core", return false;);
Sdb *analysis_db;
RZ_SERIALIZE_SUB(core_db, analysis_db, res, "analysis", return false;);
Sdb *fn_db = sdb_ns(analysis_db, "functions", true);
sdb_foreach(fn_db, v13_v14_foreach_cb, fn_db);
return true;
}
// --
// Migration 14 -> 15
//
// Changes from <commit hash not yet known>:
// Added serialization functionality for seek history
// New namespace: /core/seek
RZ_API bool rz_project_migrate_v14_v15(RzProject *prj, RzSerializeResultInfo *res) {
Sdb *core_db;
RZ_SERIALIZE_SUB(prj, core_db, res, "core", return false;);
sdb_ns(core_db, "seek", true);
return true;
}
static bool (*const migrations[])(RzProject *prj, RzSerializeResultInfo *res) = {
rz_project_migrate_v1_v2,
rz_project_migrate_v2_v3,
rz_project_migrate_v3_v4,
rz_project_migrate_v4_v5,
rz_project_migrate_v5_v6,
rz_project_migrate_v6_v7,
rz_project_migrate_v7_v8,
rz_project_migrate_v8_v9,
rz_project_migrate_v9_v10,
rz_project_migrate_v10_v11,
rz_project_migrate_v11_v12,
rz_project_migrate_v12_v13,
rz_project_migrate_v13_v14,
rz_project_migrate_v14_v15,
};
/// Migrate the given project to the current version in-place
RZ_API bool rz_project_migrate(RzProject *prj, unsigned long version, RzSerializeResultInfo *res) {
RZ_STATIC_ASSERT(RZ_ARRAY_SIZE(migrations) + 1 == RZ_PROJECT_VERSION);
while (version < RZ_PROJECT_VERSION) {
bool succ = migrations[version - 1](prj, res);
if (!succ) {
rz_list_push(res, rz_str_newf("project migration from version %lu to %lu failed.", version, version + 1));
return false;
}
rz_list_push(res, rz_str_newf("project migrated from version %lu to %lu.", version, version + 1));
version++;
}
return true;
}