Browse Source

fauxlink package, misc fixes

master
Julian Noble 3 months ago
parent
commit
779ee8bc4a
  1. 468
      src/bootsupport/modules/fauxlink-0.1.0.tm
  2. 1
      src/bootsupport/modules/include_modules.config
  3. 1
      src/bootsupport/modules/punk/du-0.1.0.tm
  4. 45
      src/bootsupport/modules/punk/lib-0.1.1.tm
  5. 8
      src/bootsupport/modules/punk/mix/cli-0.3.1.tm
  6. 1
      src/modules/punk/du-999999.0a1.0.tm
  7. 45
      src/modules/punk/lib-999999.0a1.0.tm
  8. 8
      src/modules/punk/mix/cli-999999.0a1.0.tm
  9. 116
      src/modules/punk/nav/fs-999999.0a1.0.tm
  10. 6
      src/modules/punk/winlnk-999999.0a1.0.tm
  11. 468
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/fauxlink-0.1.0.tm
  12. 1
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/include_modules.config
  13. 1
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/du-0.1.0.tm
  14. 45
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/lib-0.1.1.tm
  15. 8
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/cli-0.3.1.tm
  16. 468
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/fauxlink-0.1.0.tm
  17. 1
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/include_modules.config
  18. 1
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/du-0.1.0.tm
  19. 45
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/lib-0.1.1.tm
  20. 8
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/cli-0.3.1.tm
  21. 164
      src/vendormodules/fauxlink-0.1.0.tm
  22. 468
      src/vfs/_vfscommon/modules/fauxlink-0.1.0.tm
  23. 1
      src/vfs/_vfscommon/modules/punk/du-0.1.0.tm
  24. 45
      src/vfs/_vfscommon/modules/punk/lib-0.1.1.tm
  25. 8
      src/vfs/_vfscommon/modules/punk/mix/cli-0.3.1.tm
  26. 6
      src/vfs/_vfscommon/modules/punk/mix/commandset/project-0.1.0.tm
  27. 116
      src/vfs/_vfscommon/modules/punk/nav/fs-0.1.0.tm
  28. 68
      src/vfs/_vfscommon/modules/punk/ns-0.1.0.tm
  29. 561
      src/vfs/_vfscommon/modules/punk/winlnk-0.1.0.tm
  30. BIN
      src/vfs/_vfscommon/modules/test/tomlish-1.1.1.tm
  31. 717
      src/vfs/_vfscommon/modules/tomlish-1.1.1.tm

468
src/bootsupport/modules/fauxlink-0.1.0.tm

