commit ed606df300289fa25782df839e06884e395af00e Author: Victor Seva Date: Thu Sep 26 17:29:04 2013 +0200 Imported Upstream version 0.1+20130926+git14fa255d 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