【问题标题】:Restricting usage of keywords in Common Lisp (SBCL)在 Common Lisp (SBCL) 中限制关键字的使用
【发布时间】:2021-11-14 14:56:11
【问题描述】:

使用 SBCL 的 Common Lisp 实现,是否有可能以某种方式呈现一个禁止使用特定关键字的 REPL(有效地提供对 Common Lisp 功能子集的访问)?

【问题讨论】:

    标签: common-lisp keyword read-eval-print-loop sbcl restriction


    【解决方案1】:

    检查defpackage 宏。

    如果只想禁止部分功能,则使用:shadow关键字:

    (defpackage "MY-PACKAGE"
      (:use :common-lisp)
      (:shadow :list :cons))
    

    如果你想禁止一切,只允许某些功能,你将使用:import-from关键字:

    (defpackage "ONLY-MATH"
      (:use)
      (:import-from :common-lisp  :+ :- :* :/ :print))
    

    然后在 REPL 中调用 (in-package "MY-PACKAGE")(in-package "ONLY-MATH") 并尝试一些示例:

    ONLY-MATH 2 > (+ 1 2)
    3
    
    ONLY-MATH 3 > (cons 1 2)
    
    Error: Undefined operator CONS in form (CONS 1 2).
    

    【讨论】:

      【解决方案2】:

      虽然您只能导入特定符号,但这并不禁止它们的使用,因为您可以使用包前缀明确引用它们。

      如果您想向用户公开他们不能滥用以操纵您的系统的 REPL,您将必须安全地阅读他们的输入并明确创建评估规则。

      【讨论】:

        【解决方案3】:
        (defpackage :limited-repl
          (:use :cl :named-readtables)
          (:export #:within-sandbox
                   #:limited-repl))
        
        (in-package :limited-repl)
        

        在这个答案中,我定义了一个 within-sandbox 宏和一个 limited-repl 函数,这样:

        1. 当前包是新包,仅在沙盒执行期间存在,以避免用户输入的大量不同符号污染单个包
        2. 用户只能看到允许的符号列表
        3. 禁止使用#\:#\# 等特殊字符,以防止用户访问其他包中的符号,并避免使用可能创建数组、位向量等以及读取器变量的语法(#1=#1#),可用于创建在评估时无限循环的圆形结构。
        4. 空列表不等同于COMMON-LISP:NIL,以避免在自定义语言中引入此符号;相反,它读为零。

        可读

        我正在使用 Quicklisp 中提供的 named-readtables 库来定义自定义可读表。如有必要,这也可以通过 Common Lisp 实现。

        (ql:quickload :named-readtables)
        

        让我们为禁止字符定义一个自定义条件,这比使用(error "some string") 更简洁:

        (define-condition forbidden-character (error)
          ((character :initarg :character :reader forbidden-character.character)
           (stream :initarg :stream :reader forbidden-character.stream))
          (:report (lambda (condition stream)
                     (format stream
                             "Forbidden character ~@c"
                             (forbidden-character.character condition)))))
        

        让我们也定义一个读取器函数,该函数会为正在读取的字符发出错误信号:

        (defun forbidden-character (stream character)
          (error 'forbidden-character
                 :character character
                 :stream stream))
        

        这是在空列表上返回零的列表读取器:

        (defun read-zero-list (stream character)
          (assert (char= character #\())
          (let ((list (read-delimited-list #\) stream t)))
            (etypecase list
              (null 0)
              (cons list))))
        

        limited-repl 可读表基于标准可读表,除了两个禁止字符和一个自定义列表阅读器:

        (defreadtable limited-repl
          (:merge :standard)
          (:macro-char #\( 'read-read-zero-list)
          (:macro-char #\: 'forbidden-character nil)
          (:macro-char #\# 'forbidden-character nil))
        

        语言包

        limited-language 包定义了用户可以访问的基本语言,这里只有基本的算术符号和quit 符号:

        (defpackage limited-language
          (:use)
          (:export #:+
                   #:-
                   #:*
                   #:/
                   #:quit))
        

        我还将函数重新定义为 CL 函数的包装器,如下所示:

        (macrolet ((lift (s c) `(defun ,s (&rest args) (apply #',c args))))
          (lift limited-language:- cl:-)
          (lift limited-language:+ cl:+)
          (lift limited-language:* cl:*)
          (lift limited-language:/ cl:/))
        

        这是必要的,因为仅重新导出 CL 符号可能会产生意想不到的效果,例如用户访问绑定在 Lisp 中的变量 */,我们不希望在此处这样做。

        临时包

        以下辅助函数在*package* 绑定到使用limited-language 的新临时包的上下文中调用function。我正在使用GENTEMPpackage-names 包中生成一个新符号:

        (defpackage package-names
          (:use))
        
        (defun call-with-temporary-package (function)
          (let* ((symbol (gentemp "SANDBOX-" 'package-names))
                 (package (make-package symbol :use '(limited-language))))
            (unwind-protect (let ((*package* package))
                              (funcall function))
              (delete-package package)
              (unintern symbol 'package-names))))
        

        当没有 REPL 运行时,package-names 是空的,因为生成的符号在展开时是非驻留的。临时包也被删除。可能有一种更强大的方法来定义包的临时名称,这应该足以作为初稿。

        沙盒环境

        within-sandbox 宏建立上下文,其中 readtable 和包设置为我们想要的。我还将 *read-eval* 绑定到 nil,只是为了确保在执行 read 时不会评估代码:

        (defmacro within-sandbox (&rest body)
          `(call-with-temporary-package
            (lambda ()
              (let ((*readtable* (find-readtable 'limited-repl))
                    (*read-eval* nil))
                ,@body))))
        

        简单的有限 REPL

        最后,REPL定义如下,其中quit用于退出REPL:

        (defun limited-repl ()
          (within-sandbox
           (loop
             (format t "~&> ")
             (finish-output)
             (clear-input)
             (handler-case (let ((form (read)))
                             (when (eq form 'limited-language:quit)
                               (return))
                             (eval form))
               (cl:end-of-file ()
                 (return))
               (:no-error (v)
                 (print v))
               (error (e)
                 (format *error-output* "~&Error: ~a~%" e))))))
        

        示例

        CL-USER> (limited-repl:limited-repl)
        
        > 5
        
        5 
        > (+ () (* 4 3) (/ 7 9))
        
        115/9 
        > #1=(list 0 . #1#)
        
        Error: Forbidden character #\#
        > *
        
        Error: The variable * is unbound.
        > (cl:in-package 'cl-user)
        
        Error: Forbidden character #\:
        > quit
        NIL
        CL-USER> 
        

        结论

        当限制用户可以访问的符号时,您可以依赖eval。但是,这仍然可以在您的语言中引入 CL 特定的语义,例如单独编写 * 时会报告未定义的变量 *(可能您的语言没有变量概念)。然后,您需要编写自己的 evaluate 函数,该函数可以委托给 eval,但也可以在 Common Lisp 之外执行其他操作。 对于沙盒环境(执行时间等),我可能没有考虑其他一些事情,请小心,但这应该已经很有用了。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2016-10-27
          • 1970-01-01
          • 1970-01-01
          • 2015-04-30
          • 1970-01-01
          • 1970-01-01
          • 2017-02-27
          • 2015-09-30
          相关资源
          最近更新 更多