frida-fusion 0.1.17__tar.gz → 0.1.20__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of frida-fusion might be problematic. Click here for more details.

Files changed (47) hide show
  1. {frida_fusion-0.1.17 → frida_fusion-0.1.20}/PKG-INFO +2 -1
  2. {frida_fusion-0.1.17 → frida_fusion-0.1.20}/README.md +1 -0
  3. {frida_fusion-0.1.17 → frida_fusion-0.1.20}/frida_fusion/__meta__.py +2 -2
  4. {frida_fusion-0.1.17 → frida_fusion-0.1.20}/frida_fusion/config.py +16 -2
  5. {frida_fusion-0.1.17 → frida_fusion-0.1.20}/frida_fusion/fusion.py +45 -29
  6. {frida_fusion-0.1.17 → frida_fusion-0.1.20}/frida_fusion/libs/database.py +45 -21
  7. {frida_fusion-0.1.17 → frida_fusion-0.1.20}/frida_fusion/libs/helpers.js +49 -0
  8. {frida_fusion-0.1.17 → frida_fusion-0.1.20}/frida_fusion/libs/logger.py +40 -0
  9. {frida_fusion-0.1.17 → frida_fusion-0.1.20}/frida_fusion/module.py +73 -5
  10. {frida_fusion-0.1.17 → frida_fusion-0.1.20}/frida_fusion/modules/crypto/crypto.py +1 -1
  11. frida_fusion-0.1.20/frida_fusion/modules/hermes_injector/hermes_hook.js +36 -0
  12. frida_fusion-0.1.20/frida_fusion/modules/hermes_injector/hermes_injector.js +191 -0
  13. frida_fusion-0.1.20/frida_fusion/modules/hermes_injector/hermes_injector.py +89 -0
  14. frida_fusion-0.1.20/frida_fusion/modules/okhttp_logging/okhttp-logging.py +134 -0
  15. {frida_fusion-0.1.17 → frida_fusion-0.1.20}/frida_fusion/modules/reflection/reflection-stalker.py +1 -1
  16. {frida_fusion-0.1.17 → frida_fusion-0.1.20}/frida_fusion/modules/shared_preferences/shared_preferences.py +24 -29
  17. frida_fusion-0.1.20/frida_fusion/modules/tls_unpinning/__init__.py +0 -0
  18. {frida_fusion-0.1.17 → frida_fusion-0.1.20}/frida_fusion.egg-info/PKG-INFO +2 -1
  19. {frida_fusion-0.1.17 → frida_fusion-0.1.20}/frida_fusion.egg-info/SOURCES.txt +6 -2
  20. frida_fusion-0.1.17/frida_fusion/modules/okhttp-logging/okhttp-logging.py +0 -80
  21. {frida_fusion-0.1.17 → frida_fusion-0.1.20}/LICENSE +0 -0
  22. {frida_fusion-0.1.17 → frida_fusion-0.1.20}/frida_fusion/__init__.py +0 -0
  23. {frida_fusion-0.1.17 → frida_fusion-0.1.20}/frida_fusion/__main__.py +0 -0
  24. {frida_fusion-0.1.17 → frida_fusion-0.1.20}/frida_fusion/args.py +0 -0
  25. {frida_fusion-0.1.17 → frida_fusion-0.1.20}/frida_fusion/exceptions.py +0 -0
  26. {frida_fusion-0.1.17 → frida_fusion-0.1.20}/frida_fusion/libs/__init__.py +0 -0
  27. {frida_fusion-0.1.17 → frida_fusion-0.1.20}/frida_fusion/libs/color.py +0 -0
  28. {frida_fusion-0.1.17 → frida_fusion-0.1.20}/frida_fusion/libs/scriptlocation.py +0 -0
  29. {frida_fusion-0.1.17 → frida_fusion-0.1.20}/frida_fusion/modules/__init__.py +0 -0
  30. {frida_fusion-0.1.17 → frida_fusion-0.1.20}/frida_fusion/modules/android_setings/__init__.py +0 -0
  31. {frida_fusion-0.1.17 → frida_fusion-0.1.20}/frida_fusion/modules/android_setings/settings.js +0 -0
  32. {frida_fusion-0.1.17 → frida_fusion-0.1.20}/frida_fusion/modules/android_setings/settings.py +0 -0
  33. {frida_fusion-0.1.17 → frida_fusion-0.1.20}/frida_fusion/modules/crypto/__init__.py +0 -0
  34. {frida_fusion-0.1.17 → frida_fusion-0.1.20}/frida_fusion/modules/crypto/crypto.js +0 -0
  35. {frida_fusion-0.1.17/frida_fusion/modules/shared_preferences → frida_fusion-0.1.20/frida_fusion/modules/hermes_injector}/__init__.py +0 -0
  36. {frida_fusion-0.1.17/frida_fusion/modules/okhttp-logging → frida_fusion-0.1.20/frida_fusion/modules/okhttp_logging}/okhttp-logging.js +0 -0
  37. {frida_fusion-0.1.17 → frida_fusion-0.1.20}/frida_fusion/modules/reflection/reflection-stalker.js +0 -0
  38. {frida_fusion-0.1.17/frida_fusion/modules/tls_unpinning → frida_fusion-0.1.20/frida_fusion/modules/shared_preferences}/__init__.py +0 -0
  39. {frida_fusion-0.1.17 → frida_fusion-0.1.20}/frida_fusion/modules/shared_preferences/shared_preferences.js +0 -0
  40. {frida_fusion-0.1.17 → frida_fusion-0.1.20}/frida_fusion/modules/tls_unpinning/frida_multiple_unpinning.py +0 -0
  41. {frida_fusion-0.1.17 → frida_fusion-0.1.20}/frida_fusion.egg-info/dependency_links.txt +0 -0
  42. {frida_fusion-0.1.17 → frida_fusion-0.1.20}/frida_fusion.egg-info/entry_points.txt +0 -0
  43. {frida_fusion-0.1.17 → frida_fusion-0.1.20}/frida_fusion.egg-info/requires.txt +0 -0
  44. {frida_fusion-0.1.17 → frida_fusion-0.1.20}/frida_fusion.egg-info/top_level.txt +0 -0
  45. {frida_fusion-0.1.17 → frida_fusion-0.1.20}/pyproject.toml +0 -0
  46. {frida_fusion-0.1.17 → frida_fusion-0.1.20}/setup.cfg +0 -0
  47. {frida_fusion-0.1.17 → frida_fusion-0.1.20}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: frida-fusion