@ -0,0 +1,468 @@
# -*- tcl -*-
# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from <pkg>-buildversion.txt
#
# 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 fauxlink 0.1.0
# Meta platform tcl
# Meta license MIT
# @@ Meta End
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# doctools header
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[manpage_begin fauxlink_module_fauxlink 0 0.1.0]
#[copyright "2024"]
#[titledesc {faux link application shortcuts}] [comment {-- Name section and table of contents description --}]
#[moddesc {fauxlink .fxlnk}] [comment {-- Description at end of page heading --}]
#[require fauxlink]
#[keywords symlink faux fake shortcut toml]
#[description]
#[para] A cross platform shortcut/symlink alternative.
#[para] Unapologetically ugly - but practical in certain circumstances.
#[para] A solution is required for application-driven filesystem links that survives cross platform moves as well as
#[para] archiving and packaging systems.
#[para] The target is specified in a minimally-encoded form in the filename itself - but still human readable.
#[para] format of name <nominalname>#<encodedtarget>.fxlnk
#[para] where <nominalname> can be empty - then the effective nominal name is the tail of the <encodedtarget>
#[para] The + symbol substitutes for forward-slashes.
#[para] Other chars can be encoded using url-like encoding - (but only up to %7E !)
#[para] We deliberately treat higher % sequences literally.
#[para] This means actual uri::urn encoded unicode sequences (e.g %E2%99%A5 [heart]) can remain literal for linking to urls.
#[para] e.g if an actual + or # is required in a filename or path segment they can be encoded as %2B & %23
#[para] e.g a link to a file file#A.txt in parent dir could be:
#[para] file%23A.txt#..+file%23A.txt.fxlnk
#[para] or equivalently (but obviously affecting sorting) #..+file%23A.txt.fxlnk
#[para] The <nominalname> can be unrelated to the actual target
#[para] e.g datafile.dat#..+file%23A.txt.fxlnk
#[para] This system has no filesystem support - and must be completely application driven.
#[para] This can be useful for example in application test packages which may be tarred or zipped and moved cross platform.
#[para] The target being fully specified in the name means the file doesn't have to be read for the target to be determined
#[para] Extensions to behaviour should be added in the file as text data in Toml format,
#[para] with custom data being under a single application-chosen table name
#[para] The toplevel Toml table [fauxlink] is reserved for core extensions to this system.
#[para] Aside from the 2 used for delimiting (+ #)
#[para] certain characters which might normally be allowed in filesystems are required to be encoded
#[para] e.g space and tab are required to be %20 %09
#[para] Others that require encoding are: * ? \ / | : ; " < >
#[para] The nul character in raw form, when detected, is always mapped away to the empty string - as very few filesystems support it.
#[para] Control characters and other punctuation is optional to encode.
#[para] Generally utf-8 should be used where possible and unicode characters left as is where possible on modern systems.
#[para] Where encoding of unicode is desired in the nominalname or encodedtarget portions it can be specified as %UXXXXXXXX
#[para] There must be between 1 and 8 X digits following the %U. Interpretation of chars following %U stops at the first non-hex character.
#[para] This means %Utest would not get any translation as there were no hex digits so it would come out as %Utest
#
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#https://learn.microsoft.com/en-us/troubleshoot/windows-client/networking/url-encoding-unc-paths-not-url-decoded
# ie "//server/c/Program files" works but "//server/c/Program%20Files" is now treated by windows as a literal path with %20 in it.
#Using fauxlink - a link would be:
# "my-program-files#++server+c+Program%20Files.fxlnk"
#If we needed the old-style literal %20 it would become
# "my-program-files#++server+c+Program%2520Files.fxlnk"
#*** !doctools
#[section Overview]
#[para] overview of fauxlink
#[subsection Concepts]
#[para] -
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Requirements
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[subsection dependencies]
#[para] packages used by fauxlink
#[list_begin itemized]
package require Tcl 8.6-
#*** !doctools
#[item] [package {Tcl 8.6-}]
# #package require frobz
# #*** !doctools
# #[item] [package {frobz}]
#*** !doctools
#[list_end]
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[section API]
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# oo::class namespace
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
namespace eval fauxlink::class {
#*** !doctools
#[subsection {Namespace fauxlink::class}]
#[para] class definitions
if {[info commands [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
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
namespace eval fauxlink {
namespace export {[a-z]*}; # Convention: export all lowercase
#todo - enforce utf-8
#literal unicode chars supported by modern filesystems - leave as is - REVIEW
variable encode_map
variable decode_map
#most filesystems don't allow NULL - map to empty string
#Make sure % is not in encode_map
set encode_map [dict create\
\x00 ""\
{ } %20\
\t %09\
+ %2B\
# %23\
* %2A\
? %3F\
\\ %5C\
/ %2F\
| %7C\
: %3A\
{;} %3B\
{"} %22\
< %3C\
> %3E\
]
#above have some overlap with ctrl codes below.
#no big deal as it's a dict
#must_encode
# + # * ? \ / | : ; " < > <sp> \t
# also NUL to empty string
# also ctrl chars 01 to 1F (1..31)
for {set i 1} {$i < 32} {incr i} {
set ch [format %c $i]
set enc "%[format %02X $i]"
set enc_lower [string tolower $enc]
dict set encode_map $ch $enc
dict set decode_map $enc $ch
dict set decode_map $enc_lower $ch
}
variable must_encode
set must_encode [dict keys $encode_map]
#if they are in
#decode map doesn't include
# %00 (nul)
# %2F "/"
# %2f "/"
# %7f (del)
#we exlude the forward slash because we already have + for that - and multiple ways to specify it obscure intention.
#
set decode_map [dict merge $decode_map [dict create\
%09 \t\
%20 { }\
%21 "!"\
%22 {"}\
%23 "#"\
%24 "$"\
%25 "%"\
%26 "&"\
%27 "'"\
%28 "("\
%29 ")"\
%2A "*"\
%2a "*"\
%2B "+"\
%2b "+"\
%2C ","\
%2c ","\
%2D "-"\
%2d "-"\
%2E "."\
%2e "."\
%3A ":"\
%3a ":"\
%3B {;}\
%3b {;}\
%3D "="\
%3C "<"\
%3c "<"\
%3d "="\
%3E ">"\
%3e ">"\
%3F "?"\
%3f "?"\
%40 "@"\
%5B "\["\
%5b "\["\
%5C "\\"\
%5c "\\"\
%5D "\]"\
%5d "\]"\
%5E "^"\
%5e "^"\
%60 "`"\
%7B "{"\
%7b "{"\
%7C "|"\
%7c "|"\
%7D "}"\
%7d "}"\
%7E "~"\
%7e "~"\
]]
#Don't go above 7f
#if we want to specify p
#*** !doctools
#[subsection {Namespace fauxlink}]
#[para] Core API functions for fauxlink
#[list_begin definitions]
proc resolve {link} {
variable decode_map
variable encode_map
variable must_encode
set ftail [file tail $link]
if {[file extension $ftail] ni [list .fxlnk .fauxlink]} {
error "fauxlink::resolve refusing to process link $link - file extension must be .fxlnk or .fauxlink"
}
set linkspec [file rootname $ftail]
# - any # or + within the target path or name should have been uri encoded as %23 and %2b
if {[tcl::string::first # $linkspec] < 0} {
error "fauxlink::resolve error. Link must contain a # (usually at start if name matches target)"
}
#only the 1st 2 parts of split on # are significant.
#if there are more # chars present - the subsequent parts are effectively a comment
#check namepec already has required chars encoded
lassign [split $linkspec #] namespec targetspec
#puts stderr "-->namespec $namespec"
set nametest [tcl::string::map $encode_map $namespec]
#puts stderr "-->nametest $nametest"
#nothing should be changed - if there are unencoded chars that must be encoded it is an error
if {[tcl::string::length $nametest] ne [tcl::string::length $namespec]} {
set err "fauxlink::resolve invalid chars in name part (section prior to first #)"
set idx 0
foreach ch [split $namespec ""] {
if {$ch in $must_encode} {
set enc [dict get $encode_map $ch]
if {[dict exists $decode_map $enc]} {
append err " char $idx should be encoded as $enc" \n
} else {
append err " no %xx encoding available. Use %UXX if really required" \n
}
}
incr idx
}
error $err
}
#see comments below regarding 2 rounds and ordering.
set name [decode_unicode_escapes $namespec]
set name [tcl::string::map $decode_map $name]
#puts stderr "-->name: $name"
set targetsegment [split $targetspec +]
#check each + delimited part of targetspec already has required chars encoded
set s 0 ;#segment index
set result_segments [list]
foreach segment $targetsegment {
set targettest [tcl::string::map $encode_map $segment]
if {[tcl::string::length $targettest] ne [tcl::string::length $segment]} {
set err "fauxlink::resolve invalid chars in targetpath (section following first #)"
set idx 0
foreach ch [split $segment ""] {
if {$ch in $must_encode} {
set enc [dict get $encode_map $ch]
if {[dict exists $decode_map $enc]} {
append err " segment $s char $idx should be encoded as $enc" \n
} else {
append err " no %xx encoding available. Use %UXX if really required" \n
}
}
incr idx
}
error $err
}
#2 rounds of substitution is possibly asking for trouble..
#We allow anything in the resultant segments anyway (as %UXXXX... allows all)
#so it's not so much about what can be encoded,
# - but it makes it harder to reason about for users
# In particular - if we map %XX first it makes %25 -> % substitution tricky
# if the user requires a literal %UXXX - they can't do %25UXXX
# the double sub would make it %UXXX -> somechar anyway.
#we do unicode first - as a 2nd round of %XX substitutions is unlikely to interfere.
#There is still the opportunity to use things like %U00000025 followed by hex-chars
# and get some minor surprises, but using %U on ascii is unlikely to be done accidentally - REVIEW
set segment [decode_unicode_escapes $segment]
set segment [tcl::string::map $decode_map $segment]
lappend result_segments $segment
incr s
}
set targetpath [join $result_segments /]
if {$name eq ""} {
set name [lindex $result_segments end]
}
return [dict create name $name targetpath $targetpath]
}
variable map
#default exclusion of / (%U2f and equivs)
#this would allow obfuscation of intention - when we have + for that anyway
proc decode_unicode_escapes {str {exclusions {/ \n \r \x00}}} {
variable map
set ucstart [string first %U $str 0]
if {$ucstart < 0} {
return $str
}
set max 8
set map [list]
set strend [expr {[string length $str]-1}]
while {$ucstart >= 0} {
set s $ucstart
set i [expr {$s +2}] ;#skip the %U
set hex ""
while {[tcl::string::length $hex] < 8 && $i <= $strend} {
set in [string index $str $i]
if {[tcl::string::is xdigit -strict $in]} {
append hex $in
} else {
break
}
incr i
}
if {$hex ne ""} {
incr i -1
lappend map $s $i $hex
}
set ucstart [tcl::string::first %U $str $i]
}
set out ""
set lastidx -1
set e 0
foreach {s e hex} $map {
append out [string range $str $lastidx+1 $s-1]
set sub [format %c 0x$hex]
if {$sub in $exclusions} {
append out %U$hex ;#put it back
} else {
append out $sub
}
set lastidx $e
}
if {$e < [tcl::string::length $str]-1} {
append out [string range $str $e+1 end]
}
return $out
}
proc link_as {name target} {
}
#proc sample1 {p1 args} {
# #*** !doctools
# #[call [fun sample1] [arg p1] [opt {?option value...?}]]
# #[para]Description of sample1
# return "ok"
#}
#*** !doctools
#[list_end] [comment {--- end definitions namespace fauxlink ---}]
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# Secondary API namespace
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
namespace eval fauxlink::lib {
namespace export {[a-z]*}; # Convention: export all lowercase
namespace path [namespace parent]
#*** !doctools
#[subsection {Namespace fauxlink::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 fauxlink::lib ---}]
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[section Internal]
namespace eval fauxlink::system {
#*** !doctools
#[subsection {Namespace fauxlink::system}]
#[para] Internal functions that are not part of the API
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Ready
package provide fauxlink [namespace eval fauxlink {
variable pkg fauxlink
variable version
set version 0.1.0
}]
return
#*** !doctools
#[manpage_end]

1
src/bootsupport/modules/include_modules.config

@ -7,6 +7,7 @@ set bootsupport_modules [list\
src/vendormodules cksum\ src/vendormodules cksum\
src/vendormodules modpod\ src/vendormodules modpod\
src/vendormodules overtype\ src/vendormodules overtype\
src/vendormodules fauxlink\
src/vendormodules oolib\ src/vendormodules oolib\
src/vendormodules http\ src/vendormodules http\
src/vendormodules dictutils\ src/vendormodules dictutils\

1
src/bootsupport/modules/punk/du-0.1.0.tm

@ -942,6 +942,7 @@ namespace eval punk::du {
#struct::set difference removes duplicates (but not always.. e.g if using tcl impl and 2nd element empty!) #struct::set difference removes duplicates (but not always.. e.g if using tcl impl and 2nd element empty!)
#relying on struct::set to remove dupes is somewhat risky. It is not well documented - and behaviour of dupes in inputs is underspecified as it appears to be intended for mathematical 'sets' #relying on struct::set to remove dupes is somewhat risky. It is not well documented - and behaviour of dupes in inputs is underspecified as it appears to be intended for mathematical 'sets'
#remove links and . .. from directories, remove links from files #remove links and . .. from directories, remove links from files
#ideally we would like to classify links by whether they point to files vs dirs - but there are enough cross-platform differences that we will have to leave it to the caller to sort out for now.
#struct::set will affect order: tcl vs critcl give different ordering! #struct::set will affect order: tcl vs critcl give different ordering!
set files [struct::set difference [concat $hfiles $files[unset files]] $links] set files [struct::set difference [concat $hfiles $files[unset files]] $links]
set dirs [struct::set difference [concat $hdirs $dirs[unset dirs]] [concat $links [list [file join $folderpath .] [file join $folderpath ..] ]]] set dirs [struct::set difference [concat $hdirs $dirs[unset dirs]] [concat $links [list [file join $folderpath .] [file join $folderpath ..] ]]]

45
src/bootsupport/modules/punk/lib-0.1.1.tm

@ -372,20 +372,40 @@ namespace eval punk::lib {
proc lswap {lvar a z} { proc lswap {lvar a z} {
upvar $lvar l upvar $lvar l
if {[lindex_resolve_basic $l $a] < 0 || [lindex_resolve_basic $l $z] < 0} { if {[lindex_resolve_basic $l $a] < 0 || [lindex_resolve_basic $l $z] < 0} {
#if we didn't do this check - we could raise an error on second lset - leaving list corrupted because only one lset occurred #lindex_resolve_basic returns only -1 if out of range
#if we didn't do this check - we could raise an error on second lset below - leaving list corrupted because only one lset occurred
#(e.g using: lswap mylist end-2 end on a two element list) #(e.g using: lswap mylist end-2 end on a two element list)
#on the unhapy path we can take time to check the nature of the out-of-boundness to give a nicer report #on the unhapy path we can take time to check the nature of the out-of-boundness to give a nicer report
#use full 'lindex_resolve' which can report which side via -3 and -2 special results being lower and upper bound breaches respectively (-1 never returned)
set a_index [lindex_resolve $l $a] set a_index [lindex_resolve $l $a]
set a_msg "" set a_msg ""
switch -- $a_index { switch -- $a_index {
-2 { -2 {
"$a is greater th set a_msg "1st supplied index $a is above the upper bound for the list ([llength $l])"
} }
-3 { -3 {
set a_msg "1st supplied index $a is below the lower bound for the list (0)"
} }
} }
error "lswap cannot indices $a and $z $a is out of range" set z_index [lindex_resolve $l $z]
set z_msg ""
switch -- $z_index {
-2 {
set z_msg "2nd supplied index $z is above the upper bound for the list ([llength $l])"
}
-3 {
set z_msg "2nd supplied index $z is below the lower bound for the list (0)"
}
}
set errmsg "lswap cannot swap indices $a and $z"
if {$a_msg ne ""} {
append errmsg \n $a_msg
}
if {$z_msg ne ""} {
append errmsg \n $z_msg
}
error $errmsg
} }
set item2 [lindex $l $z] set item2 [lindex $l $z]
lset l $z [lindex $l $a] lset l $z [lindex $l $a]
@ -397,6 +417,7 @@ namespace eval punk::lib {
# #if index a strictly less <= z we can do in one-liner for fun - but it's replacing whole list - so much slower # #if index a strictly less <= z we can do in one-liner for fun - but it's replacing whole list - so much slower
# set l [concat [lrange $l 0 $a-1] [lindex $l $z] [lrange $l $a+1 $z-1] [lindex $l $a] [lrange $l $z+1 end]] # set l [concat [lrange $l 0 $a-1] [lindex $l $z] [lrange $l $a+1 $z-1] [lindex $l $a] [lrange $l $z+1 end]]
#} #}
proc lswap2 {lvar a z} { proc lswap2 {lvar a z} {
upvar $lvar l upvar $lvar l
#if index a strictly less <= z we can do in one-liner for fun - but it's replacing whole list - so much slower #if index a strictly less <= z we can do in one-liner for fun - but it's replacing whole list - so much slower
@ -3021,6 +3042,10 @@ namespace eval punk::lib {
set localeid [twapi::get_system_default_lcid] set localeid [twapi::get_system_default_lcid]
} }
} }
#when using twapi we currently only get the localeid - not the specific defaults
#when not using twapi, or on non-windows platforms - we don't currently have a mechanism to look up user preferences for this
set default_delim ","
set default_groupsize 3
set results [list] set results [list]
set nums [objclone $numbers_or_commaformattednumbers] ;#stops single num from getting internal rep of list set nums [objclone $numbers_or_commaformattednumbers] ;#stops single num from getting internal rep of list
@ -3036,15 +3061,21 @@ namespace eval punk::lib {
lappend results [twapi::format_number $number $localeid -idigits -1] lappend results [twapi::format_number $number $localeid -idigits -1]
continue continue
} else { } else {
if {$delim eq ""} {set delim ","} #setting just one of delim or groupsize means we don't get the user's localeid based default for the non-set one
if {$groupsize eq ""} {set groupsize 3} #todo - document it? Find a way to lookup localeid based defaults whenever either is unspecified?
if {$delim eq ""} {set delim $default_delim}
if {$groupsize eq ""} {set groupsize $default_groupsize}
lappend results [twapi::format_number $number 0 -idigits -1 -sthousand $delim -sgrouping $groupsize] lappend results [twapi::format_number $number 0 -idigits -1 -sthousand $delim -sgrouping $groupsize]
continue continue
} }
} }
#todo - get configured user defaults #todo - get configured user defaults
set delim "," if {$delim eq ""} {
set groupsize 3 set delim $default_delim
}
if {$groupsize eq ""} {
set groupsize $default_groupsize
}
lappend results [delimit_number $number $delim $groupsize] lappend results [delimit_number $number $delim $groupsize]
} }

8
src/bootsupport/modules/punk/mix/cli-0.3.1.tm

@ -692,6 +692,8 @@ namespace eval punk::mix::cli {
#punk::zip::mkzip stores permissions - (unix style) which confuses zipfs when reading - it misidentifies dirs as files #punk::zip::mkzip stores permissions - (unix style) which confuses zipfs when reading - it misidentifies dirs as files
} }
#zipfs mkzip does exactly what we need anyway in this case #zipfs mkzip does exactly what we need anyway in this case
#unfortunately it's not available in all Tclsh versions we might be running..
if {[llength [info commands zipfs]]} {
set wd [pwd] set wd [pwd]
cd $buildfolder cd $buildfolder
puts "zipfs mkzip $zipfile #modpod-$basename-$module_build_version" puts "zipfs mkzip $zipfile #modpod-$basename-$module_build_version"
@ -700,6 +702,12 @@ namespace eval punk::mix::cli {
package require modpod package require modpod
modpod::lib::make_zip_modpod $zipfile $modulefile modpod::lib::make_zip_modpod $zipfile $modulefile
} else {
#TODO - review punk::zip::mkzip and/or external zip to provide a fallback?
set had_err 1
lappend notest "zipfs_unavailable"
puts stderr "WARNING: zipfs unavailable can't build $modulefile"
}
if {$had_error} { if {$had_error} {

1
src/modules/punk/du-999999.0a1.0.tm

@ -942,6 +942,7 @@ namespace eval punk::du {
#struct::set difference removes duplicates (but not always.. e.g if using tcl impl and 2nd element empty!) #struct::set difference removes duplicates (but not always.. e.g if using tcl impl and 2nd element empty!)
#relying on struct::set to remove dupes is somewhat risky. It is not well documented - and behaviour of dupes in inputs is underspecified as it appears to be intended for mathematical 'sets' #relying on struct::set to remove dupes is somewhat risky. It is not well documented - and behaviour of dupes in inputs is underspecified as it appears to be intended for mathematical 'sets'
#remove links and . .. from directories, remove links from files #remove links and . .. from directories, remove links from files
#ideally we would like to classify links by whether they point to files vs dirs - but there are enough cross-platform differences that we will have to leave it to the caller to sort out for now.
#struct::set will affect order: tcl vs critcl give different ordering! #struct::set will affect order: tcl vs critcl give different ordering!
set files [struct::set difference [concat $hfiles $files[unset files]] $links] set files [struct::set difference [concat $hfiles $files[unset files]] $links]
set dirs [struct::set difference [concat $hdirs $dirs[unset dirs]] [concat $links [list [file join $folderpath .] [file join $folderpath ..] ]]] set dirs [struct::set difference [concat $hdirs $dirs[unset dirs]] [concat $links [list [file join $folderpath .] [file join $folderpath ..] ]]]

45
src/modules/punk/lib-999999.0a1.0.tm

@ -372,20 +372,40 @@ namespace eval punk::lib {
proc lswap {lvar a z} { proc lswap {lvar a z} {
upvar $lvar l upvar $lvar l
if {[lindex_resolve_basic $l $a] < 0 || [lindex_resolve_basic $l $z] < 0} { if {[lindex_resolve_basic $l $a] < 0 || [lindex_resolve_basic $l $z] < 0} {
#if we didn't do this check - we could raise an error on second lset - leaving list corrupted because only one lset occurred #lindex_resolve_basic returns only -1 if out of range
#if we didn't do this check - we could raise an error on second lset below - leaving list corrupted because only one lset occurred
#(e.g using: lswap mylist end-2 end on a two element list) #(e.g using: lswap mylist end-2 end on a two element list)
#on the unhapy path we can take time to check the nature of the out-of-boundness to give a nicer report #on the unhapy path we can take time to check the nature of the out-of-boundness to give a nicer report
#use full 'lindex_resolve' which can report which side via -3 and -2 special results being lower and upper bound breaches respectively (-1 never returned)
set a_index [lindex_resolve $l $a] set a_index [lindex_resolve $l $a]
set a_msg "" set a_msg ""
switch -- $a_index { switch -- $a_index {
-2 { -2 {
"$a is greater th set a_msg "1st supplied index $a is above the upper bound for the list ([llength $l])"
} }
-3 { -3 {
set a_msg "1st supplied index $a is below the lower bound for the list (0)"
} }
} }
error "lswap cannot indices $a and $z $a is out of range" set z_index [lindex_resolve $l $z]
set z_msg ""
switch -- $z_index {
-2 {
set z_msg "2nd supplied index $z is above the upper bound for the list ([llength $l])"
}
-3 {
set z_msg "2nd supplied index $z is below the lower bound for the list (0)"
}
}
set errmsg "lswap cannot swap indices $a and $z"
if {$a_msg ne ""} {
append errmsg \n $a_msg
}
if {$z_msg ne ""} {
append errmsg \n $z_msg
}
error $errmsg
} }
set item2 [lindex $l $z] set item2 [lindex $l $z]
lset l $z [lindex $l $a] lset l $z [lindex $l $a]
@ -397,6 +417,7 @@ namespace eval punk::lib {
# #if index a strictly less <= z we can do in one-liner for fun - but it's replacing whole list - so much slower # #if index a strictly less <= z we can do in one-liner for fun - but it's replacing whole list - so much slower
# set l [concat [lrange $l 0 $a-1] [lindex $l $z] [lrange $l $a+1 $z-1] [lindex $l $a] [lrange $l $z+1 end]] # set l [concat [lrange $l 0 $a-1] [lindex $l $z] [lrange $l $a+1 $z-1] [lindex $l $a] [lrange $l $z+1 end]]
#} #}
proc lswap2 {lvar a z} { proc lswap2 {lvar a z} {
upvar $lvar l upvar $lvar l
#if index a strictly less <= z we can do in one-liner for fun - but it's replacing whole list - so much slower #if index a strictly less <= z we can do in one-liner for fun - but it's replacing whole list - so much slower
@ -3021,6 +3042,10 @@ namespace eval punk::lib {
set localeid [twapi::get_system_default_lcid] set localeid [twapi::get_system_default_lcid]
} }
} }
#when using twapi we currently only get the localeid - not the specific defaults
#when not using twapi, or on non-windows platforms - we don't currently have a mechanism to look up user preferences for this
set default_delim ","
set default_groupsize 3
set results [list] set results [list]
set nums [objclone $numbers_or_commaformattednumbers] ;#stops single num from getting internal rep of list set nums [objclone $numbers_or_commaformattednumbers] ;#stops single num from getting internal rep of list
@ -3036,15 +3061,21 @@ namespace eval punk::lib {
lappend results [twapi::format_number $number $localeid -idigits -1] lappend results [twapi::format_number $number $localeid -idigits -1]
continue continue
} else { } else {
if {$delim eq ""} {set delim ","} #setting just one of delim or groupsize means we don't get the user's localeid based default for the non-set one
if {$groupsize eq ""} {set groupsize 3} #todo - document it? Find a way to lookup localeid based defaults whenever either is unspecified?
if {$delim eq ""} {set delim $default_delim}
if {$groupsize eq ""} {set groupsize $default_groupsize}
lappend results [twapi::format_number $number 0 -idigits -1 -sthousand $delim -sgrouping $groupsize] lappend results [twapi::format_number $number 0 -idigits -1 -sthousand $delim -sgrouping $groupsize]
continue continue
} }
} }
#todo - get configured user defaults #todo - get configured user defaults
set delim "," if {$delim eq ""} {
set groupsize 3 set delim $default_delim
}
if {$groupsize eq ""} {
set groupsize $default_groupsize
}
lappend results [delimit_number $number $delim $groupsize] lappend results [delimit_number $number $delim $groupsize]
} }

8
src/modules/punk/mix/cli-999999.0a1.0.tm

@ -692,6 +692,8 @@ namespace eval punk::mix::cli {
#punk::zip::mkzip stores permissions - (unix style) which confuses zipfs when reading - it misidentifies dirs as files #punk::zip::mkzip stores permissions - (unix style) which confuses zipfs when reading - it misidentifies dirs as files
} }
#zipfs mkzip does exactly what we need anyway in this case #zipfs mkzip does exactly what we need anyway in this case
#unfortunately it's not available in all Tclsh versions we might be running..
if {[llength [info commands zipfs]]} {
set wd [pwd] set wd [pwd]
cd $buildfolder cd $buildfolder
puts "zipfs mkzip $zipfile #modpod-$basename-$module_build_version" puts "zipfs mkzip $zipfile #modpod-$basename-$module_build_version"
@ -700,6 +702,12 @@ namespace eval punk::mix::cli {
package require modpod package require modpod
modpod::lib::make_zip_modpod $zipfile $modulefile modpod::lib::make_zip_modpod $zipfile $modulefile
} else {
#TODO - review punk::zip::mkzip and/or external zip to provide a fallback?
set had_err 1
lappend notest "zipfs_unavailable"
puts stderr "WARNING: zipfs unavailable can't build $modulefile"
}
if {$had_error} { if {$had_error} {

116
src/modules/punk/nav/fs-999999.0a1.0.tm

@ -205,12 +205,13 @@ tcl::namespace::eval punk::nav::fs {
} }
set dircount [llength [dict get $matchinfo dirs]] set dircount [llength [dict get $matchinfo dirs]]
set filecount [llength [dict get $matchinfo files]] set filecount [llength [dict get $matchinfo files]]
set symlinkcount [llength [dict get $matchinfo links]] ;#doesn't include windows shelllinks (.lnk)
#set location [file normalize [dict get $matchinfo location]] #set location [file normalize [dict get $matchinfo location]]
set location [dict get $matchinfo location] set location [dict get $matchinfo location]
#result for glob is count of matches - use dirfiles etc for script access to results #result for glob is count of matches - use dirfiles etc for script access to results
set result [list location $location dircount $dircount filecount $filecount] set result [list location $location dircount $dircount filecount $filecount symlinks $symlinkcount]
set filesizes [dict get $matchinfo filesizes] set filesizes [dict get $matchinfo filesizes]
if {[llength $filesizes]} { if {[llength $filesizes]} {
set filesizes [lsearch -all -inline -not $filesizes na] set filesizes [lsearch -all -inline -not $filesizes na]
@ -383,6 +384,7 @@ tcl::namespace::eval punk::nav::fs {
set location [file normalize [dict get $matchinfo location]] set location [file normalize [dict get $matchinfo location]]
if {[string match //xzipfs:/* $location] || $location ne $last_location} { if {[string match //xzipfs:/* $location] || $location ne $last_location} {
#REVIEW - zipfs test disabled with leading x
#emit previous result #emit previous result
if {[dict size $this_result]} { if {[dict size $this_result]} {
dict set this_result filebytes [punk::lib::format_number [dict get $this_result filebytes]] dict set this_result filebytes [punk::lib::format_number [dict get $this_result filebytes]]
@ -1035,9 +1037,6 @@ tcl::namespace::eval punk::nav::fs {
lappend nonportable {*}[dict get $contents nonportable] ;# illegal file/folder names from windows perspective lappend nonportable {*}[dict get $contents nonportable] ;# illegal file/folder names from windows perspective
lappend vfsmounts {*}[dict get $contents vfsmounts] lappend vfsmounts {*}[dict get $contents vfsmounts]
} }
if {$opt_formatsizes} {
set filesizes [punk::lib::format_number $filesizes]
}
if {$opt_stripbase && $common_base ne ""} { if {$opt_stripbase && $common_base ne ""} {
set filetails [list] set filetails [list]
@ -1049,33 +1048,124 @@ tcl::namespace::eval punk::nav::fs {
} }
set $fileset $stripped set $fileset $stripped
} }
#Note: we need to remember to use common_base to rebuild the key when we need to query the dict-based elements: sizes & times - because we didn't strip those keys.
}
# -- --- --- --- --- --- --- --- --- --- ---
#assign symlinks to the dirs or files collection (the punk::du system doesn't sort this out
#As at 2024-09 for windows symlinks - Tcl can't do file readlink on symlinks created with mklink /D name target (SYMLINKD) or mklink name target (SYMLINK)
#We can't read the target information - best we can do is classify it as a file or a dir
#we can't use 'file type' as that will report just 'link' - but file isfile and file isdirectory work and should work for links on all platforms - REVIEW
set file_symlinks [list]
set dir_symlinks [list]
set dir_shortcuts [list] ;#windows shell links (.lnk) that have a target that is a directory
foreach s $links {
if {[file isfile $s]} {
lappend file_symlinks $s
#will be appended in finfo_plus later
} elseif {[file isdirectory $s]} {
lappend dir_symlinks $s
lappend dirs $s
} else {
#dunno - warn for now
puts stderr "Warning - cannot determine link type for link $s"
} }
}
#we now have the issue that our symlinks aren't sorted within the dir/file categorisation - they currently will have to appear at beginning or end - TODO
# -- --- --- --- --- --- --- --- --- --- ---
#todo - sort whilst maintaining order for metadata? #todo - sort whilst maintaining order for metadata?
#we need to co-sort files only with filesizes (other info such as times is keyed on fname so cosorting not required)
#we can't sort on filesize after format_number (unless we were to enforce delim _ which we don't want to do)
if {$opt_formatsizes} {
set filesizes [punk::lib::format_number $filesizes] ;#accepts a list and will process each
}
#col2 with subcolumns #col2 with subcolumns
#remove punk::pipedata dependency - allow use of punk::nav::fs without punk package #remove punk::pipedata dependency - allow use of punk::nav::fs without punk package
#set widest2a [punk::pipedata [list {*}$files ""] {lmap v $data {string length $v}} {tcl::mathfunc::max {*}$data}] #set widest2a [punk::pipedata [list {*}$files ""] {lmap v $data {string length $v}} {tcl::mathfunc::max {*}$data}]
#widest2a.= concat $files [list ""] |> .=>2 lmap v {string length $v} |> .=>* tcl::mathfunc::max #widest2a.= concat $files [list ""] |> .=>2 lmap v {string length $v} |> .=>* tcl::mathfunc::max
set widest2a [tcl::mathfunc::max {*}[lmap v [concat $files [list ""]] {string length $v}]] set widest2a [tcl::mathfunc::max {*}[lmap v [list {*}$files {*}$file_symlinks ""] {string length $v}]]
set c2a [string repeat " " [expr {$widest2a + 1}]] set c2a [string repeat " " [expr {$widest2a + 1}]]
#set widest2b [punk::pipedata [list {*}$filesizes ""] {lmap v $data {string length $v}} {tcl::mathfunc::max {*}$data}] #set widest2b [punk::pipedata [list {*}$filesizes ""] {lmap v $data {string length $v}} {tcl::mathfunc::max {*}$data}]
set widest2b [tcl::mathfunc::max {*}[lmap v [concat $filesizes [list ""]] {string length $v}]] set widest2b [tcl::mathfunc::max {*}[lmap v [list {*}$filesizes ""] {string length $v}]]
set c2b [string repeat " " [expr {$widest2b + 1}]] set c2b [string repeat " " [expr {$widest2b + 1}]]
set finfo [list] set finfo [list]
foreach f $files s $filesizes { foreach f $files s $filesizes {
#note - the display entry isn't necessarily a valid tcl list e.g filename with unbalanced curly braces #note - the display entry isn't necessarily a valid tcl list e.g filename with unbalanced curly braces
#hence we need to keep the filename, as well properly protected as a list element #hence we need to keep the filename as well, properly protected as a list element
lappend finfo [list file $f display "[overtype::left $c2a $f] [overtype::right $c2b $s]"] lappend finfo [list file $f display "[overtype::left $c2a $f] [overtype::right $c2b $s]"]
} }
set flink_style [punk::ansi::a+ undercurly underline undt-green] ;#curly green underline with fallback to normal underline
set dlink_style [punk::ansi::a+ undercurly underline undt-green]
#We use an underline so the visual styling of a link can coexist with fg/bg colors applied for other attributes such as hidden
foreach flink $file_symlinks {
lappend finfo [list file $flink display "$flink_style[overtype::left $c2a $flink] [overtype::right $c2b 0]"]
}
set fshortcut_style [punk::ansi::a+ underdotted underline undt-hotpink]
set dshortcut_style [punk::ansi::a+ underdotted underline undt-hotpink]
#examine windows .lnk shell link files (shortcuts) - these could be encountered on other platforms too - we should still be able to read them
#review - symlink to shortcut? hopefully will just work
#classify as file or directory - fallback to file if unknown/undeterminable
set finfo_plus [list]
foreach fdict $finfo {
set fname [dict get $fdict file]
if {[file extension $fname] eq ".lnk"} {
if {![catch {package require punk::winlnk}]} {
set shortcutinfo [punk::winlnk::file_get_info $fname]
set target_type "file" ;#default/fallback
if {[dict exists $shortcutinfo link_target]} {
set tgt [dict get $shortcutinfo link_target]
if {[file exists $tgt]} {
#file type could return 'link' - we will use ifile/isdirectory
if {[file isfile $tgt]} {
set target_type file
} elseif {[file isdirectory $tgt]} {
set target_type directory
} else {
set target_type file ;## ?
}
} else {
#todo - see if punk::winlnk has info about the type at the time of linking
#for now - treat as file
}
}
switch -- $target_type {
file {
set display [dict get $fdict display]
set display $fshortcut_style$display ;#
dict set fdict display $display
lappend finfo_plus $fdict
}
directory {
#target of link is a dir - for display/categorisation purposes we want to see it as a dir
#will be styled later based on membership of dir_shortcuts
lappend dirs $fname
lappend dir_shortcuts $fname
}
}
}
#if we don't have punk::winlnk to read the .lnk - it will get no special highlighting and just appear as an ordinary file even if it points to a dir
} else {
lappend finfo_plus $fdict
}
}
unset finfo
#set widest1 [punk::pipedata [list {*}$dirs ""] {lmap v $data {string length $v}} {tcl::mathfunc::max {*}$data}] #set widest1 [punk::pipedata [list {*}$dirs ""] {lmap v $data {string length $v}} {tcl::mathfunc::max {*}$data}]
set widest1 [tcl::mathfunc::max {*}[lmap v [concat $dirs [list ""]] {string length $v}]] set widest1 [tcl::mathfunc::max {*}[lmap v [concat $dirs [list ""]] {string length $v}]]
set displaylist [list] set displaylist [list]
set col1 [string repeat " " [expr {$widest1 + 2}]] set col1 [string repeat " " [expr {$widest1 + 2}]]
foreach d $dirs filerec $finfo { set RST [punk::ansi::a]
foreach d $dirs filerec $finfo_plus {
set d1 [punk::ansi::a+ cyan bold] set d1 [punk::ansi::a+ cyan bold]
set d2 [punk::ansi::a+ defaultfg defaultbg normal] set d2 [punk::ansi::a+ defaultfg defaultbg normal]
#set f1 [punk::ansi::a+ white bold] #set f1 [punk::ansi::a+ white bold]
@ -1088,7 +1178,7 @@ tcl::namespace::eval punk::nav::fs {
} }
if {$d in $vfsmounts} { if {$d in $vfsmounts} {
if {$d in $flaggedhidden} { if {$d in $flaggedhidden} {
#we could have a hidden dir which is also a vfs.. color will be overridden giving no indicatio of 'hidden' status - REVIEW #we could have a hidden dir which is also a vfs.. colour will be overridden giving no indicatio of 'hidden' status - REVIEW
#(This situation encountered on windows - even though file attr showed -hidden 0 - the glob with -types hidden returned it.. possibly a tcl glob bug on windows) #(This situation encountered on windows - even though file attr showed -hidden 0 - the glob with -types hidden returned it.. possibly a tcl glob bug on windows)
#mark it differently for now.. (todo bug report?) #mark it differently for now.. (todo bug report?)
if {$d in $nonportable} { if {$d in $nonportable} {
@ -1108,6 +1198,12 @@ tcl::namespace::eval punk::nav::fs {
set d1 [punk::ansi::a+ red bold] set d1 [punk::ansi::a+ red bold]
} }
} }
#dlink-style & dshortcut_style are for underlines - can be added with colours already set
if {$d in $dir_symlinks} {
append d1 $dlink_style
} elseif {$d in $dir_shortcuts} {
append d1 $dshortcut_style
}
} }
if {[llength $filerec]} { if {[llength $filerec]} {
set fname [dict get $filerec file] set fname [dict get $filerec file]
@ -1120,7 +1216,7 @@ tcl::namespace::eval punk::nav::fs {
} }
} }
} }
lappend displaylist $d1[overtype::left $col1 $d]$d2$f1$fdisp$f2 lappend displaylist [overtype::left $col1 $d1$d$RST]$f1$fdisp$RST
} }
return [punk::lib::list_as_lines $displaylist] return [punk::lib::list_as_lines $displaylist]

6
src/modules/punk/winlnk-999999.0a1.0.tm

@ -440,16 +440,16 @@ tcl::namespace::eval punk::winlnk {
set localbase_path "" set localbase_path ""
set suffix_path "" set suffix_path ""
set linkinfocontent [dict get $linkinfo_content_dict content] set linkinfocontent [dict get $linkinfo_content_dict content]
set link_file "" set link_target ""
if {$linkinfocontent ne ""} { if {$linkinfocontent ne ""} {
set linkfields [LinkInfo_get_fields $linkinfocontent] set linkfields [LinkInfo_get_fields $linkinfocontent]
set localbase_path [dict get $linkfields localbasepath] set localbase_path [dict get $linkfields localbasepath]
set suffix_path [dict get $linkfields commonpathsuffix] set suffix_path [dict get $linkfields commonpathsuffix]
set link_file [file join $localbase_path $suffix_path] set link_target [file join $localbase_path $suffix_path]
} }
set result [dict create\ set result [dict create\
link_file $link_file\ link_target $link_target\
link_flags $flags_enabled\ link_flags $flags_enabled\
file_attributes "<unimplemented>"\ file_attributes "<unimplemented>"\
create_time "<unimplemented>"\ create_time "<unimplemented>"\

468
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/fauxlink-0.1.0.tm

@ -0,0 +1,468 @@
# -*- tcl -*-
# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from <pkg>-buildversion.txt
#
# 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 fauxlink 0.1.0
# Meta platform tcl
# Meta license MIT
# @@ Meta End
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# doctools header
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[manpage_begin fauxlink_module_fauxlink 0 0.1.0]
#[copyright "2024"]
#[titledesc {faux link application shortcuts}] [comment {-- Name section and table of contents description --}]
#[moddesc {fauxlink .fxlnk}] [comment {-- Description at end of page heading --}]
#[require fauxlink]
#[keywords symlink faux fake shortcut toml]
#[description]
#[para] A cross platform shortcut/symlink alternative.
#[para] Unapologetically ugly - but practical in certain circumstances.
#[para] A solution is required for application-driven filesystem links that survives cross platform moves as well as
#[para] archiving and packaging systems.
#[para] The target is specified in a minimally-encoded form in the filename itself - but still human readable.
#[para] format of name <nominalname>#<encodedtarget>.fxlnk
#[para] where <nominalname> can be empty - then the effective nominal name is the tail of the <encodedtarget>
#[para] The + symbol substitutes for forward-slashes.
#[para] Other chars can be encoded using url-like encoding - (but only up to %7E !)
#[para] We deliberately treat higher % sequences literally.
#[para] This means actual uri::urn encoded unicode sequences (e.g %E2%99%A5 [heart]) can remain literal for linking to urls.
#[para] e.g if an actual + or # is required in a filename or path segment they can be encoded as %2B & %23
#[para] e.g a link to a file file#A.txt in parent dir could be:
#[para] file%23A.txt#..+file%23A.txt.fxlnk
#[para] or equivalently (but obviously affecting sorting) #..+file%23A.txt.fxlnk
#[para] The <nominalname> can be unrelated to the actual target
#[para] e.g datafile.dat#..+file%23A.txt.fxlnk
#[para] This system has no filesystem support - and must be completely application driven.
#[para] This can be useful for example in application test packages which may be tarred or zipped and moved cross platform.
#[para] The target being fully specified in the name means the file doesn't have to be read for the target to be determined
#[para] Extensions to behaviour should be added in the file as text data in Toml format,
#[para] with custom data being under a single application-chosen table name
#[para] The toplevel Toml table [fauxlink] is reserved for core extensions to this system.
#[para] Aside from the 2 used for delimiting (+ #)
#[para] certain characters which might normally be allowed in filesystems are required to be encoded
#[para] e.g space and tab are required to be %20 %09
#[para] Others that require encoding are: * ? \ / | : ; " < >
#[para] The nul character in raw form, when detected, is always mapped away to the empty string - as very few filesystems support it.
#[para] Control characters and other punctuation is optional to encode.
#[para] Generally utf-8 should be used where possible and unicode characters left as is where possible on modern systems.
#[para] Where encoding of unicode is desired in the nominalname or encodedtarget portions it can be specified as %UXXXXXXXX
#[para] There must be between 1 and 8 X digits following the %U. Interpretation of chars following %U stops at the first non-hex character.
#[para] This means %Utest would not get any translation as there were no hex digits so it would come out as %Utest
#
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#https://learn.microsoft.com/en-us/troubleshoot/windows-client/networking/url-encoding-unc-paths-not-url-decoded
# ie "//server/c/Program files" works but "//server/c/Program%20Files" is now treated by windows as a literal path with %20 in it.
#Using fauxlink - a link would be:
# "my-program-files#++server+c+Program%20Files.fxlnk"
#If we needed the old-style literal %20 it would become
# "my-program-files#++server+c+Program%2520Files.fxlnk"
#*** !doctools
#[section Overview]
#[para] overview of fauxlink
#[subsection Concepts]
#[para] -
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Requirements
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[subsection dependencies]
#[para] packages used by fauxlink
#[list_begin itemized]
package require Tcl 8.6-
#*** !doctools
#[item] [package {Tcl 8.6-}]
# #package require frobz
# #*** !doctools
# #[item] [package {frobz}]
#*** !doctools
#[list_end]
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[section API]
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# oo::class namespace
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
namespace eval fauxlink::class {
#*** !doctools
#[subsection {Namespace fauxlink::class}]
#[para] class definitions
if {[info commands [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
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
namespace eval fauxlink {
namespace export {[a-z]*}; # Convention: export all lowercase
#todo - enforce utf-8
#literal unicode chars supported by modern filesystems - leave as is - REVIEW
variable encode_map
variable decode_map
#most filesystems don't allow NULL - map to empty string
#Make sure % is not in encode_map
set encode_map [dict create\
\x00 ""\
{ } %20\
\t %09\
+ %2B\
# %23\
* %2A\
? %3F\
\\ %5C\
/ %2F\
| %7C\
: %3A\
{;} %3B\
{"} %22\
< %3C\
> %3E\
]
#above have some overlap with ctrl codes below.
#no big deal as it's a dict
#must_encode
# + # * ? \ / | : ; " < > <sp> \t
# also NUL to empty string
# also ctrl chars 01 to 1F (1..31)
for {set i 1} {$i < 32} {incr i} {
set ch [format %c $i]
set enc "%[format %02X $i]"
set enc_lower [string tolower $enc]
dict set encode_map $ch $enc
dict set decode_map $enc $ch
dict set decode_map $enc_lower $ch
}
variable must_encode
set must_encode [dict keys $encode_map]
#if they are in
#decode map doesn't include
# %00 (nul)
# %2F "/"
# %2f "/"
# %7f (del)
#we exlude the forward slash because we already have + for that - and multiple ways to specify it obscure intention.
#
set decode_map [dict merge $decode_map [dict create\
%09 \t\
%20 { }\
%21 "!"\
%22 {"}\
%23 "#"\
%24 "$"\
%25 "%"\
%26 "&"\
%27 "'"\
%28 "("\
%29 ")"\
%2A "*"\
%2a "*"\
%2B "+"\
%2b "+"\
%2C ","\
%2c ","\
%2D "-"\
%2d "-"\
%2E "."\
%2e "."\
%3A ":"\
%3a ":"\
%3B {;}\
%3b {;}\
%3D "="\
%3C "<"\
%3c "<"\
%3d "="\
%3E ">"\
%3e ">"\
%3F "?"\
%3f "?"\
%40 "@"\
%5B "\["\
%5b "\["\
%5C "\\"\
%5c "\\"\
%5D "\]"\
%5d "\]"\
%5E "^"\
%5e "^"\
%60 "`"\
%7B "{"\
%7b "{"\
%7C "|"\
%7c "|"\
%7D "}"\
%7d "}"\
%7E "~"\
%7e "~"\
]]
#Don't go above 7f
#if we want to specify p
#*** !doctools
#[subsection {Namespace fauxlink}]
#[para] Core API functions for fauxlink
#[list_begin definitions]
proc resolve {link} {
variable decode_map
variable encode_map
variable must_encode
set ftail [file tail $link]
if {[file extension $ftail] ni [list .fxlnk .fauxlink]} {
error "fauxlink::resolve refusing to process link $link - file extension must be .fxlnk or .fauxlink"
}
set linkspec [file rootname $ftail]
# - any # or + within the target path or name should have been uri encoded as %23 and %2b
if {[tcl::string::first # $linkspec] < 0} {
error "fauxlink::resolve error. Link must contain a # (usually at start if name matches target)"
}
#only the 1st 2 parts of split on # are significant.
#if there are more # chars present - the subsequent parts are effectively a comment
#check namepec already has required chars encoded
lassign [split $linkspec #] namespec targetspec
#puts stderr "-->namespec $namespec"
set nametest [tcl::string::map $encode_map $namespec]
#puts stderr "-->nametest $nametest"
#nothing should be changed - if there are unencoded chars that must be encoded it is an error
if {[tcl::string::length $nametest] ne [tcl::string::length $namespec]} {
set err "fauxlink::resolve invalid chars in name part (section prior to first #)"
set idx 0
foreach ch [split $namespec ""] {
if {$ch in $must_encode} {
set enc [dict get $encode_map $ch]
if {[dict exists $decode_map $enc]} {
append err " char $idx should be encoded as $enc" \n
} else {
append err " no %xx encoding available. Use %UXX if really required" \n
}
}
incr idx
}
error $err
}
#see comments below regarding 2 rounds and ordering.
set name [decode_unicode_escapes $namespec]
set name [tcl::string::map $decode_map $name]
#puts stderr "-->name: $name"
set targetsegment [split $targetspec +]
#check each + delimited part of targetspec already has required chars encoded
set s 0 ;#segment index
set result_segments [list]
foreach segment $targetsegment {
set targettest [tcl::string::map $encode_map $segment]
if {[tcl::string::length $targettest] ne [tcl::string::length $segment]} {
set err "fauxlink::resolve invalid chars in targetpath (section following first #)"
set idx 0
foreach ch [split $segment ""] {
if {$ch in $must_encode} {
set enc [dict get $encode_map $ch]
if {[dict exists $decode_map $enc]} {
append err " segment $s char $idx should be encoded as $enc" \n
} else {
append err " no %xx encoding available. Use %UXX if really required" \n
}
}
incr idx
}
error $err
}
#2 rounds of substitution is possibly asking for trouble..
#We allow anything in the resultant segments anyway (as %UXXXX... allows all)
#so it's not so much about what can be encoded,
# - but it makes it harder to reason about for users
# In particular - if we map %XX first it makes %25 -> % substitution tricky
# if the user requires a literal %UXXX - they can't do %25UXXX
# the double sub would make it %UXXX -> somechar anyway.
#we do unicode first - as a 2nd round of %XX substitutions is unlikely to interfere.
#There is still the opportunity to use things like %U00000025 followed by hex-chars
# and get some minor surprises, but using %U on ascii is unlikely to be done accidentally - REVIEW
set segment [decode_unicode_escapes $segment]
set segment [tcl::string::map $decode_map $segment]
lappend result_segments $segment
incr s
}
set targetpath [join $result_segments /]
if {$name eq ""} {
set name [lindex $result_segments end]
}
return [dict create name $name targetpath $targetpath]
}
variable map
#default exclusion of / (%U2f and equivs)
#this would allow obfuscation of intention - when we have + for that anyway
proc decode_unicode_escapes {str {exclusions {/ \n \r \x00}}} {
variable map
set ucstart [string first %U $str 0]
if {$ucstart < 0} {
return $str
}
set max 8
set map [list]
set strend [expr {[string length $str]-1}]
while {$ucstart >= 0} {
set s $ucstart
set i [expr {$s +2}] ;#skip the %U
set hex ""
while {[tcl::string::length $hex] < 8 && $i <= $strend} {
set in [string index $str $i]
if {[tcl::string::is xdigit -strict $in]} {
append hex $in
} else {
break
}
incr i
}
if {$hex ne ""} {
incr i -1
lappend map $s $i $hex
}
set ucstart [tcl::string::first %U $str $i]
}
set out ""
set lastidx -1
set e 0
foreach {s e hex} $map {
append out [string range $str $lastidx+1 $s-1]
set sub [format %c 0x$hex]
if {$sub in $exclusions} {
append out %U$hex ;#put it back
} else {
append out $sub
}
set lastidx $e
}
if {$e < [tcl::string::length $str]-1} {
append out [string range $str $e+1 end]
}
return $out
}
proc link_as {name target} {
}
#proc sample1 {p1 args} {
# #*** !doctools
# #[call [fun sample1] [arg p1] [opt {?option value...?}]]
# #[para]Description of sample1
# return "ok"
#}
#*** !doctools
#[list_end] [comment {--- end definitions namespace fauxlink ---}]
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# Secondary API namespace
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
namespace eval fauxlink::lib {
namespace export {[a-z]*}; # Convention: export all lowercase
namespace path [namespace parent]
#*** !doctools
#[subsection {Namespace fauxlink::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 fauxlink::lib ---}]
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[section Internal]
namespace eval fauxlink::system {
#*** !doctools
#[subsection {Namespace fauxlink::system}]
#[para] Internal functions that are not part of the API
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Ready
package provide fauxlink [namespace eval fauxlink {
variable pkg fauxlink
variable version
set version 0.1.0
}]
return
#*** !doctools
#[manpage_end]

1
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/include_modules.config

@ -7,6 +7,7 @@ set bootsupport_modules [list\
src/vendormodules cksum\ src/vendormodules cksum\
src/vendormodules modpod\ src/vendormodules modpod\
src/vendormodules overtype\ src/vendormodules overtype\
src/vendormodules fauxlink\
src/vendormodules oolib\ src/vendormodules oolib\
src/vendormodules http\ src/vendormodules http\
src/vendormodules dictutils\ src/vendormodules dictutils\

1
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/du-0.1.0.tm

@ -942,6 +942,7 @@ namespace eval punk::du {
#struct::set difference removes duplicates (but not always.. e.g if using tcl impl and 2nd element empty!) #struct::set difference removes duplicates (but not always.. e.g if using tcl impl and 2nd element empty!)
#relying on struct::set to remove dupes is somewhat risky. It is not well documented - and behaviour of dupes in inputs is underspecified as it appears to be intended for mathematical 'sets' #relying on struct::set to remove dupes is somewhat risky. It is not well documented - and behaviour of dupes in inputs is underspecified as it appears to be intended for mathematical 'sets'
#remove links and . .. from directories, remove links from files #remove links and . .. from directories, remove links from files
#ideally we would like to classify links by whether they point to files vs dirs - but there are enough cross-platform differences that we will have to leave it to the caller to sort out for now.
#struct::set will affect order: tcl vs critcl give different ordering! #struct::set will affect order: tcl vs critcl give different ordering!
set files [struct::set difference [concat $hfiles $files[unset files]] $links] set files [struct::set difference [concat $hfiles $files[unset files]] $links]
set dirs [struct::set difference [concat $hdirs $dirs[unset dirs]] [concat $links [list [file join $folderpath .] [file join $folderpath ..] ]]] set dirs [struct::set difference [concat $hdirs $dirs[unset dirs]] [concat $links [list [file join $folderpath .] [file join $folderpath ..] ]]]

45
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/lib-0.1.1.tm

@ -372,20 +372,40 @@ namespace eval punk::lib {
proc lswap {lvar a z} { proc lswap {lvar a z} {
upvar $lvar l upvar $lvar l
if {[lindex_resolve_basic $l $a] < 0 || [lindex_resolve_basic $l $z] < 0} { if {[lindex_resolve_basic $l $a] < 0 || [lindex_resolve_basic $l $z] < 0} {
#if we didn't do this check - we could raise an error on second lset - leaving list corrupted because only one lset occurred #lindex_resolve_basic returns only -1 if out of range
#if we didn't do this check - we could raise an error on second lset below - leaving list corrupted because only one lset occurred
#(e.g using: lswap mylist end-2 end on a two element list) #(e.g using: lswap mylist end-2 end on a two element list)
#on the unhapy path we can take time to check the nature of the out-of-boundness to give a nicer report #on the unhapy path we can take time to check the nature of the out-of-boundness to give a nicer report
#use full 'lindex_resolve' which can report which side via -3 and -2 special results being lower and upper bound breaches respectively (-1 never returned)
set a_index [lindex_resolve $l $a] set a_index [lindex_resolve $l $a]
set a_msg "" set a_msg ""
switch -- $a_index { switch -- $a_index {
-2 { -2 {
"$a is greater th set a_msg "1st supplied index $a is above the upper bound for the list ([llength $l])"
} }
-3 { -3 {
set a_msg "1st supplied index $a is below the lower bound for the list (0)"
} }
} }
error "lswap cannot indices $a and $z $a is out of range" set z_index [lindex_resolve $l $z]
set z_msg ""
switch -- $z_index {
-2 {
set z_msg "2nd supplied index $z is above the upper bound for the list ([llength $l])"
}
-3 {
set z_msg "2nd supplied index $z is below the lower bound for the list (0)"
}
}
set errmsg "lswap cannot swap indices $a and $z"
if {$a_msg ne ""} {
append errmsg \n $a_msg
}
if {$z_msg ne ""} {
append errmsg \n $z_msg
}
error $errmsg
} }
set item2 [lindex $l $z] set item2 [lindex $l $z]
lset l $z [lindex $l $a] lset l $z [lindex $l $a]
@ -397,6 +417,7 @@ namespace eval punk::lib {
# #if index a strictly less <= z we can do in one-liner for fun - but it's replacing whole list - so much slower # #if index a strictly less <= z we can do in one-liner for fun - but it's replacing whole list - so much slower
# set l [concat [lrange $l 0 $a-1] [lindex $l $z] [lrange $l $a+1 $z-1] [lindex $l $a] [lrange $l $z+1 end]] # set l [concat [lrange $l 0 $a-1] [lindex $l $z] [lrange $l $a+1 $z-1] [lindex $l $a] [lrange $l $z+1 end]]
#} #}
proc lswap2 {lvar a z} { proc lswap2 {lvar a z} {
upvar $lvar l upvar $lvar l
#if index a strictly less <= z we can do in one-liner for fun - but it's replacing whole list - so much slower #if index a strictly less <= z we can do in one-liner for fun - but it's replacing whole list - so much slower
@ -3021,6 +3042,10 @@ namespace eval punk::lib {
set localeid [twapi::get_system_default_lcid] set localeid [twapi::get_system_default_lcid]
} }
} }
#when using twapi we currently only get the localeid - not the specific defaults
#when not using twapi, or on non-windows platforms - we don't currently have a mechanism to look up user preferences for this
set default_delim ","
set default_groupsize 3
set results [list] set results [list]
set nums [objclone $numbers_or_commaformattednumbers] ;#stops single num from getting internal rep of list set nums [objclone $numbers_or_commaformattednumbers] ;#stops single num from getting internal rep of list
@ -3036,15 +3061,21 @@ namespace eval punk::lib {
lappend results [twapi::format_number $number $localeid -idigits -1] lappend results [twapi::format_number $number $localeid -idigits -1]
continue continue
} else { } else {
if {$delim eq ""} {set delim ","} #setting just one of delim or groupsize means we don't get the user's localeid based default for the non-set one
if {$groupsize eq ""} {set groupsize 3} #todo - document it? Find a way to lookup localeid based defaults whenever either is unspecified?
if {$delim eq ""} {set delim $default_delim}
if {$groupsize eq ""} {set groupsize $default_groupsize}
lappend results [twapi::format_number $number 0 -idigits -1 -sthousand $delim -sgrouping $groupsize] lappend results [twapi::format_number $number 0 -idigits -1 -sthousand $delim -sgrouping $groupsize]
continue continue
} }
} }
#todo - get configured user defaults #todo - get configured user defaults
set delim "," if {$delim eq ""} {
set groupsize 3 set delim $default_delim
}
if {$groupsize eq ""} {
set groupsize $default_groupsize
}
lappend results [delimit_number $number $delim $groupsize] lappend results [delimit_number $number $delim $groupsize]
} }

8
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/cli-0.3.1.tm

@ -692,6 +692,8 @@ namespace eval punk::mix::cli {
#punk::zip::mkzip stores permissions - (unix style) which confuses zipfs when reading - it misidentifies dirs as files #punk::zip::mkzip stores permissions - (unix style) which confuses zipfs when reading - it misidentifies dirs as files
} }
#zipfs mkzip does exactly what we need anyway in this case #zipfs mkzip does exactly what we need anyway in this case
#unfortunately it's not available in all Tclsh versions we might be running..
if {[llength [info commands zipfs]]} {
set wd [pwd] set wd [pwd]
cd $buildfolder cd $buildfolder
puts "zipfs mkzip $zipfile #modpod-$basename-$module_build_version" puts "zipfs mkzip $zipfile #modpod-$basename-$module_build_version"
@ -700,6 +702,12 @@ namespace eval punk::mix::cli {
package require modpod package require modpod
modpod::lib::make_zip_modpod $zipfile $modulefile modpod::lib::make_zip_modpod $zipfile $modulefile
} else {
#TODO - review punk::zip::mkzip and/or external zip to provide a fallback?
set had_err 1
lappend notest "zipfs_unavailable"
puts stderr "WARNING: zipfs unavailable can't build $modulefile"
}
if {$had_error} { if {$had_error} {

468
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/fauxlink-0.1.0.tm

@ -0,0 +1,468 @@
# -*- tcl -*-
# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from <pkg>-buildversion.txt
#
# 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 fauxlink 0.1.0
# Meta platform tcl
# Meta license MIT
# @@ Meta End
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# doctools header
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[manpage_begin fauxlink_module_fauxlink 0 0.1.0]
#[copyright "2024"]
#[titledesc {faux link application shortcuts}] [comment {-- Name section and table of contents description --}]
#[moddesc {fauxlink .fxlnk}] [comment {-- Description at end of page heading --}]
#[require fauxlink]
#[keywords symlink faux fake shortcut toml]
#[description]
#[para] A cross platform shortcut/symlink alternative.
#[para] Unapologetically ugly - but practical in certain circumstances.
#[para] A solution is required for application-driven filesystem links that survives cross platform moves as well as
#[para] archiving and packaging systems.
#[para] The target is specified in a minimally-encoded form in the filename itself - but still human readable.
#[para] format of name <nominalname>#<encodedtarget>.fxlnk
#[para] where <nominalname> can be empty - then the effective nominal name is the tail of the <encodedtarget>
#[para] The + symbol substitutes for forward-slashes.
#[para] Other chars can be encoded using url-like encoding - (but only up to %7E !)
#[para] We deliberately treat higher % sequences literally.
#[para] This means actual uri::urn encoded unicode sequences (e.g %E2%99%A5 [heart]) can remain literal for linking to urls.
#[para] e.g if an actual + or # is required in a filename or path segment they can be encoded as %2B & %23
#[para] e.g a link to a file file#A.txt in parent dir could be:
#[para] file%23A.txt#..+file%23A.txt.fxlnk
#[para] or equivalently (but obviously affecting sorting) #..+file%23A.txt.fxlnk
#[para] The <nominalname> can be unrelated to the actual target
#[para] e.g datafile.dat#..+file%23A.txt.fxlnk
#[para] This system has no filesystem support - and must be completely application driven.
#[para] This can be useful for example in application test packages which may be tarred or zipped and moved cross platform.
#[para] The target being fully specified in the name means the file doesn't have to be read for the target to be determined
#[para] Extensions to behaviour should be added in the file as text data in Toml format,
#[para] with custom data being under a single application-chosen table name
#[para] The toplevel Toml table [fauxlink] is reserved for core extensions to this system.
#[para] Aside from the 2 used for delimiting (+ #)
#[para] certain characters which might normally be allowed in filesystems are required to be encoded
#[para] e.g space and tab are required to be %20 %09
#[para] Others that require encoding are: * ? \ / | : ; " < >
#[para] The nul character in raw form, when detected, is always mapped away to the empty string - as very few filesystems support it.
#[para] Control characters and other punctuation is optional to encode.
#[para] Generally utf-8 should be used where possible and unicode characters left as is where possible on modern systems.
#[para] Where encoding of unicode is desired in the nominalname or encodedtarget portions it can be specified as %UXXXXXXXX
#[para] There must be between 1 and 8 X digits following the %U. Interpretation of chars following %U stops at the first non-hex character.
#[para] This means %Utest would not get any translation as there were no hex digits so it would come out as %Utest
#
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#https://learn.microsoft.com/en-us/troubleshoot/windows-client/networking/url-encoding-unc-paths-not-url-decoded
# ie "//server/c/Program files" works but "//server/c/Program%20Files" is now treated by windows as a literal path with %20 in it.
#Using fauxlink - a link would be:
# "my-program-files#++server+c+Program%20Files.fxlnk"
#If we needed the old-style literal %20 it would become
# "my-program-files#++server+c+Program%2520Files.fxlnk"
#*** !doctools
#[section Overview]
#[para] overview of fauxlink
#[subsection Concepts]
#[para] -
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Requirements
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[subsection dependencies]
#[para] packages used by fauxlink
#[list_begin itemized]
package require Tcl 8.6-
#*** !doctools
#[item] [package {Tcl 8.6-}]
# #package require frobz
# #*** !doctools
# #[item] [package {frobz}]
#*** !doctools
#[list_end]
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[section API]
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# oo::class namespace
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
namespace eval fauxlink::class {
#*** !doctools
#[subsection {Namespace fauxlink::class}]
#[para] class definitions
if {[info commands [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
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
namespace eval fauxlink {
namespace export {[a-z]*}; # Convention: export all lowercase
#todo - enforce utf-8
#literal unicode chars supported by modern filesystems - leave as is - REVIEW
variable encode_map
variable decode_map
#most filesystems don't allow NULL - map to empty string
#Make sure % is not in encode_map
set encode_map [dict create\
\x00 ""\
{ } %20\
\t %09\
+ %2B\
# %23\
* %2A\
? %3F\
\\ %5C\
/ %2F\
| %7C\
: %3A\
{;} %3B\
{"} %22\
< %3C\
> %3E\
]
#above have some overlap with ctrl codes below.
#no big deal as it's a dict
#must_encode
# + # * ? \ / | : ; " < > <sp> \t
# also NUL to empty string
# also ctrl chars 01 to 1F (1..31)
for {set i 1} {$i < 32} {incr i} {
set ch [format %c $i]
set enc "%[format %02X $i]"
set enc_lower [string tolower $enc]
dict set encode_map $ch $enc
dict set decode_map $enc $ch
dict set decode_map $enc_lower $ch
}
variable must_encode
set must_encode [dict keys $encode_map]
#if they are in
#decode map doesn't include
# %00 (nul)
# %2F "/"
# %2f "/"
# %7f (del)
#we exlude the forward slash because we already have + for that - and multiple ways to specify it obscure intention.
#
set decode_map [dict merge $decode_map [dict create\
%09 \t\
%20 { }\
%21 "!"\
%22 {"}\
%23 "#"\
%24 "$"\
%25 "%"\
%26 "&"\
%27 "'"\
%28 "("\
%29 ")"\
%2A "*"\
%2a "*"\
%2B "+"\
%2b "+"\
%2C ","\
%2c ","\
%2D "-"\
%2d "-"\
%2E "."\
%2e "."\
%3A ":"\
%3a ":"\
%3B {;}\
%3b {;}\
%3D "="\
%3C "<"\
%3c "<"\
%3d "="\
%3E ">"\
%3e ">"\
%3F "?"\
%3f "?"\
%40 "@"\
%5B "\["\
%5b "\["\
%5C "\\"\
%5c "\\"\
%5D "\]"\
%5d "\]"\
%5E "^"\
%5e "^"\
%60 "`"\
%7B "{"\
%7b "{"\
%7C "|"\
%7c "|"\
%7D "}"\
%7d "}"\
%7E "~"\
%7e "~"\
]]
#Don't go above 7f
#if we want to specify p
#*** !doctools
#[subsection {Namespace fauxlink}]
#[para] Core API functions for fauxlink
#[list_begin definitions]
proc resolve {link} {
variable decode_map
variable encode_map
variable must_encode
set ftail [file tail $link]
if {[file extension $ftail] ni [list .fxlnk .fauxlink]} {
error "fauxlink::resolve refusing to process link $link - file extension must be .fxlnk or .fauxlink"
}
set linkspec [file rootname $ftail]
# - any # or + within the target path or name should have been uri encoded as %23 and %2b
if {[tcl::string::first # $linkspec] < 0} {
error "fauxlink::resolve error. Link must contain a # (usually at start if name matches target)"
}
#only the 1st 2 parts of split on # are significant.
#if there are more # chars present - the subsequent parts are effectively a comment
#check namepec already has required chars encoded
lassign [split $linkspec #] namespec targetspec
#puts stderr "-->namespec $namespec"
set nametest [tcl::string::map $encode_map $namespec]
#puts stderr "-->nametest $nametest"
#nothing should be changed - if there are unencoded chars that must be encoded it is an error
if {[tcl::string::length $nametest] ne [tcl::string::length $namespec]} {
set err "fauxlink::resolve invalid chars in name part (section prior to first #)"
set idx 0
foreach ch [split $namespec ""] {
if {$ch in $must_encode} {
set enc [dict get $encode_map $ch]
if {[dict exists $decode_map $enc]} {
append err " char $idx should be encoded as $enc" \n
} else {
append err " no %xx encoding available. Use %UXX if really required" \n
}
}
incr idx
}
error $err
}
#see comments below regarding 2 rounds and ordering.
set name [decode_unicode_escapes $namespec]
set name [tcl::string::map $decode_map $name]
#puts stderr "-->name: $name"
set targetsegment [split $targetspec +]
#check each + delimited part of targetspec already has required chars encoded
set s 0 ;#segment index
set result_segments [list]
foreach segment $targetsegment {
set targettest [tcl::string::map $encode_map $segment]
if {[tcl::string::length $targettest] ne [tcl::string::length $segment]} {
set err "fauxlink::resolve invalid chars in targetpath (section following first #)"
set idx 0
foreach ch [split $segment ""] {
if {$ch in $must_encode} {
set enc [dict get $encode_map $ch]
if {[dict exists $decode_map $enc]} {
append err " segment $s char $idx should be encoded as $enc" \n
} else {
append err " no %xx encoding available. Use %UXX if really required" \n
}
}
incr idx
}
error $err
}
#2 rounds of substitution is possibly asking for trouble..
#We allow anything in the resultant segments anyway (as %UXXXX... allows all)
#so it's not so much about what can be encoded,
# - but it makes it harder to reason about for users
# In particular - if we map %XX first it makes %25 -> % substitution tricky
# if the user requires a literal %UXXX - they can't do %25UXXX
# the double sub would make it %UXXX -> somechar anyway.
#we do unicode first - as a 2nd round of %XX substitutions is unlikely to interfere.
#There is still the opportunity to use things like %U00000025 followed by hex-chars
# and get some minor surprises, but using %U on ascii is unlikely to be done accidentally - REVIEW
set segment [decode_unicode_escapes $segment]
set segment [tcl::string::map $decode_map $segment]
lappend result_segments $segment
incr s
}
set targetpath [join $result_segments /]
if {$name eq ""} {
set name [lindex $result_segments end]
}
return [dict create name $name targetpath $targetpath]
}
variable map
#default exclusion of / (%U2f and equivs)
#this would allow obfuscation of intention - when we have + for that anyway
proc decode_unicode_escapes {str {exclusions {/ \n \r \x00}}} {
variable map
set ucstart [string first %U $str 0]
if {$ucstart < 0} {
return $str
}
set max 8
set map [list]
set strend [expr {[string length $str]-1}]
while {$ucstart >= 0} {
set s $ucstart
set i [expr {$s +2}] ;#skip the %U
set hex ""
while {[tcl::string::length $hex] < 8 && $i <= $strend} {
set in [string index $str $i]
if {[tcl::string::is xdigit -strict $in]} {
append hex $in
} else {
break
}
incr i
}
if {$hex ne ""} {
incr i -1
lappend map $s $i $hex
}
set ucstart [tcl::string::first %U $str $i]
}
set out ""
set lastidx -1
set e 0
foreach {s e hex} $map {
append out [string range $str $lastidx+1 $s-1]
set sub [format %c 0x$hex]
if {$sub in $exclusions} {
append out %U$hex ;#put it back
} else {
append out $sub
}
set lastidx $e
}
if {$e < [tcl::string::length $str]-1} {
append out [string range $str $e+1 end]
}
return $out
}
proc link_as {name target} {
}
#proc sample1 {p1 args} {
# #*** !doctools
# #[call [fun sample1] [arg p1] [opt {?option value...?}]]
# #[para]Description of sample1
# return "ok"
#}
#*** !doctools
#[list_end] [comment {--- end definitions namespace fauxlink ---}]
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# Secondary API namespace
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
namespace eval fauxlink::lib {
namespace export {[a-z]*}; # Convention: export all lowercase
namespace path [namespace parent]
#*** !doctools
#[subsection {Namespace fauxlink::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 fauxlink::lib ---}]
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[section Internal]
namespace eval fauxlink::system {
#*** !doctools
#[subsection {Namespace fauxlink::system}]
#[para] Internal functions that are not part of the API
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Ready
package provide fauxlink [namespace eval fauxlink {
variable pkg fauxlink
variable version
set version 0.1.0
}]
return
#*** !doctools
#[manpage_end]

1
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/include_modules.config

@ -7,6 +7,7 @@ set bootsupport_modules [list\
src/vendormodules cksum\ src/vendormodules cksum\
src/vendormodules modpod\ src/vendormodules modpod\
src/vendormodules overtype\ src/vendormodules overtype\
src/vendormodules fauxlink\
src/vendormodules oolib\ src/vendormodules oolib\
src/vendormodules http\ src/vendormodules http\
src/vendormodules dictutils\ src/vendormodules dictutils\

1
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/du-0.1.0.tm

@ -942,6 +942,7 @@ namespace eval punk::du {
#struct::set difference removes duplicates (but not always.. e.g if using tcl impl and 2nd element empty!) #struct::set difference removes duplicates (but not always.. e.g if using tcl impl and 2nd element empty!)
#relying on struct::set to remove dupes is somewhat risky. It is not well documented - and behaviour of dupes in inputs is underspecified as it appears to be intended for mathematical 'sets' #relying on struct::set to remove dupes is somewhat risky. It is not well documented - and behaviour of dupes in inputs is underspecified as it appears to be intended for mathematical 'sets'
#remove links and . .. from directories, remove links from files #remove links and . .. from directories, remove links from files
#ideally we would like to classify links by whether they point to files vs dirs - but there are enough cross-platform differences that we will have to leave it to the caller to sort out for now.
#struct::set will affect order: tcl vs critcl give different ordering! #struct::set will affect order: tcl vs critcl give different ordering!
set files [struct::set difference [concat $hfiles $files[unset files]] $links] set files [struct::set difference [concat $hfiles $files[unset files]] $links]
set dirs [struct::set difference [concat $hdirs $dirs[unset dirs]] [concat $links [list [file join $folderpath .] [file join $folderpath ..] ]]] set dirs [struct::set difference [concat $hdirs $dirs[unset dirs]] [concat $links [list [file join $folderpath .] [file join $folderpath ..] ]]]

45
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/lib-0.1.1.tm

@ -372,20 +372,40 @@ namespace eval punk::lib {
proc lswap {lvar a z} { proc lswap {lvar a z} {
upvar $lvar l upvar $lvar l
if {[lindex_resolve_basic $l $a] < 0 || [lindex_resolve_basic $l $z] < 0} { if {[lindex_resolve_basic $l $a] < 0 || [lindex_resolve_basic $l $z] < 0} {
#if we didn't do this check - we could raise an error on second lset - leaving list corrupted because only one lset occurred #lindex_resolve_basic returns only -1 if out of range
#if we didn't do this check - we could raise an error on second lset below - leaving list corrupted because only one lset occurred
#(e.g using: lswap mylist end-2 end on a two element list) #(e.g using: lswap mylist end-2 end on a two element list)
#on the unhapy path we can take time to check the nature of the out-of-boundness to give a nicer report #on the unhapy path we can take time to check the nature of the out-of-boundness to give a nicer report
#use full 'lindex_resolve' which can report which side via -3 and -2 special results being lower and upper bound breaches respectively (-1 never returned)
set a_index [lindex_resolve $l $a] set a_index [lindex_resolve $l $a]
set a_msg "" set a_msg ""
switch -- $a_index { switch -- $a_index {
-2 { -2 {
"$a is greater th set a_msg "1st supplied index $a is above the upper bound for the list ([llength $l])"
} }
-3 { -3 {
set a_msg "1st supplied index $a is below the lower bound for the list (0)"
} }
} }
error "lswap cannot indices $a and $z $a is out of range" set z_index [lindex_resolve $l $z]
set z_msg ""
switch -- $z_index {
-2 {
set z_msg "2nd supplied index $z is above the upper bound for the list ([llength $l])"
}
-3 {
set z_msg "2nd supplied index $z is below the lower bound for the list (0)"
}
}
set errmsg "lswap cannot swap indices $a and $z"
if {$a_msg ne ""} {
append errmsg \n $a_msg
}
if {$z_msg ne ""} {
append errmsg \n $z_msg
}
error $errmsg
} }
set item2 [lindex $l $z] set item2 [lindex $l $z]
lset l $z [lindex $l $a] lset l $z [lindex $l $a]
@ -397,6 +417,7 @@ namespace eval punk::lib {
# #if index a strictly less <= z we can do in one-liner for fun - but it's replacing whole list - so much slower # #if index a strictly less <= z we can do in one-liner for fun - but it's replacing whole list - so much slower
# set l [concat [lrange $l 0 $a-1] [lindex $l $z] [lrange $l $a+1 $z-1] [lindex $l $a] [lrange $l $z+1 end]] # set l [concat [lrange $l 0 $a-1] [lindex $l $z] [lrange $l $a+1 $z-1] [lindex $l $a] [lrange $l $z+1 end]]
#} #}
proc lswap2 {lvar a z} { proc lswap2 {lvar a z} {
upvar $lvar l upvar $lvar l
#if index a strictly less <= z we can do in one-liner for fun - but it's replacing whole list - so much slower #if index a strictly less <= z we can do in one-liner for fun - but it's replacing whole list - so much slower
@ -3021,6 +3042,10 @@ namespace eval punk::lib {
set localeid [twapi::get_system_default_lcid] set localeid [twapi::get_system_default_lcid]
} }
} }
#when using twapi we currently only get the localeid - not the specific defaults
#when not using twapi, or on non-windows platforms - we don't currently have a mechanism to look up user preferences for this
set default_delim ","
set default_groupsize 3
set results [list] set results [list]
set nums [objclone $numbers_or_commaformattednumbers] ;#stops single num from getting internal rep of list set nums [objclone $numbers_or_commaformattednumbers] ;#stops single num from getting internal rep of list
@ -3036,15 +3061,21 @@ namespace eval punk::lib {
lappend results [twapi::format_number $number $localeid -idigits -1] lappend results [twapi::format_number $number $localeid -idigits -1]
continue continue
} else { } else {
if {$delim eq ""} {set delim ","} #setting just one of delim or groupsize means we don't get the user's localeid based default for the non-set one
if {$groupsize eq ""} {set groupsize 3} #todo - document it? Find a way to lookup localeid based defaults whenever either is unspecified?
if {$delim eq ""} {set delim $default_delim}
if {$groupsize eq ""} {set groupsize $default_groupsize}
lappend results [twapi::format_number $number 0 -idigits -1 -sthousand $delim -sgrouping $groupsize] lappend results [twapi::format_number $number 0 -idigits -1 -sthousand $delim -sgrouping $groupsize]
continue continue
} }
} }
#todo - get configured user defaults #todo - get configured user defaults
set delim "," if {$delim eq ""} {
set groupsize 3 set delim $default_delim
}
if {$groupsize eq ""} {
set groupsize $default_groupsize
}
lappend results [delimit_number $number $delim $groupsize] lappend results [delimit_number $number $delim $groupsize]
} }

8
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/cli-0.3.1.tm

@ -692,6 +692,8 @@ namespace eval punk::mix::cli {
#punk::zip::mkzip stores permissions - (unix style) which confuses zipfs when reading - it misidentifies dirs as files #punk::zip::mkzip stores permissions - (unix style) which confuses zipfs when reading - it misidentifies dirs as files
} }
#zipfs mkzip does exactly what we need anyway in this case #zipfs mkzip does exactly what we need anyway in this case
#unfortunately it's not available in all Tclsh versions we might be running..
if {[llength [info commands zipfs]]} {
set wd [pwd] set wd [pwd]
cd $buildfolder cd $buildfolder
puts "zipfs mkzip $zipfile #modpod-$basename-$module_build_version" puts "zipfs mkzip $zipfile #modpod-$basename-$module_build_version"
@ -700,6 +702,12 @@ namespace eval punk::mix::cli {
package require modpod package require modpod
modpod::lib::make_zip_modpod $zipfile $modulefile modpod::lib::make_zip_modpod $zipfile $modulefile
} else {
#TODO - review punk::zip::mkzip and/or external zip to provide a fallback?
set had_err 1
lappend notest "zipfs_unavailable"
puts stderr "WARNING: zipfs unavailable can't build $modulefile"
}
if {$had_error} { if {$had_error} {

164
src/vendormodules/fauxlink-0.1.0.tm

@ -9,7 +9,7 @@
# @@ Meta Begin # @@ Meta Begin
# Application fauxlink 0.1.0 # Application fauxlink 0.1.0
# Meta platform tcl # Meta platform tcl
# Meta license <unspecified> # Meta license MIT
# @@ Meta End # @@ Meta End
@ -19,15 +19,55 @@
#*** !doctools #*** !doctools
#[manpage_begin fauxlink_module_fauxlink 0 0.1.0] #[manpage_begin fauxlink_module_fauxlink 0 0.1.0]
#[copyright "2024"] #[copyright "2024"]
#[titledesc {Module API}] [comment {-- Name section and table of contents description --}] #[titledesc {faux link application shortcuts}] [comment {-- Name section and table of contents description --}]
#[moddesc {-}] [comment {-- Description at end of page heading --}] #[moddesc {fauxlink .fxlnk}] [comment {-- Description at end of page heading --}]
#[require fauxlink] #[require fauxlink]
#[keywords module] #[keywords symlink faux fake shortcut toml]
#[description] #[description]
#[para] - #[para] A cross platform shortcut/symlink alternative.
#[para] Unapologetically ugly - but practical in certain circumstances.
#[para] A solution is required for application-driven filesystem links that survives cross platform moves as well as
#[para] archiving and packaging systems.
#[para] The target is specified in a minimally-encoded form in the filename itself - but still human readable.
#[para] format of name <nominalname>#<encodedtarget>.fxlnk
#[para] where <nominalname> can be empty - then the effective nominal name is the tail of the <encodedtarget>
#[para] The + symbol substitutes for forward-slashes.
#[para] Other chars can be encoded using url-like encoding - (but only up to %7E !)
#[para] We deliberately treat higher % sequences literally.
#[para] This means actual uri::urn encoded unicode sequences (e.g %E2%99%A5 [heart]) can remain literal for linking to urls.
#[para] e.g if an actual + or # is required in a filename or path segment they can be encoded as %2B & %23
#[para] e.g a link to a file file#A.txt in parent dir could be:
#[para] file%23A.txt#..+file%23A.txt.fxlnk
#[para] or equivalently (but obviously affecting sorting) #..+file%23A.txt.fxlnk
#[para] The <nominalname> can be unrelated to the actual target
#[para] e.g datafile.dat#..+file%23A.txt.fxlnk
#[para] This system has no filesystem support - and must be completely application driven.
#[para] This can be useful for example in application test packages which may be tarred or zipped and moved cross platform.
#[para] The target being fully specified in the name means the file doesn't have to be read for the target to be determined
#[para] Extensions to behaviour should be added in the file as text data in Toml format,
#[para] with custom data being under a single application-chosen table name
#[para] The toplevel Toml table [fauxlink] is reserved for core extensions to this system.
#[para] Aside from the 2 used for delimiting (+ #)
#[para] certain characters which might normally be allowed in filesystems are required to be encoded
#[para] e.g space and tab are required to be %20 %09
#[para] Others that require encoding are: * ? \ / | : ; " < >
#[para] The nul character in raw form, when detected, is always mapped away to the empty string - as very few filesystems support it.
#[para] Control characters and other punctuation is optional to encode.
#[para] Generally utf-8 should be used where possible and unicode characters left as is where possible on modern systems.
#[para] Where encoding of unicode is desired in the nominalname or encodedtarget portions it can be specified as %UXXXXXXXX
#[para] There must be between 1 and 8 X digits following the %U. Interpretation of chars following %U stops at the first non-hex character.
#[para] This means %Utest would not get any translation as there were no hex digits so it would come out as %Utest
#
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#https://learn.microsoft.com/en-us/troubleshoot/windows-client/networking/url-encoding-unc-paths-not-url-decoded
# ie "//server/c/Program files" works but "//server/c/Program%20Files" is now treated by windows as a literal path with %20 in it.
#Using fauxlink - a link would be:
# "my-program-files#++server+c+Program%20Files.fxlnk"
#If we needed the old-style literal %20 it would become
# "my-program-files#++server+c+Program%2520Files.fxlnk"
#*** !doctools #*** !doctools
#[section Overview] #[section Overview]
#[para] overview of fauxlink #[para] overview of fauxlink
@ -126,9 +166,13 @@ namespace eval fauxlink {
< %3C\ < %3C\
> %3E\ > %3E\
] ]
#above have some overlap with ctrl codes below.
#no big deal as it's a dict
#must_encode #must_encode
# + # * ? \ / | : ; " < > <sp> \t # + # * ? \ / | : ; " < > <sp> \t
# also NUL to empty string # also NUL to empty string
# also ctrl chars 01 to 1F (1..31) # also ctrl chars 01 to 1F (1..31)
for {set i 1} {$i < 32} {incr i} { for {set i 1} {$i < 32} {incr i} {
set ch [format %c $i] set ch [format %c $i]
@ -143,7 +187,17 @@ namespace eval fauxlink {
set must_encode [dict keys $encode_map] set must_encode [dict keys $encode_map]
set decode_map [dict create\ #if they are in
#decode map doesn't include
# %00 (nul)
# %2F "/"
# %2f "/"
# %7f (del)
#we exlude the forward slash because we already have + for that - and multiple ways to specify it obscure intention.
#
set decode_map [dict merge $decode_map [dict create\
%09 \t\
%20 { }\ %20 { }\
%21 "!"\ %21 "!"\
%22 {"}\ %22 {"}\
@ -160,8 +214,10 @@ namespace eval fauxlink {
%2b "+"\ %2b "+"\
%2C ","\ %2C ","\
%2c ","\ %2c ","\
%2F "/"\ %2D "-"\
%2f "/"\ %2d "-"\
%2E "."\
%2e "."\
%3A ":"\ %3A ":"\
%3a ":"\ %3a ":"\
%3B {;}\ %3B {;}\
@ -192,8 +248,9 @@ namespace eval fauxlink {
%7d "}"\ %7d "}"\
%7E "~"\ %7E "~"\
%7e "~"\ %7e "~"\
] ]]
#Don't go above 7f
#if we want to specify p
#*** !doctools #*** !doctools
@ -206,8 +263,8 @@ namespace eval fauxlink {
variable encode_map variable encode_map
variable must_encode variable must_encode
set ftail [file tail $link] set ftail [file tail $link]
if {[file extension $ftail] ne ".fauxlink"} { if {[file extension $ftail] ni [list .fxlnk .fauxlink]} {
error "fauxlink::resolve refusing to process link $link - file extension must be .fauxlink" error "fauxlink::resolve refusing to process link $link - file extension must be .fxlnk or .fauxlink"
} }
set linkspec [file rootname $ftail] set linkspec [file rootname $ftail]
# - any # or + within the target path or name should have been uri encoded as %23 and %2b # - any # or + within the target path or name should have been uri encoded as %23 and %2b
@ -229,13 +286,19 @@ namespace eval fauxlink {
foreach ch [split $namespec ""] { foreach ch [split $namespec ""] {
if {$ch in $must_encode} { if {$ch in $must_encode} {
set enc [dict get $encode_map $ch] set enc [dict get $encode_map $ch]
if {[dict exists $decode_map $enc]} {
append err " char $idx should be encoded as $enc" \n append err " char $idx should be encoded as $enc" \n
} else {
append err " no %xx encoding available. Use %UXX if really required" \n
}
} }
incr idx incr idx
} }
error $err error $err
} }
set name [tcl::string::map $decode_map $namespec] #see comments below regarding 2 rounds and ordering.
set name [decode_unicode_escapes $namespec]
set name [tcl::string::map $decode_map $name]
#puts stderr "-->name: $name" #puts stderr "-->name: $name"
set targetsegment [split $targetspec +] set targetsegment [split $targetspec +]
@ -250,20 +313,89 @@ namespace eval fauxlink {
foreach ch [split $segment ""] { foreach ch [split $segment ""] {
if {$ch in $must_encode} { if {$ch in $must_encode} {
set enc [dict get $encode_map $ch] set enc [dict get $encode_map $ch]
if {[dict exists $decode_map $enc]} {
append err " segment $s char $idx should be encoded as $enc" \n append err " segment $s char $idx should be encoded as $enc" \n
} else {
append err " no %xx encoding available. Use %UXX if really required" \n
}
} }
incr idx incr idx
} }
error $err error $err
} }
lappend result_segments [tcl::string::map $decode_map $segment] #2 rounds of substitution is possibly asking for trouble..
#We allow anything in the resultant segments anyway (as %UXXXX... allows all)
#so it's not so much about what can be encoded,
# - but it makes it harder to reason about for users
# In particular - if we map %XX first it makes %25 -> % substitution tricky
# if the user requires a literal %UXXX - they can't do %25UXXX
# the double sub would make it %UXXX -> somechar anyway.
#we do unicode first - as a 2nd round of %XX substitutions is unlikely to interfere.
#There is still the opportunity to use things like %U00000025 followed by hex-chars
# and get some minor surprises, but using %U on ascii is unlikely to be done accidentally - REVIEW
set segment [decode_unicode_escapes $segment]
set segment [tcl::string::map $decode_map $segment]
lappend result_segments $segment
incr s incr s
} }
set targetpath [join $result_segments /] set targetpath [join $result_segments /]
if {$name eq ""} {
set name [lindex $result_segments end]
}
return [dict create name $name targetpath $targetpath] return [dict create name $name targetpath $targetpath]
} }
variable map
#default exclusion of / (%U2f and equivs)
#this would allow obfuscation of intention - when we have + for that anyway
proc decode_unicode_escapes {str {exclusions {/ \n \r \x00}}} {
variable map
set ucstart [string first %U $str 0]
if {$ucstart < 0} {
return $str
}
set max 8
set map [list]
set strend [expr {[string length $str]-1}]
while {$ucstart >= 0} {
set s $ucstart
set i [expr {$s +2}] ;#skip the %U
set hex ""
while {[tcl::string::length $hex] < 8 && $i <= $strend} {
set in [string index $str $i]
if {[tcl::string::is xdigit -strict $in]} {
append hex $in
} else {
break
}
incr i
}
if {$hex ne ""} {
incr i -1
lappend map $s $i $hex
}
set ucstart [tcl::string::first %U $str $i]
}
set out ""
set lastidx -1
set e 0
foreach {s e hex} $map {
append out [string range $str $lastidx+1 $s-1]
set sub [format %c 0x$hex]
if {$sub in $exclusions} {
append out %U$hex ;#put it back
} else {
append out $sub
}
set lastidx $e
}
if {$e < [tcl::string::length $str]-1} {
append out [string range $str $e+1 end]
}
return $out
}
proc link_as {name target} { proc link_as {name target} {
} }

468
src/vfs/_vfscommon/modules/fauxlink-0.1.0.tm

@ -0,0 +1,468 @@
# -*- tcl -*-
# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from <pkg>-buildversion.txt
#
# 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 fauxlink 0.1.0
# Meta platform tcl
# Meta license MIT
# @@ Meta End
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# doctools header
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[manpage_begin fauxlink_module_fauxlink 0 0.1.0]
#[copyright "2024"]
#[titledesc {faux link application shortcuts}] [comment {-- Name section and table of contents description --}]
#[moddesc {fauxlink .fxlnk}] [comment {-- Description at end of page heading --}]
#[require fauxlink]
#[keywords symlink faux fake shortcut toml]
#[description]
#[para] A cross platform shortcut/symlink alternative.
#[para] Unapologetically ugly - but practical in certain circumstances.
#[para] A solution is required for application-driven filesystem links that survives cross platform moves as well as
#[para] archiving and packaging systems.
#[para] The target is specified in a minimally-encoded form in the filename itself - but still human readable.
#[para] format of name <nominalname>#<encodedtarget>.fxlnk
#[para] where <nominalname> can be empty - then the effective nominal name is the tail of the <encodedtarget>
#[para] The + symbol substitutes for forward-slashes.
#[para] Other chars can be encoded using url-like encoding - (but only up to %7E !)
#[para] We deliberately treat higher % sequences literally.
#[para] This means actual uri::urn encoded unicode sequences (e.g %E2%99%A5 [heart]) can remain literal for linking to urls.
#[para] e.g if an actual + or # is required in a filename or path segment they can be encoded as %2B & %23
#[para] e.g a link to a file file#A.txt in parent dir could be:
#[para] file%23A.txt#..+file%23A.txt.fxlnk
#[para] or equivalently (but obviously affecting sorting) #..+file%23A.txt.fxlnk
#[para] The <nominalname> can be unrelated to the actual target
#[para] e.g datafile.dat#..+file%23A.txt.fxlnk
#[para] This system has no filesystem support - and must be completely application driven.
#[para] This can be useful for example in application test packages which may be tarred or zipped and moved cross platform.
#[para] The target being fully specified in the name means the file doesn't have to be read for the target to be determined
#[para] Extensions to behaviour should be added in the file as text data in Toml format,
#[para] with custom data being under a single application-chosen table name
#[para] The toplevel Toml table [fauxlink] is reserved for core extensions to this system.
#[para] Aside from the 2 used for delimiting (+ #)
#[para] certain characters which might normally be allowed in filesystems are required to be encoded
#[para] e.g space and tab are required to be %20 %09
#[para] Others that require encoding are: * ? \ / | : ; " < >
#[para] The nul character in raw form, when detected, is always mapped away to the empty string - as very few filesystems support it.
#[para] Control characters and other punctuation is optional to encode.
#[para] Generally utf-8 should be used where possible and unicode characters left as is where possible on modern systems.
#[para] Where encoding of unicode is desired in the nominalname or encodedtarget portions it can be specified as %UXXXXXXXX
#[para] There must be between 1 and 8 X digits following the %U. Interpretation of chars following %U stops at the first non-hex character.
#[para] This means %Utest would not get any translation as there were no hex digits so it would come out as %Utest
#
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#https://learn.microsoft.com/en-us/troubleshoot/windows-client/networking/url-encoding-unc-paths-not-url-decoded
# ie "//server/c/Program files" works but "//server/c/Program%20Files" is now treated by windows as a literal path with %20 in it.
#Using fauxlink - a link would be:
# "my-program-files#++server+c+Program%20Files.fxlnk"
#If we needed the old-style literal %20 it would become
# "my-program-files#++server+c+Program%2520Files.fxlnk"
#*** !doctools
#[section Overview]
#[para] overview of fauxlink
#[subsection Concepts]
#[para] -
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Requirements
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[subsection dependencies]
#[para] packages used by fauxlink
#[list_begin itemized]
package require Tcl 8.6-
#*** !doctools
#[item] [package {Tcl 8.6-}]
# #package require frobz
# #*** !doctools
# #[item] [package {frobz}]
#*** !doctools
#[list_end]
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[section API]
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# oo::class namespace
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
namespace eval fauxlink::class {
#*** !doctools
#[subsection {Namespace fauxlink::class}]
#[para] class definitions
if {[info commands [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
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
namespace eval fauxlink {
namespace export {[a-z]*}; # Convention: export all lowercase
#todo - enforce utf-8
#literal unicode chars supported by modern filesystems - leave as is - REVIEW
variable encode_map
variable decode_map
#most filesystems don't allow NULL - map to empty string
#Make sure % is not in encode_map
set encode_map [dict create\
\x00 ""\
{ } %20\
\t %09\
+ %2B\
# %23\
* %2A\
? %3F\
\\ %5C\
/ %2F\
| %7C\
: %3A\
{;} %3B\
{"} %22\
< %3C\
> %3E\
]
#above have some overlap with ctrl codes below.
#no big deal as it's a dict
#must_encode
# + # * ? \ / | : ; " < > <sp> \t
# also NUL to empty string
# also ctrl chars 01 to 1F (1..31)
for {set i 1} {$i < 32} {incr i} {
set ch [format %c $i]
set enc "%[format %02X $i]"
set enc_lower [string tolower $enc]
dict set encode_map $ch $enc
dict set decode_map $enc $ch
dict set decode_map $enc_lower $ch
}
variable must_encode
set must_encode [dict keys $encode_map]
#if they are in
#decode map doesn't include
# %00 (nul)
# %2F "/"
# %2f "/"
# %7f (del)
#we exlude the forward slash because we already have + for that - and multiple ways to specify it obscure intention.
#
set decode_map [dict merge $decode_map [dict create\
%09 \t\
%20 { }\
%21 "!"\
%22 {"}\
%23 "#"\
%24 "$"\
%25 "%"\
%26 "&"\
%27 "'"\
%28 "("\
%29 ")"\
%2A "*"\
%2a "*"\
%2B "+"\
%2b "+"\
%2C ","\
%2c ","\
%2D "-"\
%2d "-"\
%2E "."\
%2e "."\
%3A ":"\
%3a ":"\
%3B {;}\
%3b {;}\
%3D "="\
%3C "<"\
%3c "<"\
%3d "="\
%3E ">"\
%3e ">"\
%3F "?"\
%3f "?"\
%40 "@"\
%5B "\["\
%5b "\["\
%5C "\\"\
%5c "\\"\
%5D "\]"\
%5d "\]"\
%5E "^"\
%5e "^"\
%60 "`"\
%7B "{"\
%7b "{"\
%7C "|"\
%7c "|"\
%7D "}"\
%7d "}"\
%7E "~"\
%7e "~"\
]]
#Don't go above 7f
#if we want to specify p
#*** !doctools
#[subsection {Namespace fauxlink}]
#[para] Core API functions for fauxlink
#[list_begin definitions]
proc resolve {link} {
variable decode_map
variable encode_map
variable must_encode
set ftail [file tail $link]
if {[file extension $ftail] ni [list .fxlnk .fauxlink]} {
error "fauxlink::resolve refusing to process link $link - file extension must be .fxlnk or .fauxlink"
}
set linkspec [file rootname $ftail]
# - any # or + within the target path or name should have been uri encoded as %23 and %2b
if {[tcl::string::first # $linkspec] < 0} {
error "fauxlink::resolve error. Link must contain a # (usually at start if name matches target)"
}
#only the 1st 2 parts of split on # are significant.
#if there are more # chars present - the subsequent parts are effectively a comment
#check namepec already has required chars encoded
lassign [split $linkspec #] namespec targetspec
#puts stderr "-->namespec $namespec"
set nametest [tcl::string::map $encode_map $namespec]
#puts stderr "-->nametest $nametest"
#nothing should be changed - if there are unencoded chars that must be encoded it is an error
if {[tcl::string::length $nametest] ne [tcl::string::length $namespec]} {
set err "fauxlink::resolve invalid chars in name part (section prior to first #)"
set idx 0
foreach ch [split $namespec ""] {
if {$ch in $must_encode} {
set enc [dict get $encode_map $ch]
if {[dict exists $decode_map $enc]} {
append err " char $idx should be encoded as $enc" \n
} else {
append err " no %xx encoding available. Use %UXX if really required" \n
}
}
incr idx
}
error $err
}
#see comments below regarding 2 rounds and ordering.
set name [decode_unicode_escapes $namespec]
set name [tcl::string::map $decode_map $name]
#puts stderr "-->name: $name"
set targetsegment [split $targetspec +]
#check each + delimited part of targetspec already has required chars encoded
set s 0 ;#segment index
set result_segments [list]
foreach segment $targetsegment {
set targettest [tcl::string::map $encode_map $segment]
if {[tcl::string::length $targettest] ne [tcl::string::length $segment]} {
set err "fauxlink::resolve invalid chars in targetpath (section following first #)"
set idx 0
foreach ch [split $segment ""] {
if {$ch in $must_encode} {
set enc [dict get $encode_map $ch]
if {[dict exists $decode_map $enc]} {
append err " segment $s char $idx should be encoded as $enc" \n
} else {
append err " no %xx encoding available. Use %UXX if really required" \n
}
}
incr idx
}
error $err
}
#2 rounds of substitution is possibly asking for trouble..
#We allow anything in the resultant segments anyway (as %UXXXX... allows all)
#so it's not so much about what can be encoded,
# - but it makes it harder to reason about for users
# In particular - if we map %XX first it makes %25 -> % substitution tricky
# if the user requires a literal %UXXX - they can't do %25UXXX
# the double sub would make it %UXXX -> somechar anyway.
#we do unicode first - as a 2nd round of %XX substitutions is unlikely to interfere.
#There is still the opportunity to use things like %U00000025 followed by hex-chars
# and get some minor surprises, but using %U on ascii is unlikely to be done accidentally - REVIEW
set segment [decode_unicode_escapes $segment]
set segment [tcl::string::map $decode_map $segment]
lappend result_segments $segment
incr s
}
set targetpath [join $result_segments /]
if {$name eq ""} {
set name [lindex $result_segments end]
}
return [dict create name $name targetpath $targetpath]
}
variable map
#default exclusion of / (%U2f and equivs)
#this would allow obfuscation of intention - when we have + for that anyway
proc decode_unicode_escapes {str {exclusions {/ \n \r \x00}}} {
variable map
set ucstart [string first %U $str 0]
if {$ucstart < 0} {
return $str
}
set max 8
set map [list]
set strend [expr {[string length $str]-1}]
while {$ucstart >= 0} {
set s $ucstart
set i [expr {$s +2}] ;#skip the %U
set hex ""
while {[tcl::string::length $hex] < 8 && $i <= $strend} {
set in [string index $str $i]
if {[tcl::string::is xdigit -strict $in]} {
append hex $in
} else {
break
}
incr i
}
if {$hex ne ""} {
incr i -1
lappend map $s $i $hex
}
set ucstart [tcl::string::first %U $str $i]
}
set out ""
set lastidx -1
set e 0
foreach {s e hex} $map {
append out [string range $str $lastidx+1 $s-1]
set sub [format %c 0x$hex]
if {$sub in $exclusions} {
append out %U$hex ;#put it back
} else {
append out $sub
}
set lastidx $e
}
if {$e < [tcl::string::length $str]-1} {
append out [string range $str $e+1 end]
}
return $out
}
proc link_as {name target} {
}
#proc sample1 {p1 args} {
# #*** !doctools
# #[call [fun sample1] [arg p1] [opt {?option value...?}]]
# #[para]Description of sample1
# return "ok"
#}
#*** !doctools
#[list_end] [comment {--- end definitions namespace fauxlink ---}]
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# Secondary API namespace
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
namespace eval fauxlink::lib {
namespace export {[a-z]*}; # Convention: export all lowercase
namespace path [namespace parent]
#*** !doctools
#[subsection {Namespace fauxlink::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 fauxlink::lib ---}]
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[section Internal]
namespace eval fauxlink::system {
#*** !doctools
#[subsection {Namespace fauxlink::system}]
#[para] Internal functions that are not part of the API
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Ready
package provide fauxlink [namespace eval fauxlink {
variable pkg fauxlink
variable version
set version 0.1.0
}]
return
#*** !doctools
#[manpage_end]

1
src/vfs/_vfscommon/modules/punk/du-0.1.0.tm

@ -942,6 +942,7 @@ namespace eval punk::du {
#struct::set difference removes duplicates (but not always.. e.g if using tcl impl and 2nd element empty!) #struct::set difference removes duplicates (but not always.. e.g if using tcl impl and 2nd element empty!)
#relying on struct::set to remove dupes is somewhat risky. It is not well documented - and behaviour of dupes in inputs is underspecified as it appears to be intended for mathematical 'sets' #relying on struct::set to remove dupes is somewhat risky. It is not well documented - and behaviour of dupes in inputs is underspecified as it appears to be intended for mathematical 'sets'
#remove links and . .. from directories, remove links from files #remove links and . .. from directories, remove links from files
#ideally we would like to classify links by whether they point to files vs dirs - but there are enough cross-platform differences that we will have to leave it to the caller to sort out for now.
#struct::set will affect order: tcl vs critcl give different ordering! #struct::set will affect order: tcl vs critcl give different ordering!
set files [struct::set difference [concat $hfiles $files[unset files]] $links] set files [struct::set difference [concat $hfiles $files[unset files]] $links]
set dirs [struct::set difference [concat $hdirs $dirs[unset dirs]] [concat $links [list [file join $folderpath .] [file join $folderpath ..] ]]] set dirs [struct::set difference [concat $hdirs $dirs[unset dirs]] [concat $links [list [file join $folderpath .] [file join $folderpath ..] ]]]

45
src/vfs/_vfscommon/modules/punk/lib-0.1.1.tm

@ -372,20 +372,40 @@ namespace eval punk::lib {
proc lswap {lvar a z} { proc lswap {lvar a z} {
upvar $lvar l upvar $lvar l
if {[lindex_resolve_basic $l $a] < 0 || [lindex_resolve_basic $l $z] < 0} { if {[lindex_resolve_basic $l $a] < 0 || [lindex_resolve_basic $l $z] < 0} {
#if we didn't do this check - we could raise an error on second lset - leaving list corrupted because only one lset occurred #lindex_resolve_basic returns only -1 if out of range
#if we didn't do this check - we could raise an error on second lset below - leaving list corrupted because only one lset occurred
#(e.g using: lswap mylist end-2 end on a two element list) #(e.g using: lswap mylist end-2 end on a two element list)
#on the unhapy path we can take time to check the nature of the out-of-boundness to give a nicer report #on the unhapy path we can take time to check the nature of the out-of-boundness to give a nicer report
#use full 'lindex_resolve' which can report which side via -3 and -2 special results being lower and upper bound breaches respectively (-1 never returned)
set a_index [lindex_resolve $l $a] set a_index [lindex_resolve $l $a]
set a_msg "" set a_msg ""
switch -- $a_index { switch -- $a_index {
-2 { -2 {
"$a is greater th set a_msg "1st supplied index $a is above the upper bound for the list ([llength $l])"
} }
-3 { -3 {
set a_msg "1st supplied index $a is below the lower bound for the list (0)"
} }
} }
error "lswap cannot indices $a and $z $a is out of range" set z_index [lindex_resolve $l $z]
set z_msg ""
switch -- $z_index {
-2 {
set z_msg "2nd supplied index $z is above the upper bound for the list ([llength $l])"
}
-3 {
set z_msg "2nd supplied index $z is below the lower bound for the list (0)"
}
}
set errmsg "lswap cannot swap indices $a and $z"
if {$a_msg ne ""} {
append errmsg \n $a_msg
}
if {$z_msg ne ""} {
append errmsg \n $z_msg
}
error $errmsg
} }
set item2 [lindex $l $z] set item2 [lindex $l $z]
lset l $z [lindex $l $a] lset l $z [lindex $l $a]
@ -397,6 +417,7 @@ namespace eval punk::lib {
# #if index a strictly less <= z we can do in one-liner for fun - but it's replacing whole list - so much slower # #if index a strictly less <= z we can do in one-liner for fun - but it's replacing whole list - so much slower
# set l [concat [lrange $l 0 $a-1] [lindex $l $z] [lrange $l $a+1 $z-1] [lindex $l $a] [lrange $l $z+1 end]] # set l [concat [lrange $l 0 $a-1] [lindex $l $z] [lrange $l $a+1 $z-1] [lindex $l $a] [lrange $l $z+1 end]]
#} #}
proc lswap2 {lvar a z} { proc lswap2 {lvar a z} {
upvar $lvar l upvar $lvar l
#if index a strictly less <= z we can do in one-liner for fun - but it's replacing whole list - so much slower #if index a strictly less <= z we can do in one-liner for fun - but it's replacing whole list - so much slower
@ -3021,6 +3042,10 @@ namespace eval punk::lib {
set localeid [twapi::get_system_default_lcid] set localeid [twapi::get_system_default_lcid]
} }
} }
#when using twapi we currently only get the localeid - not the specific defaults
#when not using twapi, or on non-windows platforms - we don't currently have a mechanism to look up user preferences for this
set default_delim ","
set default_groupsize 3
set results [list] set results [list]
set nums [objclone $numbers_or_commaformattednumbers] ;#stops single num from getting internal rep of list set nums [objclone $numbers_or_commaformattednumbers] ;#stops single num from getting internal rep of list
@ -3036,15 +3061,21 @@ namespace eval punk::lib {
lappend results [twapi::format_number $number $localeid -idigits -1] lappend results [twapi::format_number $number $localeid -idigits -1]
continue continue
} else { } else {
if {$delim eq ""} {set delim ","} #setting just one of delim or groupsize means we don't get the user's localeid based default for the non-set one
if {$groupsize eq ""} {set groupsize 3} #todo - document it? Find a way to lookup localeid based defaults whenever either is unspecified?
if {$delim eq ""} {set delim $default_delim}
if {$groupsize eq ""} {set groupsize $default_groupsize}
lappend results [twapi::format_number $number 0 -idigits -1 -sthousand $delim -sgrouping $groupsize] lappend results [twapi::format_number $number 0 -idigits -1 -sthousand $delim -sgrouping $groupsize]
continue continue
} }
} }
#todo - get configured user defaults #todo - get configured user defaults
set delim "," if {$delim eq ""} {
set groupsize 3 set delim $default_delim
}
if {$groupsize eq ""} {
set groupsize $default_groupsize
}
lappend results [delimit_number $number $delim $groupsize] lappend results [delimit_number $number $delim $groupsize]
} }

8
src/vfs/_vfscommon/modules/punk/mix/cli-0.3.1.tm

@ -692,6 +692,8 @@ namespace eval punk::mix::cli {
#punk::zip::mkzip stores permissions - (unix style) which confuses zipfs when reading - it misidentifies dirs as files #punk::zip::mkzip stores permissions - (unix style) which confuses zipfs when reading - it misidentifies dirs as files
} }
#zipfs mkzip does exactly what we need anyway in this case #zipfs mkzip does exactly what we need anyway in this case
#unfortunately it's not available in all Tclsh versions we might be running..
if {[llength [info commands zipfs]]} {
set wd [pwd] set wd [pwd]
cd $buildfolder cd $buildfolder
puts "zipfs mkzip $zipfile #modpod-$basename-$module_build_version" puts "zipfs mkzip $zipfile #modpod-$basename-$module_build_version"
@ -700,6 +702,12 @@ namespace eval punk::mix::cli {
package require modpod package require modpod
modpod::lib::make_zip_modpod $zipfile $modulefile modpod::lib::make_zip_modpod $zipfile $modulefile
} else {
#TODO - review punk::zip::mkzip and/or external zip to provide a fallback?
set had_err 1
lappend notest "zipfs_unavailable"
puts stderr "WARNING: zipfs unavailable can't build $modulefile"
}
if {$had_error} { if {$had_error} {

6
src/vfs/_vfscommon/modules/punk/mix/commandset/project-0.1.0.tm

@ -157,6 +157,9 @@ namespace eval punk::mix::commandset::project {
set opt_force [dict get $opts -force] set opt_force [dict get $opts -force]
set opt_confirm [string tolower [dict get $opts -confirm]] set opt_confirm [string tolower [dict get $opts -confirm]]
# -- --- --- --- --- --- --- --- --- --- --- --- --- # -- --- --- --- --- --- --- --- --- --- --- --- ---
set opt_layout [dict get $opts -layout]
set opt_update [dict get $opts -update]
# -- --- --- --- --- --- --- --- --- --- --- --- ---
set opt_modules [dict get $opts -modules] set opt_modules [dict get $opts -modules]
if {[llength $opt_modules] == 1 && [lindex $opt_modules 0] eq "\uFFFF"} { if {[llength $opt_modules] == 1 && [lindex $opt_modules 0] eq "\uFFFF"} {
#if not specified - add a single module matching project name #if not specified - add a single module matching project name
@ -169,9 +172,6 @@ namespace eval punk::mix::commandset::project {
} }
} }
# -- --- --- --- --- --- --- --- --- --- --- --- --- # -- --- --- --- --- --- --- --- --- --- --- --- ---
set opt_layout [dict get $opts -layout]
set opt_update [dict get $opts -update]
# -- --- --- --- --- --- --- --- --- --- --- --- ---
#todo - install support binaries on a per-project basis in a way that doesn't impact machine (e.g not added to path) - cache in user config dir if possible, supply mechanism to clear cache #todo - install support binaries on a per-project basis in a way that doesn't impact machine (e.g not added to path) - cache in user config dir if possible, supply mechanism to clear cache

116
src/vfs/_vfscommon/modules/punk/nav/fs-0.1.0.tm

@ -205,12 +205,13 @@ tcl::namespace::eval punk::nav::fs {
} }
set dircount [llength [dict get $matchinfo dirs]] set dircount [llength [dict get $matchinfo dirs]]
set filecount [llength [dict get $matchinfo files]] set filecount [llength [dict get $matchinfo files]]
set symlinkcount [llength [dict get $matchinfo links]] ;#doesn't include windows shelllinks (.lnk)
#set location [file normalize [dict get $matchinfo location]] #set location [file normalize [dict get $matchinfo location]]
set location [dict get $matchinfo location] set location [dict get $matchinfo location]
#result for glob is count of matches - use dirfiles etc for script access to results #result for glob is count of matches - use dirfiles etc for script access to results
set result [list location $location dircount $dircount filecount $filecount] set result [list location $location dircount $dircount filecount $filecount symlinks $symlinkcount]
set filesizes [dict get $matchinfo filesizes] set filesizes [dict get $matchinfo filesizes]
if {[llength $filesizes]} { if {[llength $filesizes]} {
set filesizes [lsearch -all -inline -not $filesizes na] set filesizes [lsearch -all -inline -not $filesizes na]
@ -383,6 +384,7 @@ tcl::namespace::eval punk::nav::fs {
set location [file normalize [dict get $matchinfo location]] set location [file normalize [dict get $matchinfo location]]
if {[string match //xzipfs:/* $location] || $location ne $last_location} { if {[string match //xzipfs:/* $location] || $location ne $last_location} {
#REVIEW - zipfs test disabled with leading x
#emit previous result #emit previous result
if {[dict size $this_result]} { if {[dict size $this_result]} {
dict set this_result filebytes [punk::lib::format_number [dict get $this_result filebytes]] dict set this_result filebytes [punk::lib::format_number [dict get $this_result filebytes]]
@ -1035,9 +1037,6 @@ tcl::namespace::eval punk::nav::fs {
lappend nonportable {*}[dict get $contents nonportable] ;# illegal file/folder names from windows perspective lappend nonportable {*}[dict get $contents nonportable] ;# illegal file/folder names from windows perspective
lappend vfsmounts {*}[dict get $contents vfsmounts] lappend vfsmounts {*}[dict get $contents vfsmounts]
} }
if {$opt_formatsizes} {
set filesizes [punk::lib::format_number $filesizes]
}
if {$opt_stripbase && $common_base ne ""} { if {$opt_stripbase && $common_base ne ""} {
set filetails [list] set filetails [list]
@ -1049,33 +1048,124 @@ tcl::namespace::eval punk::nav::fs {
} }
set $fileset $stripped set $fileset $stripped
} }
#Note: we need to remember to use common_base to rebuild the key when we need to query the dict-based elements: sizes & times - because we didn't strip those keys.
}
# -- --- --- --- --- --- --- --- --- --- ---
#assign symlinks to the dirs or files collection (the punk::du system doesn't sort this out
#As at 2024-09 for windows symlinks - Tcl can't do file readlink on symlinks created with mklink /D name target (SYMLINKD) or mklink name target (SYMLINK)
#We can't read the target information - best we can do is classify it as a file or a dir
#we can't use 'file type' as that will report just 'link' - but file isfile and file isdirectory work and should work for links on all platforms - REVIEW
set file_symlinks [list]
set dir_symlinks [list]
set dir_shortcuts [list] ;#windows shell links (.lnk) that have a target that is a directory
foreach s $links {
if {[file isfile $s]} {
lappend file_symlinks $s
#will be appended in finfo_plus later
} elseif {[file isdirectory $s]} {
lappend dir_symlinks $s
lappend dirs $s
} else {
#dunno - warn for now
puts stderr "Warning - cannot determine link type for link $s"
} }
}
#we now have the issue that our symlinks aren't sorted within the dir/file categorisation - they currently will have to appear at beginning or end - TODO
# -- --- --- --- --- --- --- --- --- --- ---
#todo - sort whilst maintaining order for metadata? #todo - sort whilst maintaining order for metadata?
#we need to co-sort files only with filesizes (other info such as times is keyed on fname so cosorting not required)
#we can't sort on filesize after format_number (unless we were to enforce delim _ which we don't want to do)
if {$opt_formatsizes} {
set filesizes [punk::lib::format_number $filesizes] ;#accepts a list and will process each
}
#col2 with subcolumns #col2 with subcolumns
#remove punk::pipedata dependency - allow use of punk::nav::fs without punk package #remove punk::pipedata dependency - allow use of punk::nav::fs without punk package
#set widest2a [punk::pipedata [list {*}$files ""] {lmap v $data {string length $v}} {tcl::mathfunc::max {*}$data}] #set widest2a [punk::pipedata [list {*}$files ""] {lmap v $data {string length $v}} {tcl::mathfunc::max {*}$data}]
#widest2a.= concat $files [list ""] |> .=>2 lmap v {string length $v} |> .=>* tcl::mathfunc::max #widest2a.= concat $files [list ""] |> .=>2 lmap v {string length $v} |> .=>* tcl::mathfunc::max
set widest2a [tcl::mathfunc::max {*}[lmap v [concat $files [list ""]] {string length $v}]] set widest2a [tcl::mathfunc::max {*}[lmap v [list {*}$files {*}$file_symlinks ""] {string length $v}]]
set c2a [string repeat " " [expr {$widest2a + 1}]] set c2a [string repeat " " [expr {$widest2a + 1}]]
#set widest2b [punk::pipedata [list {*}$filesizes ""] {lmap v $data {string length $v}} {tcl::mathfunc::max {*}$data}] #set widest2b [punk::pipedata [list {*}$filesizes ""] {lmap v $data {string length $v}} {tcl::mathfunc::max {*}$data}]
set widest2b [tcl::mathfunc::max {*}[lmap v [concat $filesizes [list ""]] {string length $v}]] set widest2b [tcl::mathfunc::max {*}[lmap v [list {*}$filesizes ""] {string length $v}]]
set c2b [string repeat " " [expr {$widest2b + 1}]] set c2b [string repeat " " [expr {$widest2b + 1}]]
set finfo [list] set finfo [list]
foreach f $files s $filesizes { foreach f $files s $filesizes {
#note - the display entry isn't necessarily a valid tcl list e.g filename with unbalanced curly braces #note - the display entry isn't necessarily a valid tcl list e.g filename with unbalanced curly braces
#hence we need to keep the filename, as well properly protected as a list element #hence we need to keep the filename as well, properly protected as a list element
lappend finfo [list file $f display "[overtype::left $c2a $f] [overtype::right $c2b $s]"] lappend finfo [list file $f display "[overtype::left $c2a $f] [overtype::right $c2b $s]"]
} }
set flink_style [punk::ansi::a+ undercurly underline undt-green] ;#curly green underline with fallback to normal underline
set dlink_style [punk::ansi::a+ undercurly underline undt-green]
#We use an underline so the visual styling of a link can coexist with fg/bg colors applied for other attributes such as hidden
foreach flink $file_symlinks {
lappend finfo [list file $flink display "$flink_style[overtype::left $c2a $flink] [overtype::right $c2b 0]"]
}
set fshortcut_style [punk::ansi::a+ underdotted underline undt-hotpink]
set dshortcut_style [punk::ansi::a+ underdotted underline undt-hotpink]
#examine windows .lnk shell link files (shortcuts) - these could be encountered on other platforms too - we should still be able to read them
#review - symlink to shortcut? hopefully will just work
#classify as file or directory - fallback to file if unknown/undeterminable
set finfo_plus [list]
foreach fdict $finfo {
set fname [dict get $fdict file]
if {[file extension $fname] eq ".lnk"} {
if {![catch {package require punk::winlnk}]} {
set shortcutinfo [punk::winlnk::file_get_info $fname]
set target_type "file" ;#default/fallback
if {[dict exists $shortcutinfo link_target]} {
set tgt [dict get $shortcutinfo link_target]
if {[file exists $tgt]} {
#file type could return 'link' - we will use ifile/isdirectory
if {[file isfile $tgt]} {
set target_type file
} elseif {[file isdirectory $tgt]} {
set target_type directory
} else {
set target_type file ;## ?
}
} else {
#todo - see if punk::winlnk has info about the type at the time of linking
#for now - treat as file
}
}
switch -- $target_type {
file {
set display [dict get $fdict display]
set display $fshortcut_style$display ;#
dict set fdict display $display
lappend finfo_plus $fdict
}
directory {
#target of link is a dir - for display/categorisation purposes we want to see it as a dir
#will be styled later based on membership of dir_shortcuts
lappend dirs $fname
lappend dir_shortcuts $fname
}
}
}
#if we don't have punk::winlnk to read the .lnk - it will get no special highlighting and just appear as an ordinary file even if it points to a dir
} else {
lappend finfo_plus $fdict
}
}
unset finfo
#set widest1 [punk::pipedata [list {*}$dirs ""] {lmap v $data {string length $v}} {tcl::mathfunc::max {*}$data}] #set widest1 [punk::pipedata [list {*}$dirs ""] {lmap v $data {string length $v}} {tcl::mathfunc::max {*}$data}]
set widest1 [tcl::mathfunc::max {*}[lmap v [concat $dirs [list ""]] {string length $v}]] set widest1 [tcl::mathfunc::max {*}[lmap v [concat $dirs [list ""]] {string length $v}]]
set displaylist [list] set displaylist [list]
set col1 [string repeat " " [expr {$widest1 + 2}]] set col1 [string repeat " " [expr {$widest1 + 2}]]
foreach d $dirs filerec $finfo { set RST [punk::ansi::a]
foreach d $dirs filerec $finfo_plus {
set d1 [punk::ansi::a+ cyan bold] set d1 [punk::ansi::a+ cyan bold]
set d2 [punk::ansi::a+ defaultfg defaultbg normal] set d2 [punk::ansi::a+ defaultfg defaultbg normal]
#set f1 [punk::ansi::a+ white bold] #set f1 [punk::ansi::a+ white bold]
@ -1088,7 +1178,7 @@ tcl::namespace::eval punk::nav::fs {
} }
if {$d in $vfsmounts} { if {$d in $vfsmounts} {
if {$d in $flaggedhidden} { if {$d in $flaggedhidden} {
#we could have a hidden dir which is also a vfs.. color will be overridden giving no indicatio of 'hidden' status - REVIEW #we could have a hidden dir which is also a vfs.. colour will be overridden giving no indicatio of 'hidden' status - REVIEW
#(This situation encountered on windows - even though file attr showed -hidden 0 - the glob with -types hidden returned it.. possibly a tcl glob bug on windows) #(This situation encountered on windows - even though file attr showed -hidden 0 - the glob with -types hidden returned it.. possibly a tcl glob bug on windows)
#mark it differently for now.. (todo bug report?) #mark it differently for now.. (todo bug report?)
if {$d in $nonportable} { if {$d in $nonportable} {
@ -1108,6 +1198,12 @@ tcl::namespace::eval punk::nav::fs {
set d1 [punk::ansi::a+ red bold] set d1 [punk::ansi::a+ red bold]
} }
} }
#dlink-style & dshortcut_style are for underlines - can be added with colours already set
if {$d in $dir_symlinks} {
append d1 $dlink_style
} elseif {$d in $dir_shortcuts} {
append d1 $dshortcut_style
}
} }
if {[llength $filerec]} { if {[llength $filerec]} {
set fname [dict get $filerec file] set fname [dict get $filerec file]
@ -1120,7 +1216,7 @@ tcl::namespace::eval punk::nav::fs {
} }
} }
} }
lappend displaylist $d1[overtype::left $col1 $d]$d2$f1$fdisp$f2 lappend displaylist [overtype::left $col1 $d1$d$RST]$f1$fdisp$RST
} }
return [punk::lib::list_as_lines $displaylist] return [punk::lib::list_as_lines $displaylist]

68
src/vfs/_vfscommon/modules/punk/ns-0.1.0.tm

@ -1707,6 +1707,7 @@ tcl::namespace::eval punk::ns {
lassign [internal::get_run_opts {-vars -nowarnings} {} $args] _r runopts _c cmdargs lassign [internal::get_run_opts {-vars -nowarnings} {} $args] _r runopts _c cmdargs
set use_vars [expr {"-vars" in $runopts}] set use_vars [expr {"-vars" in $runopts}]
set no_warnings [expr {"-nowarnings" in $runopts}] set no_warnings [expr {"-nowarnings" in $runopts}]
set ver ""
#todo support leading solo flags such as -capture to control whether we do a static capture of local vars in the ns #todo support leading solo flags such as -capture to control whether we do a static capture of local vars in the ns
@ -1717,15 +1718,68 @@ tcl::namespace::eval punk::ns {
} }
default { default {
if {[string match ::* $pkg_or_existing_ns]} { if {[string match ::* $pkg_or_existing_ns]} {
set pkg_unqualified [string range $pkg_or_existing_ns 2 end]
if {![tcl::namespace::exists $pkg_or_existing_ns]} { if {![tcl::namespace::exists $pkg_or_existing_ns]} {
set ver [package require [string range $pkg_or_existing_ns 2 end]] set ver [package require $pkg_unqualified]
} else { } else {
set ver "" set ver ""
} }
set ns $pkg_or_existing_ns set ns $pkg_or_existing_ns
} else { } else {
set ver [package require $pkg_or_existing_ns] set pkg_unqualified $pkg_or_existing_ns
set ns ::$pkg_or_existing_ns set ver [package require $pkg_unqualified]
set ns ::$pkg_unqualified
}
#some packages don't create their namespace immediately and/or don't populate it with commands and instead put entries in ::auto_index
set previous_command_count 0
if {[namespace exists $ns]} {
set previous_command_count [llength [info commands ${ns}::*]]
}
#also if a sub package was loaded first - then the namespace for the base or lower package may exist but have no commands
#for the purposes of pkguse - which most commonly interactive - we want the namespace populated
#It may still not be *fully* populated because we stop at first source that adds commands - REVIEW
set ns_populated [expr {[tcl::namespace::exists $ns] && [llength [info commands ${ns}::*]] > $previous_command_count}]
if {!$ns_populated} {
#we will catch-run an auto_index entry if any
#auto_index entry may or may not be prefixed with ::
set keys [list]
#first look for exact pkg_unqualified and ::pkg_unqualified
#leave these at beginning of keys list
if {[array exists ::auto_index($pkg_unqualified)]} {
lappend keys $pkg_unqualified
}
if {[array exists ::auto_index(::$pkg_unqualified)]} {
lappend keys ::$pkg_unqualified
}
#as auto_index is an array - we could get keys in arbitrary order
set matches [lsort [array names ::auto_index ${pkg_unqualified}::*]]
lappend keys {*}$matches
set matches [lsort [array names ::auto_index ::${pkg_unqualified}::*]]
lappend keys {*}$matches
set ns_populated 0
set i 0
set already_sourced [list] ;#often multiple triggers for the same source - don't waste time re-sourcing
set ns_depth [llength [punk::ns::nsparts [string trimleft $ns :]]]
while {!$ns_populated && $i < [llength $keys]} {
#todo - skip sourcing deeper entries from a subpkg which may have been loaded earlier than the base
#e.g if we are loading ::x::y
#only source for keys the same depth, or one deeper ie ::x::y, x::y, ::x::y::z not ::x or ::x::y::z::etc
set k [lindex $keys $i]
set k_depth [llength [punk::ns::nsparts [string trimleft $k :]]]
if {$k_depth == $ns_depth || $k_depth == $ns_depth + 1} {
set auto_source [set ::auto_index($k)]
if {$auto_source ni $already_sourced} {
uplevel 1 $auto_source
lappend already_sourced $auto_source
set ns_populated [expr {[tcl::namespace::exists $ns] && [llength [info commands ${ns}::*]] > $previous_command_count}]
}
}
incr i
}
} }
} }
} }
@ -1799,7 +1853,13 @@ tcl::namespace::eval punk::ns {
return $out return $out
} }
} else { } else {
error "Namespace $ns not found." if {$ver eq ""} {
error "Namespace $ns not found. No package version found."
} else {
set out "(no package namespace found) remaining in [uplevel 1 {namespace current}]"
append out \n $ver
return $out
}
} }
return $out return $out
} }

561
src/vfs/_vfscommon/modules/punk/winlnk-0.1.0.tm

@ -0,0 +1,561 @@
# -*- tcl -*-
# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'pmix make' or bin/punkmake to update from <pkg>-buildversion.txt
# module template: shellspy/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 0.1.0
# Meta platform tcl
# Meta license MIT
# @@ Meta End
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# doctools header
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[manpage_begin shellspy_module_punk::winlnk 0 0.1.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 [MS_SHLLINK].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"
}
}
#https://github.com/libyal/liblnk/blob/main/documentation/Windows%20Shortcut%20File%20(LNK)%20format.asciidoc
#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-0.1.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-0.1.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-0.1.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 0.1.0
}]
return
#*** !doctools
#[manpage_end]

BIN
src/vfs/_vfscommon/modules/test/tomlish-1.1.1.tm

Binary file not shown.

717
src/vfs/_vfscommon/modules/tomlish-1.1.1.tm

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save