lua-resty-validation

Lua 和 OpenResty 的验证库(输入验证和过滤)

$ opm get bungle/lua-resty-validation

lua-resty-validation

lua-resty-validation 是一个可扩展的链式验证和过滤库,适用于 Lua 和 OpenResty。

使用 lua-resty-validation 的 Hello World

    local validation = require "resty.validation"
    
    local valid, e = validation.number:between(0, 9)(5)  -- valid = true,  e = 5
    local valid, e = validation.number:between(0, 9)(50) -- valid = false, e = "between"
    
    -- Validators can be reused
    local smallnumber = validation.number:between(0, 9)
    local valid, e = smallnumber(5)  -- valid = true,  e = 5
    local valid, e = smallnumber(50) -- valid = false, e = "between"
    
    -- Validators can do filtering (i.e. modify the value being validated)
    -- valid = true, s = "HELLO WORLD!"
    local valid, s = validation.string.upper "hello world!"
    
    -- You may extend the validation library with your own validators and filters...
    validation.validators.capitalize = function(value) 
        return true, value:gsub("^%l", string.upper)
    end
    
    -- ... and then use it
    local valid, e = validation.capitalize "abc" -- valid = true,  e = "Abc"
    
    -- You can also group validate many values
    local group = validation.new{
        artist = validation.string:minlen(5),
        number = validation.tonumber:equal(10)
    }
    
    local valid, fields, errors = group{ artist = "Eddie Vedder", number = "10" }
    
    if valid then
      print("all the group fields are valid")
    else
      print(fields.artist.name,      fields.artist.error,
            fields.artist.valid,     fields.artist.invalid,
            fields.artist.input,     fields.artist.value, ,
            fields.artist.validated, fields.artist.unvalidated)
    end
    
    -- You can even call fields to get simple name, value table
    -- (in that case all the `nil`s are removed as well)
    
    -- By default this returns only the valid fields' names and values:
    local data = fields()
    local data = fields "valid"
    
    -- To get only the invalid fields' names and values call:
    local data = fields "invalid"
    
    -- To get only the validated fields' names and values call (whether or not they are valid):
    local data = fields "validated"
    
    -- To get only the unvalidated fields' names and values call (whether or not they are valid):
    local data = fields "unvalidated"
    
    -- To get all, call:
    local data = fields "all"
    
    -- Or combine:
    local data = fields("valid", "invalid")
    
    -- This doesn't stop here. You may also want to get only some fields by their name.
    -- You can do that by calling (returns a table):
    local data = data{ "artist" }

安装

只需将 `validation.lua``validation` 目录放置在 package.path 中的 resty 目录下。如果你使用的是 OpenResty,默认位置为 /usr/local/openresty/lualib/resty

使用 OpenResty 包管理器 (opm)

    $ opm get bungle/lua-resty-validation

使用 LuaRocks

    $ luarocks install lua-resty-validation

lua-resty-validation 的 LuaRocks 仓库位于 https://luarocks.org/modules/bungle/lua-resty-validation。

内置验证器和过滤器

lua-resty-validation 附带了几个内置验证器,该项目欢迎更多验证器的贡献。

无参数的验证器和过滤器

类型验证器可用于验证被验证值的类型。这些验证器是无参数验证器(使用点 . 调用)。

  • null["nil"](因为 nil 是 Lua 中的保留字)

  • boolean

  • number

  • string

  • table

  • userdata

  • func["function"](因为 function 是 Lua 中的保留字)

  • callable(函数或带有元方法 __call 的表)

  • thread

  • integer

  • float

  • file (io.type(value) == 'file')

类型转换过滤器

  • tostring

  • tonumber

  • tointeger

  • toboolean

其他过滤器

  • toniltonull

  • abs

  • inf

  • nan

  • finite

  • positive

  • negative

  • lower

  • upper

  • trim

  • ltrim

  • rtrim

  • reverse

  • email

  • optional

示例

    local validation = require "resty.validation"
    local ok, e = validation.null(nil)
    local ok, e = validation.boolean(true)
    local ok, e = validation.number(5.2)
    local ok, e = validation.string('Hello, World!')
    local ok, e = validation.integer(10)
    local ok, e = validation.float(math.pi)
    local f = assert(io.open('filename.txt', "r"))
    local ok, e = validation.file(f)

验证工厂验证器和过滤器

