Add SwitchyOmega rule list format. Fix #71.

This commit is contained in:
FelisCatus 2015-02-08 22:25:15 +08:00
parent 56457519ab
commit ec5a35682c
2 changed files with 260 additions and 2 deletions

View File

@ -52,7 +52,51 @@ module.exports = exports =
list.push({condition: cond, profileName: profile, source: source})
# Exclusive rules have higher priority, so they come first.
return exclusive_rules.concat normal_rules
'Switchy':
omegaPrefix: '[SwitchyOmega Conditions'
detect: (text) ->
text = text.trim()
if strStartsWith(text, exports['Switchy'].omegaPrefix)
return true
return
parse: (text, matchProfileName, defaultProfileName) ->
text = text.trim()
switchy = exports['Switchy']
parser = 'parseOmega'
if not strStartsWith(text, switchy.omegaPrefix)
if text[0] == '#' or text.indexOf('\n#') >= 0
parser = 'parseLegacy'
return switchy[parser](text, matchProfileName, defaultProfileName)
# For the omega rule list format, please see the following wiki page:
# https://github.com/FelisCatus/SwitchyOmega/wiki/SwitchyOmega-conditions-format
compose: ({rules, defaultProfileName}, {withResult, useExclusive} = {}) ->
eol = '\r\n'
ruleList = '[SwitchyOmega Conditions]' + eol
useExclusive ?= not withResult
if withResult
ruleList += '@with result' + eol + eol
else
ruleList += eol
for rule in rules
line = Conditions.str(rule.condition)
if line[0] == '#' or line[0] == '+'
# Escape leading # to avoid being detected as legacy format.
# Reserve leading + for condition results.
line = ': ' + line
if useExclusive and rule.profileName == defaultProfileName
line = '!' + line
else if withResult
# TODO(catus): What if rule.profileName contains ' +' or new lines?
line += ' +' + rule.profileName
ruleList += line + eol
if withResult
# TODO(catus): Also special chars and sequences in defaultProfileName.
ruleList += '* +' + defaultProfileName + eol
return ruleList
conditionFromLegacyWildcard: (pattern) ->
if pattern[0] == '@'
pattern = pattern.substring(1)
@ -70,8 +114,7 @@ module.exports = exports =
conditionType: 'UrlWildcardCondition'
pattern: pattern
parse: (text, matchProfileName, defaultProfileName) ->
text = text.trim()
parseLegacy: (text, matchProfileName, defaultProfileName) ->
normal_rules = []
exclusive_rules = []
begin = false
@ -107,3 +150,58 @@ module.exports = exports =
list.push({condition: cond, profileName: profile, source: source})
# Exclusive rules have higher priority, so they come first.
return exclusive_rules.concat normal_rules
parseOmega: (text, matchProfileName, defaultProfileName) ->
rules = []
rulesWithDefaultProfile = []
withResult = false
for line in text.split(/\n|\r/)
line = line.trim()
continue if line.length == 0
switch line[0]
when '[' # Header line: Ignore.
continue
when ';' # Comment line: Ignore.
continue
when '@' # Directive line:
iSpace = line.indexOf(' ')
iSpace = line.length if iSpace < 0
directive = line.substr(1, iSpace - 1)
line = line.substr(iSpace + 1).trim()
switch directive.toUpperCase()
when 'WITH'
feature = line.toUpperCase()
if feature == 'RESULT' or feature == 'RESULTS'
withResult = true
continue
source = null
if line[0] == '!'
profile = if withResult then null else defaultProfileName
source = line
line = line.substr(1)
else if withResult
iSpace = line.lastIndexOf(' +')
if iSpace < 0
throw new Error("Missing result profile name: " + line)
profile = line.substr(iSpace + 2).trim()
line = line.substr(0, iSpace).trim()
defaultProfileName = profile if line == '*'
else
profile = matchProfileName
cond = Conditions.fromStr(line)
if not cond
throw new Error("Invalid rule: " + line)
rule = {condition: cond, profileName: profile, source: source ? line}
rules.push(rule)
if not profile
rulesWithDefaultProfile.push(rule)
if withResult
if not defaultProfileName
throw new Error("Missing default rule with catch-all '*' condition!")
for rule in rulesWithDefaultProfile
rule.profileName = defaultProfileName
return rules

View File

