diff --git a/src/libstore/outputs-spec.cc b/src/libstore/outputs-spec.cc
index 096443cb2..e26c38138 100644
--- a/src/libstore/outputs-spec.cc
+++ b/src/libstore/outputs-spec.cc
@@ -1,8 +1,10 @@
-#include "util.hh"
-#include "outputs-spec.hh"
-#include "nlohmann/json.hpp"
-
 #include <regex>
+#include <nlohmann/json.hpp>
+
+#include "util.hh"
+#include "regex-combinators.hh"
+#include "outputs-spec.hh"
+#include "path-regex.hh"
 
 namespace nix {
 
@@ -18,11 +20,14 @@ bool OutputsSpec::contains(const std::string & outputName) const
     }, raw());
 }
 
+static std::string outputSpecRegexStr =
+    regex::either(
+        regex::group(R"(\*)"),
+        regex::group(regex::list(nameRegexStr)));
 
 std::optional<OutputsSpec> OutputsSpec::parseOpt(std::string_view s)
 {
-    // See checkName() for valid output name characters.
-    static std::regex regex(R"((\*)|([a-zA-Z\+\-\._\?=]+(,[a-zA-Z\+\-\._\?=]+)*))");
+    static std::regex regex(std::string { outputSpecRegexStr });
 
     std::smatch match;
     std::string s2 { s }; // until some improves std::regex
diff --git a/src/libstore/path-regex.hh b/src/libstore/path-regex.hh
new file mode 100644
index 000000000..6893c3876
--- /dev/null
+++ b/src/libstore/path-regex.hh
@@ -0,0 +1,7 @@
+#pragma once
+
+namespace nix {
+
+static constexpr std::string_view nameRegexStr = R"([0-9a-zA-Z\+\-\._\?=]+)";
+
+}
diff --git a/src/libstore/path.cc b/src/libstore/path.cc
index 392db225e..46be54281 100644
--- a/src/libstore/path.cc
+++ b/src/libstore/path.cc
@@ -8,8 +8,10 @@ static void checkName(std::string_view path, std::string_view name)
 {
     if (name.empty())
         throw BadStorePath("store path '%s' has an empty name", path);
-    if (name.size() > 211)
-        throw BadStorePath("store path '%s' has a name longer than 211 characters", path);
+    if (name.size() > StorePath::MaxPathLen)
+        throw BadStorePath("store path '%s' has a name longer than '%d characters",
+            StorePath::MaxPathLen, path);
+    // See nameRegexStr for the definition
     for (auto c : name)
         if (!((c >= '0' && c <= '9')
                 || (c >= 'a' && c <= 'z')
diff --git a/src/libstore/path.hh b/src/libstore/path.hh
index 8e1cb5e55..6a8f027f9 100644
--- a/src/libstore/path.hh
+++ b/src/libstore/path.hh
@@ -16,6 +16,8 @@ public:
     /* Size of the hash part of store paths, in base-32 characters. */
     constexpr static size_t HashLen = 32; // i.e. 160 bits
 
+    constexpr static size_t MaxPathLen = 211;
+
     StorePath() = delete;
 
     StorePath(std::string_view baseName);
diff --git a/src/libstore/tests/libstoretests.hh b/src/libstore/tests/libstoretests.hh
new file mode 100644
index 000000000..05397659b
--- /dev/null
+++ b/src/libstore/tests/libstoretests.hh
@@ -0,0 +1,23 @@
+#include <gtest/gtest.h>
+#include <gmock/gmock.h>
+
+#include "store-api.hh"
+
+namespace nix {
+
+class LibStoreTest : public ::testing::Test {
+    public:
+        static void SetUpTestSuite() {
+            initLibStore();
+        }
+
+    protected:
+        LibStoreTest()
+            : store(openStore("dummy://"))
+        { }
+
+        ref<Store> store;
+};
+
+
+} /* namespace nix */
diff --git a/src/libstore/tests/outputs-spec.cc b/src/libstore/tests/outputs-spec.cc
index 836ba7e82..06e4cabbd 100644
--- a/src/libstore/tests/outputs-spec.cc
+++ b/src/libstore/tests/outputs-spec.cc
@@ -47,6 +47,13 @@ TEST(OutputsSpec, names_underscore) {
     ASSERT_EQ(expected.to_string(), str);
 }
 
+TEST(OutputsSpec, names_numberic) {
+    std::string_view str = "01";
+    OutputsSpec expected = OutputsSpec::Names { "01" };
+    ASSERT_EQ(OutputsSpec::parse(str), expected);
+    ASSERT_EQ(expected.to_string(), str);
+}
+
 TEST(OutputsSpec, names_out_bin) {
     OutputsSpec expected = OutputsSpec::Names { "out", "bin" };
     ASSERT_EQ(OutputsSpec::parse("out,bin"), expected);
diff --git a/src/libstore/tests/path.cc b/src/libstore/tests/path.cc
new file mode 100644
index 000000000..8ea252c92
--- /dev/null
+++ b/src/libstore/tests/path.cc
@@ -0,0 +1,144 @@
+#include <regex>
+
+#include <nlohmann/json.hpp>
+#include <gtest/gtest.h>
+#include <rapidcheck/gtest.h>
+
+#include "path-regex.hh"
+#include "store-api.hh"
+
+#include "libstoretests.hh"
+
+namespace nix {
+
+#define STORE_DIR "/nix/store/"
+#define HASH_PART "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q"
+
+class StorePathTest : public LibStoreTest
+{
+};
+
+static std::regex nameRegex { std::string { nameRegexStr } };
+
+#define TEST_DONT_PARSE(NAME, STR)                           \
+    TEST_F(StorePathTest, bad_ ## NAME) {                    \
+        std::string_view str =                               \
+            STORE_DIR HASH_PART "-" STR;                     \
+        ASSERT_THROW(                                        \
+            store->parseStorePath(str),                      \
+            BadStorePath);                                   \
+        std::string name { STR };                            \
+        EXPECT_FALSE(std::regex_match(name, nameRegex));     \
+    }
+
+TEST_DONT_PARSE(empty, "")
+TEST_DONT_PARSE(garbage, "&*()")
+TEST_DONT_PARSE(double_star, "**")
+TEST_DONT_PARSE(star_first, "*,foo")
+TEST_DONT_PARSE(star_second, "foo,*")
+TEST_DONT_PARSE(bang, "foo!o")
+
+#undef TEST_DONT_PARSE
+
+#define TEST_DO_PARSE(NAME, STR)                             \
+    TEST_F(StorePathTest, good_ ## NAME) {                   \
+        std::string_view str =                               \
+            STORE_DIR HASH_PART "-" STR;                     \
+        auto p = store->parseStorePath(str);                 \
+        std::string name { p.name() };                       \
+        EXPECT_TRUE(std::regex_match(name, nameRegex));      \
+    }
+
+// 0-9 a-z A-Z + - . _ ? =
+
+TEST_DO_PARSE(numbers, "02345")
+TEST_DO_PARSE(lower_case, "foo")
+TEST_DO_PARSE(upper_case, "FOO")
+TEST_DO_PARSE(plus, "foo+bar")
+TEST_DO_PARSE(dash, "foo-dev")
+TEST_DO_PARSE(underscore, "foo_bar")
+TEST_DO_PARSE(period, "foo.txt")
+TEST_DO_PARSE(question_mark, "foo?why")
+TEST_DO_PARSE(equals_sign, "foo=foo")
+
+#undef TEST_DO_PARSE
+
+// For rapidcheck
+void showValue(const StorePath & p, std::ostream & os) {
+    os << p.to_string();
+}
+
+}
+
+namespace rc {
+using namespace nix;
+
+template<>
+struct Arbitrary<StorePath> {
+    static Gen<StorePath> arbitrary();
+};
+
+Gen<StorePath> Arbitrary<StorePath>::arbitrary()
+{
+    auto len = *gen::inRange<size_t>(1, StorePath::MaxPathLen);
+
+    std::string pre { HASH_PART "-" };
+    pre.reserve(pre.size() + len);
+
+    for (size_t c = 0; c < len; ++c) {
+        switch (auto i = *gen::inRange<uint8_t>(0, 10 + 2 * 26 + 6)) {
+            case 0 ... 9:
+                pre += '0' + i;
+            case 10 ... 35:
+                pre += 'A' + (i - 10);
+                break;
+            case 36 ... 61:
+                pre += 'a' + (i - 36);
+                break;
+            case 62:
+                pre += '+';
+                break;
+            case 63:
+                pre += '-';
+                break;
+            case 64:
+                pre += '.';
+                break;
+            case 65:
+                pre += '_';
+                break;
+            case 66:
+                pre += '?';
+                break;
+            case 67:
+                pre += '=';
+                break;
+            default:
+                assert(false);
+        }
+    }
+
+    return gen::just(StorePath { pre });
+}
+
+} // namespace rc
+
+namespace nix {
+
+RC_GTEST_FIXTURE_PROP(
+    StorePathTest,
+    prop_regex_accept,
+    (const StorePath & p))
+{
+    RC_ASSERT(std::regex_match(std::string { p.name() }, nameRegex));
+}
+
+RC_GTEST_FIXTURE_PROP(
+    StorePathTest,
+    prop_round_rip,
+    (const StorePath & p))
+{
+    RC_ASSERT(p == store->parseStorePath(store->printStorePath(p)));
+}
+
+}
diff --git a/src/libutil/regex-combinators.hh b/src/libutil/regex-combinators.hh
new file mode 100644
index 000000000..0b997b25a
--- /dev/null
+++ b/src/libutil/regex-combinators.hh
@@ -0,0 +1,30 @@
+#pragma once
+
+#include <string_view>
+
+namespace nix::regex {
+
+// TODO use constexpr string building like
+// https://github.com/akrzemi1/static_string/blob/master/include/ak_toolkit/static_string.hpp
+
+static inline std::string either(std::string_view a, std::string_view b)
+{
+    return std::string { a } + "|" + b;
+}
+
+static inline std::string group(std::string_view a)
+{
+    return std::string { "(" } + a + ")";
+}
+
+static inline std::string many(std::string_view a)
+{
+    return std::string { "(?:" } + a + ")*";
+}
+
+static inline std::string list(std::string_view a)
+{
+    return std::string { a } + many(group("," + a));
+}
+
+}