Commit Diff


commit - 19ed715d035acf481eb350478d486262d99314b2
commit + e0af76a7865fe55de80790d4129969c206394b58
blob - 28e9c68f37b47c27910971722aaf6032a9f213d6
blob + 5fc78a21d19735dd5d9eaa422635b667b3f1f2d7
--- .gitignore
+++ .gitignore
@@ -1,7 +1,6 @@
 syntax: glob
 cert/
 vostok/vostok
-tests/test_request
-tests/test_open_file
+tests/*.test
 **/*.swp
 **/*.o
blob - 69bd50cfc1aecd54f40ab90568eea2c82144fb52
blob + 6800f1b3e5a99f6cf38f3a3471d80983148f3e73
--- Makefile
+++ Makefile
@@ -1,4 +1,4 @@
-.PHONY: server clean run_server tests install
+.PHONY: server clean run tests install
 
 server:
 	${MAKE} -C vostok
@@ -7,7 +7,7 @@ clean:
 	${MAKE} -C vostok clean
 	${MAKE} -C tests clean
 
-run_server: server
+run: server
 	./vostok/vostok -c cert/server.crt -k cert/server.key -f ./
 
 tests:
blob - 0f3019466b5fcf8c9cc286c34515fa9ff67f0576
blob + 8fdc131294e9c9385f9a5f0e5a3691200b834325
--- tests/Makefile
+++ tests/Makefile
@@ -1,16 +1,20 @@
 .PHONY: tests clean
 
-CXXFLAGS	= -D_GLIBCXX_DEBUG -D_LIBCPP_DEBUG -DDEBUG -D_DEBUG -I../vostok
+CXXFLAGS	= -DDEBUG -D_DEBUG -D_LIBCPP_DEBUG=1 -D_GLIBCXX_DEBUG -I../vostok
 
-tests: test_request test_open_file
-	./test_request
-	./test_open_file
+tests: request.test open_file.test mime.test
+	./request.test
+	./open_file.test
+	./mime.test
 
-test_request: test_request.cc ../vostok/request.cc ../vostok/request.h ../vostok/gemini.cc
-	${CXX} ${CXXFLAGS} test_request.cc ../vostok/request.cc ../vostok/gemini.cc -o test_request
+request.test: test_request.cc ../vostok/request.cc ../vostok/request.h ../vostok/gemini.cc
+	${CXX} ${CXXFLAGS} test_request.cc ../vostok/request.cc ../vostok/gemini.cc -o request.test
 
-test_open_file: test_open_file.cc ../vostok/open_file.cc ../vostok/open_file.h
-	${CXX} ${CXXFLAGS} test_open_file.cc ../vostok/open_file.cc -o test_open_file
+open_file.test: test_open_file.cc ../vostok/open_file.cc ../vostok/open_file.h
+	${CXX} ${CXXFLAGS} test_open_file.cc ../vostok/open_file.cc -o open_file.test
 
+mime.test: test_mime.cc ../vostok/mime.cc ../vostok/mime.h
+	${CXX} ${CXXFLAGS} test_mime.cc ../vostok/mime.cc -o mime.test
+
 clean:
-	rm -f test_request test_open_file
+	rm -f *.test
blob - 490a7207b488c0f90940fda93e95a002abeb7a66
blob + 9417da2e6457b779766d062cf311ab409ea719fd
--- tests/test_open_file.cc
+++ tests/test_open_file.cc
@@ -1,5 +1,4 @@
 #include <string.h>
-#include <iostream>
 #include <sstream>
 #include <time.h>
 #include <unistd.h>
@@ -17,27 +16,6 @@ namespace error
 {
 std::ostream dev_null{nullptr};
 std::ostream &g_log = dev_null;
-}
-namespace
-{
-template <std::size_t N>
-void fill(std::vector<char> &v, const char (&arr)[N])
-{
-    v.resize(N);
-    std::copy(&arr[0], &arr[N], v.begin());
-}
-
-template <std::size_t N>
-bool is_tail(Span<const char> opened_path, const char (&arr)[N])
-{
-    if (opened_path.size() >= (N - 1))
-    {
-        opened_path = opened_path.subspan(opened_path.size() - (N - 1));
-        return std::equal(opened_path.begin(), opened_path.end(), &arr[0]);
-    }
-    return false;
-}
-
 }   // namespace <unnamed>
 
 TEST_START(test_open_file)
@@ -49,19 +27,20 @@ TEST_START(test_open_file)
     IS_TRUE_ERRNO(dir, "Open directory " << ss.str().c_str());
 
 
-    std::vector<char> sz_file_name;
-    Span<const char> opened_path;
+    std::string file_name;
+
     {
         UniqueFd opened_file;
+        const std::string *opened_path = nullptr;
 
-        fill(sz_file_name, "");
-        IS_TRUE(open_file(dir.get(), sz_file_name , opened_file, opened_path) == FILE_NOT_FOUND and !opened_file);
+        file_name = "";
+        IS_TRUE(open_file(dir.get(), file_name , opened_file, opened_path) == FILE_NOT_FOUND and !opened_file);
 
-        fill(sz_file_name, "non-existent-file");
-        IS_TRUE(open_file(dir.get(), sz_file_name, opened_file, opened_path) == FILE_NOT_FOUND and !opened_file);
+        file_name = "non-existent-file";
+        IS_TRUE(open_file(dir.get(), file_name, opened_file, opened_path) == FILE_NOT_FOUND and !opened_file);
 
-        fill(sz_file_name, "non-existent-dir/file");
-        IS_TRUE(open_file(dir.get(), sz_file_name, opened_file, opened_path) == FILE_NOT_FOUND and !opened_file);
+        file_name = "non-existent-dir/file";
+        IS_TRUE(open_file(dir.get(), file_name, opened_file, opened_path) == FILE_NOT_FOUND and !opened_file);
     }
 
     const UniqueFd index_gmi{openat(dir.get(), "index.gmi", O_RDONLY | O_CREAT | O_EXCL, S_IRWXU)};
@@ -71,10 +50,12 @@ TEST_START(test_open_file)
 
     {
         UniqueFd opened_file;
-        fill(sz_file_name, "");
-        IS_TRUE(open_file(dir.get(), sz_file_name, opened_file, opened_path) == FILE_OPENED and opened_file);
-        IS_TRUE(is_tail(opened_path, "index.gmi"));
+        const std::string *opened_path = nullptr;
 
+        file_name = "";
+        IS_TRUE(open_file(dir.get(), file_name, opened_file, opened_path) == FILE_OPENED and opened_file);
+        IS_TRUE(*opened_path == "index.gmi");
+
         struct stat sb2{};
         IS_TRUE_ERRNO(fstat(opened_file.get(), &sb2) != -1, "Stat file " << ss.str().c_str() << "/index.gmi");
         IS_TRUE((sb1.st_dev == sb2.st_dev) && (sb1.st_ino == sb2.st_ino));
@@ -82,9 +63,10 @@ TEST_START(test_open_file)
 
     {
         UniqueFd opened_file;
-        fill(sz_file_name, "index.gmi");
-        IS_TRUE(open_file(dir.get(), sz_file_name, opened_file, opened_path) == FILE_OPENED and opened_file);
-        IS_TRUE(is_tail(opened_path, "index.gmi"));
+        const std::string *opened_path = nullptr;
+        file_name = "index.gmi";
+        IS_TRUE(open_file(dir.get(), file_name, opened_file, opened_path) == FILE_OPENED and opened_file);
+        IS_TRUE(*opened_path == "index.gmi");
         struct stat sb2{};
         IS_TRUE_ERRNO(fstat(opened_file.get(), &sb2) != -1, "Stat file " << ss.str().c_str() << "/index.gmi");
         IS_TRUE((sb1.st_dev == sb2.st_dev) && (sb1.st_ino == sb2.st_ino));
@@ -93,10 +75,13 @@ TEST_START(test_open_file)
     IS_TRUE_ERRNO(mkdirat(dir.get(), "subdir", S_IRWXU) != -1,  "Create directory " << ss.str().c_str() << "/subdir");
     {
         UniqueFd opened_file;
-        fill(sz_file_name, "subdir");
-        IS_TRUE(open_file(dir.get(), sz_file_name, opened_file, opened_path) == FILE_NOT_FOUND and !opened_file);
-        fill(sz_file_name, "subdir/");
-        IS_TRUE(open_file(dir.get(), sz_file_name, opened_file, opened_path) == FILE_NOT_FOUND and !opened_file);
+        const std::string *opened_path = nullptr;
+
+        file_name = "subdir";
+        IS_TRUE(open_file(dir.get(), file_name, opened_file, opened_path) == FILE_NOT_FOUND and !opened_file);
+
+        file_name = "subdir/";
+        IS_TRUE(open_file(dir.get(), file_name, opened_file, opened_path) == FILE_NOT_FOUND and !opened_file);
     }
     const UniqueFd subdir{openat(dir.get(), "subdir", O_RDONLY | O_DIRECTORY)};
     IS_TRUE_ERRNO(subdir, "Open directory " << ss.str().c_str() << "/subdir");
@@ -107,9 +92,11 @@ TEST_START(test_open_file)
 
     {
         UniqueFd opened_file;
-        fill(sz_file_name, "subdir");
-        IS_TRUE(open_file(dir.get(), sz_file_name, opened_file, opened_path) == FILE_OPENED and opened_file);
-        IS_TRUE(is_tail(opened_path, "index.gmi"));
+        const std::string *opened_path = nullptr;
+
+        file_name = "subdir";
+        IS_TRUE(open_file(dir.get(), file_name, opened_file, opened_path) == FILE_OPENED and opened_file);
+        IS_TRUE(*opened_path == "index.gmi");
         struct stat sb2{};
         IS_TRUE_ERRNO(fstat(opened_file.get(), &sb2) != -1, "Stat file " << ss.str().c_str() << "/subdir/index.gmi");
         IS_TRUE((sb1.st_dev == sb2.st_dev) && (sb1.st_ino == sb2.st_ino));
@@ -117,9 +104,11 @@ TEST_START(test_open_file)
 
     {
         UniqueFd opened_file;
-        fill(sz_file_name, "subdir/");
-        IS_TRUE(open_file(dir.get(), sz_file_name, opened_file, opened_path) == FILE_OPENED and opened_file);
-        IS_TRUE(is_tail(opened_path, "index.gmi"));
+        const std::string *opened_path = nullptr;
+
+        file_name = "subdir/";
+        IS_TRUE(open_file(dir.get(), file_name, opened_file, opened_path) == FILE_OPENED and opened_file);
+        IS_TRUE(*opened_path == "index.gmi");
         struct stat sb2{};
         IS_TRUE_ERRNO(fstat(opened_file.get(), &sb2) != -1, "Stat file " << ss.str().c_str() << "/subdir/index.gmi");
         IS_TRUE((sb1.st_dev == sb2.st_dev) && (sb1.st_ino == sb2.st_ino));
@@ -127,9 +116,11 @@ TEST_START(test_open_file)
 
     {
         UniqueFd opened_file;
-        fill(sz_file_name, "subdir/index.gmi");
-        IS_TRUE(open_file(dir.get(), sz_file_name, opened_file, opened_path) == FILE_OPENED and opened_file);
-        IS_TRUE(is_tail(opened_path, "index.gmi"));
+        const std::string *opened_path = nullptr;
+
+        file_name = "subdir/index.gmi";
+        IS_TRUE(open_file(dir.get(), file_name, opened_file, opened_path) == FILE_OPENED and opened_file);
+        IS_TRUE(*opened_path ==  "subdir/index.gmi");
         struct stat sb2{};
         IS_TRUE_ERRNO(fstat(opened_file.get(), &sb2) != -1, "Stat file " << ss.str().c_str() << "/subdir/index.gmi");
         IS_TRUE((sb1.st_dev == sb2.st_dev) && (sb1.st_ino == sb2.st_ino));
@@ -140,8 +131,10 @@ TEST_START(test_open_file)
 
     {
         UniqueFd opened_file;
-        fill(sz_file_name, "subdir/EPERM.gmi");
-        IS_TRUE(open_file(dir.get(), sz_file_name, opened_file, opened_path) == FILE_OPENING_ERROR and !opened_file);
+        const std::string *opened_path = nullptr;
+
+        file_name = "subdir/EPERM.gmi";
+        IS_TRUE(open_file(dir.get(), file_name, opened_file, opened_path) == FILE_OPENING_ERROR and !opened_file);
     }
 
     IS_TRUE_ERRNO(unlinkat(subdir.get(), "EPERM.gmi", 0) != -1, "Remove file " << ss.str().c_str() << "/subdir/EPERM.gmi");
blob - /dev/null
blob + 77c955e6b386a8abd0811a768c9b61eaaf01d43e (mode 644)
--- /dev/null
+++ tests/test_mime.cc
@@ -0,0 +1,136 @@
+#include "mime.h"
+#include <cassert>
+
+#include "tests.h"
+
+
+namespace vostok
+{
+
+
+TEST_START(test_mime_get_ext)
+    std::string path{""};
+    auto it = Mime::get_ext(path);
+    IS_TRUE(it == path.end()); 
+
+    path = "a";
+    it = Mime::get_ext(path);
+    IS_TRUE(it == path.end()); 
+
+    path = "ab";
+    it = Mime::get_ext(path);
+    IS_TRUE(it == path.end()); 
+
+    path = ".";
+    it = Mime::get_ext(path);
+    IS_TRUE(it == path.end()); 
+
+    path = "/";
+    it = Mime::get_ext(path);
+    IS_TRUE(it == path.end()); 
+
+    path = "/.";
+    it = Mime::get_ext(path);
+    IS_TRUE(it == path.end()); 
+
+    path = "/a";
+    it = Mime::get_ext(path);
+    IS_TRUE(it == path.end()); 
+
+    path = "/ab";
+    it = Mime::get_ext(path);
+    IS_TRUE(it == path.end()); 
+
+    path = ".ext";
+    it = Mime::get_ext(path);
+    IS_TRUE(it != path.end() && std::string(&*it) == "ext"); 
+
+    path = "a.ext";
+    it = Mime::get_ext(path);
+    IS_TRUE(it != path.end() && std::string(&*it) == "ext"); 
+
+    path = "ab.ext";
+    it = Mime::get_ext(path);
+    IS_TRUE(it != path.end() && std::string(&*it) == "ext"); 
+
+    path = ".ext1.ext2";
+    it = Mime::get_ext(path);
+    IS_TRUE(it != path.end() && std::string(&*it) == "ext2"); 
+
+    path = "a.ext1.ext2";
+    it = Mime::get_ext(path);
+    IS_TRUE(it != path.end() && std::string(&*it) == "ext2"); 
+
+    path = "ab.ext1.ext2";
+    it = Mime::get_ext(path);
+    IS_TRUE(it != path.end() && std::string(&*it) == "ext2"); 
+
+    path = "xyz/.ext";
+    it = Mime::get_ext(path);
+    IS_TRUE(it != path.end() && std::string(&*it) == "ext"); 
+
+    path = "xyz/a.ext";
+    it = Mime::get_ext(path);
+    IS_TRUE(it != path.end() && std::string(&*it) == "ext"); 
+
+    path = "xyz/ab.ext";
+    it = Mime::get_ext(path);
+    IS_TRUE(it != path.end() && std::string(&*it) == "ext"); 
+
+    path = "xyz/.ext1.ext2";
+    it = Mime::get_ext(path);
+    IS_TRUE(it != path.end() && std::string(&*it) == "ext2"); 
+
+    path = "xyz/a.ext1.ext2";
+    it = Mime::get_ext(path);
+    IS_TRUE(it != path.end() && std::string(&*it) == "ext2"); 
+
+    path = "xyz/ab.ext1.ext2";
+    it = Mime::get_ext(path);
+    IS_TRUE(it != path.end() && std::string(&*it) == "ext2"); 
+TEST_END()
+
+
+TEST_START(test_mime_db)
+    Mime mime;
+
+    IS_TRUE(mime.parse_db("test_mime_types"));
+
+    auto ret = mime.lookup(".unk");
+    IS_TRUE(ret == nullptr);
+
+    ret = mime.lookup(".rss");
+    IS_TRUE(ret == nullptr);
+
+    ret = mime.lookup(".pd");
+    IS_TRUE(ret == nullptr);
+    ret = mime.lookup(".df");
+    IS_TRUE(ret == nullptr);
+    ret = mime.lookup(".pf");
+    IS_TRUE(ret == nullptr);
+
+    ret = mime.lookup(".pdf");
+    IS_TRUE(ret != nullptr && *ret == "application/pdf");
+
+    ret = mime.lookup(".ps");
+    IS_TRUE(ret != nullptr && *ret == "application/postscript");
+    ret = mime.lookup(".eps");
+    IS_TRUE(ret != nullptr && *ret == "application/postscript");
+    ret = mime.lookup(".ai");
+    IS_TRUE(ret != nullptr && *ret == "application/postscript");
+
+    ret = mime.lookup(".rtf");
+    IS_TRUE(ret != nullptr && *ret == "application/rtf");
+TEST_END()
+
+
+}   // namespace vostok
+
+
+extern "C"
+int
+main(int, char **)
+{
+    const auto ret = vostok::test_mime_get_ext();
+    return ret ? ret : vostok::test_mime_db();
+}
blob - 7a61653e524b22ec02a4d5d321f6ade1014aeb3e
blob + 8b266eeb9ade6abe27c9aba7f8dd16369f028e4f
--- tests/test_request.cc
+++ tests/test_request.cc
@@ -1,6 +1,5 @@
 #include "request.h"
-#include <string.h>
-#include <iostream>
+#include <cassert>
 
 #include "tests.h"
 
@@ -10,31 +9,39 @@ namespace vostok
 namespace
 {
 template <std::size_t N>
-void fill_request_buffer(std::vector<char> &v, const char (&arr)[N])
+std::string &fill_request_buffer(Request &r, const char (&arr)[N])
 {
     static_assert(N > 0, "N");
-    v.resize(N + 1);
-    auto p = std::copy(&arr[0], &arr[N - 1], v.begin());
+    auto &s = r.get_buffer();
+    assert(s.size() >= (N + 1));
+    auto p = std::copy(&arr[0], &arr[N - 1], s.begin());
     *p = '\r';
     ++p;
     *p = '\n';
     ++p;
+    s.resize(N + 1);
+    return s;
 }
 }   // namespace <unnamed>
 
 TEST_START(test_request)
     Request request;
+    std::string path;
 
-    auto &buffer = request.get_buffer();
-    IS_TRUE(buffer.size() == 1026);
+    {
+        auto &buffer = request.get_buffer();
+        IS_TRUE(buffer.size() == 1026);
+    }
 
-    fill_request_buffer(buffer, "gemini://host");
-    buffer[buffer.size() - 1] = '\0';
-    IS_TRUE(request.parse() == Request::BAD_REQUEST);
+    {
+        auto &buffer = fill_request_buffer(request, "gemini://host");
+        buffer[buffer.size() - 1] = '\0';
+        IS_TRUE(request.parse(path) == Request::BAD_REQUEST);
+    }
 
 #define CASE_ERROR(url_literal, expected_result) \
-    fill_request_buffer(request.get_buffer(), url_literal); \
-    IS_TRUE(request.parse() == expected_result)
+    fill_request_buffer(request, url_literal); \
+    IS_TRUE(request.parse(path) == expected_result)
 
     CASE_ERROR("", Request::URL_TOO_SHORT);
     CASE_ERROR("g", Request::URL_TOO_SHORT);
@@ -47,8 +54,9 @@ TEST_START(test_request)
     CASE_ERROR("gemini://host/dir/../../secret.txt", Request::URL_ROOT_TRAVERSE);
 
 #define CASE_OK(url_literal, path_literal) \
-    fill_request_buffer(request.get_buffer(), url_literal); \
-    IS_TRUE(request.parse() == Request::URL_OK && !strcmp(request.get_path().data(), path_literal))
+    fill_request_buffer(request, url_literal); \
+    IS_TRUE(request.parse(path) == Request::URL_OK); \
+    IS_TRUE(path == path_literal)
 
     CASE_OK("gemini://host", "");
     CASE_OK("gemini://host/", "");
blob - /dev/null
blob + 2437b1e2043e837bb56255a59b6de6e9011d3e06 (mode 644)
--- /dev/null
+++ tests/test_mime_types
@@ -0,0 +1,8 @@
+#application/rss+xml					rss
+
+application/pdf						pdf
+
+
+application/postscript					ps eps ai
+
+application/rtf						rtf
blob - f63e7b699fe5770773ae17075b21503eec03a33c
blob + e90e0c9567f320b98dda8251cc83251d8be4963a
--- tests/tests.h
+++ tests/tests.h
@@ -1,3 +1,5 @@
+#include <iostream>
+
 #pragma once
 
 #define TEST_START(name) int name() {
blob - 65f8d32addaca2f9201a5b5e06104c92c6b90769
blob + 9033a5cc11774e198b77ab33da9e8b3c988130ab
--- vostok/Makefile
+++ vostok/Makefile
@@ -1,16 +1,11 @@
 CXXFLAGS	+= -Wall -Wextra -std=c++11
 LIBS		= -ltls
 
-CXXFILES	= transport.cc
-CXXFILES	+= error.cc
-CXXFILES	+= gemini.cc
-CXXFILES	+= args.cc
-CXXFILES	+= request.cc
-CXXFILES	+= open_file.cc
-CXXFILES	+= vostok.cc
+CXXFILES	= transport.cc error.cc gemini.cc args.cc request.cc open_file.cc mime.cc vostok.cc
+HXXFILES	= args.h error.h gemini.h mime.h open_file.h request.h transport.h utils.h
 OFILES		= ${CXXFILES:.cc=.o}
 
-.cc.o:
+.cc.o:	${HXXFILES}
 	${CXX} ${CXXFLAGS} -c -o $@ $<
 
 vostok: ${OFILES}
blob - 19144e4a70be55173bbd017c1697f77ebf2669bc
blob + 1f1e2b7aecd1bb638f36b78758a34a75065213d9
--- vostok/args.cc
+++ vostok/args.cc
@@ -28,6 +28,7 @@ bool usage(const char *program)
     error::g_log << "\t-c FILE : Server certificate file [REQUIRED]" << std::endl;
     error::g_log << "\t-k FILE : Server key file [REQUIRED]" << std::endl;
     error::g_log << "\t-f PATH : Path to file system data [REQUIRED]" << std::endl;
+    error::g_log << "\t-m FILE : Path to file mime.types" << std::endl;
 
     return false;
 }
@@ -44,7 +45,7 @@ parse_command_line_arguments(
 {
     int ch;
     char *p = nullptr;
-    while ((ch = getopt(argc, argv, "a:p:c:k:f:")) != -1) {
+    while ((ch = getopt(argc, argv, "a:p:c:k:f:m:")) != -1) {
         switch (ch) {
         case 'a':
             args.addr = optarg;
@@ -78,6 +79,10 @@ parse_command_line_arguments(
                 return false;
             }
             break;
+        case 'm':
+            if (!args.mime.parse_db(optarg))
+                return false;
+            break;
 
         default:
             return usage(argv[0]);
blob - 0d5d970247a1b2947a8fb3f7fb034b373f784656
blob + a9a2b6a44858304b3e76f8adca2f76bcc73105fb
--- vostok/args.h
+++ vostok/args.h
@@ -1,6 +1,7 @@
 /** Parse command line arguments */
 
 #include "utils.h"
