Commit 24fa4b58 authored by Michael Müller's avatar Michael Müller Committed by Vitaly Lipatov

inseng: Implement CIF reader and download functions.

FIXME: Needs splitting.
parent 468bb4e6
MODULE = inseng.dll
IMPORTS = uuid ole32 advapi32
IMPORTS = uuid ole32 advapi32 urlmon shlwapi
EXTRADLLFLAGS = -Wb,--prefer-native
C_SRCS = inseng_main.c
C_SRCS = \
icif.c \
inf.c \
inseng_main.c
IDL_SRCS = inseng_classes.idl
/*
* Copyright 2016 Michael Müller
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
*/
#include <stdarg.h>
#include <string.h>
#include "windef.h"
#include "winbase.h"
#include "winuser.h"
#include "inseng_private.h"
#include "wine/list.h"
struct inf_value
{
struct list entry;
char *key;
char *value;
struct inf_section *section;
};
struct inf_section
{
struct list entry;
char *name;
struct list values;
struct inf_file *file;
};
struct inf_file
{
char *content;
DWORD size;
struct list sections;
};
static void inf_value_free(struct inf_value *value)
{
heap_free(value);
}
static void inf_section_free(struct inf_section *section)
{
struct inf_value *val, *val_next;
LIST_FOR_EACH_ENTRY_SAFE(val, val_next, &section->values, struct inf_value, entry)
{
list_remove(&val->entry);
inf_value_free(val);
}
heap_free(section);
}
static const char *get_substitution(struct inf_file *inf, const char *name, int len)
{
struct inf_section *sec;
struct inf_value *value = NULL;
sec = inf_get_section(inf, "Strings");
if (!sec) return NULL;
while (inf_section_next_value(sec, &value))
{
if (strlen(value->key) == len && !strncasecmp(value->key, name, len))
return value->value;
}
return NULL;
}
static int expand_variables_buffer(struct inf_file *inf, const char *str, char *output)
{
const char *p, *var_start = NULL;
int var_len = 0, len = 0;
const char *substitution;
for (p = str; *p; p++)
{
if (*p != '%')
{
if (var_start)
var_len++;
else
{
if (output)
*output++ = *p;
len++;
}
continue;
}
if (!var_start)
{
var_start = p;
var_len = 0;
continue;
}
if (!var_len)
{
/* just an escaped % */
if (output)
*output++ = '%';
len += 1;
var_start = NULL;
continue;
}
substitution = get_substitution(inf, var_start + 1, var_len);
if (!substitution)
{
if (output)
{
memcpy(output, var_start, var_len + 2);
output += var_len + 2;
}
len += var_len + 2;
}
else
{
int sub_len = strlen(substitution);
if (output)
{
memcpy(output, substitution, sub_len);
output += sub_len;
}
len += sub_len;
}
var_start = NULL;
}
if (output) *output = 0;
return len + 1;
}
static char *expand_variables(struct inf_file *inf, const char *str)
{
char *buffer;
int len;
len = expand_variables_buffer(inf, str, NULL);
buffer = heap_alloc(len);
if (!len) return NULL;
expand_variables_buffer(inf, str, buffer);
return buffer;
}
void inf_free(struct inf_file *inf)
{
struct inf_section *sec, *sec_next;
LIST_FOR_EACH_ENTRY_SAFE(sec, sec_next, &inf->sections, struct inf_section, entry)
{
list_remove(&sec->entry);
inf_section_free(sec);
}
heap_free(inf->content);
heap_free(inf);
}
BOOL inf_next_section(struct inf_file *inf, struct inf_section **sec)
{
struct list *next_entry, *cur_position;
if (*sec)
cur_position = &(*sec)->entry;
else
cur_position = &inf->sections;
next_entry = list_next(&inf->sections, cur_position);
if (!next_entry) return FALSE;
*sec = CONTAINING_RECORD(next_entry, struct inf_section, entry);
return TRUE;
}
struct inf_section *inf_get_section(struct inf_file *inf, const char *name)
{
struct inf_section *sec = NULL;
while (inf_next_section(inf, &sec))
{
if (!strcasecmp(sec->name, name))
return sec;
}
return NULL;
}
char *inf_section_get_name(struct inf_section *section)
{
return strdupA(section->name);
}
BOOL inf_section_next_value(struct inf_section *sec, struct inf_value **value)
{
struct list *next_entry, *cur_position;
if (*value)
cur_position = &(*value)->entry;
else
cur_position = &sec->values;
next_entry = list_next(&sec->values, cur_position);
if (!next_entry) return FALSE;
*value = CONTAINING_RECORD(next_entry, struct inf_value, entry);
return TRUE;
}
struct inf_value *inf_get_value(struct inf_section *sec, const char *key)
{
struct inf_value *value = NULL;
while (inf_section_next_value(sec, &value))
{
if (!strcasecmp(value->key, key))
return value;
}
return NULL;
}
char *inf_value_get_key(struct inf_value *value)
{
return strdupA(value->key);
}
char *inf_value_get_value(struct inf_value *value)
{
return expand_variables(value->section->file, value->value);
}
char *trim(char *str, char **last_chr, BOOL strip_quotes)
{
char *last;
for (; *str; str++)
{
if (*str != '\t' && *str != ' ')
break;
}
if (!*str)
{
if (last_chr) *last_chr = str;
return str;
}
last = str + strlen(str) - 1;
for (; last > str; last--)
{
if (*last != '\t' && *last != ' ')
break;
*last = 0;
}
if (strip_quotes && last != str)
{
if (*last == '"' && *str == '"')
{
str++;
*last = 0;
}
}
if (last_chr) *last_chr = last;
return str;
}
static char *get_next_line(char **str, char **last_chr)
{
BOOL in_next_line = FALSE;
char *start, *next;
start = *str;
if (!start || !*start) return NULL;
for (next = start; *next; next++)
{
if (*next == '\n' || *next == '\r')
{
*next = 0;
in_next_line = TRUE;
}
else if (in_next_line)
{
break;
}
}
*str = next;
return trim(start, last_chr, FALSE);
}
/* This function only fails in case of an memory allocation error
* and does not touch section in case the parsing failed. */
static HRESULT inf_section_parse(struct inf_file *inf, char *line, char *last_chr, struct inf_section **section)
{
struct inf_section *sec;
char *comment;
char *name;
if (*line != '[')
return S_OK;
line++;
comment = strchr(line, ';');
if (comment)
{
*comment = 0;
line = trim(line, &last_chr, FALSE);
}
if (*last_chr != ']')
return S_OK;
*last_chr = 0;
name = trim(line, NULL, FALSE);
if (!name) return S_OK;
sec = heap_alloc_zero(sizeof(*sec));
if (!sec) return E_OUTOFMEMORY;
sec->name = name;
sec->file = inf;
list_init(&sec->values);
list_add_tail(&inf->sections, &sec->entry);
*section = sec;
return S_OK;
}
static HRESULT inf_value_parse(struct inf_section *sec, char *line)
{
struct inf_value *key_val;
char *key, *value, *del;
del = strchr(line, '=');
if (!del) return S_OK;
*del = 0;
key = line;
value = del + 1;
key = trim(key, NULL, FALSE);
value = trim(value, NULL, TRUE);
key_val = heap_alloc_zero(sizeof(*key_val));
if (!key_val) return E_OUTOFMEMORY;
key_val->key = key;
key_val->value = value;
key_val->section = sec;
list_add_tail(&sec->values, &key_val->entry);
return S_OK;
}
static HRESULT inf_process_content(struct inf_file *inf)
{
struct inf_section *section = NULL;
char *content = inf->content;
char *line, *last_chr;
HRESULT hr = S_OK;
while (SUCCEEDED(hr) && (line = get_next_line(&content, &last_chr)))
{
if (*line == '[')
hr = inf_section_parse(inf, line, last_chr, &section);
else if (strchr(line, '=') && section)
hr = inf_value_parse(section, line);
}
return hr;
}
HRESULT inf_load(const char *path, struct inf_file **inf_file)
{
LARGE_INTEGER file_size;
struct inf_file *inf;
HRESULT hr = E_FAIL;
HANDLE file;
DWORD read;
file = CreateFileA(path, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (file == INVALID_HANDLE_VALUE) return E_FAIL;
inf = heap_alloc_zero(sizeof(*inf));
if (!inf) goto error;
if (!GetFileSizeEx(file, &file_size))
goto error;
inf->size = file_size.QuadPart;
inf->content = heap_alloc_zero(inf->size);
if (!inf->content) goto error;
list_init(&inf->sections);
if (!ReadFile(file, inf->content, inf->size, &read, NULL) || read != inf->size)
goto error;
hr = inf_process_content(inf);
if (FAILED(hr)) goto error;
CloseHandle(file);
*inf_file = inf;
return S_OK;
error:
if (inf) inf_free(inf);
CloseHandle(file);
return hr;
}
......@@ -7,6 +7,6 @@
@ stdcall -private DllRegisterServer()
@ stdcall -private DllUnregisterServer()
@ stub DownloadFile
@ stub GetICifFileFromFile
@ stub GetICifRWFileFromFile
@ stdcall GetICifFileFromFile(ptr str)
@ stdcall GetICifRWFileFromFile(ptr str)
@ stub PurgeDownloadDirectory
/*
* Copyright 2016 Michael Müller
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
*/
#include "windef.h"
#include "winbase.h"
#include "winuser.h"
#include "ole2.h"
#include "rpcproxy.h"
#include "inseng.h"
#include "wine/heap.h"
static inline char *strdupA(const char *src)
{
char *dest = heap_alloc(strlen(src) + 1);
if (dest) strcpy(dest, src);
return dest;
}
static inline WCHAR *strdupW(const WCHAR *src)
{
WCHAR *dest;
if (!src) return NULL;
dest = HeapAlloc(GetProcessHeap(), 0, (lstrlenW(src) + 1) * sizeof(WCHAR));
if (dest) lstrcpyW(dest, src);
return dest;
}
static inline LPWSTR strAtoW(const char *str)
{
LPWSTR ret = NULL;
if (str)
{
DWORD len = MultiByteToWideChar( CP_ACP, 0, str, -1, NULL, 0 );
if ((ret = HeapAlloc(GetProcessHeap(), 0, len * sizeof(WCHAR))))
MultiByteToWideChar(CP_ACP, 0, str, -1, ret, len);
}
return ret;
}
struct inf_value;
struct inf_section;
struct inf_file;
HRESULT inf_load(const char *path, struct inf_file **inf_file) DECLSPEC_HIDDEN;
void inf_free(struct inf_file *inf) DECLSPEC_HIDDEN;
BOOL inf_next_section(struct inf_file *inf, struct inf_section **sec) DECLSPEC_HIDDEN;
struct inf_section *inf_get_section(struct inf_file *inf, const char *name) DECLSPEC_HIDDEN;
char *inf_section_get_name(struct inf_section *section) DECLSPEC_HIDDEN;
BOOL inf_section_next_value(struct inf_section *sec, struct inf_value **value) DECLSPEC_HIDDEN;
struct inf_value *inf_get_value(struct inf_section *sec, const char *key) DECLSPEC_HIDDEN;
char *inf_value_get_key(struct inf_value *value) DECLSPEC_HIDDEN;
char *inf_value_get_value(struct inf_value *value) DECLSPEC_HIDDEN;
char *trim(char *str, char **last_chr, BOOL strip_quotes) DECLSPEC_HIDDEN;
void component_set_actual_download_size(ICifComponent *iface, DWORD size) DECLSPEC_HIDDEN;
void component_set_downloaded(ICifComponent *iface, BOOL value) DECLSPEC_HIDDEN;
void component_set_installed(ICifComponent *iface, BOOL value) DECLSPEC_HIDDEN;
char *component_get_id(ICifComponent *iface) DECLSPEC_HIDDEN;
/*
* Copyright 2015 Jacek Caban for CodeWeavers
* Copyright 2016 Michael Müller
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
......@@ -26,15 +27,231 @@ cpp_quote("#endif")
interface IStream;
/* FIXME: Add full declarations. */
interface ICifComponent;
interface IEnumCifComponents;
interface ICifGroup;
interface IEnumCifGroups;
interface ICifMode;
interface IEnumCifModes;
cpp_quote("#define MAX_ID_LENGTH 48")
cpp_quote("#define MAX_DISPLAYNAME_LENGTH 128")
typedef struct {
cpp_quote("#define URLF_DEFAULT 0x0")
cpp_quote("#define URLF_EXTRACT 0x1")
cpp_quote("#define URLF_RELATIVEURL 0x2")
cpp_quote("#define URLF_DELETE_AFTER_EXTRACT 0x4")
cpp_quote("#define ENGINESTATUS_NOTREADY 0x0")
cpp_quote("#define ENGINESTATUS_LOADING 0x1")
cpp_quote("#define ENGINESTATUS_INSTALLING 0x2")
cpp_quote("#define ENGINESTATUS_READY 0x3")
cpp_quote("#define SETACTION_NONE 0x0")
cpp_quote("#define SETACTION_INSTALL 0x1")
cpp_quote("#define INSTALLOPTIONS_NOCACHE 0x01")
cpp_quote("#define INSTALLOPTIONS_DOWNLOAD 0x02")
cpp_quote("#define INSTALLOPTIONS_INSTALL 0x04")
cpp_quote("#define INSTALLOPTIONS_DONTALLOWXPLATFORM 0x08")
cpp_quote("#define INSTALLOPTIONS_FORCEDEPENDENCIES 0x10")
cpp_quote("#define EXECUTEJOB_SILENT 0x01")
cpp_quote("#define EXECUTEJOB_DELETE_JOB 0x02")
cpp_quote("#define EXECUTEJOB_VERIFYFILES 0x08")
cpp_quote("#define EXECUTEJOB_IGNORETRUST 0x10")
cpp_quote("#define EXECUTEJOB_IGNOREDOWNLOADERROR 0x20")
cpp_quote("#define EXECUTEJOB_DONTALLOWCANCEL 0x40")
cpp_quote("#define ENGINEPROBLEM_DOWNLOADFAIL 0x1")
cpp_quote("#define PLATFORM_WIN95 0x01")
cpp_quote("#define PLATFORM_WIN98 0x02")
cpp_quote("#define PLATFORM_NT4 0x04")
cpp_quote("#define PLATFORM_NT5 0x08")
cpp_quote("#define PLATFORM_NT4ALPHA 0x10")
cpp_quote("#define PLATFORM_NT5ALPHA 0x20")
cpp_quote("#define PLATFORM_MILLEN 0x40")
cpp_quote("#define PLATFORM_ALL (PLATFORM_WIN95 | PLATFORM_WIN98 | PLATFORM_NT4 | PLATFORM_NT5 | PLATFORM_NT4ALPHA | PLATFORM_NT5ALPHA | PLATFORM_MILLEN)")
enum InstallStatus
{
INSTALLSTATUS_INITIALIZING,
INSTALLSTATUS_DEPENDENCY,
INSTALLSTATUS_DOWNLOADING,
INSTALLSTATUS_COPYING,
INSTALLSTATUS_RETRYING,
INSTALLSTATUS_CHECKINGTRUST,
INSTALLSTATUS_EXTRACTING,
INSTALLSTATUS_RUNNING,
INSTALLSTATUS_FINISHED,
INSTALLSTATUS_DOWNLOADFINISHED,
};
enum ComponentAction
{
ActionNone,
ActionInstall,
ActionUninstall,
};
[
object,
local,
pointer_default(unique)
]
interface ICifComponent
{
HRESULT GetID(char *id, DWORD size);
HRESULT GetGUID(char *guid, DWORD size);
HRESULT GetDescription(char *desc, DWORD size);
HRESULT GetDetails(char *details, DWORD size);
HRESULT GetUrl(UINT index, char *url, DWORD size, DWORD *flags);
HRESULT GetFileExtractList(UINT index, char *extract, DWORD size);
HRESULT GetUrlCheckRange(UINT index, DWORD *min, DWORD *max);
HRESULT GetCommand(UINT index, char *cmd, DWORD cmd_size, char *switches, DWORD switch_size, DWORD *type);
HRESULT GetVersion(DWORD *version, DWORD *build);
HRESULT GetLocale(char *pszLocale, DWORD size);
HRESULT GetUninstallKey(char *key, DWORD size);
HRESULT GetInstalledSize(DWORD *win, DWORD *app);
DWORD GetDownloadSize();
DWORD GetExtractSize();
HRESULT GetSuccessKey(char *key, DWORD size);
HRESULT GetProgressKeys(char *progress, DWORD progress_size, char *cancel, DWORD cancel_size);
HRESULT IsActiveSetupAware();
HRESULT IsRebootRequired();
HRESULT RequiresAdminRights();
DWORD GetPriority();
HRESULT GetDependency(UINT index, char *id, DWORD buf, char *type, DWORD *ver, DWORD *build);
DWORD GetPlatform();
HRESULT GetMode(UINT index, char *mode, DWORD size);
HRESULT GetGroup(char *id, DWORD size);
HRESULT IsUIVisible();
HRESULT GetPatchID(char *id, DWORD size);
HRESULT GetDetVersion(char *dll, DWORD dll_size, char *entry, DWORD entry_size);
HRESULT GetTreatAsOneComponents(UINT index, char *id, DWORD buf);
HRESULT GetCustomData(char *key, char *data, DWORD size);
DWORD IsComponentInstalled();
HRESULT IsComponentDownloaded();
DWORD IsThisVersionInstalled(DWORD version, DWORD build, DWORD *ret_version, DWORD *ret_build);
DWORD GetInstallQueueState();
HRESULT SetInstallQueueState(DWORD state);
DWORD GetActualDownloadSize();
DWORD GetCurrentPriority();
HRESULT SetCurrentPriority(DWORD priority);
};
[
object,
local,
pointer_default(unique)
]
interface ICifRWComponent : ICifComponent
{
HRESULT SetGUID(const char *guid);
HRESULT SetDescription(const char *desc);
HRESULT SetUrl(UINT index, const char *url, DWORD url_flags);
HRESULT SetCommand(UINT index, const char *cmd, const char *switches, DWORD type);
HRESULT SetVersion(const char *version);
HRESULT SetUninstallKey(const char *key);
HRESULT SetInstalledSize(DWORD win, DWORD app);
HRESULT SetDownloadSize(DWORD size);
HRESULT SetExtractSize(DWORD size);
HRESULT DeleteDependency(const char *id, char type);
HRESULT AddDependency(const char *id, char type);
HRESULT SetUIVisible(BOOL visible);
HRESULT SetGroup(const char *id);
HRESULT SetPlatform(DWORD platform);
HRESULT SetPriority(DWORD priority);
HRESULT SetReboot(BOOL reboot);
HRESULT DeleteFromModes(const char *mode);
HRESULT AddToMode(const char *mode);
HRESULT SetModes(const char *mode);
HRESULT CopyComponent(const char *ciffile);
HRESULT AddToTreatAsOne(const char *compid);
HRESULT SetDetails(const char *desc);
};
[
object,
local,
pointer_default(unique)
]
interface IEnumCifComponents : IUnknown
{
HRESULT Next(ICifComponent **);
HRESULT Reset();
};
[
object,
local,
pointer_default(unique)
]
interface ICifGroup
{
HRESULT GetID(char *id, DWORD size);
HRESULT GetDescription(char *desc, DWORD size);
DWORD GetPriority();
HRESULT EnumComponents(IEnumCifComponents **, DWORD filter, void *pv);
DWORD GetCurrentPriority();
};
[
object,
local,
pointer_default(unique)
]
interface ICifRWGroup : ICifGroup
{
HRESULT SetDescription(const char *desc);
HRESULT SetPriority(DWORD priority);
HRESULT SetDetails(const char *details);
};
[
object,
local,
pointer_default(unique)
]
interface IEnumCifGroups : IUnknown
{
HRESULT Next(ICifGroup **);
HRESULT Reset();
};
[
object,
local,
pointer_default(unique)
]
interface ICifMode
{
HRESULT GetID(char *id, DWORD size);
HRESULT GetDescription(char *desc, DWORD size);
HRESULT GetDetails(char *details, DWORD size);
HRESULT EnumComponents(IEnumCifComponents **, DWORD filter, void *pv);
};
[
object,
local,
pointer_default(unique)
]
interface ICifRWMode : ICifMode
{
HRESULT SetDescription(const char *desc);
HRESULT SetDetails(const char *details);
};
[
object,
local,
pointer_default(unique)
]
interface IEnumCifModes : IUnknown
{
HRESULT Next(ICifMode **);
HRESULT Reset();
};
typedef struct
{
DWORD cbSize;
DWORD dwInstallSize;
DWORD dwWinDriveSize;
......@@ -49,6 +266,15 @@ typedef struct {
DWORD dwTotalDownloadSize;
} COMPONENT_SIZES;
typedef struct
{
DWORD cbSize;
DWORD dwDownloadKBRemaining;
DWORD dwInstallKBRemaining;
DWORD dwDownloadSecsRemaining;
DWORD dwInstallSecsRemaining;
} INSTALLPROGRESS;
[
uuid(6e449688-c509-11cf-aafa-00aa00b6015c),
local
......@@ -62,7 +288,24 @@ interface ICifFile : IUnknown
HRESULT EnumModes(IEnumCifModes **cuf_modes, DWORD filter, void *pv);
HRESULT FindMode(const char *id, ICifMode **p);
HRESULT GetDescription(char *desc, DWORD size);
HRESULT GetDetDlls(char **dlls, DWORD size);
HRESULT GetDetDlls(char *dlls, DWORD size);
}
[
object,
local,
pointer_default(unique)
]
interface ICifRWFile : ICifFile
{
HRESULT SetDescription(const char *desc);
HRESULT CreateComponent(const char *id, ICifRWComponent **p);
HRESULT CreateGroup(const char *id, ICifRWGroup **p);
HRESULT CreateMode(const char *id, ICifRWMode **p);
HRESULT DeleteComponent(const char *id);
HRESULT DeleteGroup(const char *id);
HRESULT DeleteMode(const char *id);
HRESULT Flush();
}
[
......@@ -78,7 +321,7 @@ interface IInstallEngineCallback : IUnknown
const char *msg_string, ULONG progress, ULONG max);
HRESULT OnStopComponent(const char *id, HRESULT error, DWORD phrase, const char *string, DWORD status);
HRESULT OnStopInstall(HRESULT error, const char *error_string, DWORD status);
HRESULT OnEngineProblem(DWORD problem, LPDWORD action);
HRESULT OnEngineProblem(DWORD problem, DWORD *action);
}
[
......@@ -122,6 +365,16 @@ interface IInstallEngine2 : IInstallEngine
}
[
uuid(6e449687-c509-11cf-aafa-00aa00b6015c),
local
]
interface IInstallEngineTiming : IUnknown
{
HRESULT GetRates(DWORD *download, DWORD *install);
HRESULT GetInstallProgress(INSTALLPROGRESS *progress);
}
[
helpstring("Microsoft Active Setup Engine"),
threading(apartment),
uuid(6e449686-c509-11cf-aafa-00aa00b6015c)
......@@ -134,3 +387,6 @@ coclass InstallEngine { }
uuid(bfc880f1-7484-11d0-8309-00aa00b6015c)
]
coclass DownloadSiteMgr { }
cpp_quote("HRESULT WINAPI GetICifFileFromFile(ICifFile **, const char *);")
cpp_quote("HRESULT WINAPI GetICifRWFileFromFile(ICifRWFile **, const char *);")
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment