You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1610 lines
59 KiB
1610 lines
59 KiB
# -*- tcl -*- |
|
# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'dev make' or bin/punkmake to update from <pkg>-buildversion.txt |
|
# module template: punkshell/src/decktemplates/vendor/punk/modules/template_module-0.0.3.tm |
|
# |
|
# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem. |
|
# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository. |
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
# (C) 2024 |
|
# |
|
# @@ Meta Begin |
|
# Application punk::safe 999999.0a1.0 |
|
# Meta platform tcl |
|
# Meta license <unspecified> |
|
# @@ Meta End |
|
|
|
|
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
# doctools header |
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
#*** !doctools |
|
#[manpage_begin punkshell_module_punk::safe 0 999999.0a1.0] |
|
#[copyright "2024"] |
|
#[titledesc {Module API}] [comment {-- Name section and table of contents description --}] |
|
#[moddesc {punk::safe - safebase interpreters}] [comment {-- Description at end of page heading --}] |
|
#[require punk::safe] |
|
#[keywords module] |
|
#[description] |
|
#[para] - |
|
|
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
|
|
#*** !doctools |
|
#[section Overview] |
|
#[para] overview of punk::safe |
|
#[subsection Concepts] |
|
#[para] - |
|
|
|
|
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
## Requirements |
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
|
|
#*** !doctools |
|
#[subsection dependencies] |
|
#[para] packages used by punk::safe |
|
#[list_begin itemized] |
|
|
|
package require Tcl 8.6- |
|
package require punk::args |
|
#*** !doctools |
|
#[item] [package {Tcl 8.6}] |
|
#[item] [package {punk::args}] |
|
|
|
# #package require frobz |
|
# #*** !doctools |
|
# #[item] [package {frobz}] |
|
|
|
#*** !doctools |
|
#[list_end] |
|
|
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
|
|
#*** !doctools |
|
#[section API] |
|
|
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
# oo::class namespace |
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
#tcl::namespace::eval punk::safe::class { |
|
#*** !doctools |
|
#[subsection {Namespace punk::safe::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 ---}] |
|
#} |
|
#} |
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
|
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
# Secondary API namespace |
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
tcl::namespace::eval punk::safe::lib { |
|
tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase |
|
tcl::namespace::path [tcl::namespace::parent] |
|
#*** !doctools |
|
#[subsection {Namespace punk::safe::lib}] |
|
#[para] Secondary functions that are part of the API |
|
#[list_begin definitions] |
|
|
|
# ------------------------------------------------------------------------------ |
|
# Using Interpreter Names with Namespace Qualifiers |
|
# ------------------------------------------------------------------------------ |
|
# (1) We wish to preserve compatibility with existing code, in which Safe Base |
|
# interpreter names have no namespace qualifiers. |
|
# (2) safe::interpCreate and the rest of the Safe Base previously could not |
|
# accept namespace qualifiers in an interpreter name. |
|
# (3) The interp command will accept namespace qualifiers in an interpreter |
|
# name, but accepts distinct interpreters that will have the same command |
|
# name (e.g. foo, ::foo, and :::foo) (bug 66c2e8c974). |
|
# (4) To satisfy these constraints, Safe Base interpreter names will be fully |
|
# qualified namespace names with no excess colons and with the leading "::" |
|
# omitted. |
|
# (5) Trailing "::" implies a namespace tail {}, which interp reads as {{}}. |
|
# Reject such names. |
|
# (6) We could: |
|
# (a) EITHER reject usable but non-compliant names (e.g. excess colons) in |
|
# interpCreate, interpInit; |
|
# (b) OR accept such names and then translate to a compliant name in every |
|
# command. |
|
# The problem with (b) is that the user will expect to use the name with the |
|
# interp command and will find that it is not recognised. |
|
# E.g "interpCreate ::foo" creates interpreter "foo", and the user's name |
|
# "::foo" works with all the Safe Base commands, but "interp eval ::foo" |
|
# fails. |
|
# So we choose (a). |
|
# (7) The command |
|
# namespace upvar ::punk::safe::system S$child state |
|
# becomes |
|
# namespace upvar ::punk::safe::system [VarName $child] state |
|
# ------------------------------------------------------------------------------ |
|
|
|
proc RejectExcessColons {child} { |
|
set stripped [regsub -all -- {:::*} $child ::] |
|
if {[string range $stripped end-1 end] eq {::}} { |
|
return -code error {interpreter name must not end in "::"} |
|
} |
|
if {$stripped ne $child} { |
|
set msg {interpreter name has excess colons in namespace separators} |
|
return -code error $msg |
|
} |
|
if {[string range $stripped 0 1] eq {::}} { |
|
return -code error {interpreter name must not begin "::"} |
|
} |
|
return |
|
} |
|
|
|
proc VarName {child} { |
|
# return S$child |
|
return S[string map {:: @N @ @A} $child] |
|
} |
|
|
|
|
|
# Helper function to resolve the dual way of specifying staticsok (either |
|
# by -noStatics or -statics 0) |
|
proc InterpStatics {argd} { |
|
set statics [dict get $argd opts -statics] |
|
set noStatics [dict get $argd opts -noStatics] |
|
set flag [dict exists $argd received] ;#-noStatics was explicitly supplied as an argument |
|
if {$flag |
|
&& (!$noStatics == !$statics) |
|
&& [dict exists $argd received -statics]} { |
|
return -code error "conflicting values given for -statics and -noStatics" |
|
} |
|
if {$flag} { |
|
return [expr {!$noStatics}] |
|
} else { |
|
return $statics |
|
} |
|
} |
|
|
|
# Helper function to resolve the dual way of specifying nested loading |
|
# (either by -nestedLoadOk or -nested 1) |
|
proc InterpNested {argd} { |
|
set nested [dict get $argd opts -nested] |
|
set nestedLoadOk [dict get $argd opts -nestedLoadOk] |
|
set flag [dict exists $argd received -nestedLoadOk] |
|
if {$flag |
|
&& (!$nestedLoadOk != !$nested) |
|
&& [dict exists $argd received -nested]} { |
|
return -code error "conflicting values given for -nested and -nestedLoadOk" |
|
} |
|
if {$flag} { |
|
return $nestedLoadOk |
|
} else { |
|
return $nested |
|
} |
|
} |
|
|
|
#Returns the virtual token for directory number N. |
|
proc PathToken {n} { |
|
# We need to have a ":" in the token string so [file join] on the |
|
# mac won't turn it into a relative path. |
|
return "\$p(:$n:)" ;# Form tested by case 7.2 |
|
} |
|
|
|
# |
|
# translate virtual path into real path |
|
# |
|
proc TranslatePath {child path} { |
|
namespace upvar ::punk::safe::system [VarName $child] state |
|
|
|
# somehow strip the namespaces 'functionality' out (the danger is that |
|
# we would strip valid macintosh "../" queries... : |
|
if {[string match "*::*" $path] || [string match "*..*" $path]} { |
|
return -code error "invalid characters in path $path" |
|
} |
|
# Use a cached map instead of computed local vars and subst. |
|
return [string map $state(access_path,map) $path] |
|
} |
|
|
|
# file name control (limit access to files/resources that should be a |
|
# valid tcl source file) |
|
proc CheckFileName {child file} { |
|
# This used to limit what can be sourced to ".tcl" and forbid files |
|
# with more than 1 dot and longer than 14 chars, but I changed that |
|
# for 8.4 as a safe interp has enough internal protection already to |
|
# allow sourcing anything. - hobbs |
|
|
|
if {![file exists $file]} { |
|
# don't tell the file path |
|
return -code error "no such file or directory" |
|
} |
|
|
|
if {![file readable $file]} { |
|
# don't tell the file path |
|
return -code error "not readable" |
|
} |
|
} |
|
|
|
|
|
|
|
#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::safe::lib ---}] |
|
} |
|
|
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
# Base namespace |
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
tcl::namespace::eval punk::safe { |
|
tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase |
|
namespace path {::punk::safe::lib} |
|
#variable xyz |
|
|
|
|
|
|
|
#*** !doctools |
|
#[subsection {Namespace punk::safe}] |
|
#[para] Core API functions for punk::safe |
|
#[list_begin definitions] |
|
|
|
#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" |
|
#} |
|
|
|
# Accessor method for ::punk::safe::system::AutoPathSync |
|
# Usage: ::punk::safe::setSyncMode ?newValue? |
|
# Respond to changes by calling Setup again, preserving any |
|
# caller-defined logging. This allows complete equivalence with |
|
# prior Safe Base behavior if AutoPathSync is true. |
|
# |
|
# >>> WARNING <<< |
|
# |
|
# DO NOT CHANGE AutoPathSync EXCEPT BY THIS COMMAND - IT IS VITAL THAT WHENEVER |
|
# THE VALUE CHANGES, THE EXISTING PARSE TOKENS ARE DELETED AND Setup IS CALLED |
|
# AGAIN. |
|
# (The initialization of AutoPathSync at the end of this file is acceptable |
|
# because Setup has not yet been called.) |
|
|
|
proc setSyncMode {args} { |
|
upvar ::punk::safe::system::AutoPathSync AutoPathSync |
|
#*** !doctools |
|
#[call [fun setSyncMode] [arg args]] |
|
|
|
switch -- [llength $args] { |
|
0 {} |
|
1 { |
|
set newValue [lindex $args 0] |
|
if {![string is boolean -strict $newValue]} { |
|
return -code error "new value must be a valid boolean" |
|
} |
|
set args [expr {$newValue && $newValue}] |
|
if {([info vars ::punk::safe::system::S*] ne {}) && ($args != $AutoPathSync)} { |
|
return -code error \ |
|
"cannot set new value while Safe Base child interpreters exist" |
|
} |
|
if {($args != $AutoPathSync)} { |
|
set AutoPathSync {*}$args |
|
#::tcl::OptKeyDelete ::safe::interpCreate |
|
#::tcl::OptKeyDelete ::safe::interpIC |
|
set TmpLog [setLogCmd] |
|
::punk::safe::system::Setup |
|
setLogCmd $TmpLog |
|
} |
|
} |
|
default { |
|
set msg {wrong # args: should be "safe::setSyncMode ?newValue?"} |
|
return -code error $msg |
|
} |
|
} |
|
|
|
return $AutoPathSync |
|
} |
|
|
|
# Set (or get) the logging mechanism |
|
|
|
proc setLogCmd {args} { |
|
upvar ::punk::safe::system::Log Log |
|
switch -- [llength $args] { |
|
0 { |
|
return $Log |
|
} |
|
1 { |
|
set Log [lindex $args 0] |
|
} |
|
default { |
|
set Log $args |
|
} |
|
} |
|
|
|
if {$Log eq ""} { |
|
# Disable logging completely. Calls to it will be compiled out |
|
# of all users. |
|
proc ::punk::safe::Log {args} {} |
|
} else { |
|
# Activate logging, define proper command. |
|
|
|
proc ::punk::safe::Log {child msg {type ERROR}} { |
|
upvar ::punk::safe::system::Log Log |
|
{*}$Log "$type for child $child : $msg" |
|
return |
|
} |
|
} |
|
} |
|
|
|
proc interpCreate {args} { |
|
upvar ::punk::safe::system::AutoPathSync AutoPathSync |
|
if {$AutoPathSync} { |
|
#REVIEW |
|
set autoPath {} |
|
} |
|
set argd [punk::args::get_by_id ::punk::safe::interpCreate $args] |
|
set child [dict get $argd leaders child] |
|
set autoPath [dict get $argd opts -autoPath] |
|
punk::safe::lib::RejectExcessColons $child |
|
|
|
set withAutoPath [dict exists $argd received -autoPath] ;#boolean as to whether -autoPath was explicitly supplied |
|
punk::safe::system::do_interpCreate $child\ |
|
[dict get $argd opts -accessPath]\ |
|
[InterpStatics $argd]\ |
|
[InterpNested $argd]\ |
|
[dict get $argd opts -deleteHook]\ |
|
$autoPath\ |
|
$withAutoPath |
|
|
|
|
|
} |
|
proc interpInit {args} { |
|
upvar ::punk::safe::system::AutoPathSync AutoPathSync |
|
if {$AutoPathSync} { |
|
set autoPath {} |
|
} |
|
set argd [punk::args::get_by_id ::punk::safe::interpIC $args] |
|
set child [dict get $argd leaders child] |
|
set autoPath [dict get $argd opts -autoPath] |
|
if {![::interp exists $child]} { |
|
return -code error "\"$child\" is not an interpreter" |
|
} |
|
punk::safe::lib::RejectExcessColons $child |
|
|
|
set withAutoPath [dict exists $argd received -autoPath] |
|
punk::safe::system::do_interpInit $child\ |
|
[dict get $argd opts -accessPath]\ |
|
[InterpStatics $argd]\ |
|
[InterpNested $argd]\ |
|
[dict get $argd opts -deleteHook]\ |
|
$autoPath\ |
|
$withAutoPath |
|
} |
|
|
|
# Check that the given child is "one of us" |
|
proc CheckInterp {child} { |
|
namespace upvar ::punk::safe::system [VarName $child] state |
|
if {![info exists state] || ![::interp exists $child]} { |
|
return -code error \ |
|
"\"$child\" is not an interpreter managed by ::punk::safe::" |
|
} |
|
} |
|
|
|
# Interface/entry point function and front end for "Configure". This code |
|
# is awfully pedestrian because it would need more coupling and support |
|
# between the way we store the configuration values in safe::interp's and |
|
# the Opt package. Obviously we would like an OptConfigure to avoid |
|
# duplicating all this code everywhere. |
|
# -> TODO (the app should share or access easily the program/value stored |
|
# by opt) |
|
|
|
# This is even more complicated by the boolean flags with no values that |
|
# we had the bad idea to support for the sake of user simplicity in |
|
# create/init but which makes life hard in configure... |
|
# So this will be hopefully written and some integrated with opt1.0 |
|
# (hopefully for tcl9.0 ?) |
|
proc interpConfigure {args} { |
|
upvar ::punk::safe::system::AutoPathSync AutoPathSync |
|
|
|
switch [llength $args] { |
|
1 { |
|
# If we have exactly 1 argument the semantic is to return all |
|
# the current configuration. We still call OptKeyParse though |
|
# we know that "child" is our given argument because it also |
|
# checks for the "-help" option. |
|
#TODO! |
|
set argd [punk::args::get_by_id ::punk::safe::interpIC $args] |
|
set child [dict get $argd leaders child] |
|
|
|
CheckInterp $child |
|
namespace upvar ::punk::safe::system [VarName $child] state |
|
|
|
set TMP [list \ |
|
[list -accessPath $state(access_path)] \ |
|
[list -statics $state(staticsok)] \ |
|
[list -nested $state(nestedok)] \ |
|
[list -deleteHook $state(cleanupHook)] \ |
|
] |
|
if {!$AutoPathSync} { |
|
lappend TMP [list -autoPath $state(auto_path)] |
|
} |
|
return [join $TMP] |
|
} |
|
2 { |
|
# If we have exactly 2 arguments the semantic is a "configure get" |
|
lassign $args child arg |
|
|
|
set spec_dict [punk::args::define [punk::args::rawdef punk::safe::interpIC]] |
|
set opt_names [dict get $spec_dict opt_names] |
|
|
|
CheckInterp $child |
|
set name [tcl::prefix::match -error {} $opt_names $arg] |
|
namespace upvar ::punk::safe::system [VarName $child] state |
|
|
|
switch -exact -- $name { |
|
-accessPath { |
|
return [list -accessPath $state(access_path)] |
|
} |
|
-autoPath { |
|
if {$AutoPathSync} { |
|
return -code error "unknown flag $name (bug)" |
|
} else { |
|
return [list -autoPath $state(auto_path)] |
|
} |
|
} |
|
-statics { |
|
return [list -statics $state(staticsok)] |
|
} |
|
-nested { |
|
return [list -nested $state(nestedok)] |
|
} |
|
-deleteHook { |
|
return [list -deleteHook $state(cleanupHook)] |
|
} |
|
-noStatics { |
|
# it is most probably a set in fact but we would need |
|
# then to jump to the set part and it is not *sure* |
|
# that it is a set action that the user want, so force |
|
# it to use the unambiguous -statics ?value? instead: |
|
return -code error\ |
|
"ambiguous query (get or set -noStatics ?)\ |
|
use -statics instead" |
|
} |
|
-nestedLoadOk { |
|
return -code error\ |
|
"ambiguous query (get or set -nestedLoadOk ?)\ |
|
use -nested instead" |
|
} |
|
default { |
|
#return -code error "unknown flag $name. Known options: $opt_names" |
|
punk::args::get_by_id ::punk::safe::interpIC [list $child $arg] |
|
} |
|
} |
|
} |
|
default { |
|
# Otherwise we want to parse the arguments like init and create did |
|
|
|
#set Args [::tcl::OptKeyParse ::safe::interpIC $args] |
|
set argd [punk::args::get_by_id ::punk::safe::interpIC $args] |
|
set child [dict get $argd leaders child] |
|
CheckInterp $child |
|
namespace upvar ::punk::safe::system [VarName $child] state |
|
|
|
# Get the current (and not the default) values of whatever has |
|
# not been given: |
|
if {![dict exists $argd received -accessPath]} { |
|
set doreset 0 |
|
set accessPath $state(access_path) |
|
} else { |
|
set doreset 1 |
|
} |
|
if {(!$AutoPathSync) && (![dict exists $argd received -autoPath])} { |
|
set autoPath $state(auto_path) |
|
} elseif {$AutoPathSync} { |
|
set autoPath {} |
|
} else { |
|
#review |
|
set autoPath [dict get $argd opts -autoPath] |
|
} |
|
|
|
if { |
|
![dict exists $argd received -statics] |
|
&& ![dict exists $argd received -noStatics] |
|
} then { |
|
set statics $state(staticsok) |
|
} else { |
|
set statics [InterpStatics $argd] |
|
} |
|
if { |
|
[dict exists $argd received -nested] || |
|
[dict exists $argd received -nestedLoadOk] |
|
} then { |
|
set nested [InterpNested $argd] |
|
} else { |
|
set nested $state(nestedok) |
|
} |
|
if {![dict exists $argd received -deleteHook]} { |
|
set deleteHook $state(cleanupHook) |
|
} else { |
|
set deleteHook [dict get $argd opts -deleteHook] |
|
} |
|
# Now reconfigure |
|
set withAutoPath [dict exists $argd received -autoPath] |
|
::punk::safe::system::InterpSetConfig $child $accessPath $statics $nested $deleteHook $autoPath $withAutoPath |
|
|
|
# auto_reset the child (to completely sync the new access_path) tests safe-9.8 safe-9.9 |
|
if {$doreset} { |
|
if {[catch {::interp eval $child {auto_reset}} msg]} { |
|
Log $child "auto_reset failed: $msg" |
|
} else { |
|
Log $child "successful auto_reset" NOTICE |
|
} |
|
|
|
# Sync the paths used to search for Tcl modules. |
|
::interp eval $child {tcl::tm::path remove {*}[tcl::tm::list]} |
|
if {[llength $state(tm_path_child)] > 0} { |
|
::interp eval $child [list \ |
|
::tcl::tm::add {*}[lreverse $state(tm_path_child)]] |
|
} |
|
|
|
# Remove stale "package ifneeded" data for non-loaded packages. |
|
# - Not for loaded packages, because "package forget" erases |
|
# data from "package provide" as well as "package ifneeded". |
|
# - This is OK because the script cannot reload any version of |
|
# the package unless it first does "package forget". |
|
foreach pkg [::interp eval $child {package names}] { |
|
if {[::interp eval $child [list package provide $pkg]] eq ""} { |
|
::interp eval $child [list package forget $pkg] |
|
} |
|
} |
|
} |
|
return |
|
} |
|
} |
|
} |
|
|
|
# |
|
# |
|
# interpFindInAccessPath: |
|
# Search for a real directory and returns its virtual Id (including the |
|
# "$") |
|
# |
|
# When debugging, use TranslatePath for the inverse operation. |
|
proc interpFindInAccessPath {child path} { |
|
CheckInterp $child |
|
namespace upvar ::punk::safe::system [VarName $child] state |
|
|
|
if {![dict exists $state(access_path,remap) $path]} { |
|
return -code error "$path not found in access path" |
|
} |
|
|
|
return [dict get $state(access_path,remap) $path] |
|
} |
|
|
|
# |
|
# addToAccessPath: |
|
# add (if needed) a real directory to access path and return its |
|
# virtual token (including the "$"). |
|
proc interpAddToAccessPath {child path} { |
|
# first check if the directory is already in there |
|
# (inlined interpFindInAccessPath). |
|
CheckInterp $child |
|
namespace upvar ::punk::safe::system [VarName $child] state |
|
|
|
if {[dict exists $state(access_path,remap) $path]} { |
|
return [dict get $state(access_path,remap) $path] |
|
} |
|
|
|
# new one, add it: |
|
set token [PathToken [llength $state(access_path)]] |
|
|
|
lappend state(access_path) $path |
|
lappend state(access_path,child) $token |
|
lappend state(access_path,map) $token $path |
|
lappend state(access_path,remap) $path $token |
|
lappend state(access_path,norm) [file normalize $path] |
|
|
|
SyncAccessPath $child |
|
return $token |
|
} |
|
|
|
if {[catch {interp children}]} { |
|
#8.6.10 doesn't have it.. when was it introduced? |
|
proc interp_children {{i {}}} { |
|
puts stderr "punk::safe 'interp children' subcommand not available" |
|
} |
|
} else { |
|
proc interp_children {{i {}}} { |
|
interp children {*}$i |
|
} |
|
} |
|
# This procedure deletes a safe interpreter managed by Safe Tcl and cleans up |
|
# associated state. |
|
# - The command will also delete non-Safe-Base interpreters. |
|
# - This is regrettable, but to avoid breaking existing code this should be |
|
# amended at the next major revision by uncommenting "CheckInterp". |
|
|
|
proc interpDelete {child} { |
|
Log $child "About to delete" NOTICE |
|
|
|
# CheckInterp $child |
|
namespace upvar ::punk::safe::system [VarName $child] state |
|
|
|
# When an interpreter is deleted with [interp delete], any sub-interpreters |
|
# are deleted automatically, but this leaves behind their data in the Safe |
|
# Base. To clean up properly, we call safe::interpDelete recursively on each |
|
# Safe Base sub-interpreter, so each one is deleted cleanly and not by |
|
# the automatic mechanism built into [interp delete]. |
|
foreach sub [interp_children $child] { |
|
if {[info exists ::punk::safe::system::[VarName [list $child $sub]]]} { |
|
::punk::safe::interpDelete [list $child $sub] |
|
} |
|
} |
|
|
|
# If the child has a cleanup hook registered, call it. Check the |
|
# existence because we might be called to delete an interp which has |
|
# not been registered with us at all |
|
|
|
if {[info exists state(cleanupHook)]} { |
|
set hook $state(cleanupHook) |
|
if {[llength $hook]} { |
|
# remove the hook now, otherwise if the hook calls us somehow, |
|
# we'll loop |
|
unset state(cleanupHook) |
|
try { |
|
{*}$hook $child |
|
} on error err { |
|
Log $child "Delete hook error ($err)" |
|
} |
|
} |
|
} |
|
|
|
# Discard the global array of state associated with the child, and |
|
# delete the interpreter. |
|
|
|
if {[info exists state]} { |
|
unset state |
|
} |
|
|
|
# if we have been called twice, the interp might have been deleted |
|
# already |
|
if {[::interp exists $child]} { |
|
::interp delete $child |
|
Log $child "Deleted" NOTICE |
|
} |
|
|
|
return |
|
} |
|
|
|
|
|
#*** !doctools |
|
#[list_end] [comment {--- end definitions namespace punk::safe ---}] |
|
} |
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
|
|
|
|
|
|
|
|
|
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
#*** !doctools |
|
#[section Internal] |
|
tcl::namespace::eval punk::safe::system { |
|
namespace path {::punk::safe ::punk::safe::lib} |
|
|
|
#*** !doctools |
|
#[subsection {Namespace punk::safe::system}] |
|
#[para] Internal functions that are not part of the API |
|
|
|
# AutoPathSync |
|
# |
|
# Set AutoPathSync to 0 to give a child's ::auto_path the same meaning as |
|
# for an unsafe interpreter: the package command will search its directories |
|
# and first-level subdirectories for pkgIndex.tcl files; the auto-loader |
|
# will search its directories for tclIndex files. The access path and |
|
# module path will be maintained as separate values, and ::auto_path will |
|
# not be updated when the user calls ::punk::safe::interpAddToAccessPath to add to |
|
# the access path. If the user specifies an access path when calling |
|
# interpCreate, interpInit or interpConfigure, it is the user's |
|
# responsibility to define the child's auto_path. If these commands are |
|
# called with no (or empty) access path, the child's auto_path will be set |
|
# to a tokenized form of the parent's auto_path, and these directories and |
|
# their first-level subdirectories will be added to the access path. |
|
# |
|
# Set to 1 for "traditional" behavior: a child's entire access path and |
|
# module path are copied to its ::auto_path, which is updated whenever |
|
# the user calls ::punk::safe::interpAddToAccessPath to add to the access path. |
|
variable AutoPathSync 0 |
|
|
|
# Log command, set via 'setLogCmd'. Logging is disabled when empty. |
|
variable Log {} |
|
|
|
|
|
proc Setup {} { |
|
#### |
|
# |
|
# Setup the arguments parsing |
|
# |
|
#### |
|
variable AutoPathSync |
|
|
|
set OPTS { |
|
@id -id ::punk::safe::OPTS |
|
@opts -optional 1 |
|
-accessPath -type list -default {} -help\ |
|
"access path for the child" |
|
-noStatics -type none -default 0 -help\ |
|
"prevent loading of statically linked pkgs" |
|
-statics -type boolean -default true -help\ |
|
"loading of statically linked pkgs" |
|
-nestedLoadOk -type none -default 0 -help\ |
|
"allow nested loading" |
|
-nested -type boolean -default false -help\ |
|
"nested loading" |
|
-deleteHook -default {} -help\ |
|
"delete hook" |
|
} |
|
if {!$AutoPathSync} { |
|
append OPTS \n {-autoPath -type list -default {} -help\ |
|
"::auto_path for the child"} |
|
} |
|
punk::args::define $OPTS |
|
set optlines [punk::args::resolved_def -type @opts punk::safe::OPTS -*] |
|
|
|
set INTERPCREATE { |
|
@id -id ::punk::safe::interpCreate |
|
@cmd -name punk::safe::interpCreate -help\ |
|
"Create a safe interpreter with punk::safe specific aliases |
|
Returns the interpreter name" |
|
@leaders |
|
child -type string -default "" -regexprefail "^-" -regexprefailmsg "" -optional 1 -help\ |
|
"name of the child (optional)" |
|
} |
|
append INTERPCREATE \n $optlines |
|
append INTERPCREATE \n {@values -max 0} |
|
punk::args::define $INTERPCREATE |
|
|
|
|
|
set INTERPIC { |
|
@id -id ::punk::safe::interpIC |
|
@leaders |
|
child -type string -optional 0 -regexprefail "^-" -regexprefailmsg "" -help\ |
|
"name of the child" |
|
} |
|
append INTERPIC \n $optlines |
|
append INTERPIC \n {@values -max 0} |
|
punk::args::define $INTERPIC |
|
|
|
|
|
#### |
|
# |
|
# Default: No logging. |
|
# |
|
#### |
|
|
|
punk::safe::setLogCmd {} |
|
|
|
# Log eventually. |
|
# To enable error logging, set Log to {puts stderr} for instance, |
|
# via setLogCmd. |
|
return |
|
} |
|
|
|
proc do_interpCreate { |
|
child |
|
access_path |
|
staticsok |
|
nestedok |
|
deletehook |
|
autoPath |
|
withAutoPath |
|
} { |
|
# Create the child. |
|
# If evaluated in ::punk::safe, the interpreter command for foo is ::foo; |
|
# but for foo::bar is ::punk::safe::foo::bar. So evaluate in :: instead. |
|
if {$child ne ""} { |
|
namespace eval :: [list ::interp create -safe $child] |
|
} else { |
|
# empty argument: generate child name |
|
set child [::interp create -safe] |
|
} |
|
Log $child "Created" NOTICE |
|
|
|
# Initialize it. (returns child name) |
|
do_interpInit $child $access_path $staticsok $nestedok $deletehook $autoPath $withAutoPath |
|
} |
|
|
|
proc do_interpInit { |
|
child |
|
access_path |
|
staticsok |
|
nestedok |
|
deletehook |
|
autoPath |
|
withAutoPath |
|
} { |
|
|
|
# Configure will generate an access_path when access_path is empty. |
|
InterpSetConfig $child $access_path $staticsok $nestedok $deletehook $autoPath $withAutoPath |
|
|
|
# NB we need to add [namespace current], aliases are always absolute |
|
# paths. |
|
|
|
# These aliases let the child load files to define new commands |
|
# This alias lets the child use the encoding names, convertfrom, |
|
# convertto, and system, but not "encoding system <name>" to set the |
|
# system encoding. |
|
# Handling Tcl Modules, we need a restricted form of Glob. |
|
# This alias interposes on the 'exit' command and cleanly terminates |
|
# the child. |
|
|
|
foreach {command alias} { |
|
source AliasSource |
|
load AliasLoad |
|
exit interpDelete |
|
glob AliasGlob |
|
} { |
|
::interp alias $child $command {} [namespace current]::$alias $child |
|
} |
|
|
|
# UGLY POINT! These commands are safe (they're ensembles with unsafe |
|
# subcommands), but is assumed to not be by existing policies so it is |
|
# hidden by default. Hack it... |
|
foreach command {encoding file} { |
|
::interp alias $child $command {} interp invokehidden $child $command |
|
} |
|
|
|
# This alias lets the child have access to a subset of the 'file' |
|
# command functionality. |
|
foreach subcommand {dirname extension rootname tail} { |
|
::interp alias $child ::tcl::file::$subcommand {} \ |
|
::punk::safe::system::AliasFileSubcommand $child $subcommand |
|
} |
|
|
|
# Subcommand of 'encoding' that has special handling; [encoding system] is |
|
# OK provided it has no other arguments passed to it. |
|
::interp alias $child ::tcl::encoding::system {} \ |
|
::punk::safe::system::AliasEncodingSystem $child |
|
|
|
# Subcommands of info |
|
::interp alias $child ::tcl::info::nameofexecutable {} \ |
|
::punk::safe::system::AliasExeName $child |
|
|
|
# Source init.tcl and tm.tcl into the child, to get auto_load and |
|
# other procedures defined: |
|
|
|
if {[catch {::interp eval $child { |
|
source [file join $tcl_library init.tcl] |
|
}} msg opt]} { |
|
Log $child "can't source init.tcl ($msg)" |
|
return -options $opt "can't source init.tcl into child $child ($msg)" |
|
} |
|
|
|
if {[catch {::interp eval $child { |
|
source [file join $tcl_library tm.tcl] |
|
}} msg opt]} { |
|
Log $child "can't source tm.tcl ($msg)" |
|
return -options $opt "can't source tm.tcl into child $child ($msg)" |
|
} |
|
|
|
# Sync the paths used to search for Tcl modules. This can be done only |
|
# now, after tm.tcl was loaded. |
|
namespace upvar ::punk::safe::system [VarName $child] state |
|
if {[llength $state(tm_path_child)] > 0} { |
|
::interp eval $child [list \ |
|
::tcl::tm::add {*}[lreverse $state(tm_path_child)]] |
|
} |
|
return $child |
|
} |
|
|
|
# |
|
# InterpSetConfig (was setAccessPath) : |
|
# Sets up child virtual access path and corresponding structure within |
|
# the parent. Also sets the tcl_library in the child to be the first |
|
# directory in the path. |
|
# NB: If you change the path after the child has been initialized you |
|
# probably need to call "auto_reset" in the child in order that it gets |
|
# the right auto_index() array values. |
|
# |
|
# It is the caller's responsibility, if it supplies a non-empty value for |
|
# access_path, to make the first directory in the path suitable for use as |
|
# tcl_library, and (if ![setSyncMode]), to set the child's ::auto_path. |
|
|
|
proc InterpSetConfig {child access_path staticsok nestedok deletehook autoPath withAutoPath} { |
|
global auto_path |
|
variable AutoPathSync |
|
|
|
# determine and store the access path if empty |
|
if {$access_path eq ""} { |
|
set access_path $auto_path |
|
|
|
# Make sure that tcl_library is in auto_path and at the first |
|
# position (needed by setAccessPath) |
|
set where [lsearch -exact $access_path [info library]] |
|
if {$where < 0} { |
|
# not found, add it. |
|
set access_path [linsert $access_path 0 [info library]] |
|
Log $child "tcl_library was not in auto_path,\ |
|
added it to child's access_path" NOTICE |
|
} elseif {$where != 0} { |
|
# not first, move it first |
|
set access_path [linsert \ |
|
[lreplace $access_path $where $where] \ |
|
0 [info library]] |
|
Log $child "tcl_libray was not in first in auto_path,\ |
|
moved it to front of child's access_path" NOTICE |
|
} |
|
|
|
set raw_auto_path $access_path |
|
|
|
# Add 1st level subdirs (will searched by auto loading from tcl |
|
# code in the child using glob and thus fail, so we add them here |
|
# so by default it works the same). |
|
set access_path [AddSubDirs $access_path] |
|
} else { |
|
set raw_auto_path $autoPath |
|
} |
|
|
|
if {$withAutoPath} { |
|
set raw_auto_path $autoPath |
|
} |
|
|
|
Log $child "Setting accessPath=($access_path) staticsok=$staticsok\ |
|
nestedok=$nestedok deletehook=($deletehook)" NOTICE |
|
|
|
if {!$AutoPathSync} { |
|
Log $child "Setting auto_path=($raw_auto_path)" NOTICE |
|
} |
|
|
|
namespace upvar ::punk::safe::system [VarName $child] state |
|
|
|
|
|
# clear old autopath if it existed |
|
# build new one |
|
# Extend the access list with the paths used to look for Tcl Modules. |
|
# We save the virtual form separately as well, as syncing it with the |
|
# child has to be defered until the necessary commands are present for |
|
# setup. |
|
set norm_access_path {} |
|
set child_access_path {} |
|
set map_access_path {} |
|
set remap_access_path {} |
|
set child_tm_path {} |
|
|
|
set i 0 |
|
foreach dir $access_path { |
|
set token [PathToken $i] |
|
lappend child_access_path $token |
|
lappend map_access_path $token $dir |
|
lappend remap_access_path $dir $token |
|
lappend norm_access_path [file normalize $dir] |
|
incr i |
|
} |
|
|
|
# Set the child auto_path to a tokenized raw_auto_path. |
|
# Silently ignore any directories that are not in the access path. |
|
# If [setSyncMode], SyncAccessPath will overwrite this value with the |
|
# full access path. |
|
# If ![setSyncMode], Safe Base code will not change this value. |
|
set tokens_auto_path {} |
|
foreach dir $raw_auto_path { |
|
if {[dict exists $remap_access_path $dir]} { |
|
lappend tokens_auto_path [dict get $remap_access_path $dir] |
|
} |
|
} |
|
::interp eval $child [list set auto_path $tokens_auto_path] |
|
|
|
# Add the tcl::tm directories to the access path. |
|
set morepaths [::tcl::tm::list] |
|
set firstpass 1 |
|
while {[llength $morepaths]} { |
|
set addpaths $morepaths |
|
set morepaths {} |
|
|
|
foreach dir $addpaths { |
|
# Prevent the addition of dirs on the tm list to the |
|
# result if they are already known. |
|
if {[dict exists $remap_access_path $dir]} { |
|
if {$firstpass} { |
|
# $dir is in [::tcl::tm::list] and belongs in the child_tm_path. |
|
# Later passes handle subdirectories, which belong in the |
|
# access path but not in the module path. |
|
lappend child_tm_path [dict get $remap_access_path $dir] |
|
} |
|
continue |
|
} |
|
|
|
set token [PathToken $i] |
|
lappend access_path $dir |
|
lappend child_access_path $token |
|
lappend map_access_path $token $dir |
|
lappend remap_access_path $dir $token |
|
lappend norm_access_path [file normalize $dir] |
|
if {$firstpass} { |
|
# $dir is in [::tcl::tm::list] and belongs in the child_tm_path. |
|
# Later passes handle subdirectories, which belong in the |
|
# access path but not in the module path. |
|
lappend child_tm_path $token |
|
} |
|
incr i |
|
|
|
# [Bug 2854929] |
|
# Recursively find deeper paths which may contain |
|
# modules. Required to handle modules with names like |
|
# 'platform::shell', which translate into |
|
# 'platform/shell-X.tm', i.e arbitrarily deep |
|
# subdirectories. |
|
lappend morepaths {*}[glob -nocomplain -directory $dir -type d *] |
|
} |
|
set firstpass 0 |
|
} |
|
|
|
set state(access_path) $access_path |
|
set state(access_path,map) $map_access_path |
|
set state(access_path,remap) $remap_access_path |
|
set state(access_path,norm) $norm_access_path |
|
set state(access_path,child) $child_access_path |
|
set state(tm_path_child) $child_tm_path |
|
set state(staticsok) $staticsok |
|
set state(nestedok) $nestedok |
|
set state(cleanupHook) $deletehook |
|
|
|
if {!$AutoPathSync} { |
|
set state(auto_path) $raw_auto_path |
|
} |
|
|
|
SyncAccessPath $child |
|
return |
|
} |
|
|
|
# AliasSource is the target of the "source" alias in safe interpreters. |
|
proc AliasSource {child args} { |
|
set argc [llength $args] |
|
# Extended for handling of Tcl Modules to allow not only "source |
|
# filename", but "source -encoding E filename" as well. |
|
if {[lindex $args 0] eq "-encoding"} { |
|
incr argc -2 |
|
set encoding [lindex $args 1] |
|
set at 2 |
|
if {$encoding eq "identity"} { |
|
Log $child "attempt to use the identity encoding" |
|
return -code error "permission denied" |
|
} |
|
} else { |
|
set at 0 |
|
set encoding utf-8 |
|
} |
|
if {$argc != 1} { |
|
set msg "wrong # args: should be \"source ?-encoding E? fileName\"" |
|
Log $child "$msg ($args)" |
|
return -code error $msg |
|
} |
|
set file [lindex $args $at] |
|
|
|
# get the real path from the virtual one. |
|
if {[catch { |
|
set realfile [TranslatePath $child $file] |
|
} msg]} { |
|
Log $child $msg |
|
return -code error "permission denied" |
|
} |
|
|
|
# check that the path is in the access path of that child |
|
if {[catch { |
|
FileInAccessPath $child $realfile |
|
} msg]} { |
|
Log $child $msg |
|
return -code error "permission denied" |
|
} |
|
|
|
# Check that the filename exists and is readable. If it is not, deliver |
|
# this -errorcode so that caller in tclPkgUnknown does not write a message |
|
# to tclLog. Has no effect on other callers of ::source, which are in |
|
# "package ifneeded" scripts. |
|
if {[catch { |
|
CheckFileName $child $realfile |
|
} msg]} { |
|
Log $child "$realfile:$msg" |
|
return -code error -errorcode {POSIX EACCES} $msg |
|
} |
|
|
|
# Passed all the tests, lets source it. Note that we do this all manually |
|
# because we want to control [info script] in the child so information |
|
# doesn't leak so much. [Bug 2913625] |
|
set old [::interp eval $child {info script}] |
|
set replacementMsg "script error" |
|
set code [catch { |
|
set f [open $realfile] |
|
fconfigure $f -encoding $encoding -eofchar \x1A |
|
set contents [read $f] |
|
close $f |
|
::interp eval $child [list info script $file] |
|
} msg opt |
|
] |
|
|
|
if {$code == 0} { |
|
# See [Bug 1d26e580cf] |
|
if {[string index $contents 0] eq "\uFEFF"} { |
|
set contents [string range $contents 1 end] |
|
} |
|
set code [catch {::interp eval $child $contents} msg opt] |
|
set replacementMsg $msg |
|
} |
|
catch {interp eval $child [list info script $old]} |
|
# Note that all non-errors are fine result codes from [source], so we must |
|
# take a little care to do it properly. [Bug 2923613] |
|
if {$code == 1} { |
|
Log $child $msg |
|
return -code error $replacementMsg |
|
} |
|
return -code $code -options $opt $msg |
|
} |
|
|
|
|
|
# AliasLoad is the target of the "load" alias in safe interpreters. |
|
proc AliasLoad {child file args} { |
|
set argc [llength $args] |
|
if {$argc > 2} { |
|
set msg "load error: too many arguments" |
|
Log $child "$msg ($argc) {$file $args}" |
|
return -code error $msg |
|
} |
|
|
|
# prefix (can be empty if file is not). |
|
set prefix [lindex $args 0] |
|
|
|
namespace upvar ::punk::safe::system [VarName $child] state |
|
|
|
# Determine where to load. load use a relative interp path and {} |
|
# means self, so we can directly and safely use passed arg. |
|
set target [lindex $args 1] |
|
if {$target ne ""} { |
|
# we will try to load into a sub sub interp; check that we want to |
|
# authorize that. |
|
if {!$state(nestedok)} { |
|
Log $child "loading to a sub interp (nestedok)\ |
|
disabled (trying to load $prefix to $target)" |
|
return -code error "permission denied (nested load)" |
|
} |
|
} |
|
|
|
# Determine what kind of load is requested |
|
if {$file eq ""} { |
|
# static loading |
|
if {$prefix eq ""} { |
|
set msg "load error: empty filename and no prefix" |
|
Log $child $msg |
|
return -code error $msg |
|
} |
|
if {!$state(staticsok)} { |
|
Log $child "static loading disabled\ |
|
(trying to load $prefix to $target)" |
|
return -code error "permission denied (static library)" |
|
} |
|
} else { |
|
# file loading |
|
|
|
# get the real path from the virtual one. |
|
try { |
|
set file [TranslatePath $child $file] |
|
} on error msg { |
|
Log $child $msg |
|
return -code error "permission denied" |
|
} |
|
|
|
# check the translated path |
|
try { |
|
FileInAccessPath $child $file |
|
} on error msg { |
|
Log $child $msg |
|
return -code error "permission denied (path)" |
|
} |
|
} |
|
|
|
try { |
|
return [::interp invokehidden $child load $file $prefix $target] |
|
} on error msg { |
|
# Some libraries return no error message. |
|
set msg0 "load of library for prefix $prefix failed" |
|
if {$msg eq {}} { |
|
set msg $msg0 |
|
} else { |
|
set msg "$msg0: $msg" |
|
} |
|
Log $child $msg |
|
return -code error $msg |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
# FileInAccessPath raises an error if the file is not found in the list of |
|
# directories contained in the (parent side recorded) child's access path. |
|
|
|
# the security here relies on "file dirname" answering the proper |
|
# result... needs checking ? |
|
proc FileInAccessPath {child file} { |
|
namespace upvar ::punk::safe::system [VarName $child] state |
|
set access_path $state(access_path) |
|
|
|
if {[file isdirectory $file]} { |
|
return -code error "\"$file\": is a directory" |
|
} |
|
set parent [file dirname $file] |
|
|
|
# Normalize paths for comparison since lsearch knows nothing of |
|
# potential pathname anomalies. |
|
set norm_parent [file normalize $parent] |
|
|
|
namespace upvar ::punk::safe::system [VarName $child] state |
|
if {$norm_parent ni $state(access_path,norm)} { |
|
return -code error "\"$file\": not in access_path" |
|
} |
|
} |
|
|
|
proc DirInAccessPath {child dir} { |
|
namespace upvar ::punk::safe::system [VarName $child] state |
|
set access_path $state(access_path) |
|
|
|
if {[file isfile $dir]} { |
|
return -code error "\"$dir\": is a file" |
|
} |
|
|
|
# Normalize paths for comparison since lsearch knows nothing of |
|
# potential pathname anomalies. |
|
set norm_dir [file normalize $dir] |
|
|
|
namespace upvar ::punk::safe::system [VarName $child] state |
|
if {$norm_dir ni $state(access_path,norm)} { |
|
return -code error "\"$dir\": not in access_path" |
|
} |
|
} |
|
|
|
# This procedure is used to report an attempt to use an unsafe member of an |
|
# ensemble command. |
|
|
|
proc BadSubcommand {child command subcommand args} { |
|
set msg "not allowed to invoke subcommand $subcommand of $command" |
|
Log $child $msg |
|
return -code error -errorcode {TCL SAFE SUBCOMMAND} $msg |
|
} |
|
|
|
# AliasEncodingSystem is the target of the "encoding system" alias in safe |
|
# interpreters. |
|
proc AliasEncodingSystem {child args} { |
|
try { |
|
# Must not pass extra arguments; safe interpreters may not set the |
|
# system encoding but they may read it. |
|
if {[llength $args]} { |
|
return -code error -errorcode {TCL WRONGARGS} \ |
|
"wrong # args: should be \"encoding system\"" |
|
} |
|
} on error {msg options} { |
|
Log $child $msg |
|
return -options $options $msg |
|
} |
|
tailcall ::interp invokehidden $child tcl:encoding:system |
|
} |
|
|
|
# Various minor hiding of platform features. [Bug 2913625] |
|
|
|
proc AliasExeName {child} { |
|
return "" |
|
} |
|
|
|
# AliasFileSubcommand handles selected subcommands of [file] in safe |
|
# interpreters that are *almost* safe. In particular, it just acts to |
|
# prevent discovery of what home directories exist. |
|
|
|
proc AliasFileSubcommand {child subcommand name} { |
|
tailcall ::interp invokehidden $child tcl:file:$subcommand $name |
|
} |
|
|
|
# AliasGlob is the target of the "glob" alias in safe interpreters. |
|
|
|
proc AliasGlob {child args} { |
|
variable AutoPathSync |
|
Log $child "GLOB ! $args" NOTICE |
|
set cmd {} |
|
set at 0 |
|
array set got { |
|
-directory 0 |
|
-nocomplain 0 |
|
-join 0 |
|
-tails 0 |
|
-- 0 |
|
} |
|
|
|
if {$::tcl_platform(platform) eq "windows"} { |
|
set dirPartRE {^(.*)[\\/]([^\\/]*)$} |
|
} else { |
|
set dirPartRE {^(.*)/([^/]*)$} |
|
} |
|
|
|
set dir {} |
|
set virtualdir {} |
|
|
|
while {$at < [llength $args]} { |
|
switch -glob -- [set opt [lindex $args $at]] { |
|
-nocomplain - -- - -tails { |
|
lappend cmd $opt |
|
set got($opt) 1 |
|
incr at |
|
} |
|
-join { |
|
set got($opt) 1 |
|
incr at |
|
} |
|
-types - -type { |
|
lappend cmd -types [lindex $args [incr at]] |
|
incr at |
|
} |
|
-directory { |
|
if {$got($opt)} { |
|
return -code error \ |
|
{"-directory" cannot be used with "-path"} |
|
} |
|
set got($opt) 1 |
|
set virtualdir [lindex $args [incr at]] |
|
incr at |
|
} |
|
-* { |
|
Log $child "Safe base rejecting glob option '$opt'" |
|
return -code error "Safe base rejecting glob option '$opt'" |
|
# unsafe/unnecessary options rejected: -path |
|
} |
|
default { |
|
break |
|
} |
|
} |
|
if {$got(--)} break |
|
} |
|
|
|
# Get the real path from the virtual one and check that the path is in the |
|
# access path of that child. Done after basic argument processing so that |
|
# we know if -nocomplain is set. |
|
if {$got(-directory)} { |
|
try { |
|
set dir [TranslatePath $child $virtualdir] |
|
DirInAccessPath $child $dir |
|
} on error msg { |
|
Log $child $msg |
|
if {$got(-nocomplain)} return |
|
return -code error "permission denied" |
|
} |
|
if {$got(--)} { |
|
set cmd [linsert $cmd end-1 -directory $dir] |
|
} else { |
|
lappend cmd -directory $dir |
|
} |
|
} else { |
|
# The code after this "if ... else" block would conspire to return with |
|
# no results in this case, if it were allowed to proceed. Instead, |
|
# return now and reduce the number of cases to be considered later. |
|
Log $child {option -directory must be supplied} |
|
if {$got(-nocomplain)} return |
|
return -code error "permission denied" |
|
} |
|
|
|
# Apply the -join semantics ourselves (hence -join not copied to $cmd) |
|
if {$got(-join)} { |
|
set args [lreplace $args $at end [join [lrange $args $at end] "/"]] |
|
} |
|
|
|
# Process the pattern arguments. If we've done a join there is only one |
|
# pattern argument. |
|
|
|
set firstPattern [llength $cmd] |
|
foreach opt [lrange $args $at end] { |
|
if {![regexp $dirPartRE $opt -> thedir thefile]} { |
|
set thedir . |
|
# The *.tm search comes here. |
|
} |
|
# "Special" treatment for (joined) argument {*/pkgIndex.tcl}. |
|
# Do the expansion of "*" here, and filter out any directories that are |
|
# not in the access path. The outcome is to lappend to cmd a path of |
|
# the form $virtualdir/subdir/pkgIndex.tcl for each subdirectory subdir, |
|
# after removing any subdir that are not in the access path. |
|
if {($thedir eq "*") && ($thefile eq "pkgIndex.tcl")} { |
|
set mapped 0 |
|
foreach d [glob -directory [TranslatePath $child $virtualdir] \ |
|
-types d -tails *] { |
|
catch { |
|
DirInAccessPath $child \ |
|
[TranslatePath $child [file join $virtualdir $d]] |
|
lappend cmd [file join $d $thefile] |
|
set mapped 1 |
|
} |
|
} |
|
if {$mapped} continue |
|
# Don't [continue] if */pkgIndex.tcl has no matches in the access |
|
# path. The pattern will now receive the same treatment as a |
|
# "non-special" pattern (and will fail because it includes a "*" in |
|
# the directory name). |
|
} |
|
# Any directory pattern that is not an exact (i.e. non-glob) match to a |
|
# directory in the access path will be rejected here. |
|
# - Rejections include any directory pattern that has glob matching |
|
# patterns "*", "?", backslashes, braces or square brackets, (UNLESS |
|
# it corresponds to a genuine directory name AND that directory is in |
|
# the access path). |
|
# - The only "special matching characters" that remain in patterns for |
|
# processing by glob are in the filename tail. |
|
# - [file join $anything ~${foo}] is ~${foo}, which is not an exact |
|
# match to any directory in the access path. Hence directory patterns |
|
# that begin with "~" are rejected here. Tests safe-16.[5-8] check |
|
# that "file join" remains as required and does not expand ~${foo}. |
|
# - Bug [3529949] relates to unwanted expansion of ~${foo} and this is |
|
# how the present code avoids the bug. All tests safe-16.* relate. |
|
try { |
|
DirInAccessPath $child [TranslatePath $child \ |
|
[file join $virtualdir $thedir]] |
|
} on error msg { |
|
Log $child $msg |
|
if {$got(-nocomplain)} continue |
|
return -code error "permission denied" |
|
} |
|
lappend cmd $opt |
|
} |
|
|
|
Log $child "GLOB = $cmd" NOTICE |
|
|
|
if {$got(-nocomplain) && [llength $cmd] eq $firstPattern} { |
|
return |
|
} |
|
try { |
|
# >>>>>>>>>> HERE'S THE CALL TO SAFE INTERP GLOB <<<<<<<<<< |
|
# - Pattern arguments added to cmd have NOT been translated from tokens. |
|
# Only the virtualdir is translated (to dir). |
|
# - In the pkgIndex.tcl case, there is no "*" in the pattern arguments, |
|
# which are a list of names each with tail pkgIndex.tcl. The purpose |
|
# of the call to glob is to remove the names for which the file does |
|
# not exist. |
|
set entries [::interp invokehidden $child glob {*}$cmd] |
|
} on error msg { |
|
# This is the only place that a call with -nocomplain and no invalid |
|
# "dash-options" can return an error. |
|
Log $child $msg |
|
return -code error "script error" |
|
} |
|
|
|
Log $child "GLOB < $entries" NOTICE |
|
|
|
# Translate path back to what the child should see. |
|
set res {} |
|
set l [string length $dir] |
|
foreach p $entries { |
|
if {[string equal -length $l $dir $p]} { |
|
set p [string replace $p 0 [expr {$l-1}] $virtualdir] |
|
} |
|
lappend res $p |
|
} |
|
|
|
Log $child "GLOB > $res" NOTICE |
|
return $res |
|
} |
|
|
|
|
|
# Add (only if needed, avoid duplicates) 1 level of sub directories to an |
|
# existing path list. Also removes non directories from the returned |
|
# list. |
|
proc AddSubDirs {pathList} { |
|
set res {} |
|
foreach dir $pathList { |
|
if {[file isdirectory $dir]} { |
|
# check that we don't have it yet as a children of a previous |
|
# dir |
|
if {$dir ni $res} { |
|
lappend res $dir |
|
} |
|
foreach sub [glob -directory $dir -nocomplain *] { |
|
if {[file isdirectory $sub] && ($sub ni $res)} { |
|
# new sub dir, add it ! |
|
lappend res $sub |
|
} |
|
} |
|
} |
|
} |
|
return $res |
|
} |
|
|
|
# |
|
# Sets the child auto_path to its recorded access path. Also sets |
|
# tcl_library to the first token of the access path. |
|
# |
|
proc SyncAccessPath {child} { |
|
variable AutoPathSync |
|
namespace upvar ::punk::safe::system [VarName $child] state |
|
|
|
set child_access_path $state(access_path,child) |
|
if {$AutoPathSync} { |
|
::interp eval $child [list set auto_path $child_access_path] |
|
|
|
Log $child "auto_path in $child has been set to $child_access_path"\ |
|
NOTICE |
|
} |
|
|
|
# This code assumes that info library is the first element in the |
|
# list of access path's. See -> InterpSetConfig for the code which |
|
# ensures this condition. |
|
|
|
::interp eval $child [list \ |
|
set tcl_library [lindex $child_access_path 0]] |
|
return |
|
} |
|
|
|
} |
|
|
|
tcl::namespace::eval punk::safe { |
|
# internal variables (must not begin with "S") |
|
|
|
|
|
# The package maintains a state array per child interp under its |
|
# control. The name of this array is S<interp-name>. This array is |
|
# brought into scope where needed, using 'namespace upvar'. The S |
|
# prefix is used to avoid that a child interp called "Log" smashes |
|
# the "Log" variable. |
|
# |
|
# The array's elements are: |
|
# |
|
# access_path : List of paths accessible to the child. |
|
# access_path,norm : Ditto, in normalized form. |
|
# access_path,child : Ditto, as the path tokens as seen by the child. |
|
# access_path,map : dict ( token -> path ) |
|
# access_path,remap : dict ( path -> token ) |
|
# auto_path : List of paths requested by the caller as child's ::auto_path. |
|
# tm_path_child : List of TM root directories, as tokens seen by the child. |
|
# staticsok : Value of option -statics |
|
# nestedok : Value of option -nested |
|
# cleanupHook : Value of option -deleteHook |
|
# |
|
# In principle, the child can change its value of ::auto_path - |
|
# - a package might add a path (that is already in the access path) for |
|
# access to tclIndex files; |
|
# - the script might remove some elements of the auto_path. |
|
# However, this is really the business of the parent, and the auto_path will |
|
# be reset whenever the token mapping changes (i.e. when option -accessPath is |
|
# used to change the access path). |
|
# -autoPath is now stored in the array and is no longer obtained from |
|
# the child. |
|
|
|
} |
|
::punk::safe::system::Setup |
|
|
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
## Ready |
|
package provide punk::safe [tcl::namespace::eval punk::safe { |
|
variable pkg punk::safe |
|
variable version |
|
set version 999999.0a1.0 |
|
}] |
|
return |
|
|
|
#*** !doctools |
|
#[manpage_end] |
|
|
|
|