在 Common Lisp (SBCL) 中限制关键字的使用

Restricting usage of keywords in Common Lisp (SBCL)

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

检查 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).

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

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

(defpackage :limited-repl
  (:use :cl :named-readtables)
  (:export #:within-sandbox
           #:limited-repl))

(in-package :limited-repl)

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

  1. 当前包是一个全新的包,只在沙箱执行过程中存在,避免用户输入的大量不同符号污染单个包
  2. 用户只能看到允许的符号列表
  3. 禁止使用 #\:#\# 等特殊字符,以防止用户访问其他包中的符号,并避免使用可能创建数组、位向量等的语法以及reader 个变量(#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)))))

我们还可以定义一个 reader 函数,为正在读取的字符发出错误信号:

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

这是列表 reader,returns 在空列表中为零:

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

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

(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 宏建立上下文,其中可读表和包被设置为我们想要的。我还将 *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 之外做其他事情。 对于沙盒环境,可能还有一些我没有考虑到的其他事情(执行时间等),请小心,但这应该已经很有用了。