commencing full rewrite of hyde

This commit is contained in:
Yessiest 2023-08-28 04:15:09 +04:00
parent ff36326ec8
commit 161689bec0
22 changed files with 605 additions and 398 deletions

0
.yardoc/checksums Normal file
View File

0
.yardoc/complete Normal file
View File

BIN
.yardoc/object_types Normal file

Binary file not shown.

BIN
.yardoc/objects/root.dat Normal file

Binary file not shown.

BIN
.yardoc/proxy_types Normal file

Binary file not shown.

8
config.ru Normal file
View File

@ -0,0 +1,8 @@
require 'rack'
require_relative 'test_app'
app = Rack::Builder.new do
use Rack::Lint
run TestApp::App.new
end
run app

View File

@ -3,21 +3,29 @@ require 'webrick'
require 'uri' require 'uri'
require 'pp' require 'pp'
# Primary module
module Hyde module Hyde
# Branding and version # Hyde version
VERSION = "0.5 (alpha)" # @type [String]
VERSION = '0.5 (alpha)'
attr_reader :VERSION attr_reader :VERSION
# Hyde branding and version (for error templates)
# @type [String]
VLINE = "Hyde/#{Hyde::VERSION} on WEBrick/#{WEBrick::VERSION} (Ruby/#{RUBY_VERSION}/#{RUBY_RELEASE_DATE})\n" VLINE = "Hyde/#{Hyde::VERSION} on WEBrick/#{WEBrick::VERSION} (Ruby/#{RUBY_VERSION}/#{RUBY_RELEASE_DATE})\n"
attr_reader :VLINE attr_reader :VLINE
def error_template(errortext,backtrace) # Generate HTML error template
<<HTMLEOF # @param errortext [String] Error explanation
<!DOCTYPE HTML> # @param backtrace [String] Ruby backtrace
<html> def error_template(errortext, backtrace)
<<~HTMLEOF
<!DOCTYPE HTML>
<html>
<head> <head>
<title>#{WEBrick::HTMLUtils.escape(errortext)}</title> <title>#{WEBrick::HTMLUtils.escape(errortext)}</title>
<style> .header {background-color: #CC7878; padding: 0.5rem; border-bottom-width: 0.2rem; border-bottom-style: solid; border-bottom-color: #202222; overflow: auto;} .title { font-weight: bolder; font-size: 36px; margin: 0 0; text-shadow: 1px 1px 1px #202222, 2px 2px 2px #404444; float: left } body { margin: 0; <style> .header {background-color: #CC7878; padding: 0.5rem; border-bottom-width: 0.2rem; border-bottom-style: solid; border-bottom-color: #202222; overflow: auto;} .title { font-weight: bolder; font-size: 36px; margin: 0 0; text-shadow: 1px 1px 1px #202222, 2px 2px 2px #404444; float: left } body { margin: 0;
} .text { font-size 1rem; } .small { color: #7D7D7D; font-size: 12px;} .code { font-family: monospace; font-size: 0.7rem; } </style> } .text { font-size 1rem; } .small { color: #7D7D7D; font-size: 12px;} .code { font-family: monospace; font-size: 0.7rem; } </style>
</head> </head>
<body> <body>
<div class="header"> <div class="header">
@ -27,70 +35,41 @@ module Hyde
<div style="padding: 0.5rem"> <div style="padding: 0.5rem">
<p class="text">#{WEBrick::HTMLUtils.escape(errortext)}</p> <p class="text">#{WEBrick::HTMLUtils.escape(errortext)}</p>
<pre><code class="text code"> <pre><code class="text code">
#{WEBrick::HTMLUtils.escape(backtrace) or "\n\n\n"} #{WEBrick::HTMLUtils.escape(backtrace) or "\n\n\n"}
</code></pre> </code></pre>
<hr/> <hr/>
<p class="small">#{WEBrick::HTMLUtils.escape(VLINE)}</p> <p class="small">#{WEBrick::HTMLUtils.escape(Hyde::VLINE)}</p>
</div> </div>
</body> </body>
</html> </html>
HTMLEOF HTMLEOF
end end
module_function :error_template module_function :error_template
WEBrick::HTTPResponse.class_exec do WEBrick::HTTPResponse.class_exec do
attr_accessor :recent_backtrace
public public
def set_backtrace(backtrace)
@recent_backtrace = backtrace
end
def create_error_page
@body = Hyde.error_template(@reason_phrase,@recent_backtrace)
end
end
class Server < WEBrick::HTTPServer attr_accessor :recent_backtrace
def initialize(config={},&setup)
super(config) def create_error_page
@hyde_pathspec = Hyde::Pathspec.new "/", &setup @body = Hyde.error_template(@reason_phrase, @recent_backtrace)
self.mount_proc '/' do |req,res|
context = Hyde::Context.new(req.path, req, res)
begin
while context and (not context.exit_loop) do
context.exit_loop = true
context = catch :controlled_exit do
@hyde_pathspec._match(context)
context
end
while postprocessor = context.queue_postprocess.shift do
postprocessor.call(context)
end
end
while finalizer = context.queue_finalize.shift do
finalizer.call(context)
end
rescue Exception => e
puts e.message
puts e.backtrace
res.set_backtrace "#{e.message} (#{e.class})\n#{e.backtrace.join "\n"}"
res.status = 500
end
end
end end
end end
# Interchangeable glob/regex/string pattern matching # Interchangeable glob/regex/string pattern matching
module PatternMatching module PatternMatching
def _prep_path(path,safe_regexp: true) def _prep_path(path, safe_regexp: true)
@safe_regexp = safe_regexp @safe_regexp = safe_regexp
@path = _normalize(path) if path.kind_of? String @path = _normalize(path) if path.is_a? String
@path = path if path.kind_of? Regexp @path = path if path.is_a? Regexp
end end
def _match?(path, ctx)
# @return [Boolean]
def _match?(path, _ctx)
# behvaiour used by "index" method # behvaiour used by "index" method
return true if @path == "" return true if @path == ''
split_path = path.split("/").filter { |x| x != "" } split_path = path.split('/').filter { |x| x != '' }
if @path.kind_of? Regexp then if @path.is_a? Regexp
# this chunk of fuck is needed to force regexp into 3 rules: # this chunk of fuck is needed to force regexp into 3 rules:
# 1) unsafe regexp means match the whole (remaining) line. # 1) unsafe regexp means match the whole (remaining) line.
# 3) safe regexp means match only the part before the next slash # 3) safe regexp means match only the part before the next slash
@ -98,38 +77,40 @@ HTMLEOF
# this forces the matching to work somewhat consistently # this forces the matching to work somewhat consistently
test = @path.match _normalize_input(path) unless @safe_regexp test = @path.match _normalize_input(path) unless @safe_regexp
test = @path.match split_path[0] if @safe_regexp test = @path.match split_path[0] if @safe_regexp
if test and (test.pre_match == "") and (test.post_match == "") then if test and (test.pre_match == '') and (test.post_match == '')
return true true
else else
return false false
end end
else else
# algorithm to match path segments until no more left in @path # algorithm to match path segments until no more left in @path
@path.split("/").filter { |x| x != "" } @path.split('/').filter { |x| x != '' }
.each_with_index { |x,i| .each_with_index do |x, i|
return false if x != split_path[i] return false if x != split_path[i]
} end
return true true
end end
end end
def _normalize_input(path) def _normalize_input(path)
# remove duplicate slashes and trim edge slashes # remove duplicate slashes and trim edge slashes
(path.split "/").filter { |x| x != "" }.join("/") (path.split '/').filter { |x| x != '' }.join('/')
end end
def _normalize(path) def _normalize(path)
# remove duplicate slashe s and trim edge slashes # remove duplicate slashe s and trim edge slashes
path = _normalize_input(path) path = _normalize_input(path)
# globbing behaviour simulated with regexp # globbing behaviour simulated with regexp
if path.match /(?<!\\)[\*\?\[]/ then if path.match /(?<!\\)[\*\?\[]/
path = Regexp.new(path path = Regexp.new(path
.gsub(/[\^\$\.\|\+\(\)\{\}]/,"\\\\\\0") .gsub(/[\^\$\.\|\+\(\)\{\}]/, '\\\\\\0')
.gsub(/(?<!\\)\*\*/,".*") .gsub(/(?<!\\)\*\*/, '.*')
.gsub(/(?<![.\\])\*/,"[^/]*") .gsub(/(?<![.\\])\*/, '[^/]*')
.gsub(/(?<!\\)\?/,".") .gsub(/(?<!\\)\?/, '.')
.gsub(/(?<!\\)\[\!/,"[^") .gsub(/(?<!\\)\[\!/, '[^')
) )
end end
return path path
end end
end end
@ -142,18 +123,18 @@ HTMLEOF
res.header['location'] = URI(url).to_s res.header['location'] = URI(url).to_s
throw :controlled_exit, @current_context throw :controlled_exit, @current_context
end end
def rewrite(url) def rewrite(url)
new_context = Context::rewrite(@current_context,url) new_context = Context.rewrite(@current_context, url)
new_context.exit_loop = false new_context.exit_loop = false
throw :controlled_exit, new_context throw :controlled_exit, new_context
end end
def die(code, message=nil, backtrace="")
def die(code, message = nil, backtrace = '')
@current_context.response.status = code @current_context.response.status = code
@current_context.response.set_backtrace(backtrace) @current_context.response.backtrace = backtrace
if not message then message ||= WEBrick::HTTPStatus::StatusMessage[code]
message = WEBrick::HTTPStatus::StatusMessage[code] if @current_context.codehandlers[code]
end
if @current_context.codehandlers[code] then
@current_context.codehandlers[code].call(@current_context, message, backtrace) @current_context.codehandlers[code].call(@current_context, message, backtrace)
else else
@current_context.response.body = Hyde.error_template(message, backtrace) @current_context.response.body = Hyde.error_template(message, backtrace)
@ -164,9 +145,9 @@ HTMLEOF
# Request wrapper class # Request wrapper class
class Context class Context
def initialize(path,request,response) def initialize(path, request, response)
@path = path @path = path
@filepath = "" @filepath = ''
@request = request @request = request
@response = response @response = response
@indexlist = [] @indexlist = []
@ -176,24 +157,22 @@ HTMLEOF
@queue_finalize = [] @queue_finalize = []
@exit_loop = false @exit_loop = false
end end
def self.rewrite(pctx,newpath)
newctx = Context.new(newpath,pctx.request,pctx.response) def self.rewrite(pctx, newpath)
newctx = Context.new(newpath, pctx.request, pctx.response)
newctx.vars = pctx.vars newctx.vars = pctx.vars
newctx.queue_finalize = pctx.queue_finalize.clone newctx.queue_finalize = pctx.queue_finalize.clone
newctx.queue_postprocess = pctx.queue_postprocess.clone newctx.queue_postprocess = pctx.queue_postprocess.clone
return newctx newctx
end end
def enqueue_finalizer(dup: false, &block) def enqueue_finalizer(dup: false, &block)
if block_given? then return unless block_given?
if dup or not @queue_finalize.include? block then @queue_finalize.append(block) if dup or !@queue_finalize.include? block
@queue_finalize.append(block)
end
end
end end
def enqueue_postprocessor(&block) def enqueue_postprocessor(&block)
if block_given? then @queue_postprocess.append(block) if block_given?
@queue_postprocess.append(block)
end
end end
attr_reader :request attr_reader :request
attr_reader :response attr_reader :response
@ -232,95 +211,94 @@ HTMLEOF
class Probe class Probe
include Hyde::PatternMatching include Hyde::PatternMatching
include Hyde::PublicContextControlMethods include Hyde::PublicContextControlMethods
def initialize (path, safe_regexp: true, &block_optional) def initialize(path, safe_regexp: true, &block_optional)
_prep_path path, safe_regexp: safe_regexp _prep_path path, safe_regexp: safe_regexp
@block = block_optional @block = block_optional
end end
def _match(request) def _match(request)
if @block and (_match? request.path, request) then return unless @block and (_match? request.path, request)
@current_context = Hyde::ProtectedContext.new(request) @current_context = Hyde::ProtectedContext.new(request)
return_later = self.instance_exec @current_context, &@block return_later = instance_exec @current_context, &@block
return return_later return_later
end
end end
# @sg-ignore
def _match?(path, request) def _match?(path, request)
# End node - nothing must be after it # End node - nothing must be after it
if super(path,request) then return unless super(path, request)
match_path = _normalize_input(path).match(@path) match_path = _normalize_input(path).match(@path)
return (match_path.post_match == "") match_path.post_match == ''
end
end end
end end
class Serve < Hyde::Probe class Serve < Hyde::Probe
def _match(request) def _match(request)
return super if @block return super if @block
if _match? request.path, request then return unless _match? request.path, request
@current_context = request @current_context = request
match_path = _normalize_input(request.path).match(@path)[0] match_path = _normalize_input(request.path).match(@path)[0]
filepath = request.filepath+match_path filepath = request.filepath + match_path
begin begin
mimetype = MIME::Types.type_for(filepath) mimetype = MIME::Types.type_for(filepath)
file = File.new filepath, "r" file = File.new filepath, 'r'
data = file.read() data = file.read
request.response.body = data request.response.body = data
request.response["Content-Type"] = mimetype request.response['Content-Type'] = mimetype
rescue Errno::ENOENT rescue Errno::ENOENT
die(404) die(404)
end end
end end
end end
end
class GetMatch < Hyde::Probe class GetMatch < Hyde::Probe
@match_method = "get" @match_method = 'get'
def initialize(*a, **b, &block) def initialize(*a, **b, &block)
@match_method = (self.class.instance_variable_get :@match_method) @match_method = (self.class.instance_variable_get :@match_method)
raise Exception, "block required!" if not block raise Exception, 'block required!' unless block
super(*a, **b, &block) super(*a, **b, &block)
end end
def _match?(path, ctx) def _match?(path, ctx)
if ctx.request.request_method == @match_method.upcase then if ctx.request.request_method == @match_method.upcase
return super(path, ctx) super(path, ctx)
else else
return false false
end end
end end
end end
class PostMatch < GetMatch class PostMatch < GetMatch
@match_method = "post" @match_method = 'post'
end end
class PutMatch < GetMatch class PutMatch < GetMatch
@match_method = "put" @match_method = 'put'
end end
class PatchMatch < GetMatch class PatchMatch < GetMatch
@match_method = "patch" @match_method = 'patch'
end end
class DeleteMatch < GetMatch class DeleteMatch < GetMatch
@match_method = "delete" @match_method = 'delete'
end end
class OptionsMatch < GetMatch class OptionsMatch < GetMatch
@match_method = "options" @match_method = 'options'
end end
class LinkMatch < GetMatch class LinkMatch < GetMatch
@match_method = "link" @match_method = 'link'
end end
class UnlinkMatch < GetMatch class UnlinkMatch < GetMatch
@match_method = "unlink" @match_method = 'unlink'
end end
class PrintProbe < Hyde::Probe class PrintProbe < Hyde::Probe
def _match(request) def _match(request)
if _match? request.path, request then puts "#{request.path} matched!" if _match? request.path, request
puts "#{request.path} matched!"
end
end end
end end
@ -338,9 +316,9 @@ HTMLEOF
options: Hyde::OptionsMatch, options: Hyde::OptionsMatch,
link: Hyde::LinkMatch, link: Hyde::LinkMatch,
unlink: Hyde::UnlinkMatch unlink: Hyde::UnlinkMatch
}.each_pair { |name, newclass| }.each_pair do |name, newclass|
define_method name do |path, *a, **b, &block| define_method name do |path, *a, **b, &block|
if path.kind_of? Array then if path.is_a? Array
path.each do |x| path.each do |x|
@chain.append newclass.new x, *a, **b, &block @chain.append newclass.new x, *a, **b, &block
end end
@ -348,22 +326,23 @@ HTMLEOF
@chain.append newclass.new path, *a, **b, &block @chain.append newclass.new path, *a, **b, &block
end end
end end
} end
end end
class Pathspec class Pathspec
include Hyde::PatternMatching include Hyde::PatternMatching
include Hyde::Handlers include Hyde::Handlers
include Hyde::PublicContextControlMethods include Hyde::PublicContextControlMethods
def initialize (path, root_path: nil, safe_regexp: true, &block) def initialize(path, root_path: nil, safe_regexp: true, &block)
_prep_path path, safe_regexp: safe_regexp _prep_path path, safe_regexp: safe_regexp
@chain = [] @chain = []
@root_override = root_path @root_override = root_path
@remap = false @remap = false
self.instance_exec &block instance_exec(&block)
end end
def path(path, *a, **b, &block) def path(path, *a, **b, &block)
if path.kind_of? Array then if path.is_a? Array
path.each do |x| path.each do |x|
@chain.append Hyde::Pathspec.new x, *a, **b, &block @chain.append Hyde::Pathspec.new x, *a, **b, &block
end end
@ -371,51 +350,58 @@ HTMLEOF
@chain.append Hyde::Pathspec.new path, *a, **b, &block @chain.append Hyde::Pathspec.new path, *a, **b, &block
end end
end end
def root(path) def root(path)
@root_override = "/"+_normalize_input(path) @root_override = '/' + _normalize_input(path)
end end
def remap(path) def remap(path)
@root_override = "/"+_normalize_input(path) @root_override = '/' + _normalize_input(path)
@remap = true @remap = true
end end
def index(list) def index(list)
@indexlist = list if list.kind_of? Array @indexlist = list if list.is_a? Array
@indexlist = [list] if list.kind_of? String @indexlist = [list] if list.is_a? String
end end
def preprocess(&block) def preprocess(&block)
@preprocessor = block @preprocessor = block
end end
def postprocess(&block) def postprocess(&block)
@postprocessor = block @postprocessor = block
end end
def finalize(dup: false, &block) def finalize(dup: false, &block)
@finalizer = block @finalizer = block
@finalizer_dup = dup @finalizer_dup = dup
end end
def _match(request) def _match(request)
@current_context = request @current_context = request
self.instance_exec request, &@preprocessor if @preprocessor instance_exec request, &@preprocessor if @preprocessor
request.enqueue_postprocessor &@postprocessor if @preprocessor request.enqueue_postprocessor(&@postprocessor) if @preprocessor
request.enqueue_finalizer dup: @finalizer_dup, &@finalizer if @finalizer request.enqueue_finalizer dup: @finalizer_dup, &@finalizer if @finalizer
if _match? request.path, request then if _match? request.path, request
match_path = _normalize_input(request.path).match(@path) match_path = _normalize_input(request.path).match(@path)
next_path = match_path[0] next_path = match_path[0]
request.path = cut_path = match_path.post_match request.path = cut_path = match_path.post_match
# remap/root method handling # remap/root method handling
if @root_override then if @root_override
request.filepath = if @remap then request.filepath = if @remap
@root_override+"/" @root_override + '/'
else @root_override+"/"+next_path+"/" end else @root_override + '/' + next_path + '/' end
else else
request.filepath = request.filepath+next_path+"/" request.filepath = request.filepath + next_path + '/'
end end
# redefine indexing parameters if they are defined for a pathspec # redefine indexing parameters if they are defined for a pathspec
request.indexlist = @indexlist if @indexlist request.indexlist = @indexlist if @indexlist
# do directory indexing # do directory indexing
if cut_path.match /^\/?$/ then if cut_path.match %r{^/?$}
request.indexlist.each do |x| request.indexlist.each do |x|
try_index = @chain.find { |y| y._match? x, request } try_index = @chain.find { |y| y._match? x, request }
if try_index then if try_index
request.path = x request.path = x
return try_index._match request return try_index._match request
end end
@ -424,7 +410,7 @@ HTMLEOF
# passthrough to the next path object # passthrough to the next path object
next_pathspec = @chain.find { |x| x._match? cut_path, request } next_pathspec = @chain.find { |x| x._match? cut_path, request }
next_pathspec._match request if next_pathspec next_pathspec._match request if next_pathspec
unless next_pathspec then unless next_pathspec
# die and throw up if nowhere to go # die and throw up if nowhere to go
die(404) die(404)
end end

189
lib/hyde.rb Normal file
View File

@ -0,0 +1,189 @@
module Hyde
# String and path processing utilities
module PatternMatching
# Strips extra slashes from a string
# (including slashes at the start and end of the string)
# @param string [String]
# @return [String]
def self.canonicalize(string)
string.gsub(/\/+/, "/")
.delete_prefix("/")
.delete_suffix("/")
end
# Implements glob-like pattern matching
# Exact specifications for globbing rules:
# "/"
# - act as directory separators
# - multiple slashes (i.e. "///") are the same as one slash ("/")
# - slashes are stripped at start and end of an expression or path
# - slashes are not matched by anything but the globstar ("**")
#
# "*" ( regexp: /([^/]*)/ )
# - matches from 0 to any number of characters
# - does not match nothing if placed between two slashes (i.e "/*/")
# - result is captured in an array
# - stops at slashes
# - greedy (matches as much as possible)
#
# "**" ( regexp: /(.*)/ )
# - matches any number of characters
# - matches slashes ("/")
# - result is captured in an array
# - does not stop at slashes
# - greedy (matches as much as possible)
#
# "?" ( regexp: /[^/]/ )
# - matches exactly one character
# - result is not captured
# - cannot match slashes
#
# "[...]" ( regexp: itself, ! and ^ at the start are interchangeable )
# - acts like a regexp range
# - matches any characters, including slashes if specified
# - valid ways to specify a range: [A-z], [a-z], [9-z] (ascii order)
# - ! or ^ at the start invert meaning (any character not in range)
# - result is not captured
#
# ":<name>" ( regexp: acts like a named group for /[^/]*/ )
# - acts like * as defined above
# - result is captured in a hash with <name> as key
# - <name> allows alphanumeric characters and underscores
class Glob
# @param input [String] Glob pattern
def initialize(pattern)
pattern = Hyde::PatternMatching.canonicalize(pattern)
pieces = pattern.split(/(\/\*\*\/|\*\*|\*|\?|\[!?\w-\w\]|:[^\/]+)/)
# @type [Array<String,Integer>]
@index = build_index(pieces)
# @type [Regexp]
@glob = Regexp.new(pieces.map do |filter|
case filter
when "/**/" then "/(.*/|)"
when "**" then "(.*)"
when "*" then "([^/]*)"
when "?" then "[^/]"
when /^\[!?\w-\w\]$/ then filter.sub('!', '^')
when /:[\w_]+/ then "[^/]*"
else filter.gsub(/[\^$.|+(){}]/, '\\\\\\0')
end
end.join("").prepend("^/?"))
puts @glob
end
# Match the string and assign matches to parameters
# Returns:
# - Unmatched part of a string
# - Unnamed parameters
# - Named parameters
# @param input [String] String to match
# @return [Array(String,Array,Hash)]
def match(input)
input = Hyde::PatternMatching.canonicalize(input)
result = input.match(@glob)
input = result.post_match
named_params, params = assign_by_index(@index, result.captures)
[input, params, named_params]
end
# Test if a string can be matched
# Lighter version of match that doesn't assign any variables
# @param input [String] String to match
# @return [Boolean]
def match?(input)
input = Hyde::PatternMatching.canonicalize(input)
input.match? @glob
end
# Test if input is convertible to a Glob and if there is any reason to
# @param input
# @return [Boolean] Input can be safely converted to Glob
def self.can_convert?(input)
input.kinf_of? String and
input.match?(/(?<!^\\)(?:\*\*|\*|\?|\[!?\w-\w\]|:[^\/]+)/)
end
private
# Build an index for separating normal matches from named matches
# @param pieces [Array(String)] Glob pattern after being splitted
# @return [Array(String,Integer)] Index array to use with assign_to_index
def build_index(pieces)
count = -1
index = []
pieces.each do |x|
if x.match?(/(\*\*|\*)/)
index.append(count += 1)
elsif (name = x.match(/:[^\/]+/))
index.append(name)
end
end
index
end
# Assign values from match.captures to named and numbered groups
# @param index [Array(String,Integer)] Index array generated by build_index
# @param params [Array] Unnamed captures from a String.match
def assign_by_index(index, params)
named_params = {}
new_params = []
params.each_with_index do |x, k|
if index[k].is_a? String
named_params[index[k]] = x
else
new_params[index[k]] = x
end
end
[named_params, new_params]
end
end
# Implements regexp pattern matching
class ReMatch
def initialize(pattern)
@glob = pattern
end
def match
end
def self.can_convert?(string)
string.is_a? Regexp
end
end
# Umbrella class that picks a suitable pattern to be a middle man for
class Pattern
def initialize(pattern, **options)
@pattern = patternify(pattern)
@static = @pattern.is_a? String
@options = options
end
def static?
@static
end
def match
end
private
# Try and convert the string to a pattern, if possible
def patternify(pattern)
Glob.new(pattern, **@options) if Glob.can_convert?(pattern)
ReMatch.new(pattern, **@options) if Glob.can_convert?(pattern)
end
end
end
class PathBinding
end
class Path
end
end

View File

@ -0,0 +1,2 @@
require_relative 'pattern_matching/util'
require_relative 'pattern_matching/glob'

View File

@ -0,0 +1,130 @@
module Hyde
module PatternMatching
# Implements glob-like pattern matching
# Exact specifications for globbing rules:
# "/"
# - act as directory separators
# - multiple slashes (i.e. "///") are the same as one slash ("/")
# - slashes are stripped at start and end of an expression or path
# - slashes are not matched by anything but the globstar ("**")
#
# "*" ( regexp: /([^/]*)/ )
# - matches from 0 to any number of characters
# - does not match nothing if placed between two slashes (i.e "/*/")
# - result is captured in an array
# - stops at slashes
# - greedy (matches as much as possible)
#
# "**" ( regexp: /(.*)/ )
# - matches any number of characters
# - matches slashes ("/")
# - result is captured in an array
# - does not stop at slashes
# - greedy (matches as much as possible)
#
# "?" ( regexp: /[^/]/ )
# - matches exactly one character
# - result is captured
# - cannot match slashes
#
# "[...]" ( regexp: itself, ! and ^ at the start are interchangeable )
# - acts like a regexp range
# - matches any characters, including slashes if specified
# - valid ways to specify a range: [A-z], [a-z], [9-z] (ascii order)
# - ! or ^ at the start invert meaning (any character not in range)
# - result is captured
#
# ":<name>" ( regexp: acts like a named group for /[^/]*/ )
# - acts like * as defined above
# - result is captured in a hash with <name> as key
# - <name> allows alphanumeric characters and underscores
class Glob
# @param input [String] Glob pattern
def initialize(pattern)
pattern = Hyde::PatternMatching.canonicalize(pattern)
pieces = pattern.split(/(\/\*\*\/|\*\*|\*|\?|\[!?\w-\w\]|:[^\/]+)/)
# @type [Array<String,Integer>]
@index = build_index(pieces)
# @type [Regexp]
@glob = Regexp.new(pieces.map do |filter|
case filter
when "/**/" then "/(.*/|)"
when "**" then "(.*)"
when "*" then "([^/]*)"
when "?" then "([^/])"
when /^\[!?\w-\w\]$/ then "(#{filter.sub('!', '^')})"
when /:[\w_]+/ then "([^/]*)"
else filter.gsub(/[\^$.|+(){}]/, '\\\\\\0')
end
end.join("").prepend("^/?"))
end
# Match the string and assign matches to parameters
# Returns:
# - Unmatched part of a string
# - Unnamed parameters
# - Named parameters
# @param input [String] String to match
# @return [Array(String,Array,Hash)]
def match(input)
input = Hyde::PatternMatching.canonicalize(input)
result = input.match(@glob)
input = result.post_match
named_params, params = assign_by_index(@index, result.captures)
[input, params, named_params]
end
# Test if a string can be matched
# Lighter version of match that doesn't assign any variables
# @param input [String] String to match
# @return [Boolean]
def match?(input)
input = Hyde::PatternMatching.canonicalize(input)
input.match? @glob
end
# Test if input is convertible to a Glob and if there is any reason to
# @param input
# @return [Boolean] Input can be safely converted to Glob
def self.can_convert?(input)
input.kinf_of? String and
input.match?(/(?<!^\\)(?:\*\*|\*|\?|\[!?\w-\w\]|:[^\/]+)/)
end
private
# Build an index for separating normal matches from named matches
# @param pieces [Array(String)] Glob pattern after being splitted
# @return [Array(String,Integer)] Index array to use with assign_to_index
def build_index(pieces)
count = -1
index = []
pieces.each do |x|
if x.match?(/(\/\*\*\/|\*\*|\*|\?|\[!?\w-\w\])/)
index.append(count += 1)
elsif (name = x.match(/(?<=:)[^\/]+/))
index.append(name[0])
end
end
index
end
# Assign values from match.captures to named and numbered groups
# @param index [Array(String,Integer)] Index array generated by build_index
# @param params [Array] Unnamed captures from a String.match
def assign_by_index(index, params)
named_params = {}
new_params = []
puts index.inspect
params.each_with_index do |x, k|
if index[k].is_a? String
named_params[index[k]] = x
else
new_params[index[k]] = x
end
end
[named_params, new_params]
end
end
end
end

View File

@ -0,0 +1,13 @@
module Hyde
module PatternMatching
# Strips extra slashes from a string
# (including slashes at the start and end of the string)
# @param string [String]
# @return [String]
def self.canonicalize(string)
string.gsub(/\/+/, "/")
.delete_prefix("/")
.delete_suffix("/")
end
end
end

View File

@ -0,0 +1,106 @@
require_relative "../lib/hyde/pattern_matching"
require "test/unit"
class TestGlob < Test::Unit::TestCase
include Hyde::PatternMatching
# match? test
def test_matchq
# testing "*"
unit = Glob.new("/test/*")
[
"est/anything", false,
"/test", false,
"/test/anything", true,
"/test//as", true,
"/test/", false,
"/test/as/whatever", true,
"test/as", true
].each_slice(2) do |test, result|
puts("Testing: #{test}")
assert_equal(result, unit.match?(test))
end
unit = Glob.new("/test/*/something")
[
"/test/s/something", true,
"/test//something", false,
"/test/something", false,
"test/b/something", true,
"/test/b/someth", false
].each_slice(2) do |test, result|
puts("Testing: #{test}")
assert_equal(result, unit.match?(test))
end
# testing "**"
unit = Glob.new("/test/**/something")
[
"/test/s/something", true,
"/test/dir/something", true,
"/test/something", true,
"test/b/something", true,
"/test/b/someth", false,
"/test/a/b/c/something", true,
"/test/a/b/csomething", false,
"/testsomething", false,
"/something", false
].each_slice(2) do |test, result|
puts("Testing: #{test}")
assert_equal(result, unit.match?(test))
end
unit = Glob.new("/test/**/*.php")
[
"/test/archive.php", true,
"/test/assets/thing.js", false,
"/test/assetsthing.js", false,
"/test/parts/thing.php", true,
"/test/partsthing.php", true,
"/test/.php", true,
"/test/parts/extra/test.php", true,
"test/archive.php", true,
"test/assets/thing.js", false,
"test/assetsthing.js", false,
"test/parts/thing.php", true,
"test/partsthing.php", true,
"test/.php", true,
"test/parts/extra/test.php", true,
"/test/parts/extra/test.php/literally/anything/here", true
].each_slice(2) do |test, result|
puts("Testing: #{test}")
assert_equal(result, unit.match?(test))
end
# testing ?
unit = Glob.new("/test/?hit")
[
"/test/thing", false,
"/test/chit", true,
"/test//hit", false,
"/testhit", false
].each_slice(2) do |test, result|
puts("Testing: #{test}")
assert_equal(result, unit.match?(test))
end
# testing char ranges
unit = Glob.new("/test/[9-z]+")
[
"/test/t+", true,
"/test/$+", false,
"/test/aosidujqwi", false,
"/test/9+", true
].each_slice(2) do |test, result|
puts("Testing: #{test}")
assert_equal(result, unit.match?(test))
end
# testing named captures
unit = Glob.new("/test/:name/something")
[
"/test/something/something", true,
"/test/asd/something/extra", true,
"/test//something/what", false,
"/test/something/what", false,
"/test/asd/asd/something/what", false
].each_slice(2) do |test, result|
puts("Testing: #{test}")
assert_equal(result, unit.match?(test))
end
end
end

View File

@ -1,18 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title> Welcome to Hyde </title>
</head>
<body>
<h1> Welcome to <a href="https://adastra7.net/git/Yessiest/hyde">Hyde</a> </h1>
<p> Hyde is the horrible side of Jekyll, and, consequently, this particular project.</p>
<ul>
<li> <a href="/uploads/">Uploads</a> </li>
<li> <a href="/about/webrick">WEBrick</a> </li>
<li> <a href="/about/hyde">Hyde</a> </li>
</ul>
<hr />
<p> Created by Yessiest </p>
</body>
</html>

View File

@ -1,11 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title> test </title>
</head>
<body>
<h1> This is a test </h1>
<hr>
<address> yes </address>
</body>
</html>

View File

@ -1,7 +0,0 @@
# Welcome to the INFINITE HYPERNET
---
- THE HYPE IS REAL
- SCENE IS DEAD
- BLOOD OF LAMERS IS FUEL

View File

@ -1,11 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title> very test </title>
</head>
<body>
<h1> This is a very test </h1>
<hr>
<address> yes 2 </address>
</body>
</html>

View File

@ -1 +0,0 @@
# YES

View File

@ -1,15 +0,0 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN">
<HTML>
<HEAD><TITLE>File listing</TITLE></HEAD>
<BODY>
<H1>File listing:</H1>
<ul>
<li><a href="/uploads/01-blog/test.md">this</a></li>
<li><a href="/uploads/02-rules/megafuck.md">that</a></li>
</ul>
<HR>
<ADDRESS>
welcum to this crap
</ADDRESS>
</BODY>
</HTML>

View File

@ -1,32 +0,0 @@
require_relative "hyde"
server = Hyde::Server.new Port: 8000 do
{"add" => -> (a,b) { a + b },
"sub" => -> (a,b) { a - b },
"mul" => -> (a,b) { a * b },
"div" => -> (a,b) {
begin
return a/b
rescue ZeroDivisionError
return "Divided by zero"
end
}
}.each_pair do |k,v|
serve k do |ctx|
req,res = ctx.request,ctx.response
a,b = req.query["a"],req.query["b"]
result = (a and b) ? v.call(a.to_f,b.to_f) : "Invalid parameters"
res.body = "
<!DOCTYPE html>
<html>
<head>
<title> Calculator API test </title>
</head>
<body>
<h> Result: #{result} </h>
</body>
</html>"
res['Content-Type'] = "text/html"
end
end
end
server.start

View File

@ -1,46 +0,0 @@
require_relative 'hyde'
server = Hyde::Server.new Port: 8000 do
root "/home/yessiest/Projects/hyde/test/"
serve "index.html"
index ["index.html"]
path "about" do
preprocess do |ctx|
puts "#{ctx} entered fully virtual directory!"
end
postprocess do |ctx|
puts "#{ctx} reached endpoint!"
end
finalize do |ctx|
puts "#{ctx} finished processing!"
end
get "portal" do |ctx|
ctx.vars[:ass] = true
rewrite "/about/hyde"
end
get "webrick" do |ctx|
ctx.response.body = "WEBrick is a modular http server stack"
ctx.response['Content-Type'] = "text/plain"
end
get "en_passant" do |ctx|
puts "holy hell!"
redirect "https://google.com/search?q=en+passant"
end
get "hyde" do |ctx|
puts ctx.vars[:ass]
ctx.response.body = "Hyde is the disgusting side of Jekyll, and, by extension, the thing that makes WEBrick usable."
ctx.response['Content-Type'] = "text/plain"
end
post "hyde" do |ctx|
ctx.response.body = "Your message: #{ctx.request.body}"
ctx.response['Content-Type'] = "text/plain"
end
end
path "uploads" do
index ["index.html"]
serve "**/*.md", safe_regexp: false
serve ["*.html","**/*.html"], safe_regexp: false
end
end
server.start

View File

@ -1,72 +0,0 @@
require_relative "hyde"
path = Hyde::Pathspec.new "/" do
root "/var/www"
path "about" do
printProbe "test"
printProbe "test_*"
end
path "docs" do
remap "/var/www/markdown_compiled/"
printProbe "test"
printProbe "test_*"
probe "speen" do |request|
puts "maurice spinning"
redirect "https://www.youtube.com/watch?v=KeNyN_rVL_c"
pp request
end
end
path "cell_1337" do
root "/var/www/cells"
path "control" do
probe "close" do |request|
puts "Permissions level 4 required to control this cell"
pp request
end
probe "open" do |request|
puts "Permissions level 4 required to control this cell"
pp request
end
end
printProbe "info"
end
path (/cell_[^\/]*/) do
root "/var/www/cells"
path "control" do
probe "close" do |request|
puts "Closing cell #{request.filepath.match /cell_[^\/]*/}"
pp request
end
probe "open" do |request|
puts "Opening cell #{request.filepath.match /cell_[^\/]*/}"
pp request
end
end
printProbe "dura"
printProbe "info"
end
path "bad_?" do
printProbe "path"
end
probe ["info","hyde"] do
puts "this is the most disgusting and visceral thing i've written yet, and i love it"
end
end
[
Hyde::Request.new("/about/speen",nil,nil),
Hyde::Request.new("/about/test",nil,nil),
Hyde::Request.new("/about/test_2",nil,nil),
Hyde::Request.new("/docs/speen",nil,nil),
Hyde::Request.new("/docs/test",nil,nil),
Hyde::Request.new("/docs/test_3",nil,nil),
Hyde::Request.new("/cell_41/control/open",nil,nil),
Hyde::Request.new("/cell_21/control/close",nil,nil),
Hyde::Request.new("/cell_19283/info",nil,nil),
Hyde::Request.new("/cell_1337/control/close",nil,nil),
Hyde::Request.new("/duracell_129/control/open",nil,nil),
Hyde::Request.new("/duracell_1447/control/close",nil,nil),
Hyde::Request.new("/bad_2path",nil,nil),
Hyde::Request.new("/info",nil,nil),
Hyde::Request.new("/hyde",nil,nil)
].each { |x| path.match(x) }
# what a load of fuck this is

View File

@ -1,14 +0,0 @@
require 'webrick'
server = WEBrick::HTTPServer.new :Port => 8000
trap 'INT' do server.shutdown end
server.mount_proc '/' do |req, res|
pp res
pp req
res['Content-Type'] = "text/plain"
res.body = 'A'*65536+"Hello world"
end
server.start