atomicshop 2.10.8__py3-none-any.whl → 2.11.0__py3-none-any.whl
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 atomicshop might be problematic. Click here for more details.
- atomicshop/__init__.py +1 -1
- atomicshop/addons/mains/msi_unpacker.py +5 -0
- atomicshop/archiver/sevenz_app_w.py +86 -0
- atomicshop/process.py +18 -0
- atomicshop/wrappers/ctyping/__init__.py +0 -0
- atomicshop/wrappers/ctyping/msi_windows_installer/__init__.py +0 -0
- atomicshop/wrappers/ctyping/msi_windows_installer/base.py +134 -0
- atomicshop/wrappers/ctyping/msi_windows_installer/cabs.py +75 -0
- atomicshop/wrappers/ctyping/msi_windows_installer/extract_msi_main.py +136 -0
- atomicshop/wrappers/ctyping/msi_windows_installer/tables.py +419 -0
- atomicshop/wrappers/dockerw/install_docker.py +65 -31
- atomicshop/wrappers/olefilew.py +63 -0
- atomicshop/wrappers/ubuntu_terminal.py +72 -9
- {atomicshop-2.10.8.dist-info → atomicshop-2.11.0.dist-info}/METADATA +2 -1
- {atomicshop-2.10.8.dist-info → atomicshop-2.11.0.dist-info}/RECORD +18 -9
- {atomicshop-2.10.8.dist-info → atomicshop-2.11.0.dist-info}/LICENSE.txt +0 -0
- {atomicshop-2.10.8.dist-info → atomicshop-2.11.0.dist-info}/WHEEL +0 -0
- {atomicshop-2.10.8.dist-info → atomicshop-2.11.0.dist-info}/top_level.txt +0 -0
atomicshop/__init__.py
CHANGED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from ..print_api import print_api
|
|
6
|
+
from .. import process, filesystem
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def is_path_contains_7z_executable(sevenz_path: str) -> bool:
|
|
10
|
+
"""
|
|
11
|
+
Checks if the path contains 7z executable.
|
|
12
|
+
:param sevenz_path: string, The path to the 7z executable.
|
|
13
|
+
:return: bool, True if the path contains 7z executable, False otherwise.
|
|
14
|
+
"""
|
|
15
|
+
executable_path_parts: tuple = Path(sevenz_path).parts
|
|
16
|
+
|
|
17
|
+
if '7z' not in executable_path_parts[-1]:
|
|
18
|
+
return False
|
|
19
|
+
else:
|
|
20
|
+
return True
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def is_executable_a_7z(sevenz_path: str) -> bool:
|
|
24
|
+
"""
|
|
25
|
+
Checks if the 7z executable is installed.
|
|
26
|
+
:param sevenz_path: string, The path to the 7z executable.
|
|
27
|
+
:return: bool, True if the 7z executable is installed, False otherwise.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
# Check if the process itself is installed.
|
|
31
|
+
if process.is_command_exists(sevenz_path):
|
|
32
|
+
# Check that this is the 7z executable.
|
|
33
|
+
try:
|
|
34
|
+
# Run '7z' command and capture output
|
|
35
|
+
result = subprocess.run([sevenz_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
36
|
+
|
|
37
|
+
# Check if the output contains identifying information
|
|
38
|
+
if b"7-Zip" in result.stdout or b"7-Zip" in result.stderr:
|
|
39
|
+
return True
|
|
40
|
+
except Exception:
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def extract_file(
|
|
47
|
+
file_path: str,
|
|
48
|
+
extract_to: str,
|
|
49
|
+
sevenz_path: str = None,
|
|
50
|
+
force_overwrite: bool = False,
|
|
51
|
+
print_kwargs: dict = None
|
|
52
|
+
):
|
|
53
|
+
"""
|
|
54
|
+
Extracts a file to a directory using 7z executable.
|
|
55
|
+
:param file_path: string, The path to the file to extract.
|
|
56
|
+
:param extract_to: string, The directory to extract the file to.
|
|
57
|
+
:param sevenz_path: string, The path to the 7z executable.
|
|
58
|
+
If None, the default path is used, assuming you added 7z to the PATH environment variable.
|
|
59
|
+
:param force_overwrite: bool, If True, the files will be overwritten if they already exist in the output folder.
|
|
60
|
+
:param print_kwargs: dict, The keyword arguments to pass to the print function.
|
|
61
|
+
:return:
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
# Check if the path contains 7z executable.
|
|
65
|
+
if not is_path_contains_7z_executable(sevenz_path):
|
|
66
|
+
raise ValueError("The path to 7z does not contain 7z executable")
|
|
67
|
+
|
|
68
|
+
if not sevenz_path:
|
|
69
|
+
sevenz_path = '7z'
|
|
70
|
+
|
|
71
|
+
# Check if the 7z executable is installed.
|
|
72
|
+
if not is_executable_a_7z(sevenz_path):
|
|
73
|
+
raise RuntimeError("'7z executable' is not a 7z")
|
|
74
|
+
|
|
75
|
+
if not os.path.exists(extract_to):
|
|
76
|
+
os.makedirs(extract_to)
|
|
77
|
+
|
|
78
|
+
command = [f'{sevenz_path}', 'x', file_path, f'-o{extract_to}']
|
|
79
|
+
if force_overwrite:
|
|
80
|
+
command.append('-y')
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
subprocess.run(command, check=True)
|
|
84
|
+
print_api(f"Extracted {file_path} to {extract_to}", **(print_kwargs or {}))
|
|
85
|
+
except subprocess.CalledProcessError as e:
|
|
86
|
+
print_api(f"An error occurred: {e}", **(print_kwargs or {}))
|
atomicshop/process.py
CHANGED
|
@@ -3,6 +3,7 @@ import functools
|
|
|
3
3
|
from typing import Union
|
|
4
4
|
import shlex
|
|
5
5
|
import subprocess
|
|
6
|
+
import shutil
|
|
6
7
|
|
|
7
8
|
from .print_api import print_api
|
|
8
9
|
from .inspect_wrapper import get_target_function_default_args_and_combine_with_current
|
|
@@ -12,6 +13,23 @@ if os.name == 'nt':
|
|
|
12
13
|
from .process_poller import GetProcessList
|
|
13
14
|
|
|
14
15
|
|
|
16
|
+
def is_command_exists(cmd: str) -> bool:
|
|
17
|
+
"""
|
|
18
|
+
The function checks if the command exists in the system.
|
|
19
|
+
:param cmd: string, the command to check.
|
|
20
|
+
:return: bool, True if the command exists, False otherwise.
|
|
21
|
+
|
|
22
|
+
With this you can also check if the command is in the PATH environment variable.
|
|
23
|
+
Example:
|
|
24
|
+
print(is_command_exists('7z')) # C:\\Program Files\\7-Zip\\7z.exe
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
if shutil.which(cmd):
|
|
28
|
+
return True
|
|
29
|
+
else:
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
|
|
15
33
|
def process_execution_decorator(function_name):
|
|
16
34
|
@functools.wraps(function_name)
|
|
17
35
|
def wrapper_process_execution_decorator(*args, **kwargs):
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
import ctypes
|
|
3
|
+
from ctypes import wintypes
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# Load Windows Installer functions
|
|
7
|
+
msi = ctypes.windll.msi
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# Constants
|
|
11
|
+
MSIDBOPEN_READONLY = 0
|
|
12
|
+
ERROR_NO_MORE_ITEMS = 259
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def create_open_db_handle(msi_filepath):
|
|
16
|
+
"""Create a database handle for the specified MSI file."""
|
|
17
|
+
db_handle = ctypes.c_void_p()
|
|
18
|
+
|
|
19
|
+
result = msi.MsiOpenDatabaseW(msi_filepath, MSIDBOPEN_READONLY, ctypes.byref(db_handle))
|
|
20
|
+
if result != 0:
|
|
21
|
+
print(f"Failed to open MSI database. Error code: {result}")
|
|
22
|
+
raise ctypes.WinError(result)
|
|
23
|
+
return db_handle
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def create_open_execute_view_handle(db_handle, query):
|
|
27
|
+
"""Create a view handle for the specified query."""
|
|
28
|
+
# Create view handle.
|
|
29
|
+
view_handle = ctypes.c_void_p()
|
|
30
|
+
|
|
31
|
+
# Open the view.
|
|
32
|
+
result = msi.MsiDatabaseOpenViewW(db_handle, query, ctypes.byref(view_handle))
|
|
33
|
+
if result != 0:
|
|
34
|
+
print(f"Failed to open view. Error code: {result}")
|
|
35
|
+
msi.MsiCloseHandle(db_handle)
|
|
36
|
+
raise ctypes.WinError(result)
|
|
37
|
+
|
|
38
|
+
# Execute the view.
|
|
39
|
+
result = msi.MsiViewExecute(view_handle, None)
|
|
40
|
+
if result != 0:
|
|
41
|
+
print(f"Failed to execute view. Error code: {result}")
|
|
42
|
+
msi.MsiCloseHandle(view_handle)
|
|
43
|
+
msi.MsiCloseHandle(db_handle)
|
|
44
|
+
raise ctypes.WinError(result)
|
|
45
|
+
|
|
46
|
+
return view_handle
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def create_fetch_record_from_view_handle(view_handle):
|
|
50
|
+
"""Fetch a record from the specified view handle."""
|
|
51
|
+
# Fetch the record handle.
|
|
52
|
+
record_handle = ctypes.c_void_p()
|
|
53
|
+
|
|
54
|
+
# Fetch the record.
|
|
55
|
+
fetch_record_result = msi.MsiViewFetch(view_handle, ctypes.byref(record_handle))
|
|
56
|
+
if fetch_record_result == ERROR_NO_MORE_ITEMS:
|
|
57
|
+
msi.MsiCloseHandle(view_handle)
|
|
58
|
+
return record_handle
|
|
59
|
+
elif fetch_record_result != 0:
|
|
60
|
+
print(f"Failed to fetch record. Error code: {fetch_record_result}")
|
|
61
|
+
msi.MsiCloseHandle(view_handle)
|
|
62
|
+
raise ctypes.WinError(fetch_record_result)
|
|
63
|
+
|
|
64
|
+
return record_handle
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def get_table_field_data_from_record(
|
|
68
|
+
record_handle,
|
|
69
|
+
field_index,
|
|
70
|
+
data_type: Literal['stringw', 'integer', 'stream'],
|
|
71
|
+
buffer_size: int = 2048
|
|
72
|
+
):
|
|
73
|
+
"""
|
|
74
|
+
Read data from a specific field in a record.
|
|
75
|
+
:param record_handle: Record handle.
|
|
76
|
+
:param field_index: The field index. Example: 2 for the second field.
|
|
77
|
+
Name,Data
|
|
78
|
+
field_index = 1 for Name
|
|
79
|
+
field_index = 2 for Data
|
|
80
|
+
:param data_type: The type of data to read.
|
|
81
|
+
stringw: Read the data as a wide string.
|
|
82
|
+
integer: Read the data as an integer.
|
|
83
|
+
stream: Read the data as a binary stream (bytes).
|
|
84
|
+
:param buffer_size: The size of the buffer to use when reading the data.
|
|
85
|
+
:return:
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
if data_type == 'stringw':
|
|
89
|
+
buf_size = wintypes.DWORD(buffer_size)
|
|
90
|
+
buf = ctypes.create_unicode_buffer(buf_size.value)
|
|
91
|
+
result = msi.MsiRecordGetStringW(record_handle, field_index, buf, ctypes.byref(buf_size))
|
|
92
|
+
if result != 0:
|
|
93
|
+
if result == 234:
|
|
94
|
+
print("Buffer size too small. Try again with a larger buffer.")
|
|
95
|
+
print(f"Failed to get string from record. Error code: {result}")
|
|
96
|
+
msi.MsiCloseHandle(record_handle)
|
|
97
|
+
raise ctypes.WinError(result)
|
|
98
|
+
return str(buf.value)
|
|
99
|
+
|
|
100
|
+
elif data_type == 'integer':
|
|
101
|
+
int_value = ctypes.c_int()
|
|
102
|
+
result = msi.MsiRecordGetInteger(record_handle, field_index, ctypes.byref(int_value))
|
|
103
|
+
if result != 0:
|
|
104
|
+
print(f"Failed to get integer from record. Error code: {result}")
|
|
105
|
+
msi.MsiCloseHandle(record_handle)
|
|
106
|
+
raise ctypes.WinError(result)
|
|
107
|
+
return int_value.value
|
|
108
|
+
|
|
109
|
+
elif data_type == 'stream':
|
|
110
|
+
# stream_size = wintypes.DWORD()
|
|
111
|
+
# result = msi.MsiRecordReadStream(record_handle, field_index, None, ctypes.byref(stream_size))
|
|
112
|
+
# if result != 0:
|
|
113
|
+
# print(f"Failed to read stream data. Error code: {result}")
|
|
114
|
+
# msi.MsiCloseHandle(record_handle)
|
|
115
|
+
# raise ctypes.WinError(result)
|
|
116
|
+
#
|
|
117
|
+
# stream_data = ctypes.create_string_buffer(stream_size.value)
|
|
118
|
+
# msi.MsiRecordReadStream(record_handle, field_index, stream_data, ctypes.byref(stream_size))
|
|
119
|
+
# return stream_data.raw
|
|
120
|
+
|
|
121
|
+
buffer = ctypes.create_string_buffer(buffer_size)
|
|
122
|
+
read_size = wintypes.DWORD(buffer_size)
|
|
123
|
+
data = bytearray()
|
|
124
|
+
|
|
125
|
+
while True:
|
|
126
|
+
result = msi.MsiRecordReadStream(record_handle, field_index, buffer, ctypes.byref(read_size))
|
|
127
|
+
if result != 0 or read_size.value == 0:
|
|
128
|
+
break
|
|
129
|
+
data.extend(buffer.raw[:read_size.value])
|
|
130
|
+
|
|
131
|
+
return data
|
|
132
|
+
|
|
133
|
+
else:
|
|
134
|
+
raise ValueError(f"Invalid data type: {data_type}. Valid options are 'stringw', 'integer', 'stream'.")
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from . import tables
|
|
5
|
+
from ....archiver import sevenz_app_w
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def resolve_directory_path(directory_info, directory_key):
|
|
9
|
+
parts = []
|
|
10
|
+
current_key = directory_key
|
|
11
|
+
while current_key:
|
|
12
|
+
entry = directory_info.get(current_key)
|
|
13
|
+
if entry:
|
|
14
|
+
parts.append(entry['default_dir'])
|
|
15
|
+
current_key = entry['parent']
|
|
16
|
+
else:
|
|
17
|
+
break
|
|
18
|
+
if not parts:
|
|
19
|
+
return ""
|
|
20
|
+
return str(os.path.join(*reversed(parts)))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def rename_extracted_files_by_file_table_info(
|
|
24
|
+
extracted_files_dir, file_table_info, component_table_info, directory_table_info):
|
|
25
|
+
|
|
26
|
+
for file_key, info in file_table_info.items():
|
|
27
|
+
component = info['component']
|
|
28
|
+
directory_key = component_table_info[component]['directory']
|
|
29
|
+
resolved_directory_path: str = resolve_directory_path(directory_table_info, directory_key)
|
|
30
|
+
|
|
31
|
+
# Divide the path into parts and remove the first part if it is a 'SourceDir' and the last part if it is a dot.
|
|
32
|
+
resolved_directory_parts = Path(resolved_directory_path).parts
|
|
33
|
+
if resolved_directory_parts[-1] == '.':
|
|
34
|
+
resolved_directory_parts = resolved_directory_parts[:-1]
|
|
35
|
+
if resolved_directory_parts[0] == 'SourceDir':
|
|
36
|
+
resolved_directory_parts = resolved_directory_parts[1:]
|
|
37
|
+
resolved_directory_path = str(os.path.join(*resolved_directory_parts))
|
|
38
|
+
|
|
39
|
+
extracted_path = os.path.join(extracted_files_dir, file_key)
|
|
40
|
+
if os.path.exists(extracted_path):
|
|
41
|
+
new_file_name = f"{info['file_name']}"
|
|
42
|
+
new_file_path = os.path.join(extracted_files_dir, resolved_directory_path, new_file_name)
|
|
43
|
+
os.makedirs(os.path.dirname(new_file_path), exist_ok=True)
|
|
44
|
+
os.rename(extracted_path, new_file_path)
|
|
45
|
+
print(f"Renamed: [{file_key}] to [{new_file_name}]")
|
|
46
|
+
else:
|
|
47
|
+
print(f"File not found: [{file_key}] in {extracted_path}")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def extract_files_from_cab(db_handle, main_cab_path: str, output_directory: str, sevenz_path: str = None):
|
|
51
|
+
"""
|
|
52
|
+
Extracts the files from the CAB file using 7z executable and the MSI database handle to get the real file
|
|
53
|
+
names and paths.
|
|
54
|
+
:param db_handle: Database handle to the MSI database.
|
|
55
|
+
:param main_cab_path: string, The path to the main CAB file.
|
|
56
|
+
:param output_directory: string, The directory to extract the files to.
|
|
57
|
+
:param sevenz_path: string, Full path to the 7z executable.
|
|
58
|
+
:return:
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
# Get all the tables entries from the MSI database to correlate them while building the file paths and renaming.
|
|
62
|
+
file_table_info = tables.get_file_table_info(db_handle)
|
|
63
|
+
component_table_info = tables.get_component_table_info(db_handle)
|
|
64
|
+
directory_table_info = tables.get_directory_table_info(db_handle)
|
|
65
|
+
|
|
66
|
+
# Extract the contents of the CAB file (if there are several CAB files, they will be extracted as well).
|
|
67
|
+
sevenz_app_w.extract_file(
|
|
68
|
+
file_path=main_cab_path,
|
|
69
|
+
extract_to=output_directory,
|
|
70
|
+
sevenz_path=sevenz_path,
|
|
71
|
+
force_overwrite=True
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
rename_extracted_files_by_file_table_info(
|
|
75
|
+
output_directory, file_table_info, component_table_info, directory_table_info)
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
from .base import msi
|
|
6
|
+
from . import base, tables, cabs
|
|
7
|
+
from ... import olefilew
|
|
8
|
+
from ....print_api import print_api
|
|
9
|
+
from ....archiver import sevenz_app_w
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Directory names.
|
|
13
|
+
EXTRACTED_FILES: str = "Extracted_MSI_Installation_Files"
|
|
14
|
+
EMBEDDED_FILES: str = "Embedded_MSI_Files"
|
|
15
|
+
CAB_FILES: str = "CAB_Files"
|
|
16
|
+
BINARY_FILES: str = "Binary_Files"
|
|
17
|
+
TABLE_CONTENTS: str = "Table_Contents"
|
|
18
|
+
ICON_FILES: str = "Icon_Files"
|
|
19
|
+
ISSETUPFILES: str = "ISSETUPFILES"
|
|
20
|
+
OLE_METADATA: str = "OLE_Metadata"
|
|
21
|
+
REGISTRY_CHANGES: str = "Registry_Changes"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def parse_args():
|
|
25
|
+
parser = argparse.ArgumentParser(description="Extract files from the MSI file.")
|
|
26
|
+
parser.add_argument("--msi_filepath", "-m", type=str, help="The path to the MSI file.")
|
|
27
|
+
parser.add_argument("--output_directory", "-o", type=str, help="The main output directory.")
|
|
28
|
+
parser.add_argument("--sevenz_path", "-s", type=str, help="The path to the 7z executable.")
|
|
29
|
+
return parser.parse_args()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def extract_files_from_msi_main(
|
|
33
|
+
msi_filepath: str = None,
|
|
34
|
+
main_out_directory: str = None,
|
|
35
|
+
sevenz_path: str = None
|
|
36
|
+
):
|
|
37
|
+
"""
|
|
38
|
+
Extracts files from the MSI file using the MSI database.
|
|
39
|
+
What can be extracted:
|
|
40
|
+
- MSI file Table contents
|
|
41
|
+
- MSI file Binary files
|
|
42
|
+
- MSI file Icon files
|
|
43
|
+
- MSI file ISSETUPFILES
|
|
44
|
+
- MSI file Registry changes
|
|
45
|
+
- MSI file CAB files
|
|
46
|
+
- Application Installation files.
|
|
47
|
+
These are extracted from the CAB files and renamed to their real names from the DB.
|
|
48
|
+
- MSI file OLE metadata
|
|
49
|
+
|
|
50
|
+
Usage:
|
|
51
|
+
# You can create a python file with the following content and run it:
|
|
52
|
+
from atomicshop.wrappers.ctyping.msi_windows_installer import extract_msi_main
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
if __name__ == "__main__":
|
|
56
|
+
extract_msi_main.extract_files_from_msi_main()
|
|
57
|
+
|
|
58
|
+
Or you can just use the function with the appropriate parameters directly:
|
|
59
|
+
extract_msi_main.extract_files_from_msi_main(
|
|
60
|
+
msi_filepath=r"C:\\Setup.msi",
|
|
61
|
+
main_out_directory=r"c:\\unpacked_msi",
|
|
62
|
+
sevenz_path=r"C:\\7z.exe")
|
|
63
|
+
|
|
64
|
+
:param msi_filepath: string, The path to the MSI file.
|
|
65
|
+
:param main_out_directory: string, The main output directory.
|
|
66
|
+
:param sevenz_path: string, The path to the 7z executable. If None, the default path is used, which assumes that 7z
|
|
67
|
+
is in the PATH environment variable.
|
|
68
|
+
:return:
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
args = parse_args()
|
|
72
|
+
|
|
73
|
+
# If arguments to the function were not provided, use the arguments from the command line.
|
|
74
|
+
if not msi_filepath:
|
|
75
|
+
msi_filepath = args.msi_filepath
|
|
76
|
+
if not main_out_directory:
|
|
77
|
+
main_out_directory = args.output_directory
|
|
78
|
+
if not sevenz_path:
|
|
79
|
+
sevenz_path = args.sevenz_path
|
|
80
|
+
|
|
81
|
+
# If not provided, raise an error.
|
|
82
|
+
if not msi_filepath:
|
|
83
|
+
print_api("The path to the MSI file is not provided.", color="red")
|
|
84
|
+
sys.exit()
|
|
85
|
+
if not main_out_directory:
|
|
86
|
+
print_api("The main output directory is not provided.", color="red")
|
|
87
|
+
if not sevenz_path:
|
|
88
|
+
print_api(
|
|
89
|
+
"The path to the 7z executable is not provided. Assuming 7z is in the PATH environment variable.",
|
|
90
|
+
color="yellow")
|
|
91
|
+
|
|
92
|
+
if not sevenz_app_w.is_path_contains_7z_executable(sevenz_path):
|
|
93
|
+
print_api("The path to 7z does not contain 7z executable", color="red")
|
|
94
|
+
sys.exit()
|
|
95
|
+
|
|
96
|
+
if not os.path.isfile(sevenz_path):
|
|
97
|
+
print_api("The path to 7z executable doesn't exist.", color="red")
|
|
98
|
+
sys.exit()
|
|
99
|
+
|
|
100
|
+
if not sevenz_app_w.is_executable_a_7z(sevenz_path):
|
|
101
|
+
print_api("Provided 7z executable is not 7z.", color="red")
|
|
102
|
+
sys.exit()
|
|
103
|
+
|
|
104
|
+
# Create the main output directory.
|
|
105
|
+
os.makedirs(main_out_directory, exist_ok=True)
|
|
106
|
+
|
|
107
|
+
# Open the MSI database file.
|
|
108
|
+
db_handle = base.create_open_db_handle(msi_filepath)
|
|
109
|
+
|
|
110
|
+
embedded_files_directory = os.path.join(main_out_directory, EMBEDDED_FILES)
|
|
111
|
+
|
|
112
|
+
# Extract files using the MSI database handle.
|
|
113
|
+
tables.extract_table_contents_to_csv(db_handle, os.path.join(embedded_files_directory, TABLE_CONTENTS))
|
|
114
|
+
tables.extract_binary_table_entries(db_handle, os.path.join(embedded_files_directory, BINARY_FILES))
|
|
115
|
+
tables.extract_icon_table_entries(db_handle, os.path.join(embedded_files_directory, ICON_FILES))
|
|
116
|
+
tables.extract_issetupfiles_table_entries(db_handle, os.path.join(embedded_files_directory, ISSETUPFILES))
|
|
117
|
+
tables.extract_registry_changes(db_handle, os.path.join(embedded_files_directory, REGISTRY_CHANGES))
|
|
118
|
+
|
|
119
|
+
# Extract CAB files from the MSI file.
|
|
120
|
+
cab_files = tables.list_media_table_entries(db_handle)
|
|
121
|
+
print(f"CAB files found: {cab_files}")
|
|
122
|
+
extracted_cab_file_paths: list = tables.extract_cab_files_from_media(
|
|
123
|
+
db_handle, os.path.join(embedded_files_directory, CAB_FILES))
|
|
124
|
+
|
|
125
|
+
# Extract installation files from CAB files and rename them to their real names from DB.
|
|
126
|
+
cabs.extract_files_from_cab(
|
|
127
|
+
db_handle,
|
|
128
|
+
extracted_cab_file_paths[0],
|
|
129
|
+
os.path.join(main_out_directory, EXTRACTED_FILES),
|
|
130
|
+
sevenz_path=sevenz_path)
|
|
131
|
+
|
|
132
|
+
# Close the MSI database handle.
|
|
133
|
+
msi.MsiCloseHandle(db_handle)
|
|
134
|
+
|
|
135
|
+
# Extract OLE metadata from the MSI file.
|
|
136
|
+
olefilew.extract_ole_metadata(msi_filepath, os.path.join(embedded_files_directory, OLE_METADATA))
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import csv
|
|
3
|
+
import ctypes
|
|
4
|
+
from ctypes import wintypes
|
|
5
|
+
|
|
6
|
+
from . import base
|
|
7
|
+
from .base import msi
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_column_names(db_handle, table_name):
|
|
11
|
+
"""Fetch column names for a specific table."""
|
|
12
|
+
column_names = []
|
|
13
|
+
query = f"SELECT `Name` FROM `_Columns` WHERE `Table`='{table_name}' ORDER BY `Number`"
|
|
14
|
+
view_handle = base.create_open_execute_view_handle(db_handle, query)
|
|
15
|
+
|
|
16
|
+
while True:
|
|
17
|
+
record_handle = base.create_fetch_record_from_view_handle(view_handle)
|
|
18
|
+
if not record_handle:
|
|
19
|
+
break
|
|
20
|
+
|
|
21
|
+
column_name = base.get_table_field_data_from_record(record_handle, field_index=1, data_type='stringw')
|
|
22
|
+
if column_name:
|
|
23
|
+
column_names.append(column_name)
|
|
24
|
+
|
|
25
|
+
msi.MsiCloseHandle(record_handle)
|
|
26
|
+
msi.MsiCloseHandle(view_handle)
|
|
27
|
+
return column_names
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def list_all_table_names(db_handle) -> list:
|
|
31
|
+
"""List all tables in the MSI database."""
|
|
32
|
+
query = "SELECT `Name` FROM `_Tables`"
|
|
33
|
+
view_handle = base.create_open_execute_view_handle(db_handle, query)
|
|
34
|
+
|
|
35
|
+
tables: list = []
|
|
36
|
+
while True:
|
|
37
|
+
record_handle = base.create_fetch_record_from_view_handle(view_handle)
|
|
38
|
+
|
|
39
|
+
# If record handle is empty then there is nothing more in the buffer, and we can stop the loop.
|
|
40
|
+
if not record_handle:
|
|
41
|
+
break
|
|
42
|
+
|
|
43
|
+
tables.append(base.get_table_field_data_from_record(record_handle, field_index=1, data_type='stringw'))
|
|
44
|
+
|
|
45
|
+
msi.MsiCloseHandle(record_handle)
|
|
46
|
+
msi.MsiViewClose(view_handle)
|
|
47
|
+
|
|
48
|
+
return tables
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def get_table_contents(db_handle, table_name):
|
|
52
|
+
"""Fetch all contents of a specific table using ctypes."""
|
|
53
|
+
view_handle = base.create_open_execute_view_handle(db_handle, f"SELECT * FROM `{table_name}`")
|
|
54
|
+
contents = []
|
|
55
|
+
|
|
56
|
+
# Fetch column names
|
|
57
|
+
column_names = get_column_names(db_handle, table_name)
|
|
58
|
+
contents.append(column_names)
|
|
59
|
+
|
|
60
|
+
while True:
|
|
61
|
+
record_handle = base.create_fetch_record_from_view_handle(view_handle)
|
|
62
|
+
if not record_handle:
|
|
63
|
+
break
|
|
64
|
+
|
|
65
|
+
row = []
|
|
66
|
+
field_count = msi.MsiRecordGetFieldCount(record_handle)
|
|
67
|
+
|
|
68
|
+
for i in range(1, field_count + 1):
|
|
69
|
+
# Try to fetch as a string
|
|
70
|
+
buf_size = wintypes.DWORD(1024)
|
|
71
|
+
buf = ctypes.create_unicode_buffer(buf_size.value)
|
|
72
|
+
result = msi.MsiRecordGetStringW(record_handle, i, buf, ctypes.byref(buf_size))
|
|
73
|
+
|
|
74
|
+
if result == 0:
|
|
75
|
+
row.append(buf.value)
|
|
76
|
+
elif result == 234: # ERROR_MORE_DATA
|
|
77
|
+
# Increase buffer size and try again
|
|
78
|
+
buf_size = wintypes.DWORD(buf_size.value + 1)
|
|
79
|
+
buf = ctypes.create_unicode_buffer(buf_size.value)
|
|
80
|
+
result = msi.MsiRecordGetStringW(record_handle, i, buf, ctypes.byref(buf_size))
|
|
81
|
+
if result == 0:
|
|
82
|
+
row.append(buf.value)
|
|
83
|
+
else:
|
|
84
|
+
row.append(None)
|
|
85
|
+
else:
|
|
86
|
+
# Try to fetch as an integer
|
|
87
|
+
int_value = ctypes.c_int()
|
|
88
|
+
result = msi.MsiRecordGetInteger(record_handle, i, ctypes.byref(int_value))
|
|
89
|
+
if result == 0:
|
|
90
|
+
row.append(int_value.value)
|
|
91
|
+
else:
|
|
92
|
+
# Try to fetch as a stream
|
|
93
|
+
stream_size = wintypes.DWORD()
|
|
94
|
+
result = msi.MsiRecordReadStream(record_handle, i, None, ctypes.byref(stream_size))
|
|
95
|
+
if result == 0:
|
|
96
|
+
stream_data = ctypes.create_string_buffer(stream_size.value)
|
|
97
|
+
msi.MsiRecordReadStream(record_handle, i, stream_data, ctypes.byref(stream_size))
|
|
98
|
+
row.append(stream_data.raw)
|
|
99
|
+
else:
|
|
100
|
+
row.append(None)
|
|
101
|
+
|
|
102
|
+
contents.append(row)
|
|
103
|
+
msi.MsiCloseHandle(record_handle)
|
|
104
|
+
|
|
105
|
+
msi.MsiCloseHandle(view_handle)
|
|
106
|
+
return contents
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def extract_table_contents_to_csv(db_handle, output_directory):
|
|
110
|
+
"""Extracts all table contents to separate CSV files."""
|
|
111
|
+
|
|
112
|
+
os.makedirs(output_directory, exist_ok=True)
|
|
113
|
+
|
|
114
|
+
# Get all the table names.
|
|
115
|
+
table_names: list = list_all_table_names(db_handle)
|
|
116
|
+
# Get all the table contents by fetching each table.
|
|
117
|
+
table_contents = {table: get_table_contents(db_handle, table) for table in table_names}
|
|
118
|
+
print(f"Tables and their contents have been fetched.")
|
|
119
|
+
|
|
120
|
+
# Save each table to a separate CSV file
|
|
121
|
+
for table, contents in table_contents.items():
|
|
122
|
+
csv_file_path = os.path.join(output_directory, f"{table}.csv")
|
|
123
|
+
with open(csv_file_path, "w", newline='') as csv_file:
|
|
124
|
+
writer = csv.writer(csv_file)
|
|
125
|
+
writer.writerows(contents)
|
|
126
|
+
print(f"Table {table} contents saved to {csv_file_path}")
|
|
127
|
+
|
|
128
|
+
print("All table contents saved to separate CSV files in the 'tables' directory.")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def extract_binary_table_entries(db_handle, output_directory: str):
|
|
132
|
+
os.makedirs(output_directory, exist_ok=True)
|
|
133
|
+
|
|
134
|
+
# Create and execute a view to query the Binary table
|
|
135
|
+
query = "SELECT Name, Data FROM Binary"
|
|
136
|
+
view_handle = base.create_open_execute_view_handle(db_handle, query)
|
|
137
|
+
|
|
138
|
+
while True:
|
|
139
|
+
# Fetch a record from the view
|
|
140
|
+
record_handle = base.create_fetch_record_from_view_handle(view_handle)
|
|
141
|
+
if not record_handle:
|
|
142
|
+
break
|
|
143
|
+
|
|
144
|
+
# Get the binary name
|
|
145
|
+
name = base.get_table_field_data_from_record(record_handle, field_index=1, data_type='stringw')
|
|
146
|
+
|
|
147
|
+
# Get the size of the binary data
|
|
148
|
+
data_size = msi.MsiRecordDataSize(record_handle, 2)
|
|
149
|
+
if data_size == 0:
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
# Read the binary data
|
|
153
|
+
binary_data = base.get_table_field_data_from_record(
|
|
154
|
+
record_handle, field_index=2, data_type='stream', buffer_size=data_size)
|
|
155
|
+
|
|
156
|
+
# Save the binary data to a file
|
|
157
|
+
output_filepath = os.path.join(output_directory, name)
|
|
158
|
+
print(f"Extracting binary file [{name}] to {output_filepath}")
|
|
159
|
+
with open(output_filepath, 'wb') as f:
|
|
160
|
+
f.write(binary_data)
|
|
161
|
+
|
|
162
|
+
msi.MsiCloseHandle(record_handle)
|
|
163
|
+
msi.MsiCloseHandle(view_handle)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def extract_icon_table_entries(db_handle, output_directory: str):
|
|
167
|
+
"""Extracts icons from the Icon table in the specified MSI file."""
|
|
168
|
+
os.makedirs(output_directory, exist_ok=True)
|
|
169
|
+
|
|
170
|
+
query = "SELECT Name, Data FROM Icon"
|
|
171
|
+
view_handle = base.create_open_execute_view_handle(db_handle, query)
|
|
172
|
+
|
|
173
|
+
while True:
|
|
174
|
+
# Fetch a record from the view
|
|
175
|
+
record_handle = base.create_fetch_record_from_view_handle(view_handle)
|
|
176
|
+
if not record_handle:
|
|
177
|
+
break
|
|
178
|
+
|
|
179
|
+
# Read the Name (field 1) and Data (field 2) from the record
|
|
180
|
+
icon_filename = base.get_table_field_data_from_record(record_handle, 1, 'stringw')
|
|
181
|
+
icon_data = base.get_table_field_data_from_record(record_handle, 2, 'stream')
|
|
182
|
+
|
|
183
|
+
# Define the output file path
|
|
184
|
+
output_file_path = os.path.join(output_directory, f"{icon_filename}")
|
|
185
|
+
|
|
186
|
+
# Write the icon data to a file
|
|
187
|
+
with open(output_file_path, 'wb') as icon_file:
|
|
188
|
+
icon_file.write(icon_data)
|
|
189
|
+
|
|
190
|
+
print(f"Extracted icon: {output_file_path}")
|
|
191
|
+
|
|
192
|
+
# Close handles.
|
|
193
|
+
msi.MsiCloseHandle(record_handle)
|
|
194
|
+
msi.MsiCloseHandle(view_handle)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def extract_issetupfiles_table_entries(db_handle, output_directory: str):
|
|
198
|
+
"""Extracts icons from the Icon table in the specified MSI file."""
|
|
199
|
+
os.makedirs(output_directory, exist_ok=True)
|
|
200
|
+
|
|
201
|
+
query = "SELECT FileName, Stream FROM ISSetupFile"
|
|
202
|
+
view_handle = base.create_open_execute_view_handle(db_handle, query)
|
|
203
|
+
|
|
204
|
+
while True:
|
|
205
|
+
# Fetch a record from the view
|
|
206
|
+
record_handle = base.create_fetch_record_from_view_handle(view_handle)
|
|
207
|
+
if not record_handle:
|
|
208
|
+
break
|
|
209
|
+
|
|
210
|
+
# Read the Name (field 1) and Data (field 2) from the record
|
|
211
|
+
file_name = base.get_table_field_data_from_record(record_handle, 1, 'stringw')
|
|
212
|
+
file_data = base.get_table_field_data_from_record(record_handle, 2, 'stream')
|
|
213
|
+
|
|
214
|
+
# Define the output file path
|
|
215
|
+
output_file_path = os.path.join(output_directory, f"{file_name}")
|
|
216
|
+
|
|
217
|
+
# Write the icon data to a file
|
|
218
|
+
with open(output_file_path, 'wb') as icon_file:
|
|
219
|
+
icon_file.write(file_data)
|
|
220
|
+
|
|
221
|
+
print(f"Extracted IsSetupFile: {output_file_path}")
|
|
222
|
+
|
|
223
|
+
# Close handles.
|
|
224
|
+
msi.MsiCloseHandle(record_handle)
|
|
225
|
+
msi.MsiCloseHandle(view_handle)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def extract_registry_changes(db_handle, output_directory: str):
|
|
229
|
+
"""Extracts registry changes from the MSI file and writes them to a .reg file."""
|
|
230
|
+
|
|
231
|
+
os.makedirs(output_directory, exist_ok=True)
|
|
232
|
+
registry_file_path: str = os.path.join(output_directory, "registry_changes.reg")
|
|
233
|
+
|
|
234
|
+
# Create and execute a view for the Registry table
|
|
235
|
+
query = "SELECT `Root`, `Key`, `Name`, `Value` FROM `Registry`"
|
|
236
|
+
view_handle = base.create_open_execute_view_handle(db_handle, query)
|
|
237
|
+
|
|
238
|
+
with open(registry_file_path, 'w') as reg_file:
|
|
239
|
+
# Write the .reg file header
|
|
240
|
+
reg_file.write("Windows Registry Editor Version 5.00\n\n")
|
|
241
|
+
|
|
242
|
+
while True:
|
|
243
|
+
# Fetch a record from the view
|
|
244
|
+
record_handle = base.create_fetch_record_from_view_handle(view_handle)
|
|
245
|
+
if not record_handle:
|
|
246
|
+
break
|
|
247
|
+
|
|
248
|
+
# Read the Root (field 1), Key (field 2), Name (field 3), and Value (field 4) from the record
|
|
249
|
+
root = int(base.get_table_field_data_from_record(record_handle, 1, 'stringw'))
|
|
250
|
+
key = base.get_table_field_data_from_record(record_handle, 2, 'stringw')
|
|
251
|
+
name = base.get_table_field_data_from_record(record_handle, 3, 'stringw')
|
|
252
|
+
value = base.get_table_field_data_from_record(record_handle, 4, 'stringw')
|
|
253
|
+
|
|
254
|
+
# Determine the root key name
|
|
255
|
+
root_key = {
|
|
256
|
+
0: "HKEY_CLASSES_ROOT",
|
|
257
|
+
1: "HKEY_CURRENT_USER",
|
|
258
|
+
2: "HKEY_LOCAL_MACHINE",
|
|
259
|
+
3: "HKEY_USERS"
|
|
260
|
+
}.get(root, "UNKNOWN_ROOT")
|
|
261
|
+
|
|
262
|
+
# Format the registry entry
|
|
263
|
+
if name:
|
|
264
|
+
reg_entry = f'[{root_key}\\{key}]\n"{name}"="{value}"\n\n'
|
|
265
|
+
else:
|
|
266
|
+
reg_entry = f'[{root_key}\\{key}]\n@="{value}"\n\n'
|
|
267
|
+
|
|
268
|
+
# Write the registry entry to the .reg file
|
|
269
|
+
reg_file.write(reg_entry)
|
|
270
|
+
|
|
271
|
+
msi.MsiCloseHandle(record_handle)
|
|
272
|
+
msi.MsiCloseHandle(view_handle)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def list_media_table_entries(db_handle):
|
|
276
|
+
"""List all CAB files from the Media table."""
|
|
277
|
+
query = "SELECT `Cabinet` FROM `Media`"
|
|
278
|
+
media_view = base.create_open_execute_view_handle(db_handle, query)
|
|
279
|
+
|
|
280
|
+
cab_files = []
|
|
281
|
+
while True:
|
|
282
|
+
record_handle = base.create_fetch_record_from_view_handle(media_view)
|
|
283
|
+
if not record_handle:
|
|
284
|
+
break
|
|
285
|
+
|
|
286
|
+
# The CAB file names are prefixed with #
|
|
287
|
+
cab_name = base.get_table_field_data_from_record(record_handle, field_index=1, data_type='stringw')
|
|
288
|
+
cab_files.append(cab_name.strip("#"))
|
|
289
|
+
|
|
290
|
+
msi.MsiCloseHandle(record_handle)
|
|
291
|
+
msi.MsiCloseHandle(media_view)
|
|
292
|
+
return cab_files
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def extract_cab_files_from_media(db_handle, output_directory: str):
|
|
296
|
+
"""Extract all CAB files from the list."""
|
|
297
|
+
os.makedirs(output_directory, exist_ok=True)
|
|
298
|
+
|
|
299
|
+
query = "SELECT `Cabinet` FROM `Media` WHERE `Cabinet` IS NOT NULL"
|
|
300
|
+
# Query to fetch CAB files from the Media table
|
|
301
|
+
view_handle = base.create_open_execute_view_handle(db_handle, query)
|
|
302
|
+
|
|
303
|
+
cabinet_name_list: list = []
|
|
304
|
+
while True:
|
|
305
|
+
record_handle = base.create_fetch_record_from_view_handle(view_handle)
|
|
306
|
+
if not record_handle:
|
|
307
|
+
break
|
|
308
|
+
|
|
309
|
+
cabinet_name = base.get_table_field_data_from_record(record_handle, field_index=1, data_type='stringw')
|
|
310
|
+
cabinet_name_list.append(cabinet_name)
|
|
311
|
+
msi.MsiCloseHandle(record_handle)
|
|
312
|
+
msi.MsiCloseHandle(view_handle)
|
|
313
|
+
|
|
314
|
+
cab_file_paths: list = []
|
|
315
|
+
for cabinet_name in cabinet_name_list:
|
|
316
|
+
if cabinet_name.startswith("#"):
|
|
317
|
+
cabinet_name = cabinet_name[1:] # Remove the leading #
|
|
318
|
+
|
|
319
|
+
cab_file_path = os.path.join(output_directory, cabinet_name)
|
|
320
|
+
with open(cab_file_path, 'wb') as f:
|
|
321
|
+
# Read the binary stream from the MSI package
|
|
322
|
+
stream_query = f"SELECT `Data` FROM `_Streams` WHERE `Name`='{cabinet_name}'"
|
|
323
|
+
stream_view = base.create_open_execute_view_handle(db_handle, stream_query)
|
|
324
|
+
|
|
325
|
+
while True:
|
|
326
|
+
stream_record = base.create_fetch_record_from_view_handle(stream_view)
|
|
327
|
+
if not stream_record:
|
|
328
|
+
break
|
|
329
|
+
|
|
330
|
+
data = base.get_table_field_data_from_record(stream_record, field_index=1, data_type='stream')
|
|
331
|
+
|
|
332
|
+
f.write(data)
|
|
333
|
+
|
|
334
|
+
msi.MsiCloseHandle(stream_record)
|
|
335
|
+
msi.MsiViewClose(stream_view)
|
|
336
|
+
msi.MsiCloseHandle(stream_view)
|
|
337
|
+
|
|
338
|
+
print(f"Extracted: {cabinet_name}")
|
|
339
|
+
cab_file_paths.append(cab_file_path)
|
|
340
|
+
|
|
341
|
+
print(f"CAB files extraction completed. Files are saved to {output_directory}")
|
|
342
|
+
return cab_file_paths
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def get_file_table_info(db_handle):
|
|
346
|
+
query = "SELECT `File`, `FileName`, `Component_` FROM `File`"
|
|
347
|
+
view_handle = base.create_open_execute_view_handle(db_handle, query)
|
|
348
|
+
|
|
349
|
+
file_info = {}
|
|
350
|
+
|
|
351
|
+
# Fetch the records
|
|
352
|
+
while True:
|
|
353
|
+
record_handle = base.create_fetch_record_from_view_handle(view_handle)
|
|
354
|
+
if not record_handle:
|
|
355
|
+
break
|
|
356
|
+
|
|
357
|
+
file_key = base.get_table_field_data_from_record(record_handle, field_index=1, data_type='stringw')
|
|
358
|
+
# Handle cases with multiple file names
|
|
359
|
+
file_name = base.get_table_field_data_from_record(record_handle, field_index=2, data_type='stringw')
|
|
360
|
+
file_name = file_name.split('|')[-1]
|
|
361
|
+
component = base.get_table_field_data_from_record(record_handle, field_index=3, data_type='stringw')
|
|
362
|
+
|
|
363
|
+
file_info[file_key] = {'file_name': file_name, 'component': component}
|
|
364
|
+
|
|
365
|
+
msi.MsiCloseHandle(record_handle)
|
|
366
|
+
msi.MsiCloseHandle(view_handle)
|
|
367
|
+
|
|
368
|
+
return file_info
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def get_component_table_info(db_handle):
|
|
372
|
+
component_info = {}
|
|
373
|
+
|
|
374
|
+
query = "SELECT `Component`, `Directory_` FROM `Component`"
|
|
375
|
+
view_handle = base.create_open_execute_view_handle(db_handle, query)
|
|
376
|
+
|
|
377
|
+
# Fetch the records
|
|
378
|
+
while True:
|
|
379
|
+
record_handle = base.create_fetch_record_from_view_handle(view_handle)
|
|
380
|
+
if not record_handle:
|
|
381
|
+
break
|
|
382
|
+
|
|
383
|
+
component_key = base.get_table_field_data_from_record(record_handle, field_index=1, data_type='stringw')
|
|
384
|
+
directory = base.get_table_field_data_from_record(record_handle, field_index=2, data_type='stringw')
|
|
385
|
+
|
|
386
|
+
component_info[component_key] = {'directory': directory}
|
|
387
|
+
|
|
388
|
+
msi.MsiCloseHandle(record_handle)
|
|
389
|
+
msi.MsiCloseHandle(view_handle)
|
|
390
|
+
|
|
391
|
+
return component_info
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def get_directory_table_info(db_handle):
|
|
395
|
+
directory_info = {}
|
|
396
|
+
|
|
397
|
+
# Open a view to the Directory table
|
|
398
|
+
query = "SELECT `Directory`, `Directory_Parent`, `DefaultDir` FROM `Directory`"
|
|
399
|
+
view_handle = base.create_open_execute_view_handle(db_handle, query)
|
|
400
|
+
|
|
401
|
+
# Fetch the records
|
|
402
|
+
while True:
|
|
403
|
+
record_handle = base.create_fetch_record_from_view_handle(view_handle)
|
|
404
|
+
if not record_handle:
|
|
405
|
+
break
|
|
406
|
+
|
|
407
|
+
directory_key = base.get_table_field_data_from_record(record_handle, field_index=1, data_type='stringw')
|
|
408
|
+
parent_key = base.get_table_field_data_from_record(record_handle, field_index=2, data_type='stringw')
|
|
409
|
+
|
|
410
|
+
# Handle cases with multiple directory names with '|' character.
|
|
411
|
+
default_dir_buffer = base.get_table_field_data_from_record(record_handle, field_index=3, data_type='stringw')
|
|
412
|
+
default_dir = default_dir_buffer.split('|')[-1]
|
|
413
|
+
|
|
414
|
+
directory_info[directory_key] = {'parent': parent_key, 'default_dir': default_dir}
|
|
415
|
+
|
|
416
|
+
msi.MsiCloseHandle(record_handle)
|
|
417
|
+
msi.MsiCloseHandle(view_handle)
|
|
418
|
+
|
|
419
|
+
return directory_info
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import os
|
|
1
2
|
import subprocess
|
|
2
3
|
import getpass
|
|
3
4
|
|
|
4
5
|
from ... import process, filesystem, permissions
|
|
5
6
|
from ...print_api import print_api
|
|
7
|
+
from .. import ubuntu_terminal
|
|
6
8
|
|
|
7
9
|
|
|
8
10
|
def is_docker_installed():
|
|
@@ -59,14 +61,21 @@ def add_current_user_to_docker_group(print_kwargs: dict = None):
|
|
|
59
61
|
return False
|
|
60
62
|
|
|
61
63
|
|
|
62
|
-
def install_docker_ubuntu(
|
|
64
|
+
def install_docker_ubuntu(
|
|
65
|
+
use_docker_installer: bool = True,
|
|
66
|
+
rootless: bool = False,
|
|
67
|
+
add_current_user_to_docker_group_bool: bool = False
|
|
68
|
+
):
|
|
63
69
|
"""
|
|
64
70
|
The function will install docker on ubuntu.
|
|
65
71
|
:param rootless: bool, if True, the rootless installation will be performed.
|
|
66
72
|
Meaning, you will be able to run the 'docker' command without sudo and you will not need to add the
|
|
67
73
|
current user to the docker group.
|
|
74
|
+
:param use_docker_installer: bool, if True, the docker installer will be used.
|
|
75
|
+
If False, the docker will be installed using the apt package manager, custom repo and keyring.
|
|
68
76
|
:param add_current_user_to_docker_group_bool: bool, if True, the current user will be added to the docker group.
|
|
69
|
-
So the user will be able to run the 'docker' command without sudo.
|
|
77
|
+
So the user will be able to run the 'docker' command without sudo. If you install docker in rootless mode
|
|
78
|
+
this is not needed.
|
|
70
79
|
|
|
71
80
|
Usage in main.py (run with sudo):
|
|
72
81
|
from atomicshop.wrappers.dockerw import install_docker
|
|
@@ -80,41 +89,66 @@ def install_docker_ubuntu(rootless: bool = True, add_current_user_to_docker_grou
|
|
|
80
89
|
main()
|
|
81
90
|
"""
|
|
82
91
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
92
|
+
if use_docker_installer:
|
|
93
|
+
# Use the docker installer script.
|
|
94
|
+
# The script will install docker and add the current user to the docker group.
|
|
95
|
+
# The script will also install docker-compose and docker-buildx.
|
|
96
|
+
process.execute_script('curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh', shell=True)
|
|
97
|
+
else:
|
|
98
|
+
# Remove the existing keyrings, so we will not be asked to overwrite it if it exists.
|
|
99
|
+
docker_keyring_file_path: str = "/etc/apt/keyrings/docker.gpg"
|
|
100
|
+
filesystem.remove_file(docker_keyring_file_path)
|
|
101
|
+
|
|
102
|
+
script = f"""
|
|
103
|
+
# Step 1: Set up Docker's apt repository
|
|
104
|
+
sudo apt-get update
|
|
105
|
+
sudo apt-get install -y ca-certificates curl gnupg
|
|
106
|
+
sudo install -m 0755 -d /etc/apt/keyrings
|
|
107
|
+
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
|
108
|
+
sudo chmod a+r /etc/apt/keyrings/docker.gpg
|
|
109
|
+
|
|
110
|
+
# Add the repository to Apt sources
|
|
111
|
+
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
|
112
|
+
sudo apt-get update
|
|
113
|
+
|
|
114
|
+
# Step 2: Install the Docker packages
|
|
115
|
+
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
|
116
|
+
|
|
117
|
+
# Step 3: Verify the installation
|
|
118
|
+
# sudo docker run hello-world
|
|
119
|
+
|
|
120
|
+
# Add Privileges to run docker without sudo. Add current user to Docker superuser group.
|
|
121
|
+
# sudo usermod -aG docker $USER
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
process.execute_script(script, shell=True)
|
|
125
|
+
|
|
126
|
+
if rootless:
|
|
127
|
+
# Install uidmap package.
|
|
128
|
+
ubuntu_terminal.update_system_packages()
|
|
129
|
+
ubuntu_terminal.install_packages(['uidmap'])
|
|
130
|
+
|
|
131
|
+
# After 'get-docker.sh' execution, we will install docker in rootless mode.
|
|
132
|
+
subprocess.run(['dockerd-rootless-setuptool.sh', 'install'], shell=True)
|
|
133
|
+
|
|
134
|
+
# Start and enable the docker service in user mode.
|
|
135
|
+
ubuntu_terminal.start_enable_service_check_availability('docker.service', user_mode=True, sudo=False)
|
|
136
|
+
|
|
137
|
+
# Enable lingering so Docker runs when the user is not logged in
|
|
138
|
+
subprocess.run(['sudo', 'loginctl', 'enable-linger', os.getlogin()], shell=True)
|
|
139
|
+
|
|
140
|
+
# Add $HOME/bin to your PATH if it's not already there.
|
|
141
|
+
ubuntu_terminal.add_path_to_bashrc()
|
|
110
142
|
|
|
111
143
|
if add_current_user_to_docker_group_bool:
|
|
112
144
|
# Check if current user that executed the script is a sudo user. If not, use the current user.
|
|
113
145
|
# Add the current user to the docker group.
|
|
114
146
|
add_current_user_to_docker_group()
|
|
115
147
|
|
|
116
|
-
|
|
117
|
-
|
|
148
|
+
# Verify the installation.
|
|
149
|
+
result: list = process.execute_with_live_output('sudo docker run hello-world')
|
|
150
|
+
else:
|
|
151
|
+
result: list = process.execute_with_live_output('docker run hello-world')
|
|
118
152
|
|
|
119
153
|
print_api('\n'.join(result))
|
|
120
154
|
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import datetime
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
import olefile
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def convert_object_to_string(obj):
|
|
9
|
+
if isinstance(obj, bytes):
|
|
10
|
+
# MSI Database uses latin-1 encoding for strings, could be that other ole files too.
|
|
11
|
+
return obj.decode('latin-1')
|
|
12
|
+
elif isinstance(obj, datetime.datetime):
|
|
13
|
+
return obj.strftime('%Y-%m-%d-%H:%M:%S')
|
|
14
|
+
return obj
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def extract_ole_metadata(ole_file_path: str, output_directory: str):
|
|
18
|
+
"""
|
|
19
|
+
Extract metadata from an OLE2 file.
|
|
20
|
+
:param ole_file_path:
|
|
21
|
+
:param output_directory:
|
|
22
|
+
:return:
|
|
23
|
+
"""
|
|
24
|
+
os.makedirs(output_directory, exist_ok=True)
|
|
25
|
+
metadata_file_path = os.path.join(output_directory, "metadata.json")
|
|
26
|
+
|
|
27
|
+
# Check if the file is ole2 file.
|
|
28
|
+
if not olefile.isOleFile(ole_file_path):
|
|
29
|
+
message = f"The file {ole_file_path} is not an OLE2 file."
|
|
30
|
+
print(message)
|
|
31
|
+
with open(metadata_file_path, "w") as metadata_file:
|
|
32
|
+
metadata_file.write(message)
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
# Open the OLE2 file.
|
|
36
|
+
ole = olefile.OleFileIO(ole_file_path)
|
|
37
|
+
|
|
38
|
+
# Get the metadata of the OLE2 file.
|
|
39
|
+
metadata = ole.get_metadata()
|
|
40
|
+
|
|
41
|
+
meta_properties: dict = {
|
|
42
|
+
'SummaryInformation': {},
|
|
43
|
+
'DocumentSummaryInformation': {}
|
|
44
|
+
}
|
|
45
|
+
# Properties from SummaryInformation stream.
|
|
46
|
+
for prop in metadata.SUMMARY_ATTRIBS:
|
|
47
|
+
value = getattr(metadata, prop)
|
|
48
|
+
value = convert_object_to_string(value)
|
|
49
|
+
meta_properties['SummaryInformation'][prop] = value
|
|
50
|
+
# Properties from DocumentSummaryInformation stream.
|
|
51
|
+
for prop in metadata.DOCSUM_ATTRIBS:
|
|
52
|
+
value = getattr(metadata, prop)
|
|
53
|
+
value = convert_object_to_string(value)
|
|
54
|
+
meta_properties['DocumentSummaryInformation'][prop] = value
|
|
55
|
+
|
|
56
|
+
# Save the metadata to a file.
|
|
57
|
+
with open(metadata_file_path, "w") as metadata_file:
|
|
58
|
+
json.dump(meta_properties, metadata_file, indent=4)
|
|
59
|
+
|
|
60
|
+
print(f"Metadata of the OLE2 file saved to {metadata_file_path}")
|
|
61
|
+
|
|
62
|
+
# Close the OLE2 file.
|
|
63
|
+
ole.close()
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import os
|
|
1
2
|
import sys
|
|
2
3
|
import subprocess
|
|
3
4
|
import shutil
|
|
@@ -48,10 +49,25 @@ def upgrade_system_packages():
|
|
|
48
49
|
subprocess.check_call(['sudo', 'apt-get', 'upgrade', '-y'])
|
|
49
50
|
|
|
50
51
|
|
|
51
|
-
def is_service_running(service_name: str, return_false_on_error: bool = False) -> bool:
|
|
52
|
+
def is_service_running(service_name: str, user_mode: bool = False, return_false_on_error: bool = False) -> bool:
|
|
53
|
+
"""
|
|
54
|
+
Function checks if a service is running.
|
|
55
|
+
:param service_name: str, the service name.
|
|
56
|
+
:param user_mode: bool, if True, the service will be checked in user mode.
|
|
57
|
+
:param return_false_on_error: bool, if True, the function will return False if an error occurs.
|
|
58
|
+
:return:
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
command: list = ['systemctl']
|
|
62
|
+
|
|
63
|
+
if user_mode:
|
|
64
|
+
command.append('--user')
|
|
65
|
+
|
|
66
|
+
command.extend(['is-active', service_name])
|
|
67
|
+
|
|
52
68
|
try:
|
|
53
69
|
# Use subprocess to run 'systemctl is-active' and capture its output
|
|
54
|
-
status = subprocess.check_output(
|
|
70
|
+
status = subprocess.check_output(command, text=True).strip()
|
|
55
71
|
except subprocess.CalledProcessError as e:
|
|
56
72
|
if return_false_on_error:
|
|
57
73
|
# Handle error if systemctl command fails
|
|
@@ -66,22 +82,52 @@ def is_service_running(service_name: str, return_false_on_error: bool = False) -
|
|
|
66
82
|
return False
|
|
67
83
|
|
|
68
84
|
|
|
69
|
-
def enable_service(service_name: str):
|
|
85
|
+
def enable_service(service_name: str, sudo: bool = True, user_mode: bool = False):
|
|
70
86
|
"""
|
|
71
87
|
Function enables a service.
|
|
72
88
|
:param service_name: str, the service name.
|
|
89
|
+
:param sudo: bool, if True, the command will be executed with sudo.
|
|
90
|
+
:param user_mode: bool, if True, the service will be enabled in user mode.
|
|
73
91
|
:return:
|
|
74
92
|
"""
|
|
75
|
-
subprocess.check_call(['sudo', 'systemctl', 'enable', service_name])
|
|
76
93
|
|
|
94
|
+
command: list = []
|
|
95
|
+
|
|
96
|
+
if sudo:
|
|
97
|
+
command.append('sudo')
|
|
98
|
+
|
|
99
|
+
command.append('systemctl')
|
|
77
100
|
|
|
78
|
-
|
|
101
|
+
if user_mode:
|
|
102
|
+
command.append('--user')
|
|
103
|
+
|
|
104
|
+
command.extend(['enable', service_name])
|
|
105
|
+
|
|
106
|
+
subprocess.check_call(command)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def start_service(service_name: str, sudo: bool = True, user_mode: bool = False):
|
|
79
110
|
"""
|
|
80
111
|
Function starts a service.
|
|
81
112
|
:param service_name: str, the service name.
|
|
113
|
+
:param sudo: bool, if True, the command will be executed with sudo.
|
|
114
|
+
:param user_mode: bool, if True, the service will be started in user mode.
|
|
82
115
|
:return:
|
|
83
116
|
"""
|
|
84
|
-
|
|
117
|
+
|
|
118
|
+
command: list = []
|
|
119
|
+
|
|
120
|
+
if sudo:
|
|
121
|
+
command.append('sudo')
|
|
122
|
+
|
|
123
|
+
command.append('systemctl')
|
|
124
|
+
|
|
125
|
+
if user_mode:
|
|
126
|
+
command.append('--user')
|
|
127
|
+
|
|
128
|
+
command.extend(['start', service_name])
|
|
129
|
+
|
|
130
|
+
subprocess.check_call(command)
|
|
85
131
|
|
|
86
132
|
|
|
87
133
|
def start_enable_service_check_availability(
|
|
@@ -91,6 +137,8 @@ def start_enable_service_check_availability(
|
|
|
91
137
|
start_service_bool: bool = True,
|
|
92
138
|
enable_service_bool: bool = True,
|
|
93
139
|
check_service_running: bool = True,
|
|
140
|
+
user_mode: bool = False,
|
|
141
|
+
sudo: bool = True,
|
|
94
142
|
print_kwargs: dict = None
|
|
95
143
|
):
|
|
96
144
|
"""
|
|
@@ -103,6 +151,8 @@ def start_enable_service_check_availability(
|
|
|
103
151
|
:param start_service_bool: bool, if True, the service will be started.
|
|
104
152
|
:param enable_service_bool: bool, if True, the service will be enabled.
|
|
105
153
|
:param check_service_running: bool, if True, the function will check if the service is running.
|
|
154
|
+
:param user_mode: bool, if True, the service will be started and enabled in user mode.
|
|
155
|
+
:param sudo: bool, if True, the command will be executed with sudo.
|
|
106
156
|
:param print_kwargs: dict, the print arguments.
|
|
107
157
|
:return:
|
|
108
158
|
"""
|
|
@@ -112,9 +162,9 @@ def start_enable_service_check_availability(
|
|
|
112
162
|
|
|
113
163
|
# Start and enable the service.
|
|
114
164
|
if start_service_bool:
|
|
115
|
-
start_service(service_name)
|
|
165
|
+
start_service(service_name, user_mode=user_mode, sudo=sudo)
|
|
116
166
|
if enable_service_bool:
|
|
117
|
-
enable_service(service_name)
|
|
167
|
+
enable_service(service_name, user_mode=user_mode,sudo=sudo)
|
|
118
168
|
|
|
119
169
|
if check_service_running:
|
|
120
170
|
print_api(
|
|
@@ -122,10 +172,23 @@ def start_enable_service_check_availability(
|
|
|
122
172
|
**(print_kwargs or {}))
|
|
123
173
|
time.sleep(wait_time_seconds)
|
|
124
174
|
|
|
125
|
-
if not is_service_running(service_name):
|
|
175
|
+
if not is_service_running(service_name, user_mode=user_mode):
|
|
126
176
|
print_api(
|
|
127
177
|
f"[{service_name}] service failed to start.", color='red', error_type=True, **(print_kwargs or {}))
|
|
128
178
|
if exit_on_error:
|
|
129
179
|
sys.exit(1)
|
|
130
180
|
else:
|
|
131
181
|
print_api(f"[{service_name}] service is running.", color='green', **(print_kwargs or {}))
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def add_path_to_bashrc():
|
|
185
|
+
"""Add $HOME/bin to the PATH in .bashrc if it's not already present."""
|
|
186
|
+
bashrc_path = os.path.expanduser("~/.bashrc")
|
|
187
|
+
new_path = 'export PATH=$PATH:$HOME/bin\n'
|
|
188
|
+
with open(bashrc_path, 'r+') as bashrc:
|
|
189
|
+
content = bashrc.read()
|
|
190
|
+
if "$HOME/bin" not in content:
|
|
191
|
+
bashrc.write(new_path)
|
|
192
|
+
print("Added $HOME/bin to .bashrc")
|
|
193
|
+
else:
|
|
194
|
+
print("$HOME/bin already in .bashrc")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: atomicshop
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.11.0
|
|
4
4
|
Summary: Atomic functions and classes to make developer life easier
|
|
5
5
|
Author: Denis Kras
|
|
6
6
|
License: MIT License
|
|
@@ -39,6 +39,7 @@ Requires-Dist: dnspython
|
|
|
39
39
|
Requires-Dist: docker
|
|
40
40
|
Requires-Dist: elasticsearch
|
|
41
41
|
Requires-Dist: numpy
|
|
42
|
+
Requires-Dist: olefile
|
|
42
43
|
Requires-Dist: openpyxl
|
|
43
44
|
Requires-Dist: pandas
|
|
44
45
|
Requires-Dist: paramiko
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
atomicshop/__init__.py,sha256=
|
|
1
|
+
atomicshop/__init__.py,sha256=s2wz8eSBb6qWra33TBucyEy5wBD-UCjN1HUhZ99l0VE,123
|
|
2
2
|
atomicshop/_basics_temp.py,sha256=6cu2dd6r2dLrd1BRNcVDKTHlsHs_26Gpw8QS6v32lQ0,3699
|
|
3
3
|
atomicshop/_create_pdf_demo.py,sha256=Yi-PGZuMg0RKvQmLqVeLIZYadqEZwUm-4A9JxBl_vYA,3713
|
|
4
4
|
atomicshop/_patch_import.py,sha256=ENp55sKVJ0e6-4lBvZnpz9PQCt3Otbur7F6aXDlyje4,6334
|
|
@@ -24,7 +24,7 @@ atomicshop/keyboard_press.py,sha256=1W5kRtOB75fulVx-uF2yarBhW0_IzdI1k73AnvXstk0,
|
|
|
24
24
|
atomicshop/pbtkmultifile_argparse.py,sha256=aEk8nhvoQVu-xyfZosK3ma17CwIgOjzO1erXXdjwtS4,4574
|
|
25
25
|
atomicshop/permissions.py,sha256=y63kcxU6GAmnVXqWhs_N29XorOD2xYWlK_CjZ1PpHgA,2897
|
|
26
26
|
atomicshop/print_api.py,sha256=DhbCQd0MWZZ5GYEk4oTu1opRFC-b31g1VWZgTGewG2Y,11568
|
|
27
|
-
atomicshop/process.py,sha256=
|
|
27
|
+
atomicshop/process.py,sha256=iP9H9Tr51o98REVjcFJ8D0BJUcjRpOVsD9yaEWoJ_h4,14686
|
|
28
28
|
atomicshop/process_name_cmd.py,sha256=TNAK6kQZm5JKWzEW6QLqVHEG98ZLNDQiSS4YwDk8V8c,3830
|
|
29
29
|
atomicshop/process_poller.py,sha256=WfmwCLALfTYOq8ri0vkPeqq8ruEyA_43DaN--CU2_XY,10854
|
|
30
30
|
atomicshop/python_file_patcher.py,sha256=kd3rBWvTcosLEk-7TycNdfKW9fZbe161iVwmH4niUo0,5515
|
|
@@ -51,6 +51,7 @@ atomicshop/addons/a_setup_scripts/install_psycopg2_ubuntu.sh,sha256=lM7LkXQ2AxfF
|
|
|
51
51
|
atomicshop/addons/a_setup_scripts/install_pywintrace_0.3.cmd,sha256=lEP_o6rWcBFUyup6_c-LTL3Q2LRMqryLuG3mJw080Zc,115
|
|
52
52
|
atomicshop/addons/mains/install_docker_ubuntu_main_sudo.py,sha256=3VDGDO41Vubzf64DaBapLlFYX52dEdyPBNfolSsbGcM,161
|
|
53
53
|
atomicshop/addons/mains/install_wsl_ubuntu_lts_admin.py,sha256=PrvZ4hMuadzj2GYKRZSwyNayJUuaSnCF9nV6ORqoPdo,123
|
|
54
|
+
atomicshop/addons/mains/msi_unpacker.py,sha256=XAJdEqs-3s9JqIgHpGRL-HxLKpFMXdrlXmq2Is2Pyfk,164
|
|
54
55
|
atomicshop/addons/mains/search_for_hyperlinks_in_docx.py,sha256=HkIdo_Sz9nPbbbJf1mwfwFkyI7vkvpH8qiIkuYopN4w,529
|
|
55
56
|
atomicshop/addons/mains/FACT/factw_fact_extractor_docker_image_main_sudo.py,sha256=DDKX3Wp2SmzMCEtCIEOUbEKMob2ZQ7VEQGLEf9uYXrs,320
|
|
56
57
|
atomicshop/addons/mains/FACT/update_extract.py,sha256=H3VsdhlA7xxK5lI_nyrWUdk8GNZXbEUVR_K9cJ4ECAw,506
|
|
@@ -68,6 +69,7 @@ atomicshop/archiver/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSu
|
|
|
68
69
|
atomicshop/archiver/_search_in_zip.py,sha256=dd8qFSvIhcKmtnPj_uYNJFPmMwZp4tZys0kKgTw_ACw,8385
|
|
69
70
|
atomicshop/archiver/archiver.py,sha256=BomnK7zT-nQXA1z0i2R2aTv8eu88wPx7tf2HtOdbmEc,1280
|
|
70
71
|
atomicshop/archiver/search_in_archive.py,sha256=kui33oobL2F3VLgAE8L037rHR8Ud3HpXz_E0tbuiDD4,10473
|
|
72
|
+
atomicshop/archiver/sevenz_app_w.py,sha256=TEOhlZPzKqQPStmyIq5E_yP98brj1t_GuoBgeLXA4dA,3019
|
|
71
73
|
atomicshop/archiver/sevenzs.py,sha256=5i_C50-deC1Cz_GQdMGofV2jeMPbbGWAvih-QnA72cg,1370
|
|
72
74
|
atomicshop/archiver/zips.py,sha256=k742K1bEDtc_4N44j_Waebi-uOkxxavqltvV6q-BLW4,14402
|
|
73
75
|
atomicshop/basics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -145,17 +147,24 @@ atomicshop/wrappers/cryptographyw.py,sha256=H5NaHHDkr97QYhUrHFO9vY218u8k3N3Zgh6b
|
|
|
145
147
|
atomicshop/wrappers/ffmpegw.py,sha256=wcq0ZnAe0yajBOuTKZCCaKI7CDBjkq7FAgdW5IsKcVE,6031
|
|
146
148
|
atomicshop/wrappers/githubw.py,sha256=mQGtj6up1HIvjOD2t0bmOWjLooJLYvuIa7d7H-tknrw,9998
|
|
147
149
|
atomicshop/wrappers/numpyw.py,sha256=sBV4gSKyr23kXTalqAb1oqttzE_2XxBooCui66jbAqc,1025
|
|
150
|
+
atomicshop/wrappers/olefilew.py,sha256=biD5m58rogifCYmYhJBrAFb9O_Bn_spLek_9HofLeYE,2051
|
|
148
151
|
atomicshop/wrappers/pipw.py,sha256=iq4pDhmrHqKbcRi3Y6_v2PQSdb4fqfAx79rsX7JEmXo,801
|
|
149
152
|
atomicshop/wrappers/process_wrapper_pbtk.py,sha256=ycPmBRnv627RWks6N8OhxJQe8Gu3h3Vwj-4HswPOw0k,599
|
|
150
153
|
atomicshop/wrappers/pyopensslw.py,sha256=OBWxA6EJ2vU_Qlf4M8m6ilcG3hyYB4yB0EsXUf7NhEU,6804
|
|
151
|
-
atomicshop/wrappers/ubuntu_terminal.py,sha256=
|
|
154
|
+
atomicshop/wrappers/ubuntu_terminal.py,sha256=Ykt3vSkvS2IddjRfZqwDbW98g4E1cServj6iUhg0n30,6121
|
|
152
155
|
atomicshop/wrappers/wslw.py,sha256=AKphiHLSddL7ErevUowr3f9Y1AgGz_R3KZ3NssW07h8,6959
|
|
153
156
|
atomicshop/wrappers/certauthw/certauth.py,sha256=hKedW0DOWlEigSNm8wu4SqHkCQsGJ1tJfH7s4nr3Bk0,12223
|
|
154
157
|
atomicshop/wrappers/certauthw/certauthw.py,sha256=4WvhjANI7Kzqrr_nKmtA8Kf7B6rute_5wfP65gwQrjw,8082
|
|
158
|
+
atomicshop/wrappers/ctyping/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
155
159
|
atomicshop/wrappers/ctyping/process_winapi.py,sha256=QcXL-ETtlSSkoT8F7pYle97ubGWsjYp8cx8HxkVMgAc,2762
|
|
160
|
+
atomicshop/wrappers/ctyping/msi_windows_installer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
161
|
+
atomicshop/wrappers/ctyping/msi_windows_installer/base.py,sha256=Uu9SlWLsQQ6mjE-ek-ptHcmgiI3Ruah9bdZus70EaVY,4884
|
|
162
|
+
atomicshop/wrappers/ctyping/msi_windows_installer/cabs.py,sha256=htzwb2ROYI8yyc82xApStckPS2yCcoyaw32yC15KROs,3285
|
|
163
|
+
atomicshop/wrappers/ctyping/msi_windows_installer/extract_msi_main.py,sha256=eyFAD1itG0UClPs92GFefIWTccRgRF55KVLKl1pugGw,5402
|
|
164
|
+
atomicshop/wrappers/ctyping/msi_windows_installer/tables.py,sha256=ajiFiLXl1prg2kOhatfT8E2RKX7F8JjUJpwkVuKu1GY,16263
|
|
156
165
|
atomicshop/wrappers/dockerw/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
157
166
|
atomicshop/wrappers/dockerw/dockerw.py,sha256=w8zSJr5C7cbvbuG09ORCpAe0BOcibqqL_Z2EKEBHYK4,6266
|
|
158
|
-
atomicshop/wrappers/dockerw/install_docker.py,sha256=
|
|
167
|
+
atomicshop/wrappers/dockerw/install_docker.py,sha256=QNKLgm_UVpwN63D8v8k8FhOyEwA9XjHi0CT5f1FfEhA,6846
|
|
159
168
|
atomicshop/wrappers/elasticsearchw/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
160
169
|
atomicshop/wrappers/elasticsearchw/config_basic.py,sha256=fDujtrjEjbWiYh_WQ3OcYp_8mXhXPYeKLy4wSPL5qM0,1177
|
|
161
170
|
atomicshop/wrappers/elasticsearchw/elasticsearchw.py,sha256=7TqFdEFznO8NlligJhEKk1vm641ALpCYdaRl1uoXdzM,9768
|
|
@@ -233,8 +242,8 @@ atomicshop/wrappers/socketw/socket_server_tester.py,sha256=AhpurHJmP2kgzHaUbq5ey
|
|
|
233
242
|
atomicshop/wrappers/socketw/socket_wrapper.py,sha256=aXBwlEIJhFT0-c4i8iNlFx2It9VpCEpsv--5Oqcpxao,11624
|
|
234
243
|
atomicshop/wrappers/socketw/ssl_base.py,sha256=k4V3gwkbq10MvOH4btU4onLX2GNOsSfUAdcHmL1rpVE,2274
|
|
235
244
|
atomicshop/wrappers/socketw/statistics_csv.py,sha256=t3dtDEfN47CfYVi0CW6Kc2QHTEeZVyYhc57IYYh5nmA,826
|
|
236
|
-
atomicshop-2.
|
|
237
|
-
atomicshop-2.
|
|
238
|
-
atomicshop-2.
|
|
239
|
-
atomicshop-2.
|
|
240
|
-
atomicshop-2.
|
|
245
|
+
atomicshop-2.11.0.dist-info/LICENSE.txt,sha256=lLU7EYycfYcK2NR_1gfnhnRC8b8ccOTElACYplgZN88,1094
|
|
246
|
+
atomicshop-2.11.0.dist-info/METADATA,sha256=4qiSE0G-IMCVID80QijXfSjtq7_eJvhsvkINwCxNGjA,10447
|
|
247
|
+
atomicshop-2.11.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
248
|
+
atomicshop-2.11.0.dist-info/top_level.txt,sha256=EgKJB-7xcrAPeqTRF2laD_Np2gNGYkJkd4OyXqpJphA,11
|
|
249
|
+
atomicshop-2.11.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|