3
- Version: 0.1.17
3
+ Version: 0.1.20
4
4
  Summary: Hook your mobile tests with Frida
5
5
  Author-email: "Helvio Junior (M4v3r1ck)" <helvio_junior@hotmail.com>
6
6
  Maintainer-email: "Helvio Junior (M4v3r1ck)" <helvio_junior@hotmail.com>
@@ -34,6 +34,7 @@ Requires-Dist: frida-tools>=10.8.0
34
34
  Dynamic: license-file
35
35
 
36
36
  # Frida Fusion
37
+ <img src="./fusion_logo.svg" alt="Frida Fusion logo" align="right" width="20%"/>
37
38
 
38
39
  Hook your mobile tests with Frida.
39
40
 
@@ -1,4 +1,5 @@
1
1
  # Frida Fusion
2
+ <img src="./fusion_logo.svg" alt="Frida Fusion logo" align="right" width="20%"/>
2
3
 
3
4
  Hook your mobile tests with Frida.
4
5
 
@@ -1,8 +1,8 @@
1
- __version__ = '0.1.17'
1
+ __version__ = '0.1.20'
2
2
  __title__ = "Frida Fusion"
3
3
  __description__ = "📱 frida-fusion - runtime mobile exploration"
4
4
  __url__ = "https://github.com/helviojunior/frida-fusion"
5
- __build__ = 0xe289b59
5
+ __build__ = 0x472df92
6
6
  __author__ = "Helvio Junior (M4v3r1ck)"
