summaryrefslogtreecommitdiff
path: root/server/src/glob
diff options
context:
space:
mode:
Diffstat (limited to 'server/src/glob')
-rw-r--r--server/src/glob/gitignore.lua222
-rw-r--r--server/src/glob/glob.lua120
-rw-r--r--server/src/glob/init.lua4
-rw-r--r--server/src/glob/matcher.lua146
4 files changed, 492 insertions, 0 deletions
diff --git a/server/src/glob/gitignore.lua b/server/src/glob/gitignore.lua
new file mode 100644
index 00000000..9ba2d564
--- /dev/null
+++ b/server/src/glob/gitignore.lua
@@ -0,0 +1,222 @@
+local m = require 'lpeglabel'
+local matcher = require 'glob.matcher'
+
+local function prop(name, pat)
+ return m.Cg(m.Cc(true), name) * pat
+end
+
+local function object(type, pat)
+ return m.Ct(
+ m.Cg(m.Cc(type), 'type') *
+ m.Cg(pat, 'value')
+ )
+end
+
+local function expect(p, err)
+ return p + m.T(err)
+end
+
+local parser = m.P {
+ 'Main',
+ ['Sp'] = m.S(' \t')^0,
+ ['Slash'] = m.S('/\\')^1,
+ ['Main'] = m.Ct(m.V'Sp' * m.P'{' * m.V'Pattern' * (',' * expect(m.V'Pattern', 'Miss exp after ","'))^0 * m.P'}')
+ + m.Ct(m.V'Pattern')
+ + m.T'Main Failed'
+ ,
+ ['Pattern'] = m.Ct(m.V'Sp' * prop('neg', m.P'!') * expect(m.V'Unit', 'Miss exp after "!"'))
+ + m.Ct(m.V'Unit')
+ ,
+ ['NeedRoot'] = prop('root', (m.P'.' * m.V'Slash' + m.V'Slash')),
+ ['Unit'] = m.V'Sp' * m.V'NeedRoot'^-1 * expect(m.V'Exp', 'Miss exp') * m.V'Sp',
+ ['Exp'] = m.V'Sp' * (m.V'FSymbol' + object('/', m.V'Slash') + m.V'Word')^0 * m.V'Sp',
+ ['Word'] = object('word', m.Ct((m.V'CSymbol' + m.V'Char' - m.V'FSymbol')^1)),
+ ['CSymbol'] = object('*', m.P'*')
+ + object('?', m.P'?')
+ + object('[]', m.V'Range')
+ ,
+ ['Char'] = object('char', (1 - m.S',{}[]*?/\\')^1),
+ ['FSymbol'] = object('**', m.P'**'),
+ ['Range'] = m.P'[' * m.Ct(m.V'RangeUnit'^0) * m.P']'^-1,
+ ['RangeUnit'] = m.Ct(- m.P']' * m.C(m.P(1)) * (m.P'-' * - m.P']' * m.C(m.P(1)))^-1),
+}
+
+---@class gitignore
+local mt = {}
+mt.__index = mt
+mt.__name = 'gitignore'
+
+function mt:addPattern(pat)
+ if type(pat) ~= 'string' then
+ return
+ end
+ self.pattern[#self.pattern+1] = pat
+ if self.options.ignoreCase then
+ pat = pat:lower()
+ end
+ local states, err = parser:match(pat)
+ if not states then
+ self.errors[#self.errors+1] = {
+ pattern = pat,
+ message = err
+ }
+ return
+ end
+ for _, state in ipairs(states) do
+ self.matcher[#self.matcher+1] = matcher(state)
+ end
+end
+
+function mt:setOption(op, val)
+ if val == nil then
+ val = true
+ end
+ self.options[op] = val
+end
+
+---@param key string | "'type'" | "'list'"
+---@param func function | "function (path) end"
+function mt:setInterface(key, func)
+ if type(func) ~= 'function' then
+ return
+ end
+ self.interface[key] = func
+end
+
+function mt:callInterface(name, ...)
+ local func = self.interface[name]
+ return func(...)
+end
+
+function mt:hasInterface(name)
+ return self.interface[name] ~= nil
+end
+
+function mt:checkDirectory(catch, path, matcher)
+ if not self:hasInterface 'type' then
+ return true
+ end
+ if not matcher:isNeedDirectory() then
+ return true
+ end
+ if #catch < #path then
+ -- if path is 'a/b/c' and catch is 'a/b'
+ -- then the catch must be a directory
+ return true
+ else
+ return self:callInterface('type', path) == 'directory'
+ end
+end
+
+function mt:simpleMatch(path)
+ for i = #self.matcher, 1, -1 do
+ local matcher = self.matcher[i]
+ local catch = matcher(path)
+ if catch and self:checkDirectory(catch, path, matcher) then
+ if matcher:isNegative() then
+ return false
+ else
+ return true
+ end
+ end
+ end
+end
+
+function mt:finishMatch(path)
+ local paths = {}
+ for filename in path:gmatch '[^/\\]+' do
+ paths[#paths+1] = filename
+ end
+ for i = 1, #paths do
+ local newPath = table.concat(paths, '/', 1, i)
+ local passed = self:simpleMatch(newPath)
+ if passed == true then
+ return true
+ elseif passed == false then
+ return false
+ end
+ end
+ return false
+end
+
+function mt:scan(callback)
+ local files = {}
+ if type(callback) ~= 'function' then
+ callback = nil
+ end
+ local list = {}
+ local result = self:callInterface('list', '')
+ if type(result) ~= 'table' then
+ return files
+ end
+ for _, path in ipairs(result) do
+ list[#list+1] = path
+ end
+ while #list > 0 do
+ local current = list[#list]
+ if not current then
+ break
+ end
+ list[#list] = nil
+ if not self:simpleMatch(current) then
+ local fileType = self:callInterface('type', current)
+ if fileType == 'file' then
+ if callback then
+ callback(current)
+ end
+ files[#files+1] = current
+ elseif fileType == 'directory' then
+ local result = self:callInterface('list', current)
+ if type(result) == 'table' then
+ for _, path in ipairs(result) do
+ local filename = path:match '([^/\\]+)[/\\]*$'
+ if filename then
+ list[#list+1] = current .. '\\' .. filename
+ end
+ end
+ end
+ end
+ end
+ end
+ table.sort(files)
+ return files
+end
+
+function mt:__call(path)
+ if self.options.ignoreCase then
+ path = path:lower()
+ end
+ return self:finishMatch(path)
+end
+
+return function (pattern, options, interface)
+ local self = setmetatable({
+ pattern = {},
+ options = {},
+ matcher = {},
+ errors = {},
+ interface = {},
+ }, mt)
+
+ if type(pattern) == 'table' then
+ for _, pat in ipairs(pattern) do
+ self:addPattern(pat)
+ end
+ else
+ self:addPattern(pattern)
+ end
+
+ if type(options) == 'table' then
+ for op, val in pairs(options) do
+ self:setOption(op, val)
+ end
+ end
+
+ if type(interface) == 'table' then
+ for key, func in pairs(interface) do
+ self:setInterface(key, func)
+ end
+ end
+
+ return self
+end
diff --git a/server/src/glob/glob.lua b/server/src/glob/glob.lua
new file mode 100644
index 00000000..8c82d186
--- /dev/null
+++ b/server/src/glob/glob.lua
@@ -0,0 +1,120 @@
+local m = require 'lpeglabel'
+local matcher = require 'glob.matcher'
+
+local function prop(name, pat)
+ return m.Cg(m.Cc(true), name) * pat
+end
+
+local function object(type, pat)
+ return m.Ct(
+ m.Cg(m.Cc(type), 'type') *
+ m.Cg(pat, 'value')
+ )
+end
+
+local function expect(p, err)
+ return p + m.T(err)
+end
+
+local parser = m.P {
+ 'Main',
+ ['Sp'] = m.S(' \t')^0,
+ ['Slash'] = m.S('/\\')^1,
+ ['Main'] = m.Ct(m.V'Sp' * m.P'{' * m.V'Pattern' * (',' * expect(m.V'Pattern', 'Miss exp after ","'))^0 * m.P'}')
+ + m.Ct(m.V'Pattern')
+ + m.T'Main Failed'
+ ,
+ ['Pattern'] = m.Ct(m.V'Sp' * prop('neg', m.P'!') * expect(m.V'Unit', 'Miss exp after "!"'))
+ + m.Ct(m.V'Unit')
+ ,
+ ['NeedRoot'] = prop('root', (m.P'.' * m.V'Slash' + m.V'Slash')),
+ ['Unit'] = m.V'Sp' * m.V'NeedRoot'^-1 * expect(m.V'Exp', 'Miss exp') * m.V'Sp',
+ ['Exp'] = m.V'Sp' * (m.V'FSymbol' + object('/', m.V'Slash') + m.V'Word')^0 * m.V'Sp',
+ ['Word'] = object('word', m.Ct((m.V'CSymbol' + m.V'Char' - m.V'FSymbol')^1)),
+ ['CSymbol'] = object('*', m.P'*')
+ + object('?', m.P'?')
+ + object('[]', m.V'Range')
+ ,
+ ['Char'] = object('char', (1 - m.S',{}[]*?/\\')^1),
+ ['FSymbol'] = object('**', m.P'**'),
+ ['Range'] = m.P'[' * m.Ct(m.V'RangeUnit'^0) * m.P']'^-1,
+ ['RangeUnit'] = m.Ct(- m.P']' * m.C(m.P(1)) * (m.P'-' * - m.P']' * m.C(m.P(1)))^-1),
+}
+
+local mt = {}
+mt.__index = mt
+mt.__name = 'glob'
+
+function mt:addPattern(pat)
+ if type(pat) ~= 'string' then
+ return
+ end
+ self.pattern[#self.pattern+1] = pat
+ if self.options.ignoreCase then
+ pat = pat:lower()
+ end
+ local states, err = parser:match(pat)
+ if not states then
+ self.errors[#self.errors+1] = {
+ pattern = pat,
+ message = err
+ }
+ return
+ end
+ for _, state in ipairs(states) do
+ if state.neg then
+ self.refused[#self.refused+1] = matcher(state)
+ else
+ self.passed[#self.passed+1] = matcher(state)
+ end
+ end
+end
+
+function mt:setOption(op, val)
+ if val == nil then
+ val = true
+ end
+ self.options[op] = val
+end
+
+function mt:__call(path)
+ if self.options.ignoreCase then
+ path = path:lower()
+ end
+ for _, refused in ipairs(self.refused) do
+ if refused(path) then
+ return false
+ end
+ end
+ for _, passed in ipairs(self.passed) do
+ if passed(path) then
+ return true
+ end
+ end
+ return false
+end
+
+return function (pattern, options)
+ local self = setmetatable({
+ pattern = {},
+ options = {},
+ passed = {},
+ refused = {},
+ errors = {},
+ }, mt)
+
+ if type(pattern) == 'table' then
+ for _, pat in ipairs(pattern) do
+ self:addPattern(pat)
+ end
+ else
+ self:addPattern(pattern)
+ end
+
+ if type(options) == 'table' then
+ for op, val in pairs(options) do
+ self:setOption(op, val)
+ end
+ end
+ return self
+end
diff --git a/server/src/glob/init.lua b/server/src/glob/init.lua
new file mode 100644
index 00000000..6578a0d4
--- /dev/null
+++ b/server/src/glob/init.lua
@@ -0,0 +1,4 @@
+return {
+ glob = require 'glob.glob',
+ gitignore = require 'glob.gitignore',
+}
diff --git a/server/src/glob/matcher.lua b/server/src/glob/matcher.lua
new file mode 100644
index 00000000..2e07cf95
--- /dev/null
+++ b/server/src/glob/matcher.lua
@@ -0,0 +1,146 @@
+local m = require 'lpeglabel'
+
+local Slash = m.S('/\\')^1
+local Symbol = m.S',{}[]*?/\\'
+local Char = 1 - Symbol
+local Path = Char^1 * Slash
+local NoWord = #(m.P(-1) + Symbol)
+local function whatHappened()
+ return m.Cmt(m.P(1)^1, function (...)
+ print(...)
+ end)
+end
+
+local mt = {}
+mt.__index = mt
+mt.__name = 'matcher'
+
+function mt:exp(state, index)
+ local exp = state[index]
+ if not exp then
+ return
+ end
+ if exp.type == 'word' then
+ return self:word(exp, state, index + 1)
+ elseif exp.type == 'char' then
+ return self:char(exp, state, index + 1)
+ elseif exp.type == '**' then
+ return self:anyPath(exp, state, index + 1)
+ elseif exp.type == '*' then
+ return self:anyChar(exp, state, index + 1)
+ elseif exp.type == '?' then
+ return self:oneChar(exp, state, index + 1)
+ elseif exp.type == '[]' then
+ return self:range(exp, state, index + 1)
+ elseif exp.type == '/' then
+ return self:slash(exp, state, index + 1)
+ end
+end
+
+function mt:word(exp, state, index)
+ local current = self:exp(exp.value, 1)
+ local after = self:exp(state, index)
+ if after then
+ return current * Slash * after
+ else
+ return current
+ end
+end
+
+function mt:char(exp, state, index)
+ local current = m.P(exp.value)
+ local after = self:exp(state, index)
+ if after then
+ return current * after * NoWord
+ else
+ return current * NoWord
+ end
+end
+
+function mt:anyPath(_, state, index)
+ local after = self:exp(state, index)
+ if after then
+ return m.P {
+ 'Main',
+ Main = after
+ + Path * m.V'Main'
+ }
+ else
+ return Path^0
+ end
+end
+
+function mt:anyChar(_, state, index)
+ local after = self:exp(state, index)
+ if after then
+ return m.P {
+ 'Main',
+ Main = after
+ + Char * m.V'Main'
+ }
+ else
+ return Char^0
+ end
+end
+
+function mt:oneChar(_, state, index)
+ local after = self:exp(state, index)
+ if after then
+ return Char * after
+ else
+ return Char
+ end
+end
+
+function mt:range(exp, state, index)
+ local after = self:exp(state, index)
+ local ranges = {}
+ for _, range in ipairs(exp.value) do
+ ranges[#ranges+1] = range[1] .. range[2]
+ end
+ local current = m.R(table.unpack(ranges))
+ if after then
+ return current * after
+ else
+ return current
+ end
+end
+
+function mt:slash(_, state, index)
+ local after = self:exp(state, index)
+ if after then
+ return after
+ else
+ self.needDirectory = true
+ return nil
+ end
+end
+
+function mt:pattern(state)
+ if state.root then
+ return m.C(self:exp(state, 1))
+ else
+ return m.C(self:anyPath(nil, state, 1))
+ end
+end
+
+function mt:isNeedDirectory()
+ return self.needDirectory == true
+end
+
+function mt:isNegative()
+ return self.state.neg == true
+end
+
+function mt:__call(path)
+ return self.matcher:match(path)
+end
+
+return function (state, options)
+ local self = setmetatable({
+ options = options,
+ state = state,
+ }, mt)
+ self.matcher = self:pattern(state)
+ return self
+end