// file      : openssl/agent/pkcs11/url.cxx -*- C++ -*-
// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd
// license   : MIT; see accompanying LICENSE file

#include <openssl/agent/pkcs11/url.hxx>

#include <cerrno>
#include <cstdlib>  // strtoull()
#include <iterator> // back_inserter()

namespace openssl
{
  namespace agent
  {
    namespace pkcs11
    {
      using namespace std;

      // Convenience functions.
      //
      static uint64_t
      parse_uint64 (const string& s,
                    uint64_t min,
                    uint64_t max,
                    const char* what)
      {
        if (s[0] != '-' && s[0] != '+') // strtoull() allows these.
        {
          const char* b (s.c_str ());
          char* e (nullptr);
          uint64_t v (strtoull (b, &e, 10)); // Can't throw.

          if (errno != ERANGE && e == b + s.size () && v >= min && v <= max)
            return v;
        }

        throw invalid_argument (string ("invalid ") + what + " '" + s + "'");
      }

      // url_traits
      //
      optional<url_traits::scheme_type> url_traits::
      translate_scheme (const string_type&         /* url */,
                        string_type&&              scheme,
                        optional<authority_type>&  authority,
                        optional<path_type>&       path,
                        optional<string_type>&     /* query */,
                        optional<string_type>&     fragment,
                        bool&                      rootless)
      {
        // If something is wrong with the URL leave the basic_url constructor
        // to throw.
        //
        if (scheme.empty ())
          return nullopt;

        if (casecmp (scheme, "pkcs11") != 0)
          throw invalid_argument ("invalid scheme");

        if (authority)
          throw invalid_argument ("unexpected authority");

        if (path && (!rootless || path->find ('/') != string::npos))
          throw invalid_argument ("one-level path expected");

        if (fragment)
          throw invalid_argument ("unexpected fragment");

        return move (scheme);
      }

      url_traits::string_type url_traits::
      translate_scheme (string_type&                     /* url */,
                        const scheme_type&               scheme,
                        const optional<authority_type>&  /* authority */,
                        const optional<path_type>&       /* path */,
                        const optional<string_type>&     /* query */,
                        const optional<string_type>&     /* fragment */,
                        bool                             /* rootless */)
      {
        return scheme;
      }

      url_traits::path_type url_traits::
      translate_path (string_type&& path)
      {
        return move (path);
      }

      url_traits::string_type url_traits::
      translate_path (const path_type& path)
      {
        return path;
      }

      // library_version
      //
      library_version::
      library_version (const string& s)
      {
        auto num = [] (const string& s, const char* what)
        {
          return static_cast<unsigned char> (parse_uint64 (s, 0, 255, what));
        };

        size_t p (s.find ('.'));

        if (p != string::npos)
        {
          major = num (string (s, 0, p),  "library major version");
          minor = num (string (s, p + 1), "library minor version");
        }
        else
        {
          major = num (s, "library major version");
          minor = 0;
        }
      }

      // Parse the attribute name=value representation. The value can contain
      // binary data.
      //
      // It would probably be cleaner to return pair<string, vector<char>>,
      // but this would uglify a client code quite a bit and make it less
      // efficient.
      //
      static pair<string, string>
      attribute (const string& s, size_t b, size_t n)
      {
        size_t i (b);
        size_t e (b + n);

        for (; i != e && s[i] != '='; ++i) ;

        if (i == e)
          throw invalid_argument (
            "no value for attribute '" + string (s, b, n) + "'");

        string a;
        url::decode (s.begin () + b, s.begin () + i, back_inserter (a));

        string v;
        url::decode (s.begin () + i + 1, s.begin () + e, back_inserter (v));

        return make_pair (move (a), move (v));
      }

      // identity
      //
      identity::
      identity (const url& u)
      {
        const optional<string>& path (u.path);

        // If URL path component is absent then create the identity that
        // matches all PKCS#11 entities in the system.
        //
        if (!path)
          return;

        for (size_t b (0), e (0), n; (n = next_word (*path, b, e, ';')); )
        {
          pair<string, string> a (attribute (*path, b, n));

          const string& an (a.first);
          string& av (a.second);

          auto set = [&an] (auto& attr, auto&& val)
          {
            if (attr)
              throw invalid_argument ("duplicate attribute '" + an + "'");

            attr = move (val);
          };

          // Module.
          //
          if (an == "library-manufacturer")
            set (library_manufacturer, move (av));
          else if (an == "library-version")
            set (library_version, library_version_type (av));
          else if (an == "library-description")
            set (library_description, move (av));

          // Slot.
          //
          else if (an == "slot-id")
            set (slot_id,
                 static_cast<unsigned long> (
                   parse_uint64 (av, 0, ~0UL, "slot-id attribute value")));
          else if (an == "slot-manufacturer")
            set (slot_manufacturer, move (av));
          else if (an == "slot-description")
            set (slot_description, move (av));

          // Token.
          //
          else if (an == "serial")
            set (serial, move (av));
          else if (an == "token")
            set (token, move (av));
          else if (an == "model")
            set (model, move (av));
          else if (an == "manufacturer")
            set (manufacturer, move (av));

          // Storage object.
          //
          else if (an == "id")
            set (id, vector<unsigned char> (av.begin (), av.end ()));
          else if (an == "object")
            set (object, move (av));
          else if (an == "type")
            set (type, move (av));
          else
            throw invalid_argument ("unknown attribute '" + an + "'");
        }
      }

      // access
      //
      access::
      access (const url& u)
      {
        const optional<string>& query (u.query);

        // If URL query component is absent then create an object that
        // provides no access attributes.
        //
        if (!query)
          return;

        for (size_t b (0), e (0), n; (n = next_word (*query, b, e, ';')); )
        {
          pair<string, string> a (attribute (*query, b, n));

          const string& an (a.first);
          string& av (a.second);

          auto set = [&an] (auto& attr, auto&& val)
          {
            if (attr)
              throw invalid_argument ("duplicate attribute '" + an + "'");

            attr = move (val);
          };

          // Note that unrecognized attributes are ignored (see the traits
          // class notes for details).
          //
          if (an == "pin-source")
            set (pin_source, move (av));
          else if (an == "pin-value")
            set (pin_value, move (av));
          else if (an == "module-name")
          {
            try
            {
              path p (av);

              if (!p.empty () && p.simple ())
              {
                set (module_name, move (p));
                continue;
              }

              // Fall through.
            }
            catch (const invalid_path& e)
            {
              // Fall through.
            }

            throw invalid_argument (
              "invalid value '" + av + "' for module-name attribute");
          }
          else if (an == "module-path")
          {
            try
            {
              path p (move (av));

              if (p.relative ())
                throw invalid_argument ("relative path '" + p.string () +
                                        "' for module-path attribute");

              set (module_path, move (p));
            }
            catch (const invalid_path& e)
            {
              throw invalid_argument (
                "invalid path '" + e.path + "' for module-path attribute");
            }
          }
        }

        if (pin_source && pin_value)
          throw invalid_argument (
            "both pin-source and pin-value attributes specified");
      }
    }
  }
}