7
7
  __author_email__ = "helvio_junior@hotmail.com"
8
8
  __license__ = "GPL-3.0"
@@ -7,7 +7,7 @@ import signal
7
7
  from argparse import Namespace
8
8
  from pathlib import Path
9
9
 
10
- from .module import Module, ModuleManager, InternalModule, ExternalModule
10
+ from .module import Module, ModuleManager, InternalModule, ExternalModule, LocalModule
11
11
  from .libs.color import Color
12
12
  from .libs.logger import Logger
13
13
  from .__meta__ import __version__
@@ -70,6 +70,8 @@ class Configuration(object):
70
70
 
71
71
  sys.argv[0] = 'frida-fusion'
72
72
 
73
+ Configuration.cmd_line = ' '.join([word for word in sys.argv])
74
+
73
75
  list_modules = any(['--list-modules' == word for word in sys.argv])
74
76
  #show_help = any(['-h' == word for word in sys.argv])
75
77
 
@@ -132,6 +134,8 @@ class Configuration(object):
132
134
  Color.pl('{!} {R}error: you must specify just one parameter {O}--package{R} or {O}--attach-pid{R}{W}\r\n')
133
135
  Configuration.mandatory()
134
136
 
137
+ Logger.pl(' {C}command line:{O} %s{W}' % Configuration.cmd_line)
138
+
135
139
  if args.app_id is not None:
136
140
  Configuration.package = args.app_id
137
141
  Logger.pl(' {C}package:{O} %s{W}' % Configuration.package)
@@ -184,9 +188,10 @@ class Configuration(object):
184
188
 
185
189
  Logger.pl(' {C}min debug level:{O} %s{W}' % str(args.debug_level).upper())
186
190
 
191
+ mods = ModuleManager.list_modules(local_path=Path(Configuration.frida_scripts))
187
192
  if (args.enabled_modules is not None and isinstance(args.enabled_modules, list)) or \
188
193
  (args.ignore_messages_modules is not None and isinstance(args.ignore_messages_modules, list)):
