在 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
函数,这样:
- 当前包是一个全新的包,只在沙箱执行过程中存在,避免用户输入的大量不同符号污染单个包
- 用户只能看到允许的符号列表
- 禁止使用
#\:
和 #\#
等特殊字符,以防止用户访问其他包中的符号,并避免使用可能创建数组、位向量等的语法以及reader 个变量(#1=
和 #1#
),可用于创建在求值时无限循环的循环结构。
- 空列表不等同于
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
。我正在使用 GENTEMP
在 package-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 之外做其他事情。
对于沙盒环境,可能还有一些我没有考虑到的其他事情(执行时间等),请小心,但这应该已经很有用了。
使用 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
函数,这样:
- 当前包是一个全新的包,只在沙箱执行过程中存在,避免用户输入的大量不同符号污染单个包
- 用户只能看到允许的符号列表
- 禁止使用
#\:
和#\#
等特殊字符,以防止用户访问其他包中的符号,并避免使用可能创建数组、位向量等的语法以及reader 个变量(#1=
和#1#
),可用于创建在求值时无限循环的循环结构。 - 空列表不等同于
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
。我正在使用 GENTEMP
在 package-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 之外做其他事情。
对于沙盒环境,可能还有一些我没有考虑到的其他事情(执行时间等),请小心,但这应该已经很有用了。