+#include "mime.h"
 
 #pragma once
 
@@ -16,6 +17,7 @@ struct CommandLineArguments
     czstring cert_file{nullptr};
     czstring key_file{nullptr};
     UniqueFd directory;
+    Mime mime;
 };
 
 
blob - bf7fc548d06c4122ff18d59004d8010ca6c8636c
blob + 7e53ac30ac83be704bad5b3eb71ab4cf0ab4c44e
--- vostok/open_file.cc
+++ vostok/open_file.cc
@@ -9,39 +9,26 @@
 
 namespace vostok
 {
-namespace
-{
-const char index_gmi[] = "index.gmi";
-}   // namespace <unnamed>
+static const std::string index_gmi{"index.gmi"};
 
 
 OpenFileResult
 open_file(
     /* in */ int directory_fd,
-    /* in */ const std::vector<char> &sz_file_name,
+    /* in */ const std::string &request_path,
     /* out */ UniqueFd &opened_file,
-    /* out */ Span<const char> &opened_path
+    /* out */ const std::string* &opened_path
 )
 {
-    assert(sz_file_name.size());
-
-    NotNull<czstring> file_name = sz_file_name.data();
-    opened_path = cut_null(sz_file_name);
-
-    if (!*file_name)
-    {
-        file_name = index_gmi;
-        opened_path = cut_null(index_gmi);
-    }
-
-    opened_file.reset(openat(directory_fd, file_name, O_RDONLY));
+    opened_path = request_path.empty() ? &index_gmi : &request_path;
+    opened_file.reset(openat(directory_fd, opened_path->c_str(), O_RDONLY));
     if (!opened_file)
     {
         const auto error_code = errno;
         error::occurred(
-            [file_name]
+            [&opened_path]
             {
-                error::g_log << "Open file \"" << file_name << "\"";
+                error::g_log << "Open file \"" << opened_path->c_str() << "\"";
             },
             error::Print{error_code}
         );
@@ -51,9 +38,9 @@ open_file(
     if (fstat(opened_file.get(), &sb) == -1)
     {
         error::occurred(
-            [file_name]
+            [&opened_path]
             {
-                error::g_log << "Stat file \"" << file_name << "\"";
+                error::g_log << "Stat file \"" << opened_path->c_str() << "\"";
             },
             error::Print{}
         );
@@ -62,15 +49,15 @@ open_file(
     if (S_ISDIR(sb.st_mode))
     {
         const UniqueFd new_parent{opened_file.release()};
-        opened_path = cut_null(index_gmi);
-        opened_file.reset(openat(new_parent.get(), index_gmi, O_RDONLY));
+        opened_path = &index_gmi;
+        opened_file.reset(openat(new_parent.get(), opened_path->c_str(), O_RDONLY));
         if (!opened_file)
         {
             const auto error_code = errno;
             error::occurred(
-                [file_name]
+                [&opened_path]
                 {
-                    error::g_log << "Open file \"" << file_name << "\" /" << index_gmi;
+                    error::g_log << "Open file \"" << opened_path->c_str() << "\" /" << index_gmi;
                 },
                 error::Print{error_code}
             );
blob - /dev/null
blob + 80a02c2b1fc22087cef8d3252cc8cb4c67b82f37 (mode 644)
--- /dev/null
+++ vostok/mime.cc
@@ -0,0 +1,94 @@
+/* Detect MIME file type */
+
+#include "mime.h"
+
+#include <fstream>
+#include <sstream>
+
+
+namespace vostok
+{
+
+
+bool Mime::parse_db(NotNull<czstring> sz_path)
+{
+    std::ifstream ifstream;
+    ifstream.open(sz_path);
+
+    for (std::string line; std::getline(ifstream, line); )
+    {
+        if (line.empty())
+            continue;
+        if (line[0] == '#')
+            continue;
+
+        std::istringstream line_stream(line);
+        std::string type;
+        for (std::string token; std::getline(line_stream, token, '\t'); )
+        {
+            if (token.empty())
+                continue;
+            if (type.empty())
+            {
+                type = token;
+                continue;
+            }
+
+            std::istringstream ext_stream(token);
+            for (std::string ext; std::getline(ext_stream, ext, ' '); )
+            {
+                if (!ext.empty())
+                {
+                    Key key{ext};
+                    m_map.emplace(std::move(key), type);
+                }
+            }
+        }
+    }
+    return true;
+}
+
+
+const std::string *Mime::lookup(const std::string &path) const
+{
+    const auto it = get_ext(path);
+    if (it == path.end())
+        return nullptr;
+
+    auto found = m_map.find(Key{&*it});
+    return found == m_map.end() ? nullptr : &(found->second);
+}
+
+
+std::string::const_iterator Mime::get_ext(const std::string &path)
+{
+    if (!path.empty())
+    {
+        auto current = (path.end() - 1);
+        for (; ; )
+        {
+            if (*current == '/')
+                break;
+
+            if (*current != '.')
+            {
+                if (current == path.begin())
+                    break;
+
+                --current;
+                continue;
+            }
+            return current + 1;
+        }
+    }
+    return path.end();
+}
+
+
+bool Mime::Less::operator() (const Mime::Key &key1, const Mime::Key &key2) const
+{
+    return strcasecmp(key1.c_str(), key2.c_str()) < 0;
+}
+
+
+}   // namespace vostok
blob - a69a53ff70dbd260bb80d14e0faa7e9017c6584a
blob + 3b9c2eb7ae75aafc8380763b767c4323b8f75ee5
--- vostok/open_file.h
+++ vostok/open_file.h
@@ -1,6 +1,7 @@
 /** Open file for Gemini response */
 
 #include "utils.h"
+#include <string>
 
 #pragma once
 
@@ -8,7 +9,6 @@
 namespace vostok
 {
 
-
 enum OpenFileResult
 {
     FILE_OPENED,
@@ -20,9 +20,9 @@ enum OpenFileResult
 OpenFileResult
 open_file(
     /* in */ int directory_fd,
-    /* in */ const std::vector<char> &sz_file_name,
+    /* in */ const std::string &request_path,
     /* out */ UniqueFd &opened_file,
-    /* out */ Span<const char> &opened_path
+    /* out */ const std::string* &opened_path
 );
 
 
blob - /dev/null
blob + d090a2aee0616e7839bf45500d48bda520a7d7da (mode 644)
--- /dev/null
+++ vostok/mime.h
@@ -0,0 +1,50 @@
+/* Detect MIME file type */
+
+#include "utils.h"
+
+#include <string>
+#include <map>
+
+#pragma once
+
+
+namespace vostok
+{
+
+
+class Mime
+{
+public:
+    /* Read and parse "mime.types" file content */
+    bool parse_db(NotNull<czstring> sz_path);
+
+    /* Lookup MIME type by FS path */
+    const std::string *lookup(const std::string &path) const;
+
+    /* Get file extension from path. Return path.end() if no extension. */
+    static std::string::const_iterator get_ext(const std::string &path);
+
+protected:
+    class Key
+    {
+    public:
+        explicit Key(/* in/out */ std::string &string) {m_string.swap(string);}
+        explicit Key(const char *p) : m_p(p) {}
+        const char *c_str() const { return m_p ? m_p : m_string.c_str(); }
+
+    private:
+        std::string m_string;
+        const char *m_p{nullptr};
+    };
+
+private:
+    class Less
+    {
+    public:
+        bool operator ()(const Key &key1, const Key &key2) const;
+    };
+    std::map<Key, std::string, Less> m_map;
+};
+
+
+}   // namespace vostok
blob - 7bc242fbd334c6b9df1ffaa6c3a54ad249ca3f5c
blob + e0d4eb17cf214b9012e7bfc4b6191d91444626cd
--- vostok/request.cc
+++ vostok/request.cc
@@ -16,7 +16,7 @@ namespace
 {
 
 const auto gemini_scheme = cut_null("gemini://");
-inline bool is_gemini_scheme(const std::vector<char> &url)
+inline bool is_gemini_scheme(const std::string &url)
 {
     return std::equal(
         gemini_scheme.begin(),
@@ -27,7 +27,7 @@ inline bool is_gemini_scheme(const std::vector<char> &
 }
 
 
-bool cut_crlf(std::vector<char> &buffer)
+bool cut_crlf(std::string &buffer)
 {
     // > servers MUST ignore anything sent after the first occurrence of a <CR><LF>.
     for (auto current = buffer.cbegin(); current != buffer.cend(); ++current)
@@ -53,8 +53,8 @@ class PathNormalization (public)
 
     Request::ParseResult
     operator() (
-        std::vector<char>::const_iterator p,
-        std::vector<char> &url
+        std::string::const_iterator p,
+        std::string &url
     );
 
 protected:
@@ -65,7 +65,7 @@ class PathNormalization (public)
         return Request::URL_OK;
     }
 
-    void fill(std::vector<char> &url) const;
+    void fill(std::string &url) const;
 
 private:
     Components::value_type m_inprogress;
@@ -75,8 +75,8 @@ class PathNormalization (public)
 
 Request::ParseResult
 PathNormalization::operator() (
-    std::vector<char>::const_iterator p,
-    std::vector<char> &url
+    std::string::const_iterator p,
+    std::string &url
 )
 {
     m_inprogress = Components::value_type{nullptr, 0};
@@ -125,7 +125,7 @@ Request::ParseResult PathNormalization::on_component()
     return on_component_ok();
 }
 
-void PathNormalization::fill(std::vector<char> &url) const
+void PathNormalization::fill(std::string &url) const
 {
     auto current = url.begin();
     for (auto it = m_result.cbegin(); it != m_result.cend(); ++it)
@@ -138,23 +138,20 @@ void PathNormalization::fill(std::vector<char> &url) c
         }
         current = std::copy(it->begin(), it->end(), current);
     }
-    *current = '\0';
-    ++current;
-
     url.resize(current - url.begin());
 }
 
 }   // namespace <unnamed>
 
 
-std::vector<char> &Request::get_buffer()
+std::string &Request::get_buffer()
 {
     m_buffer.resize(gemini::MAX_REQUEST_LENGTH);
     return m_buffer;
 }
 
 
-Request::ParseResult Request::parse()
+Request::ParseResult Request::parse(/* out */ std::string &path)
 {
     if (!cut_crlf(m_buffer))
         return BAD_REQUEST;
@@ -177,17 +174,11 @@ Request::ParseResult Request::parse()
     }
 
     // normalize '.' and '..'
-    return PathNormalization{}(current, m_buffer);
+    const auto ret = PathNormalization{}(current, m_buffer);
+    if (ret == URL_OK)
+        m_buffer.swap(path);
+    return ret;
 }
 
 
-const std::vector<char> &Request::get_path() const
-{
-    assert(m_buffer.size() > 0);
-    assert(m_buffer.size() < gemini::MAX_REQUEST_LENGTH);
-    assert(m_buffer[m_buffer.size() - 1] == '\0');
-    return m_buffer;
-}
-
-
 }   // namespace vostok
blob - 9beac489676e75d5c1a7deee44be8c08bd0475e9
blob + 1359056f3684043421faf449d2d868ff02af7caa
--- vostok/request.h
+++ vostok/request.h
@@ -1,6 +1,6 @@
 /** Gemini request parser */
 
-#include <vector>
+#include <string>
 
 #pragma once
 
@@ -13,9 +13,10 @@ class Request
 {
 public:
     /* Get buffer for incoming Gemini request */
-    std::vector<char> &get_buffer();
+    std::string &get_buffer();
 
-    /* Parse incoming Gemini request */
+    /* Parse incoming Gemini request 
+       and return normalized path as zero-terminated string (if URL_OK) */
     enum ParseResult
     {
         URL_OK,
@@ -25,14 +26,10 @@ class Request (public)
         URL_NON_GEMINI,
         URL_ROOT_TRAVERSE,
     };
-    ParseResult parse();
+    ParseResult parse(/* out */ std::string &path);
 
-
-    /* Get normalized path (if parse() return URL_OK) as zero-terminated string */
-    const std::vector<char> &get_path() const;
-
 private:
-    std::vector<char> m_buffer;
+    std::string m_buffer;
 };
 
 
blob - ce1c426013b7734b3cb83baba2ea6915037fc386
blob + c7684b15aa5d1d31442d925abd0729a7f80e1e78
--- vostok/transport.cc
+++ vostok/transport.cc
@@ -136,7 +136,7 @@ AcceptedClient::AcceptedClient(int server_socket, stru
 
 bool recv(
     /* in */ NotNull<struct tls *> ctx,
-    /* in/out */ std::vector<char> &buff
+    /* in/out */ Span<char> &buff
 )
 {
     ssize_t ret{};
@@ -152,7 +152,7 @@ bool recv(
         error::occurred("TLS read", PrintIfError{tls_error(ctx)});
         return false;
     }
-    buff.resize(ret);
+    buff = buff.first(ret);
     return true;
 }
 
@@ -167,10 +167,10 @@ send(
     while (buff.size() > 0)
     {
         ret = tls_write(ctx, buff.begin(), buff.size());
+        error::g_log << "|" << std::dec << ret << " (0x" << std::hex << ret << ")|" << std::endl;
         if (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT)
             continue;
-        break;
-        buff = buff.subspan(buff.size() - ret);
+        buff = buff.subspan(ret);
     }
     if (ret == -1)
     {
blob - 08c3def08a75168b755be2465e2b8845f5f517c3
blob + 5533e0ea87859946aed4bfb8a63870bd800eb07a
--- vostok/transport.h
+++ vostok/transport.h
@@ -54,10 +54,26 @@ class AcceptedClient (private)
 bool
 recv(
     /* in */ NotNull<struct tls *> ctx,
-    /* in/out */ std::vector<char> &buff
+    /* in/out */ Span<char> &buff
 );
 
+template <class Container>
+bool
+recv(
+    /* in */ NotNull<struct tls *> ctx,
+    /* in/out */ Container &buff
+)
+{
+    Span<char> span{buff};
+    const auto ret = recv(ctx, span);
+    if (!ret)
+        return false;
 
+    buff.resize(span.size());
+    return true;
+}
+
+
 /** Send raw bytes */
 bool
 send(
blob - 60749f41c58b753468d478666c6e0568f18c687b
blob + a112a379cd9c0119277eb500296040c22a07bf0d
--- vostok/utils.h
+++ vostok/utils.h
@@ -2,8 +2,7 @@
 
 #include <cstddef>
 #include <cassert>
-#include <array>
-#include <vector>
+#include <iterator>
 
 #include <unistd.h>
 
@@ -65,34 +64,24 @@ class Span
 public:
     using element_type = ElemT;
     using iterator = ElemT *;
+    using reverse_iterator = std::reverse_iterator<iterator>;
 
     
     constexpr Span() : m_p{nullptr}, m_count{0} {}
     constexpr Span(element_type *p, std::size_t count) : m_p{count ? p : nullptr}, m_count{count} {}
     template <std::size_t N>
     constexpr Span(element_type (&arr)[N]) : m_p{arr}, m_count{N} {}
-    template <std::size_t N>
-    constexpr Span(const std::array<element_type, N> &arr) : m_p{arr.data()}, m_count{N} {}
-    template <typename other_element_type>
-    Span(const std::vector<other_element_type> &v)
-    {
-        if (v.size())
-        {
-            m_p = v.data();
-            m_count = v.size();
-        }
-        else
-        {
-            m_p = nullptr;
-            m_count = 0;
-        }
-    }
+    template <class Container>
+    constexpr Span(Container &c) : m_p{c.size() ? &*c.begin() : nullptr}, m_count{c.size()} {}
 
     constexpr std::size_t size() const {return m_count;}
 
     constexpr iterator begin() const noexcept { return m_p; }
     constexpr iterator end() const noexcept { return m_p + size(); }
 
+    reverse_iterator rbegin() const noexcept { return reverse_iterator(end()); }
+    reverse_iterator rend() const noexcept { return reverse_iterator(begin());}
+
     Span<element_type> first(std::size_t count) const
     {
         assert(count <= m_count);
@@ -104,6 +93,7 @@ class Span
         return (offset < m_count) ? Span<element_type>{m_p + offset, m_count - offset} : Span<element_type>{};
     }
 
+    element_type *data() const { return m_p; }
     element_type &operator[](std::size_t idx) const
     {
         assert(idx < m_count);
@@ -123,13 +113,6 @@ constexpr Span<const char> cut_null(const char (&arr)[
     static_assert(N > 0, "!(N > 0)");
     return Span<const char>{arr, N - 1};
 }
-template <typename ElemT>
-Span<const char> cut_null(const std::vector<ElemT> &v)
-{
-    assert(v.size() > 0);
-    assert(v[v.size() - 1] == '\0');
-    return Span<const char>{v.data(), v.size() - 1};
-}
 
 
 /** Smart pointer for file descriptor */
blob - 35c28e6faddbc3bc9f924773c695981ddadef313
blob + cc2dda64753c42de52c31b28c53ec3385379e310
--- vostok/vostok.cc
+++ vostok/vostok.cc
@@ -11,6 +11,7 @@
 #include <arpa/inet.h>
 #include <netinet/in.h>
 
+#include <vector>
 #include <thread>
 
 
@@ -21,33 +22,24 @@ namespace
 
 namespace meta
 {
-const char sz_bad_request[] = "Bad request";
-const auto bad_request = cut_null(sz_bad_request);
-
-const char sz_url_too_short[] = "URL is too short";
-const auto url_too_short = cut_null(sz_url_too_short);
-
-const char sz_non_gemini[] = "No proxying to non-Gemini content";
-const auto non_gemini = cut_null(sz_non_gemini);
-
-const char sz_root_traverse[] = "Wrong traverse";
-const auto root_traverse = cut_null(sz_root_traverse);
-
-const char sz_not_found[] = "Not found";
-const auto not_found = cut_null(sz_not_found);
-
-const char sz_temporary_failure[] = "Temporary failure";
-const auto temporary_failure = cut_null(sz_temporary_failure);
+const std::string _EMPTY;
+const std::string BAD_REQUEST{"Bad request"};
+const std::string URL_TOO_SHORT{"URL is too short"};
+const std::string NON_GEMINI{"No proxying to non-Gemini content"};
+const std::string ROOT_TRAVERSE{"Wrong traverse"};
+const std::string NOT_FOUND{"Not found"};
+const std::string TEMPORARY_FAILURE{"Temporary failure"};
 }   // namespace meta
 
-bool send_response(NotNull<struct tls *> ctx, gemini::Status status, Span<const char> meta)
+
+bool send_response(NotNull<struct tls *> ctx, gemini::Status status, const std::string &meta)
 {
     // <STATUS><SPACE><META><CR><LF>
     if (!transport::send(ctx, status))
         return false;
     if (!transport::send(ctx, gemini::SPACE))
         return false;
-    if (meta.size())
+    if (!meta.empty())
     {
         if (!transport::send(ctx, meta))
             return false;
@@ -59,7 +51,7 @@ bool send_response(NotNull<struct tls *> ctx, gemini::
 
 
 
-void client_thread(const transport::AcceptedClient *accepted_client, int directory_fd)
+void client_thread(const transport::AcceptedClient *accepted_client, int directory_fd, const Mime &mime)
 {
     assert(accepted_client);
     std::unique_ptr<const transport::AcceptedClient> accepted_client_deleter{accepted_client};
@@ -68,24 +60,25 @@ void client_thread(const transport::AcceptedClient *ac
     if (!transport::recv(accepted_client->get_ctx(), request.get_buffer()))
         return;
 
-    const auto parse_result = request.parse();
+    std::string path;
+    const auto parse_result = request.parse(path);
     switch (parse_result)
     {
     case Request::BAD_REQUEST:
-        error::occurred("Parse request", []{error::g_log << meta::sz_bad_request;});
-        send_response(accepted_client->get_ctx(), gemini::STATUS_59_BAD_REQUEST, meta::bad_request);
+        error::occurred("Parse request", []{error::g_log << meta::BAD_REQUEST;});
+        send_response(accepted_client->get_ctx(), gemini::STATUS_59_BAD_REQUEST, meta::BAD_REQUEST);
         return;
     case Request::URL_TOO_SHORT:
-        error::occurred("Parse request", []{error::g_log << meta::sz_url_too_short;});
-        send_response(accepted_client->get_ctx(), gemini::STATUS_59_BAD_REQUEST, meta::url_too_short);
+        error::occurred("Parse request", []{error::g_log << meta::URL_TOO_SHORT;});
+        send_response(accepted_client->get_ctx(), gemini::STATUS_59_BAD_REQUEST, meta::URL_TOO_SHORT);
         return;
     case Request::URL_NON_GEMINI:
-        error::occurred("Parse request", []{error::g_log << meta::sz_non_gemini;});
-        send_response(accepted_client->get_ctx(), gemini::STATUS_53_PROXY_REQUEST_REFUSED, meta::non_gemini);
+        error::occurred("Parse request", []{error::g_log << meta::NON_GEMINI;});
+        send_response(accepted_client->get_ctx(), gemini::STATUS_53_PROXY_REQUEST_REFUSED, meta::NON_GEMINI);
         return;
     case Request::URL_ROOT_TRAVERSE:
-        error::occurred("Parse request", []{error::g_log << meta::sz_root_traverse;});
-        send_response(accepted_client->get_ctx(), gemini::STATUS_50_PERMANENT_FAILURE, meta::root_traverse);
+        error::occurred("Parse request", []{error::g_log << meta::ROOT_TRAVERSE;});
+        send_response(accepted_client->get_ctx(), gemini::STATUS_50_PERMANENT_FAILURE, meta::ROOT_TRAVERSE);
         return;
 
     case Request::URL_OK:
@@ -93,24 +86,29 @@ void client_thread(const transport::AcceptedClient *ac
     }
 
     UniqueFd opened_file{};
-    Span<const char> opened_path;
-    const auto open_file_result = open_file(directory_fd, request.get_path(), opened_file, opened_path);
+    const std::string *opened_path = nullptr;
+    const auto open_file_result = open_file(directory_fd, path, opened_file, opened_path);
     switch (open_file_result)
     {
     case FILE_NOT_FOUND:
-        send_response(accepted_client->get_ctx(), gemini::STATUS_51_NOT_FOUND, meta::not_found);
+        send_response(accepted_client->get_ctx(), gemini::STATUS_51_NOT_FOUND, meta::NOT_FOUND);
         return;
     case FILE_OPENING_ERROR:
-        send_response(accepted_client->get_ctx(), gemini::STATUS_40_TEMPORARY_FAILURE, meta::temporary_failure);
+        send_response(accepted_client->get_ctx(), gemini::STATUS_40_TEMPORARY_FAILURE, meta::TEMPORARY_FAILURE);
         return;
 
     case FILE_OPENED:
-        assert(opened_file);
+        assert(opened_file && opened_path);
         break;
     }
 
     // > If <META> is an empty string, the MIME type MUST default to "text/gemini; charset=utf-8".
-    send_response(accepted_client->get_ctx(), gemini::STATUS_20_SUCCESS, {});
+    const auto mime_type = mime.lookup(*opened_path);
+    send_response(
+        accepted_client->get_ctx(),
+        gemini::STATUS_20_SUCCESS,
+        mime_type ? *mime_type : meta::_EMPTY
+    );
 
     std::vector<char> buffer;
     buffer.resize(64 * 1024);
@@ -120,9 +118,9 @@ void client_thread(const transport::AcceptedClient *ac
         if (ret == -1)
         {
             error::occurred(
-                [&request]
+                [&path]
                 {
-                    error::g_log << "Read file \"" << request.get_path().data() << "\"";
+                    error::g_log << "Read file \"" << path.c_str() << "\"";
                 },
                 error::Print{}
             );
@@ -137,11 +135,11 @@ void client_thread(const transport::AcceptedClient *ac
         if (readed < buffer.size())
             break;
     }
-    error::g_log << "20 " << "\"" << request.get_path().data() << "\"" << std::endl;
+    error::g_log << "20 " << (mime_type ? *mime_type : meta::_EMPTY) << " \"" << path.c_str() << "\"" << std::endl;
 }
 
 
-bool server_loop(int server_socket, NotNull<struct tls *>server_ctx, int directory_fd)
+bool server_loop(int server_socket, NotNull<struct tls *>server_ctx, int directory_fd, const Mime &mime)
 {
     error::g_log << "🚀 Vostok server listening..." << std::endl;
     for (; ; )
@@ -159,7 +157,7 @@ bool server_loop(int server_socket, NotNull<struct tls
         {
             try
             {
-                std::thread{client_thread, accepted_client.get(), directory_fd}.detach();
+                std::thread{client_thread, accepted_client.get(), directory_fd, mime}.detach();
             }
             catch (const std::system_error &e)
             {
@@ -213,7 +211,7 @@ bool main(const CommandLineArguments &args)
         return false;
     }
 
-    return server_loop(server_socket.get(), server_ctx.get(), args.directory.get());
+    return server_loop(server_socket.get(), server_ctx.get(), args.directory.get(), args.mime);
 }