189
- mods = ModuleManager.list_modules()
194
+
190
195
  for mod in [
191
196
  m.strip()
192
197
  for md in args.enabled_modules
@@ -206,6 +211,7 @@ class Configuration(object):
206
211
  name = fm.safe_name()
207
212
  if name not in Configuration.enabled_modules.keys():
208
213
  Configuration.enabled_modules[name] = fm
214
+
209
215
  if args.ignore_messages_modules is not None and isinstance(args.ignore_messages_modules, list):
210
216
  for mod in [
211
217
  m.strip()
@@ -227,6 +233,14 @@ class Configuration(object):
227
233
  if name not in Configuration.ignore_messages_modules.keys():
228
234
  Configuration.ignore_messages_modules[name] = fm
229
235
 
236
+ # Enable user defined local modules
237
+ for _, fm in mods.items():
238
+ if isinstance(fm, LocalModule):
239
+ name = fm.safe_name()
240
+ if name not in Configuration.ignore_messages_modules.keys():
241
+ Configuration.ignore_messages_modules[name] = fm
242
+ Configuration.enabled_modules[name] = fm
243
+
230
244
  if len(Configuration.enabled_modules) > 0:
231
245
  Logger.pl(' {C}modules:{O} %s{W}' % ', '.join([
232
246
  m.name
@@ -84,9 +84,9 @@ class Fusion(object):
84
84
  elif Configuration.remote_host is not None:
85
85
  self.device = process.add_remote_device(Configuration.remote_host)
86
86
 
87
- except Exception as e:
87
+ except Exception as err:
88
88
  self.device = None
89
- Logger.pl('\n{!} {R}Error:{O} %s{W}' % str(e))
89
+ Logger.print_exception(err)
90
90
 
91
91
  return self.device
92
92
 
@@ -144,7 +144,8 @@ class Fusion(object):
144
144
  src += dyn
145
145
 
146
146
  if os.path.isfile(Configuration.frida_scripts):
147
- files_js += [Configuration.frida_scripts]
147
+ if Path(Configuration.frida_scripts).suffix.lower() == ".js":
148
+ files_js += [Configuration.frida_scripts]
148
149
  else:
149
150
  files_js += [
150
151
  os.path.join(Configuration.frida_scripts, f)
@@ -152,7 +153,15 @@ class Fusion(object):
152
153
  if f.endswith(".js")
153
154
  ]
154
155
 
156
+ # Keep unique files
157
+ # Do not use list(set(files_js)) because it will lose the order of modules
158
+ done: set[str] = set()
155
159
  for file_path in files_js:
160
+ if file_path in done:
161
+ continue
162
+
163
+ done.add(file_path)
164
+
156
165
  file_name = Path(file_path).name
157
166
  file_data = self.sanitize_js(open(file_path, 'r', encoding='utf-8').read())
158
167
  if '#NOLOAD' in file_data:
@@ -172,7 +181,7 @@ class Fusion(object):
172
181
 
173
182
  line_cnt = len(file_data.split("\n")) - 1
174
183
 
175
- self.script_trace[file_name] = (offset, offset + line_cnt)
184
+ self.script_trace[file_name] = (offset, offset + line_cnt - 1)
176
185
  offset += line_cnt
177
186
 
178
187
  src += file_data
@@ -186,10 +195,17 @@ class Fusion(object):
186
195
  s = self.session.create_script(src, name="fusion_bundle")
187
196
  s.on("message", self.make_handler("fusion_bundle.js")) # register the message handler
188
197
  s.load()
189
- except Exception as e:
190
-
198
+ except Exception as err:
191
199
  try:
192
- err = str(e)
200
+ from traceback import format_exc
201
+ err_txt = 'Error:{O} %s{W}' % str(err)
202
+ err_txt += '\n{O}Full stack trace below\n'
203
+ err_txt += format_exc().strip()
204
+
205
+ err_txt = err_txt.replace('\n', '\n{W} ')
206
+ err_txt = err_txt.replace(' File', '{W}{D}File')
207
+ err_txt = err_txt.replace(' Exception: ', '{R}Exception: {O}')
208
+
193
209
  pattern = re.compile(r'script\(line (\d+)\):')
194
210
  matches = [
195
211
  (
@@ -199,15 +215,16 @@ class Fusion(object):
199
215
  line=m.group(1),
200
216
  ))
201
217
  )
202
- for m in pattern.finditer(err)
218
+ for m in pattern.finditer(err_txt)
203
219
  ]
204
220
  for m in matches:
205
- err = err.replace(m[0], f"{m[1].file_name}(line {m[1].line})")
206
- Logger.pl('{!} {R}Error:{O} %s{W}' % err)
221
+ err_txt = err_txt.replace(m[0], f"{m[1].file_name}(line {m[1].line})")
222
+
223
+ Logger.pl(err_txt)
207
224
  print("")
208
225
  sys.exit(1)
209
- except Exception:
210
- Logger.pl('{!} {R}Error:{O} %s{W}' % str(e))
226
+ except Exception as e2:
227
+ Logger.print_exception(e2)
211
228
  print("")
212
229
  sys.exit(1)
213
230
 
@@ -383,7 +400,7 @@ class Fusion(object):
383
400
  skm = str(sk)
384
401
 
385
402
  self.print_message_inst("D", "Silent kill requested",
386
- script_location=Logger.get_caller_info(stack_index=1))
403
+ script_location=Logger.get_caller_info(stack_index=1))
387
404
  Fusion.running = False
388
405
  time.sleep(0.2)
389
406
  if skm != "":
@@ -436,7 +453,7 @@ class Fusion(object):
436
453
  }))
437
454
 
438
455
  self.print_message_inst("F", description + stack,
439
- script_location=script_location)
456
+ script_location=script_location)
440
457
  Fusion.running = False
441
458
  time.sleep(0.2)
442
459
  Logger.pl('\n{+} {O}Exiting...{O}{W}')
