You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
561 lines
20 KiB
561 lines
20 KiB
# -*- tcl -*- |
|
# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'dev make' or bin/punkmake to update from <pkg>-buildversion.txt |
|
# module template: punkshell/src/decktemplates/vendor/punk/modules/template_module-0.0.3.tm |
|
# |
|
# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem. |
|
# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository. |
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
# (C) 2024 |
|
# |
|
# @@ Meta Begin |
|
# Application punk::winlnk 999999.0a1.0 |
|
# Meta platform tcl |
|
# Meta license MIT |
|
# @@ Meta End |
|
|
|
|
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
# doctools header |
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
#*** !doctools |
|
#[manpage_begin punkshell_module_punk::winlnk 0 999999.0a1.0] |
|
#[copyright "2024"] |
|
#[titledesc {windows shortcut .lnk library}] [comment {-- Name section and table of contents description --}] |
|
#[moddesc {punk::winlnk}] [comment {-- Description at end of page heading --}] |
|
#[require punk::winlnk] |
|
#[keywords module shortcut lnk parse windows crossplatform] |
|
#[description] |
|
#[para] Tools for reading windows shortcuts (.lnk files) on any platform |
|
|
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
|
|
#*** !doctools |
|
#[section Overview] |
|
#[para] overview of punk::winlnk |
|
#[subsection Concepts] |
|
#[para] Windows shortcuts are a binary format file with a .lnk extension |
|
#[para] Shell Link (.LNK) Binary File Format is documented in [lb]MS_SHLLINK[rb].pdf published by Microsoft. |
|
#[para] Revision 8.0 published 2024-04-23 |
|
|
|
|
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
## Requirements |
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
|
|
#*** !doctools |
|
#[subsection dependencies] |
|
#[para] packages used by punk::winlnk |
|
#[list_begin itemized] |
|
|
|
package require Tcl 8.6- |
|
#*** !doctools |
|
#[item] [package {Tcl 8.6}] |
|
|
|
#TODO - logger |
|
|
|
#*** !doctools |
|
#[list_end] |
|
|
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
|
|
#*** !doctools |
|
#[section API] |
|
|
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
# oo::class namespace |
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
#tcl::namespace::eval punk::winlnk::class { |
|
#*** !doctools |
|
#[subsection {Namespace punk::winlnk::class}] |
|
#[para] class definitions |
|
#if {[tcl::info::commands [tcl::namespace::current]::interface_sample1] eq ""} { |
|
#*** !doctools |
|
#[list_begin enumerated] |
|
|
|
# oo::class create interface_sample1 { |
|
# #*** !doctools |
|
# #[enum] CLASS [class interface_sample1] |
|
# #[list_begin definitions] |
|
|
|
# method test {arg1} { |
|
# #*** !doctools |
|
# #[call class::interface_sample1 [method test] [arg arg1]] |
|
# #[para] test method |
|
# puts "test: $arg1" |
|
# } |
|
|
|
# #*** !doctools |
|
# #[list_end] [comment {-- end definitions interface_sample1}] |
|
# } |
|
|
|
#*** !doctools |
|
#[list_end] [comment {--- end class enumeration ---}] |
|
#} |
|
#} |
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
|
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
# Base namespace |
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
tcl::namespace::eval punk::winlnk { |
|
tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase |
|
#variable xyz |
|
|
|
#*** !doctools |
|
#[subsection {Namespace punk::winlnk}] |
|
#[para] Core API functions for punk::winlnk |
|
#[list_begin definitions] |
|
|
|
|
|
variable magic_HeaderSize "0000004C" ;#HeaderSize MUST equal this |
|
variable magic_LinkCLSID "00021401-0000-0000-C000-000000000046" ;#LinkCLSID MUST equal this |
|
|
|
proc Get_contents {path {bytes all}} { |
|
if {![file exists $path] || [file type $path] ne "file"} { |
|
error "punk::winlnk::get_contents cannot find a filesystem object of type 'file' at location: $path" |
|
} |
|
set fd [open $path r] |
|
chan configure $fd -translation binary -encoding iso8859-1 |
|
if {$bytes eq "all"} { |
|
set data [read $fd] |
|
} else { |
|
set data [read $fd $bytes] |
|
} |
|
close $fd |
|
return $data |
|
} |
|
proc Get_HeaderSize {contents} { |
|
set 4bytes [split [string range $contents 0 3] ""] |
|
set hex4 "" |
|
foreach b [lreverse $4bytes] { |
|
set dec [scan $b %c] ;# 0-255 decimal |
|
set HH [format %2.2llX $dec] |
|
append hex4 $HH |
|
} |
|
return $hex4 |
|
} |
|
proc Get_LinkCLSID {contents} { |
|
set 16bytes [string range $contents 4 19] |
|
#CLSID hex textual representation is split as 4-2-2-2-6 bytes(hex pairs) |
|
#e.g We expect 00021401-0000-0000-C000-000000000046 for .lnk files |
|
#for endianness - it is little endian all the way but the split is 4-2-2-1-1-1-1-1-1-1-1 REVIEW |
|
#(so it can appear as mixed endianness if you don't know the splits) |
|
#https://devblogs.microsoft.com/oldnewthing/20220928-00/?p=107221 |
|
#This is based on COM textual representation of GUIDS |
|
#Apparently a CLSID is a GUID that identifies a COM object |
|
set clsid "" |
|
set s1 [tcl::string::range $16bytes 0 3] |
|
set declist [scan [string reverse $s1] %c%c%c%c] |
|
set fmt "%02X%02X%02X%02X" |
|
append clsid [format $fmt {*}$declist] |
|
|
|
append clsid - |
|
set s2 [tcl::string::range $16bytes 4 5] |
|
set declist [scan [string reverse $s2] %c%c] |
|
set fmt "%02X%02X" |
|
append clsid [format $fmt {*}$declist] |
|
|
|
append clsid - |
|
set s3 [tcl::string::range $16bytes 6 7] |
|
set declist [scan [string reverse $s3] %c%c] |
|
append clsid [format $fmt {*}$declist] |
|
|
|
append clsid - |
|
#now treat bytes individually - so no endianness conversion |
|
set declist [scan [tcl::string::range $16bytes 8 9] %c%c] |
|
append clsid [format $fmt {*}$declist] |
|
|
|
append clsid - |
|
set scan [string repeat %c 6] |
|
set fmt [string repeat %02X 6] |
|
set declist [scan [tcl::string::range $16bytes 10 15] $scan] |
|
append clsid [format $fmt {*}$declist] |
|
|
|
return $clsid |
|
} |
|
proc Contents_check_header {contents} { |
|
variable magic_HeaderSize |
|
variable magic_LinkCLSID |
|
expr {[Get_HeaderSize $contents] eq $magic_HeaderSize && [Get_LinkCLSID $contents] eq $magic_LinkCLSID} |
|
} |
|
|
|
#LinkFlags - 4 bytes - specifies information about the shell link and the presence of optional portions of the structure. |
|
proc Show_LinkFlags {contents} { |
|
set 4bytes [string range $contents 20 23] |
|
set r [binary scan $4bytes i val] ;# i for little endian 32-bit signed int |
|
puts "val: $val" |
|
set declist [scan [string reverse $4bytes] %c%c%c%c] |
|
set fmt [string repeat %08b 4] |
|
puts "LinkFlags:[format $fmt {*}$declist]" |
|
|
|
set r [binary scan $4bytes b32 val] |
|
puts "bscan-le: $val" |
|
set r [binary scan [string reverse $4bytes] b32 val] |
|
puts "bscan-2 : $val" |
|
} |
|
proc Get_LinkFlags {contents} { |
|
set 4bytes [string range $contents 20 23] |
|
set r [binary scan $4bytes i val] ;# i for little endian 32-bit signed int |
|
return $val |
|
} |
|
variable LinkFlags |
|
set LinkFlags [dict create\ |
|
hasLinkTargetIDList 1\ |
|
HasLinkInfo 2\ |
|
HasName 4\ |
|
HasRelativePath 8\ |
|
HasWorkingDir 16\ |
|
HasArguments 32\ |
|
HasIconLocation 64\ |
|
IsUnicode 128\ |
|
ForceNoLinkInfo 256\ |
|
HasExpString 512\ |
|
RunInSeparateProcess 1024\ |
|
Unused1 2048\ |
|
HasDarwinID 4096\ |
|
RunAsUser 8192\ |
|
HasExpIcon 16394\ |
|
NoPidlAlias 32768\ |
|
Unused2 65536\ |
|
RunWithShimLayer 131072\ |
|
ForceNoLinkTrack 262144\ |
|
EnableTargetMetadata 524288\ |
|
DisableLinkPathTracking 1048576\ |
|
DisableKnownFolderTracking 2097152\ |
|
DisableKnownFolderAlias 4194304\ |
|
AllowLinkToLink 8388608\ |
|
UnaliasOnSave 16777216\ |
|
PreferEnvironmentPath 33554432\ |
|
KeepLocalIDListForUNCTarget 67108864\ |
|
] |
|
variable LinkFlagLetters [list A B C D E F G H I J K L M N O P Q R S T U V W X Y Z AA] |
|
proc Has_LinkFlag {contents flagname} { |
|
variable LinkFlags |
|
variable LinkFlagLetters |
|
if {[string length $flagname] <= 2} { |
|
set idx [lsearch $LinkFlagLetters $flagname] |
|
if {$idx < 0} { |
|
error "punk::winlnk::Has_LinkFlag error - flagname $flagname not known" |
|
} |
|
set binflag [expr {2**$idx}] |
|
set allflags [Get_LinkFlags $contents] |
|
return [expr {$allflags & $binflag}] |
|
} |
|
if {[dict exists $LinkFlags $flagname]} { |
|
set binflag [dict get $LinkFlags $flagname] |
|
set allflags [Get_LinkFlags $contents] |
|
return [expr {$allflags & $binflag}] |
|
} else { |
|
error "punk::winlnk::Has_LinkFlag error - flagname $flagname not known" |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
#offset 24 4 bytes |
|
#File attribute flags |
|
|
|
#offset 28 8 bytes |
|
#creation date and time |
|
|
|
#offset 36 8 bytes |
|
#last access date and time |
|
|
|
#offset 44 8 bytes |
|
#last modification date and time |
|
|
|
#offset 52 4 bytes - unsigned int |
|
#file size in bytes (of target) |
|
proc Get_FileSize {contents} { |
|
set 4bytes [string range $contents 52 55] |
|
set r [binary scan $4bytes i val] |
|
return $val |
|
} |
|
|
|
#offset 56 4 bytes signed integer |
|
#icon index value |
|
|
|
#offset 60 4 bytes - unsigned integer |
|
#SW_SHOWNORMAL 0x00000001 |
|
#SW_SHOWMAXIMIZED 0x00000001 |
|
#SW_SHOWMINNOACTIVE 0x00000007 |
|
# - all other values MUST be treated as SW_SHOWNORMAL |
|
proc Get_ShowCommand {contents} { |
|
set 4bytes [string range $contents 60 63] |
|
set r [binary scan $4bytes i val] |
|
return $val |
|
} |
|
|
|
#offset 64 Bytes 2 |
|
#Hot key |
|
|
|
#offset 66 2 bytes - reserved |
|
|
|
#offset 68 4 bytes - reserved |
|
|
|
#offset 72 4 bytes - reserved |
|
|
|
#next 76 |
|
|
|
proc Get_LinkTargetIDList_size {contents} { |
|
if {[Has_LinkFlag $contents "A"]} { |
|
set 2bytes [string range $contents 76 77] |
|
set r [binary scan $2bytes s val] ;#short |
|
#logger |
|
#puts stderr "LinkTargetIDList_size: $val" |
|
return $val |
|
} else { |
|
return 0 |
|
} |
|
} |
|
proc Get_LinkInfo_content {contents} { |
|
set idlist_size [Get_LinkTargetIDList_size $contents] |
|
if {$idlist_size == 0} { |
|
set offset 0 |
|
} else { |
|
set offset [expr {2 + $idlist_size}] ;#LinkTargetIdList IDListSize field + value |
|
} |
|
set linkinfo_start [expr {76 + $offset}] |
|
if {[Has_LinkFlag $contents B]} { |
|
#puts stderr "linkinfo_start: $linkinfo_start" |
|
set 4bytes [string range $contents $linkinfo_start $linkinfo_start+3] |
|
binary scan $4bytes i val ;#size *including* these 4 bytes |
|
set linkinfo_content [string range $contents $linkinfo_start [expr {$linkinfo_start + $val -1}]] |
|
return [dict create linkinfo_start $linkinfo_start size $val next_start [expr {$linkinfo_start + $val}] content $linkinfo_content] |
|
} else { |
|
return [dict create linkinfo_start $linkinfo_start size 0 next_start $linkinfo_start content ""] |
|
} |
|
} |
|
|
|
proc LinkInfo_get_fields {linkinfocontent} { |
|
set 4bytes [string range $linkinfocontent 0 3] |
|
binary scan $4bytes i val ;#size *including* these 4 bytes |
|
set bytes_linkinfoheadersize [string range $linkinfocontent 4 7] |
|
set bytes_linkinfoflags [string range $linkinfocontent 8 11] |
|
set r [binary scan $4bytes i flags] ;# i for little endian 32-bit signed int |
|
#puts "linkinfoflags: $flags" |
|
|
|
set localbasepath "" |
|
set commonpathsuffix "" |
|
|
|
#REVIEW - flags problem? |
|
if {$flags & 1} { |
|
#VolumeIDAndLocalBasePath |
|
#logger |
|
#puts stderr "VolumeIDAndLocalBasePath" |
|
} |
|
if {$flags & 2} { |
|
#logger |
|
#puts stderr "CommonNetworkRelativeLinkAndPathSuffix" |
|
} |
|
set bytes_volumeid_offset [string range $linkinfocontent 12 15] |
|
set bytes_localbasepath_offset [string range $linkinfocontent 16 19] ;# a |
|
set bytes_commonnetworkrelativelinkoffset [string range $linkinfocontent 20 23] |
|
set bytes_commonpathsuffix_offset [string range $linkinfocontent 24 27] ;# a |
|
|
|
binary scan $bytes_localbasepath_offset i bp_offset |
|
if {$bp_offset > 0} { |
|
set tail [string range $linkinfocontent $bp_offset end] |
|
set stringterminator 0 |
|
set i 0 |
|
set localbasepath "" |
|
#TODO |
|
while {!$stringterminator & $i < 100} { |
|
set c [string index $tail $i] |
|
if {$c eq "\x00"} { |
|
set stringterminator 1 |
|
} else { |
|
append localbasepath $c |
|
} |
|
incr i |
|
} |
|
} |
|
binary scan $bytes_commonpathsuffix_offset i cps_offset |
|
if {$cps_offset > 0} { |
|
set tail [string range $linkinfocontent $cps_offset end] |
|
set stringterminator 0 |
|
set i 0 |
|
set commonpathsuffix "" |
|
#TODO |
|
while {!$stringterminator && $i < 100} { |
|
set c [string index $tail $i] |
|
if {$c eq "\x00"} { |
|
set stringterminator 1 |
|
} else { |
|
append commonpathsuffix $c |
|
} |
|
incr i |
|
} |
|
} |
|
|
|
|
|
return [dict create localbasepath $localbasepath commonpathsuffix $commonpathsuffix] |
|
} |
|
|
|
proc contents_get_info {contents} { |
|
|
|
#todo - return something like the perl lnk-parse-1.0.pl script? |
|
|
|
#Link File: C:/repo/jn/tclmodules/tomlish/src/modules/test/#modpod-tomlish-999999.0a1.0/suites/all/arrays_1.toml#roundtrip+roundtrip_files+arrays_1.toml.fauxlink.lnk |
|
#Link Flags: HAS SHELLIDLIST | POINTS TO FILE/DIR | NO DESCRIPTION | HAS RELATIVE PATH STRING | HAS WORKING DIRECTORY | NO CMD LINE ARGS | NO CUSTOM ICON | |
|
#File Attributes: ARCHIVE |
|
#Create Time: Sun Jul 14 2024 10:41:34 |
|
#Last Accessed time: Sat Sept 21 2024 02:46:10 |
|
#Last Modified Time: Tue Sept 10 2024 17:16:07 |
|
#Target Length: 479 |
|
#Icon Index: 0 |
|
#ShowWnd: 1 SW_NORMAL |
|
#HotKey: 0 |
|
#(App Path:) Remaining Path: repo\jn\tclmodules\tomlish\src\modules\test\#modpod-tomlish-999999.0a1.0\suites\roundtrip\roundtrip_files\arrays_1.toml |
|
#Relative Path: ..\roundtrip\roundtrip_files\arrays_1.toml |
|
#Working Dir: C:\repo\jn\tclmodules\tomlish\src\modules\test\#modpod-tomlish-999999.0a1.0\suites\roundtrip\roundtrip_files |
|
|
|
variable LinkFlags |
|
set flags_enabled [list] |
|
dict for {k v} $LinkFlags { |
|
if {[Has_LinkFlag $contents $k] > 0} { |
|
lappend flags_enabled $k |
|
} |
|
} |
|
|
|
set showcommand_val [Get_ShowCommand $contents] |
|
switch -- $showcommand_val { |
|
1 { |
|
set showwnd [list 1 SW_SHOWNORMAL] |
|
} |
|
3 { |
|
set showwnd [list 3 SW_SHOWMAXIMIZED] |
|
} |
|
7 { |
|
set showwnd [list 7 SW_SHOWMINNOACTIVE] |
|
} |
|
default { |
|
set showwnd [list $showcommand_val SW_SHOWNORMAL-effective] |
|
} |
|
} |
|
|
|
set linkinfo_content_dict [Get_LinkInfo_content $contents] |
|
set localbase_path "" |
|
set suffix_path "" |
|
set linkinfocontent [dict get $linkinfo_content_dict content] |
|
set link_target "" |
|
if {$linkinfocontent ne ""} { |
|
set linkfields [LinkInfo_get_fields $linkinfocontent] |
|
set localbase_path [dict get $linkfields localbasepath] |
|
set suffix_path [dict get $linkfields commonpathsuffix] |
|
set link_target [file join $localbase_path $suffix_path] |
|
} |
|
|
|
set result [dict create\ |
|
link_target $link_target\ |
|
link_flags $flags_enabled\ |
|
file_attributes "<unimplemented>"\ |
|
create_time "<unimplemented>"\ |
|
last_accessed_time "<unimplemented"\ |
|
last_modified_time "<unimplementd>"\ |
|
target_length [Get_FileSize $contents]\ |
|
icon_index "<unimplemented>"\ |
|
showwnd "$showwnd"\ |
|
hotkey "<unimplemented>"\ |
|
relative_path "?"\ |
|
] |
|
} |
|
|
|
proc file_check_header {path} { |
|
#*** !doctools |
|
#[call [fun file_check_header] [arg path] ] |
|
#[para]Return 0|1 |
|
#[para]Determines if the .lnk file specified in path has a valid header for a windows shortcut |
|
set c [Get_contents $path 20] |
|
return [Contents_check_header $c] |
|
} |
|
proc file_get_info {path} { |
|
#*** !doctools |
|
#[call [fun file_get_info] [arg path] ] |
|
#[para] Return a dict of info obtained by parsing the binary data in a windows .lnk file |
|
#[para] If the .lnk header check fails, then the .lnk file probably isn't really a shortcut file and the dictionary will contain an 'error' key |
|
set c [Get_contents $path] |
|
if {[Contents_check_header $c]} { |
|
return [contents_get_info $c] |
|
} else { |
|
return [dict create error "lnk_header_check_failed"] |
|
} |
|
} |
|
proc file_show_info {path} { |
|
package require punk::lib |
|
punk::lib::showdict [file_get_info $path] * |
|
} |
|
|
|
#proc sample1 {p1 n args} { |
|
# #*** !doctools |
|
# #[call [fun sample1] [arg p1] [arg n] [opt {option value...}]] |
|
# #[para]Description of sample1 |
|
# #[para] Arguments: |
|
# # [list_begin arguments] |
|
# # [arg_def tring p1] A description of string argument p1. |
|
# # [arg_def integer n] A description of integer argument n. |
|
# # [list_end] |
|
# return "ok" |
|
#} |
|
|
|
|
|
|
|
|
|
#*** !doctools |
|
#[list_end] [comment {--- end definitions namespace punk::winlnk ---}] |
|
} |
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
|
|
|
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
# Secondary API namespace |
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
tcl::namespace::eval punk::winlnk::lib { |
|
tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase |
|
tcl::namespace::path [tcl::namespace::parent] |
|
#*** !doctools |
|
#[subsection {Namespace punk::winlnk::lib}] |
|
#[para] Secondary functions that are part of the API |
|
#[list_begin definitions] |
|
|
|
#proc utility1 {p1 args} { |
|
# #*** !doctools |
|
# #[call lib::[fun utility1] [arg p1] [opt {?option value...?}]] |
|
# #[para]Description of utility1 |
|
# return 1 |
|
#} |
|
|
|
|
|
|
|
#*** !doctools |
|
#[list_end] [comment {--- end definitions namespace punk::winlnk::lib ---}] |
|
} |
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
|
|
|
|
|
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
#*** !doctools |
|
#[section Internal] |
|
#tcl::namespace::eval punk::winlnk::system { |
|
#*** !doctools |
|
#[subsection {Namespace punk::winlnk::system}] |
|
#[para] Internal functions that are not part of the API |
|
|
|
|
|
|
|
#} |
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
## Ready |
|
package provide punk::winlnk [tcl::namespace::eval punk::winlnk { |
|
variable pkg punk::winlnk |
|
variable version |
|
set version 999999.0a1.0 |
|
}] |
|
return |
|
|
|
#*** !doctools |
|
#[manpage_end] |
|
|
|
|