jpayne@68: # safe.tcl -- jpayne@68: # jpayne@68: # This file provide a safe loading/sourcing mechanism for safe interpreters. jpayne@68: # It implements a virtual path mechanism to hide the real pathnames from the jpayne@68: # child. It runs in a parent interpreter and sets up data structure and jpayne@68: # aliases that will be invoked when used from a child interpreter. jpayne@68: # jpayne@68: # See the safe.n man page for details. jpayne@68: # jpayne@68: # Copyright (c) 1996-1997 Sun Microsystems, Inc. jpayne@68: # jpayne@68: # See the file "license.terms" for information on usage and redistribution of jpayne@68: # this file, and for a DISCLAIMER OF ALL WARRANTIES. jpayne@68: jpayne@68: # jpayne@68: # The implementation is based on namespaces. These naming conventions are jpayne@68: # followed: jpayne@68: # Private procs starts with uppercase. jpayne@68: # Public procs are exported and starts with lowercase jpayne@68: # jpayne@68: jpayne@68: # Needed utilities package jpayne@68: package require opt 0.4.8 jpayne@68: jpayne@68: # Create the safe namespace jpayne@68: namespace eval ::safe { jpayne@68: # Exported API: jpayne@68: namespace export interpCreate interpInit interpConfigure interpDelete \ jpayne@68: interpAddToAccessPath interpFindInAccessPath setLogCmd jpayne@68: } jpayne@68: jpayne@68: # Helper function to resolve the dual way of specifying staticsok (either jpayne@68: # by -noStatics or -statics 0) jpayne@68: proc ::safe::InterpStatics {} { jpayne@68: foreach v {Args statics noStatics} { jpayne@68: upvar $v $v jpayne@68: } jpayne@68: set flag [::tcl::OptProcArgGiven -noStatics] jpayne@68: if {$flag && (!$noStatics == !$statics) jpayne@68: && ([::tcl::OptProcArgGiven -statics])} { jpayne@68: return -code error\ jpayne@68: "conflicting values given for -statics and -noStatics" jpayne@68: } jpayne@68: if {$flag} { jpayne@68: return [expr {!$noStatics}] jpayne@68: } else { jpayne@68: return $statics jpayne@68: } jpayne@68: } jpayne@68: jpayne@68: # Helper function to resolve the dual way of specifying nested loading jpayne@68: # (either by -nestedLoadOk or -nested 1) jpayne@68: proc ::safe::InterpNested {} { jpayne@68: foreach v {Args nested nestedLoadOk} { jpayne@68: upvar $v $v jpayne@68: } jpayne@68: set flag [::tcl::OptProcArgGiven -nestedLoadOk] jpayne@68: # note that the test here is the opposite of the "InterpStatics" one jpayne@68: # (it is not -noNested... because of the wanted default value) jpayne@68: if {$flag && (!$nestedLoadOk != !$nested) jpayne@68: && ([::tcl::OptProcArgGiven -nested])} { jpayne@68: return -code error\ jpayne@68: "conflicting values given for -nested and -nestedLoadOk" jpayne@68: } jpayne@68: if {$flag} { jpayne@68: # another difference with "InterpStatics" jpayne@68: return $nestedLoadOk jpayne@68: } else { jpayne@68: return $nested jpayne@68: } jpayne@68: } jpayne@68: jpayne@68: #### jpayne@68: # jpayne@68: # API entry points that needs argument parsing : jpayne@68: # jpayne@68: #### jpayne@68: jpayne@68: # Interface/entry point function and front end for "Create" jpayne@68: proc ::safe::interpCreate {args} { jpayne@68: set Args [::tcl::OptKeyParse ::safe::interpCreate $args] jpayne@68: RejectExcessColons $slave jpayne@68: InterpCreate $slave $accessPath \ jpayne@68: [InterpStatics] [InterpNested] $deleteHook jpayne@68: } jpayne@68: jpayne@68: proc ::safe::interpInit {args} { jpayne@68: set Args [::tcl::OptKeyParse ::safe::interpIC $args] jpayne@68: if {![::interp exists $slave]} { jpayne@68: return -code error "\"$slave\" is not an interpreter" jpayne@68: } jpayne@68: RejectExcessColons $slave jpayne@68: InterpInit $slave $accessPath \ jpayne@68: [InterpStatics] [InterpNested] $deleteHook jpayne@68: } jpayne@68: jpayne@68: # Check that the given child is "one of us" jpayne@68: proc ::safe::CheckInterp {child} { jpayne@68: namespace upvar ::safe [VarName $child] state jpayne@68: if {![info exists state] || ![::interp exists $child]} { jpayne@68: return -code error \ jpayne@68: "\"$child\" is not an interpreter managed by ::safe::" jpayne@68: } jpayne@68: } jpayne@68: jpayne@68: # Interface/entry point function and front end for "Configure". This code jpayne@68: # is awfully pedestrian because it would need more coupling and support jpayne@68: # between the way we store the configuration values in safe::interp's and jpayne@68: # the Opt package. Obviously we would like an OptConfigure to avoid jpayne@68: # duplicating all this code everywhere. jpayne@68: # -> TODO (the app should share or access easily the program/value stored jpayne@68: # by opt) jpayne@68: jpayne@68: # This is even more complicated by the boolean flags with no values that jpayne@68: # we had the bad idea to support for the sake of user simplicity in jpayne@68: # create/init but which makes life hard in configure... jpayne@68: # So this will be hopefully written and some integrated with opt1.0 jpayne@68: # (hopefully for tcl8.1 ?) jpayne@68: proc ::safe::interpConfigure {args} { jpayne@68: switch [llength $args] { jpayne@68: 1 { jpayne@68: # If we have exactly 1 argument the semantic is to return all jpayne@68: # the current configuration. We still call OptKeyParse though jpayne@68: # we know that "child" is our given argument because it also jpayne@68: # checks for the "-help" option. jpayne@68: set Args [::tcl::OptKeyParse ::safe::interpIC $args] jpayne@68: CheckInterp $slave jpayne@68: namespace upvar ::safe [VarName $slave] state jpayne@68: jpayne@68: return [join [list \ jpayne@68: [list -accessPath $state(access_path)] \ jpayne@68: [list -statics $state(staticsok)] \ jpayne@68: [list -nested $state(nestedok)] \ jpayne@68: [list -deleteHook $state(cleanupHook)]]] jpayne@68: } jpayne@68: 2 { jpayne@68: # If we have exactly 2 arguments the semantic is a "configure jpayne@68: # get" jpayne@68: lassign $args slave arg jpayne@68: jpayne@68: # get the flag sub program (we 'know' about Opt's internal jpayne@68: # representation of data) jpayne@68: set desc [lindex [::tcl::OptKeyGetDesc ::safe::interpIC] 2] jpayne@68: set hits [::tcl::OptHits desc $arg] jpayne@68: if {$hits > 1} { jpayne@68: return -code error [::tcl::OptAmbigous $desc $arg] jpayne@68: } elseif {$hits == 0} { jpayne@68: return -code error [::tcl::OptFlagUsage $desc $arg] jpayne@68: } jpayne@68: CheckInterp $slave jpayne@68: namespace upvar ::safe [VarName $slave] state jpayne@68: jpayne@68: set item [::tcl::OptCurDesc $desc] jpayne@68: set name [::tcl::OptName $item] jpayne@68: switch -exact -- $name { jpayne@68: -accessPath { jpayne@68: return [list -accessPath $state(access_path)] jpayne@68: } jpayne@68: -statics { jpayne@68: return [list -statics $state(staticsok)] jpayne@68: } jpayne@68: -nested { jpayne@68: return [list -nested $state(nestedok)] jpayne@68: } jpayne@68: -deleteHook { jpayne@68: return [list -deleteHook $state(cleanupHook)] jpayne@68: } jpayne@68: -noStatics { jpayne@68: # it is most probably a set in fact but we would need jpayne@68: # then to jump to the set part and it is not *sure* jpayne@68: # that it is a set action that the user want, so force jpayne@68: # it to use the unambigous -statics ?value? instead: jpayne@68: return -code error\ jpayne@68: "ambigous query (get or set -noStatics ?)\ jpayne@68: use -statics instead" jpayne@68: } jpayne@68: -nestedLoadOk { jpayne@68: return -code error\ jpayne@68: "ambigous query (get or set -nestedLoadOk ?)\ jpayne@68: use -nested instead" jpayne@68: } jpayne@68: default { jpayne@68: return -code error "unknown flag $name (bug)" jpayne@68: } jpayne@68: } jpayne@68: } jpayne@68: default { jpayne@68: # Otherwise we want to parse the arguments like init and jpayne@68: # create did jpayne@68: set Args [::tcl::OptKeyParse ::safe::interpIC $args] jpayne@68: CheckInterp $slave jpayne@68: namespace upvar ::safe [VarName $slave] state jpayne@68: jpayne@68: # Get the current (and not the default) values of whatever has jpayne@68: # not been given: jpayne@68: if {![::tcl::OptProcArgGiven -accessPath]} { jpayne@68: set doreset 0 jpayne@68: set accessPath $state(access_path) jpayne@68: } else { jpayne@68: set doreset 1 jpayne@68: } jpayne@68: if { jpayne@68: ![::tcl::OptProcArgGiven -statics] jpayne@68: && ![::tcl::OptProcArgGiven -noStatics] jpayne@68: } then { jpayne@68: set statics $state(staticsok) jpayne@68: } else { jpayne@68: set statics [InterpStatics] jpayne@68: } jpayne@68: if { jpayne@68: [::tcl::OptProcArgGiven -nested] || jpayne@68: [::tcl::OptProcArgGiven -nestedLoadOk] jpayne@68: } then { jpayne@68: set nested [InterpNested] jpayne@68: } else { jpayne@68: set nested $state(nestedok) jpayne@68: } jpayne@68: if {![::tcl::OptProcArgGiven -deleteHook]} { jpayne@68: set deleteHook $state(cleanupHook) jpayne@68: } jpayne@68: # we can now reconfigure : jpayne@68: InterpSetConfig $slave $accessPath $statics $nested $deleteHook jpayne@68: # auto_reset the child (to completly synch the new access_path) jpayne@68: if {$doreset} { jpayne@68: if {[catch {::interp eval $slave {auto_reset}} msg]} { jpayne@68: Log $slave "auto_reset failed: $msg" jpayne@68: } else { jpayne@68: Log $slave "successful auto_reset" NOTICE jpayne@68: } jpayne@68: jpayne@68: # Sync the paths used to search for Tcl modules. jpayne@68: ::interp eval $slave {tcl::tm::path remove {*}[tcl::tm::list]} jpayne@68: if {[llength $state(tm_path_slave)] > 0} { jpayne@68: ::interp eval $slave [list \ jpayne@68: ::tcl::tm::add {*}[lreverse $state(tm_path_slave)]] jpayne@68: } jpayne@68: jpayne@68: # Remove stale "package ifneeded" data for non-loaded packages. jpayne@68: # - Not for loaded packages, because "package forget" erases jpayne@68: # data from "package provide" as well as "package ifneeded". jpayne@68: # - This is OK because the script cannot reload any version of jpayne@68: # the package unless it first does "package forget". jpayne@68: foreach pkg [::interp eval $slave {package names}] { jpayne@68: if {[::interp eval $slave [list package provide $pkg]] eq ""} { jpayne@68: ::interp eval $slave [list package forget $pkg] jpayne@68: } jpayne@68: } jpayne@68: } jpayne@68: return jpayne@68: } jpayne@68: } jpayne@68: } jpayne@68: jpayne@68: #### jpayne@68: # jpayne@68: # Functions that actually implements the exported APIs jpayne@68: # jpayne@68: #### jpayne@68: jpayne@68: # jpayne@68: # safe::InterpCreate : doing the real job jpayne@68: # jpayne@68: # This procedure creates a safe interpreter and initializes it with the safe jpayne@68: # base aliases. jpayne@68: # NB: child name must be simple alphanumeric string, no spaces, no (), no jpayne@68: # {},... {because the state array is stored as part of the name} jpayne@68: # jpayne@68: # Returns the child name. jpayne@68: # jpayne@68: # Optional Arguments : jpayne@68: # + child name : if empty, generated name will be used jpayne@68: # + access_path: path list controlling where load/source can occur, jpayne@68: # if empty: the parent auto_path will be used. jpayne@68: # + staticsok : flag, if 0 :no static package can be loaded (load {} Xxx) jpayne@68: # if 1 :static packages are ok. jpayne@68: # + nestedok: flag, if 0 :no loading to sub-sub interps (load xx xx sub) jpayne@68: # if 1 : multiple levels are ok. jpayne@68: jpayne@68: # use the full name and no indent so auto_mkIndex can find us jpayne@68: proc ::safe::InterpCreate { jpayne@68: child jpayne@68: access_path jpayne@68: staticsok jpayne@68: nestedok jpayne@68: deletehook jpayne@68: } { jpayne@68: # Create the child. jpayne@68: # If evaluated in ::safe, the interpreter command for foo is ::foo; jpayne@68: # but for foo::bar is safe::foo::bar. So evaluate in :: instead. jpayne@68: if {$child ne ""} { jpayne@68: namespace eval :: [list ::interp create -safe $child] jpayne@68: } else { jpayne@68: # empty argument: generate child name jpayne@68: set child [::interp create -safe] jpayne@68: } jpayne@68: Log $child "Created" NOTICE jpayne@68: jpayne@68: # Initialize it. (returns child name) jpayne@68: InterpInit $child $access_path $staticsok $nestedok $deletehook jpayne@68: } jpayne@68: jpayne@68: # jpayne@68: # InterpSetConfig (was setAccessPath) : jpayne@68: # Sets up child virtual auto_path and corresponding structure within jpayne@68: # the parent. Also sets the tcl_library in the child to be the first jpayne@68: # directory in the path. jpayne@68: # NB: If you change the path after the child has been initialized you jpayne@68: # probably need to call "auto_reset" in the child in order that it gets jpayne@68: # the right auto_index() array values. jpayne@68: jpayne@68: proc ::safe::InterpSetConfig {child access_path staticsok nestedok deletehook} { jpayne@68: global auto_path jpayne@68: jpayne@68: # determine and store the access path if empty jpayne@68: if {$access_path eq ""} { jpayne@68: set access_path $auto_path jpayne@68: jpayne@68: # Make sure that tcl_library is in auto_path and at the first jpayne@68: # position (needed by setAccessPath) jpayne@68: set where [lsearch -exact $access_path [info library]] jpayne@68: if {$where < 0} { jpayne@68: # not found, add it. jpayne@68: set access_path [linsert $access_path 0 [info library]] jpayne@68: Log $child "tcl_library was not in auto_path,\ jpayne@68: added it to slave's access_path" NOTICE jpayne@68: } elseif {$where != 0} { jpayne@68: # not first, move it first jpayne@68: set access_path [linsert \ jpayne@68: [lreplace $access_path $where $where] \ jpayne@68: 0 [info library]] jpayne@68: Log $child "tcl_libray was not in first in auto_path,\ jpayne@68: moved it to front of slave's access_path" NOTICE jpayne@68: } jpayne@68: jpayne@68: # Add 1st level sub dirs (will searched by auto loading from tcl jpayne@68: # code in the child using glob and thus fail, so we add them here jpayne@68: # so by default it works the same). jpayne@68: set access_path [AddSubDirs $access_path] jpayne@68: } jpayne@68: jpayne@68: Log $child "Setting accessPath=($access_path) staticsok=$staticsok\ jpayne@68: nestedok=$nestedok deletehook=($deletehook)" NOTICE jpayne@68: jpayne@68: namespace upvar ::safe [VarName $child] state jpayne@68: jpayne@68: # clear old autopath if it existed jpayne@68: # build new one jpayne@68: # Extend the access list with the paths used to look for Tcl Modules. jpayne@68: # We save the virtual form separately as well, as syncing it with the jpayne@68: # child has to be deferred until the necessary commands are present for jpayne@68: # setup. jpayne@68: jpayne@68: set norm_access_path {} jpayne@68: set slave_access_path {} jpayne@68: set map_access_path {} jpayne@68: set remap_access_path {} jpayne@68: set slave_tm_path {} jpayne@68: jpayne@68: set i 0 jpayne@68: foreach dir $access_path { jpayne@68: set token [PathToken $i] jpayne@68: lappend slave_access_path $token jpayne@68: lappend map_access_path $token $dir jpayne@68: lappend remap_access_path $dir $token jpayne@68: lappend norm_access_path [file normalize $dir] jpayne@68: incr i jpayne@68: } jpayne@68: jpayne@68: set morepaths [::tcl::tm::list] jpayne@68: set firstpass 1 jpayne@68: while {[llength $morepaths]} { jpayne@68: set addpaths $morepaths jpayne@68: set morepaths {} jpayne@68: jpayne@68: foreach dir $addpaths { jpayne@68: # Prevent the addition of dirs on the tm list to the jpayne@68: # result if they are already known. jpayne@68: if {[dict exists $remap_access_path $dir]} { jpayne@68: if {$firstpass} { jpayne@68: # $dir is in [::tcl::tm::list] and belongs in the slave_tm_path. jpayne@68: # Later passes handle subdirectories, which belong in the jpayne@68: # access path but not in the module path. jpayne@68: lappend slave_tm_path [dict get $remap_access_path $dir] jpayne@68: } jpayne@68: continue jpayne@68: } jpayne@68: jpayne@68: set token [PathToken $i] jpayne@68: lappend access_path $dir jpayne@68: lappend slave_access_path $token jpayne@68: lappend map_access_path $token $dir jpayne@68: lappend remap_access_path $dir $token jpayne@68: lappend norm_access_path [file normalize $dir] jpayne@68: if {$firstpass} { jpayne@68: # $dir is in [::tcl::tm::list] and belongs in the slave_tm_path. jpayne@68: # Later passes handle subdirectories, which belong in the jpayne@68: # access path but not in the module path. jpayne@68: lappend slave_tm_path $token jpayne@68: } jpayne@68: incr i jpayne@68: jpayne@68: # [Bug 2854929] jpayne@68: # Recursively find deeper paths which may contain jpayne@68: # modules. Required to handle modules with names like jpayne@68: # 'platform::shell', which translate into jpayne@68: # 'platform/shell-X.tm', i.e arbitrarily deep jpayne@68: # subdirectories. jpayne@68: lappend morepaths {*}[glob -nocomplain -directory $dir -type d *] jpayne@68: } jpayne@68: set firstpass 0 jpayne@68: } jpayne@68: jpayne@68: set state(access_path) $access_path jpayne@68: set state(access_path,map) $map_access_path jpayne@68: set state(access_path,remap) $remap_access_path jpayne@68: set state(access_path,norm) $norm_access_path jpayne@68: set state(access_path,slave) $slave_access_path jpayne@68: set state(tm_path_slave) $slave_tm_path jpayne@68: set state(staticsok) $staticsok jpayne@68: set state(nestedok) $nestedok jpayne@68: set state(cleanupHook) $deletehook jpayne@68: jpayne@68: SyncAccessPath $child jpayne@68: return jpayne@68: } jpayne@68: jpayne@68: # jpayne@68: # jpayne@68: # FindInAccessPath: jpayne@68: # Search for a real directory and returns its virtual Id (including the jpayne@68: # "$") jpayne@68: proc ::safe::interpFindInAccessPath {child path} { jpayne@68: CheckInterp $child jpayne@68: namespace upvar ::safe [VarName $child] state jpayne@68: jpayne@68: if {![dict exists $state(access_path,remap) $path]} { jpayne@68: return -code error "$path not found in access path" jpayne@68: } jpayne@68: jpayne@68: return [dict get $state(access_path,remap) $path] jpayne@68: } jpayne@68: jpayne@68: # jpayne@68: # addToAccessPath: jpayne@68: # add (if needed) a real directory to access path and return its jpayne@68: # virtual token (including the "$"). jpayne@68: proc ::safe::interpAddToAccessPath {child path} { jpayne@68: # first check if the directory is already in there jpayne@68: # (inlined interpFindInAccessPath). jpayne@68: CheckInterp $child jpayne@68: namespace upvar ::safe [VarName $child] state jpayne@68: jpayne@68: if {[dict exists $state(access_path,remap) $path]} { jpayne@68: return [dict get $state(access_path,remap) $path] jpayne@68: } jpayne@68: jpayne@68: # new one, add it: jpayne@68: set token [PathToken [llength $state(access_path)]] jpayne@68: jpayne@68: lappend state(access_path) $path jpayne@68: lappend state(access_path,slave) $token jpayne@68: lappend state(access_path,map) $token $path jpayne@68: lappend state(access_path,remap) $path $token jpayne@68: lappend state(access_path,norm) [file normalize $path] jpayne@68: jpayne@68: SyncAccessPath $child jpayne@68: return $token jpayne@68: } jpayne@68: jpayne@68: # This procedure applies the initializations to an already existing jpayne@68: # interpreter. It is useful when you want to install the safe base aliases jpayne@68: # into a preexisting safe interpreter. jpayne@68: proc ::safe::InterpInit { jpayne@68: child jpayne@68: access_path jpayne@68: staticsok jpayne@68: nestedok jpayne@68: deletehook jpayne@68: } { jpayne@68: # Configure will generate an access_path when access_path is empty. jpayne@68: InterpSetConfig $child $access_path $staticsok $nestedok $deletehook jpayne@68: jpayne@68: # NB we need to add [namespace current], aliases are always absolute jpayne@68: # paths. jpayne@68: jpayne@68: # These aliases let the child load files to define new commands jpayne@68: # This alias lets the child use the encoding names, convertfrom, jpayne@68: # convertto, and system, but not "encoding system " to set the jpayne@68: # system encoding. jpayne@68: # Handling Tcl Modules, we need a restricted form of Glob. jpayne@68: # This alias interposes on the 'exit' command and cleanly terminates jpayne@68: # the child. jpayne@68: jpayne@68: foreach {command alias} { jpayne@68: source AliasSource jpayne@68: load AliasLoad jpayne@68: encoding AliasEncoding jpayne@68: exit interpDelete jpayne@68: glob AliasGlob jpayne@68: } { jpayne@68: ::interp alias $child $command {} [namespace current]::$alias $child jpayne@68: } jpayne@68: jpayne@68: # This alias lets the child have access to a subset of the 'file' jpayne@68: # command functionality. jpayne@68: jpayne@68: ::interp expose $child file jpayne@68: foreach subcommand {dirname extension rootname tail} { jpayne@68: ::interp alias $child ::tcl::file::$subcommand {} \ jpayne@68: ::safe::AliasFileSubcommand $child $subcommand jpayne@68: } jpayne@68: foreach subcommand { jpayne@68: atime attributes copy delete executable exists isdirectory isfile jpayne@68: link lstat mtime mkdir nativename normalize owned readable readlink jpayne@68: rename size stat tempfile type volumes writable jpayne@68: } { jpayne@68: ::interp alias $child ::tcl::file::$subcommand {} \ jpayne@68: ::safe::BadSubcommand $child file $subcommand jpayne@68: } jpayne@68: jpayne@68: # Subcommands of info jpayne@68: foreach {subcommand alias} { jpayne@68: nameofexecutable AliasExeName jpayne@68: } { jpayne@68: ::interp alias $child ::tcl::info::$subcommand \ jpayne@68: {} [namespace current]::$alias $child jpayne@68: } jpayne@68: jpayne@68: # The allowed child variables already have been set by Tcl_MakeSafe(3) jpayne@68: jpayne@68: # Source init.tcl and tm.tcl into the child, to get auto_load and jpayne@68: # other procedures defined: jpayne@68: jpayne@68: if {[catch {::interp eval $child { jpayne@68: source [file join $tcl_library init.tcl] jpayne@68: }} msg opt]} { jpayne@68: Log $child "can't source init.tcl ($msg)" jpayne@68: return -options $opt "can't source init.tcl into slave $child ($msg)" jpayne@68: } jpayne@68: jpayne@68: if {[catch {::interp eval $child { jpayne@68: source [file join $tcl_library tm.tcl] jpayne@68: }} msg opt]} { jpayne@68: Log $child "can't source tm.tcl ($msg)" jpayne@68: return -options $opt "can't source tm.tcl into slave $child ($msg)" jpayne@68: } jpayne@68: jpayne@68: # Sync the paths used to search for Tcl modules. This can be done only jpayne@68: # now, after tm.tcl was loaded. jpayne@68: namespace upvar ::safe [VarName $child] state jpayne@68: if {[llength $state(tm_path_slave)] > 0} { jpayne@68: ::interp eval $child [list \ jpayne@68: ::tcl::tm::add {*}[lreverse $state(tm_path_slave)]] jpayne@68: } jpayne@68: return $child jpayne@68: } jpayne@68: jpayne@68: # Add (only if needed, avoid duplicates) 1 level of sub directories to an jpayne@68: # existing path list. Also removes non directories from the returned jpayne@68: # list. jpayne@68: proc ::safe::AddSubDirs {pathList} { jpayne@68: set res {} jpayne@68: foreach dir $pathList { jpayne@68: if {[file isdirectory $dir]} { jpayne@68: # check that we don't have it yet as a children of a previous jpayne@68: # dir jpayne@68: if {$dir ni $res} { jpayne@68: lappend res $dir jpayne@68: } jpayne@68: foreach sub [glob -directory $dir -nocomplain *] { jpayne@68: if {[file isdirectory $sub] && ($sub ni $res)} { jpayne@68: # new sub dir, add it ! jpayne@68: lappend res $sub jpayne@68: } jpayne@68: } jpayne@68: } jpayne@68: } jpayne@68: return $res jpayne@68: } jpayne@68: jpayne@68: # This procedure deletes a safe interpreter managed by Safe Tcl and cleans up jpayne@68: # associated state. jpayne@68: # - The command will also delete non-Safe-Base interpreters. jpayne@68: # - This is regrettable, but to avoid breaking existing code this should be jpayne@68: # amended at the next major revision by uncommenting "CheckInterp". jpayne@68: jpayne@68: proc ::safe::interpDelete {child} { jpayne@68: Log $child "About to delete" NOTICE jpayne@68: jpayne@68: # CheckInterp $child jpayne@68: namespace upvar ::safe [VarName $child] state jpayne@68: jpayne@68: # When an interpreter is deleted with [interp delete], any sub-interpreters jpayne@68: # are deleted automatically, but this leaves behind their data in the Safe jpayne@68: # Base. To clean up properly, we call safe::interpDelete recursively on each jpayne@68: # Safe Base sub-interpreter, so each one is deleted cleanly and not by jpayne@68: # the automatic mechanism built into [interp delete]. jpayne@68: foreach sub [interp children $child] { jpayne@68: if {[info exists ::safe::[VarName [list $child $sub]]]} { jpayne@68: ::safe::interpDelete [list $child $sub] jpayne@68: } jpayne@68: } jpayne@68: jpayne@68: # If the child has a cleanup hook registered, call it. Check the jpayne@68: # existance because we might be called to delete an interp which has jpayne@68: # not been registered with us at all jpayne@68: jpayne@68: if {[info exists state(cleanupHook)]} { jpayne@68: set hook $state(cleanupHook) jpayne@68: if {[llength $hook]} { jpayne@68: # remove the hook now, otherwise if the hook calls us somehow, jpayne@68: # we'll loop jpayne@68: unset state(cleanupHook) jpayne@68: try { jpayne@68: {*}$hook $child jpayne@68: } on error err { jpayne@68: Log $child "Delete hook error ($err)" jpayne@68: } jpayne@68: } jpayne@68: } jpayne@68: jpayne@68: # Discard the global array of state associated with the child, and jpayne@68: # delete the interpreter. jpayne@68: jpayne@68: if {[info exists state]} { jpayne@68: unset state jpayne@68: } jpayne@68: jpayne@68: # if we have been called twice, the interp might have been deleted jpayne@68: # already jpayne@68: if {[::interp exists $child]} { jpayne@68: ::interp delete $child jpayne@68: Log $child "Deleted" NOTICE jpayne@68: } jpayne@68: jpayne@68: return jpayne@68: } jpayne@68: jpayne@68: # Set (or get) the logging mecanism jpayne@68: jpayne@68: proc ::safe::setLogCmd {args} { jpayne@68: variable Log jpayne@68: set la [llength $args] jpayne@68: if {$la == 0} { jpayne@68: return $Log jpayne@68: } elseif {$la == 1} { jpayne@68: set Log [lindex $args 0] jpayne@68: } else { jpayne@68: set Log $args jpayne@68: } jpayne@68: jpayne@68: if {$Log eq ""} { jpayne@68: # Disable logging completely. Calls to it will be compiled out jpayne@68: # of all users. jpayne@68: proc ::safe::Log {args} {} jpayne@68: } else { jpayne@68: # Activate logging, define proper command. jpayne@68: jpayne@68: proc ::safe::Log {child msg {type ERROR}} { jpayne@68: variable Log jpayne@68: {*}$Log "$type for slave $child : $msg" jpayne@68: return jpayne@68: } jpayne@68: } jpayne@68: } jpayne@68: jpayne@68: # ------------------- END OF PUBLIC METHODS ------------ jpayne@68: jpayne@68: # jpayne@68: # Sets the child auto_path to the parent recorded value. Also sets jpayne@68: # tcl_library to the first token of the virtual path. jpayne@68: # jpayne@68: proc ::safe::SyncAccessPath {child} { jpayne@68: namespace upvar ::safe [VarName $child] state jpayne@68: jpayne@68: set slave_access_path $state(access_path,slave) jpayne@68: ::interp eval $child [list set auto_path $slave_access_path] jpayne@68: jpayne@68: Log $child "auto_path in $child has been set to $slave_access_path"\ jpayne@68: NOTICE jpayne@68: jpayne@68: # This code assumes that info library is the first element in the jpayne@68: # list of auto_path's. See -> InterpSetConfig for the code which jpayne@68: # ensures this condition. jpayne@68: jpayne@68: ::interp eval $child [list \ jpayne@68: set tcl_library [lindex $slave_access_path 0]] jpayne@68: } jpayne@68: jpayne@68: # Returns the virtual token for directory number N. jpayne@68: proc ::safe::PathToken {n} { jpayne@68: # We need to have a ":" in the token string so [file join] on the jpayne@68: # mac won't turn it into a relative path. jpayne@68: return "\$p(:$n:)" ;# Form tested by case 7.2 jpayne@68: } jpayne@68: jpayne@68: # jpayne@68: # translate virtual path into real path jpayne@68: # jpayne@68: proc ::safe::TranslatePath {child path} { jpayne@68: namespace upvar ::safe [VarName $child] state jpayne@68: jpayne@68: # somehow strip the namespaces 'functionality' out (the danger is that jpayne@68: # we would strip valid macintosh "../" queries... : jpayne@68: if {[string match "*::*" $path] || [string match "*..*" $path]} { jpayne@68: return -code error "invalid characters in path $path" jpayne@68: } jpayne@68: jpayne@68: # Use a cached map instead of computed local vars and subst. jpayne@68: jpayne@68: return [string map $state(access_path,map) $path] jpayne@68: } jpayne@68: jpayne@68: # file name control (limit access to files/resources that should be a jpayne@68: # valid tcl source file) jpayne@68: proc ::safe::CheckFileName {child file} { jpayne@68: # This used to limit what can be sourced to ".tcl" and forbid files jpayne@68: # with more than 1 dot and longer than 14 chars, but I changed that jpayne@68: # for 8.4 as a safe interp has enough internal protection already to jpayne@68: # allow sourcing anything. - hobbs jpayne@68: jpayne@68: if {![file exists $file]} { jpayne@68: # don't tell the file path jpayne@68: return -code error "no such file or directory" jpayne@68: } jpayne@68: jpayne@68: if {![file readable $file]} { jpayne@68: # don't tell the file path jpayne@68: return -code error "not readable" jpayne@68: } jpayne@68: } jpayne@68: jpayne@68: # AliasFileSubcommand handles selected subcommands of [file] in safe jpayne@68: # interpreters that are *almost* safe. In particular, it just acts to jpayne@68: # prevent discovery of what home directories exist. jpayne@68: jpayne@68: proc ::safe::AliasFileSubcommand {child subcommand name} { jpayne@68: if {[string match ~* $name]} { jpayne@68: set name ./$name jpayne@68: } jpayne@68: tailcall ::interp invokehidden $child tcl:file:$subcommand $name jpayne@68: } jpayne@68: jpayne@68: # AliasGlob is the target of the "glob" alias in safe interpreters. jpayne@68: jpayne@68: proc ::safe::AliasGlob {child args} { jpayne@68: Log $child "GLOB ! $args" NOTICE jpayne@68: set cmd {} jpayne@68: set at 0 jpayne@68: array set got { jpayne@68: -directory 0 jpayne@68: -nocomplain 0 jpayne@68: -join 0 jpayne@68: -tails 0 jpayne@68: -- 0 jpayne@68: } jpayne@68: jpayne@68: if {$::tcl_platform(platform) eq "windows"} { jpayne@68: set dirPartRE {^(.*)[\\/]([^\\/]*)$} jpayne@68: } else { jpayne@68: set dirPartRE {^(.*)/([^/]*)$} jpayne@68: } jpayne@68: jpayne@68: set dir {} jpayne@68: set virtualdir {} jpayne@68: jpayne@68: while {$at < [llength $args]} { jpayne@68: switch -glob -- [set opt [lindex $args $at]] { jpayne@68: -nocomplain - -- - -tails { jpayne@68: lappend cmd $opt jpayne@68: set got($opt) 1 jpayne@68: incr at jpayne@68: } jpayne@68: -join { jpayne@68: set got($opt) 1 jpayne@68: incr at jpayne@68: } jpayne@68: -types - -type { jpayne@68: lappend cmd -types [lindex $args [incr at]] jpayne@68: incr at jpayne@68: } jpayne@68: -directory { jpayne@68: if {$got($opt)} { jpayne@68: return -code error \ jpayne@68: {"-directory" cannot be used with "-path"} jpayne@68: } jpayne@68: set got($opt) 1 jpayne@68: set virtualdir [lindex $args [incr at]] jpayne@68: incr at jpayne@68: } jpayne@68: -* { jpayne@68: Log $child "Safe base rejecting glob option '$opt'" jpayne@68: return -code error "Safe base rejecting glob option '$opt'" jpayne@68: } jpayne@68: default { jpayne@68: break jpayne@68: } jpayne@68: } jpayne@68: if {$got(--)} break jpayne@68: } jpayne@68: jpayne@68: # Get the real path from the virtual one and check that the path is in the jpayne@68: # access path of that child. Done after basic argument processing so that jpayne@68: # we know if -nocomplain is set. jpayne@68: if {$got(-directory)} { jpayne@68: try { jpayne@68: set dir [TranslatePath $child $virtualdir] jpayne@68: DirInAccessPath $child $dir jpayne@68: } on error msg { jpayne@68: Log $child $msg jpayne@68: if {$got(-nocomplain)} return jpayne@68: return -code error "permission denied" jpayne@68: } jpayne@68: if {$got(--)} { jpayne@68: set cmd [linsert $cmd end-1 -directory $dir] jpayne@68: } else { jpayne@68: lappend cmd -directory $dir jpayne@68: } jpayne@68: } else { jpayne@68: # The code after this "if ... else" block would conspire to return with jpayne@68: # no results in this case, if it were allowed to proceed. Instead, jpayne@68: # return now and reduce the number of cases to be considered later. jpayne@68: Log $child {option -directory must be supplied} jpayne@68: if {$got(-nocomplain)} return jpayne@68: return -code error "permission denied" jpayne@68: } jpayne@68: jpayne@68: # Apply the -join semantics ourselves. jpayne@68: if {$got(-join)} { jpayne@68: set args [lreplace $args $at end [join [lrange $args $at end] "/"]] jpayne@68: } jpayne@68: jpayne@68: # Process the pattern arguments. If we've done a join there is only one jpayne@68: # pattern argument. jpayne@68: jpayne@68: set firstPattern [llength $cmd] jpayne@68: foreach opt [lrange $args $at end] { jpayne@68: if {![regexp $dirPartRE $opt -> thedir thefile]} { jpayne@68: set thedir . jpayne@68: # The *.tm search comes here. jpayne@68: } jpayne@68: # "Special" treatment for (joined) argument {*/pkgIndex.tcl}. jpayne@68: # Do the expansion of "*" here, and filter out any directories that are jpayne@68: # not in the access path. The outcome is to lappend to cmd a path of jpayne@68: # the form $virtualdir/subdir/pkgIndex.tcl for each subdirectory subdir, jpayne@68: # after removing any subdir that are not in the access path. jpayne@68: if {($thedir eq "*") && ($thefile eq "pkgIndex.tcl")} { jpayne@68: set mapped 0 jpayne@68: foreach d [glob -directory [TranslatePath $child $virtualdir] \ jpayne@68: -types d -tails *] { jpayne@68: catch { jpayne@68: DirInAccessPath $child \ jpayne@68: [TranslatePath $child [file join $virtualdir $d]] jpayne@68: lappend cmd [file join $d $thefile] jpayne@68: set mapped 1 jpayne@68: } jpayne@68: } jpayne@68: if {$mapped} continue jpayne@68: # Don't [continue] if */pkgIndex.tcl has no matches in the access jpayne@68: # path. The pattern will now receive the same treatment as a jpayne@68: # "non-special" pattern (and will fail because it includes a "*" in jpayne@68: # the directory name). jpayne@68: } jpayne@68: # Any directory pattern that is not an exact (i.e. non-glob) match to a jpayne@68: # directory in the access path will be rejected here. jpayne@68: # - Rejections include any directory pattern that has glob matching jpayne@68: # patterns "*", "?", backslashes, braces or square brackets, (UNLESS jpayne@68: # it corresponds to a genuine directory name AND that directory is in jpayne@68: # the access path). jpayne@68: # - The only "special matching characters" that remain in patterns for jpayne@68: # processing by glob are in the filename tail. jpayne@68: # - [file join $anything ~${foo}] is ~${foo}, which is not an exact jpayne@68: # match to any directory in the access path. Hence directory patterns jpayne@68: # that begin with "~" are rejected here. Tests safe-16.[5-8] check jpayne@68: # that "file join" remains as required and does not expand ~${foo}. jpayne@68: # - Bug [3529949] relates to unwanted expansion of ~${foo} and this is jpayne@68: # how the present code avoids the bug. All tests safe-16.* relate. jpayne@68: try { jpayne@68: DirInAccessPath $child [TranslatePath $child \ jpayne@68: [file join $virtualdir $thedir]] jpayne@68: } on error msg { jpayne@68: Log $child $msg jpayne@68: if {$got(-nocomplain)} continue jpayne@68: return -code error "permission denied" jpayne@68: } jpayne@68: lappend cmd $opt jpayne@68: } jpayne@68: jpayne@68: Log $child "GLOB = $cmd" NOTICE jpayne@68: jpayne@68: if {$got(-nocomplain) && [llength $cmd] eq $firstPattern} { jpayne@68: return jpayne@68: } jpayne@68: try { jpayne@68: # >>>>>>>>>> HERE'S THE CALL TO SAFE INTERP GLOB <<<<<<<<<< jpayne@68: # - Pattern arguments added to cmd have NOT been translated from tokens. jpayne@68: # Only the virtualdir is translated (to dir). jpayne@68: # - In the pkgIndex.tcl case, there is no "*" in the pattern arguments, jpayne@68: # which are a list of names each with tail pkgIndex.tcl. The purpose jpayne@68: # of the call to glob is to remove the names for which the file does jpayne@68: # not exist. jpayne@68: set entries [::interp invokehidden $child glob {*}$cmd] jpayne@68: } on error msg { jpayne@68: # This is the only place that a call with -nocomplain and no invalid jpayne@68: # "dash-options" can return an error. jpayne@68: Log $child $msg jpayne@68: return -code error "script error" jpayne@68: } jpayne@68: jpayne@68: Log $child "GLOB < $entries" NOTICE jpayne@68: jpayne@68: # Translate path back to what the child should see. jpayne@68: set res {} jpayne@68: set l [string length $dir] jpayne@68: foreach p $entries { jpayne@68: if {[string equal -length $l $dir $p]} { jpayne@68: set p [string replace $p 0 [expr {$l-1}] $virtualdir] jpayne@68: } jpayne@68: lappend res $p jpayne@68: } jpayne@68: jpayne@68: Log $child "GLOB > $res" NOTICE jpayne@68: return $res jpayne@68: } jpayne@68: jpayne@68: # AliasSource is the target of the "source" alias in safe interpreters. jpayne@68: jpayne@68: proc ::safe::AliasSource {child args} { jpayne@68: set argc [llength $args] jpayne@68: # Extended for handling of Tcl Modules to allow not only "source jpayne@68: # filename", but "source -encoding E filename" as well. jpayne@68: if {[lindex $args 0] eq "-encoding"} { jpayne@68: incr argc -2 jpayne@68: set encoding [lindex $args 1] jpayne@68: set at 2 jpayne@68: if {$encoding eq "identity"} { jpayne@68: Log $child "attempt to use the identity encoding" jpayne@68: return -code error "permission denied" jpayne@68: } jpayne@68: } else { jpayne@68: set at 0 jpayne@68: set encoding {} jpayne@68: } jpayne@68: if {$argc != 1} { jpayne@68: set msg "wrong # args: should be \"source ?-encoding E? fileName\"" jpayne@68: Log $child "$msg ($args)" jpayne@68: return -code error $msg jpayne@68: } jpayne@68: set file [lindex $args $at] jpayne@68: jpayne@68: # get the real path from the virtual one. jpayne@68: if {[catch { jpayne@68: set realfile [TranslatePath $child $file] jpayne@68: } msg]} { jpayne@68: Log $child $msg jpayne@68: return -code error "permission denied" jpayne@68: } jpayne@68: jpayne@68: # check that the path is in the access path of that child jpayne@68: if {[catch { jpayne@68: FileInAccessPath $child $realfile jpayne@68: } msg]} { jpayne@68: Log $child $msg jpayne@68: return -code error "permission denied" jpayne@68: } jpayne@68: jpayne@68: # Check that the filename exists and is readable. If it is not, deliver jpayne@68: # this -errorcode so that caller in tclPkgUnknown does not write a message jpayne@68: # to tclLog. Has no effect on other callers of ::source, which are in jpayne@68: # "package ifneeded" scripts. jpayne@68: if {[catch { jpayne@68: CheckFileName $child $realfile jpayne@68: } msg]} { jpayne@68: Log $child "$realfile:$msg" jpayne@68: return -code error -errorcode {POSIX EACCES} $msg jpayne@68: } jpayne@68: jpayne@68: # Passed all the tests, lets source it. Note that we do this all manually jpayne@68: # because we want to control [info script] in the child so information jpayne@68: # doesn't leak so much. [Bug 2913625] jpayne@68: set old [::interp eval $child {info script}] jpayne@68: set replacementMsg "script error" jpayne@68: set code [catch { jpayne@68: set f [open $realfile] jpayne@68: fconfigure $f -eofchar "\032 {}" jpayne@68: if {$encoding ne ""} { jpayne@68: fconfigure $f -encoding $encoding jpayne@68: } jpayne@68: set contents [read $f] jpayne@68: close $f jpayne@68: ::interp eval $child [list info script $file] jpayne@68: } msg opt] jpayne@68: if {$code == 0} { jpayne@68: set code [catch {::interp eval $child $contents} msg opt] jpayne@68: set replacementMsg $msg jpayne@68: } jpayne@68: catch {interp eval $child [list info script $old]} jpayne@68: # Note that all non-errors are fine result codes from [source], so we must jpayne@68: # take a little care to do it properly. [Bug 2923613] jpayne@68: if {$code == 1} { jpayne@68: Log $child $msg jpayne@68: return -code error $replacementMsg jpayne@68: } jpayne@68: return -code $code -options $opt $msg jpayne@68: } jpayne@68: jpayne@68: # AliasLoad is the target of the "load" alias in safe interpreters. jpayne@68: jpayne@68: proc ::safe::AliasLoad {child file args} { jpayne@68: set argc [llength $args] jpayne@68: if {$argc > 2} { jpayne@68: set msg "load error: too many arguments" jpayne@68: Log $child "$msg ($argc) {$file $args}" jpayne@68: return -code error $msg jpayne@68: } jpayne@68: jpayne@68: # package name (can be empty if file is not). jpayne@68: set package [lindex $args 0] jpayne@68: jpayne@68: namespace upvar ::safe [VarName $child] state jpayne@68: jpayne@68: # Determine where to load. load use a relative interp path and {} jpayne@68: # means self, so we can directly and safely use passed arg. jpayne@68: set target [lindex $args 1] jpayne@68: if {$target ne ""} { jpayne@68: # we will try to load into a sub sub interp; check that we want to jpayne@68: # authorize that. jpayne@68: if {!$state(nestedok)} { jpayne@68: Log $child "loading to a sub interp (nestedok)\ jpayne@68: disabled (trying to load $package to $target)" jpayne@68: return -code error "permission denied (nested load)" jpayne@68: } jpayne@68: } jpayne@68: jpayne@68: # Determine what kind of load is requested jpayne@68: if {$file eq ""} { jpayne@68: # static package loading jpayne@68: if {$package eq ""} { jpayne@68: set msg "load error: empty filename and no package name" jpayne@68: Log $child $msg jpayne@68: return -code error $msg jpayne@68: } jpayne@68: if {!$state(staticsok)} { jpayne@68: Log $child "static packages loading disabled\ jpayne@68: (trying to load $package to $target)" jpayne@68: return -code error "permission denied (static package)" jpayne@68: } jpayne@68: } else { jpayne@68: # file loading jpayne@68: jpayne@68: # get the real path from the virtual one. jpayne@68: try { jpayne@68: set file [TranslatePath $child $file] jpayne@68: } on error msg { jpayne@68: Log $child $msg jpayne@68: return -code error "permission denied" jpayne@68: } jpayne@68: jpayne@68: # check the translated path jpayne@68: try { jpayne@68: FileInAccessPath $child $file jpayne@68: } on error msg { jpayne@68: Log $child $msg jpayne@68: return -code error "permission denied (path)" jpayne@68: } jpayne@68: } jpayne@68: jpayne@68: try { jpayne@68: return [::interp invokehidden $child load $file $package $target] jpayne@68: } on error msg { jpayne@68: # Some packages return no error message. jpayne@68: set msg0 "load of binary library for package $package failed" jpayne@68: if {$msg eq {}} { jpayne@68: set msg $msg0 jpayne@68: } else { jpayne@68: set msg "$msg0: $msg" jpayne@68: } jpayne@68: Log $child $msg jpayne@68: return -code error $msg jpayne@68: } jpayne@68: } jpayne@68: jpayne@68: # FileInAccessPath raises an error if the file is not found in the list of jpayne@68: # directories contained in the (parent side recorded) child's access path. jpayne@68: jpayne@68: # the security here relies on "file dirname" answering the proper jpayne@68: # result... needs checking ? jpayne@68: proc ::safe::FileInAccessPath {child file} { jpayne@68: namespace upvar ::safe [VarName $child] state jpayne@68: set access_path $state(access_path) jpayne@68: jpayne@68: if {[file isdirectory $file]} { jpayne@68: return -code error "\"$file\": is a directory" jpayne@68: } jpayne@68: set parent [file dirname $file] jpayne@68: jpayne@68: # Normalize paths for comparison since lsearch knows nothing of jpayne@68: # potential pathname anomalies. jpayne@68: set norm_parent [file normalize $parent] jpayne@68: jpayne@68: namespace upvar ::safe [VarName $child] state jpayne@68: if {$norm_parent ni $state(access_path,norm)} { jpayne@68: return -code error "\"$file\": not in access_path" jpayne@68: } jpayne@68: } jpayne@68: jpayne@68: proc ::safe::DirInAccessPath {child dir} { jpayne@68: namespace upvar ::safe [VarName $child] state jpayne@68: set access_path $state(access_path) jpayne@68: jpayne@68: if {[file isfile $dir]} { jpayne@68: return -code error "\"$dir\": is a file" jpayne@68: } jpayne@68: jpayne@68: # Normalize paths for comparison since lsearch knows nothing of jpayne@68: # potential pathname anomalies. jpayne@68: set norm_dir [file normalize $dir] jpayne@68: jpayne@68: namespace upvar ::safe [VarName $child] state jpayne@68: if {$norm_dir ni $state(access_path,norm)} { jpayne@68: return -code error "\"$dir\": not in access_path" jpayne@68: } jpayne@68: } jpayne@68: jpayne@68: # This procedure is used to report an attempt to use an unsafe member of an jpayne@68: # ensemble command. jpayne@68: jpayne@68: proc ::safe::BadSubcommand {child command subcommand args} { jpayne@68: set msg "not allowed to invoke subcommand $subcommand of $command" jpayne@68: Log $child $msg jpayne@68: return -code error -errorcode {TCL SAFE SUBCOMMAND} $msg jpayne@68: } jpayne@68: jpayne@68: # AliasEncoding is the target of the "encoding" alias in safe interpreters. jpayne@68: jpayne@68: proc ::safe::AliasEncoding {child option args} { jpayne@68: # Note that [encoding dirs] is not supported in safe children at all jpayne@68: set subcommands {convertfrom convertto names system} jpayne@68: try { jpayne@68: set option [tcl::prefix match -error [list -level 1 -errorcode \ jpayne@68: [list TCL LOOKUP INDEX option $option]] $subcommands $option] jpayne@68: # Special case: [encoding system] ok, but [encoding system foo] not jpayne@68: if {$option eq "system" && [llength $args]} { jpayne@68: return -code error -errorcode {TCL WRONGARGS} \ jpayne@68: "wrong # args: should be \"encoding system\"" jpayne@68: } jpayne@68: } on error {msg options} { jpayne@68: Log $child $msg jpayne@68: return -options $options $msg jpayne@68: } jpayne@68: tailcall ::interp invokehidden $child encoding $option {*}$args jpayne@68: } jpayne@68: jpayne@68: # Various minor hiding of platform features. [Bug 2913625] jpayne@68: jpayne@68: proc ::safe::AliasExeName {child} { jpayne@68: return "" jpayne@68: } jpayne@68: jpayne@68: # ------------------------------------------------------------------------------ jpayne@68: # Using Interpreter Names with Namespace Qualifiers jpayne@68: # ------------------------------------------------------------------------------ jpayne@68: # (1) We wish to preserve compatibility with existing code, in which Safe Base jpayne@68: # interpreter names have no namespace qualifiers. jpayne@68: # (2) safe::interpCreate and the rest of the Safe Base previously could not jpayne@68: # accept namespace qualifiers in an interpreter name. jpayne@68: # (3) The interp command will accept namespace qualifiers in an interpreter jpayne@68: # name, but accepts distinct interpreters that will have the same command jpayne@68: # name (e.g. foo, ::foo, and :::foo) (bug 66c2e8c974). jpayne@68: # (4) To satisfy these constraints, Safe Base interpreter names will be fully jpayne@68: # qualified namespace names with no excess colons and with the leading "::" jpayne@68: # omitted. jpayne@68: # (5) Trailing "::" implies a namespace tail {}, which interp reads as {{}}. jpayne@68: # Reject such names. jpayne@68: # (6) We could: jpayne@68: # (a) EITHER reject usable but non-compliant names (e.g. excess colons) in jpayne@68: # interpCreate, interpInit; jpayne@68: # (b) OR accept such names and then translate to a compliant name in every jpayne@68: # command. jpayne@68: # The problem with (b) is that the user will expect to use the name with the jpayne@68: # interp command and will find that it is not recognised. jpayne@68: # E.g "interpCreate ::foo" creates interpreter "foo", and the user's name jpayne@68: # "::foo" works with all the Safe Base commands, but "interp eval ::foo" jpayne@68: # fails. jpayne@68: # So we choose (a). jpayne@68: # (7) The command jpayne@68: # namespace upvar ::safe S$child state jpayne@68: # becomes jpayne@68: # namespace upvar ::safe [VarName $child] state jpayne@68: # ------------------------------------------------------------------------------ jpayne@68: jpayne@68: proc ::safe::RejectExcessColons {child} { jpayne@68: set stripped [regsub -all -- {:::*} $child ::] jpayne@68: if {[string range $stripped end-1 end] eq {::}} { jpayne@68: return -code error {interpreter name must not end in "::"} jpayne@68: } jpayne@68: if {$stripped ne $child} { jpayne@68: set msg {interpreter name has excess colons in namespace separators} jpayne@68: return -code error $msg jpayne@68: } jpayne@68: if {[string range $stripped 0 1] eq {::}} { jpayne@68: return -code error {interpreter name must not begin "::"} jpayne@68: } jpayne@68: return jpayne@68: } jpayne@68: jpayne@68: proc ::safe::VarName {child} { jpayne@68: # return S$child jpayne@68: return S[string map {:: @N @ @A} $child] jpayne@68: } jpayne@68: jpayne@68: proc ::safe::Setup {} { jpayne@68: #### jpayne@68: # jpayne@68: # Setup the arguments parsing jpayne@68: # jpayne@68: #### jpayne@68: jpayne@68: # Share the descriptions jpayne@68: set temp [::tcl::OptKeyRegister { jpayne@68: {-accessPath -list {} "access path for the slave"} jpayne@68: {-noStatics "prevent loading of statically linked pkgs"} jpayne@68: {-statics true "loading of statically linked pkgs"} jpayne@68: {-nestedLoadOk "allow nested loading"} jpayne@68: {-nested false "nested loading"} jpayne@68: {-deleteHook -script {} "delete hook"} jpayne@68: }] jpayne@68: jpayne@68: # create case (slave is optional) jpayne@68: ::tcl::OptKeyRegister { jpayne@68: {?slave? -name {} "name of the slave (optional)"} jpayne@68: } ::safe::interpCreate jpayne@68: jpayne@68: # adding the flags sub programs to the command program (relying on Opt's jpayne@68: # internal implementation details) jpayne@68: lappend ::tcl::OptDesc(::safe::interpCreate) $::tcl::OptDesc($temp) jpayne@68: jpayne@68: # init and configure (slave is needed) jpayne@68: ::tcl::OptKeyRegister { jpayne@68: {slave -name {} "name of the slave"} jpayne@68: } ::safe::interpIC jpayne@68: jpayne@68: # adding the flags sub programs to the command program (relying on Opt's jpayne@68: # internal implementation details) jpayne@68: lappend ::tcl::OptDesc(::safe::interpIC) $::tcl::OptDesc($temp) jpayne@68: jpayne@68: # temp not needed anymore jpayne@68: ::tcl::OptKeyDelete $temp jpayne@68: jpayne@68: #### jpayne@68: # jpayne@68: # Default: No logging. jpayne@68: # jpayne@68: #### jpayne@68: jpayne@68: setLogCmd {} jpayne@68: jpayne@68: # Log eventually. jpayne@68: # To enable error logging, set Log to {puts stderr} for instance, jpayne@68: # via setLogCmd. jpayne@68: return jpayne@68: } jpayne@68: jpayne@68: namespace eval ::safe { jpayne@68: # internal variables jpayne@68: jpayne@68: # Log command, set via 'setLogCmd'. Logging is disabled when empty. jpayne@68: variable Log {} jpayne@68: jpayne@68: # The package maintains a state array per child interp under its jpayne@68: # control. The name of this array is S. This array is jpayne@68: # brought into scope where needed, using 'namespace upvar'. The S jpayne@68: # prefix is used to avoid that a child interp called "Log" smashes jpayne@68: # the "Log" variable. jpayne@68: # jpayne@68: # The array's elements are: jpayne@68: # jpayne@68: # access_path : List of paths accessible to the child. jpayne@68: # access_path,norm : Ditto, in normalized form. jpayne@68: # access_path,slave : Ditto, as the path tokens as seen by the child. jpayne@68: # access_path,map : dict ( token -> path ) jpayne@68: # access_path,remap : dict ( path -> token ) jpayne@68: # tm_path_slave : List of TM root directories, as tokens seen by the child. jpayne@68: # staticsok : Value of option -statics jpayne@68: # nestedok : Value of option -nested jpayne@68: # cleanupHook : Value of option -deleteHook jpayne@68: } jpayne@68: jpayne@68: ::safe::Setup