@@ -465,7 +482,6 @@ class Fusion(object):
465
482
  Logger.pl("")
466
483
  self.done.set()
467
484
 
468
-
469
485
  def _replace_location(self, message: str) -> str:
470
486
  try:
471
487
  matches = [
@@ -501,11 +517,11 @@ class Fusion(object):
501
517
  )
502
518
  except SilentKillError as ske:
503
519
  raise ske
504
- except Exception as e:
505
- if Configuration.debug_level >= 2:
506
- self.print_message_inst("E", f"Error resizing event to module {m.name}: {str(e)}")
507
- else:
508
- self.print_exception(e)
520
+ except Exception as err:
521
+ self.print_exception(
522
+ err,
523
+ script_location=Logger.get_error_info_from_format_exc(stack_index=-1)
524
+ )
509
525
 
510
526
  def _raise_data_event(self,
511
527
  script_location: ScriptLocation = None,
@@ -520,14 +536,14 @@ class Fusion(object):
520
536
  )
521
537
  except SilentKillError as ske:
522
538
  raise ske
523
- except Exception as e:
524
- if Configuration.debug_level >= 2:
525
- self.print_message_inst("E", f"Error resizing event to module {m.name}: {str(e)}")
526
- else:
527
- self.print_exception(e)
539
+ except Exception as err:
540
+ self.print_exception(
541
+ err,
542
+ script_location=Logger.get_error_info_from_format_exc(stack_index=-1)
543
+ )
528
544
 
529
545
  def print_message_inst(self, level: str = "*", message: str = "",
530
- script_location: ScriptLocation = None):
546
+ script_location: ScriptLocation = None):
531
547
 
532
548
  return type(self)._print_message(
533
549
  level=level,
@@ -546,7 +562,7 @@ class Fusion(object):
546
562
 
547
563
  @classmethod
548
564
  def _print_message(cls, level: str = "*", message: str = "",
549
- script_location: ScriptLocation = None):
565
+ script_location: ScriptLocation = None):
550
566
 
551
567
  if Fusion.running is False and Logger.debug_level >= 2:
552
568
  return
@@ -580,7 +596,7 @@ class Fusion(object):
580
596
  db.insert_history(**kwargs)
581
597
 
582
598
  @classmethod
583
- def print_exception(cls, err):
599
+ def print_exception(cls, err, script_location: ScriptLocation = None):
584
600
  from traceback import format_exc
585
601
  err_txt = 'Error:{O} %s{W}' % str(err)
586
602
  err_txt += '\n{O}Full stack trace below\n'
@@ -594,7 +610,7 @@ class Fusion(object):
594
610
  level="E",
595
611
  message=Color.s(err_txt),
596
612
  filename_col_len=Fusion.max_filename,
597
- script_location=Logger.get_caller_info(stack_index=2)
613
+ script_location=script_location if script_location is not None else Logger.get_caller_info(stack_index=2)
598
614
  )
599
615
 
600
616
  @classmethod
@@ -4,6 +4,7 @@ import math
4
4
  import shutil
5
5
  import os.path
6
6
  import sqlite3
7
+ import time
7
8
  from functools import reduce
8
9
  from pathlib import Path
9
10
  from sqlite3 import Connection, ProgrammingError
@@ -78,7 +79,7 @@ class Database(object):
78
79
  (columns, values) = self.parse_args(kwargs)
79
80
  sql = "INSERT INTO {} ({}) VALUES ({})" \
80
81
  .format(table_name, ','.join(columns), ', '.join(['?'] * len(columns)))
81
- conn.execute(sql, values)
82
+ self.resilient_execute(conn, sql, values)
82
83
  conn.commit()
83
84
 
84
85
  @connect
@@ -89,7 +90,7 @@ class Database(object):
89
90
  (columns, values) = self.parse_args(kwargs)
90
91
  sql = "INSERT INTO {} ({}) VALUES ({})" \
91
92
  .format(table_name, ','.join(columns), ', '.join(['?'] * len(columns)))