验证工厂由用于验证或过滤值的各种验证器和过滤器组成(使用冒号 : 调用)。

  • type(t),验证值是否为 t 类型(参见类型验证器)

  • nil()["null"](),检查值类型是否为 nil

  • boolean(),检查值类型是否为 boolean

  • number(),检查值类型是否为 number

  • string(),检查值类型是否为 string

  • table(),检查值类型是否为 table

  • userdata(),检查值类型是否为 userdata

  • func()["function"](),检查值类型是否为 function

  • callable(),检查值是否可调用(即函数或带有元方法 __call 的表)

  • thread(),检查值类型是否为 thread

  • integer(),检查值类型是否为 integer

  • float(),检查值类型是否为 float

  • file(),检查值类型是否为 file (io.type(value) == 'file')

  • abs(),过滤值并返回绝对值 (math.abs)

  • inf(),检查值是否为 inf-inf

  • nan(),检查值是否为 nan

  • finite(),检查值是否不是 naninf-inf

  • positive(),验证值是否为正 (> 0)

  • negative(),验证值是否为负 (< 0)

  • min(min),验证值是否至少为 min (>=)

  • max(max),验证值是否最多为 max (<=)

  • between(min[, max = min]),验证值是否介于 minmax 之间

  • outside(min[, max = min]),验证值是否不介于 minmax 之间

  • divisible(number),验证值是否可以被 number 整除

  • indivisible(number),验证值是否不可被 number 整除

  • len(min[, max = min]),验证值的长度是否正好为 min 或介于 minmax 之间(UTF-8)

  • minlen(min),验证值的长度是否至少为 min(UTF-8)

  • maxlen(max),验证值的长度是否最多为 max(UTF-8)

  • equals(equal)equal(equal),验证值是否正好是某个值

  • unequals(equal)unequal(equal),验证值是否不正好是某个值

  • oneof(...),验证值是否等于提供的参数之一

  • noneof(...),验证值是否不等于提供的任何参数

  • match(pattern[, init]),验证值是否匹配(string.match)模式

  • unmatch(pattern[, init]),验证值是否不匹配(string.match)模式

  • tostring(),将值转换为字符串

  • tonumber([base]),将值转换为数字

  • tointeger(),将值转换为整数

  • toboolean(),将值转换为布尔值(使用 not not value

  • tonil()tonull(),将值转换为 nil

  • lower(),将值转换为小写(UTF-8 支持尚未实现)

  • upper(),将值转换为大写(UTF-8 支持尚未实现)

  • trim([pattern]),从左侧和右侧修剪空格(你也可以使用模式)

  • ltrim([pattern]),从左侧修剪空格(你也可以使用模式)

  • rtrim([pattern]),从右侧修剪空格(你也可以使用模式)

  • starts(starts),检查字符串是否以 starts 开头

  • ends(ends),检查字符串是否以 ends 结尾

  • reverse,反转值(字符串或数字)(UTF-8)

  • coalesce(...),如果值为 nil,则返回作为参数传递的第一个非 nil 值

  • email(),验证值是否为电子邮件地址

  • call(function),根据自定义内联验证器/过滤器验证/过滤值

  • optional([default]),如果值为空字符串 ""nil,则停止验证并返回 true,并且返回 defaultvalue

条件验证工厂验证器

对于所有验证工厂验证器,都有一个条件版本,它始终验证为 true,但你可以根据原始验证器是否验证来替换实际值。嘿,这比说起来容易做

    local validation = require "resty.validation"
    
    -- ok == true, value == "Yes, the value is nil"
    local ok, value = validation:ifnil(
        "Yes, the value is nil",
        "No, you did not supply a nil value")(nil)
    
    -- ok == true, value == "No, you did not supply a nil value"
    local ok, value = validation:ifnil(
        "Yes, the value is nil",
        "No, you did not supply a nil value")("non nil")
        
    -- ok == true, value == "Yes, the number is betweeb 1 and 10"    
    local ok, value = validation:ifbetween(1, 10,
        "Yes, the number is between 1 and 10",
        "No, the number is not between 1 and 10")(5)
    
    -- ok == true, value == "No, the number is not between 1 and 10"
    local ok, value = validation:ifbetween(1, 10,
        "Yes, the number is between 1 and 10",
        "No, the number is not between 1 and 10")(100)

条件验证工厂验证器的最后两个参数是 truthyfalsy 值。其他所有参数都传递给实际的验证工厂验证器。

分组验证器

lua-resty-validation 目前支持一些预定义的验证器

  • compare(comparison),比较两个字段并根据比较设置字段为无效或有效

  • requisite{ fields },至少需要一个必需字段,即使它们本身是可选的

  • requisites({ fields }, number),至少需要 number 个必需字段(默认情况下所有字段都必须存在)

  • call(function),调用自定义(或内联)分组验证函数

    local ispassword = validation.trim:minlen(8)
    local group = validation.new{
        password1 = ispassword,
        password2 = ispassword
    }
    group:compare "password1 == password2"
    local valid, fields, errors = group{ password1 = "qwerty123", password2 = "qwerty123" }
    
    local optional = validation:optional"".trim
    local group = validation.new{
        text = optional,
        html = optional
    }
    group:requisite{ "text", "html" }
    local valid, fields, errors = group{ text = "", html = "" }
    
    
    local optional = validation:optional ""
    local group = validation.new{
        text = optional,
        html = optional
    }
    group:requisites({ "text", "html" }, 2)
    -- or group:requisites{ "text", "html" }
    local valid, fields, errors = group{ text = "", html = "" }
    
    
    group:call(function(fields)
        if fields.text.value == "hello" then
            fields.text:reject "text cannot be 'hello'"
            fields.html:reject "because text was 'hello', this field is also invalidated"
        end
    end)

你可以在 compare 分组验证器中使用普通的 Lua 关系运算符

  • <

  • >

  • <=

  • >=

  • ==

  • ~=

requisiterequisites 检查字段值是否为 nil""(空字符串)。使用 requisite,如果所有指定的字段都为 nil"",则所有字段均无效(前提是它们本身不为无效),如果至少有一个字段有效,则所有字段均有效。requisites 的工作原理相同,但你可以定义要具有非 nil 值和非空字符串 "" 值的字段数量。这些提供了条件验证,即

  1. 我有(两个或更多)个字段

  2. 所有字段都是可选的

  3. 至少应填写一个/定义数量的字段,但我并不关心是哪一个,只要至少填写了一个/定义数量的字段即可

停止验证器

停止验证器,如 optional,就像普通的验证器一样,但不是返回 truefalse 作为验证结果或过滤后的值,你可以返回 validation.stop。此值也可以在条件验证器和支持默认值的验证器中使用。以下是 optional 验证器的实现方式

    function factory.optional(default)
        return function(value)
            if value == nil or value == "" then
                return validation.stop, default ~= nil and default or value
            end
            return true, value
        end
    end

这些大致等效

    -- Both return: true, "default" (they stop prosessing :minlen(10) on nil and "" inputs
    local input = ""
    local ok, val = validation.optional:minlen(10)(input)
    local ok, val = validation:optional(input):minlen(10)(input)
    local ok, val = validation:ifoneof("", nil, validation.stop(input), input):minlen(10)(input)

过滤值并将值设置为 nil

大多数不过滤值的验证器只返回 truefalse 作为结果。这意味着现在没有办法通知 resty.validation 实际将值设置为 nil。因此有一个解决方法,你可以返回 validation.nothing 作为值,这将更改值为 nil,例如,内置的 tonil 验证器实际上是这样实现的(伪代码)

    function()
        return true, validation.nothing
    end

自定义(内联)验证器和过滤器

有时你可能只有一个性的验证器/过滤器,你没有在其他地方使用,或者你只是想快速提供一个额外的验证器/过滤器来处理特定情况。为了使这变得容易和直接,我们在 lua-resty-validation 2.4 中引入了 call 工厂方法。以下是一个示例

    validation:call(function(value)
        -- now validate / filter the value, and return the results
        -- here we just return false (aka making validation to fail) 
        return false
    end)("Check this value"))

(当然,它不需要是内联函数,因为在 Lua 中所有函数都是一等公民,可以作为参数传递)

内置验证器扩展

目前 lua-resty-validation 支持两个可以启用的扩展或插件

  • resty.validation.ngx

  • resty.validation.tz

  • resty.validation.utf8

如果你想构建自己的验证器扩展,可以参考这些。如果你这样做,并且认为它也适合其他人使用,请考虑将你的扩展作为拉取请求提交到这个项目,非常感谢,;-)。

