From ed606df300289fa25782df839e06884e395af00e Mon Sep 17 00:00:00 2001 From: Victor Seva Date: Thu, 26 Sep 2013 17:29:04 +0200 Subject: [PATCH] Imported Upstream version 0.1+20130926+git14fa255d --- .gitignore | 3 + COPYRIGHT | 23 ++ Changes | 9 + MANIFEST | 79 +++++ Makefile | 58 ++++ README | 17 + TODO | 50 +++ doc/.gitignore | 2 + doc/lua-uri-_login.pod | 68 ++++ doc/lua-uri-_util.pod | 110 +++++++ doc/lua-uri-data.pod | 67 ++++ doc/lua-uri-file.pod | 137 ++++++++ doc/lua-uri-ftp.pod | 45 +++ doc/lua-uri-http.pod | 28 ++ doc/lua-uri-pop.pod | 63 ++++ doc/lua-uri-rtsp.pod | 24 ++ doc/lua-uri-telnet.pod | 25 ++ doc/lua-uri-urn-isbn.pod | 52 +++ doc/lua-uri-urn-issn.pod | 41 +++ doc/lua-uri-urn-oid.pod | 51 +++ doc/lua-uri-urn.pod | 72 +++++ doc/lua-uri.pod | 463 +++++++++++++++++++++++++++ lunit-console.lua | 141 ++++++++ lunit.lua | 670 +++++++++++++++++++++++++++++++++++++++ test/_generic.lua | 636 +++++++++++++++++++++++++++++++++++++ test/_pristine.lua | 36 +++ test/_relative.lua | 54 ++++ test/_resolve.lua | 201 ++++++++++++ test/_util.lua | 92 ++++++ test/data.lua | 125 ++++++++ test/file.lua | 147 +++++++++ test/ftp.lua | 55 ++++ test/http.lua | 67 ++++ test/pop.lua | 129 ++++++++ test/rtsp.lua | 44 +++ test/telnet.lua | 141 ++++++++ test/urn-isbn.lua | 110 +++++++ test/urn-issn.lua | 110 +++++++ test/urn-oid.lua | 101 ++++++ test/urn.lua | 137 ++++++++ uri-test.lua | 85 +++++ uri.lua | 507 +++++++++++++++++++++++++++++ uri/_login.lua | 96 ++++++ uri/_relative.lua | 81 +++++ uri/_util.lua | 130 ++++++++ uri/data.lua | 116 +++++++ uri/file.lua | 72 +++++ uri/file/unix.lua | 27 ++ uri/file/win32.lua | 34 ++ uri/ftp.lua | 54 ++++ uri/http.lua | 32 ++ uri/https.lua | 9 + uri/pop.lua | 111 +++++++ uri/rtsp.lua | 9 + uri/rtspu.lua | 7 + uri/telnet.lua | 39 +++ uri/urn.lua | 133 ++++++++ uri/urn/isbn.lua | 67 ++++ uri/urn/issn.lua | 65 ++++ uri/urn/oid.lua | 64 ++++ 60 files changed, 6221 insertions(+) create mode 100644 .gitignore create mode 100644 COPYRIGHT create mode 100644 Changes create mode 100644 MANIFEST create mode 100644 Makefile create mode 100644 README create mode 100644 TODO create mode 100644 doc/.gitignore create mode 100644 doc/lua-uri-_login.pod create mode 100644 doc/lua-uri-_util.pod create mode 100644 doc/lua-uri-data.pod create mode 100644 doc/lua-uri-file.pod create mode 100644 doc/lua-uri-ftp.pod create mode 100644 doc/lua-uri-http.pod create mode 100644 doc/lua-uri-pop.pod create mode 100644 doc/lua-uri-rtsp.pod create mode 100644 doc/lua-uri-telnet.pod create mode 100644 doc/lua-uri-urn-isbn.pod create mode 100644 doc/lua-uri-urn-issn.pod create mode 100644 doc/lua-uri-urn-oid.pod create mode 100644 doc/lua-uri-urn.pod create mode 100644 doc/lua-uri.pod create mode 100644 lunit-console.lua create mode 100644 lunit.lua create mode 100644 test/_generic.lua create mode 100644 test/_pristine.lua create mode 100644 test/_relative.lua create mode 100644 test/_resolve.lua create mode 100644 test/_util.lua create mode 100644 test/data.lua create mode 100644 test/file.lua create mode 100644 test/ftp.lua create mode 100644 test/http.lua create mode 100644 test/pop.lua create mode 100644 test/rtsp.lua create mode 100644 test/telnet.lua create mode 100644 test/urn-isbn.lua create mode 100644 test/urn-issn.lua create mode 100644 test/urn-oid.lua create mode 100644 test/urn.lua create mode 100644 uri-test.lua create mode 100644 uri.lua create mode 100644 uri/_login.lua create mode 100644 uri/_relative.lua create mode 100644 uri/_util.lua create mode 100644 uri/data.lua create mode 100644 uri/file.lua create mode 100644 uri/file/unix.lua create mode 100644 uri/file/win32.lua create mode 100644 uri/ftp.lua create mode 100644 uri/http.lua create mode 100644 uri/https.lua create mode 100644 uri/pop.lua create mode 100644 uri/rtsp.lua create mode 100644 uri/rtspu.lua create mode 100644 uri/telnet.lua create mode 100644 uri/urn.lua create mode 100644 uri/urn/isbn.lua create mode 100644 uri/urn/issn.lua create mode 100644 uri/urn/oid.lua diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c86cf1c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +lua-uri-*.tar.bz2 +lua-uri-*.tar.gz +lua-uri-*.zip diff --git a/COPYRIGHT b/COPYRIGHT new file mode 100644 index 0000000..0a6e225 --- /dev/null +++ b/COPYRIGHT @@ -0,0 +1,23 @@ +This software and documentation is distributed under the same terms as +Lua version 5.0, the MIT/X Consortium license. The full terms are as +follows: + +Copyright (C) 2007 Geoff Richards + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/Changes b/Changes new file mode 100644 index 0000000..5c9ad6b --- /dev/null +++ b/Changes @@ -0,0 +1,9 @@ +1.1 2012-00-00 + + * Fix accidental setting of global variable 'err' in 'uri.ftp' and + 'uri.telnet'. + * Switch test suite from Lunit 0.3 to Lunit 0.5. + +1.0 2007-11-30 + + * Initial release. diff --git a/MANIFEST b/MANIFEST new file mode 100644 index 0000000..52bf849 --- /dev/null +++ b/MANIFEST @@ -0,0 +1,79 @@ +COPYRIGHT +Changes +MANIFEST +Makefile +README +TODO +debian/Makefile.Debian.conf +debian/changelog +debian/compat +debian/control +debian/copyright +debian/liblua5.1-uri-dev.manpages +debian/rules +doc/lua-uri-_login.3 +doc/lua-uri-_login.pod +doc/lua-uri-_util.3 +doc/lua-uri-_util.pod +doc/lua-uri-data.3 +doc/lua-uri-data.pod +doc/lua-uri-file.3 +doc/lua-uri-file.pod +doc/lua-uri-ftp.3 +doc/lua-uri-ftp.pod +doc/lua-uri-http.3 +doc/lua-uri-http.pod +doc/lua-uri-pop.3 +doc/lua-uri-pop.pod +doc/lua-uri-rtsp.3 +doc/lua-uri-rtsp.pod +doc/lua-uri-telnet.3 +doc/lua-uri-telnet.pod +doc/lua-uri-urn-isbn.3 +doc/lua-uri-urn-isbn.pod +doc/lua-uri-urn-issn.3 +doc/lua-uri-urn-issn.pod +doc/lua-uri-urn-oid.3 +doc/lua-uri-urn-oid.pod +doc/lua-uri-urn.3 +doc/lua-uri-urn.pod +doc/lua-uri.3 +doc/lua-uri.pod +lunit-console.lua +lunit.lua +test/_generic.lua +test/_pristine.lua +test/_relative.lua +test/_resolve.lua +test/_util.lua +test/data.lua +test/file.lua +test/ftp.lua +test/http.lua +test/pop.lua +test/rtsp.lua +test/telnet.lua +test/urn-isbn.lua +test/urn-issn.lua +test/urn-oid.lua +test/urn.lua +uri-test.lua +uri.lua +uri/_login.lua +uri/_relative.lua +uri/_util.lua +uri/data.lua +uri/file.lua +uri/file/unix.lua +uri/file/win32.lua +uri/ftp.lua +uri/http.lua +uri/https.lua +uri/pop.lua +uri/rtsp.lua +uri/rtspu.lua +uri/telnet.lua +uri/urn.lua +uri/urn/isbn.lua +uri/urn/issn.lua +uri/urn/oid.lua diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5016bec --- /dev/null +++ b/Makefile @@ -0,0 +1,58 @@ +PACKAGE=lua-uri +VERSION=$(shell head -1 Changes | sed 's/ .*//') +RELEASEDATE=$(shell head -1 Changes | sed 's/.* //') +PREFIX=/usr/local +DISTNAME=$(PACKAGE)-$(VERSION) + +# The path to where the module's source files should be installed. +LUA_SPATH:=$(shell pkg-config lua5.1 --define-variable=prefix=$(PREFIX) \ + --variable=INSTALL_LMOD) + +MANPAGES = doc/lua-uri.3 doc/lua-uri-_login.3 doc/lua-uri-_util.3 doc/lua-uri-data.3 doc/lua-uri-file.3 doc/lua-uri-ftp.3 doc/lua-uri-http.3 doc/lua-uri-pop.3 doc/lua-uri-rtsp.3 doc/lua-uri-telnet.3 doc/lua-uri-urn.3 doc/lua-uri-urn-isbn.3 doc/lua-uri-urn-issn.3 doc/lua-uri-urn-oid.3 + +all: $(MANPAGES) + +doc/lua-%.3: doc/lua-%.pod Changes + sed 's/E/(c)/g' <$< | sed 's/E/-/g' | \ + pod2man --center="Lua $(shell echo $< | sed 's/^doc\/lua-//' | sed 's/\.pod$$//' | sed 's/-/./g') module" \ + --name="$(shell echo $< | sed 's/^doc\///' | sed 's/\.pod$$//' | tr a-z A-Z)" --section=3 \ + --release="$(VERSION)" --date="$(RELEASEDATE)" >$@ + +test: all + echo 'lunit.main({...})' | $(VALGRIND) lua -llunit - test/*.lua + +install: all + mkdir -p $(LUA_SPATH)/uri/{file,urn} + mkdir -p $(PREFIX)/share/man/man3 + install --mode=644 uri.lua $(LUA_SPATH)/ + for module in _login _relative _util data file ftp http https pop rtsp rtspu telnet urn; do \ + install --mode=644 uri/$$module.lua $(LUA_SPATH)/uri/; \ + done + for module in unix win32; do \ + install --mode=644 uri/file/$$module.lua $(LUA_SPATH)/uri/file/; \ + done + for module in isbn issn oid; do \ + install --mode=644 uri/urn/$$module.lua $(LUA_SPATH)/uri/urn/; \ + done + for manpage in $(MANPAGES); do \ + gzip -c $$manpage >$(PREFIX)/share/man/man3/$$(echo $$manpage | sed -e 's/^doc\///').gz; \ + done + +checktmp: + @if [ -e tmp ]; then \ + echo "Can't proceed if file 'tmp' exists"; \ + false; \ + fi +dist: all checktmp + mkdir -p tmp/$(DISTNAME) + tar cf - --files-from MANIFEST | (cd tmp/$(DISTNAME) && tar xf -) + cd tmp && tar cf - $(DISTNAME) | gzip -9 >../$(DISTNAME).tar.gz + cd tmp && tar cf - $(DISTNAME) | bzip2 -9 >../$(DISTNAME).tar.bz2 + rm -f $(DISTNAME).zip + cd tmp && zip -q -r -9 ../$(DISTNAME).zip $(DISTNAME) + rm -rf tmp + +clean: + rm -f $(MANPAGES) + +.PHONY: all test install checktmp dist clean diff --git a/README b/README new file mode 100644 index 0000000..68e33c9 --- /dev/null +++ b/README @@ -0,0 +1,17 @@ +This library allows you to normalize and validate URIs, and provides methods +for manipulating them in various ways. + +The Lua-URI library is written in pure Lua. No C compilation is required +to install it. + +When you unpack the source code everything should already be ready for +installation. Doing "make install" as root will install the Lua source +files and the man pages containing the documentation. + +See lua-uri(3) for information about how to use the library. The same +documentation is available on the website, where you can also get the +latest packages: + + http://www.geoffrichards.co.uk/lua/uri/ + +Send bug reports, suggestions, etc. to Geoff Richards diff --git a/TODO b/TODO new file mode 100644 index 0000000..3ba00c6 --- /dev/null +++ b/TODO @@ -0,0 +1,50 @@ +Perhaps incorporate Mozilla test suite for data: URIs: + http://www.mozilla.org/quality/networking/testing/datatests.html +also validate the mediatype part and normalize it by omitting things which +will be the same by default. + +Check for compliance with latest RFC: + http://tools.ietf.org/html/rfc3986 +(uri_encode might use the wrong default for 'patn') + +Try to integrate support for IRIs: + http://tools.ietf.org/html/rfc3987 + +Other schemes: +complete IANA list: http://www.iana.org/assignments/uri-schemes.html +opaquelocktoken - http://www.rfc-editor.org/rfc/rfc4918.txt (Appendix C) +svn +mailto - http://tools.ietf.org/html/rfc2368 +info - http://tools.ietf.org/html/rfc4452 +prospero - http://tools.ietf.org/html/rfc4157 +wais - http://tools.ietf.org/html/rfc4156 +tel - http://tools.ietf.org/html/rfc3966 +sip and sips - http://tools.ietf.org/html/rfc3261 +gopher - http://tools.ietf.org/html/rfc4266 +tag - http://tools.ietf.org/html/rfc4151 + +new IMAP URI RFC: + http://www.rfc-editor.org/rfc/rfc5092.txt + +Other NIDs for URNs, some of which are shown here: + http://en.wikipedia.org/wiki/Uniform_Resource_Name + +Other NIDs which have RFCs: + ietf - http://tools.ietf.org/html/rfc2648 + publicid - http://tools.ietf.org/html/rfc3151 + uuid - http://tools.ietf.org/html/rfc4122 + sici - http://tools.ietf.org/html/rfc2288 (not really standardised there, but is there a proper RFC?) + service - http://tools.ietf.org/html/rfc5031 + epc - http://tools.ietf.org/html/rfc5134 + +check these CPAN bugs against URI module: + mailto encoding: http://rt.cpan.org/Public/Bug/Display.html?id=24934 + encode: http://rt.cpan.org/Public/Bug/Display.html?id=21640 + +gopher URI found in the wild, use for testing: + + +Once again provide the stripping of '' and such like. + +See if this API has any good ideas: + http://addressable.rubyforge.org/api/ diff --git a/doc/.gitignore b/doc/.gitignore new file mode 100644 index 0000000..0e049ad --- /dev/null +++ b/doc/.gitignore @@ -0,0 +1,2 @@ +lua-uri.3 +lua-uri-*.3 diff --git a/doc/lua-uri-_login.pod b/doc/lua-uri-_login.pod new file mode 100644 index 0000000..7645a1d --- /dev/null +++ b/doc/lua-uri-_login.pod @@ -0,0 +1,68 @@ +=head1 Name + +lua-uri-_login - Lua URI library support for URIs containing usernames and passwords + +=head1 Description + +The C class is used as a base class by classes implementing URI +schemes which can have a username and password in the userinfo part, separated +by a colon. + +A URI of this type where the userinfo part contains more than one colon +is considered invalid. They must also have a non-empty host part. The +username and password are each optional. + +The current implementation requires subclasses to call this class's +C method within their C method to do the extra validation. +This may change if I think of a better way of doing it. + +=head1 Methods + +All the methods defined in L are supported, in addition to the +following: + +=over + +=item uri:username(...) + +Mutator for the username in the userinfo part. Returns an optionally sets +the first part of the userinfo, before the colon. If there is no password +then the username will be the whole of the userinfo part, and no colon will +be present. + +=for syntax-highlight lua + + local uri = assert(URI:new("ftp://host/path")) + uri:username("fred") -- ftp://fred@host/path + uri:username(nil) -- ftp://host/path + +Passing nil as the new username will also remove any password in the userinfo, +since the password is expected to be meaningless without the username. + +The username is appropriately percent encoded and decoded by this method. + +=item uri:password(...) + +Mutator for the password part of the userinfo. This will appear after a +colon, whether or not there is a username. + +The password is appropriately percent encoded and decoded by this method. + +=for syntax-highlight lua + + local password = uri:password() + uri:password("secret") + +=back + +=head1 References + +The main RFC for URIs (L) does not specify a syntax for the +userinfo part of the authority, which is why the C and C +methods are not provided in the generic C class. The use of the colon +to separate these parts, and the escaping conventions, are instead derived +from the older L, and the up to date telnet URI +specification in L. + +=for comment +vi:ts=4 sw=4 expandtab diff --git a/doc/lua-uri-_util.pod b/doc/lua-uri-_util.pod new file mode 100644 index 0000000..12cecf8 --- /dev/null +++ b/doc/lua-uri-_util.pod @@ -0,0 +1,110 @@ +=head1 Name + +lua-uri-_util - Utility functions for Lua URI library + +=head1 Description + +This module contains various utility functions used by the rest of the +library. They are mostly intended only for internal use, and are subject +to change in future versions, but the URI encoding and decoding functions +may be more widely useful. + +On loading, the module returns a table containing the functions, but like +all the modules in this library it does not install itself into any global +variables. + +=for syntax-highlight lua + + local Util = require "uri._util" + +=head1 Functions + +The following functions can be found in the table returned from C: + +=over + +=item uri_encode(text, pattern) + +Use URI encoding (or 'percent encoding') to encode any unsafe characters +in C. If C is specified then it should be part of a Lua +pattern which can be enclosed in square brackets to make a character class. +Usually it will start with C<^> so that the rest of the characters will be +considered the 'safe' ones, not to be encoded. Any character matched by the +pattern will be encoded. + +=for syntax-highlight lua + + print(Util.uri_encode("foo bar!")) + ---> foo%20bar! + print(Util.uri_encode("foo bar!", "^A-Za-z0-9")) + ---> foo%20bar%21 + +The default pattern is: C<^A-Za-z0-9%-_.!~*'()> + +=item uri_decode(text, pattern) + +Decode any URI encoding in C. If C is nil then all encoded +characters will be decoded. If a pattern is supplied then it should be in +the same form as for C. Any character not matched by the pattern +will be left encoded as it was. + +=for syntax-highlight lua + + print(Util.uri_decode("foo%20bar%21")) + ---> foo bar! + print(Util.uri_decode("foo%20bar%21", "^!")) + ---> foo bar%21 + +=item remove_dot_segments(path) + +Removes single and double dot segments from a URI path. + +This is the 'remove_dot_segments' algorithm from L. +The value of C is used as the input buffer, and the contents of the +output buffer are returned. + +=item split(pattern, str, max) + +Split the string C wherever C matches it, returning the pieces +as individual strings in an array. If C is not nil, then stop splitting +after that many pieces have been created. + +=item attempt_require(name) + +Calling this function is the same as calling Lua's built in C +function, except that if a module called C cannot be found, it returns +nil instead of throwing an exception. If loading the module is successful +then the result of C is returned. An exception is thrown if any +error occurs loading the module other than it not being found. + +=item subclass_of(class, baseclass) + +Sets up the metatable and a few other things for the table C so that it +will be a subclass of C. This is used by the classes in this +library to implement inheritance. + +=item do_class_changing_change(uri, baseclass, changedesc, newvalue, changefunc) + +This is used when a mutator method changes something about a URI which leads it +to need to belong to a different class. C is the URI object to change, +C is the class to reset it to before making the change, +C is a description to be included in an error message if necessary, +C is the new value to be set (which must be a string, as it is also +included in error messages), and C is a function which is called +with a temporary URI object it should adjust and C. + +=item uri_part_not_allowed (class, method) + +This should be called in scheme-specific classes where certain parts of URIs +are not allowed to be present (e.g., the 'host' part in a URN). It will +override the named method in the class with one which throws an exception +if an attempt is made to set the part to anything other than nil. If the +rest of the code for the scheme keeps objects internally consistent then the +new method should always return nil, although when a URI is being validated +during the C method's execution, it may return other things, which can +be used to detect disallowed parts in a URI being parsed. + +=back + +=for comment +vi:ts=4 sw=4 expandtab diff --git a/doc/lua-uri-data.pod b/doc/lua-uri-data.pod new file mode 100644 index 0000000..5c75496 --- /dev/null +++ b/doc/lua-uri-data.pod @@ -0,0 +1,67 @@ +=head1 Name + +lua-uri-data - data URI support for Lua URI library + +=head1 Description + +The class C is used for URIs with the C scheme. It inherits +from the L class. + +Some of the features of this module require the L library +to be installed, but URI objects can still be created from C URIs +even if it isn't available. + +Any C URI containing an authority part is considered to be invalid, +as is one whose path does not contain a comma. If the URI has the +C<;base64> parameter, then the data must consist only of characters allowed +in base64 encoded data (upper and lowercase ASCII letters, digits, and the +forward slash and plus characters). + +=head1 Methods + +All the methods defined in L are supported. The C, +C, and C methods will always return nil, and will throw an +exception when passed anything other than nil. The C method will throw +an exception if given a new path which is nil or not valid for the C +scheme. + +The following additional methods are supported: + +=over + +=item uri:data_bytes(...) + +Get or set the data stored in the URI. The existing data is decoded and +returned. If a new value is supplied it must be a string, and will cause +the path of the URI to be changed to encode the new data. The method will +choose the encoding which will result in the smallest URI, unless the +datafilter module is not installed, in which case it will always use +percent encoding. + +An exception is thrown if the datafilter module is not installed and the data +in the URI is encoded as base64, although a data URI using percent encoding +will not cause an exception. + +The data passed in and returned should not be encoded in any special way, +that is taken care of by the library. + +=item uri:data_media_type(...) + +Get or set the media type (MIME type) stored in the URI's path before the +comma. This should not include the C<;base64> parameter, which will be +included in the path automatically when appropriate. + +If there is no media type given in the URI then the default value of +C will be returned, and if there is no C parameter +given then the default C<;charset=US-ASCII> will be included. + +The media type is encoded and decoded automatically by this method. + +=back + +=head1 References + +This class is based on L. + +=for comment +vi:ts=4 sw=4 expandtab diff --git a/doc/lua-uri-file.pod b/doc/lua-uri-file.pod new file mode 100644 index 0000000..668f827 --- /dev/null +++ b/doc/lua-uri-file.pod @@ -0,0 +1,137 @@ +=head1 Name + +lua-uri-file - File URI support for Lua URI library + +=head1 Description + +The class C is used for URIs with the C scheme. It inherits +from the L class. + +A file URI without an authority doesn't have a well defined meaning. This +library considers such URIs to be invalid when they have a path which does not +start with '/' (for example C). It is likely that any such URI +should really be a relative URI reference. If the path does start with a slash +then this library will attempt to 'repair' the URI by adding an empty authority +part, so that C will be changed automatically to +C. + +A host value of C is normalized to an empty host, so that +C will become C. An empty path is +normalized to '/'. + +The path part is always considered to be case sensitive, so no case folding +is done even when converting to a filesystem path for Windows. + +Query parts and fragments are left alone by this library, but are not used +in converting URIs to filesystem paths. + +=head1 Converting between URIs and filesystem paths + +A C object can be converted into an absolute path suitable for +use on a particular operating system by calling the C +method: + +=for syntax-highlight lua + + local uri = assert(URI:new("file:///foo/bar")) + print(uri:filesystem_path("unix")) -- /foo/bar + print(uri:filesystem_path("win32")) -- \foo\bar + +This method will throw an exception if the path cannot be converted. +For example, a file URI containing a host name cannot be represented on +a Unix filesystem, but on a Win32 system it will be converted to a UNC path: + +=for syntax-highlight lua + + local uri = assert(URI:new("file://server/path")) + print(uri:filesystem_path("unix")) -- error + print(uri:filesystem_path("win32")) -- \\server\path + +To convert a filesystem path into a URI, call the class method +C: + +=for syntax-highlight lua + + local FileURI = require "uri.file" + local uri = FileURI.make_file_uri("/foo/bar", "unix") + print(uri) -- file:///foo/bar + uri = FileURI.make_file_uri("C:\foo\bar", "win32") + print(uri) -- file:///C:/foo/bar + +To convert a relative URI reference (a L +object) into a filesystem path you should first resolve it against an +appropriate C URI, and then call the C method on that. + +=head1 Methods + +All the methods defined in L are supported. The C, +and C methods will always return nil, and will throw an +exception when passed anything other than nil. The C method will +normalize C to an empty host name, and will throw an exception if +given a new value of nil. The C method will normalize an empty path +or nil value to '/'. + +In addition to the standard methods, file URIs support the C +method, and the C class contains the C function, +both of which are described above. + +=head1 Operating systems supported + +The conversion between a file URI and a path suitable for use on a particular +operating system are defined in additional classes, which are loaded +automatically based on the operating system name supplied to the two conversion +functions. For example, passing the string C to the functions will +invoke the implementation in the class C. An exception will be +thrown if no class exists to support a given operating system. The following +operating system classes are provided: + +=over + +=item C + +A URI containing a host name will cause an exception to be thrown, as there +is no obvious way for these to be represented in Unix paths. If the path +contains an encoded null byte (C<%00>) or encoded slash (C<%2F>) then an +exception will be thrown. + +Attempting to convert a relative path to a URI will cause an exception. + +=item C + +Forward slashes ('/') in URIs will be converted to backslashes ('\') in +paths, and vice versa. + +URIs containing host names will be converted to UNC paths, starting with +a '\\' followed by the hostname and then the path part. If the path part +of a URI appears to begin with a drive letter, then the first slash will +be removed so that the resulting path starts with the letter. Encoded +pipe characters ('%7C') will be recognized as equivalent to colons for the +purpose of identifying drive letters, since they have been historically +used in that way, but I believe they are not allowed to occur in the path +unencoded in a URI nowadays. + +=back + +The operating system names are case insensitive, and are folded to lowercase +before being converted into a Lua module name. + +Currently there is no way for this library to recognise the operating system it +is running on, since Lua has no built-in way of providing that information. + +=head1 References + +The most up to date IETF standard for the C URI scheme is still +L, but this does not specify exactly how to convert +between URIs and filesystem paths on particular platforms. It does however +specify the equivalence between 'localhost' and an empty host. + +The correct form of file URI to represent a Windows filesystem path is +described in a blog article: +L + +There is a standard of sorts describing the conversion between Unix paths +and file URIs: +L + +=for comment +vi:ts=4 sw=4 expandtab diff --git a/doc/lua-uri-ftp.pod b/doc/lua-uri-ftp.pod new file mode 100644 index 0000000..c40208e --- /dev/null +++ b/doc/lua-uri-ftp.pod @@ -0,0 +1,45 @@ +=head1 Name + +lua-uri-ftp - FTP URI support for Lua URI library + +=head1 Description + +The class C is used for URIs with the C scheme. It inherits from +the L class. + +FTP URIs with a missing authority part or an empty host part are considered +to be invalid. An empty path is always normalized to '/'. The default port +S. + +=head1 Methods + +All the methods defined in L and L are +supported, in addition to the following: + +=over + +=item uri:ftp_typecode(...) + +Mutator for the 'type' parameter at the end of the path. If the optional +argument is supplied then a new type is set, replacing the existing one, or +causing the type parameter to be added to the path if it isn't there already. + +=for syntax-highlight lua + + local uri = assert(URI:new("ftp://host/path")) + uri:ftp_typecode("a") -- ftp://host/path;type=a + uri:ftp_typecode(nil) -- ftp://host/path + +Passing in an empty string has the same effect as nil, removing the parameter. +An empty type parameter will be returned as nil, the same as if the parameter +was missing. + +=back + +=head1 References + +This class is based on L. Unfortunately there isn't +currently an RFC for FTP URIs based on the more up to date L. + +=for comment +vi:ts=4 sw=4 expandtab diff --git a/doc/lua-uri-http.pod b/doc/lua-uri-http.pod new file mode 100644 index 0000000..3c3ea05 --- /dev/null +++ b/doc/lua-uri-http.pod @@ -0,0 +1,28 @@ +=head1 Name + +lua-uri-http - HTTP URI support for Lua URI library + +=head1 Description + +The classes C and C are used for URIs with the C and +C schemes respectively. C inherits from the generic +L class, and C inherits from C. + +An HTTP or HTTPS URI containing any userinfo part is considered to be +invalid. An empty path is normalized to '/', since browsers usually do +that, and an empty path cannot be used in an HTTP GET request. + +The default port for the C scheme S, and for C +S. + +There are no extra methods defined for telnet URIs, only those described in +L. + +=head1 References + +As far as I can tell there is no up to date specification of the syntax of +HTTP URIs, so this class is based on L and +L. + +=for comment +vi:ts=4 sw=4 expandtab diff --git a/doc/lua-uri-pop.pod b/doc/lua-uri-pop.pod new file mode 100644 index 0000000..c760f16 --- /dev/null +++ b/doc/lua-uri-pop.pod @@ -0,0 +1,63 @@ +=head1 Name + +lua-uri-pop - POP URI support for Lua URI library + +=head1 Description + +The class C is used for URIs with the C scheme. It inherits +from the L class. + +POP URIs with a non-empty path part are considered invalid. There are also +invalid if there is a userinfo part which has an empty POP username or an +empty POP 'auth' type. + +The ';auth=' part of a POP URI is normalized so that the word 'auth' is in +lowercase, and if the auth type is the default '*' value then it is removed +altogether, leaving just the username. + +The default port S. + +=head1 Methods + +All the methods defined in L are supported. The C +method will throw an exception if the new userinfo would form an invalid +POP URI, and will normalize the auth type part if appropriate. The C +method will always return empty strings for POP URIs, and will throw an +exception if given a new value which is not the empty string. + +The following additional methods are provided: + +=over + +=item uri:pop_user(...) + +Get or set the POP username, which is stored in the userinfo part of the +authority. + +The return value will be nil if there is no user information in the URI, +or a fully decoded string if there is. + +If the new value is empty, and exception is thrown. The user can be set to +nil to remove the userinfo part from the URI, but this will also throw an +exception if there is an 'auth' type specified. + +=item uri:pop_auth(...) + +Get or set the POP authentication type, which is stored in the userinfo +part of the authority after the string ';auth='. + +The value returned is just the auth type, not the ';auth=' part, and will be +fully decoded. The value returned will never be nil. If there is no auth type +specified then the default value of '*' will be returned. + +Setting a new value of nil or the empty string will cause an exception +to be thrown. + +=back + +=head1 References + +This class is based on L. + +=for comment +vi:ts=4 sw=4 expandtab diff --git a/doc/lua-uri-rtsp.pod b/doc/lua-uri-rtsp.pod new file mode 100644 index 0000000..3136baf --- /dev/null +++ b/doc/lua-uri-rtsp.pod @@ -0,0 +1,24 @@ +=head1 Name + +lua-uri-rtsp - RTSP URI support for Lua URI library + +=head1 Description + +The classes C and C are used for URIs with the C and +C schemes respectively. C inherits from the +L class, and C inherits from C. + +There is no special validation or normalization applied to these URIs beyond +that done for HTTP URIs. + +The default port for both schemes S. + +There are no extra methods defined for telnet URIs, only those described in +L. + +=head1 References + +This class is based on L. + +=for comment +vi:ts=4 sw=4 expandtab diff --git a/doc/lua-uri-telnet.pod b/doc/lua-uri-telnet.pod new file mode 100644 index 0000000..0944a4c --- /dev/null +++ b/doc/lua-uri-telnet.pod @@ -0,0 +1,25 @@ +=head1 Name + +lua-uri-telnet - Telnet URI support for Lua URI library + +=head1 Description + +The class C is used for URIs with the C scheme. It +inherits from the L class. + +Telnet URIs are not allowed to have any information in their path part, +because there isn't any specification defining what it would mean. An empty +path or a path of '/' is acceptable, and normalized to '/'. Any other path +is considered invalid. + +The default port for telnet URIs S. + +There are no extra methods defined for telnet URIs, only those described in +L and L. + +=head1 References + +This class is based on L. + +=for comment +vi:ts=4 sw=4 expandtab diff --git a/doc/lua-uri-urn-isbn.pod b/doc/lua-uri-urn-isbn.pod new file mode 100644 index 0000000..58ad886 --- /dev/null +++ b/doc/lua-uri-urn-isbn.pod @@ -0,0 +1,52 @@ +=head1 Name + +lua-uri-urn-isbn - ISBN URN support for Lua URI library + +=head1 Description + +The class C is used for URNs with the NID 'isbn', that is, URIs +which begin C. It inherits from the L +class. + +Some of the functionality of this class depends on the L module +being installed, although it can be used without that. In particular, if the +module is installed then full checksum validation of the ISBN is performed, +whereas without it the ISBN is only checked for invalid characters. The ISBN +value is normalized to include hyphens in the conventional places if the +lua-isbn module is installed (the exact hyphen positions depend on the number), +but without it all hyphens are removed instead. If the ISBN ends in a checksum +of 'x', then it folded to uppercase. + +=head1 Methods + +All the methods defined in L and L are supported, +as well as the following: + +=over + +=item uri:isbn(...) + +Get or set the ISBN value as an object of the type provided by the C +class in the L library. This method will throw an exception +if this library is not installed, or if the object supplied is not a valid +ISBN object (it will currently accept a string, but you shouldn't rely on +this). + +=item uri:isbn_digits(...) + +Get or set the ISBN value as a string containing just the numbers (and +possibly an 'X' as the last digit). There will be no hyphens in this value, +and it should be exactly 10 or 13 characters long. + +If a new value is provided then it must not be nil, and will be validated in +the normal way, causing an exception if it is invalid. + +=back + +=head1 References + +This implements the 'isbn' NID defined in L, and is consistent +with the same NID suggested in L. + +=for comment +vi:ts=4 sw=4 expandtab diff --git a/doc/lua-uri-urn-issn.pod b/doc/lua-uri-urn-issn.pod new file mode 100644 index 0000000..b781a47 --- /dev/null +++ b/doc/lua-uri-urn-issn.pod @@ -0,0 +1,41 @@ +=head1 Name + +lua-uri-urn-issn - ISSN URN support for Lua URI library + +=head1 Description + +The class C is used for URNs with the NID 'issn', that is, URIs +which begin C. It inherits from the L +class. + +The URI is considered invalid if it doesn't have 8 digits, if there is +anything extra in the NSS other than the digits and optional single hyphen, +or if the checksum digit is wrong. + +As specified, the check digit is canonicalized to uppercase. The canonical +form has a single hyphen in the middle of the digits. + +=head1 Methods + +All the methods defined in L and L as supported, as +well as the following: + +=over + +=item uri:issn_digits(...) + +Get or set the ISSN value as a string containing just the numbers. There +will be no hyphens in this value, and it should be exactly 8 characters long. + +If a new value is provided then it must not be nil, and will be validated in +the normal way, causing an exception if it is invalid. + +=back + +=head1 References + +This implements the 'issn' NID defined in L, and is consistent +with the same NID suggested in L. + +=for comment +vi:ts=4 sw=4 expandtab diff --git a/doc/lua-uri-urn-oid.pod b/doc/lua-uri-urn-oid.pod new file mode 100644 index 0000000..f0f472e --- /dev/null +++ b/doc/lua-uri-urn-oid.pod @@ -0,0 +1,51 @@ +=head1 Name + +lua-uri-urn-oid - OID URN support for Lua URI library + +=head1 Description + +The class C is used for URNs with the NID 'oid', that is, URIs +which begin C. It inherits from the L +class. + +The URI is considered invalid if its NSS doesn't consist only of non-negative +integers separated by full stop characters. Numbers with leading zeroes +are not allowed (although the number '0' on its own is). There must be at +least one number. + +There is no normalization performed beyound that performed by the C +class. + +=head1 Methods + +All the methods defined in L and L as supported, as +well as the following: + +=over + +=item uri:oid_numbers(...) + +Get or set the OID as an array of Lua number values. + +If a new value is provided then it must be a table containing an array of +at least one number. All the values in the table must be non-negative +numbers. Non-integer numbers are rounded down to an integer value. Strings +containing only decimal digits are also allowed. + +=for syntax-highlight lua + + local uri = assert(URI:new("urn:oid:1.0.23")) + local nums = uri:oid_numbers() + for i, v in ipairs(nums) do print(i, v) end + + uri:oid_numbers({ 5, 4, 3, 2, 1 }) + print(uri) -- urn:oid:5.4.3.2.1 + +=back + +=head1 References + +This implements the 'oid' NID defined in L. + +=for comment +vi:ts=4 sw=4 expandtab diff --git a/doc/lua-uri-urn.pod b/doc/lua-uri-urn.pod new file mode 100644 index 0000000..d1d83bb --- /dev/null +++ b/doc/lua-uri-urn.pod @@ -0,0 +1,72 @@ +=head1 Name + +lua-uri-urn - URN support for Lua URI library + +=head1 Description + +The class C is used for URNs, that is, URIs with the C scheme. +It inherits from the L class. + +Any URN containing an authority part or query part is considered to be invalid, +as is one which does not have a valid NID. URNs must be of the form +C, where the NSS part has a syntax specific to the NID. The +scheme and NID part are both normalized to lowercase. Some NIDs have +subclasses which enforce further syntax constraints, do NID-specific +normalization, or provide additional methods. + +=head1 Methods + +All the methods defined in L are supported. The C, +C, C, and C methods will always return nil, and will throw +an exception when passed anything other than nil. The C method will +throw an exception if given a new path which is nil or not valid for the C +scheme. + +The following additional methods are supported: + +=over + +=item uri:nid(...) + +Get or set the NID (Namespace Identifier) of the URN (the part of the path +before the first colon). If a new value is supplied then the URI's path will +be changed to have the new NID but with the same NSS value. + +An exception will be thrown if the new NID is invalid, or if the existing +NSS value is invalid in the context of the new NID. Note that the value +'urn' is an invalid NID. + +This can cause the class of the URI object to change, if a different class +is appropriate for the new NID. + +=item uri:nss(...) + +Get or set the NSS (Namespace Specific String) part of the URN (the part of the +path after the first colon). If a new value is supplied then the URI's path +will be changed to use the new NSS, but the NID will be unchanged. + +This will throw an exception if the new value is invalid for the current NID. + +=back + +=head1 Subclasses + +The following subclasses are used for URNs with certain NIDs. URNs with +other NIDs just use the generic C class. + +=over + +=item L + +=item L + +=item L + +=back + +=head1 References + +This class is based on L. + +=for comment +vi:ts=4 sw=4 expandtab diff --git a/doc/lua-uri.pod b/doc/lua-uri.pod new file mode 100644 index 0000000..9209159 --- /dev/null +++ b/doc/lua-uri.pod @@ -0,0 +1,463 @@ +=head1 Name + +lua-uri - Lua module for manipulating URIs + +=head1 Loading the module + +The URI module doesn't alter any global variables when it loads, so you can +decide what name you want to use to access it. You will probably want to +load it like this: + +=for syntax-highlight lua + + local URI = require "uri" + +You can use a variable called something other than C if you'd like, +or you could assign the table returned by C to a global variable. +In this documentation we'll assume you're using a variable called C. + +=head1 Parsing, validating and normalizing URIs + +When you create a URI object, the string you supply is checked to make sure +it conforms to the appropriate standards. +If everything is OK, the new object will be returned, otherwise nil +and an error message will be returned. You can convert any errors into +Lua exceptions using the C function. + +=for syntax-highlight lua + + local URI = require "URI" + + local uri = assert(URI:new("http://example.com/foo")) + + -- In this case, these will print the original string. + -- They are both the same. + print(tostring(uri)) + print(uri:uri()) + +You can extract individual parts of the URI with various accessor methods: + +=for syntax-highlight lua + + print(uri:scheme()) -- http + print(uri:host()) -- example.com + print(uri:path()) -- /foo + +Some URIs will be 'normalized' automatically to produce an equivalent +canonical version. Nothing will be changed which would affect how the +URI will be interpreted. For example: + +=for syntax-highlight lua + + local uri = assert(URI:new("HTTP://EXAMPLE.COM:80/FOO")) + print(tostring(uri)) -- http://example.com/FOO + +In this case the scheme and hostname were both converted to lowercase +(but not the path part, because that's case sensitive). The port number +was also removed because S is the default anyway for HTTP URIs. + +If you just want to make sure a URI is correct, but without throwing an +exception, use code like this: + +=for syntax-highlight lua + + local uri, err = URI:new(uri_to_test) + + if uri then + print("valid, normalized to " .. tostring(uri)) + else + print("invalid, error message is " .. err) + end + +(Note that many invalid URIs will get processed as relative URI references, +so if you're expecting an absolute URI it's also a good idea to check that +the C method returns false.) + +=head1 Cloning URIs + +To make a copy of a URI object, pass it to the constructor: + +=for syntax-highlight lua + + local original = URI:new("http://www/foo") + local copy = URI:new(original) + +The two objects will contain the same information, but can be changed +independently. + +=head1 Relative URIs + +A relative URI reference is not a complete URI. It doesn't have a scheme, +so it doesn't really mean anything until it is resolved against an absolute +URI. For this reason, when you create a URI object from a relative URI, +it will belong to the special class C. There is very little +you can do with a relative URI object other than get and set its path, query +string, and fragment identifier. + +Relative URI objects can be created in the same way as absolute ones: + +=for syntax-highlight lua + + local uri = assert(URI:new("../path?query#fragment")) + print(uri:is_relative()) -- true + print(uri._NAME) -- uri._relative + +There are two ways to resolve a relative URI reference against an absolute +URI to get another absolute URI. One is to create a new URI object, passing +the base URI as a second argument to the constructor: + +=for syntax-highlight lua + + local rel = assert(URI:new("../quux.html")) + local base = assert(URI:new("http://example.com/foo/bar/")) + local abs = assert(URI:new(rel, base)) + print(tostring(abs)) -- http://example.com/foo/quux.html + +You can also do this by passing strings to C, instead of objects: + +=for syntax-highlight lua + + local abs = assert(URI:new("../quux.html", + "http://example.com/foo/bar/")) + print(tostring(abs)) -- http://example.com/foo/quux.html + +Alternatively, a URI object containing a relative URI can be made absolute +without creating a new object using the C method: + +=for syntax-highlight lua + + local uri = assert(URI:new("../quux.html")) + local base = assert(URI:new("http://example.com/foo/bar/")) + uri:resolve(base) + print(tostring(uri)) -- http://example.com/foo/quux.html + +The reverse process can be carried out with the C method, +creating a relative URI from an absolute one, where the relative URI +can be later resolved against a particular base URI: + +=for syntax-highlight lua + + local uri = assert(URI:new("http://example.com/foo/quux.html")) + local base = assert(URI:new("http://example.com/foo/bar/")) + uri:relativize(base) + print(tostring(uri)) -- ../quux.html + +It is possible for a relative URI to have an authority part, although this +is very rare in practice. It is unlikely that you'll ever need to do this, +but you can create a URI like this: + +=for syntax-highlight lua + + local uri = assert(URI:new("//example.com/path")) + +=head1 Methods + +This is a complete list of the methods you can call on a generic C +object once created by calling C. Some URIs are created in more +specific classes (listed in the I section), which may have +additional methods. Arguments shown in square brackets below are optional. + +Note that all the accessor methods, like C and C, can be used just +to return the current value (if they are called without an argument), or can +set a new value while returning the old value. Passing nil as the argument is +generally different from not passing an argument at all, or to passing an +empty string. + +=over + +=item uri:default_port() + +Returns the default port used for this type of URI when no port number is +supplied in the authority part. This will be nil if the standard for the +URI's current scheme doesn't specify a default port, or if the scheme is +one which this library doesn't have any special understanding of. + +=for syntax-highlight lua + + local uri = assert(URI:new("http://example.com:123/")) + print(uri:default_port()) -- 80 + +=item uri:eq(other) + +Returns true if the two URI objects contain the same URI. C can also +be a string, which will be converted to a URI object (in order for the +normalization to be done). + +This can also be called as a stand-alone function if you don't know whether +either URI is an object or a string. For example: + +=for syntax-highlight lua + + print(URI.eq("http://example.com", + "HTTP://EXAMPLE.COM/")) + +If either value is a string which isn't a valid URI, this will throw an +exception. It will however accept relative URIs, and they will be compared +as normal. A relative URI is never equal to an absolute one. + +There is no less-than comparison function, as URIs don't have any particular +ordering. If you want to sort URI objects you're best bet is probably just +to compare the string versions: + +=for syntax-highlight lua + + function urisort (a, b) + return a:uri() < b:uri() + end + + table.sort(t, urisort) + +=item uri:fragment([newvalue]) + +Returns the current fragment part of the URI (the part after the C<#> +character), or nil if the URI has no fragment part. Note that an empty +fragment (zero characters long) is different from one which is completely +missing. + +If C is supplied, changes the fragment to the new value, percent +encoding any characters which would not be valid in a fragment part. Any +percent encoding already done on the string will be left in place (not double +encoded). If C is nil then any existing fragment will be removed. + +The syntax of fragments are meaningful only for particular media types +of resources, so there is no special behaviour for different URI schemes. + +=item uri:host([newvalue]) + +Get and set the host part of the authority in a URI. This can be a domain +name, an IPv4 address (four numbers separated by dots), or an IPv6 address +(which must include the enclosing square brackets used in URIs). + +When setting a new host, the value is normalized to lowercase. An invalid +value will cause an exception to be thrown. The value can be an empty string +to indicate the default host. + +Setting the value to nil will cause the host to be removed altogether, +leaving the URI with no authority component. This will throw an exception +if there is a userinfo or port component in the URI, because it is impossible +to represent a URI with no host when there is an authority component. + +Some URI schemes may throw an exception when setting the host to nil or the +empty string, and others when setting it to anything other than nil, if those +schemes require or disallow authority components. + +=item uri:init() + +This method is called internally to make a URI object belong to the right +class and do any scheme-specific validation an normalization. It is only +of interest if you want to write a new C subclass for particular types +of URIs. + +The implementation in the C class itself changes the class of the object +to the one appropriate to the scheme (if there is a more specific class +available). It also removes the port number from the authority component if +it is unnecessary because the scheme defines it as the default port. Finally, +if there is a more specific class available it calls the C method in +that. + +C is called after the URI has been split into components according to +the generic syntax, so it can use the accessor methods to get at them. +It should return the same values as C, either the new URI object (the +object it was called on), or nil and an error message. + +=item uri:is_relative() + +Returns true if this is a relative URI reference, false otherwise. All +relative URIs belong to the class C. All the other URI +classes are for absolute URIs. + +=item uri:path([newvalue]) + +Get or set the path component of the URI. Throws an exception if the new +value is not valid in the context of the rest of the URI. + +=for syntax-highlight lua + + local uri = assert(URI:new("http://example.com/foo")) + local old = uri:path("/bar/") + print(old) -- /foo + print(uri:path()) -- /bar/ + +When a new path value is supplied, it can already be percent encoded, but +any characters which aren't allowed are encoded as well. Percent characters +are not encoded themselves, because they are assumed to be part of the existing +encoding. The existing percent encoding is normalized, and any invalid +encoding will cause an exception. + +There are certain paths which cannot be expressed in the URI syntax. A path +which does not start with a C character (unless it's completely empty) +cannot be represented when there is an authority component, so this will +cause an exception to be thrown. A path which starts with C when there +is no authority component would be misinterpreted, so the second slash is +percent encoded. + +Some URI schemes may impose further restrictions on what is allowed in a +path, so other path values may cause exceptions in certain cases. + +=item uri:port([newvalue]) + +Get or set the port number in a URI. The value returned is always an +integer number or nil. + +If C is supplied it should be a non-negative integer number, or +a string containing only digits, or nil to remove any existing port number. +An exception is thrown if it is an invalid value, or if the URI scheme +doesn't allow port numbers to be specified. If there is currently no +authority part in the URI, then an empty host will be added to create one. + +If the port number is the default for a URI scheme (the same as the number +returned from the C method), then the C method will +return that number, but the number won't actually be shown in the URI when +it is represented as a string, because it would be redundant. Setting the +port number to nil has the same effect as setting it to the default port +number. + +=item uri:query([newvalue]) + +Get or set the query part of a URI. + +If C is supplied it should be the new string, or nil to remove +any existing query part. The query part can be an empty string, which is +different from it not being present at all (the C character will still +be included to indicate that there is a query part, even if it is not +followed by anything else). Any characters which would not be valid in +a query part will be percent encoded, but any percent encoding already done +on the string will be left in place (not double encoded). + +The base-class implementation of this method never throws exceptions, but +some scheme-specific classes may throw exceptions if they impose constraints +on the syntax of query parts. + +=item uri:resolve(base) + +Given an object representing a relative URI, resolve it against the base +URI C (which can be a URI object or string) and update the C +object to contain an absolute URI. + +Has no effect if C is already an absolute URI. Throws an exception +if C is not an absolute URI, or if the new URI formed by combining +them would be invalid for the given scheme. + +See also the section I and the C method. + +=item uri:scheme([newvalue]) + +Get and set the scheme of the URI. Altering the scheme of an existing URI +is very unlikely to be useful. + +Throws an exception if C is nil or not a valid scheme, or if the +rest of the URI is not valid when interpreted with the new scheme. +After calling this method the class of the object may have been changed, +if the old class is not appropriate for the new value. + +=item uri:relativize(base) + +If possible, update the absolute URI C to contain a relative URI +which, when resolved again against C, will yield the original URI +value. This doesn't return anything, just modifies the object. + +Has no effect if C is already relative, or if there is no way to create +an appropriate relative URI (so the URI will remain absolute for example if +C has a different scheme from C). Throws an exception if C +is not absolute. + +This method will never result in a network-path reference (a relative URI +which includes an authority part). In cases where that would be possible +the value in C will be left as an absolute URI, which is less likely +to cause problems. + +See also the section I and the C method. + +=item uri:uri([newvalue]) + +Returns the URI value as a string. The return value is the same as you'll +get from C. + +If an argument is supplied, this replaces the URI in the C object with +a different one. C must be a complete new URI or relative URI +reference in a string, or a URI object. + +This is equivalent to creating a new URI object by calling C, +except that instead of creating a new object the existing object is updated +with the new information. It is also not possible to pass a base URI to +the C method. + +Throws an exception if C is nil or if there is any error in parsing +the new URI string. After calling this method the class of the object may +have been changed, if the old class is not appropriate for the new value. + +=item uri:userinfo([newvalue]) + +Get or set the userinfo part of the URI. If C is supplied then +it is expected to be percent encoded already. Percent encoding is normalized. +An exception will be thrown if the new value is invalid, or if the URI scheme +does not allow a userinfo part (for example if it is an HTTP URI). If there +is currently no authority part in the URI, then an empty host will be added +to create one. + +If C is nil then any existing userinfo part is removed. + +=back + +=head1 URI schemes + +The following Lua modules provide classes which implement extra validation +and normalization, or provide extra methods, for URIs which specific schemes: + +=over + +=item L + +=item L + +=item L + +=item L and uri.https + +=item L + +=item L and uri.rtspu + +=item L + +=item L + +=back + +=head1 Other modules + +Other Lua modules provide additional functionality used in the library, +or act as base classes for the scheme-specific classes: + +=over + +=item L + +Baseclass for URI schemes which use a username and password in their userinfo +part, separated by a colon (for example FTP). + +=item L + +Utility functions used by the rest of the library. Contains useful +C and C functions which might be useful elsewhere. + +=back + +=head1 References + +The parsing of URI syntax is based primarily on L. + +=head1 Copyright + +This software and documentation is Copyright E 2007 Geoff Richards +Egeoff@geoffrichards.co.ukE. It is free software; you can redistribute it +and/or modify it under the terms of the S license. The full terms +are given in the file F supplied with the source code package, +and are also available here: L + +An older unreleased version of this library was created as a direct port +of the Perl URI library, by Gisle Aas and others. It has since been +rewritten with a somewhat different design. + +=for comment +vi:ts=4 sw=4 expandtab diff --git a/lunit-console.lua b/lunit-console.lua new file mode 100644 index 0000000..90c5165 --- /dev/null +++ b/lunit-console.lua @@ -0,0 +1,141 @@ + +--[[-------------------------------------------------------------------------- + + This file is part of lunit 0.5. + + For Details about lunit look at: http://www.mroth.net/lunit/ + + Author: Michael Roth + + Copyright (c) 2006-2008 Michael Roth + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +--]]-------------------------------------------------------------------------- + + + +--[[ + + begin() + run(testcasename, testname) + err(fullname, message, traceback) + fail(fullname, where, message, usermessage) + pass(testcasename, testname) + done() + + Fullname: + testcase.testname + testcase.testname:setupname + testcase.testname:teardownname + +--]] + + +require "lunit" + +module( "lunit-console", package.seeall ) + + +local function printformat(format, ...) + io.write( string.format(format, ...) ) +end + + +local columns_printed = 0 + +local function writestatus(char) + if columns_printed == 0 then + io.write(" ") + end + if columns_printed == 60 then + io.write("\n ") + columns_printed = 0 + end + io.write(char) + io.flush() + columns_printed = columns_printed + 1 +end + + +local msgs = {} + + +function begin() + local total_tc = 0 + local total_tests = 0 + + for tcname in lunit.testcases() do + total_tc = total_tc + 1 + for testname, test in lunit.tests(tcname) do + total_tests = total_tests + 1 + end + end + + printformat("Loaded testsuite with %d tests in %d testcases.\n\n", total_tests, total_tc) +end + + +function run(testcasename, testname) + -- NOP +end + + +function err(fullname, message, traceback) + writestatus("E") + msgs[#msgs+1] = "Error! ("..fullname.."):\n"..message.."\n\t"..table.concat(traceback, "\n\t") .. "\n" +end + + +function fail(fullname, where, message, usermessage) + writestatus("F") + local text = "Failure ("..fullname.."):\n".. + where..": "..message.."\n" + + if usermessage then + text = text .. where..": "..usermessage.."\n" + end + + msgs[#msgs+1] = text +end + + +function pass(testcasename, testname) + writestatus(".") +end + + + +function done() + printformat("\n\n%d Assertions checked.\n", lunit.stats.assertions ) + print() + + for i, msg in ipairs(msgs) do + printformat( "%3d) %s\n", i, msg ) + end + + printformat("Testsuite finished (%d passed, %d failed, %d errors).\n", + lunit.stats.passed, lunit.stats.failed, lunit.stats.errors ) +end + + + + + diff --git a/lunit.lua b/lunit.lua new file mode 100644 index 0000000..52d4588 --- /dev/null +++ b/lunit.lua @@ -0,0 +1,670 @@ + +--[[-------------------------------------------------------------------------- + + This file is part of lunit 0.5. + + For Details about lunit look at: http://www.mroth.net/lunit/ + + Author: Michael Roth + + Copyright (c) 2004, 2006-2009 Michael Roth + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +--]]-------------------------------------------------------------------------- + + + + +local orig_assert = assert + +local pairs = pairs +local ipairs = ipairs +local next = next +local type = type +local error = error +local tostring = tostring + +local string_sub = string.sub +local string_format = string.format + + +module("lunit", package.seeall) -- FIXME: Remove package.seeall + +local lunit = _M + +local __failure__ = {} -- Type tag for failed assertions + +local typenames = { "nil", "boolean", "number", "string", "table", "function", "thread", "userdata" } + + + +local traceback_hide -- Traceback function which hides lunit internals +local mypcall -- Protected call to a function with own traceback +do + local _tb_hide = setmetatable( {}, {__mode="k"} ) + + function traceback_hide(func) + _tb_hide[func] = true + end + + local function my_traceback(errobj) + if is_table(errobj) and errobj.type == __failure__ then + local info = debug.getinfo(5, "Sl") -- FIXME: Hardcoded integers are bad... + errobj.where = string_format( "%s:%d", info.short_src, info.currentline) + else + errobj = { msg = tostring(errobj) } + errobj.tb = {} + local i = 2 + while true do + local info = debug.getinfo(i, "Snlf") + if not is_table(info) then + break + end + if not _tb_hide[info.func] then + local line = {} -- Ripped from ldblib.c... + line[#line+1] = string_format("%s:", info.short_src) + if info.currentline > 0 then + line[#line+1] = string_format("%d:", info.currentline) + end + if info.namewhat ~= "" then + line[#line+1] = string_format(" in function '%s'", info.name) + else + if info.what == "main" then + line[#line+1] = " in main chunk" + elseif info.what == "C" or info.what == "tail" then + line[#line+1] = " ?" + else + line[#line+1] = string_format(" in function <%s:%d>", info.short_src, info.linedefined) + end + end + errobj.tb[#errobj.tb+1] = table.concat(line) + end + i = i + 1 + end + end + return errobj + end + + function mypcall(func) + orig_assert( is_function(func) ) + local ok, errobj = xpcall(func, my_traceback) + if not ok then + return errobj + end + end + traceback_hide(mypcall) +end + + +-- Type check functions + +for _, typename in ipairs(typenames) do + lunit["is_"..typename] = function(x) + return type(x) == typename + end +end + +local is_nil = is_nil +local is_boolean = is_boolean +local is_number = is_number +local is_string = is_string +local is_table = is_table +local is_function = is_function +local is_thread = is_thread +local is_userdata = is_userdata + + +local function failure(name, usermsg, defaultmsg, ...) + local errobj = { + type = __failure__, + name = name, + msg = string_format(defaultmsg,...), + usermsg = usermsg + } + error(errobj, 0) +end +traceback_hide( failure ) + + +local function format_arg(arg) + local argtype = type(arg) + if argtype == "string" then + return "'"..arg.."'" + elseif argtype == "number" or argtype == "boolean" or argtype == "nil" then + return tostring(arg) + else + return "["..tostring(arg).."]" + end +end + + +function fail(msg) + stats.assertions = stats.assertions + 1 + failure( "fail", msg, "failure" ) +end +traceback_hide( fail ) + + +function assert(assertion, msg) + stats.assertions = stats.assertions + 1 + if not assertion then + failure( "assert", msg, "assertion failed" ) + end + return assertion +end +traceback_hide( assert ) + + +function assert_true(actual, msg) + stats.assertions = stats.assertions + 1 + local actualtype = type(actual) + if actualtype ~= "boolean" then + failure( "assert_true", msg, "true expected but was a "..actualtype ) + end + if actual ~= true then + failure( "assert_true", msg, "true expected but was false" ) + end + return actual +end +traceback_hide( assert_true ) + + +function assert_false(actual, msg) + stats.assertions = stats.assertions + 1 + local actualtype = type(actual) + if actualtype ~= "boolean" then + failure( "assert_false", msg, "false expected but was a "..actualtype ) + end + if actual ~= false then + failure( "assert_false", msg, "false expected but was true" ) + end + return actual +end +traceback_hide( assert_false ) + + +function assert_equal(expected, actual, msg) + stats.assertions = stats.assertions + 1 + if expected ~= actual then + failure( "assert_equal", msg, "expected %s but was %s", format_arg(expected), format_arg(actual) ) + end + return actual +end +traceback_hide( assert_equal ) + + +function assert_not_equal(unexpected, actual, msg) + stats.assertions = stats.assertions + 1 + if unexpected == actual then + failure( "assert_not_equal", msg, "%s not expected but was one", format_arg(unexpected) ) + end + return actual +end +traceback_hide( assert_not_equal ) + + +function assert_match(pattern, actual, msg) + stats.assertions = stats.assertions + 1 + local patterntype = type(pattern) + if patterntype ~= "string" then + failure( "assert_match", msg, "expected the pattern as a string but was a "..patterntype ) + end + local actualtype = type(actual) + if actualtype ~= "string" then + failure( "assert_match", msg, "expected a string to match pattern '%s' but was a %s", pattern, actualtype ) + end + if not string.find(actual, pattern) then + failure( "assert_match", msg, "expected '%s' to match pattern '%s' but doesn't", actual, pattern ) + end + return actual +end +traceback_hide( assert_match ) + + +function assert_not_match(pattern, actual, msg) + stats.assertions = stats.assertions + 1 + local patterntype = type(pattern) + if patterntype ~= "string" then + failure( "assert_not_match", msg, "expected the pattern as a string but was a "..patterntype ) + end + local actualtype = type(actual) + if actualtype ~= "string" then + failure( "assert_not_match", msg, "expected a string to not match pattern '%s' but was a %s", pattern, actualtype ) + end + if string.find(actual, pattern) then + failure( "assert_not_match", msg, "expected '%s' to not match pattern '%s' but it does", actual, pattern ) + end + return actual +end +traceback_hide( assert_not_match ) + + +function assert_error(msg, func) + stats.assertions = stats.assertions + 1 + if func == nil then + func, msg = msg, nil + end + local functype = type(func) + if functype ~= "function" then + failure( "assert_error", msg, "expected a function as last argument but was a "..functype ) + end + local ok, errmsg = pcall(func) + if ok then + failure( "assert_error", msg, "error expected but no error occurred" ) + end +end +traceback_hide( assert_error ) + + +function assert_error_match(msg, pattern, func) + stats.assertions = stats.assertions + 1 + if func == nil then + msg, pattern, func = nil, msg, pattern + end + local patterntype = type(pattern) + if patterntype ~= "string" then + failure( "assert_error_match", msg, "expected the pattern as a string but was a "..patterntype ) + end + local functype = type(func) + if functype ~= "function" then + failure( "assert_error_match", msg, "expected a function as last argument but was a "..functype ) + end + local ok, errmsg = pcall(func) + if ok then + failure( "assert_error_match", msg, "error expected but no error occurred" ) + end + local errmsgtype = type(errmsg) + if errmsgtype ~= "string" then + failure( "assert_error_match", msg, "error as string expected but was a "..errmsgtype ) + end + if not string.find(errmsg, pattern) then + failure( "assert_error_match", msg, "expected error '%s' to match pattern '%s' but doesn't", errmsg, pattern ) + end +end +traceback_hide( assert_error_match ) + + +function assert_pass(msg, func) + stats.assertions = stats.assertions + 1 + if func == nil then + func, msg = msg, nil + end + local functype = type(func) + if functype ~= "function" then + failure( "assert_pass", msg, "expected a function as last argument but was a %s", functype ) + end + local ok, errmsg = pcall(func) + if not ok then + failure( "assert_pass", msg, "no error expected but error was: '%s'", errmsg ) + end +end +traceback_hide( assert_pass ) + + +-- lunit.assert_typename functions + +for _, typename in ipairs(typenames) do + local assert_typename = "assert_"..typename + lunit[assert_typename] = function(actual, msg) + stats.assertions = stats.assertions + 1 + local actualtype = type(actual) + if actualtype ~= typename then + failure( assert_typename, msg, typename.." expected but was a "..actualtype ) + end + return actual + end + traceback_hide( lunit[assert_typename] ) +end + + +-- lunit.assert_not_typename functions + +for _, typename in ipairs(typenames) do + local assert_not_typename = "assert_not_"..typename + lunit[assert_not_typename] = function(actual, msg) + stats.assertions = stats.assertions + 1 + if type(actual) == typename then + failure( assert_not_typename, msg, typename.." not expected but was one" ) + end + end + traceback_hide( lunit[assert_not_typename] ) +end + + +function lunit.clearstats() + stats = { + assertions = 0; + passed = 0; + failed = 0; + errors = 0; + } +end + + +local report, reporterrobj +do + local testrunner + + function lunit.setrunner(newrunner) + if not ( is_table(newrunner) or is_nil(newrunner) ) then + return error("lunit.setrunner: Invalid argument", 0) + end + local oldrunner = testrunner + testrunner = newrunner + return oldrunner + end + + function lunit.loadrunner(name) + if not is_string(name) then + return error("lunit.loadrunner: Invalid argument", 0) + end + local ok, runner = pcall( require, name ) + if not ok then + return error("lunit.loadrunner: Can't load test runner: "..runner, 0) + end + return setrunner(runner) + end + + function report(event, ...) + local f = testrunner and testrunner[event] + if is_function(f) then + pcall(f, ...) + end + end + + function reporterrobj(context, tcname, testname, errobj) + local fullname = tcname .. "." .. testname + if context == "setup" then + fullname = fullname .. ":" .. setupname(tcname, testname) + elseif context == "teardown" then + fullname = fullname .. ":" .. teardownname(tcname, testname) + end + if errobj.type == __failure__ then + stats.failed = stats.failed + 1 + report("fail", fullname, errobj.where, errobj.msg, errobj.usermsg) + else + stats.errors = stats.errors + 1 + report("err", fullname, errobj.msg, errobj.tb) + end + end +end + + + +local function key_iter(t, k) + return (next(t,k)) +end + + +local testcase +do + -- Array with all registered testcases + local _testcases = {} + + -- Marks a module as a testcase. + -- Applied over a module from module("xyz", lunit.testcase). + function lunit.testcase(m) + orig_assert( is_table(m) ) + --orig_assert( m._M == m ) + orig_assert( is_string(m._NAME) ) + --orig_assert( is_string(m._PACKAGE) ) + + -- Register the module as a testcase + _testcases[m._NAME] = m + + -- Import lunit, fail, assert* and is_* function to the module/testcase + m.lunit = lunit + m.fail = lunit.fail + for funcname, func in pairs(lunit) do + if "assert" == string_sub(funcname, 1, 6) or "is_" == string_sub(funcname, 1, 3) then + m[funcname] = func + end + end + end + + -- Iterator (testcasename) over all Testcases + function lunit.testcases() + -- Make a copy of testcases to prevent confusing the iterator when + -- new testcase are defined + local _testcases2 = {} + for k,v in pairs(_testcases) do + _testcases2[k] = true + end + return key_iter, _testcases2, nil + end + + function testcase(tcname) + return _testcases[tcname] + end +end + + +do + -- Finds a function in a testcase case insensitive + local function findfuncname(tcname, name) + for key, value in pairs(testcase(tcname)) do + if is_string(key) and is_function(value) and string.lower(key) == name then + return key + end + end + end + + function lunit.setupname(tcname) + return findfuncname(tcname, "setup") + end + + function lunit.teardownname(tcname) + return findfuncname(tcname, "teardown") + end + + -- Iterator over all test names in a testcase. + -- Have to collect the names first in case one of the test + -- functions creates a new global and throws off the iteration. + function lunit.tests(tcname) + local testnames = {} + for key, value in pairs(testcase(tcname)) do + if is_string(key) and is_function(value) then + local lfn = string.lower(key) + if string.sub(lfn, 1, 4) == "test" or string.sub(lfn, -4) == "test" then + testnames[key] = true + end + end + end + return key_iter, testnames, nil + end +end + + + + +function lunit.runtest(tcname, testname) + orig_assert( is_string(tcname) ) + orig_assert( is_string(testname) ) + + local function callit(context, func) + if func then + local err = mypcall(func) + if err then + reporterrobj(context, tcname, testname, err) + return false + end + end + return true + end + traceback_hide(callit) + + report("run", tcname, testname) + + local tc = testcase(tcname) + local setup = tc[setupname(tcname)] + local test = tc[testname] + local teardown = tc[teardownname(tcname)] + + local setup_ok = callit( "setup", setup ) + local test_ok = setup_ok and callit( "test", test ) + local teardown_ok = setup_ok and callit( "teardown", teardown ) + + if setup_ok and test_ok and teardown_ok then + stats.passed = stats.passed + 1 + report("pass", tcname, testname) + end +end +traceback_hide(runtest) + + + +function lunit.run() + clearstats() + report("begin") + for testcasename in lunit.testcases() do + -- Run tests in the testcases + for testname in lunit.tests(testcasename) do + runtest(testcasename, testname) + end + end + report("done") + return stats +end +traceback_hide(run) + + +function lunit.loadonly() + clearstats() + report("begin") + report("done") + return stats +end + + + + + + + + + +local lunitpat2luapat +do + local conv = { + ["^"] = "%^", + ["$"] = "%$", + ["("] = "%(", + [")"] = "%)", + ["%"] = "%%", + ["."] = "%.", + ["["] = "%[", + ["]"] = "%]", + ["+"] = "%+", + ["-"] = "%-", + ["?"] = ".", + ["*"] = ".*" + } + function lunitpat2luapat(str) + return "^" .. string.gsub(str, "%W", conv) .. "$" + end +end + + + +local function in_patternmap(map, name) + if map[name] == true then + return true + else + for _, pat in ipairs(map) do + if string.find(name, pat) then + return true + end + end + end + return false +end + + + + + + + + +-- Called from 'lunit' shell script. + +function main(argv) + argv = argv or {} + + -- FIXME: Error handling and error messages aren't nice. + + local function checkarg(optname, arg) + if not is_string(arg) then + return error("lunit.main: option "..optname..": argument missing.", 0) + end + end + + local function loadtestcase(filename) + if not is_string(filename) then + return error("lunit.main: invalid argument") + end + local chunk, err = loadfile(filename) + if err then + return error(err) + else + chunk() + end + end + + local testpatterns = nil + local doloadonly = false + local runner = nil + + local i = 0 + while i < #argv do + i = i + 1 + local arg = argv[i] + if arg == "--loadonly" then + doloadonly = true + elseif arg == "--runner" or arg == "-r" then + local optname = arg; i = i + 1; arg = argv[i] + checkarg(optname, arg) + runner = arg + elseif arg == "--test" or arg == "-t" then + local optname = arg; i = i + 1; arg = argv[i] + checkarg(optname, arg) + testpatterns = testpatterns or {} + testpatterns[#testpatterns+1] = arg + elseif arg == "--" then + while i < #argv do + i = i + 1; arg = argv[i] + loadtestcase(arg) + end + else + loadtestcase(arg) + end + end + + loadrunner(runner or "lunit-console") + + if doloadonly then + return loadonly() + else + return run(testpatterns) + end +end + +clearstats() diff --git a/test/_generic.lua b/test/_generic.lua new file mode 100644 index 0000000..213ce4d --- /dev/null +++ b/test/_generic.lua @@ -0,0 +1,636 @@ +require "uri-test" +local URI = require "uri" + +module("test.generic", lunit.testcase, package.seeall) + +function test_normalize_percent_encoding () + -- Don't use unnecessary percent encoding for unreserved characters. + test_norm("x:ABCDEFGHIJKLM", "x:%41%42%43%44%45%46%47%48%49%4A%4b%4C%4d") + test_norm("x:NOPQRSTUVWXYZ", "x:%4E%4f%50%51%52%53%54%55%56%57%58%59%5A") + test_norm("x:abcdefghijklm", "x:%61%62%63%64%65%66%67%68%69%6A%6b%6C%6d") + test_norm("x:nopqrstuvwxyz", "x:%6E%6f%70%71%72%73%74%75%76%77%78%79%7A") + test_norm("x:0123456789", "x:%30%31%32%33%34%35%36%37%38%39") + test_norm("x:-._~", "x:%2D%2e%5F%7e") + + -- Keep percent encoding for other characters in US-ASCII. + test_norm_already("x:%00%01%02%03%04%05%06%07%08%09%0A%0B%0C%0D%0E%0F") + test_norm_already("x:%10%11%12%13%14%15%16%17%18%19%1A%1B%1C%1D%1E%1F") + test_norm_already("x:%20%21%22%23%24%25%26%27%28%29%2A%2B%2C") + test_norm_already("x:%2F") + test_norm_already("x:%3A%3B%3C%3D%3E%3F%40") + test_norm_already("x:%5B%5C%5D%5E") + test_norm_already("x:%60") + test_norm_already("x:%7B%7C%7D") + test_norm_already("x:%7F") + + -- Normalize hex digits in percent encoding to uppercase. + test_norm("x:%0A%0B%0C%0D%0E%0F", "x:%0a%0b%0c%0d%0e%0f") + test_norm("x:%AA%BB%CC%DD%EE%FF", "x:%aA%bB%cC%dD%eE%fF") + + -- Keep percent encoding, and normalize hex digit case, for all characters + -- outside US-ASCII. + for i = 0x80, 0xFF do + test_norm_already(string.format("x:%%%02X", i)) + test_norm(string.format("x:%%%02X", i), string.format("x:%%%02x", i)) + end +end + +function test_bad_percent_encoding () + assert_error("double percent", function () URI:new("x:foo%%2525") end) + assert_error("no hex digits", function () URI:new("x:foo%") end) + assert_error("no hex digits 2nd time", function () URI:new("x:f%20o%") end) + assert_error("1 hex digit", function () URI:new("x:foo%2") end) + assert_error("1 hex digit 2nd time", function () URI:new("x:f%20o%2") end) + assert_error("bad hex digit 1", function () URI:new("x:foo%G2bar") end) + assert_error("bad hex digit 2", function () URI:new("x:foo%2Gbar") end) + assert_error("bad hex digit both", function () URI:new("x:foo%GGbar") end) +end + +function test_scheme () + test_norm_already("foo:") + test_norm_already("foo:-+.:") + test_norm_already("foo:-+.0123456789:") + test_norm_already("x:") + test_norm("example:FooBar:Baz", "ExAMplE:FooBar:Baz") + + local uri = assert(URI:new("Foo-Bar:Baz%20Quux")) + is("foo-bar", uri:scheme()) +end + +function test_change_scheme () + local uri = assert(URI:new("x-foo://example.com/blah")) + is("x-foo://example.com/blah", tostring(uri)) + is("x-foo", uri:scheme()) + is("uri", uri._NAME) + + -- x-foo -> x-bar + is("x-foo", uri:scheme("x-bar")) + is("x-bar", uri:scheme()) + is("x-bar://example.com/blah", tostring(uri)) + is("uri", uri._NAME) + + -- x-bar -> http + is("x-bar", uri:scheme("http")) + is("http", uri:scheme()) + is("http://example.com/blah", tostring(uri)) + is("uri.http", uri._NAME) + + -- http -> x-foo + is("http", uri:scheme("x-foo")) + is("x-foo", uri:scheme()) + is("x-foo://example.com/blah", tostring(uri)) + is("uri", uri._NAME) +end + +function test_change_scheme_bad () + local uri = assert(URI:new("x-foo://foo@bar/")) + + -- Try changing the scheme to something invalid + assert_error("bad scheme '-x-foo'", function () uri:scheme("-x-foo") end) + assert_error("bad scheme 'x,foo'", function () uri:scheme("x,foo") end) + assert_error("bad scheme 'x:foo'", function () uri:scheme("x:foo") end) + assert_error("bad scheme 'x-foo:'", function () uri:scheme("x-foo:") end) + + -- Change to valid scheme, but where the rest of the URI is not valid for it + assert_error("bad HTTP URI", function () uri:scheme("http") end) + + -- Original URI should be left unchanged + is("x-foo://foo@bar/", tostring(uri)) + is("x-foo", uri:scheme()) + is("uri", uri._NAME) +end + +function test_auth_userinfo () + local uri = assert(URI:new("X://a-zA-Z09!$:&%40@FOO.com:80/")) + is("x://a-zA-Z09!$:&%40@foo.com:80/", tostring(uri)) + is("x", uri:scheme()) + is("a-zA-Z09!$:&%40", uri:userinfo()) + is("foo.com", uri:host()) + is(80, uri:port()) +end + +function test_auth_userinfo_bad () + is_bad_uri("bad character in userinfo", "x-a://foo^bar@example.com/") +end + +function test_auth_set_userinfo () + local uri = assert(URI:new("X-foo://user:pass@FOO.com:80/")) + is("user:pass", uri:userinfo("newuserinfo")) + is("newuserinfo", uri:userinfo()) + is("x-foo://newuserinfo@foo.com:80/", tostring(uri)) + + -- Userinfo should be supplied already percent-encoded, but the percent + -- encoding should be normalized. + is("newuserinfo", uri:userinfo("foo%3abar%3A:%78")) + is("foo%3Abar%3A:x", uri:userinfo()) + + -- It should be OK to use more than one colon in userinfo for generic URIs, + -- although not for ones which specificly divide it into username:password. + is("foo%3Abar%3A:x", uri:userinfo("foo:bar:baz::")) + is("foo:bar:baz::", uri:userinfo()) +end + +function test_auth_set_bad_userinfo () + local uri = assert(URI:new("X-foo://user:pass@FOO.com:80/")) + assert_error("/ in userinfo", function () uri:userinfo("foo/bar") end) + assert_error("@ in userinfo", function () uri:userinfo("foo@bar") end) + is("user:pass", uri:userinfo()) + is("x-foo://user:pass@foo.com:80/", tostring(uri)) +end + +function test_auth_reg_name () + local uri = assert(URI:new("x://azAZ0-9--foo.bqr_baz~%20!$;/")) + -- TODO - %20 should probably be rejected. Apparently only UTF-8 pctenc + -- should be produced, so after unescaping unreserved chars there should + -- be nothing left percent encoded other than valid UTF-8 sequences. If + -- that's right I could safely decode the host before returning it. + is("azaz0-9--foo.bqr_baz~%20!$;", uri:host()) +end + +function test_auth_ip4 () + local uri = assert(URI:new("x://0.0.0.0/path")) + is("0.0.0.0", uri:host()) + uri = assert(URI:new("x://192.168.0.1/path")) + is("192.168.0.1", uri:host()) + uri = assert(URI:new("x://255.255.255.255/path")) + is("255.255.255.255", uri:host()) +end + +function test_auth_ip4_or_reg_name_bad () + is_bad_uri("bad character in host part", "x://foo:bar/") +end + +function test_auth_ip6 () + -- The example addresses in here are all from RFC 4291 section 2.2, except + -- that they get normalized to lowercase here in the results. + local uri = assert(URI:new("x://[ABCD:EF01:2345:6789:ABCD:EF01:2345:6789]")) + is("[abcd:ef01:2345:6789:abcd:ef01:2345:6789]", uri:host()) + uri = assert(URI:new("x://[ABCD:EF01:2345:6789:ABCD:EF01:2345:6789]/")) + is("[abcd:ef01:2345:6789:abcd:ef01:2345:6789]", uri:host()) + uri = assert(URI:new("x://[ABCD:EF01:2345:6789:ABCD:EF01:2345:6789]:")) + is("[abcd:ef01:2345:6789:abcd:ef01:2345:6789]", uri:host()) + uri = assert(URI:new("x://[ABCD:EF01:2345:6789:ABCD:EF01:2345:6789]:/")) + is("[abcd:ef01:2345:6789:abcd:ef01:2345:6789]", uri:host()) + uri = assert(URI:new("x://[ABCD:EF01:2345:6789:ABCD:EF01:2345:6789]:0/")) + is("[abcd:ef01:2345:6789:abcd:ef01:2345:6789]", uri:host()) + uri = assert(URI:new("x://y:z@[ABCD:EF01:2345:6789:ABCD:EF01:2345:6789]:80/")) + is("[abcd:ef01:2345:6789:abcd:ef01:2345:6789]", uri:host()) + uri = assert(URI:new("x://[2001:DB8:0:0:8:800:200C:417A]/")) + is("[2001:db8:0:0:8:800:200c:417a]", uri:host()) + uri = assert(URI:new("x://[FF01:0:0:0:0:0:0:101]/")) + is("[ff01:0:0:0:0:0:0:101]", uri:host()) + uri = assert(URI:new("x://[ff01::101]/")) + is("[ff01::101]", uri:host()) + uri = assert(URI:new("x://[0:0:0:0:0:0:0:1]/")) + is("[0:0:0:0:0:0:0:1]", uri:host()) + uri = assert(URI:new("x://[::1]/")) + is("[::1]", uri:host()) + uri = assert(URI:new("x://[0:0:0:0:0:0:0:0]/")) + is("[0:0:0:0:0:0:0:0]", uri:host()) + uri = assert(URI:new("x://[0:0:0:0:0:0:13.1.68.3]/")) + is("[0:0:0:0:0:0:13.1.68.3]", uri:host()) + uri = assert(URI:new("x://[::13.1.68.3]/")) + is("[::13.1.68.3]", uri:host()) + uri = assert(URI:new("x://[0:0:0:0:0:FFFF:129.144.52.38]/")) + is("[0:0:0:0:0:ffff:129.144.52.38]", uri:host()) + uri = assert(URI:new("x://[::FFFF:129.144.52.38]/")) + is("[::ffff:129.144.52.38]", uri:host()) + + -- These try all the cominations of abbreviating using '::'. + uri = assert(URI:new("x://[08:19:2a:3B:4c:5D:6e:7F]/")) + is("[08:19:2a:3b:4c:5d:6e:7f]", uri:host()) + uri = assert(URI:new("x://[::19:2a:3B:4c:5D:6e:7F]/")) + is("[::19:2a:3b:4c:5d:6e:7f]", uri:host()) + uri = assert(URI:new("x://[::2a:3B:4c:5D:6e:7F]/")) + is("[::2a:3b:4c:5d:6e:7f]", uri:host()) + uri = assert(URI:new("x://[::3B:4c:5D:6e:7F]/")) + is("[::3b:4c:5d:6e:7f]", uri:host()) + uri = assert(URI:new("x://[::4c:5D:6e:7F]/")) + is("[::4c:5d:6e:7f]", uri:host()) + uri = assert(URI:new("x://[::5D:6e:7F]/")) + is("[::5d:6e:7f]", uri:host()) + uri = assert(URI:new("x://[::6e:7F]/")) + is("[::6e:7f]", uri:host()) + uri = assert(URI:new("x://[::7F]/")) + is("[::7f]", uri:host()) + uri = assert(URI:new("x://[::]/")) + is("[::]", uri:host()) + uri = assert(URI:new("x://[08::]/")) + is("[08::]", uri:host()) + uri = assert(URI:new("x://[08:19::]/")) + is("[08:19::]", uri:host()) + uri = assert(URI:new("x://[08:19:2a::]/")) + is("[08:19:2a::]", uri:host()) + uri = assert(URI:new("x://[08:19:2a:3B::]/")) + is("[08:19:2a:3b::]", uri:host()) + uri = assert(URI:new("x://[08:19:2a:3B:4c::]/")) + is("[08:19:2a:3b:4c::]", uri:host()) + uri = assert(URI:new("x://[08:19:2a:3B:4c:5D::]/")) + is("[08:19:2a:3b:4c:5d::]", uri:host()) + uri = assert(URI:new("x://[08:19:2a:3B:4c:5D:6e::]/")) + is("[08:19:2a:3b:4c:5d:6e::]", uri:host()) + + -- Try extremes of good IPv4 addresses mapped to IPv6. + uri = assert(URI:new("x://[::FFFF:0.0.0.0]/path")) + is("[::ffff:0.0.0.0]", uri:host()) + uri = assert(URI:new("x://[::ffff:255.255.255.255]/path")) + is("[::ffff:255.255.255.255]", uri:host()) +end + +function test_auth_ip6_bad () + is_bad_uri("empty brackets", "x://[]") + is_bad_uri("just colon", "x://[:]") + is_bad_uri("3 colons only", "x://[:::]") + is_bad_uri("3 colons at start", "x://[:::1234]") + is_bad_uri("3 colons at end", "x://[1234:::]") + is_bad_uri("3 colons in middle", "x://[1234:::5678]") + is_bad_uri("non-hex char", "x://[ABCD:EF01:2345:6789:ABCD:EG01:2345:6789]") + is_bad_uri("chunk too big", + "x://[ABCD:EF01:2345:6789:ABCD:EFF01:2345:6789]") + is_bad_uri("too many chunks", + "x://[ABCD:EF01:2345:6789:ABCD:EF01:2345:6789:1]") + is_bad_uri("not enough chunks", "x://[ABCD:EF01:2345:6789:ABCD:EF01:2345]") + is_bad_uri("too many chunks with ellipsis in middle", + "x://[ABCD:EF01:2345:6789:ABCD::EF01:2345:6789]") + is_bad_uri("too many chunks with ellipsis at end", + "x://[ABCD:EF01:2345:6789:ABCD:EF01:2345:6789::]") + is_bad_uri("too many chunks with ellipsis at start", + "x://[::ABCD:EF01:2345:6789:ABCD:EF01:2345:6789]") + is_bad_uri("two elipses, middle and end", + "x://[EF01:2345::6789:ABCD:EF01:2345::]") + is_bad_uri("two elipses, start and middle", + "x://[::EF01:2345::6789:ABCD:EF01:2345]") + is_bad_uri("two elipses, both ends", + "x://[::EF01:2345:6789:ABCD:EF01:2345::]") + is_bad_uri("two elipses, both middle", + "x://[EF01:2345::6789:ABCD:::EF01:2345]") + is_bad_uri("extra colon at start", + "x://[:ABCD:EF01:2345:6789:ABCD:EF01:2345:6789]") + is_bad_uri("missing chunk at start", + "x://[:EF01:2345:6789:ABCD:EF01:2345:6789]") + is_bad_uri("extra colon at end", + "x://[ABCD:EF01:2345:6789:ABCD:EF01:2345:6789:]") + is_bad_uri("missing chunk at end", + "x://[ABCD:EF01:2345:6789:ABCD:EF01:2345:]") + + -- Bad IPv4 addresses mapped to IPv6. + is_bad_uri("octet 1 too big", "x://[::FFFF:256.2.3.4]/") + is_bad_uri("octet 2 too big", "x://[::FFFF:1.256.3.4]/") + is_bad_uri("octet 3 too big", "x://[::FFFF:1.2.256.4]/") + is_bad_uri("octet 4 too big", "x://[::FFFF:1.2.3.256]/") + is_bad_uri("octet 1 leading zeroes", "x://[::FFFF:01.2.3.4]/") + is_bad_uri("octet 2 leading zeroes", "x://[::FFFF:1.02.3.4]/") + is_bad_uri("octet 3 leading zeroes", "x://[::FFFF:1.2.03.4]/") + is_bad_uri("octet 4 leading zeroes", "x://[::FFFF:1.2.3.04]/") + is_bad_uri("only 2 octets", "x://[::FFFF:1.2]/") + is_bad_uri("only 3 octets", "x://[::FFFF:1.2.3]/") + is_bad_uri("5 octets", "x://[::FFFF:1.2.3.4.5]/") +end + +function test_auth_ipvfuture () + local uri = assert(URI:new("x://[v123456789ABCdef.foo=bar]/")) + is("[v123456789abcdef.foo=bar]", uri:host()) +end + +function test_auth_ipvfuture_bad () + is_bad_uri("missing dot", "x://[v999]") + is_bad_uri("missing hex num", "x://[v.foo]") + is_bad_uri("missing bit after dot", "x://[v999.]") + is_bad_uri("bad character in hex num", "x://[v99g.foo]") + is_bad_uri("bad character after dot", "x://[v999.foo:bar]") +end + +function test_auth_set_host () + local uri = assert(URI:new("x-a://host/path")) + is("host", uri:host("FOO.BAR")) + is("x-a://foo.bar/path", tostring(uri)) + is("foo.bar", uri:host("[::6e:7F]")) + is("x-a://[::6e:7f]/path", tostring(uri)) + is("[::6e:7f]", uri:host("[v7F.foo=BAR]")) + is("x-a://[v7f.foo=bar]/path", tostring(uri)) + is("[v7f.foo=bar]", uri:host("")) + is("x-a:///path", tostring(uri)) + is("", uri:host(nil)) + is(nil, uri:host()) + is("x-a:/path", tostring(uri)) +end + +function test_auth_set_host_bad () + local uri = assert(URI:new("x-a://host/path")) + assert_error("bad char in host", function () uri:host("foo^bar") end) + assert_error("invalid IPv6 host", function () uri:host("[::3G]") end) + assert_error("invalid IPvFuture host", function () uri:host("[v7.]") end) + is("host", uri:host()) + is("x-a://host/path", tostring(uri)) + -- There must be a hsot when there is a userinfo or port. + uri = assert(URI:new("x-a://foo@/")) + assert_error("userinfo but no host", function () uri:host(nil) end) + is("x-a://foo@/", tostring(uri)) + uri = assert(URI:new("x-a://:123/")) + assert_error("port but no host", function () uri:host(nil) end) + is("x-a://:123/", tostring(uri)) +end + +function test_auth_port () + local uri = assert(URI:new("x://localhost:0/path")) + is(0, uri:port()) + uri = assert(URI:new("x://localhost:0")) + is(0, uri:port()) + uri = assert(URI:new("x://foo:bar@localhost:0")) + is(0, uri:port()) + uri = assert(URI:new("x://localhost:00/path")) + is(0, uri:port()) + uri = assert(URI:new("x://localhost:00")) + is(0, uri:port()) + uri = assert(URI:new("x://foo:bar@localhost:00")) + is(0, uri:port()) + uri = assert(URI:new("x://localhost:54321/path")) + is(54321, uri:port()) + uri = assert(URI:new("x://localhost:54321")) + is(54321, uri:port()) + uri = assert(URI:new("x://foo:bar@localhost:54321")) + is(54321, uri:port()) + uri = assert(URI:new("x://foo:bar@localhost:")) + is(nil, uri:port()) + uri = assert(URI:new("x://foo:bar@localhost:/")) + is(nil, uri:port()) + uri = assert(URI:new("x://foo:bar@localhost")) + is(nil, uri:port()) + uri = assert(URI:new("x://foo:bar@localhost/")) + is(nil, uri:port()) +end + +function test_auth_set_port () + -- Test unusual but valid values for port. + local uri = assert(URI:new("x://localhost/path")) + is(nil, uri:port("12345")) -- string + is(12345, uri:port()) + is("x://localhost:12345/path", tostring(uri)) + uri = assert(URI:new("x://localhost/path")) + is(nil, uri:port(12345.0)) -- float + is(12345, uri:port()) + is("x://localhost:12345/path", tostring(uri)) +end + +function test_auth_set_port_without_host () + local uri = assert(URI:new("x:///path")) + is(nil, uri:port(80)) + is(80, uri:port()) + is("", uri:host()) + is("x://:80/path", tostring(uri)) + uri = assert(URI:new("x:/path")) + is(nil, uri:port(80)) + is(80, uri:port()) + is("", uri:host()) + is("x://:80/path", tostring(uri)) +end + +function test_auth_set_port_bad () + local uri = assert(URI:new("x://localhost:54321/path")) + assert_error("negative port number", function () uri:port(-23) end) + assert_error("port not integer", function () uri:port(23.00001) end) + assert_error("string not number", function () uri:port("x") end) + assert_error("string not all number", function () uri:port("x23") end) + assert_error("string negative number", function () uri:port("-23") end) + assert_error("string empty", function () uri:port("") end) + is(54321, uri:port()) + is("x://localhost:54321/path", tostring(uri)) +end + +function test_path () + local uri = assert(URI:new("x:")) + is("", uri:path()) + uri = assert(URI:new("x:?")) + is("", uri:path()) + uri = assert(URI:new("x:#")) + is("", uri:path()) + uri = assert(URI:new("x:/")) + is("/", uri:path()) + uri = assert(URI:new("x://")) + is("", uri:path()) + uri = assert(URI:new("x://?")) + is("", uri:path()) + uri = assert(URI:new("x://#")) + is("", uri:path()) + uri = assert(URI:new("x:///")) + is("/", uri:path()) + uri = assert(URI:new("x:////")) + is("//", uri:path()) + uri = assert(URI:new("x:foo")) + is("foo", uri:path()) + uri = assert(URI:new("x:/foo")) + is("/foo", uri:path()) + uri = assert(URI:new("x://foo")) + is("", uri:path()) + uri = assert(URI:new("x://foo?")) + is("", uri:path()) + uri = assert(URI:new("x://foo#")) + is("", uri:path()) + uri = assert(URI:new("x:///foo")) + is("/foo", uri:path()) + uri = assert(URI:new("x:////foo")) + is("//foo", uri:path()) + uri = assert(URI:new("x://foo/")) + is("/", uri:path()) + uri = assert(URI:new("x://foo/bar")) + is("/bar", uri:path()) +end + +function test_path_bad () + is_bad_uri("bad character in path", "x-a://host/^/") +end + +function test_set_path_without_auth () + local uri = assert(URI:new("x:blah")) + is("blah", uri:path("frob%25%3a%78/%2F")) + is("frob%25%3Ax/%2F", uri:path("/foo/bar")) + is("/foo/bar", uri:path("//foo//bar")) + is("/%2Ffoo//bar", uri:path("x ?#\"\0\127\255")) + is("x%20%3F%23%22%00%7F%FF", uri:path("")) + is("", uri:path(nil)) + is("", uri:path()) + is("x:", tostring(uri)) +end + +function test_set_path_with_auth () + local uri = assert(URI:new("x://host/wibble")) + is("/wibble", uri:path("/foo/bar")) + is("/foo/bar", uri:path("//foo//bar")) + is("//foo//bar", uri:path(nil)) + is("", uri:path("")) + is("", uri:path()) + is("x://host", tostring(uri)) +end + +function test_set_path_bad () + local uri = assert(URI:new("x://host/wibble")) + tostring(uri) + assert_error("with authority, path must start with /", + function () uri:path("foo") end) + assert_error("bad %-encoding, % at end", function () uri:path("foo%") end) + assert_error("bad %-encoding, %2 at end", function () uri:path("foo%2") end) + assert_error("bad %-encoding, %gf", function () uri:path("%gf") end) + assert_error("bad %-encoding, %fg", function () uri:path("%fg") end) + is("/wibble", uri:path()) + is("x://host/wibble", tostring(uri)) +end + +function test_query () + local uri = assert(URI:new("x:?")) + is("", uri:query()) + uri = assert(URI:new("x:")) + is(nil, uri:query()) + uri = assert(URI:new("x:/foo")) + is(nil, uri:query()) + uri = assert(URI:new("x:/foo#")) + is(nil, uri:query()) + uri = assert(URI:new("x:/foo#bar?baz")) + is(nil, uri:query()) + uri = assert(URI:new("x:/foo?")) + is("", uri:query()) + uri = assert(URI:new("x://foo?")) + is("", uri:query()) + uri = assert(URI:new("x://foo/?")) + is("", uri:query()) + uri = assert(URI:new("x:/foo?bar")) + is("bar", uri:query()) + uri = assert(URI:new("x:?foo?bar?")) + is("foo?bar?", uri:query()) + uri = assert(URI:new("x:?foo?bar?#quux?frob")) + is("foo?bar?", uri:query()) + uri = assert(URI:new("x://foo/bar%3Fbaz?")) + is("", uri:query()) + uri = assert(URI:new("x:%3F?foo")) + is("%3F", uri:path()) + is("foo", uri:query()) +end + +function test_query_bad () + is_bad_uri("bad character in query", "x-a://host/path/?foo^bar") +end + +function test_set_query () + local uri = assert(URI:new("x://host/path")) + is(nil, uri:query("foo/bar?baz")) + is("x://host/path?foo/bar?baz", tostring(uri)) + is("foo/bar?baz", uri:query("")) + is("x://host/path?", tostring(uri)) + is("", uri:query("foo^bar#baz")) + is("x://host/path?foo%5Ebar%23baz", tostring(uri)) + is("foo%5Ebar%23baz", uri:query(nil)) + is(nil, uri:query()) + is("x://host/path", tostring(uri)) +end + +function test_fragment () + local uri = assert(URI:new("x:")) + is(nil, uri:fragment()) + uri = assert(URI:new("x:#")) + is("", uri:fragment()) + uri = assert(URI:new("x://#")) + is("", uri:fragment()) + uri = assert(URI:new("x:///#")) + is("", uri:fragment()) + uri = assert(URI:new("x:////#")) + is("", uri:fragment()) + uri = assert(URI:new("x:#foo")) + is("foo", uri:fragment()) + uri = assert(URI:new("x:%23#foo")) + is("%23", uri:path()) + is("foo", uri:fragment()) + uri = assert(URI:new("x:?foo?bar?#quux?frob")) + is("quux?frob", uri:fragment()) +end + +function test_fragment_bad () + is_bad_uri("bad character in fragment", "x-a://host/path/#foo^bar") +end + +function test_set_fragment () + local uri = assert(URI:new("x://host/path")) + is(nil, uri:fragment("foo/bar#baz")) + is("x://host/path#foo/bar%23baz", tostring(uri)) + is("foo/bar%23baz", uri:fragment("")) + is("x://host/path#", tostring(uri)) + is("", uri:fragment("foo^bar?baz")) + is("x://host/path#foo%5Ebar?baz", tostring(uri)) + is("foo%5Ebar?baz", uri:fragment(nil)) + is(nil, uri:fragment()) + is("x://host/path", tostring(uri)) +end + +function test_bad_usage () + assert_error("missing uri arg", function () URI:new() end) + assert_error("nil uri arg", function () URI:new(nil) end) +end + +function test_clone_with_new () + -- Test cloning with as many components set as possible. + local uri = assert(URI:new("x-foo://user:pass@bar.com:123/blah?q#frag")) + tostring(uri) + local clone = URI:new(uri) + assert_table(clone) + is("x-foo://user:pass@bar.com:123/blah?q#frag", tostring(uri)) + is("x-foo://user:pass@bar.com:123/blah?q#frag", tostring(clone)) + is("uri", getmetatable(uri)._NAME) + is("uri", getmetatable(clone)._NAME) + + -- Test cloning with less stuff specified, but not in the base class. + uri = assert(URI:new("http://example.com/")) + clone = URI:new(uri) + assert_table(clone) + is("http://example.com/", tostring(uri)) + is("http://example.com/", tostring(clone)) + is("uri.http", getmetatable(uri)._NAME) + is("uri.http", getmetatable(clone)._NAME) +end + +function test_set_uri () + local uri = assert(URI:new("x-foo://user:pass@bar.com:123/blah?q#frag")) + is("x-foo://user:pass@bar.com:123/blah?q#frag", + uri:uri("http://example.com:81/blah2?q2#frag2")) + is("http://example.com:81/blah2?q2#frag2", uri:uri()) + is("uri.http", getmetatable(uri)._NAME) + is("http", uri:scheme()) + is("q2", uri:query()) + is("http://example.com:81/blah2?q2#frag2", uri:uri("Urn:X-FOO:bar")) + is("uri.urn", getmetatable(uri)._NAME) + is("x-foo", uri:nid()) + is("urn:x-foo:bar", tostring(uri)) +end + +function test_set_uri_bad () + local uri = assert(URI:new("x-foo://user:pass@bar.com:123/blah?q#frag")) + assert_error("can't set URI to nil", function () uri:uri(nil) end) + assert_error("invalid authority", function () uri:uri("foo://@@") end) + is("x-foo://user:pass@bar.com:123/blah?q#frag", uri:uri()) + is("uri", getmetatable(uri)._NAME) + is("x-foo", uri:scheme()) +end + +function test_eq () + local uri1str, uri2str = "x-a://host/foo", "x-a://host/bar" + local uri1obj, uri2obj = assert(URI:new(uri1str)), assert(URI:new(uri2str)) + assert_true(URI.eq(uri1str, uri1str), "str == str") + assert_false(URI.eq(uri1str, uri2str), "str ~= str") + assert_true(URI.eq(uri1str, uri1obj), "str == obj") + assert_false(URI.eq(uri1str, uri2obj), "str ~= obj") + assert_true(URI.eq(uri1obj, uri1str), "obj == str") + assert_false(URI.eq(uri1obj, uri2str), "obj ~= str") + assert_true(URI.eq(uri1obj, uri1obj), "obj == obj") + assert_false(URI.eq(uri1obj, uri2obj), "obj ~= obj") +end + +function test_eq_bad_uri () + -- Check that an exception is thrown when 'eq' is given a bad URI string, + -- and also that it's not just the error from trying to call the 'uri' + -- method on nil, because that won't be very helpful to the caller. + local ok, err = pcall(URI.eq, "^", "x-a://x/") + assert_false(ok) + assert_not_match("a nil value", err) + ok, err = pcall(URI.eq, "x-a://x/", "^") + assert_false(ok) + assert_not_match("a nil value", err) +end + +-- vi:ts=4 sw=4 expandtab diff --git a/test/_pristine.lua b/test/_pristine.lua new file mode 100644 index 0000000..892f2d8 --- /dev/null +++ b/test/_pristine.lua @@ -0,0 +1,36 @@ +require "lunit" + +module("test.pristine", lunit.testcase, package.seeall) + +function test_no_global_clobbering () + local globals = {} + for key in pairs(_G) do globals[key] = true end + + -- Load all the modules for the different types of URIs, in case any one + -- of those treads on a global. I keep them around in a table to make + -- sure they're all loaded at the same time, just in case that does + -- anything interesting. + local schemes = { + "_login", "_relative", "_util", "data", + "file", "file.unix", "file.win32", + "ftp", "http", "https", + "pop", "rtsp", "rtspu", "telnet", + "urn", "urn.isbn", "urn.issn", "urn.oid" + } + local loaded = {} + local URI = require "uri" + for _, name in ipairs(schemes) do + loaded[name] = require("uri." .. name) + end + + for key in pairs(_G) do + lunit.assert_not_nil(globals[key], + "global '" .. key .. "' created by lib") + end + for key in pairs(globals) do + lunit.assert_not_nil(_G[key], + "global '" .. key .. "' destroyed by lib") + end +end + +-- vi:ts=4 sw=4 expandtab diff --git a/test/_relative.lua b/test/_relative.lua new file mode 100644 index 0000000..89f666b --- /dev/null +++ b/test/_relative.lua @@ -0,0 +1,54 @@ +require "uri-test" +local URI = require "uri" + +module("test.relative", lunit.testcase, package.seeall) + +local function test_rel (input, userinfo, host, port, path, query, frag, + expected) + local uri = assert(URI:new(input)) + assert_true(uri:is_relative()) + is("uri._relative", getmetatable(uri)._NAME) + is(nil, uri:scheme()) + is(userinfo, uri:userinfo()) + is(host, uri:host()) + is(port, uri:port()) + is(path, uri:path()) + is(query, uri:query()) + is(frag, uri:fragment()) + if not expected then expected = input end + is(expected, uri:uri()) + is(expected, tostring(uri)) +end + +function test_relative () + test_rel("", nil, nil, nil, "", nil, nil) + test_rel("foo/bar", nil, nil, nil, "foo/bar", nil, nil) + test_rel("/foo/bar", nil, nil, nil, "/foo/bar", nil, nil) + test_rel("?query", nil, nil, nil, "", "query", nil) + test_rel("?", nil, nil, nil, "", "", nil) + test_rel("#foo", nil, nil, nil, "", nil, "foo") + test_rel("#", nil, nil, nil, "", nil, "") + test_rel("?q#f", nil, nil, nil, "", "q", "f") + test_rel("?#", nil, nil, nil, "", "", "") + test_rel("foo?q#f", nil, nil, nil, "foo", "q", "f") + test_rel("//host.com", nil, "host.com", nil, "", nil, nil) + test_rel("//host.com/blah?q#f", nil, "host.com", nil, "/blah", "q", "f") + test_rel("//host.com:123/blah?q#f", nil, "host.com", 123, "/blah", "q", "f") + test_rel("//u:p@host.com:123/blah?q#f", + "u:p", "host.com", 123, "/blah", "q", "f") + + -- Paths shouldn't be normalized in a relative reference, only after it + -- has been used to create an absolute one. + test_rel("./foo/bar", nil, nil, nil, "./foo/bar", nil, nil) + test_rel("././foo/./bar", nil, nil, nil, "././foo/./bar", nil, nil) + test_rel("../foo/bar", nil, nil, nil, "../foo/bar", nil, nil) + test_rel("../../foo/../bar", nil, nil, nil, "../../foo/../bar", nil, nil) +end + +function test_bad_usage () + local uri = assert(URI:new("foo")) + assert_error("set scheme on relative ref", + function () uri:scheme("x-foo") end) +end + +-- vi:ts=4 sw=4 expandtab diff --git a/test/_resolve.lua b/test/_resolve.lua new file mode 100644 index 0000000..3844f55 --- /dev/null +++ b/test/_resolve.lua @@ -0,0 +1,201 @@ +require "uri-test" +local URI = require "uri" + +module("test.resolve", lunit.testcase, package.seeall) + +-- Test data from RFC 3986. The 'http' prefix has been changed throughout +-- to 'x-foo' so as not to trigger any scheme-specific normalization. +local resolve_tests = { + -- 5.4.1. Normal Examples + ["g:h"] = "g:h", + ["g"] = "x-foo://a/b/c/g", + ["./g"] = "x-foo://a/b/c/g", + ["g/"] = "x-foo://a/b/c/g/", + ["/g"] = "x-foo://a/g", + ["//g"] = "x-foo://g", + ["?y"] = "x-foo://a/b/c/d;p?y", + ["g?y"] = "x-foo://a/b/c/g?y", + ["#s"] = "x-foo://a/b/c/d;p?q#s", + ["g#s"] = "x-foo://a/b/c/g#s", + ["g?y#s"] = "x-foo://a/b/c/g?y#s", + [";x"] = "x-foo://a/b/c/;x", + ["g;x"] = "x-foo://a/b/c/g;x", + ["g;x?y#s"] = "x-foo://a/b/c/g;x?y#s", + [""] = "x-foo://a/b/c/d;p?q", + ["."] = "x-foo://a/b/c/", + ["./"] = "x-foo://a/b/c/", + [".."] = "x-foo://a/b/", + ["../"] = "x-foo://a/b/", + ["../g"] = "x-foo://a/b/g", + ["../.."] = "x-foo://a/", + ["../../"] = "x-foo://a/", + ["../../g"] = "x-foo://a/g", + + -- 5.4.2. Abnormal Examples + ["../../../g"] = "x-foo://a/g", + ["../../../../g"] = "x-foo://a/g", + ["/./g"] = "x-foo://a/g", + ["/../g"] = "x-foo://a/g", + ["g."] = "x-foo://a/b/c/g.", + [".g"] = "x-foo://a/b/c/.g", + ["g.."] = "x-foo://a/b/c/g..", + ["..g"] = "x-foo://a/b/c/..g", + ["./../g"] = "x-foo://a/b/g", + ["./g/."] = "x-foo://a/b/c/g/", + ["g/./h"] = "x-foo://a/b/c/g/h", + ["g/../h"] = "x-foo://a/b/c/h", + ["g;x=1/./y"] = "x-foo://a/b/c/g;x=1/y", + ["g;x=1/../y"] = "x-foo://a/b/c/y", + ["g?y/./x"] = "x-foo://a/b/c/g?y/./x", + ["g?y/../x"] = "x-foo://a/b/c/g?y/../x", + ["g#s/./x"] = "x-foo://a/b/c/g#s/./x", + ["g#s/../x"] = "x-foo://a/b/c/g#s/../x", + ["x-foo:g"] = "x-foo:g", + + -- Some extra tests for good measure + ["#foo?"] = "x-foo://a/b/c/d;p?q#foo?", + ["?#foo"] = "x-foo://a/b/c/d;p?#foo", +} + +local function test_abs_rel (base, uref, expect) + local bad = false + + -- Test 'resolve' method with object as argument. + local u = assert(URI:new(uref)) + local b = assert(URI:new(base)) + u:resolve(b) + local got = tostring(u) + if got ~= expect then + bad = true + print("URI:new(" .. uref .. "):resolve(URI:new(" .. base .. ") ===> " .. + expect .. " (not " .. got .. ")") + end + + -- Test 'resolve' method with string as argument. + u = assert(URI:new(uref)) + u:resolve(base) + local got = tostring(u) + if got ~= expect then + bad = true + print("URI:new(" .. uref .. "):resolve(URI:new(" .. base .. ") ===> " .. + expect .. " (not " .. got .. ")") + end + + -- Test resolving relative URI using the constructor. + local u = assert(URI:new(uref, base)) + local got = tostring(u) + if got ~= expect then + bad = true + print("URI:new(" .. uref .. ", " .. base .. ") ==> " .. expect .. + " (not " .. got .. ")") + end + + return bad +end + +function test_resolve () + local base = "x-foo://a/b/c/d;p?q" + local testno = 1 + local bad = false + + for rel, abs in pairs(resolve_tests) do + if test_abs_rel(base, rel, abs) then bad = true end + end + + if bad then fail("one of the checks went wrong") end +end + +function test_resolve_error () + local base = assert(URI:new("urn:oid:1.2.3")) + local uri = assert(URI:new("not-valid-path-for-urn")) + + -- The 'resolve' method should throw an exception if the absolute URI + -- that results from the resolution would be invalid. + assert_error("calling resolve() creates invalid URI", + function () uri:resolve(base) end) + assert_true(uri:is_relative()) + is("not-valid-path-for-urn", tostring(uri)) + + -- But the constructor should return an error in its normal fashion. + local ok, err = URI:new(uri, base) + assert_nil(ok) + assert_string(err) +end + +local relativize_tests = { + -- Empty path if the path is the same as the base URI's. + { "http://ex/", "http://ex/", "" }, + { "http://ex/a/b", "http://ex/a/b", "" }, + { "http://ex/a/b/", "http://ex/a/b/", "" }, + -- Absolute path if the base URI's path doesn't help. + { "http://ex/", "http://ex/a/b", "/" }, + { "http://ex/", "http://ex/a/b/", "/" }, + { "http://ex/x/y", "http://ex/", "/x/y" }, + { "http://ex/x/y/", "http://ex/", "/x/y/" }, + { "http://ex/x", "http://ex/a", "/x" }, + { "http://ex/x", "http://ex/a/", "/x" }, + { "http://ex/x/", "http://ex/a", "/x/" }, + { "http://ex/x/", "http://ex/a/", "/x/" }, + { "http://ex/x/y", "http://ex/a/b", "/x/y" }, + { "http://ex/x/y", "http://ex/a/b/", "/x/y" }, + { "http://ex/x/y/", "http://ex/a/b", "/x/y/" }, + { "http://ex/x/y/", "http://ex/a/b/", "/x/y/" }, + -- Add to the end of the base path. + { "x-a://ex/a/b/c", "x-a://ex/a/b/", "c" }, + { "x-a://ex/a/b/c/", "x-a://ex/a/b/", "c/" }, + { "x-a://ex/a/b/c/d", "x-a://ex/a/b/", "c/d" }, + { "x-a://ex/a/b/c/d/", "x-a://ex/a/b/", "c/d/" }, + { "x-a://ex/a/b/c/d/e", "x-a://ex/a/b/", "c/d/e" }, + { "x-a://ex/a/b/c:foo/d/e", "x-a://ex/a/b/", "./c:foo/d/e" }, + -- Change last segment in base path, and add to it. + { "x-a://ex/a/b/", "x-a://ex/a/b/c", "./" }, + { "x-a://ex/a/b/x", "x-a://ex/a/b/c", "x" }, + { "x-a://ex/a/b/x/", "x-a://ex/a/b/c", "x/" }, + { "x-a://ex/a/b/x/y", "x-a://ex/a/b/c", "x/y" }, + { "x-a://ex/a/b/x:foo/y", "x-a://ex/a/b/c", "./x:foo/y" }, + -- Use '..' segments. + { "x-a://ex/a/b/c", "x-a://ex/a/b/c/d", "../c" }, + { "x-a://ex/a/b/c", "x-a://ex/a/b/c/", "../c" }, + { "x-a://ex/a/b/", "x-a://ex/a/b/c/", "../" }, + { "x-a://ex/a/b/", "x-a://ex/a/b/c/d", "../" }, + { "x-a://ex/a/b", "x-a://ex/a/b/c/", "../../b" }, + { "x-a://ex/a/b", "x-a://ex/a/b/c/d", "../../b" }, + { "x-a://ex/a/", "x-a://ex/a/b/c/", "../../" }, + { "x-a://ex/a/", "x-a://ex/a/b/c/d", "../../" }, + -- Preserve query and fragment parts. + { "http://ex/a/b", "http://ex/a/b?baseq#basef", "b" }, + { "http://ex/a/b:c", "http://ex/a/b:c?baseq#basef", "./b:c" }, + { "http://ex/a/b?", "http://ex/a/b?baseq#basef", "?" }, + { "http://ex/a/b?foo", "http://ex/a/b?baseq#basef", "?foo" }, + { "http://ex/a/b?foo#", "http://ex/a/b?baseq#basef", "?foo#" }, + { "http://ex/a/b?foo#bar", "http://ex/a/b?baseq#basef", "?foo#bar" }, + { "http://ex/a/b#bar", "http://ex/a/b?baseq#basef", "b#bar" }, + { "http://ex/a/b:foo#bar", "http://ex/a/b:foo?baseq#basef", "./b:foo#bar" }, + { "http://ex/a/b:foo#bar", "http://ex/a/b:foo#basef", "#bar" }, +} + +function test_relativize () + for _, test in ipairs(relativize_tests) do + local uri = assert(URI:new(test[1])) + uri:relativize(test[2]) + is(test[3], tostring(uri)) + + -- Make sure it will resolve back to the original value. + uri:resolve(test[2]) + is(test[1], tostring(uri)) + end +end + +function test_relativize_already_is () + local uri = assert(URI:new("../foo")) + uri:relativize("http://host/") + is("../foo", tostring(uri)) +end + +function test_relativize_urn () + local uri = assert(URI:new("urn:oid:1.2.3")) + uri:relativize("urn:oid:1") + is("urn:oid:1.2.3", tostring(uri)) +end + +-- vi:ts=4 sw=4 expandtab diff --git a/test/_util.lua b/test/_util.lua new file mode 100644 index 0000000..17160ae --- /dev/null +++ b/test/_util.lua @@ -0,0 +1,92 @@ +require "uri-test" +local Util = require "uri._util" + +module("test.util", lunit.testcase, package.seeall) + +function test_metadata () + is("uri._util", Util._NAME) +end + +function test_uri_encode () + is("%7Cabc%E5", Util.uri_encode("|abc\229")) + is("a%62%63", Util.uri_encode("abc", "b-d")) + assert_nil(Util.uri_encode(nil)) +end + +function test_uri_decode () + is("|abc\229", Util.uri_decode("%7Cabc%e5")) + is("@AB", Util.uri_decode("%40A%42")) + is("CDE", Util.uri_decode("CDE")) +end + +function test_uri_decode () + is("/%2F%25/..!%A1", Util.uri_decode("/%2F%25/%2e.%21%A1", "%-.!")) +end + +function test_remove_dot_segments () + is("/", Util.remove_dot_segments("/foo/../")) + is("/bar", Util.remove_dot_segments("/foo/./../bar")) +end + +function test_split () + local list + list = Util.split(";", "") + assert_array_shallow_equal({}, list) + list = Util.split(";", "foo") + assert_array_shallow_equal({"foo"}, list) + list = Util.split(";", "foo;bar") + assert_array_shallow_equal({"foo","bar"}, list) + list = Util.split(";", "foo;bar;baz") + assert_array_shallow_equal({"foo","bar","baz"}, list) + list = Util.split(";", ";") + assert_array_shallow_equal({"",""}, list) + list = Util.split(";", "foo;") + assert_array_shallow_equal({"foo",""}, list) + list = Util.split(";", ";foo") + assert_array_shallow_equal({"","foo"}, list) + -- TODO test with multi-char and more complex patterns +end + +function test_split_with_max () + local list + list = Util.split(";", "foo;bar;baz", 4) + assert_array_shallow_equal({"foo","bar","baz"}, list) + list = Util.split(";", "foo;bar;baz", 3) + assert_array_shallow_equal({"foo","bar","baz"}, list) + list = Util.split(";", "foo;bar;baz", 2) + assert_array_shallow_equal({"foo","bar;baz"}, list) + list = Util.split(";", "foo;bar;baz", 1) + assert_array_shallow_equal({"foo;bar;baz"}, list) +end + +function test_attempt_require () + local mod = Util.attempt_require("string") + assert_table(mod) + mod = Util.attempt_require("lua-module-which-doesn't-exist") + assert_nil(mod) +end + +function test_subclass_of () + local baseclass = {} + baseclass.__index = baseclass + baseclass.overridden = function () return "baseclass" end + baseclass.inherited = function () return "inherited" end + + local subclass = {} + Util.subclass_of(subclass, baseclass) + subclass.overridden = function () return "subclass" end + + assert(getmetatable(subclass) == baseclass) + assert(subclass._SUPER == baseclass) + + local baseobject, subobject = {}, {} + setmetatable(baseobject, baseclass) + setmetatable(subobject, subclass) + + is("baseclass", baseobject:overridden()) + is("subclass", subobject:overridden()) + is("inherited", baseobject:inherited()) + is("inherited", subobject:inherited()) +end + +-- vi:ts=4 sw=4 expandtab diff --git a/test/data.lua b/test/data.lua new file mode 100644 index 0000000..a981318 --- /dev/null +++ b/test/data.lua @@ -0,0 +1,125 @@ +require "uri-test" +local URI = require "uri" +local Util = require "uri._util" + +local Filter = Util.attempt_require("datafilter") + +module("test.data", lunit.testcase, package.seeall) + +function test_data_uri_encoded () + local uri = assert(URI:new("data:,A%20brief%20note")) + is("uri.data", uri._NAME) + is(",A%20brief%20note", uri:path()) + is("data", uri:scheme()) + + is("text/plain;charset=US-ASCII", uri:data_media_type()) + is("A brief note", uri:data_bytes()) + + local old = uri:data_bytes("F\229r-i-k\229l er tingen!") + is("A brief note", old) + is("data:,F%E5r-i-k%E5l%20er%20tingen!", tostring(uri)) + + old = uri:data_media_type("text/plain;charset=iso-8859-1") + is("text/plain;charset=US-ASCII", old) + is("data:text/plain;charset=iso-8859-1,F%E5r-i-k%E5l%20er%20tingen!", + tostring(uri)) +end + +function test_data_big_base64_chunk () + local imgdata = "R0lGODdhMAAwAPAAAAAAAP///ywAAAAAMAAwAAAC8IyPqcvt3wCcDkiLc7C0qwyGHhSWpjQu5yqmCYsapyuvUUlvONmOZtfzgFzByTB10QgxOR0TqBQejhRNzOfkVJ+5YiUqrXF5Y5lKh/DeuNcP5yLWGsEbtLiOSpa/TPg7JpJHxyendzWTBfX0cxOnKPjgBzi4diinWGdkF8kjdfnycQZXZeYGejmJlZeGl9i2icVqaNVailT6F5iJ90m6mvuTS4OK05M0vDk0Q4XUtwvKOzrcd3iq9uisF81M1OIcR7lEewwcLp7tuNNkM3uNna3F2JQFo97Vriy/Xl4/f1cf5VWzXyym7PHhhx4dbgYKAAA7" + local uri = assert(URI:new("data:image/gif;base64," .. imgdata)) + is("image/gif", uri:data_media_type()) + + if Filter then + local gotdata = uri:data_bytes() + is(273, gotdata:len()) + is(imgdata, Filter.base64_encode(gotdata)) + end +end + +function test_data_containing_commas () + local uri = assert(URI:new("data:application/vnd-xxx-query,select_vcount,fcol_from_fieldtable/local")) + is("application/vnd-xxx-query", uri:data_media_type()) + is("select_vcount,fcol_from_fieldtable/local", uri:data_bytes()) + uri:data_bytes("") + is("data:application/vnd-xxx-query,", tostring(uri)) + + uri:data_bytes("a,b") + uri:data_media_type(nil) + is("data:,a,b", tostring(uri)) + + is("a,b", uri:data_bytes(nil)) + is("", uri:data_bytes()) +end + +function test_automatic_selection_of_uri_or_base64_encoding () + local uri = assert(URI:new("data:,")) + uri:data_bytes("") + is("data:,", tostring(uri)) + + uri:data_bytes(">") + is("data:,%3E", tostring(uri)) + is(">", uri:data_bytes()) + + uri:data_bytes(">>>>>") + is("data:,%3E%3E%3E%3E%3E", tostring(uri)) + + if Filter then + uri:data_bytes(">>>>>>") + is("data:;base64,Pj4+Pj4+", tostring(uri)) + + uri:data_media_type("text/plain;foo=bar") + is("data:text/plain;foo=bar;base64,Pj4+Pj4+", tostring(uri)) + + uri:data_media_type("foo") + is("data:foo;base64,Pj4+Pj4+", tostring(uri)) + + uri:data_bytes((">"):rep(3000)) + is("data:foo;base64," .. ("Pj4+"):rep(1000), tostring(uri)) + is((">"):rep(3000), uri:data_bytes()) + else + uri:data_bytes(">>>>>>") + is("data:,%3E%3E%3E%3E%3E%3E", tostring(uri)) + uri:data_media_type("foo") + is("data:foo,%3E%3E%3E%3E%3E%3E", tostring(uri)) + end + + uri:data_media_type(nil) + uri:data_bytes(nil) + is("data:,", tostring(uri)) +end + +function test_bad_uri () + is_bad_uri("missing comma", "data:foo") + is_bad_uri("no path at all", "data:") + is_bad_uri("has host", "data://host/,") +end + +function test_set_path () + local uri = assert(URI:new("data:image/gif,foobar")) + is("image/gif,foobar", uri:path("image/jpeg;foo=bar,x y,?")) + is("image/jpeg;foo=bar,x%20y,%3F", uri:path(",blah")) + is(",blah", uri:path(",")) + is(",", uri:path()) + is("data:,", tostring(uri)) +end + +function test_set_path_bad () + local uri = assert(URI:new("data:image/gif,foobar")) + assert_error("no path", function () uri:path(nil) end) + assert_error("empty path", function () uri:path("") end) + assert_error("no comma", function () uri:path("foo;bar") end) + assert_error("bad base64 encoding", function () uri:path(";base64,x_0") end) + is("image/gif,foobar", uri:path()) + is("data:image/gif,foobar", tostring(uri)) +end + +function test_set_disallowed_stuff () + local uri = assert(URI:new("data:,")) + assert_error("can't set userinfo", function () uri:userinfo("x") end) + assert_error("can't set host", function () uri:host("x") end) + assert_error("can't set port", function () uri:port(23) end) + is("data:,", tostring(uri)) +end + +-- vi:ts=4 sw=4 expandtab diff --git a/test/file.lua b/test/file.lua new file mode 100644 index 0000000..5854464 --- /dev/null +++ b/test/file.lua @@ -0,0 +1,147 @@ +require "uri-test" +local URI = require "uri" +local URIFile = require "uri.file" + +module("test.file", lunit.testcase, package.seeall) + +function test_normalize () + test_norm("file:///foo", "file://LocalHost/foo") + test_norm("file:///", "file://localhost/") + test_norm("file:///", "file://localhost") + test_norm("file:///", "file://") + test_norm("file:///", "file:/") + test_norm("file:///foo", "file:/foo") + test_norm("file://foo/", "file://foo") +end + +function test_invalid () + is_bad_uri("just scheme", "file:") + is_bad_uri("scheme with relative path", "file:foo/bar") +end + +function test_set_host () + local uri = assert(URI:new("file:///foo")) + is("", uri:host()) + is("", uri:host("LocalHost")) + is("file:///foo", tostring(uri)) + is("", uri:host("host.name")) + is("file://host.name/foo", tostring(uri)) + is("host.name", uri:host("")) + is("file:///foo", tostring(uri)) +end + +function test_set_path () + local uri = assert(URI:new("file:///foo")) + is("/foo", uri:path()) + is("/foo", uri:path(nil)) + is("file:///", tostring(uri)) + is("/", uri:path("")) + is("file:///", tostring(uri)) + is("/", uri:path("/bar/frob")) + is("file:///bar/frob", tostring(uri)) + is("/bar/frob", uri:path("/")) + is("file:///", tostring(uri)) +end + +function test_bad_usage () + local uri = assert(URI:new("file:///foo")) + assert_error("nil host", function () uri:host(nil) end) + assert_error("set userinfo", function () uri:userinfo("foo") end) + assert_error("set port", function () uri:userinfo(23) end) + assert_error("set relative path", function () uri:userinfo("foo/") end) +end + +local function uri_to_fs (os, uristr, expected) + local uri = assert(URI:new(uristr)) + is(expected, uri:filesystem_path(os)) +end + +local function fs_to_uri (os, path, expected) + is(expected, tostring(URIFile.make_file_uri(path, os))) +end + +function test_uri_to_fs_unix () + uri_to_fs("unix", "file:///", "/") + uri_to_fs("unix", "file:///c:", "/c:") + uri_to_fs("unix", "file:///C:/", "/C:/") + uri_to_fs("unix", "file:///C:/Program%20Files", "/C:/Program Files") + uri_to_fs("unix", "file:///C:/Program%20Files/", "/C:/Program Files/") + uri_to_fs("unix", "file:///Program%20Files/", "/Program Files/") +end + +function test_uri_to_fs_unix_bad () + -- On Unix platforms, there's no equivalent of UNC paths. + local uri = assert(URI:new("file://laptop/My%20Documents/FileSchemeURIs.doc")) + assert_error("Unix path with host name", + function () uri:filesystem_path("unix") end) + -- Unix paths can't contain null bytes or encoded slashes. + uri = assert(URI:new("file:///frob/foo%00bar/quux")) + assert_error("Unix path with null byte", + function () uri:filesystem_path("unix") end) + uri = assert(URI:new("file:///frob/foo%2Fbar/quux")) + assert_error("Unix path with encoded slash", + function () uri:filesystem_path("unix") end) +end + +function test_fs_to_uri_unix () + fs_to_uri("unix", "/", "file:///") + fs_to_uri("unix", "//", "file:///") + fs_to_uri("unix", "///", "file:///") + fs_to_uri("unix", "/foo/bar", "file:///foo/bar") + fs_to_uri("unix", "/foo/bar/", "file:///foo/bar/") + fs_to_uri("unix", "//foo///bar//", "file:///foo/bar/") + fs_to_uri("unix", "/foo bar/%2F", "file:///foo%20bar/%252F") +end + +function test_fs_to_uri_unix_bad () + -- Relative paths can't be converted to URIs, because URIs are inherently + -- absolute. + assert_error("relative Unix path", + function () URIFile.make_file_uri("foo/bar", "unix") end) + assert_error("relative empty Unix path", + function () URIFile.make_file_uri("", "unix") end) +end + +function test_uri_to_fs_win32 () + uri_to_fs("win32", "file:///", "\\") + uri_to_fs("win32", "file:///c:", "c:\\") + uri_to_fs("win32", "file:///C:/", "C:\\") + uri_to_fs("win32", "file:///C:/Program%20Files", "C:\\Program Files") + uri_to_fs("win32", "file:///C:/Program%20Files/", "C:\\Program Files\\") + uri_to_fs("win32", "file:///Program%20Files/", "\\Program Files\\") + -- http://blogs.msdn.com/ie/archive/2006/12/06/file-uris-in-windows.aspx + uri_to_fs("win32", "file://laptop/My%20Documents/FileSchemeURIs.doc", + "\\\\laptop\\My Documents\\FileSchemeURIs.doc") + uri_to_fs("win32", + "file:///C:/Documents%20and%20Settings/davris/FileSchemeURIs.doc", + "C:\\Documents and Settings\\davris\\FileSchemeURIs.doc") + -- For backwards compatibility with deprecated way of indicating drives. + uri_to_fs("win32", "file:///c%7C", "c:\\") + uri_to_fs("win32", "file:///c%7C/", "c:\\") + uri_to_fs("win32", "file:///C%7C/foo/", "C:\\foo\\") +end + +function test_fs_to_uri_win32 () + fs_to_uri("win32", "", "file:///") + fs_to_uri("win32", "\\", "file:///") + fs_to_uri("win32", "c:", "file:///c:/") + fs_to_uri("win32", "C:\\", "file:///C:/") + fs_to_uri("win32", "C:/", "file:///C:/") + fs_to_uri("win32", "C:\\Program Files", "file:///C:/Program%20Files") + fs_to_uri("win32", "C:\\Program Files\\", "file:///C:/Program%20Files/") + fs_to_uri("win32", "C:/Program Files/", "file:///C:/Program%20Files/") + fs_to_uri("win32", "\\Program Files\\", "file:///Program%20Files/") + fs_to_uri("win32", "\\\\laptop\\My Documents\\FileSchemeURIs.doc", + "file://laptop/My%20Documents/FileSchemeURIs.doc") + fs_to_uri("win32", "c:\\foo bar\\%2F", "file:///c:/foo%20bar/%252F") +end + +function test_convert_on_unknown_os () + local uri = assert(URI:new("file:///foo")) + assert_error("filesystem_path, unknown os", + function () uri:filesystem_path("NonExistent") end) + assert_error("make_file_uri, unknown os", + function () URIFile.make_file_uri("/foo", "NonExistent") end) +end + +-- vi:ts=4 sw=4 expandtab diff --git a/test/ftp.lua b/test/ftp.lua new file mode 100644 index 0000000..7a224bc --- /dev/null +++ b/test/ftp.lua @@ -0,0 +1,55 @@ +require "uri-test" +local URI = require "uri" + +module("test.ftp", lunit.testcase, package.seeall) + +function test_ftp () + local uri = assert(URI:new("ftp://ftp.example.com/path")) + is("ftp", uri:scheme()) + is("ftp.example.com", uri:host()) + is(21, uri:port()) + is(nil, uri:userinfo()) + is(nil, uri:username()) + is(nil, uri:password()) +end + +function test_ftp_typecode () + local uri = assert(URI:new("ftp://host/path")) + is(nil, uri:ftp_typecode()) + is(nil, uri:ftp_typecode("d")) + is("/path;type=d", uri:path()) + is("ftp://host/path;type=d", tostring(uri)) + is("d", uri:ftp_typecode("a")) + is("/path;type=a", uri:path()) + is("ftp://host/path;type=a", tostring(uri)) + is("a", uri:ftp_typecode("")) + is("/path", uri:path()) + is("ftp://host/path", tostring(uri)) + + local uri = assert(URI:new("ftp://host/path;type=xyzzy")) + is("/path;type=xyzzy", uri:path()) + is("ftp://host/path;type=xyzzy", tostring(uri)) + is("xyzzy", uri:ftp_typecode()) + is("xyzzy", uri:ftp_typecode(nil)) + is(nil, uri:ftp_typecode()) + is("/path", uri:path()) + is("ftp://host/path", tostring(uri)) +end + +function test_normalize_path () + local uri = assert(URI:new("ftp://host")) + is("ftp://host/", tostring(uri)) + is("/", uri:path("/foo")) + is("/foo", uri:path("")) + is("/", uri:path("/foo")) + is("/foo", uri:path(nil)) + is("/", uri:path()) +end + +function test_bad_host () + is_bad_uri("missing authority, just scheme", "ftp:") + is_bad_uri("missing authority, just scheme and path", "ftp:/foo") + is_bad_uri("empty host", "ftp:///foo") +end + +-- vi:ts=4 sw=4 expandtab diff --git a/test/http.lua b/test/http.lua new file mode 100644 index 0000000..09ec92c --- /dev/null +++ b/test/http.lua @@ -0,0 +1,67 @@ +require "uri-test" +local URI = require "uri" + +module("test.http", lunit.testcase, package.seeall) + +function test_http () + local uri = assert(URI:new("HTtp://FOo/Blah?Search#Id")) + is("uri.http", uri._NAME) + is("http://foo/Blah?Search#Id", uri:uri()) + is("http://foo/Blah?Search#Id", tostring(uri)) + is("http", uri:scheme()) + is("foo", uri:host()) + is(80, uri:port()) + is("/Blah", uri:path()) + is(nil, uri:userinfo()) + is("Search", uri:query()) + is("Id", uri:fragment()) +end + +function test_https () + local uri = assert(URI:new("HTtpS://FOo/Blah?Search#Id")) + is("uri.https", uri._NAME) + is("https://foo/Blah?Search#Id", uri:uri()) + is("https://foo/Blah?Search#Id", tostring(uri)) + is("https", uri:scheme()) + is("foo", uri:host()) + is(443, uri:port()) + is("/Blah", uri:path()) + is(nil, uri:userinfo()) + is("Search", uri:query()) + is("Id", uri:fragment()) +end + +function test_http_port () + local uri = assert(URI:new("http://example.com:8080/foo")) + is(8080, uri:port()) + local old = uri:port(1234) + is(8080, old) + is(1234, uri:port()) + is("http://example.com:1234/foo", tostring(uri)) + old = uri:port(80) + is(1234, old) + is(80, uri:port()) + is("http://example.com/foo", tostring(uri)) +end + +function test_normalize_port () + local uri = assert(URI:new("http://foo:80/")) + is("http://foo/", tostring(uri)) + is(80, uri:port()) + uri = assert(URI:new("http://foo:443/")) + is("http://foo:443/", tostring(uri)) + is(443, uri:port()) + uri = assert(URI:new("https://foo:443/")) + is("https://foo/", tostring(uri)) + is(443, uri:port()) + uri = assert(URI:new("https://foo:80/")) + is("https://foo:80/", tostring(uri)) + is(80, uri:port()) +end + +function test_set_userinfo () + local uri = assert(URI:new("http://host/path")) + assert_error("can't set userinfo", function () uri:userinfo("x") end) +end + +-- vi:ts=4 sw=4 expandtab diff --git a/test/pop.lua b/test/pop.lua new file mode 100644 index 0000000..10478b8 --- /dev/null +++ b/test/pop.lua @@ -0,0 +1,129 @@ +require "uri-test" +local URI = require "uri" + +module("test.pop", lunit.testcase, package.seeall) + +function test_pop_parse_1 () + local uri = assert(URI:new("Pop://rg@MAILSRV.qualcomm.COM")) + is("pop://rg@mailsrv.qualcomm.com", tostring(uri)) + is("pop", uri:scheme()) + is("rg", uri:userinfo()) + is("mailsrv.qualcomm.com", uri:host()) + is(110, uri:port()) + is("rg", uri:pop_user()) + is("*", uri:pop_auth()) +end + +function test_pop_parse_2 () + local uri = assert(URI:new("pop://rg;AUTH=+APOP@mail.eudora.com:8110")) + is("pop://rg;auth=+APOP@mail.eudora.com:8110", tostring(uri)) + is("rg;auth=+APOP", uri:userinfo()) + is("mail.eudora.com", uri:host()) + is(8110, uri:port()) + is("rg", uri:pop_user()) + is("+APOP", uri:pop_auth()) +end + +function test_pop_parse_3 () + local uri = assert(URI:new("pop://baz;AUTH=SCRAM-MD5@foo.bar")) + is("pop://baz;auth=SCRAM-MD5@foo.bar", tostring(uri)) + is("baz;auth=SCRAM-MD5", uri:userinfo()) + is("foo.bar", uri:host()) + is(110, uri:port()) + is("baz", uri:pop_user()) + is("SCRAM-MD5", uri:pop_auth()) +end + +function test_pop_normalize () + local uri = assert(URI:new("Pop://Baz;Auth=*@Foo.Bar:110")) + is("pop://Baz@foo.bar", tostring(uri)) + is("Baz", uri:userinfo()) + is("foo.bar", uri:host()) + is(110, uri:port()) + is("Baz", uri:pop_user()) + is("*", uri:pop_auth()) +end + +function test_pop_set_user () + local uri = assert(URI:new("pop://host")) + is(nil, uri:pop_user("foo ;bar")) + is("pop://foo%20%3Bbar@host", tostring(uri)) + assert_error("empty user not allowed", function () uri:pop_user("") end) + is("foo ;bar", uri:pop_user(nil)) + is(nil, uri:pop_user()) + is("pop://host", tostring(uri)) +end + +function test_pop_set_user_bad () + local uri = assert(URI:new("pop://foo@host")) + assert_error("empty user not allowed", function () uri:pop_user("") end) + is("foo", uri:pop_user()) + is("pop://foo@host", tostring(uri)) + uri = assert(URI:new("pop://foo;auth=+APOP@host")) + assert_error("user required when auth specified", + function () uri:pop_user(nil) end) + is("foo", uri:pop_user()) + is("+APOP", uri:pop_auth()) + is("pop://foo;auth=+APOP@host", tostring(uri)) +end + +function test_pop_set_auth () + local uri = assert(URI:new("pop://user@host")) + is("*", uri:pop_auth("foo ;bar")) + is("pop://user;auth=foo%20%3Bbar@host", tostring(uri)) + is("foo ;bar", uri:pop_auth("*")) + is("*", uri:pop_auth()) + is("pop://user@host", tostring(uri)) +end + +function test_pop_set_auth_bad () + local uri = assert(URI:new("pop://host")) + assert_error("auth not allowed without user", + function () uri:pop_auth("+APOP") end) + uri:pop_user("user") + assert_error("empty auth not allowed", function () uri:pop_auth("") end) + assert_error("nil auth not allowed", function () uri:pop_auth(nil) end) + is("pop://user@host", tostring(uri)) +end + +function test_pop_bad_syntax () + is_bad_uri("path not empty", "pop://foo@host/") + is_bad_uri("user empty", "pop://@host") + is_bad_uri("user empty with auth", "pop://;auth=+APOP@host") + is_bad_uri("auth empty", "pop://user;auth=@host") +end + +function test_set_userinfo () + local uri = assert(URI:new("pop://host")) + is(nil, uri:userinfo("foo ;bar")) + is("pop://foo%20%3Bbar@host", tostring(uri)) + is("foo%20%3Bbar", uri:userinfo("foo;auth=+APOP")) + is("pop://foo;auth=+APOP@host", tostring(uri)) + is("foo;auth=+APOP", uri:userinfo("foo;AUTH=+APOP")) + is("pop://foo;auth=+APOP@host", tostring(uri)) + is("foo;auth=+APOP", uri:userinfo("bar;auth=*")) + is("pop://bar@host", tostring(uri)) + is("bar", uri:userinfo(nil)) + is("pop://host", tostring(uri)) +end + +function test_set_userinfo_bad () + local uri = assert(URI:new("pop://host")) + assert_error("empty userinfo", function () uri:userinfo("") end) + assert_error("empty user with auth", + function () uri:userinfo(";auth=*") end) + assert_error("empty auth on its own", + function () uri:userinfo(";auth=") end) + assert_error("empty auth with user", + function () uri:userinfo("foo;auth=") end) +end + +function test_set_path () + local uri = assert(URI:new("pop://host")) + is("", uri:path("")) + is("", uri:path(nil)) + is("", uri:path()) + assert_error("non-empty path", function () uri:path("/") end) +end + +-- vi:ts=4 sw=4 expandtab diff --git a/test/rtsp.lua b/test/rtsp.lua new file mode 100644 index 0000000..9e6ed9b --- /dev/null +++ b/test/rtsp.lua @@ -0,0 +1,44 @@ +require "uri-test" +local URI = require "uri" + +module("test.rtsp", lunit.testcase, package.seeall) + +function test_rtsp () + local u = assert(URI:new("RTSP://MEDIA.EXAMPLE.COM:554/twister/audiotrack")) + is("rtsp://media.example.com/twister/audiotrack", tostring(u)) + is("media.example.com", u:host()) + is("/twister/audiotrack", u:path()) +end + +function test_rtspu () + local uri = assert(URI:new("rtspu://media.perl.com/f%C3%B4o.smi/")) + is("rtspu://media.perl.com/f%C3%B4o.smi/", tostring(uri)) + is("media.perl.com", uri:host()) + is("/f%C3%B4o.smi/", uri:path()) +end + +function test_switch_scheme () + -- Should be no problem switching between TCP and UDP URIs, because they + -- have the same syntax. + local uri = assert(URI:new("rtsp://media.example.com/twister/audiotrack")) + is("rtsp://media.example.com/twister/audiotrack", tostring(uri)) + is("rtsp", uri:scheme("rtspu")) + is("rtspu://media.example.com/twister/audiotrack", tostring(uri)) + is("rtspu", uri:scheme("rtsp")) + is("rtsp://media.example.com/twister/audiotrack", tostring(uri)) + is("rtsp", uri:scheme()) +end + +function test_rtsp_default_port () + local uri = assert(URI:new("rtsp://host/path/")) + is(554, uri:port()) + uri = assert(URI:new("rtspu://host/path/")) + is(554, uri:port()) + + is(554, uri:port(8554)) + is("rtspu://host:8554/path/", tostring(uri)) + is(8554, uri:port(554)) + is("rtspu://host/path/", tostring(uri)) +end + +-- vi:ts=4 sw=4 expandtab diff --git a/test/telnet.lua b/test/telnet.lua new file mode 100644 index 0000000..e009ac4 --- /dev/null +++ b/test/telnet.lua @@ -0,0 +1,141 @@ +require "uri-test" +local URI = require "uri" + +module("test.telnet", lunit.testcase, package.seeall) + +-- This tests the generic login stuff ('username' and 'password' methods, and +-- additional userinfo validation), as well as the stuff specific to telnet. + +function test_telnet () + local uri = assert(URI:new("telnet://telnet.example.com/")) + is("telnet://telnet.example.com/", uri:uri()) + is("telnet://telnet.example.com/", tostring(uri)) + is("uri.telnet", uri._NAME) + is("telnet", uri:scheme()) + is("telnet.example.com", uri:host()) + is("/", uri:path()) +end + +function test_telnet_normalize () + local uri = assert(URI:new("telnet://user:password@host.com")) + is("telnet://user:password@host.com/", tostring(uri)) + is("/", uri:path()) + is(23, uri:port()) + uri = assert(URI:new("telnet://user:password@host.com:23/")) + is("telnet://user:password@host.com/", tostring(uri)) + is("/", uri:path()) + is(23, uri:port()) +end + +function test_telnet_invalid () + is_bad_uri("no authority, empty path", "telnet:") + is_bad_uri("no authority, normal path", "telnet:/") + is_bad_uri("empty authority, empty path", "telnet://") + is_bad_uri("empty authority, normal path", "telnet:///") + is_bad_uri("bad path /x", "telnet://host/x") + is_bad_uri("bad path //", "telnet://host//") +end + +function test_telnet_set_path () + local uri = assert(URI:new("telnet://foo/")) + is("/", uri:path("/")) + is("/", uri:path("")) + is("/", uri:path(nil)) + is("/", uri:path()) +end + +function test_telnet_set_bad_path () + local uri = assert(URI:new("telnet://foo/")) + assert_error("bad path x", function () uri:path("x") end) + assert_error("bad path /x", function () uri:path("/x") end) + assert_error("bad path //", function () uri:path("//") end) +end + +-- These test the generic stuff in uri._login. Some of the examples are +-- directly from RFC 1738 section 3.1, but substituting 'telnet' for 'ftp'. +function test_telnet_userinfo () + local uri = assert(URI:new("telnet://host.com/")) + is(nil, uri:userinfo()) + is(nil, uri:username()) + is(nil, uri:password()) + uri = assert(URI:new("telnet://foo:bar@host.com/")) + is("foo:bar", uri:userinfo()) + is("foo", uri:username()) + is("bar", uri:password()) + uri = assert(URI:new("telnet://%3a%40:%3a%40@host.com/")) + is("%3A%40:%3A%40", uri:userinfo()) + is(":@", uri:username()) + is(":@", uri:password()) + uri = assert(URI:new("telnet://foo:@host.com/")) + is("foo:", uri:userinfo()) + is("foo", uri:username()) + is("", uri:password()) + uri = assert(URI:new("telnet://@host.com/")) + is("", uri:userinfo()) + is("", uri:username()) + is(nil, uri:password()) + uri = assert(URI:new("telnet://:@host.com/")) + is(":", uri:userinfo()) + is("", uri:username()) + is("", uri:password()) +end + +function test_telnet_set_userinfo () + local uri = assert(URI:new("telnet://host.com/")) + is(nil, uri:userinfo("")) + is("telnet://@host.com/", tostring(uri)) + is("", uri:userinfo(":")) + is("telnet://:@host.com/", tostring(uri)) + is(":", uri:userinfo("foo:")) + is("telnet://foo:@host.com/", tostring(uri)) + is("foo:", uri:userinfo(":bar")) + is("telnet://:bar@host.com/", tostring(uri)) + is(":bar", uri:userinfo("foo:bar")) + is("telnet://foo:bar@host.com/", tostring(uri)) + is("foo:bar", uri:userinfo()) +end + +function test_telnet_set_bad_userinfo () + local uri = assert(URI:new("telnet://host.com/")) + assert_error("more than one colon", function () uri:userinfo("x::y") end) + assert_error("invalid character", function () uri:userinfo("x/y") end) +end + +function test_telnet_set_username () + local uri = assert(URI:new("telnet://host.com/")) + is(nil, uri:username("foo")) + is(nil, uri:password()) + is("telnet://foo@host.com/", tostring(uri)) + is("foo", uri:username("x:y@z%")) + is(nil, uri:password()) + is("telnet://x%3Ay%40z%25@host.com/", tostring(uri)) + is("x:y@z%", uri:username("")) + is(nil, uri:password()) + is("telnet://@host.com/", tostring(uri)) + is("", uri:username(nil)) + is(nil, uri:password()) + is("telnet://host.com/", tostring(uri)) + is(nil, uri:username()) +end + +function test_telnet_set_password () + local uri = assert(URI:new("telnet://host.com/")) + is(nil, uri:password("foo")) + is("", uri:username()) + is("telnet://:foo@host.com/", tostring(uri)) + is("foo", uri:password("x:y@z%")) + is("", uri:username()) + is("telnet://:x%3Ay%40z%25@host.com/", tostring(uri)) + is("x:y@z%", uri:password("")) + is("", uri:username()) + is("telnet://:@host.com/", tostring(uri)) + is("", uri:password(nil)) + is("", uri:username()) + is("telnet://@host.com/", tostring(uri)) + is("", uri:username(nil)) + is(nil, uri:password(nil)) + is("telnet://host.com/", tostring(uri)) + is(nil, uri:password()) +end + +-- vi:ts=4 sw=4 expandtab diff --git a/test/urn-isbn.lua b/test/urn-isbn.lua new file mode 100644 index 0000000..fef8c4a --- /dev/null +++ b/test/urn-isbn.lua @@ -0,0 +1,110 @@ +require "uri-test" +local URI = require "uri" +local Util = require "uri._util" + +local have_isbn_module = Util.attempt_require("isbn") + +module("test.urn_isbn", lunit.testcase, package.seeall) + +function test_isbn () + -- Example from RFC 2288 + local u = URI:new("URN:ISBN:0-395-36341-1") + is(have_isbn_module and "urn:isbn:0-395-36341-1" or "urn:isbn:0395363411", + u:uri()) + is("urn", u:scheme()) + is("isbn", u:nid()) + is(have_isbn_module and "0-395-36341-1" or "0395363411", u:nss()) + is("0395363411", u:isbn_digits()) + + u = URI:new("URN:ISBN:0395363411") + is(have_isbn_module and "urn:isbn:0-395-36341-1" or "urn:isbn:0395363411", + u:uri()) + is("urn", u:scheme()) + is("isbn", u:nid()) + is(have_isbn_module and "0-395-36341-1" or "0395363411", u:nss()) + is("0395363411", u:isbn_digits()) + + if have_isbn_module then + local isbn = u:isbn() + assert_table(isbn) + is("0-395-36341-1", tostring(isbn)) + is("0", isbn:group_code()) + is("395", isbn:publisher_code()) + is("978-0-395-36341-6", tostring(isbn:as_isbn13())) + end + + assert_true(URI.eq("urn:isbn:088730866x", "URN:ISBN:0-88-73-08-66-X")) +end + +function test_set_nss () + local uri = assert(URI:new("urn:isbn:039-53-63411")) + is(have_isbn_module and "0-395-36341-1" or "0395363411", + uri:nss("088-7308-66x")) + is(have_isbn_module and "urn:isbn:0-88730-866-X" or "urn:isbn:088730866X", + tostring(uri)) + is(have_isbn_module and "0-88730-866-X" or "088730866X", uri:nss()) +end + +function test_set_bad_nss () + local uri = assert(URI:new("urn:ISBN:039-53-63411")) + assert_error("set NSS to non-string value", function () uri:nss({}) end) + assert_error("set NSS to empty", function () uri:nss("") end) + assert_error("set NSS to wrong length", function () uri:nss("123") end) + + -- None of that should have had any affect + is(have_isbn_module and "urn:isbn:0-395-36341-1" or "urn:isbn:0395363411", + tostring(uri)) + is(have_isbn_module and "0-395-36341-1" or "0395363411", uri:nss()) + is("0395363411", uri:isbn_digits()) + is("uri.urn.isbn", uri._NAME) +end + +function test_set_path () + local uri = assert(URI:new("urn:ISBN:039-53-63411")) + is(have_isbn_module and "isbn:0-395-36341-1" or "isbn:0395363411", + uri:path("ISbn:088-73-0866x")) + is(have_isbn_module and "urn:isbn:0-88730-866-X" or "urn:isbn:088730866X", + tostring(uri)) + + assert_error("bad path", function () uri:path("isbn:1234567") end) + is(have_isbn_module and "urn:isbn:0-88730-866-X" or "urn:isbn:088730866X", + tostring(uri)) + is(have_isbn_module and "isbn:0-88730-866-X" or "isbn:088730866X", + uri:path()) +end + +function test_isbn_setting_digits () + local u = assert(URI:new("URN:ISBN:0395363411")) + local old = u:isbn_digits("0-88730-866-x") + is("0395363411", old) + is("088730866X", u:isbn_digits()) + is(have_isbn_module and "0-88730-866-X" or "088730866X", u:nss()) + if have_isbn_module then + is("0-88730-866-X", tostring(u:isbn())) + end +end + +function test_isbn_setting_object () + if have_isbn_module then + local ISBN = require "isbn" + local u = assert(URI:new("URN:ISBN:0395363411")) + local old = u:isbn(ISBN:new("0-88730-866-x")) + assert_table(old) + is("0-395-36341-1", tostring(old)) + is("088730866X", u:isbn_digits()) + is("0-88730-866-X", u:nss()) + local new = u:isbn() + assert_table(new) + is("0-88730-866-X", tostring(new)) + end +end + +function test_illegal_isbn () + is_bad_uri("invalid characters", "urn:ISBN:abc") + if have_isbn_module then + is_bad_uri("bad checksum", "urn:isbn:0395363412") + is_bad_uri("wrong length", "urn:isbn:03953634101") + end +end + +-- vi:ts=4 sw=4 expandtab diff --git a/test/urn-issn.lua b/test/urn-issn.lua new file mode 100644 index 0000000..6b78e33 --- /dev/null +++ b/test/urn-issn.lua @@ -0,0 +1,110 @@ +require "uri-test" +local URI = require "uri" + +module("test.urn_issn", lunit.testcase, package.seeall) + +local good_issn_digits = { + "02613077", -- The Guardian + "14734966", -- Photography Monthly + + -- From the Wikipedia article on ISSN. + "03178471", + "15340481", + + -- From RFC 3044 section 5. + "0259000X", + "15601560", +} + +function test_parse_and_normalize () + local uri = assert(URI:new("urn:ISSN:1560-1560")) + is("uri.urn.issn", uri._NAME) + is("urn:issn:1560-1560", uri:uri()) + is("15601560", uri:issn_digits()) + uri = assert(URI:new("URN:Issn:0259-000X")) + is("urn:issn:0259-000X", uri:uri()) + is("0259000X", uri:issn_digits()) + uri = assert(URI:new("urn:issn:0259000x")) + is("urn:issn:0259-000X", uri:uri()) + is("0259000X", uri:issn_digits()) +end + +function test_bad_syntax () + is_bad_uri("too many digits", "urn:issn:026130707") + is_bad_uri("not enough digits", "urn:issn:0261377") + is_bad_uri("too many hyphens in middle", "urn:issn:0261--3077") + is_bad_uri("hyphen in wrong place", "urn:issn:026-13077") + is_bad_uri("X digit in wrong place", "urn:issn:025900X0") +end + +-- Try all the known-good sequences of digits with all possible checksums +-- other than the right one, to make sure they're all detected as errors. +function test_bad_checksum () + for _, issn in ipairs(good_issn_digits) do + local digits, good_checksum = issn:sub(1, 7), issn:sub(8, 8) + good_checksum = (good_checksum == "X") and 10 or tonumber(good_checksum) + for i = 0, 10 do + if i ~= good_checksum then + local urn = "urn:issn:" .. digits .. (i == 10 and "X" or i) + is_bad_uri("bad checksum in " .. urn, urn) + end + end + end +end + +function test_set_nss () + local uri = assert(URI:new("urn:issn:0261-3077")) + is("0261-3077", uri:nss("14734966")) + is("urn:issn:1473-4966", tostring(uri)) + is("1473-4966", uri:nss("0259-000x")) + is("urn:issn:0259-000X", tostring(uri)) + is("0259-000X", uri:nss()) +end + +function test_set_bad_nss () + local uri = assert(URI:new("urn:ISSN:02613077")) + assert_error("set NSS to non-string value", function () uri:nss({}) end) + assert_error("set NSS to empty", function () uri:nss("") end) + assert_error("set NSS to bad char", function () uri:nss("x") end) + + -- None of that should have had any affect + is("urn:issn:0261-3077", tostring(uri)) + is("0261-3077", uri:nss()) + is("02613077", uri:issn_digits()) + is("uri.urn.issn", uri._NAME) +end + +function test_set_path () + local uri = assert(URI:new("urn:ISSN:02613077")) + is("issn:0261-3077", uri:path("ISsn:14734966")) + is("urn:issn:1473-4966", tostring(uri)) + + assert_error("bad path", function () uri:path("issn:1234567") end) + is("urn:issn:1473-4966", tostring(uri)) + is("issn:1473-4966", uri:path()) +end + +function test_set_issn_digits () + local uri = assert(URI:new("urn:ISSN:0261-3077")) + is("02613077", uri:issn_digits(nil)) + local old = uri:issn_digits("14734966") + is("02613077", old) + is("14734966", uri:issn_digits()) + is("urn:issn:1473-4966", uri:uri()) + old = uri:issn_digits("0259-000x") + is("14734966", old) + is("0259000X", uri:issn_digits()) + is("urn:issn:0259-000X", uri:uri()) +end + +function test_set_bad_issn_digits () + local uri = assert(URI:new("urn:ISSN:0261-3077")) + assert_error("set ISSN with bad char", + function () uri:issn_digits("0261-3077Y") end) + assert_error("set ISSN with too many digits", + function () uri:issn_digits("0261-30770") end) + assert_error("set ISSN of empty string", + function () uri:issn_digits("") end) +end + +-- vi:ts=4 sw=4 expandtab diff --git a/test/urn-oid.lua b/test/urn-oid.lua new file mode 100644 index 0000000..f047404 --- /dev/null +++ b/test/urn-oid.lua @@ -0,0 +1,101 @@ +require "uri-test" +local URI = require "uri" + +module("test.urn_oid", lunit.testcase, package.seeall) + +function test_parse_and_normalize () + local uri = assert(URI:new("urn:OId:1.3.50403060.0.23")) + is("uri.urn.oid", uri._NAME) + is("urn:oid:1.3.50403060.0.23", uri:uri()) + is("urn:oid:1.3.50403060.0.23", tostring(uri)) + is("oid", uri:nid()) + is("1.3.50403060.0.23", uri:nss()) + is("oid:1.3.50403060.0.23", uri:path()) + assert_array_shallow_equal({ 1, 3, 50403060, 0, 23 }, uri:oid_numbers()) + + -- Examples from RFC 3061 section 3 + uri = assert(URI:new("urn:oid:1.3.6.1")) + is("urn:oid:1.3.6.1", tostring(uri)) + assert_array_shallow_equal({ 1, 3, 6, 1 }, uri:oid_numbers()) + uri = assert(URI:new("urn:oid:1.3.6.1.4.1")) + is("urn:oid:1.3.6.1.4.1", tostring(uri)) + assert_array_shallow_equal({ 1, 3, 6, 1, 4, 1 }, uri:oid_numbers()) + uri = assert(URI:new("urn:oid:1.3.6.1.2.1.27")) + is("urn:oid:1.3.6.1.2.1.27", tostring(uri)) + assert_array_shallow_equal({ 1, 3, 6, 1, 2, 1, 27 }, uri:oid_numbers()) + uri = assert(URI:new("URN:OID:0.9.2342.19200300.100.4")) + is("urn:oid:0.9.2342.19200300.100.4", tostring(uri)) + assert_array_shallow_equal({ 0, 9, 2342, 19200300, 100, 4 }, + uri:oid_numbers()) +end + +function test_bad_syntax () + is_bad_uri("empty nss", "urn:oid:") + is_bad_uri("bad character", "urn:oid:1.2.x.3") + is_bad_uri("missing number", "urn:oid:1.2..3") + is_bad_uri("leading zero", "urn:oid:1.2.03.3") + is_bad_uri("leading zero at start", "urn:oid:01.2.3.3") +end + +function test_set_nss () + local uri = assert(URI:new("urn:oid:0.1.23")) + is("0.1.23", uri:nss("1")) + is("urn:oid:1", tostring(uri)) + is("1", uri:nss("234252345.340.4.0")) + is("urn:oid:234252345.340.4.0", tostring(uri)) + is("234252345.340.4.0", uri:nss()) +end + +function test_set_bad_nss () + local uri = assert(URI:new("urn:OID:0.1.23")) + assert_error("set NSS to non-string value", function () uri:nss({}) end) + assert_error("set NSS to empty", function () uri:nss("") end) + assert_error("set NSS to bad char", function () uri:nss("x") end) + + -- None of that should have had any affect + is("urn:oid:0.1.23", tostring(uri)) + is("0.1.23", uri:nss()) + assert_array_shallow_equal({ 0, 1, 23 }, uri:oid_numbers()) + is("uri.urn.oid", uri._NAME) +end + +function test_set_path () + local uri = assert(URI:new("urn:OID:0.1.23")) + is("oid:0.1.23", uri:path("OId:23.1.0")) + is("urn:oid:23.1.0", tostring(uri)) + + assert_error("bad path", function () uri:path("oid:1.02") end) + is("urn:oid:23.1.0", tostring(uri)) + is("oid:23.1.0", uri:path()) +end + +function test_set_oid_numbers () + local uri = assert(URI:new("urn:oid:0.1.23")) + assert_array_shallow_equal({ 0, 1, 23 }, uri:oid_numbers({ 1 })) + is("urn:oid:1", tostring(uri)) + assert_array_shallow_equal({ 1 }, uri:oid_numbers({ 234252345, 340, 4, 0 })) + is("urn:oid:234252345.340.4.0", tostring(uri)) + assert_array_shallow_equal({ 234252345, 340, 4, 0 }, + uri:oid_numbers({ 23.42 })) + is("urn:oid:23", tostring(uri)) + assert_array_shallow_equal({ 23 }, uri:oid_numbers()) +end + +function test_set_bad_oid_numbers () + local uri = assert(URI:new("urn:OID:0.1.23")) + assert_error("set OID numbers to non-table value", + function () uri:oid_numbers("1") end) + assert_error("set OID to empty list of numbers", + function () uri:oid_numbers({}) end) + assert_error("set OID number to negative number", + function () uri:oid_numbers({ -23 }) end) + assert_error("set OID number array containing bad type", + function () uri:oid_numbers({ "x" }) end) + + -- None of that should have had any affect + is("urn:oid:0.1.23", tostring(uri)) + assert_array_shallow_equal({ 0, 1, 23 }, uri:oid_numbers()) + is("uri.urn.oid", uri._NAME) +end + +-- vi:ts=4 sw=4 expandtab diff --git a/test/urn.lua b/test/urn.lua new file mode 100644 index 0000000..d01db28 --- /dev/null +++ b/test/urn.lua @@ -0,0 +1,137 @@ +require "uri-test" +local URI = require "uri" + +module("test.urn", lunit.testcase, package.seeall) + +function test_urn_parsing () + local uri = assert(URI:new("urn:x-FOO-01239-:Nss")) + is("urn:x-foo-01239-:Nss", uri:uri()) + is("urn", uri:scheme()) + is("x-foo-01239-:Nss", uri:path()) + is("x-foo-01239-", uri:nid()) + is("Nss", uri:nss()) + is(nil, uri:userinfo()) + is(nil, uri:host()) + is(nil, uri:port()) + is(nil, uri:query()) + is(nil, uri:fragment()) +end + +function test_set_nss () + local uri = assert(URI:new("urn:x-FOO-01239-:Nss")) + is("Nss", uri:nss("FooBar")) + is("urn:x-foo-01239-:FooBar", tostring(uri)) + assert_error("bad NSS, empty", function () uri:nss("") end) + assert_error("bad NSS, illegal character", function () uri:nss('x"y') end) + is("urn:x-foo-01239-:FooBar", tostring(uri)) +end + +function test_bad_urn_syntax () + is_bad_uri("missing nid", "urn::bar") + is_bad_uri("hyphen at start of nid", "urn:-x-foo:bar") + is_bad_uri("plus in middle of nid", "urn:x+foo:bar") + is_bad_uri("underscore in middle of nid", "urn:x_foo:bar") + is_bad_uri("dot in middle of nid", "urn:x.foo:bar") + is_bad_uri("nid too long", "urn:x-012345678901234567890123456789x:bar") + is_bad_uri("reserved 'urn' nid", "urn:urn:bar") + is_bad_uri("missing nss", "urn:x-foo:") + is_bad_uri("bad char in nss", "urn:x-foo:bar&") + is_bad_uri("shoudn't have host part", "urn://foo.com/x-foo:bar") + is_bad_uri("shoudn't have query part", "urn:x-foo:bar?baz") +end + +function test_change_nid () + local urn = assert(URI:new("urn:x-foo:14734966")) + is("urn:x-foo:14734966", tostring(urn)) + is("x-foo", urn:nid()) + is("uri.urn", urn._NAME) + + -- x-foo -> x-bar + is("x-foo", urn:nid("X-BAR")) + is("x-bar", urn:nid()) + is("urn:x-bar:14734966", tostring(urn)) + is("uri.urn", urn._NAME) + + -- x-bar -> issn + is("x-bar", urn:nid("issn")) + is("issn", urn:nid()) + is("urn:issn:1473-4966", tostring(urn)) + is("uri.urn.issn", urn._NAME) + + -- issn -> x-foo + is("issn", urn:nid("x-foo")) + is("x-foo", urn:nid()) + is("urn:x-foo:1473-4966", tostring(urn)) + is("uri.urn", urn._NAME) +end + +function test_change_nid_bad () + local urn = assert(URI:new("urn:x-foo:frob")) + + -- Try changing the NID to something invalid + assert_error("bad NID 'urn'", function () urn:nid("urn") end) + assert_error("bad NID '-x-foo'", function () urn:nid("-x-foo") end) + assert_error("bad NID 'x+foo'", function () urn:nid("x+foo") end) + + -- Change to valid NID, but where the NSS is not valid for it + assert_error("bad NSS for ISSN URN", function () urn:nid("issn") end) + + -- Original URN should be left unchanged + is("urn:x-foo:frob", tostring(urn)) + is("x-foo", urn:nid()) + is("uri.urn", urn._NAME) +end + +function test_change_path () + local urn = assert(URI:new("urn:x-foo:foopath")) + is("x-foo:foopath", urn:path()) + + -- x-foo -> x-bar + is("x-foo:foopath", urn:path("X-BAR:barpath")) + is("x-bar:barpath", urn:path()) + is("urn:x-bar:barpath", tostring(urn)) + is("uri.urn", urn._NAME) + + -- x-bar -> issn + is("x-bar:barpath", urn:path("issn:14734966")) + is("issn:1473-4966", urn:path()) + is("urn:issn:1473-4966", tostring(urn)) + is("uri.urn.issn", urn._NAME) + + -- issn -> x-foo + is("issn:1473-4966", urn:path("x-foo:foopath2")) + is("x-foo:foopath2", urn:path()) + is("urn:x-foo:foopath2", tostring(urn)) + is("uri.urn", urn._NAME) +end + +function test_change_path_bad () + local urn = assert(URI:new("urn:x-foo:frob")) + + -- Try changing the NID to something invalid + assert_error("bad NID 'urn'", function () urn:path("urn:frob") end) + assert_error("bad NID '-x-foo'", function () urn:path("-x-foo:frob") end) + assert_error("bad NID 'x+foo'", function () urn:path("x+foo:frob") end) + assert_error("bad NSS, empty", function () urn:path("x-foo:") end) + assert_error("bad NSS, bad char", function () urn:path('x-foo:x"y') end) + + -- Change to valid NID, but where the NSS is not valid for it + assert_error("bad NSS for ISSN URN", function () urn:path("issn:frob") end) + + -- Original URN should be left unchanged + is("urn:x-foo:frob", tostring(urn)) + is("x-foo:frob", urn:path()) + is("x-foo", urn:nid()) + is("frob", urn:nss()) + is("uri.urn", urn._NAME) +end + +function test_set_disallowed_stuff () + local urn = assert(URI:new("urn:x-foo:frob")) + assert_error("can't set userinfo", function () urn:userinfo("x") end) + assert_error("can't set host", function () urn:host("x") end) + assert_error("can't set port", function () urn:port(23) end) + assert_error("can't set query", function () urn:query("x") end) +end + +-- vi:ts=4 sw=4 expandtab diff --git a/uri-test.lua b/uri-test.lua new file mode 100644 index 0000000..af2cee0 --- /dev/null +++ b/uri-test.lua @@ -0,0 +1,85 @@ +require "lunit" +local URI = require "uri" + +is = lunit.assert_equal + +function is_one_of (expecteds, actual, msg) + for _, v in ipairs(expecteds) do + if actual == v then return end + end + + -- Not any of the expected answers matched. In order to report the error + -- usefully, we have to list the alternatives in the error message. + local err = "expected one of {" + for i, v in ipairs(expecteds) do + if i > 1 then err = err .. ", " end + err = err .. "'" .. tostring(v) .. "'" + end + err = err .. "}, but was '" .. tostring(actual) .. "'" + if msg then err = err .. ": " .. msg end + lunit.fail(err) +end + +function assert_isa(actual, class) + lunit.assert_table(actual) + lunit.assert_table(class) + local mt = actual + while true do + mt = getmetatable(mt) + if not mt then error"class not found as metatable at any level" end + if mt == actual then error"circular metatables" end + if mt == class then return nil end + end +end + +function assert_array_shallow_equal (expected, actual, msg) + if not msg then msg = "assert_array_shallow_equal" end + lunit.assert_table(actual, msg .. ", is table") + is(#expected, #actual, msg .. ", same size") + if #expected == #actual then + for i = 1, #expected do + is(expected[i], actual[i], msg .. ", element " .. i) + end + end + for key in pairs(actual) do + lunit.assert_number(key, msg .. ", non-number key in array") + end +end + +local function _count_hash_pairs (hash) + local count = 0 + for _, _ in pairs(hash) do count = count + 1 end + return count +end + +function assert_hash_shallow_equal (expected, actual, msg) + if not msg then msg = "assert_hash_shallow_equal" end + lunit.assert_table(actual, msg .. ", is table") + local expsize, actualsize = _count_hash_pairs(expected), + _count_hash_pairs(actual) + is(expsize, actualsize, msg .. ", same size") + if expsize == actualsize then + for k, v in pairs(expected) do + is(expected[k], actual[k], msg .. ", element " .. tostring(k)) + end + end +end + +function is_bad_uri (msg, uri) + local ok, err = URI:new(uri) + lunit.assert_nil(ok, msg) + lunit.assert_string(err, msg) +end + +function test_norm (expected, input) + local uri = assert(URI:new(input)) + is(expected, uri:uri()) + is(expected, tostring(uri)) + lunit.assert_false(uri:is_relative()) +end + +function test_norm_already (input) + test_norm(input, input) +end + +-- vi:ts=4 sw=4 expandtab diff --git a/uri.lua b/uri.lua new file mode 100644 index 0000000..7b1896e --- /dev/null +++ b/uri.lua @@ -0,0 +1,507 @@ +local M = { _NAME = "uri", VERSION = "1.0" } +M.__index = M + +local Util = require "uri._util" + +local _UNRESERVED = "A-Za-z0-9%-._~" +local _GEN_DELIMS = ":/?#%[%]@" +local _SUB_DELIMS = "!$&'()*+,;=" +local _RESERVED = _GEN_DELIMS .. _SUB_DELIMS +local _USERINFO = "^[" .. _UNRESERVED .. "%%" .. _SUB_DELIMS .. ":]*$" +local _REG_NAME = "^[" .. _UNRESERVED .. "%%" .. _SUB_DELIMS .. "]*$" +local _IP_FUTURE_LITERAL = "^v[0-9A-Fa-f]+%." .. + "[" .. _UNRESERVED .. _SUB_DELIMS .. "]+$" +local _QUERY_OR_FRAG = "^[" .. _UNRESERVED .. "%%" .. _SUB_DELIMS .. ":@/?]*$" +local _PATH_CHARS = "^[" .. _UNRESERVED .. "%%" .. _SUB_DELIMS .. ":@/]*$" + +local function _normalize_percent_encoding (s) + if s:find("%%$") or s:find("%%.$") then + error("unfinished percent encoding at end of URI '" .. s .. "'", 3) + end + + return s:gsub("%%(..)", function (hex) + if not hex:find("^[0-9A-Fa-f][0-9A-Fa-f]$") then + error("invalid percent encoding '%" .. hex .. + "' in URI '" .. s .. "'", 5) + end + + -- Never percent-encode unreserved characters, and always use uppercase + -- hexadecimal for percent encoding. RFC 3986 section 6.2.2.2. + local char = string.char(tonumber("0x" .. hex)) + return char:find("^[" .. _UNRESERVED .. "]") and char or "%" .. hex:upper() + end) +end + +local function _is_ip4_literal (s) + if not s:find("^[0-9]+%.[0-9]+%.[0-9]+%.[0-9]+$") then return false end + + for dec_octet in s:gmatch("[0-9]+") do + if dec_octet:len() > 3 or dec_octet:find("^0.") or + tonumber(dec_octet) > 255 then + return false + end + end + + return true +end + +local function _is_ip6_literal (s) + local had_elipsis = false -- true when '::' found + local num_chunks = 0 + while s ~= "" do + num_chunks = num_chunks + 1 + local p1, p2 = s:find("::?") + local chunk + if p1 then + chunk = s:sub(1, p1 - 1) + s = s:sub(p2 + 1) + if p2 ~= p1 then -- found '::' + if had_elipsis then return false end -- two of '::' + had_elipsis = true + if chunk == "" then num_chunks = num_chunks - 1 end + else + if chunk == "" then return false end -- ':' at start + if s == "" then return false end -- ':' at end + end + else + chunk = s + s = "" + end + + -- Chunk is neither 4-digit hex num, nor IPv4address in last chunk. + if (not chunk:find("^[0-9a-f]+$") or chunk:len() > 4) and + (s ~= "" or not _is_ip4_literal(chunk)) and + chunk ~= "" then + return false + end + + -- IPv4address in last position counts for two chunks of hex digits. + if chunk:len() > 4 then num_chunks = num_chunks + 1 end + end + + if had_elipsis then + if num_chunks > 7 then return false end + else + if num_chunks ~= 8 then return false end + end + + return true +end + +local function _is_valid_host (host) + if host:find("^%[.*%]$") then + local ip_literal = host:sub(2, -2) + if ip_literal:find("^v") then + if not ip_literal:find(_IP_FUTURE_LITERAL) then + return "invalid IPvFuture literal '" .. ip_literal .. "'" + end + else + if not _is_ip6_literal(ip_literal) then + return "invalid IPv6 address '" .. ip_literal .. "'" + end + end + elseif not _is_ip4_literal(host) and not host:find(_REG_NAME) then + return "invalid host value '" .. host .. "'" + end + + return nil +end + +local function _normalize_and_check_path (s, normalize) + if not s:find(_PATH_CHARS) then return false end + if not normalize then return s end + + -- Remove unnecessary percent encoding for path values. + -- TODO - I think this should be HTTP-specific (probably file also). + --s = Util.uri_decode(s, _SUB_DELIMS .. ":@") + + return Util.remove_dot_segments(s) +end + +function M.new (class, uri, base) + if not class or not uri then + error("usage: URI:new(uristring, [baseuri])", 2) + end + if type(uri) ~= "string" then uri = tostring(uri) end + + if base then + local uri, err = M.new(class, uri) + if not uri then return nil, err end + if type(base) ~= "table" then + base, err = M.new(class, base) + if not base then return nil, "error parsing base URI: " .. err end + end + if base:is_relative() then return nil, "base URI must be absolute" end + local ok, err = pcall(uri.resolve, uri, base) + if not ok then return nil, err end + return uri + end + + local s = _normalize_percent_encoding(uri) + + local _, p + local scheme, authority, userinfo, host, port, path, query, fragment + + _, p, scheme = s:find("^([a-zA-Z][-+.a-zA-Z0-9]*):") + if scheme then + scheme = scheme:lower() + s = s:sub(p + 1) + end + + _, p, authority = s:find("^//([^/?#]*)") + if authority then + s = s:sub(p + 1) + + _, p, userinfo = authority:find("^([^@]*)@") + if userinfo then + if not userinfo:find(_USERINFO) then + return nil, "invalid userinfo value '" .. userinfo .. "'" + end + authority = authority:sub(p + 1) + end + + p, _, port = authority:find(":([0-9]*)$") + if port then + port = (port ~= "") and tonumber(port) or nil + authority = authority:sub(1, p - 1) + end + + host = authority:lower() + local err = _is_valid_host(host) + if err then return nil, err end + end + + _, p, path = s:find("^([^?#]*)") + if path ~= "" then + local normpath = _normalize_and_check_path(path, scheme) + if not normpath then return nil, "invalid path '" .. path .. "'" end + path = normpath + s = s:sub(p + 1) + end + + _, p, query = s:find("^%?([^#]*)") + if query then + s = s:sub(p + 1) + if not query:find(_QUERY_OR_FRAG) then + return nil, "invalid query value '?" .. query .. "'" + end + end + + _, p, fragment = s:find("^#(.*)") + if fragment then + if not fragment:find(_QUERY_OR_FRAG) then + return nil, "invalid fragment value '#" .. fragment .. "'" + end + end + + local o = { + _scheme = scheme, + _userinfo = userinfo, + _host = host, + _port = port, + _path = path, + _query = query, + _fragment = fragment, + } + setmetatable(o, scheme and class or (require "uri._relative")) + + return o:init() +end + +function M.uri (self, ...) + local uri = self._uri + + if not uri then + local scheme = self:scheme() + if scheme then + uri = scheme .. ":" + else + uri = "" + end + + local host, port, userinfo = self:host(), self._port, self:userinfo() + if host or port or userinfo then + uri = uri .. "//" + if userinfo then uri = uri .. userinfo .. "@" end + if host then uri = uri .. host end + if port then uri = uri .. ":" .. port end + end + + local path = self:path() + if uri == "" and path:find("^[^/]*:") then + path = "./" .. path + end + + uri = uri .. path + if self:query() then uri = uri .. "?" .. self:query() end + if self:fragment() then uri = uri .. "#" .. self:fragment() end + + self._uri = uri -- cache + end + + if select("#", ...) > 0 then + local new = ... + if not new then error("URI can't be set to nil", 2) end + local newuri, err = M:new(new) + if not newuri then + error("new URI string is invalid (" .. err .. ")", 2) + end + setmetatable(self, getmetatable(newuri)) + for k in pairs(self) do self[k] = nil end + for k, v in pairs(newuri) do self[k] = v end + end + + return uri +end + +function M.__tostring (self) return self:uri() end + +function M.eq (a, b) + if type(a) == "string" then a = assert(M:new(a)) end + if type(b) == "string" then b = assert(M:new(b)) end + return a:uri() == b:uri() +end + +function M.scheme (self, ...) + local old = self._scheme + + if select("#", ...) > 0 then + local new = ... + if not new then error("can't remove scheme from absolute URI", 2) end + if type(new) ~= "string" then new = tostring(new) end + if not new:find("^[a-zA-Z][-+.a-zA-Z0-9]*$") then + error("invalid scheme '" .. new .. "'", 2) + end + Util.do_class_changing_change(self, M, "scheme", new, + function (uri, new) uri._scheme = new end) + end + + return old +end + +function M.userinfo (self, ...) + local old = self._userinfo + + if select("#", ...) > 0 then + local new = ... + if new then + if not new:find(_USERINFO) then + error("invalid userinfo value '" .. new .. "'", 2) + end + new = _normalize_percent_encoding(new) + end + self._userinfo = new + if new and not self._host then self._host = "" end + self._uri = nil + end + + return old +end + +function M.host (self, ...) + local old = self._host + + if select("#", ...) > 0 then + local new = ... + if new then + new = tostring(new):lower() + local err = _is_valid_host(new) + if err then error(err, 2) end + else + if self._userinfo or self._port then + error("there must be a host if there is a userinfo or port," .. + " although it can be the empty string", 2) + end + end + self._host = new + self._uri = nil + end + + return old +end + +function M.port (self, ...) + local old = self._port or self:default_port() + + if select("#", ...) > 0 then + local new = ... + if new then + if type(new) == "string" then new = tonumber(new) end + if not new then error("port number must be a number", 2) end + if new < 0 then error("port number must not be negative", 2) end + local newint = new - new % 1 + if newint ~= new then error("port number not integer", 2) end + if new == self:default_port() then new = nil end + end + self._port = new + if new and not self._host then self._host = "" end + self._uri = nil + end + + return old +end + +function M.path (self, ...) + local old = self._path + + if select("#", ...) > 0 then + local new = ... or "" + new = _normalize_percent_encoding(new) + new = Util.uri_encode(new, "^A-Za-z0-9%-._~%%!$&'()*+,;=:@/") + if self._host then + if new ~= "" and not new:find("^/") then + error("path must begin with '/' when there is an authority", 2) + end + else + if new:find("^//") then new = "/%2F" .. new:sub(3) end + end + self._path = new + self._uri = nil + end + + return old +end + +function M.query (self, ...) + local old = self._query + + if select("#", ...) > 0 then + local new = ... + if new then + new = Util.uri_encode(new, "^" .. _UNRESERVED .. "%%" .. _SUB_DELIMS .. ":@/?") + end + self._query = new + self._uri = nil + end + + return old +end + +function M.fragment (self, ...) + local old = self._fragment + + if select("#", ...) > 0 then + local new = ... + if new then + new = Util.uri_encode(new, "^" .. _UNRESERVED .. "%%" .. _SUB_DELIMS .. ":@/?") + end + self._fragment = new + self._uri = nil + end + + return old +end + +function M.init (self) + local scheme_class + = Util.attempt_require("uri." .. self._scheme:gsub("[-+.]", "_")) + if scheme_class then + setmetatable(self, scheme_class) + if self._port and self._port == self:default_port() then + self._port = nil + end + -- Call the subclass 'init' method, if it has its own. + if scheme_class ~= M and self.init ~= M.init then + return self:init() + end + end + return self +end + +function M.default_port () return nil end +function M.is_relative () return false end +function M.resolve () end -- only does anything in uri._relative + +-- TODO - there should probably be an option or something allowing you to +-- choose between making a link relative whenever possible (always using a +-- relative path if the scheme and authority are the same as the base URI) or +-- just using a relative reference to make the link as small as possible, which +-- might meaning using a path of '/' instead if '../../../' or whatever. +-- This method's algorithm is loosely based on the one described here: +-- http://lists.w3.org/Archives/Public/uri/2007Sep/0003.html +function M.relativize (self, base) + if type(base) == "string" then base = assert(M:new(base)) end + + -- Leave it alone if we can't a relative URI, or if it would be a network + -- path reference. + if self._scheme ~= base._scheme or self._host ~= base._host or + self._port ~= base._port or self._userinfo ~= base._userinfo then + return + end + + local basepath = base._path + local oldpath = self._path + -- This is to avoid trying to make a URN or something relative, which + -- is likely to lead to grief. + if not basepath:find("^/") or not oldpath:find("^/") then return end + + -- Turn it into a relative reference. + self._uri = nil + self._scheme = nil + self._host = nil + self._port = nil + self._userinfo = nil + setmetatable(self, require "uri._relative") + + -- Use empty path if the path in the base URI is already correct. + if oldpath == basepath then + if self._query or not base._query then + self._path = "" + else + -- An empty URI reference leaves the query string in the base URI + -- unchanged, so to get a result with no query part we have to + -- have something in the relative path. + local _, _, lastseg = oldpath:find("/([^/]+)$") + if lastseg and lastseg:find(":") then lastseg = "./" .. lastseg end + self._path = lastseg or "." + end + return + end + + if oldpath == "/" or basepath == "/" then return end + + local basesegs = Util.split("/", basepath:sub(2)) + local oldsegs = Util.split("/", oldpath:sub(2)) + + if oldsegs[1] ~= basesegs[1] then return end + + table.remove(basesegs) + + while #oldsegs > 1 and #basesegs > 0 and oldsegs[1] == basesegs[1] do + table.remove(oldsegs, 1) + table.remove(basesegs, 1) + end + + local path_naked = true + local newpath = "" + while #basesegs > 0 do + table.remove(basesegs, 1) + newpath = newpath .. "../" + path_naked = false + end + + if path_naked and #oldsegs == 1 and oldsegs[1] == "" then + newpath = "./" + table.remove(oldsegs) + end + + while #oldsegs > 0 do + if path_naked then + if oldsegs[1]:find(":") then + newpath = newpath .. "./" + elseif #oldsegs > 1 and oldsegs[1] == "" and oldsegs[2] == "" then + newpath = newpath .. "/." + end + end + + newpath = newpath .. oldsegs[1] + path_naked = false + table.remove(oldsegs, 1) + if #oldsegs > 0 then newpath = newpath .. "/" end + end + + self._path = newpath +end + +return M +-- vi:ts=4 sw=4 expandtab diff --git a/uri/_login.lua b/uri/_login.lua new file mode 100644 index 0000000..3a531c6 --- /dev/null +++ b/uri/_login.lua @@ -0,0 +1,96 @@ +local M = { _NAME = "uri._login" } +local Util = require "uri._util" +local URI = require "uri" +Util.subclass_of(M, URI) + +-- Generic terminal logins. This is used as a base class for 'telnet' and +-- 'ftp' URL schemes. + +local function _valid_userinfo (userinfo) + if userinfo then + local colon = userinfo:find(":") + if colon and userinfo:find(":", colon + 1) then + return nil, "only one colon allowed in userinfo" + end + end + return true +end + +-- TODO - this is a bit of a hack because currently subclasses are required +-- to know whether their superclass has one of these that needs calling. +-- It should be called from 'init' before anything more specific is done, +-- and it has the same calling convention. +-- According to RFC 1738 there should be at most one colon in the userinfo. +-- I apply that restriction for schemes where it's used for a username/password +-- pair. +function M.init_base (self) + local host = self:host() + if not host or host == "" then + return nil, "host missing from login URI" + end + + local ok, err = _valid_userinfo(self:userinfo()) + if not ok then return nil, err end + + return self +end + +function M.userinfo (self, ...) + if select("#", ...) > 0 then + local ok, err = _valid_userinfo(...) + if not ok then error("invalid userinfo value (" .. err .. ")", 2) end + end + return M._SUPER.userinfo(self, ...) +end + +function M.username (self, ...) + local info = M._SUPER.userinfo(self) + local old, colon + if info then + local colon = info and info:find(":") + old = colon and info:sub(1, colon - 1) or info + old = Util.uri_decode(old) + end + + if select('#', ...) > 0 then + local pass = colon and info:sub(colon) or "" -- includes colon + local new = ... + if not new then + M._SUPER.userinfo(self, nil) + else + -- Escape anything that's not allowed in a userinfo, and also + -- colon, because that indicates the end of the username. + new = Util.uri_encode(new, "^A-Za-z0-9%-._~!$&'()*+,;=") + M._SUPER.userinfo(self, new .. pass) + end + end + + return old +end + +function M.password (self, ...) + local info = M._SUPER.userinfo(self) + local old, colon + if info then + colon = info and info:find(":") + old = colon and info:sub(colon + 1) or nil + if old then old = Util.uri_decode(old) end + end + + if select('#', ...) > 0 then + local new = ... + local user = colon and info:sub(1, colon - 1) or info + if not new then + M._SUPER.userinfo(self, user) + else + if not user then user = "" end + new = Util.uri_encode(new, "^A-Za-z0-9%-._~!$&'()*+,;=") + M._SUPER.userinfo(self, user .. ":" .. new) + end + end + + return old +end + +return M +-- vi:ts=4 sw=4 expandtab diff --git a/uri/_relative.lua b/uri/_relative.lua new file mode 100644 index 0000000..91cf63f --- /dev/null +++ b/uri/_relative.lua @@ -0,0 +1,81 @@ +local M = { _NAME = "uri._relative" } +local Util = require "uri._util" +local URI = require "uri" +Util.subclass_of(M, URI) + +-- There needs to be an 'init' method in this class, to because the base-class +-- one expects there to be a 'scheme' value. +function M.init (self) + return self +end + +function M.scheme (self, ...) + if select("#", ...) > 0 then + error("relative URI references can't have a scheme, perhaps you" .. + " need to resolve this against an absolute URI instead", 2) + end + return nil +end + +function M.is_relative () return true end + +-- This implements the algorithm from RFC 3986 section 5.2.3 +-- Note that this takes an additional argument which appears to be required +-- by the algorithm, but isn't shown when it is used in the RFC. +local function _merge_paths (base, r, base_has_auth) + if base_has_auth and base == "" then + return "/" .. r + end + + return base:gsub("[^/]+$", "", 1) .. r +end + +local function _do_resolve (self, base) + if type(base) == "string" then base = assert(URI:new(base)) end + setmetatable(self, URI) + + if self:host() or self:userinfo() or self:port() then + -- network path reference, just needs a scheme + self:path(Util.remove_dot_segments(self:path())) + self:scheme(base:scheme()) + return + end + + local path = self:path() + if path == "" then + self:path(base:path()) + if not self:query() then self:query(base:query()) end + else + if path:find("^/") then + self:path(Util.remove_dot_segments(path)) + else + local base_has_auth = base:host() or base:userinfo() or base:port() + local merged = _merge_paths(base:path(), path, base_has_auth) + self:path(Util.remove_dot_segments(merged)) + end + end + self:host(base:host()) + self:userinfo(base:userinfo()) + self:port(base:port()) + self:scheme(base:scheme()) +end + +function M.resolve (self, base) + local orig = tostring(self) + local ok, result = pcall(_do_resolve, self, base) + if ok then return end + + -- If the resolving causes an exception, it means that the resulting URI + -- would be invalid, so we restore self to its original state and rethrow + -- the exception. + local restored = assert(URI:new(orig)) + for k in pairs(self) do self[k] = nil end + for k, v in pairs(restored) do self[k] = v end + setmetatable(self, getmetatable(restored)) + error("resolved URI reference would be invalid: " .. result, 2) +end + +function M.relativize (self, base) end -- already relative + +return M +-- vi:ts=4 sw=4 expandtab diff --git a/uri/_util.lua b/uri/_util.lua new file mode 100644 index 0000000..40f9130 --- /dev/null +++ b/uri/_util.lua @@ -0,0 +1,130 @@ +local M = { _NAME = "uri._util" } + +local string_char, string_format = string.char, string.format + +-- Build a char->hex map +local escapes = {} +for i = 0, 255 do + escapes[string_char(i)] = string_format("%%%02X", i) +end +local function _encode_char (chr) return escapes[chr] end + +function M.uri_encode (text, patn) + if not text then return end + if not patn then + -- Default unsafe characters. RFC 2732 ^(uric - reserved) + -- TODO - this should be updated to the latest RFC. + patn = "^A-Za-z0-9%-_.!~*'()" + end + return (text:gsub("([" .. patn .. "])", _encode_char)) +end + +function M.uri_decode (str, patn) + -- Note from RFC1630: "Sequences which start with a percent sign + -- but are not followed by two hexadecimal characters are reserved + -- for future extension" + if not str then return end + if patn then patn = "[" .. patn .. "]" end + return (str:gsub("%%(%x%x)", function (hex) + local char = string_char(tonumber(hex, 16)) + return (patn and not char:find(patn)) and "%" .. hex or char + end)) +end + +-- This is the remove_dot_segments algorithm from RFC 3986 section 5.2.4. +-- The input buffer is 's', the output buffer 'path'. +function M.remove_dot_segments (s) + local path = "" + + while s ~= "" do + if s:find("^%.%.?/") then -- A + s = s:gsub("^%.%.?/", "", 1) + elseif s:find("^/%./") or s == "/." then -- B + s = s:gsub("^/%./?", "/", 1) + elseif s:find("^/%.%./") or s == "/.." then -- C + s = s:gsub("^/%.%./?", "/", 1) + if path:find("/") then + path = path:gsub("/[^/]*$", "", 1) + else + path = "" + end + elseif s == "." or s == ".." then -- D + s = "" + else -- E + local _, p, seg = s:find("^(/?[^/]*)") + s = s:sub(p + 1) + path = path .. seg + end + end + + return path +end + +-- TODO - wouldn't this be better as a method on string? s:split(patn) +function M.split (patn, s, max) + if s == "" then return {} end + + local i, j = 1, string.find(s, patn) + if not j then return { s } end + + local list = {} + while true do + if #list + 1 == max then list[max] = s:sub(i); return list end + list[#list + 1] = s:sub(i, j - 1) + i = j + 1 + j = string.find(s, patn, i) + if not j then + list[#list + 1] = s:sub(i) + break + end + end + return list +end + +function M.attempt_require (modname) + local ok, result = pcall(require, modname) + if ok then + return result + elseif type(result) == "string" and + result:find("module '.*' not found") then + return nil + else + error(result) + end +end + +function M.subclass_of (class, baseclass) + class.__index = class + class.__tostring = baseclass.__tostring + class._SUPER = baseclass + setmetatable(class, baseclass) +end + +function M.do_class_changing_change (uri, baseclass, changedesc, newvalue, + changefunc) + local tmpuri = {} + setmetatable(tmpuri, baseclass) + for k, v in pairs(uri) do tmpuri[k] = v end + changefunc(tmpuri, newvalue) + tmpuri._uri = nil + + local foo, err = tmpuri:init() + if not foo then + error("URI not valid after " .. changedesc .. " changed to '" .. + newvalue .. "': " .. err, 3) + end + + setmetatable(uri, getmetatable(tmpuri)) + for k in pairs(uri) do uri[k] = nil end + for k, v in pairs(tmpuri) do uri[k] = v end +end + +function M.uri_part_not_allowed (class, method) + class[method] = function (self, new) + if new then error(method .. " not allowed on this kind of URI") end + return self["_" .. method] + end +end + +return M +-- vi:ts=4 sw=4 expandtab diff --git a/uri/data.lua b/uri/data.lua new file mode 100644 index 0000000..39916af --- /dev/null +++ b/uri/data.lua @@ -0,0 +1,116 @@ +local M = { _NAME = "uri.data" } +local Util = require "uri._util" +local URI = require "uri" +Util.subclass_of(M, URI) + +-- This implements the 'data' scheme defined in RFC 2397. + +local Filter = Util.attempt_require("datafilter") + +local function _valid_base64 (data) return data:find("^[0-9a-zA-Z/+]*$") end + +local function _split_path (path) + local _, _, mediatype, data = path:find("^([^,]*),(.*)") + if not mediatype then return "must have comma in path" end + local base64 = false + if mediatype:find(";base64$") then + base64 = true + mediatype = mediatype:sub(1, -8) + end + if base64 and not _valid_base64(data) then + return "illegal character in base64 encoding" + end + return nil, mediatype, base64, data +end + +function M.init (self) + if M._SUPER.host(self) then + return nil, "data URIs may not have authority parts" + end + local err, mediatype, base64, data = _split_path(M._SUPER.path(self)) + if err then return nil, "invalid data URI (" .. err .. ")" end + return self +end + +function M.data_media_type (self, ...) + local _, old, base64, data = _split_path(M._SUPER.path(self)) + + if select('#', ...) > 0 then + local new = ... or "" + new = Util.uri_encode(new, "^A-Za-z0-9%-._~!$&'()*+;=:@/") + if base64 then new = new .. ";base64" end + M._SUPER.path(self, new .. "," .. data) + end + + if old ~= "" then + if old:find("^;") then old = "text/plain" .. old end + return Util.uri_decode(old) + else + return "text/plain;charset=US-ASCII" -- default type + end +end + +local function _urienc_len (s) + local num_unsafe_chars = s:gsub("[A-Za-z0-9%-._~!$&'()*+,;=:@/]", ""):len() + local num_safe_chars = s:len() - num_unsafe_chars + return num_safe_chars + num_unsafe_chars * 3 +end + +local function _base64_len (s) + local num_blocks = (s:len() + 2) / 3 + num_blocks = num_blocks - num_blocks % 1 + return num_blocks * 4 + + 7 -- because of ";base64" marker +end + +local function _do_filter (algorithm, input) + return Filter[algorithm](input) +end + +function M.data_bytes (self, ...) + local _, mediatype, base64, old = _split_path(M._SUPER.path(self)) + if base64 then + if not Filter then + error("'datafilter' Lua module required to decode base64 data", 2) + end + old = _do_filter("base64_decode", old) + else + old = Util.uri_decode(old) + end + + if select('#', ...) > 0 then + local new = ... or "" + local urienc_len = _urienc_len(new) + local base64_len = _base64_len(new) + if base64_len < urienc_len and Filter then + mediatype = mediatype .. ";base64" + new = _do_filter("base64_encode", new) + else + new = new:gsub("%%", "%%25") + end + M._SUPER.path(self, mediatype .. "," .. new) + end + + return old +end + +function M.path (self, ...) + local old = M._SUPER.path(self) + + if select('#', ...) > 0 then + local new = ... + if not new then error("there must be a path in a data URI") end + local err = _split_path(new) + if err then error("invalid data URI (" .. err .. ")") end + M._SUPER.path(self, new) + end + + return old +end + +Util.uri_part_not_allowed(M, "userinfo") +Util.uri_part_not_allowed(M, "host") +Util.uri_part_not_allowed(M, "port") + +return M +-- vi:ts=4 sw=4 expandtab diff --git a/uri/file.lua b/uri/file.lua new file mode 100644 index 0000000..214cf8d --- /dev/null +++ b/uri/file.lua @@ -0,0 +1,72 @@ +local M = { _NAME = "uri.file" } +local Util = require "uri._util" +local URI = require "uri" +Util.subclass_of(M, URI) + +function M.init (self) + if self:userinfo() or self:port() then + return nil, "usernames and passwords are not allowed in HTTP URIs" + end + + local host = self:host() + local path = self:path() + if host then + if host:lower() == "localhost" then self:host("") end + else + if not path:find("^/") then + return nil, "file URIs must contain a host, even if it's empty" + end + self:host("") + end + + if path == "" then self:path("/") end + + return self +end + +function M.host (self, ...) + local old = M._SUPER.host(self) + + if select('#', ...) > 0 then + local new = ... + if not new then error("file URIs must have an authority part", 2) end + if new:lower() == "localhost" then new = "" end + M._SUPER.host(self, new) + end + + return old +end + +function M.path (self, ...) + local old = M._SUPER.path(self) + + if select('#', ...) > 0 then + local new = ... + if not new or new == "" then new = "/" end + M._SUPER.path(self, new) + end + + return old +end + +local function _os_implementation (os) + local FileImpl = Util.attempt_require("uri.file." .. os:lower(), 3) + if not FileImpl then + error("no file URI implementation for operating system " .. os) + end + return FileImpl +end + +function M.filesystem_path (self, os) + return _os_implementation(os).filesystem_path(self) +end + +function M.make_file_uri (path, os) + return _os_implementation(os).make_file_uri(path) +end + +Util.uri_part_not_allowed(M, "userinfo") +Util.uri_part_not_allowed(M, "port") + +return M +-- vi:ts=4 sw=4 expandtab diff --git a/uri/file/unix.lua b/uri/file/unix.lua new file mode 100644 index 0000000..55ce0f6 --- /dev/null +++ b/uri/file/unix.lua @@ -0,0 +1,27 @@ +local M = { _NAME = "uri.file.unix" } +local URI = require "uri" +local Util = require "uri._util" + +function M.filesystem_path (uri) + if uri:host() ~= "" then + error("a file URI with a host name can't be converted to a Unix path", + 2) + end + local path = uri:path() + if path:find("%%00") or path:find("%%2F") then + error("Unix paths cannot contain encoded null bytes or slashes", 2) + end + return Util.uri_decode(path) +end + +function M.make_file_uri (path) + if not path:find("^/") then + error("Unix relative paths can't be converted to file URIs", 2) + end + path = path:gsub("//+", "/") + path = Util.uri_encode(path, "^A-Za-z0-9%-._~!$&'()*+,;=:@/") + return assert(URI:new("file://" .. path)) +end + +return M +-- vi:ts=4 sw=4 expandtab diff --git a/uri/file/win32.lua b/uri/file/win32.lua new file mode 100644 index 0000000..bdacbe1 --- /dev/null +++ b/uri/file/win32.lua @@ -0,0 +1,34 @@ +local M = { _NAME = "uri.file.win32" } +local URI = require "uri" +local Util = require "uri._util" + +function M.filesystem_path (uri) + local host = uri:host() + local path = Util.uri_decode(uri:path()) + if host ~= "" then path = "//" .. host .. path end + if path:find("^/[A-Za-z]|/") or path:find("^/[A-Za-z]|$") then + path = path:gsub("|", ":", 1) + end + if path:find("^/[A-Za-z]:/") then + path = path:sub(2) + elseif path:find("^/[A-Za-z]:$") then + path = path:sub(2) .. "/" + end + path = path:gsub("/", "\\") + return path +end + +function M.make_file_uri (path) + if path:find("^[A-Za-z]:$") then path = path .. "\\" end + local _, _, host, hostpath = path:find("^\\\\([A-Za-z.]+)\\(.*)$") + host = host or "" + hostpath = hostpath or path + hostpath = hostpath:gsub("\\", "/") + :gsub("//+", "/") + hostpath = Util.uri_encode(hostpath, "^A-Za-z0-9%-._~!$&'()*+,;=:@/") + if not hostpath:find("^/") then hostpath = "/" .. hostpath end + return assert(URI:new("file://" .. host .. hostpath)) +end + +return M +-- vi:ts=4 sw=4 expandtab diff --git a/uri/ftp.lua b/uri/ftp.lua new file mode 100644 index 0000000..0fd055c --- /dev/null +++ b/uri/ftp.lua @@ -0,0 +1,54 @@ +local M = { _NAME = "uri.ftp" } +local Util = require "uri._util" +local LoginURI = require "uri._login" +Util.subclass_of(M, LoginURI) + +function M.default_port () return 21 end + +function M.init (self) + local err + self, err = M._SUPER.init_base(self) + if not self then return nil, err end + + local host = self:host() + if not host or host == "" then + return nil, "FTP URIs must have a hostname" + end + + -- I don't think there's any distinction in FTP URIs between empty path + -- and the root directory, so probably best to normalize as we do for HTTP. + if self:path() == "" then self:path("/") end + + return self +end + +function M.path (self, ...) + local old = M._SUPER.path(self) + + if select("#", ...) > 0 then + local new = ... + if not new or new == "" then new = "/" end + M._SUPER.path(self, new) + end + + return old +end + +function M.ftp_typecode (self, ...) + local path = M._SUPER.path(self) + local _, _, withouttype, old = path:find("^(.*);type=(.*)$") + if not withouttype then withouttype = path end + if old == "" then old = nil end + + if select("#", ...) > 0 then + local new = ... + if not new then new = "" end + if new ~= "" then new = ";type=" .. new end + M._SUPER.path(self, withouttype .. new) + end + + return old +end + +return M +-- vi:ts=4 sw=4 expandtab diff --git a/uri/http.lua b/uri/http.lua new file mode 100644 index 0000000..91f7a57 --- /dev/null +++ b/uri/http.lua @@ -0,0 +1,32 @@ +local M = { _NAME = "uri.http" } +local Util = require "uri._util" +local URI = require "uri" +Util.subclass_of(M, URI) + +-- This implementation is based on RFC 2616 section 3.2 and RFC 1738 +-- section 3.3. +-- +-- An HTTP URI with a 'userinfo' field is considered invalid, because it isn't +-- shown in the syntax given in RFC 2616, and is explicitly disallowed by +-- RFC 1738. + +function M.default_port () return 80 end + +function M.init (self) + if self:userinfo() then + return nil, "usernames and passwords are not allowed in HTTP URIs" + end + + -- RFC 2616 section 3.2.3 says that this is OK, but not that using the + -- redundant slash is canonical. I'm adding it because browsers tend to + -- treat the version with the extra slash as the normalized form, and + -- the initial slash is always present in an HTTP GET request. + if self:path() == "" then self:path("/") end + + return self +end + +Util.uri_part_not_allowed(M, "userinfo") + +return M +-- vi:ts=4 sw=4 expandtab diff --git a/uri/https.lua b/uri/https.lua new file mode 100644 index 0000000..0c4c8bc --- /dev/null +++ b/uri/https.lua @@ -0,0 +1,9 @@ +local M = { _NAME = "uri.https" } +local Util = require "uri._util" +local Http = require "uri.http" +Util.subclass_of(M, Http) + +function M.default_port () return 443 end + +return M +-- vi:ts=4 sw=4 expandtab diff --git a/uri/pop.lua b/uri/pop.lua new file mode 100644 index 0000000..8572779 --- /dev/null +++ b/uri/pop.lua @@ -0,0 +1,111 @@ +local M = { _NAME = "uri.pop" } +local URI = require "uri" +local Util = require "uri._util" +Util.subclass_of(M, URI) + +-- This is the set of characters must be encoded in a POP userinfo, which +-- unlike for other schemes includes the ';' character. +local _POP_USERINFO_ENCODE = "^A-Za-z0-9%-._~%%!$&'()*+,=:" + +function M.default_port () return 110 end + +local function _update_userinfo (self, old, new) + if new then + local _, _, user, auth = new:find("^(.*);[Aa][Uu][Tt][Hh]=(.*)$") + if not user then user = new end + if user == "" then return "pop user name must not be empty" end + user = Util.uri_encode(user, _POP_USERINFO_ENCODE) + if auth then + if auth == "" then return "pop auth type must not be empty" end + if auth == "*" then auth = nil end + auth = Util.uri_encode(auth, _POP_USERINFO_ENCODE) + end + new = user .. (auth and ";auth=" .. auth or "") + end + + if new ~= old then M._SUPER.userinfo(self, new) end + return nil +end + +function M.init (self) + if M._SUPER.path(self) ~= "" then + return nil, "pop URIs must have an empty path" + end + + local userinfo = M._SUPER.userinfo(self) + local err = _update_userinfo(self, userinfo, userinfo) + if err then return nil, err end + + return self +end + +function M.userinfo (self, ...) + local old = M._SUPER.userinfo(self) + + if select('#', ...) > 0 then + local new = ... + local err = _update_userinfo(self, old, new) + if err then error(err, 2) end + end + + return old +end + +function M.path (self, new) + if new and new ~= "" then error("POP URIs must have an empty path", 2) end + return "" +end + +local function _decode_userinfo (self) + local old = M._SUPER.userinfo(self) + if not old then return nil, nil end + local _, _, old_user, old_auth = old:find("^(.*);auth=(.*)$") + if not old_user then old_user = old end + return old_user, old_auth +end + +function M.pop_user (self, ...) + local old_user, old_auth = _decode_userinfo(self) + + if select('#', ...) > 0 then + local new = ... + if new == "" then error("pop user name must not be empty", 2) end + if not new and old_auth then + error("pop user name required when an auth type is specified", 2) + end + if new then + new = Util.uri_encode(new, _POP_USERINFO_ENCODE) + if old_auth then new = new .. ";auth=" .. old_auth end + end + M._SUPER.userinfo(self, new) + end + + return Util.uri_decode(old_user) +end + +function M.pop_auth (self, ...) + local old_user, old_auth = _decode_userinfo(self) + + if select('#', ...) > 0 then + local new = ... + if not new or new == "" + then error("pop auth type must not be empty", 2) + end + if new == "*" then new = nil end + if new and not old_user then + error("pop auth type can't be specified without user name", 2) + end + if new then + new = old_user .. ";auth=" .. + Util.uri_encode(new, _POP_USERINFO_ENCODE) + else + new = old_user + end + M._SUPER.userinfo(self, new) + end + + return old_auth and Util.uri_decode(old_auth) or "*" +end + +return M +-- vi:ts=4 sw=4 expandtab diff --git a/uri/rtsp.lua b/uri/rtsp.lua new file mode 100644 index 0000000..03c7148 --- /dev/null +++ b/uri/rtsp.lua @@ -0,0 +1,9 @@ +local M = { _NAME = "uri.rtsp" } +local Util = require "uri._util" +local HttpURI = require "uri.http" +Util.subclass_of(M, HttpURI) + +function M.default_port () return 554 end + +return M +-- vi:ts=4 sw=4 expandtab diff --git a/uri/rtspu.lua b/uri/rtspu.lua new file mode 100644 index 0000000..16f5e3e --- /dev/null +++ b/uri/rtspu.lua @@ -0,0 +1,7 @@ +local M = { _NAME = "uri.rtspu" } +local Util = require "uri._util" +local RtspURI = require "uri.rtsp" +Util.subclass_of(M, RtspURI) + +return M +-- vi:ts=4 sw=4 expandtab diff --git a/uri/telnet.lua b/uri/telnet.lua new file mode 100644 index 0000000..86b69b4 --- /dev/null +++ b/uri/telnet.lua @@ -0,0 +1,39 @@ +local M = { _NAME = "uri.telnet" } +local Util = require "uri._util" +local LoginURI = require "uri._login" +Util.subclass_of(M, LoginURI) + +function M.default_port () return 23 end + +function M.init (self) + local err + self, err = M._SUPER.init_base(self) + if not self then return nil, err end + + -- RFC 4248 does not discuss what a path longer than '/' might mean, and + -- there are no examples with anything significant in the path, so I'm + -- assuming that extra information in the path is not allowed. + local path = M._SUPER.path(self) + if path ~= "" and path ~= "/" then + return nil, "superfluous information in path of telnet URI" + end + + -- RFC 4248 section 2 says that the '/' can be omitted. I chose to + -- normalize to having it there, since the example shown in the RFC has + -- it, and this is consistent with the way I treat HTTP URIs. + if path == "" then self:path("/") end + + return self +end + +-- The path is always '/', so setting it won't do anything, but we do throw +-- an exception on an attempt to set it to anything invalid. +function M.path (self, new) + if new and new ~= "" and new ~= "/" then + error("invalid path for telnet URI", 2) + end + return "/" +end + +return M +-- vi:ts=4 sw=4 expandtab diff --git a/uri/urn.lua b/uri/urn.lua new file mode 100644 index 0000000..5f7cb60 --- /dev/null +++ b/uri/urn.lua @@ -0,0 +1,133 @@ +local M = { _NAME = "uri.urn" } +local Util = require "uri._util" +local URI = require "uri" +Util.subclass_of(M, URI) + +-- This implements RFC 2141, and attempts to change the class of the URI object +-- to one of its subclasses for further validation and normalization of the +-- namespace-specific string. + +-- Check NID syntax matches RFC 2141 section 2.1. +local function _valid_nid (nid) + if nid == "" then return nil, "missing completely" end + if nid:len() > 32 then return nil, "too long" end + if not nid:find("^[A-Za-z0-9][-A-Za-z0-9]*$") then + return nil, "contains illegal character" + end + if nid:lower() == "urn" then return nil, "'urn' is reserved" end + return true +end + +-- Check NSS syntax matches RFC 2141 section 2.2. +local function _valid_nss (nss) + if nss == "" then return nil, "can't be empty" end + if nss:find("[^A-Za-z0-9()+,%-.:=@;$_!*'/%%]") then + return nil, "contains illegal character" + end + return true +end + +local function _validate_and_normalize_path (path) + local _, _, nid, nss = path:find("^([^:]+):(.*)$") + if not nid then return nil, "illegal path syntax for URN" end + + local ok, msg = _valid_nid(nid) + if not ok then + return nil, "invalid namespace identifier (" .. msg .. ")" + end + ok, msg = _valid_nss(nss) + if not ok then + return nil, "invalid namespace specific string (" .. msg .. ")" + end + + return nid:lower() .. ":" .. nss +end + +-- TODO - this should check that percent-encoded bytes are valid UTF-8 +function M.init (self) + if M._SUPER.query(self) then + return nil, "URNs may not have query parts" + end + if M._SUPER.host(self) then + return nil, "URNs may not have authority parts" + end + + local path, msg = _validate_and_normalize_path(self:path()) + if not path then return nil, msg end + M._SUPER.path(self, path) + + local nid_class + = Util.attempt_require("uri.urn." .. self:nid():gsub("%-", "_")) + if nid_class then + setmetatable(self, nid_class) + if self.init ~= M.init then return self:init() end + end + + return self +end + +function M.nid (self, new) + local _, _, old = self:path():find("^([^:]+)") + + if new then + new = new:lower() + if new ~= old then + local ok, msg = _valid_nid(new) + if not ok then + error("invalid namespace identifier (" .. msg .. ")", 2) + end + end + Util.do_class_changing_change(self, M, "NID", new, function (uri, new) + M._SUPER.path(uri, new .. ":" .. uri:nss()) + end) + end + + return old +end + +function M.nss (self, new) + local _, _, old = self:path():find(":(.*)") + + if new and new ~= old then + local ok, msg = _valid_nss(new) + if not ok then + error("invalid namespace specific string (" .. msg .. ")", 2) + end + M._SUPER.path(self, self:nid() .. ":" .. new) + end + + return old +end + +function M.path (self, new) + local old = M._SUPER.path(self) + + if new and new ~= old then + local path, msg = _validate_and_normalize_path(new) + if not path then + error("invalid path for URN '" .. new .. "' (" ..msg .. ")", 2) + end + local _, _, newnid, newnss = path:find("^([^:]+):(.*)") + if not newnid then error("bad path for URN, no NID part found", 2) end + local ok, msg = _valid_nid(newnid) + if not ok then + error("invalid namespace identifier (" .. msg .. ")", 2) + end + if newnid:lower() == self:nid() then + self:nss(newnss) + else + Util.do_class_changing_change(self, M, "path", path, + function (uri, new) M._SUPER.path(uri, new) end) + end + end + + return old +end + +Util.uri_part_not_allowed(M, "userinfo") +Util.uri_part_not_allowed(M, "host") +Util.uri_part_not_allowed(M, "port") +Util.uri_part_not_allowed(M, "query") + +return M +-- vi:ts=4 sw=4 expandtab diff --git a/uri/urn/isbn.lua b/uri/urn/isbn.lua new file mode 100644 index 0000000..091d6a3 --- /dev/null +++ b/uri/urn/isbn.lua @@ -0,0 +1,67 @@ +local M = { _NAME = "uri.urn.isbn" } +local Util = require "uri._util" +local URN = require "uri.urn" +Util.subclass_of(M, URN) + +-- This implements the 'isbn' NID defined in RFC 3187, and is consistent +-- with the same NID suggested in RFC 2288. + +local function _valid_isbn (isbn) + if not isbn:find("^[-%d]+[%dXx]$") then return nil, "invalid character" end + local ISBN = Util.attempt_require("isbn") + if ISBN then return ISBN:new(isbn) end + return isbn +end + +local function _normalize_isbn (isbn) + isbn = isbn:gsub("%-", ""):upper() + local ISBN = Util.attempt_require("isbn") + if ISBN then return tostring(ISBN:new(isbn)) end + return isbn +end + +function M.init (self) + local nss = self:nss() + local ok, msg = _valid_isbn(nss) + if not ok then return nil, "invalid ISBN value (" .. msg .. ")" end + self:nss(_normalize_isbn(nss)) + return self +end + +function M.nss (self, new) + local old = M._SUPER.nss(self) + + if new then + local ok, msg = _valid_isbn(new) + if not ok then + error("bad ISBN value '" .. new .. "' (" .. msg .. ")", 2) + end + M._SUPER.nss(self, _normalize_isbn(new)) + end + + return old +end + +function M.isbn_digits (self, new) + local old = self:nss():gsub("%-", "") + + if new then + local ok, msg = _valid_isbn(new) + if not ok then + error("bad ISBN value '" .. new .. "' (" .. msg .. ")", 2) + end + self._SUPER.nss(self, _normalize_isbn(new)) + end + + return old +end + +function M.isbn (self, new) + local ISBN = require "isbn" + local old = ISBN:new(self:nss()) + if new then self:nss(tostring(new)) end + return old +end + +return M +-- vi:ts=4 sw=4 expandtab diff --git a/uri/urn/issn.lua b/uri/urn/issn.lua new file mode 100644 index 0000000..3eddfda --- /dev/null +++ b/uri/urn/issn.lua @@ -0,0 +1,65 @@ +local M = { _NAME = "uri.urn.issn" } +local Util = require "uri._util" +local URN = require "uri.urn" +Util.subclass_of(M, URN) + +local function _parse_issn (issn) + local _, _, nums1, nums2, checksum + = issn:find("^(%d%d%d%d)-?(%d%d%d)([%dxX])$") + if checksum == "x" then checksum = "X" end + return nums1, nums2, checksum +end + +local function _valid_issn (issn) + local nums1, nums2, actual_checksum = _parse_issn(issn) + if not nums1 then return nil, "invalid ISSN syntax" end + local nums = nums1 .. nums2 + + local expected_checksum = 0 + for i = 1, 7 do + expected_checksum = expected_checksum + tonumber(nums:sub(i, i)) * (9 - i) + end + expected_checksum = (11 - expected_checksum % 11) % 11 + expected_checksum = (expected_checksum == 10) and "X" + or tostring(expected_checksum) + if actual_checksum ~= expected_checksum then + return nil, "wrong checksum, expected " .. expected_checksum + end + + return true +end + +local function _normalize_issn (issn) + local nums1, nums2, checksum = _parse_issn(issn) + return nums1 .. "-" .. nums2 .. checksum +end + +function M.init (self) + local nss = self:nss() + local ok, msg = _valid_issn(nss) + if not ok then return nil, "bad NSS value for ISSN URI (" .. msg .. ")" end + M._SUPER.nss(self, _normalize_issn(nss)) + return self +end + +function M.nss (self, new) + local old = M._SUPER.nss(self) + + if new then + local ok, msg = _valid_issn(new) + if not ok then + error("bad ISSN value '" .. new .. "' (" .. msg .. ")", 2) + end + M._SUPER.nss(self, _normalize_issn(new)) + end + + return old +end + +function M.issn_digits (self, new) + local old = self:nss(new) + return old:sub(1, 4) .. old:sub(6, 9) +end + +return M +-- vi:ts=4 sw=4 expandtab diff --git a/uri/urn/oid.lua b/uri/urn/oid.lua new file mode 100644 index 0000000..22bd2f0 --- /dev/null +++ b/uri/urn/oid.lua @@ -0,0 +1,64 @@ +local M = { _NAME = "uri.urn.oid" } +local Util = require "uri._util" +local URN = require "uri.urn" +Util.subclass_of(M, URN) + +-- This implements RFC 3061. + +local function _valid_oid (oid) + if oid == "" then return nil, "OID can't be zero-length" end + if not oid:find("^[.0-9]*$") then return nil, "bad character in OID" end + if oid:find("%.%.") then return nil, "missing number in OID" end + if oid:find("^0[^.]") or oid:find("%.0[^.]") then + return nil, "OID numbers shouldn't have leading zeros" + end + return true +end + +function M.init (self) + local nss = self:nss() + local ok, msg = _valid_oid(nss) + if not ok then return nil, "bad NSS value for OID URI (" .. msg .. ")" end + return self +end + +function M.nss (self, new) + local old = M._SUPER.nss(self) + + if new then + local ok, msg = _valid_oid(new) + if not ok then + error("bad OID value '" .. new .. "' (" .. msg .. ")", 2) + end + M._SUPER.nss(self, new) + end + + return old +end + +function M.oid_numbers (self, new) + local old = Util.split("%.", self:nss()) + for i = 1, #old do old[i] = tonumber(old[i]) end + + if new then + if type(new) ~= "table" then error("expected array of numbers", 2) end + local nss = "" + for _, n in ipairs(new) do + if type(n) == "string" and n:find("^%d+$") then n = tonumber(n) end + if type(n) ~= "number" then + error("bad type for number in OID", 2) + end + n = n - n % 1 + if n < 0 then error("negative numbers not allowed in OID", 2) end + if nss ~= "" then nss = nss .. "." end + nss = nss .. n + end + if nss == "" then error("no numbers in new OID value", 2) end + self:nss(nss) + end + + return old +end + +return M +-- vi:ts=4 sw=4 expandtab