92
- conn.execute(sql, values)
93
+ self.resilient_execute(conn, sql, values)
93
94
  conn.commit()
94
95
 
95
96
  @connect
@@ -98,7 +99,7 @@ class Database(object):
98
99
  (columns, values) = self.parse_args(kwargs)
99
100
  sql = "INSERT OR IGNORE INTO {} ({}) VALUES ({})" \
100
101
  .format(table_name, ','.join(columns), ', '.join(['?'] * len(columns)))
101
- conn.execute(sql, values)
102
+ self.resilient_execute(conn, sql, values)
102
103
  conn.commit()
103
104
 
104
105
  @connect
@@ -107,7 +108,7 @@ class Database(object):
107
108
  (columns, values) = self.parse_args(kwargs)
108
109
  sql = "INSERT OR REPLACE INTO {} ({}) VALUES ({})" \
109
110
  .format(table_name, ','.join(columns), ', '.join(['?'] * len(columns)))
110
- conn.execute(sql, values)
111
+ self.resilient_execute(conn, sql, values)
111
112
  conn.commit()
112
113
 
113
114
  def insert_update_one(self, table_name: str, **kwargs):
@@ -119,7 +120,7 @@ class Database(object):
119
120
  (columns, values) = self.parse_args(kwargs)
120
121
  sql = "INSERT OR IGNORE INTO {} ({}) VALUES ({})" \
121
122
  .format(table_name, ','.join(columns), ', '.join(['?'] * len(columns)))
122
- c = conn.execute(sql, values)
123
+ c = self.resilient_execute(conn, sql, values)
123
124
 
124
125
  status = {'inserted': c.rowcount, 'updated': 0}
125
126
 
@@ -135,7 +136,7 @@ class Database(object):
135
136
  sql += "{}".format(', '.join([f'{col} = ?' for col in u_columns]))
136
137
  if len(f_columns) > 0:
137
138
  sql += " WHERE {}".format(f' and '.join([f'{col} = ?' for col in f_columns]))
138
- c = conn.execute(sql, tuple(u_values + f_values, ))
139
+ c = self.resilient_execute(conn, sql, tuple(u_values + f_values, ))
139
140
  conn.commit()
140
141
 
141
142
  status['updated'] = c.rowcount
@@ -155,12 +156,16 @@ class Database(object):
155
156
  if len(columns) > 0:
156
157
  sql += " WHERE {}".format(f' {operator} '.join([f'{col} = ?' for col in columns]))
157
158
 
158
- cursor = conn.execute(sql, values)
159
- if cursor.rowcount == 0:
160
- return []
159
+ data = []
160
+ with conn: # Transaction
161
+ cursor = self.resilient_execute(conn, sql, values)
162
+ if cursor.rowcount == 0:
163
+ return data
161
164
 
162
- columns = cursor.description
163
- return [{columns[index][0]: column for index, column in enumerate(value)} for value in cursor.fetchall()]
165
+ columns = cursor.description
166
+ data = [{columns[index][0]: column for index, column in enumerate(value)} for value in cursor.fetchall()]
167
+
168
+ return data
164
169
 
165
170
  def select_first(self, table_name, **kwargs):
166
171
  data = self.select(table_name, **kwargs)
@@ -170,11 +175,15 @@ class Database(object):
170
175
 
171
176
  @connect
172
177
  def select_raw(self, conn: Connection, sql: str, args: any):
173
- cursor = conn.execute(sql, tuple(args,))
178
+ cursor = self.resilient_execute(conn, sql, tuple(args,))
174
179
  if cursor.rowcount == 0:
175
180
  return []
176
181
  columns = cursor.description
177
- return [{columns[index][0]: column for index, column in enumerate(value)} for value in cursor.fetchall()]
182
+ data = []
183
+ with conn: # Transaction
184
+ data = [{columns[index][0]: column for index, column in enumerate(value)} for value in cursor.fetchall()]
185
+
186
+ return data
178
187
 
179
188
  @connect