resty.validation.ngx 扩展

顾名思义,这组验证器扩展需要 OpenResty(至少需要 Lua Nginx 模块)。要使用此扩展,你只需

    require "resty.validation.ngx"

它将修补它将在 resty.validation 中提供的适配器,这些适配器目前是

  • escapeuri

  • unescapeuri

  • base64enc

  • base64dec

  • crc32short

  • crc32long

  • crc32

  • md5

(这些都有工厂版本和无参数版本)

ngx 扩展中还有一个使用 ngx.re.match 的正则表达式匹配器,以及参数化的 md5

  • regex(regex[, options])

  • md5([bin])

示例

    require "resty.validation.ngx"
    local validation = require "resty.validation"
    local valid, value = validation.unescapeuri.crc32("https://github.com/")
    local valid, value = validation:unescapeuri():crc32()("https://github.com/")

resty.validation.tz 扩展

这组验证器和过滤器基于 `luatz` 库,该库由 @daurnimator 开发,用于时间和日期操作。要使用此扩展,你只需

    require "resty.validation.tz"

它将修补它将在 resty.validation 中提供的适配器,这些适配器目前是

  • totimetable

  • totimestamp

(这些都有工厂版本和无参数版本)

totimestamptotimetable 过滤器非常适合 HTML5 日期和日期时间输入字段。顾名思义,totimetable 返回 luatz timetable,而 totimestamp 返回自 Unix 纪元(1970-01-01)以来的秒数,作为 Lua 数字。