@ -234,3 +234,163 @@ describe 'RuleList', ->
conditionType: 'UrlWildcardCondition'
pattern: 'http://www.example.com/*'
)
describe 'Switchy (omega format)', ->
parse = RuleList['Switchy'].parse
compose = RuleList['Switchy'].compose
it 'should parse empty rule lists', ->
list = compose {rules: []}
result = parse(list, 'match', 'notmatch')
result.should.have.length(0)
it 'should ignore comment lines.', ->
list = compose {rules: []}
list += ';*.example.com \r\n'
result = parse(list, 'match', 'notmatch')
result.should.have.length(0)
it 'should compose and parse HostWildcardCondition', ->
rule =
source: '*.example.com'
condition:
conditionType: 'HostWildcardCondition',
pattern: '*.example.com'
profileName: 'match'
list = compose({rules: [rule], defaultProfileName: 'notmatch'})
result = parse(list, 'match', 'notmatch')
result.should.have.length(1)
result[0].should.eql(rule)
it 'should compose and parse HostRegexCondition', ->
rule =
source: 'HostRegex: ^http://www\.example\.com/.*'
condition:
conditionType: 'HostRegexCondition',
pattern: '^http://www\.example\.com/.*'
profileName: 'match'
list = compose({rules: [rule], defaultProfileName: 'notmatch'})
result = parse(list, 'match', 'notmatch')
result.should.have.length(1)
result[0].should.eql(rule)
it 'should compose and parse disabled rules', ->
rule =
source: 'Disabled: *.example.com'
condition:
conditionType: 'FalseCondition',
pattern: '*.example.com'
profileName: 'match'
list = compose({rules: [rule], defaultProfileName: 'notmatch'})
result = parse(list, 'match', 'notmatch')
result.should.have.length(1)
result[0].should.eql(rule)
it 'should compose and parse exclusive rules', ->
rule =
source: '!*.example.com'
condition:
conditionType: 'HostWildcardCondition',
pattern: '*.example.com'
profileName: 'notmatch'
list = compose({rules: [rule], defaultProfileName: 'notmatch'})
result = parse(list, 'match', 'notmatch')
result.should.have.length(1)
result[0].should.eql(rule)
it 'should parse multiple conditions', ->
rules = [{
source: '*.example.com'
condition:
conditionType: 'HostWildcardCondition',
pattern: '*.example.com'
profileName: 'match'
}, {
source: '*.example.org'
condition:
conditionType: 'HostWildcardCondition',
pattern: '*.example.org'
profileName: 'match'
}]
list = compose({rules: rules, defaultProfileName: 'notmatch'})
result = parse(list, 'match', 'notmatch')
result.should.eql(rules)
it 'should respect the top-down order of conditions', ->
rules = [{
source: 'b.example.com'
condition:
conditionType: 'HostWildcardCondition',
pattern: 'b.example.com'
profileName: 'match'
}, {
source: '!a.example.org'
condition:
conditionType: 'HostWildcardCondition',
pattern: 'a.example.org'
profileName: 'notmatch'
}]
list = compose({rules: rules, defaultProfileName: 'notmatch'})
result = parse(list, 'match', 'notmatch')
result.should.eql(rules)
it 'should add a default rule when results are enabled', ->
list = compose(
{rules: [], defaultProfileName: 'notmatch'}
{withResult: true}
)
list.split(/\r|\n/).should.contain('@with result')
result = parse(list, 'ignored', 'alsoIgnored')
result.should.have.length(1)
result[0].should.eql({
source: '*'
condition:
conditionType: 'HostWildcardCondition',
pattern: '*'
profileName: 'notmatch',
})
it 'should compose and parse conditions with results', ->
rules = [{
source: 'b.example.com'
condition:
conditionType: 'HostWildcardCondition',
pattern: 'b.example.com'
profileName: 'abc'
}, {
source: 'a.example.org'
condition:
conditionType: 'HostWildcardCondition',
pattern: 'a.example.org'
profileName: 'def'
}]
list = compose(
{rules: rules, defaultProfileName: 'ghi'}
{withResult: true}
)
result = parse(list, 'ignored', 'alsoIgnored')
rules.push({
source: '*'
condition:
conditionType: 'HostWildcardCondition',
pattern: '*'
profileName: 'ghi',
})
result.should.eql(rules)
it 'should compose and parse exclusive conditions with results', ->
rules = [{
source: '!b.example.com'
condition:
conditionType: 'HostWildcardCondition',
pattern: 'b.example.com'
profileName: 'default profile'
}, {
source: 'a.example.org'
condition:
conditionType: 'HostWildcardCondition',
pattern: 'a.example.org'
profileName: 'some profile'
}]
list = compose(
{rules: rules, defaultProfileName: 'default profile'}
{withResult: true, useExclusive: true}
)
result = parse(list, 'ignored', 'alsoIgnored')
rules.push({
source: '*'
condition:
conditionType: 'HostWildcardCondition',
pattern: '*'
profileName: 'default profile',
})
result.should.eql(rules)