180
189
  def select_count(self, conn: Connection, table_name, **kwargs) -> int:
@@ -187,10 +196,13 @@ class Database(object):
187
196
  sql = f"SELECT count(*) FROM {table_name}"
188
197
  if len(columns) > 0:
189
198
  sql += " WHERE {}".format(f' {operator} '.join([f'{col} = ?' for col in columns]))
190
- cursor = conn.execute(sql, values)
191
- if cursor.rowcount == 0:
192
- return 0
193
- data = cursor.fetchone()
199
+
200
+ data = []
201
+ with conn: # Transaction
202
+ cursor = self.resilient_execute(conn, sql, values)
203
+ if cursor.rowcount == 0:
204
+ return 0
205
+ data = cursor.fetchone()
194
206
 
195
207
  return int(data[0])
196
208
 
@@ -205,7 +217,7 @@ class Database(object):
205
217
  sql = f"DELETE FROM {table_name}"
206
218
  if len(columns) > 0:
207
219
  sql += " WHERE {}".format(f' {operator} '.join([f'{col} = ?' for col in columns]))
208
- conn.execute(sql, values)
220
+ self.resilient_execute(conn, sql, values)
209
221
  conn.commit()
210
222
 
211
223
  @connect
@@ -221,7 +233,7 @@ class Database(object):
221
233
  sql += "{}".format(', '.join([f'{col} = ?' for col in u_columns]))
222
234
  if len(f_columns) > 0:
223
235
  sql += " WHERE {}".format(f' {operator} '.join([f'{col} = ?' for col in f_columns]))
224
- conn.execute(sql, tuple(u_values + f_values, ))
236
+ self.resilient_execute(conn, sql, tuple(u_values + f_values, ))
225
237
  conn.commit()
226
238
 
227
239
  def get_constraints(self, conn: Connection) -> dict:
@@ -238,7 +250,7 @@ class Database(object):
238
250
  ' il.origin = "u" '
239
251
  'ORDER BY table_name, key_name, ii.seqno')
240
252
 
241
- cursor = conn.execute(sql)
253
+ cursor = self.resilient_execute(conn, sql)
242
254
  columns = cursor.description
243
255
  db_scheme = [{columns[index][0]: column for index, column in enumerate(value)} for value in cursor.fetchall()]
244
256
 
@@ -333,7 +345,7 @@ class Database(object):
333
345
 
334
346
  conn.commit()
335
347
 
336
- #Must get the constraints
348
+ # Must get the constraints
337
349
  self.get_constraints(conn)
338
350
 
339
351
  def insert_history(self, source: str, data: str, stack_trace: str = ''):
@@ -363,3 +375,15 @@ class Database(object):
363
375
  str