示例

    require "resty.validation.tz"
    local validation = require "resty.validation"
    local valid, ts = validation.totimestamp("1990-12-31T23:59:60Z")
    local valid, ts = validation.totimestamp("1996-12-19")

resty.validation.utf8 扩展

这组验证器和过滤器基于 Quinten Lansu 开发的优秀 `utf8rewind` 库 - 一个用 C 编写的系统库,旨在扩展默认字符串处理函数,以支持 UTF-8 编码文本。它需要我的 LuaJIT FFI 包装器 `lua-resty-utf8rewind` 才能工作。安装完上述要求后,剩下的就很简单了。要使用此扩展,你只需

    require "resty.validation.utf8"

它将修补它将在 resty.validation 中提供的适配器,这些适配器目前是

  • utf8upper

  • utf8lower

  • utf8title

(这些都有工厂版本和无参数版本)

还有一些工厂验证器/过滤器

  • utf8normalize(form)

  • utf8category(category)

utf8normalize 将 UTF-8 输入规范化为以下规范化格式之一

  • C(或 NFC

  • D (或 NFD)

  • KC (或 NFKC)

  • KD (或 NFKD)

utf8category 检查输入字符串是否属于以下类别之一(因此,您可能会认为它内置了多个验证器来处理 UTF-8 字符串验证)

  • LETTER_UPPERCASE

  • LETTER_LOWERCASE

  • LETTER_TITLECASE

  • LETTER_MODIFIER

  • CASE_MAPPED

  • LETTER_OTHER

  • LETTER

  • MARK_NON_SPACING

  • MARK_SPACING

  • MARK_ENCLOSING

  • MARK

  • NUMBER_DECIMAL

  • NUMBER_LETTER

  • NUMBER_OTHER

  • NUMBER

  • PUNCTUATION_CONNECTOR

  • PUNCTUATION_DASH

  • PUNCTUATION_OPEN

  • PUNCTUATION_CLOSE

  • PUNCTUATION_INITIAL

  • PUNCTUATION_FINAL

  • PUNCTUATION_OTHER

  • PUNCTUATION

  • SYMBOL_MATH

  • SYMBOL_CURRENCY

  • SYMBOL_MODIFIER

  • SYMBOL_OTHER

  • SYMBOL

  • SEPARATOR_SPACE

  • SEPARATOR_LINE

  • SEPARATOR_PARAGRAPH

  • SEPARATOR

  • CONTROL

  • FORMAT

  • SURROGATE

  • PRIVATE_USE

  • UNASSIGNED

  • COMPATIBILITY

  • ISUPPER

  • ISLOWER

  • ISALPHA

  • ISDIGIT

  • ISALNUM

  • ISPUNCT

  • ISGRAPH

  • ISSPACE

  • ISPRINT

  • ISCNTRL

  • ISXDIGIT

  • ISBLANK

  • IGNORE_GRAPHEME_CLUSTER

示例

    require "resty.validation.utf8"
    local validation = require "resty.validation"
    local valid, ts = validation:utf8category("LETTER_UPPERCASE")("TEST")

resty.validation.injection 扩展

这组验证器和过滤器基于 Nick Galbreath 的出色 `libinjection` 库,这是一个 SQL/SQLI/XSS 标记器解析器分析器。它需要我的 LuaJIT FFI 包装器 `lua-resty-injection` 才能工作。安装完上述要求后,剩下的就容易了。要使用此扩展,您只需

    require "resty.validation.injection"

它将修补它将在 resty.validation 中提供的适配器,这些适配器目前是

  • sqli,如果检测到 SQL 注入,则返回 false,否则返回 true

  • xss,如果检测到跨站点脚本注入,则返回 false,否则返回 true

示例

    require "resty.validation.injection"
    local validation = require "resty.validation"
    local valid, ts = validation.sqli("test'; DELETE FROM users;")
    local valid, ts = validation.xss("test <script>alert('XSS');</script>")

API

我不会在这里详细介绍所有不同的验证器和过滤器,因为它们都遵循相同的逻辑,但我会展示一些关于其工作原理的一般方法。

validation._VERSION

此字段包含验证库的版本,例如,对于此库的 2.5 版,其值为 "2.5"

boolean,值/错误验证...

... 表示验证链。这用于定义单个验证器链。链长没有限制。它将始终返回布尔值(验证是否有效)。第二个返回值将是未返回 true 作为验证结果的过滤器的名称,或者过滤后的值。

    local v = require "resty.validation"
    
    -- The below means, create validator that checks that the input is:
    -- 1. string
    -- If, it is, then trim whitespaces from begin and end of the string:
    -- 2. trim
    -- Then check that the trimmed string's length is at least 5 characters (UTF-8):
    -- 3. minlen(5)
    -- And if everything is still okay, convert that string to upper case
    -- (UTF-8 is not yet supported in upper):
    -- 4. upper
    local myvalidator = v.string.trim:minlen(5).upper
    
    -- This example will return false and "minlen"
    local valid, value = myvalidator(" \n\t a \t\n ")
    
    -- This example will return true and "ABCDE"
    local valid, value = myvalidator(" \n\t abcde \t\n ")

只要验证器失败并返回 false,您就不应将返回的值用于除错误报告之外的任何其他目的。因此,链的工作方式如下。lua-resty-validation 不会尝试做任何事情,如果您指定永远不会使用的链,例如

    local v = require "resty.validation"
    -- The input value can never be both string and number at the same time:
    local myvalidator = v.string.number:max(3)
    -- But you could write this like this
    -- (take input as a string, try to convert it to number, and check it is at most 3):
    local myvalidator = v.string.tonumber:max(3)

如您所见,这是一种定义单个可重用验证器的方法。例如,您可以预定义一组基本的单个验证器链,并将其存储在您自己的模块中,这样您可以在应用程序的不同部分重复使用相同的验证逻辑。最好先定义单个可重用验证器,然后在组验证器中重复使用它们。

例如,假设您有一个名为 validators 的模块

    local v = require "resty.validation"
    return {
        nick     = v.string.trim:minlen(2),
        email    = v.string.trim.email,
        password = v.string.trim:minlen(8)
    }

现在,您的应用程序中某个地方有 register 函数

    local validate = require "validators"
    local function register(nick, email, password)
        local vn, nick     = validate.nick(nick)
        local ve, email    = validate.email(email)
        local vp, password = validate.password(password)
        if vn and ve and vp then
            -- input is valid, do something with nick, email, and password
        else
            -- input is invalid, nick, email, and password contain the error reasons
        end
    end

这很快就会变得有点乱,这就是我们有组验证器的原因。

table validation.new([验证器表])

此函数是组验证开始的地方。假设您有一个注册表单,要求您提供昵称、电子邮件(两次相同)和密码(两次相同)。

我们将重复使用在 validators 模块中定义的单个验证器

    local v = require "resty.validation"
    return {
        nick     = v.string.trim:minlen(2),
        email    = v.string.trim.email,
        password = v.string.trim:minlen(8)
    }

现在,让我们在 forms 模块中创建可重用的组验证器

    local v        = require "resty.validation"
    local validate = require "validators"
    
    -- First we create single validators for each form field
    local register = v.new{
        nick      = validate.nick,
        email     = validate.email,
        email2    = validate.email,
        password  = validate.password,
        password2 = validate.password
    }
    
    -- Next we create group validators for email and password:
    register:compare "email    == email2"
    register:compare "password == password2"
    
    -- And finally we return from this forms module
    
    return {
        register = register
    }
    

现在,您的应用程序中某个地方有这个 register 函数

    local forms = require "forms"
    local function register(data)
        local valid, fields, errors = forms.register(data)
        if valid then
            -- input is valid, do something with fields
        else
            -- input is invalid, do something with fields and errors
        end
    end
    
    -- And you might call it like:
    
    register{
        nick      = "test",
        email     = "test@test.org",
        email2    = "test@test.org",
        password  = "qwerty123",
        password2 = "qwerty123"
    }
    

组验证器最棒的一点是,您可以将字段和错误表编码为 JSON 并将其返回给客户端。这在构建单页应用程序时可能很方便,您需要在客户端报告服务器端错误。在上面的示例中,fields 变量将如下所示(valid 将为 true:,errors 将为 nil

    {
        nick = {
            unvalidated = false,
            value = "test",
            input = "test",
            name = "nick",
            valid = true,
            invalid = false,
            validated = true
        },
        email = {
            unvalidated = false,
            value = "test@test.org",
            input = "test@test.org",
            name = "email",
            valid = true,
            invalid = false,
            validated = true
        },
        email2 = {
            unvalidated = false,
            value = "test@test.org",
            input = "test@test.org",
            name = "email2",
            valid = true,
            invalid = false,
            validated = true
        },
        password = {
            unvalidated = false,
            value = "qwerty123",
            input = "qwerty123",
            name = "password",
            valid = true,
            invalid = false,
            validated = true
        },
        password2 = {
            unvalidated = false,
            value = "qwerty123",
            input = "qwerty123",
            name = "password2",
            valid = true,
            invalid = false,
            validated = true
        }
    }

这非常适合进一步处理并将字段作为 JSON 编码发送回客户端 JavaScript 应用程序,但通常这个结构太重了,无法发送到后端层。要获得一个简单的键值表,我们可以调用此字段表

    local data = fields()

data 变量现在将包含

    {
        nick = "test",
        email = "test@test.org",
        email2 = "test@test.org",
        password = "qwerty123",
        password2 = "qwerty123"
    }

现在,您可以将其发送到例如 Redis 或您拥有的任何数据库(抽象)层。但是,这并没有就此结束,如果您的数据库层只对 nickemailpassword 感兴趣(例如,剥离这些重复项),您甚至可以调用 data

    local realdata = data("nick", "email", "password")

realdata 现在将包含

    {
        nick = "test",
        email = "test@test.org",
        password = "qwerty123"
    }

field:accept(value)

对于字段,您可以调用 accept,它会执行以下操作

    self.error = nil
    self.value = value
    self.valid = true
    self.invalid = false
    self.validated = true
    self.unvalidated = false

field:reject(error)

对于字段,您可以调用 reject,它会执行以下操作

    self.error = error
    self.valid = false
    self.invalid = true
    self.validated = true
    self.unvalidated = false

string field:state(invalid, valid, unvalidated)

在字段上调用 state 在将验证结果嵌入到例如 HTML 模板(例如 lua-resty-template)中时非常有用。以下是一个使用 lua-resty-template 的示例

    <form method="post">
        <input class="{{ form.email:state('invalid', 'valid') }}"
                name="email"
                type="text"
                placeholder="Email"
                value="{{ form.email.input }}">
        <button type="submit">Join</button>
    </form>

因此,根据电子邮件字段的状态,这将为输入元素添加一个类(例如,使输入的边框变为红色或绿色)。我们在这里不关心未验证(例如,当用户首次加载页面和表单时)状态。

更改

此模块每次发布的更改记录在 Changes.md 文件中。

另请参见

许可证

lua-resty-validation 使用双条款 BSD 许可证。

    Copyright (c) 2014 - 2017, Aapo Talvensaari
    All rights reserved.
    
    Redistribution and use in source and binary forms, with or without modification,
    are permitted provided that the following conditions are met:
    
    * Redistributions of source code must retain the above copyright notice, this
      list of conditions and the following disclaimer.
    
    * Redistributions in binary form must reproduce the above copyright notice, this
      list of conditions and the following disclaimer in the documentation and/or
      other materials provided with the distribution.
    
    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
    ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
    WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
    DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
    ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES`

作者

Aapo Talvensaari (@bungle)

许可证

2bsd

版本