Skip to content

Commit 22be21d

Browse files
committed
refactor: remove netnodes in favor of local json config
1 parent bc7e47e commit 22be21d

File tree

2 files changed

+46
-233
lines changed

2 files changed

+46
-233
lines changed

readme.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
6. That's pretty much it.
1414

1515
### Tested on:
16-
- [ ] v7.7 SP 1
16+
- [x] v8.3
17+
- [x] v7.7 SP 3
18+
- [x] v7.7 SP 1
1719
- [x] v7.7
1820
- [x] v7.5
1921
- [x] v7.2
@@ -39,8 +41,7 @@ Any contributions you make are **greatly appreciated**.
3941
5. Open a Pull Request
4042

4143
### Thanks to:
42-
[wakatime/sublime-wakatime](https://github.com/wakatime/sublime-wakatime) - Pretty much everything related to `wakatime-cli`\
43-
[williballenthin/ida-netnode](https://github.com/williballenthin/ida-netnode) - `Netnode` class
44+
[wakatime/sublime-wakatime](https://github.com/wakatime/sublime-wakatime) - Pretty much everything related to `wakatime-cli`
4445

4546
### Topics:
4647
[unknowncheats](https://www.unknowncheats.me/forum/general-programming-and-reversing/499989-wakatime-integration-ida-pro.html) \

wakatime.py

Lines changed: 42 additions & 230 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import time
2020
import traceback
2121
import webbrowser
22-
import zlib
22+
from pathlib import Path
2323
from subprocess import PIPE
2424
from subprocess import STDOUT
2525
from zipfile import ZipFile
@@ -55,13 +55,7 @@
5555
is_py3 = (sys.version_info[0] == 3)
5656

5757
# @note: @es3n1n: plugin-related stuff
58-
VERSION = "1.0"
59-
NETNODE_NAME = "$ WakaTime"
60-
61-
# @note: @es3n1n: netnode-related stuff
62-
BLOB_SIZE = 1024
63-
STR_KEYS_TAG = 'N'
64-
STR_TO_INT_MAP_TAG = 'O'
58+
VERSION = "1.1"
6559

6660
# @note: @es3n1n: ida-related stuff
6761
ida_ver = idaapi.get_kernel_version()
@@ -103,222 +97,6 @@
10397
SEND_BUFFER_SECONDS = 30 # seconds between sending buffered heartbeats to API
10498

10599

106-
# @credits: https://github.com/williballenthin/ida-netnode
107-
class NetnodeCorruptError(RuntimeError):
108-
pass
109-
110-
111-
class Netnode(object):
112-
def __init__(self, netnode_name):
113-
self._netnode_name = netnode_name
114-
# self._n = idaapi.netnode(netnode_name, namelen=0, do_create=True)
115-
self._n = idaapi.netnode(netnode_name, 0, True)
116-
117-
@staticmethod
118-
def _decompress(data):
119-
"""
120-
args:
121-
data (bytes): the data to decompress
122-
returns:
123-
bytes: the decompressed data.
124-
"""
125-
return zlib.decompress(data)
126-
127-
@staticmethod
128-
def _compress(data):
129-
"""
130-
args:
131-
data (bytes): the data to compress
132-
returns:
133-
bytes: the compressed data.
134-
"""
135-
return zlib.compress(data)
136-
137-
@staticmethod
138-
def _encode(data):
139-
"""
140-
args:
141-
data (object): the data to serialize to json.
142-
returns:
143-
bytes: the ascii-encoded serialized data buffer.
144-
"""
145-
return json.dumps(data).encode("ascii")
146-
147-
@staticmethod
148-
def _decode(data):
149-
"""
150-
args:
151-
data (bytes): the ascii-encoded json serialized data buffer.
152-
returns:
153-
object: the deserialized object.
154-
"""
155-
return json.loads(data.decode("ascii"))
156-
157-
def _get_next_slot(self, tag):
158-
"""
159-
get the first unused supval table key, or 0 if the
160-
table is empty.
161-
useful for filling the supval table sequentially.
162-
"""
163-
slot = self._n.suplast(tag)
164-
if slot is None or slot == idaapi.BADNODE:
165-
return 0
166-
else:
167-
return slot + 1
168-
169-
def _strdel(self, key):
170-
assert isinstance(key, str)
171-
172-
did_del = False
173-
storekey = self._n.hashval(key, STR_TO_INT_MAP_TAG)
174-
if storekey is not None:
175-
storekey = int(storekey.decode('utf-8'))
176-
self._n.delblob(storekey, STR_KEYS_TAG)
177-
self._n.hashdel(key, STR_TO_INT_MAP_TAG)
178-
did_del = True
179-
if self._n.hashval(key):
180-
self._n.hashdel(key)
181-
did_del = True
182-
183-
if not did_del:
184-
raise KeyError("'{}' not found".format(key))
185-
186-
def _strset(self, key, value):
187-
assert isinstance(key, str)
188-
assert value is not None
189-
190-
try:
191-
self._strdel(key)
192-
except KeyError:
193-
pass
194-
195-
if len(value) > BLOB_SIZE:
196-
storekey = self._get_next_slot(STR_KEYS_TAG)
197-
self._n.setblob(value, storekey, STR_KEYS_TAG)
198-
self._n.hashset(key, str(storekey).encode('utf-8'),
199-
STR_TO_INT_MAP_TAG)
200-
else:
201-
self._n.hashset(key, bytes(value))
202-
203-
def _strget(self, key):
204-
assert isinstance(key, str)
205-
206-
storekey = self._n.hashval(key, STR_TO_INT_MAP_TAG)
207-
if storekey is not None:
208-
storekey = int(storekey.decode('utf-8'))
209-
v = self._n.getblob(storekey, STR_KEYS_TAG)
210-
if v is None:
211-
raise NetnodeCorruptError()
212-
return v
213-
214-
v = self._n.hashval(key)
215-
if v is not None:
216-
return v
217-
218-
raise KeyError("'{}' not found".format(key))
219-
220-
def __getitem__(self, key):
221-
if isinstance(key, str):
222-
v = self._strget(key)
223-
else:
224-
raise TypeError("cannot use {} as key".format(type(key)))
225-
226-
data = self._decompress(v)
227-
return self._decode(data)
228-
229-
def __setitem__(self, key, value):
230-
"""
231-
does not support setting a value to None.
232-
value must be json-serializable.
233-
key must be a string or integer.
234-
"""
235-
assert value is not None
236-
237-
v = self._compress(self._encode(value))
238-
if isinstance(key, str):
239-
self._strset(key, v)
240-
else:
241-
raise TypeError("cannot use {} as key".format(type(key)))
242-
243-
def __delitem__(self, key):
244-
if isinstance(key, str):
245-
self._strdel(key)
246-
else:
247-
raise TypeError("cannot use {} as key".format(type(key)))
248-
249-
def get(self, key, default=None):
250-
try:
251-
return self[key]
252-
except (KeyError, zlib.error):
253-
return default
254-
255-
def __contains__(self, key):
256-
try:
257-
if self[key] is not None:
258-
return True
259-
return False
260-
except (KeyError, zlib.error):
261-
return False
262-
263-
def _iter_str_keys_small(self):
264-
# string keys for all small values
265-
if using_ida7api:
266-
i = self._n.hashfirst()
267-
else:
268-
i = self._n.hash1st() # noqa
269-
while i != idaapi.BADNODE and i is not None:
270-
yield i
271-
if using_ida7api:
272-
i = self._n.hashnext(i)
273-
else:
274-
i = self._n.hashnxt(i) # noqa
275-
276-
def _iter_str_keys_large(self):
277-
# string keys for all big values
278-
if using_ida7api:
279-
i = self._n.hashfirst(STR_TO_INT_MAP_TAG)
280-
else:
281-
i = self._n.hash1st(STR_TO_INT_MAP_TAG) # noqa
282-
while i != idaapi.BADNODE and i is not None:
283-
yield i
284-
if using_ida7api:
285-
i = self._n.hashnext(i, STR_TO_INT_MAP_TAG)
286-
else:
287-
i = self._n.hashnxt(i, STR_TO_INT_MAP_TAG) # noqa
288-
289-
def iterkeys(self):
290-
for key in self._iter_str_keys_small():
291-
yield key
292-
293-
for key in self._iter_str_keys_large():
294-
yield key
295-
296-
def keys(self):
297-
return [k for k in list(self.iterkeys())]
298-
299-
def itervalues(self):
300-
for k in list(self.keys()):
301-
yield self[k]
302-
303-
def values(self):
304-
return [v for v in list(self.itervalues())]
305-
306-
def iteritems(self):
307-
for k in list(self.keys()):
308-
yield k, self[k]
309-
310-
def items(self):
311-
return [(k, v) for k, v in list(self.iteritems())]
312-
313-
def kill(self):
314-
self._n.kill()
315-
self._n = idaapi.netnode(self._netnode_name, 0, True)
316-
317-
318-
# @note: @es3n1n: Initializing global netnode for config
319-
NETNODE = Netnode(NETNODE_NAME)
320-
321-
322100
# @note: @es3n1n: Utils
323101
class Popen(subprocess.Popen):
324102
"""Patched Popen to prevent opening cmd window on Windows platform."""
@@ -335,8 +113,42 @@ def __init__(self, *args, **kwargs):
335113
super(Popen, self).__init__(*args, **kwargs)
336114

337115

116+
class Config:
117+
path = Path(__file__).parent.resolve().absolute() / 'wakatime.config.json'
118+
_cached = None
119+
120+
@classmethod
121+
def read(cls):
122+
if cls._cached is not None:
123+
return cls._cached
124+
125+
if not cls.path.exists():
126+
cls._cached = dict()
127+
return cls._cached
128+
129+
try:
130+
cls._cached = json.loads(cls.path.read_text())
131+
except json.JSONDecodeError:
132+
cls._cached = dict()
133+
return cls._cached
134+
135+
@classmethod
136+
def write(cls, value):
137+
cls.path.write_text(json.dumps(value))
138+
139+
@classmethod
140+
def get_var(cls, key, default=None):
141+
return cls.read().get(key, default)
142+
143+
@classmethod
144+
def set_var(cls, key, value):
145+
cfg = cls.read()
146+
cfg[key] = value
147+
cls.write(cfg)
148+
149+
338150
def log(lvl, message, *args, **kwargs):
339-
if lvl == DEBUG and NETNODE.get('debug', 'false') == 'false':
151+
if lvl == DEBUG and Config.get_var('debug', 'false') == 'false':
340152
return
341153

342154
msg = message
@@ -695,7 +507,7 @@ def read(self):
695507
if self._key:
696508
return self._key
697509

698-
key = NETNODE.get('api_key')
510+
key = Config.get_var('api_key')
699511
if key:
700512
self._key = key
701513
return self._key
@@ -714,9 +526,8 @@ def read(self):
714526
return self._key
715527

716528
def write(self, key):
717-
global NETNODE
718529
self._key = key
719-
NETNODE['api_key'] = str(key)
530+
Config.set_var('api_key', key)
720531

721532

722533
APIKEY = ApiKey()
@@ -989,7 +800,8 @@ def term(self):
989800

990801
@staticmethod
991802
def run(*args): # noqa
992-
dbg = NETNODE.get('debug', "false")
803+
dbg = Config.get_var('debug', "false")
804+
993805
fmt = '''AUTOHIDE NONE
994806
WakaTime integration for IDA Pro
995807
Plugin version: v{}
@@ -1000,7 +812,7 @@ def run(*args): # noqa
1000812

1001813
if ret == 1:
1002814
dbg = "false" if dbg == "true" else "true"
1003-
NETNODE['debug'] = dbg
815+
Config.set_var('debug', dbg)
1004816
log(INFO, 'Set debug to: {}'.format(dbg))
1005817

1006818
if ret == 0:

0 commit comments

Comments
 (0)