Retrieve icon (link / filename) from a .LNK file
Retrieve icon (link / filename) from a .LNK file
I'm looking for a way to retrieve the icon (I think it's only a path to the icon file) from a .lnk (shortcut) file.
I've found some examples on the Internet, but it seems they're all object based (the only object oriented coding I understand is throwing heavy objects at my screen when something doesn't work).
Another way might be to deduce the structure of the .LNK file, and I found some descriptions, but couldn't get those to work yet.
Anyone that tried to retrieve such information from a .LNK file? (And was succesful )
I've found some examples on the Internet, but it seems they're all object based (the only object oriented coding I understand is throwing heavy objects at my screen when something doesn't work).
Another way might be to deduce the structure of the .LNK file, and I found some descriptions, but couldn't get those to work yet.
Anyone that tried to retrieve such information from a .LNK file? (And was succesful )
( PB6.00 LTS Win11 x64 Asrock AB350 Pro4 Ryzen 5 3600 32GB GTX1060 6GB)
( The path to enlightenment and the PureBasic Survival Guide right here... )
( The path to enlightenment and the PureBasic Survival Guide right here... )
Re: Retrieve icon (link / filename) from a .LNK file
link
example
example
Code: Select all
Procedure$ GetShellLinkTargetPath(shellLinkFilePath$)
Protected result$ = Space(#MAX_PATH + 1)
Protected linkFile.IPersistFile
Protected shellLink.IShellLinkW
Protected IconIndexInFile.l
CoInitialize_(0)
If CoCreateInstance_(?CLSID_ShellLink, 0, 1, ?IID_IShellLink, @shellLink) = #S_OK
If shellLink\QueryInterface(?IID_IPersistFile, @linkFile) = #S_OK
If linkFile\Load(shellLinkFilePath$, 0) = #S_OK
If shellLink\Resolve(0, 1) = #S_OK
; shellLink\GetPath(@result$, #MAX_PATH, 0, 0)
shellLink\getIconLocation(@result$, #MAX_PATH, @IconIndexInFile)
Debug IconIndexInFile
Debug result$
EndIf
EndIf
LinkFile\Release()
EndIf
shellLink\Release()
EndIf
CoUninitialize_()
ProcedureReturn result$
DataSection
CLSID_ShellLink:
; 00021401-0000-0000-C000-000000000046
Data.l $00021401
Data.w $0000,$0000
Data.b $C0,$00,$00,$00,$00,$00,$00,$46
IID_IShellLink:
CompilerIf #PB_Compiler_Unicode
; IID_IShellLinkW
; {000214F9-0000-0000-C000-000000000046}
Data.l $000214F9
Data.w $0000,$0000
Data.b $C0,$00,$00,$00,$00,$00,$00,$46
CompilerElse
; 000214EE-0000-0000-C000-000000000046
Data.l $000214EE
Data.w $0000,$0000
Data.b $C0,$00,$00,$00,$00,$00,$00,$46
CompilerEndIf
IID_IPersistFile:
; 0000010b-0000-0000-C000-000000000046
Data.l $0000010b
Data.w $0000,$0000
Data.b $C0,$00,$00,$00,$00,$00,$00,$46
EndDataSection
EndProcedure
GetShellLinkTargetPath("C:\Users\user\Desktop\reboot.lnk")
Re: Retrieve icon (link / filename) from a .LNK file
Hmmmm.
Interesting.
I dove a bit into the .LNK file, trying to figure out the contents / structure. Looks like MS's documentation is a little confusing
I found some of the information I was looking for, and it turns out there are many different ways of using .LNK files. Code below may be of interest to those trying to puzzle it out (I found what I was looking for, but there are some cases where .LNK files contain very weird field combos).
Try the code below on different .LNK files, sometimes it manages to find the proper destination, but I found a few cases it would not, where Windows would still manage to launch the program. I suspect it's a fallback to what's contained inside the LinkInfo section.
Interesting.
I dove a bit into the .LNK file, trying to figure out the contents / structure. Looks like MS's documentation is a little confusing
I found some of the information I was looking for, and it turns out there are many different ways of using .LNK files. Code below may be of interest to those trying to puzzle it out (I found what I was looking for, but there are some cases where .LNK files contain very weird field combos).
Try the code below on different .LNK files, sometimes it manages to find the proper destination, but I found a few cases it would not, where Windows would still manage to launch the program. I suspect it's a fallback to what's contained inside the LinkInfo section.
Code: Select all
EnableExplicit
Declare init()
Declare main()
init()
main()
Procedure init()
;
Structure shelllinkheader
headersize.l ; 4 bytes, always contains $0000004C (dec 76) in reverse byte order (?)
linkclsid.b[16] ; 16 bytes, SHOULD contain $00021401-0000-0000-C000-000000000046 but I've found other values
linkflags.l ; 4 bytes, defining all optional fields in the .lnk file
fileattributes.b[4]
creationtime.FILETIME ; 8 bytes, creation time of link target in UTC
accesstime.FILETIME ; 8 bytes, last access time to link target
writetime.FILETIME ; 8 bytes, last write to link target in UTC
filesize.b[4]
iconindex.b[4]
showcommand.b[4]
hotkey.b[2]
reserved1.b[2]
reserved2.b[4]
reserved3.b[4]
EndStructure
;
; Debug SizeOf(shelllinkheader)
;
EndProcedure
Procedure.s peekhex(p,n)
Protected x.s, s.s, nn.i
;
nn = 0
While nn < n
x = Hex(PeekA(p+nn),#PB_Ascii)
x = RSet(x,2,"0")
nn = nn+1
If nn < n
s = s+x+"."
Else
s = s+x
EndIf
Wend
ProcedureReturn s
EndProcedure
Procedure.i filetimetodate(*ft.FILETIME) ; convert windows file timestamp to pb's date format
Protected ft2.FILETIME, st.SYSTEMTIME
;
FileTimeToLocalFileTime_(*ft,ft2)
FileTimeToSystemTime_(ft2,st)
ProcedureReturn Date(st\wYear,st\wMonth,st\wDay,st\wHour,st\wMinute,st\wSecond)
EndProcedure
Procedure.s GetShellLinkTargetPath(shellLinkFilePath$)
Protected result.s = Space(#MAX_PATH + 1)
Protected linkFile.IPersistFile
Protected shellLink.IShellLinkW
Protected IconIndexInFile.l
;
; this section is by AZJIO from the PureBasic forum
;
DataSection
CLSID_ShellLink:
; 00021401-0000-0000-C000-000000000046
Data.l $00021401
Data.w $0000,$0000
Data.b $C0,$00,$00,$00,$00,$00,$00,$46
IID_IShellLink:
CompilerIf #PB_Compiler_Unicode
; IID_IShellLinkW
; {000214F9-0000-0000-C000-000000000046}
Data.l $000214F9
Data.w $0000,$0000
Data.b $C0,$00,$00,$00,$00,$00,$00,$46
CompilerElse
; 000214EE-0000-0000-C000-000000000046
Data.l $000214EE
Data.w $0000,$0000
Data.b $C0,$00,$00,$00,$00,$00,$00,$46
CompilerEndIf
IID_IPersistFile:
; 0000010b-0000-0000-C000-000000000046
Data.l $0000010b
Data.w $0000,$0000
Data.b $C0,$00,$00,$00,$00,$00,$00,$46
EndDataSection
;
CoInitialize_(0)
;
If CoCreateInstance_(?CLSID_ShellLink, 0, 1, ?IID_IShellLink, @shellLink) = #S_OK
If shellLink\QueryInterface(?IID_IPersistFile, @linkFile) = #S_OK
If linkFile\Load(shellLinkFilePath$, 0) = #S_OK
If shellLink\Resolve(0, 1) = #S_OK
shellLink\GetPath(@result, #MAX_PATH, 0, 0)
Debug "path "+result
shellLink\getIconLocation(@result, #MAX_PATH, @IconIndexInFile)
; Debug IconIndexInFile
Debug "icon "+result
EndIf
EndIf
LinkFile\Release()
EndIf
shellLink\Release()
EndIf
;
CoUninitialize_()
;
ProcedureReturn result
EndProcedure
Procedure main()
Protected file.s
Protected size.i
Protected shelllinkheader.shelllinkheader
Protected l.i, n.i, s.s, q.i
Protected buffer_p.i, buffer_hnd.i
;
file = "d:\PureBasic 64.lnk"
file = "C:\Users\Admin\Desktop\Programming\PureBasic 64.lnk"
file = "d:\WallX Rounded random.lnk"
;
With shelllinkheader
;
ReadFile(1,file,#PB_Ascii)
;
; a .lnk file always starts with a shelllinkheader of 76 bytes
;
; interesting links
;
; - https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-shllink/c3376b21-0931-45e4-b2fc-a48ac0e60d15
; - https://helgeklein.com/blog/dissecting-a-shortcut/
;
ReadData(1,@shelllinkheader,76)
;
; some data retrieved
;
Debug "size $"+peekhex(@\headersize,4)
Debug "linkclsid %"+peekhex(@\linkclsid,16)
Debug "last access "+FormatDate("%dd.%mm.%yyyy %hh:%ii:%ss",filetimetodate(@\accesstime))
;
; linkflags is 32 bits, definining any optional componets of the .lnk file
;
Debug "linkflags %"+RSet(Bin(\linkflags,#PB_Long),32,"0")
Debug "as admin %00000000000000000110000011110011"
Debug "not admin %00000000000000000100000011110011"
;
; linkflags bit 0 - 1 - A - linktargetidlist - variable size, first 2 bytes contain size
;
If (\linkflags & 1)
l = ReadWord(1)
Debug "A linktargetidlist "+Str(l)+" bytes"
n = 0
While n < l
ReadByte(1)
n = n+1
Wend
EndIf
;
; linkflags bit 1 - 2 - B - linkinfo - variable size, first 4 bytes contain size
;
If (\linkflags & 2)
l = ReadLong(1)
Debug "B linkinfo "+Str(l)+" bytes"
n = 0
While n < l-4
ReadByte(1)
n = n+1
Wend
EndIf
;
; linkflags bit 2 - 4 - C - name - stringdata structure, first 2 bytes contain size in characters (unicode)
;
If (\linkflags & 4)
l = ReadWord(1)*2
n = 0
While n < l-2
ReadByte(1)
n = n+1
Wend
Debug "C name "+Str(l)+" bytes"
EndIf
;
; linkflags bit 3 - 8 - D - relativepath
;
If (\linkflags & 8)
l = ReadWord(1)
n = 0
s = ""
While n < l
s = s+Chr(ReadWord(1))
n = n+1
Wend
Debug "D relative path "+Str(l)+" bytes "+s
EndIf
;
; linkflags bit 4 - 16 - E - working dir
;
If (\linkflags & 16)
l = ReadWord(1)
n = 0
s = ""
While n < l
s = s+Chr(ReadWord(1))
n = n+1
Wend
Debug "E working dir "+Str(l)+" bytes "+s
EndIf
;
; linkflags bit 5 - 32 - F - arguments
;
If (\linkflags & 32)
l = ReadWord(1)
n = 0
s = ""
While n < l
s = s+Chr(ReadWord(1))
n = n+1
Wend
Debug "F arguments "+Str(l)+" bytes "+s
EndIf
;
; linkflags bit 6 - 64 - G - iconlocation
;
If (\linkflags & 64)
l = ReadWord(1)
n = 0
s = ""
While n < l
s = s+Chr(ReadWord(1))
n = n+1
Wend
Debug "G iconlocation "+Str(l)+" bytes "+s
EndIf
;
; linkflags bit 13 - 8192 - N - runasuser - application is run as a different user
;
If (\linkflags & 8192)
Debug "run as admin"
Else
Debug "run as user"
EndIf
;
CloseFile(1)
EndWith
;
; try azjio's code
;
Debug ""
GetShellLinkTargetPath(file)
;
EndProcedure
( PB6.00 LTS Win11 x64 Asrock AB350 Pro4 Ryzen 5 3600 32GB GTX1060 6GB)
( The path to enlightenment and the PureBasic Survival Guide right here... )
( The path to enlightenment and the PureBasic Survival Guide right here... )
Re: Retrieve icon (link / filename) from a .LNK file
I found several examples yesterday, including in C++ and tried to make it work on my own, but it didn't work out and I had to search more carefully in the code archives using function names and I found a ready-made example. But since you only asked for the icon, I did it only for the icon, but in my example from the Internet there were functions for other fields of the structure, if you need them:
As you can see, there are functions here:
Therefore, you do not have to explore the binary file
Here is a ready example
I think there shouldn't be a #MAX_PATH for getDescription, maybe 4096. Checked, 259 (+Null), and #MAX_PATH=260
Code: Select all
;Autor: bembulak
EnableExplicit
#STGM_READ = 0
#SLGP_SHORTPATH = 1
#SLGP_UNCPRIORITY = 2
#SLGP_RAWPATH = 4
#SLGP_RELATIVEPRIORITY = 8
Define File$ = "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\7-Zip\7-Zip File Manager.lnk"
Define psl.IShellLinkA
Define ppf.IPersistFile
Define OutSpace$
Define hres
Define Path$
Define udata.WIN32_FIND_DATA
Define Argument$
Define WorkingDirectory$
Define DESCRIPTION$
Define ShowCommand.l
Define HotKey
Define IconFile$
Define IconIndexInFile
Define rclsid.CLSID, riid.CLSID, riid2.CLSID
CLSIDFromString_("{00021401-0000-0000-C000-000000000046}", @rclsid)
IIDFromString_("{000214EE-0000-0000-C000-000000000046}", @riid)
IIDFromString_("{0000010b-0000-0000-C000-000000000046}", @riid2)
CoInitialize_(0)
; If CoCreateInstance_(?CLSID_ShellLink, 0, #CLSCTX_INPROC_SERVER, ?IID_IShellLink, @psl.IShellLinkA) = 0
If CoCreateInstance_(@rclsid, #Null, #CLSCTX_INPROC_SERVER, @riid, @psl) = #S_OK
Debug psl ; указатель не должен быть равен 0
; If psl\QueryInterface(?IID_IPersistFile, @ppf.IPersistFile) >= 0
If psl\QueryInterface(@riid2, @ppf) = #S_OK
OutSpace$ = Space(#MAX_PATH)
MultiByteToWideChar_(#CP_ACP, 0, @File$, -1, @OutSpace$, #MAX_PATH) ; в UTF-8
hres = ppf\Load(OutSpace$, #STGM_READ)
; hres = psl\Resolve(0, 0);
Debug hres
Debug OutSpace$
EndIf
Path$=Space(#MAX_PATH)
If psl\GetPath(Path$, #MAX_PATH, udata.WIN32_FIND_DATA, #SLGP_SHORTPATH)
Debug "1=" + Path$
EndIf
; Arguments for the Target
Argument$ = Space(256)
psl\GetArguments(@Argument$,Len(Argument$))
Debug "2=" + Argument$
; Working Directory
WorkingDirectory$ = Space(#MAX_PATH)
psl\GetWorkingDirectory(@WorkingDirectory$,Len(WorkingDirectory$))
Debug "3=" + WorkingDirectory$
; Description ( also used as Tooltip for the Link )
;
DESCRIPTION$ = Space(4096)
psl\GetDescription(@DESCRIPTION$,Len(DESCRIPTION$))
Debug "4=" + DESCRIPTION$
; Show command:
; SW_SHOWNORMAL = Default
; SW_SHOWMAXIMIZED = aehmm... Maximized
; SW_SHOWMINIMIZED = play Unreal Tournament
ShowCommand.l = 0
psl\GetShowCmd(@ShowCommand)
psl\GetHotkey(@HotKey)
; Set Icon for the Link:
; There can be more than 1 icons in an icon resource file,
; so you have to specify the index.
;
IconFile$ = Space(#MAX_PATH)
psl\getIconLocation(@IconFile$, Len(IconFile$),@IconIndexInFile)
Debug IconFile$
psl\Release()
ppf\Release()
EndIf
CoUninitialize_()
DataSection
CLSID_ShellLink:
; 00021401-0000-0000-C000-000000000046
Data.l $00021401
Data.w $0000,$0000
Data.b $C0,$00,$00,$00,$00,$00,$00,$46
IID_IShellLink:
; DEFINE_SHLGUID(IID_IShellLinkA, 0x000214EEL, 0, 0);
; C000-000000000046
; 000214EE-0000-0000-C000-000000000046
Data.l $000214EE
Data.w $0000,$0000
Data.b $C0,$00,$00,$00,$00,$00,$00,$46
IID_IPersistFile:
; 0000010b-0000-0000-C000-000000000046
Data.l $0000010b
Data.w $0000,$0000
Data.b $C0,$00,$00,$00,$00,$00,$00,$46
EndDataSection
Code: Select all
psl\GetPath
psl\GetArguments
psl\GetWorkingDirectory
psl\GetDescription
psl\GetShowCmd
psl\GetHotkey
psl\getIconLocation
Here is a ready example
Code: Select all
EnableExplicit
Procedure$ GetShellLinkTargetPath(shellLinkFilePath$)
Protected Path$ = Space(#MAX_PATH + 1)
Protected Argument$ = Space(#MAX_PATH + 1)
Protected WorkingDirectory$ = Space(#MAX_PATH + 1)
Protected Description$ = Space(#MAX_PATH + 1)
Protected IconFile$ = Space(#MAX_PATH + 1)
Protected IconIndexInFile.l
Protected ShowCommand.l
Protected Hotkey
Protected linkFile.IPersistFile
Protected shellLink.IShellLinkW
CoInitialize_(0)
If CoCreateInstance_(?CLSID_ShellLink, 0, 1, ?IID_IShellLink, @shellLink) = #S_OK
If shellLink\QueryInterface(?IID_IPersistFile, @linkFile) = #S_OK
If linkFile\Load(shellLinkFilePath$, 0) = #S_OK
If shellLink\Resolve(0, 1) = #S_OK
shellLink\GetPath(@Path$, #MAX_PATH, 0, 0)
shellLink\GetArguments(@Argument$, #MAX_PATH)
shellLink\GetWorkingDirectory(@WorkingDirectory$, #MAX_PATH)
shellLink\GetDescription(@Description$, #MAX_PATH)
shellLink\GetShowCmd(@ShowCommand)
shellLink\GetHotkey(@Hotkey)
shellLink\getIconLocation(@IconFile$, #MAX_PATH, @IconIndexInFile)
Debug Path$
Debug Argument$
Debug WorkingDirectory$
Debug Description$
Debug ShowCommand
Debug Hotkey
Debug IconFile$
Debug IconIndexInFile
EndIf
EndIf
LinkFile\Release()
EndIf
shellLink\Release()
EndIf
CoUninitialize_()
DataSection
CLSID_ShellLink:
; 00021401-0000-0000-C000-000000000046
Data.l $00021401
Data.w $0000,$0000
Data.b $C0,$00,$00,$00,$00,$00,$00,$46
IID_IShellLink:
CompilerIf #PB_Compiler_Unicode
; IID_IShellLinkW
; {000214F9-0000-0000-C000-000000000046}
Data.l $000214F9
Data.w $0000,$0000
Data.b $C0,$00,$00,$00,$00,$00,$00,$46
CompilerElse
; 000214EE-0000-0000-C000-000000000046
Data.l $000214EE
Data.w $0000,$0000
Data.b $C0,$00,$00,$00,$00,$00,$00,$46
CompilerEndIf
IID_IPersistFile:
; 0000010b-0000-0000-C000-000000000046
Data.l $0000010b
Data.w $0000,$0000
Data.b $C0,$00,$00,$00,$00,$00,$00,$46
EndDataSection
EndProcedure
GetShellLinkTargetPath("C:\ProgramData\Microsoft\Windows\Start Menu\Programs\7-Zip\7-Zip File Manager.lnk")
Re: Retrieve icon (link / filename) from a .LNK file
Do you only need the icon handle/image?
Code:
Code:
Code: Select all
EnableExplicit
UsePNGImageEncoder()
Procedure.i GetIcon(Path.s)
Protected fileinfo.SHFILEINFO
Protected image.i
SHGetFileInfo_(Path,#FILE_ATTRIBUTE_NORMAL,@fileinfo,SizeOf(SHFILEINFO),#SHGFI_USEFILEATTRIBUTES|#SHGFI_ICON)
If fileinfo\hIcon
image = CreateImage(#PB_Any,32,32,32)
If image
If StartDrawing(ImageOutput(image))
DrawingMode(#PB_2DDrawing_AllChannels)
DrawImage(fileinfo\hIcon,0,0,32,32)
StopDrawing()
Else
FreeImage(image)
image = #Null
EndIf
EndIf
DestroyIcon_(fileinfo\hIcon)
EndIf
ProcedureReturn image
EndProcedure
Debug GetIcon("XYZ.lnk");<- returns the icon as image
End
Re: Retrieve icon (link / filename) from a .LNK file
Code: Select all
EnableExplicit
Procedure.i GetIcon(Path.s)
Protected fileinfo.SHFILEINFO
SHGetFileInfo_(Path,#FILE_ATTRIBUTE_NORMAL,@fileinfo,SizeOf(SHFILEINFO),#SHGFI_USEFILEATTRIBUTES|#SHGFI_ICON)
ProcedureReturn fileinfo\hIcon
EndProcedure
Define himg = GetIcon( "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\7-Zip\7-Zip File Manager.lnk")
If OpenWindow(0, 0, 0, 220, 100, "Example...", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
ButtonImageGadget(1, 10, 10, 40, 40, himg)
Repeat : Until WaitWindowEvent() = #PB_Event_CloseWindow
EndIf
Re: Retrieve icon (link / filename) from a .LNK file
Originally I was only looking for the icon, but I got puzzled on the internals. The shell functions listed above work well for most .LNK files, so I stick to those. There are a few cases where they don't find the right info.
The few cases are pretty rare. Some very old .LNK files, some .LNK files created by old pre-Win10 software, and some Metro apps give either no icon, no executable, or both. Sometimes, when you 'toggle' the 'runasadmin' link in the properties of a .LNK, it drops the executable path. Then there are .LNK's with a relative path that is stripped on some occasions.
I've based my code on the stuff that you posted above, and I'm fine for now Those few rare cases are not important enough to cover, thx again!
The few cases are pretty rare. Some very old .LNK files, some .LNK files created by old pre-Win10 software, and some Metro apps give either no icon, no executable, or both. Sometimes, when you 'toggle' the 'runasadmin' link in the properties of a .LNK, it drops the executable path. Then there are .LNK's with a relative path that is stripped on some occasions.
I've based my code on the stuff that you posted above, and I'm fine for now Those few rare cases are not important enough to cover, thx again!
( PB6.00 LTS Win11 x64 Asrock AB350 Pro4 Ryzen 5 3600 32GB GTX1060 6GB)
( The path to enlightenment and the PureBasic Survival Guide right here... )
( The path to enlightenment and the PureBasic Survival Guide right here... )
Re: Retrieve icon (link / filename) from a .LNK file
What type of program does the LNK file launch? Because if it's a Win 10 UWP program, then the LNK file won't have the literal path in it.
Re: Retrieve icon (link / filename) from a .LNK file
Any and all kinds. I now use a workaround:
.LNK's that do not return a target I'll launch by calling that lnk with 'RunProgram("uwp_metroshit.lnk').
The other stuff I take apart for the different fields.
Like I said, some older software create their own funny desktop shortcuts, non NWP. But I think some of that code is WinXP, or even earlier
( PB6.00 LTS Win11 x64 Asrock AB350 Pro4 Ryzen 5 3600 32GB GTX1060 6GB)
( The path to enlightenment and the PureBasic Survival Guide right here... )
( The path to enlightenment and the PureBasic Survival Guide right here... )
Re: Retrieve icon (link / filename) from a .LNK file
blueznl
As I understand it, your function reads a shortcut. Can you make a function to create a shortcut? The fact is that using WinAPI the function of the function checks the existence of a file and icons, so when generating a couple of hundreds of shortcuts is noticeable braking.
As I understand it, your function reads a shortcut. Can you make a function to create a shortcut? The fact is that using WinAPI the function of the function checks the existence of a file and icons, so when generating a couple of hundreds of shortcuts is noticeable braking.
Re: Retrieve icon (link / filename) from a .LNK file
You mean by directly WRITING the shortcut file?
Yeah, that's actually not that hard. If you look at one of the earlier posts, I tried to deduct the .LNK format, you can simply create your own .LNK files using that format.
Yeah, that's actually not that hard. If you look at one of the earlier posts, I tried to deduct the .LNK format, you can simply create your own .LNK files using that format.
( PB6.00 LTS Win11 x64 Asrock AB350 Pro4 Ryzen 5 3600 32GB GTX1060 6GB)
( The path to enlightenment and the PureBasic Survival Guide right here... )
( The path to enlightenment and the PureBasic Survival Guide right here... )