364
376
  """
365
377
  return ''.join(k for k in input_string if k.isalnum() or k in '_-')
378
+
379
+ @classmethod
380
+ def resilient_execute(cls, conn: Connection, *args, **kwargs):
381
+ for _ in range(5):
382
+ try:
383
+ return conn.execute(*args, **kwargs)
384
+ except sqlite3.OperationalError as e:
385
+ if 'database is locked' in str(e).lower():
386
+ time.sleep(0.3)
387
+ else:
388
+ raise e
389
+
@@ -181,6 +181,55 @@ function fusion_bytesToBase64(byteArray){
181
181
  }
182
182
  }
183
183
 
184
+ function fusion_base64ToString(b64) {
185
+ try {
186
+ const StringClass = Java.use('java.lang.String');
187
+ const Base64Class = Java.use('android.util.Base64');
188
+
189
+ // Flags úteis (só para referência/legibilidade)
190
+ const BASE64_DEFAULT = 0x00000000; // decode padrão
191
+ const BASE64_URL_SAFE = 0x00000008; // para strings base64 url-safe
192
+
193
+ // Normaliza entrada
194
+ let s = ('' + b64).trim();
195
+ // Remove prefixo data URI, se existir
196
+ s = s.replace(/^data:.*;base64,/, '');
197
+ // Remove espaços/linhas quebradas
198
+ s = s.replace(/\s+/g, '');
199
+
200
+ // Função para padding quando faltam '='
201
+ function padBase64(x) {
202
+ const m = x.length % 4;
203
+ return m === 0 ? x : x + '===='.slice(m);
204
+ }
205
+
206
+ let decoded = null;
207
+
208
+ // 1) Tenta DEFAULT
209
+ try {
210
+ decoded = Base64Class.decode(s, BASE64_DEFAULT);
211
+ } catch (e1) {
212
+ // 2) Tenta URL_SAFE
213
+ try {
214
+ decoded = Base64Class.decode(s, BASE64_URL_SAFE);
215
+ } catch (e2) {
216
+ // 3) Tenta com padding
217
+ const sp = padBase64(s);
218
+ decoded = Base64Class.decode(sp, BASE64_DEFAULT);
219
+ }
220
+ }
221
+
222
+ // Converte bytes -> String UTF-8
223
+ const result = StringClass.$new(decoded, 'utf-8').toString();
224
+ return result;
225
+
226
+ } catch (err) {
227
+ // mesmo logger que você usa na encode
228
+ fusion_sendMessage("W", err);
229
+ return null;
230
+ }
231
+ }
232
+
184
233
  function fusion_normalizePtr(addr) {
185
234
  let p = ptr(addr);
186
235
  if (Process.arch === 'arm64') p = p.and('0x00FFFFFFFFFFFFFF'); // limpa TBI
@@ -4,7 +4,9 @@ import base64
4
4
  import inspect
5
5
  import json
6
6
  import datetime
7
+ import re
7
8
  from pathlib import Path
9
+ from typing import Optional
8
10
 
9
11
  from .scriptlocation import ScriptLocation
10
12
  from ..libs.color import Color
@@ -83,6 +85,31 @@ class Logger(object):
83
85
  line=str(line_number)
84
86
  )
85
87
 
88
+ @classmethod
89
+ def get_error_info_from_format_exc(cls, stack_index: int = -1) -> Optional[ScriptLocation]:
90
+ """
91
+ Faz o *parse* do texto gerado por traceback.format_exc() e extrai arquivo/linha/função.
92
+ - frame_index: -1 pega o último frame (onde a exceção estourou).
93
+ Formato esperado das linhas:
94
+ File "/caminho/mod.py", line 123, in func
95
+ """
96
+ from traceback import format_exc
97
+
98
+ # Captura tuplas (arquivo, linha, função)
99
+ # Obs.: tolera espaços e caminhos com aspas; não captura a linha de código em si.
100
+ pattern = r'(?i:File) "(.+?)", (?i:line) (\d+), (?i:in) ([^\n\r]+)'
101
+ matches = re.findall(pattern, format_exc())
102
+
103
+ if not matches:
104
+ return None
105
+
106
+ file_path, lineno, func = matches[stack_index]
107
+ return ScriptLocation(
108
+ file_name=Path(file_path).name,
109
+ function_name=func.strip(),
110
+ line=str(lineno),
111
+ )
112
+
86
113
  @staticmethod
87
114
  def json_serial(obj):
88
115
  """JSON serializer for objects not serializable by default json code"""
@@ -167,3 +194,16 @@ class Logger(object):
167
194
  f"{Color.color_reset} {fg_color}{line}{Color.color_reset}")
168
195
 
169
196
  Logger.pl(f_message)
197
+
198
+ @classmethod
199
+ def print_exception(cls, err):
200
+ from traceback import format_exc
201
+ err_txt = 'Error:{O} %s{W}' % str(err)
202
+ err_txt += '\n{O}Full stack trace below\n'
203
+ err_txt += format_exc().strip()
204
+
205
+ err_txt = err_txt.replace('\n', '\n{W} ')
206
+ err_txt = err_txt.replace(' File', '{W}{D}File')
207
+ err_txt = err_txt.replace(' Exception: ', '{R}Exception: {O}')
208
+
209
+ Logger.pl(f"{err_txt}\n")