7#include "lix/libutil/json-fwd.hh"
21#include "lix/libutil/json-fwd.hh"
31#include <unordered_map>
32#include <unordered_set>
65MakeError(SubstError, Error);
69MakeError(BuildError, Error);
74MakeError(InvalidPath, Error);
75MakeError(Unsupported, Error);
76MakeError(SubstituteGone, Error);
77MakeError(SubstituterDisabled, Error);
82MakeError(BadStorePath, Error);
84MakeError(InvalidStoreURI, Error);
93typedef std::map<std::string, StorePath> OutputPathMap;
96enum CheckSigsFlag :
bool { NoCheckSigs =
false, CheckSigs =
true };
97enum SubstituteFlag :
bool { NoSubstitute =
false, Substitute =
true };
98enum AllowInvalidFlag :
bool { DisallowInvalid =
false, AllowInvalid =
true };
106enum BuildMode { bmNormal, bmRepair, bmCheck };
108BuildMode buildModeFromInteger(
int);
110enum TrustedFlag :
bool { NotTrusted =
false, Trusted =
true };
119typedef std::map<StorePath, std::optional<ContentAddress>> StorePathCAMap;
121struct StoreConfig :
public Config
123 typedef std::map<std::string, std::string> Params;
125 using Config::Config;
127 StoreConfig() =
delete;
129 static StringSet getDefaultSystemFeatures();
131 virtual ~StoreConfig()
noexcept(
false) { }
136 virtual const std::string
name() = 0;
158 Logical location of the Nix store, usually
159 `/nix/store`. Note that you can only copy store paths
160 between stores if they have the same `store` setting.
162 const Path storeDir = storeDir_;
164 const Setting<int> pathInfoCacheSize{
this, 65536,
"path-info-cache-size",
165 "Size of the in-memory store path metadata cache."};
167 const Setting<bool> isTrusted{
this,
false,
"trusted",
169 Whether paths from this store can be used as substitutes
170 even if they are not signed by a key listed in the
171 [`trusted-public-keys`](@docroot@/command-ref/conf-file.md#conf-trusted-public-keys)
175 Setting<int> priority{this, 0,
"priority",
177 Priority of this store when used as a substituter. A lower value means a higher priority.
180 Setting<bool> wantMassQuery{this,
false,
"want-mass-query",
182 Whether this store (when used as a substituter) can be
183 queried efficiently for path validity.
186 Setting<StringSet> systemFeatures{this, getDefaultSystemFeatures(),
188 "Optional features that the system this store builds on implements (like \"kvm\").",
194class Store :
public std::enable_shared_from_this<Store>
203 std::chrono::time_point<std::chrono::steady_clock>
time_point = std::chrono::steady_clock::now();
208 std::shared_ptr<const ValidPathInfo>
value;
221 return value !=
nullptr;
232 std::shared_ptr<NarInfoDiskCache> diskCache;
257 virtual kj::Promise<Result<void>>
init()
259 return {result::success()};
265 virtual ~Store() noexcept(false) { }
267 virtual std::string getUri() = 0;
269 StorePath parseStorePath(std::string_view path)
const;
271 std::optional<StorePath> maybeParseStorePath(std::string_view path)
const;
273 std::string printStorePath(
const StorePath & path)
const;
282 PathSet printStorePathSet(
const StorePathSet & path)
const;
288 std::string
showPaths(
const StorePathSet & paths);
306 std::pair<StorePath, Path>
toStorePath(PathView path)
const;
323 std::string_view hash, std::string_view name)
const;
325 const Hash & hash, std::string_view name)
const;
327 StorePath makeOutputPath(std::string_view
id,
328 const Hash & hash, std::string_view name)
const;
330 StorePath makeFixedOutputPath(std::string_view name,
const FixedOutputInfo & info)
const;
332 StorePath makeTextPath(std::string_view name,
const TextInfo & info)
const;
334 StorePath makeFixedOutputPathFromCA(std::string_view name,
const ContentAddressWithReferences & ca)
const;
343 StorePath computeStorePathForPathFlat(std::string_view name,
const Path & srcPath)
const;
362 std::string_view name,
364 const StorePathSet & references)
const;
370 kj::Promise<Result<bool>>
isValidPath(
const StorePath & path);
374 virtual kj::Promise<Result<bool>> isValidPathUncached(
const StorePath & path);
389 virtual kj::Promise<Result<StorePathSet>>
queryValidPaths(
const StorePathSet & paths,
390 SubstituteFlag maybeSubstitute = NoSubstitute);
401 try {
unsupported(
"queryAllValidPaths"); }
catch (...) {
return {result::current_exception()}; }
403 constexpr static const char * MissingName =
"x";
409 kj::Promise<Result<ref<const ValidPathInfo>>>
queryPathInfo(
const StorePath & path);
414 kj::Promise<Result<std::shared_ptr<const Realisation>>>
queryRealisation(
const DrvOutput &);
433 virtual bool realisationIsUntrusted(
const Realisation & )
444 virtual kj::Promise<Result<std::shared_ptr<const ValidPathInfo>>>
446 virtual kj::Promise<Result<std::shared_ptr<const Realisation>>>
447 queryRealisationUncached(
const DrvOutput &) = 0;
455 virtual kj::Promise<Result<void>>
457 try {
unsupported(
"queryReferrers"); }
catch (...) {
return {result::current_exception()}; }
469 return {StorePathSet{}};
482 virtual kj::Promise<Result<std::map<std::string, std::optional<StorePath>>>>
493 virtual kj::Promise<Result<std::map<std::string, std::optional<StorePath>>>>
500 kj::Promise<Result<OutputPathMap>>
507 virtual kj::Promise<Result<std::optional<StorePath>>>
515 return {StorePathSet{}};
526 SubstitutablePathInfos & infos);
534 RepairFlag repair = NoRepair,
535 CheckSigsFlag checkSigs = CheckSigs
543 std::pair<ValidPathInfo, std::function<kj::Promise<Result<box_ptr<AsyncInputStream>>>()>>>;
551 RepairFlag repair = NoRepair,
552 CheckSigsFlag checkSigs = CheckSigs);
563 std::string_view name,
565 HashType hashAlgo = HashType::SHA256,
566 RepairFlag repair = NoRepair);
567 virtual kj::Promise<Result<StorePath>> addToStoreFlat(
568 std::string_view name,
569 const Path & srcPath,
570 HashType hashAlgo = HashType::SHA256,
571 RepairFlag repair = NoRepair);
578 kj::Promise<Result<ValidPathInfo>>
addToStoreSlow(std::string_view name,
const Path & srcPath,
580 std::optional<Hash> expectedCAHash = {});
593 std::string_view name,
595 HashType hashAlgo = HashType::SHA256,
596 RepairFlag repair = NoRepair,
597 const StorePathSet & references = StorePathSet()
599 try {
unsupported(
"addToStoreFromDump"); }
catch (...) {
return {result::current_exception()}; }
606 std::string_view name,
608 const StorePathSet & references,
609 RepairFlag repair = NoRepair) = 0;
621 try {
unsupported(
"registerDrvOutput"); }
catch (...) {
return {result::current_exception()}; }
622 virtual kj::Promise<Result<void>>
644 const std::vector<DerivedPath> & paths,
645 BuildMode buildMode = bmNormal,
646 std::shared_ptr<Store> evalStore =
nullptr);
655 const std::vector<DerivedPath> & paths,
656 BuildMode buildMode = bmNormal,
657 std::shared_ptr<Store> evalStore =
nullptr);
697 BuildMode buildMode = bmNormal
713 debug(
"not creating temporary root, store doesn't support GC");
714 return {result::success()};
716 return {result::current_exception()};
725 bool showDerivers,
bool showHash);
737 kj::Promise<Result<JSON>>
pathInfoToJSON(
const StorePathSet & storePaths,
738 bool includeImpureInfo,
bool showClosureSize,
739 Base hashBase = Base::Base32,
740 AllowInvalidFlag allowInvalid = DisallowInvalid);
747 kj::Promise<Result<std::pair<uint64_t, uint64_t>>>
getClosureSize(
const StorePath & storePath);
753 virtual kj::Promise<Result<void>>
optimiseStore() {
return {result::success()}; }
760 virtual kj::Promise<Result<bool>>
verifyStore(
bool checkContents, RepairFlag repair = NoRepair)
780 virtual kj::Promise<Result<void>>
782 try {
unsupported(
"addSignatures"); }
catch (...) {
return {result::current_exception()}; }
795 kj::Promise<Result<Derivation>>
readDerivation(
const StorePath & drvPath);
812 virtual kj::Promise<Result<void>>
computeFSClosure(
const StorePathSet & paths,
813 StorePathSet & out,
bool flipDirection =
false,
814 bool includeOutputs =
false,
bool includeDerivers =
false);
817 StorePathSet & out,
bool flipDirection =
false,
818 bool includeOutputs =
false,
bool includeDerivers =
false);
825 virtual kj::Promise<Result<void>>
queryMissing(
const std::vector<DerivedPath> & targets,
826 StorePathSet & willBuild, StorePathSet & willSubstitute, StorePathSet & unknown,
827 uint64_t & downloadSize, uint64_t & narSize);
833 kj::Promise<Result<StorePaths>>
topoSortPaths(
const StorePathSet & paths);
839 kj::Promise<Result<void>>
exportPaths(
const StorePathSet & paths, Sink & sink);
841 kj::Promise<Result<void>> exportPath(
const StorePath & path, Sink & sink);
848 kj::Promise<Result<StorePaths>>
849 importPaths(Source & source, CheckSigsFlag checkSigs = CheckSigs);
851 template<
template<
typename>
typename Wrapper = std::type_identity_t>
854 Wrapper<uint64_t> narInfoRead{0};
855 Wrapper<uint64_t> narInfoReadAverted{0};
856 Wrapper<uint64_t> narInfoMissing{0};
857 Wrapper<uint64_t> narInfoWrite{0};
858 Wrapper<uint64_t> pathInfoCacheSize{0};
859 Wrapper<uint64_t> narRead{0};
860 Wrapper<uint64_t> narReadBytes{0};
861 Wrapper<uint64_t> narReadCompressedBytes{0};
862 Wrapper<uint64_t> narWrite{0};
863 Wrapper<uint64_t> narWriteAverted{0};
864 Wrapper<uint64_t> narWriteBytes{0};
865 Wrapper<uint64_t> narWriteCompressedBytes{0};
866 Wrapper<uint64_t> narWriteCompressionTimeMs{0};
869 kj::Promise<Result<Stats<>>> getStats();
875 kj::Promise<Result<StorePathSet>>
876 exportReferences(
const StorePathSet & storePaths,
const StorePathSet & inputPaths);
891 (
co_await state.lock())->pathInfoCache.clear();
898 virtual kj::Promise<Result<void>>
connect() {
return {result::success()}; }
905 return {result::success(0)};
919 virtual Path toRealPath(
const Path & storePath)
926 return toRealPath(printStorePath(storePath));
933 virtual kj::Promise<Result<void>>
setOptions() {
return {result::success()}; }
935 virtual kj::Promise<Result<std::optional<std::string>>> getVersion()
937 return {result::success(std::nullopt)};
953 throw Unsupported(
"operation '%s' is not supported by store '%s'", op, getUri());
962kj::Promise<Result<void>> copyStorePath(
965 const StorePath & storePath,
966 RepairFlag repair = NoRepair,
967 CheckSigsFlag checkSigs = CheckSigs);
978kj::Promise<Result<std::map<StorePath, StorePath>>> copyPaths(
979 Store & srcStore, Store & dstStore,
980 const RealisedPath::Set &,
981 RepairFlag repair = NoRepair,
982 CheckSigsFlag checkSigs = CheckSigs,
983 SubstituteFlag substitute = NoSubstitute);
985kj::Promise<Result<std::map<StorePath, StorePath>>> copyPaths(
986 Store & srcStore, Store & dstStore,
987 const StorePathSet & paths,
988 RepairFlag repair = NoRepair,
989 CheckSigsFlag checkSigs = CheckSigs,
990 SubstituteFlag substitute = NoSubstitute);
995kj::Promise<Result<void>> copyClosure(
996 Store & srcStore, Store & dstStore,
997 const RealisedPath::Set & paths,
998 RepairFlag repair = NoRepair,
999 CheckSigsFlag checkSigs = CheckSigs,
1000 SubstituteFlag substitute = NoSubstitute);
1002kj::Promise<Result<void>> copyClosure(
1003 Store & srcStore, Store & dstStore,
1004 const StorePathSet & paths,
1005 RepairFlag repair = NoRepair,
1006 CheckSigsFlag checkSigs = CheckSigs,
1007 SubstituteFlag substitute = NoSubstitute);
1021kj::Promise<Result<StorePath>>
1023kj::Promise<Result<OutputPathMap>>
1024resolveDerivedPath(
Store &,
const DerivedPath::Built &,
Store * evalStore =
nullptr);
1058kj::Promise<Result<ref<Store>>> openStore(
const std::string & uri = settings.storeUri.get(),
1059 const StoreConfig::Params & extraParams = {});
1066kj::Promise<Result<std::list<ref<Store>>>> getDefaultSubstituters();
1070 std::set<std::string> uriSchemes;
1072 std::optional<ref<Store>> (
1073 const std::string & scheme,
1074 const std::string & uri,
1075 const StoreConfig::Params & params
1078 std::function<std::shared_ptr<StoreConfig> ()> getConfig;
1083 static std::vector<StoreFactory> * registered;
1085 template<
typename T,
typename TConfig>
1088 if (!registered) registered =
new std::vector<StoreFactory>();
1090 .uriSchemes = T::uriSchemes(),
1092 ([](
const std::string & scheme,
const std::string & uri,
const StoreConfig::Params & params)
1093 -> std::optional<ref<Store>>
1094 {
return make_ref<T>(scheme, uri, params); }),
1097 -> std::shared_ptr<StoreConfig>
1098 {
return std::make_shared<TConfig>(StringMap({})); })
1100 registered->push_back(factory);
1109std::string showPaths(
const PathSet & paths);
1112std::optional<ValidPathInfo> decodeValidPathInfo(
1113 const Store & store,
1115 std::optional<HashResult> hashGiven = std::nullopt);
1120std::pair<std::string, StoreConfig::Params> splitUriAndParams(
const std::string & uri);
1124kj::Promise<Result<std::map<DrvOutput, StorePath>>> drvOutputReferences(
1128 Store * evalStore =
nullptr);
Definition fs-accessor.hh:15
Definition lru-cache.hh:16
Definition nar-info-disk-cache.hh:11
Definition store-api.hh:195
virtual kj::Promise< Result< StorePathSet > > queryValidDerivers(const StorePath &path)
Definition store-api.hh:467
virtual kj::Promise< Result< StorePathSet > > querySubstitutablePaths(const StorePathSet &paths)
Definition store-api.hh:513
virtual kj::Promise< Result< std::map< std::string, std::optional< StorePath > > > > queryPartialDerivationOutputMap(const StorePath &path, Store *evalStore=nullptr)
Definition store-api.cc:530
kj::Promise< Result< ref< const ValidPathInfo > > > queryPathInfo(const StorePath &path)
Definition store-api.cc:706
virtual kj::Promise< Result< void > > buildPaths(const std::vector< DerivedPath > &paths, BuildMode buildMode=bmNormal, std::shared_ptr< Store > evalStore=nullptr)
Definition entry-points.cc:11
kj::Promise< Result< StorePathSet > > exportReferences(const StorePathSet &storePaths, const StorePathSet &inputPaths)
Definition store-api.cc:916
StorePath makeStorePath(std::string_view type, std::string_view hash, std::string_view name) const
Definition store-api.cc:162
virtual kj::Promise< Result< void > > queryMissing(const std::vector< DerivedPath > &targets, StorePathSet &willBuild, StorePathSet &willSubstitute, StorePathSet &unknown, uint64_t &downloadSize, uint64_t &narSize)
Definition misc.cc:358
virtual kj::Promise< Result< bool > > verifyStore(bool checkContents, RepairFlag repair=NoRepair)
Definition store-api.hh:760
kj::Promise< Result< std::optional< StorePath > > > getBuildDerivationPath(const StorePath &)
Definition store-api.cc:1438
virtual kj::Promise< Result< StorePath > > addToStoreRecursive(std::string_view name, const PreparedDump &source, HashType hashAlgo=HashType::SHA256, RepairFlag repair=NoRepair)
Definition store-api.cc:284
kj::Promise< Result< Derivation > > readDerivation(const StorePath &drvPath)
Definition store-api.cc:1467
virtual kj::Promise< Result< void > > ensurePath(const StorePath &path)
Definition entry-points.cc:110
kj::Promise< Result< std::shared_ptr< const Realisation > > > queryRealisation(const DrvOutput &)
Definition store-api.cc:760
virtual kj::Promise< Result< void > > optimiseStore()
Definition store-api.hh:753
virtual kj::Promise< Result< void > > repairPath(const StorePath &path)
Definition entry-points.cc:136
StorePathSet parseStorePathSet(const PathSet &paths) const
Definition path.cc:95
StorePath followLinksToStorePath(std::string_view path) const
Definition store-api.cc:74
virtual kj::Promise< Result< void > > registerDrvOutput(const Realisation &output)
Definition store-api.hh:620
virtual kj::Promise< Result< void > > querySubstitutablePathInfos(const StorePathCAMap &paths, SubstitutablePathInfos &infos)
Definition store-api.cc:586
kj::Promise< Result< Derivation > > readInvalidDerivation(const StorePath &drvPath)
Definition store-api.cc:1470
kj::Promise< Result< Derivation > > derivationFromPath(const StorePath &drvPath)
Definition store-api.cc:1413
virtual kj::Promise< Result< StorePath > > addToStoreFromDump(AsyncInputStream &dump, std::string_view name, FileIngestionMethod method=FileIngestionMethod::Recursive, HashType hashAlgo=HashType::SHA256, RepairFlag repair=NoRepair, const StorePathSet &references=StorePathSet())
Definition store-api.hh:591
kj::Promise< Result< ValidPathInfo > > addToStoreSlow(std::string_view name, const Path &srcPath, FileIngestionMethod method=FileIngestionMethod::Recursive, HashType hashAlgo=HashType::SHA256, std::optional< Hash > expectedCAHash={})
Definition store-api.cc:409
kj::Promise< void > clearPathInfoCache()
Definition store-api.hh:889
kj::Promise< Result< JSON > > pathInfoToJSON(const StorePathSet &storePaths, bool includeImpureInfo, bool showClosureSize, Base hashBase=Base::Base32, AllowInvalidFlag allowInvalid=DisallowInvalid)
Definition store-api.cc:953
kj::Promise< Result< bool > > isValidPath(const StorePath &path)
Definition store-api.cc:643
virtual kj::Promise< Result< std::optional< StorePath > > > queryPathFromHashPart(const std::string &hashPart)=0
virtual kj::Promise< Result< unsigned int > > getProtocol()
Definition store-api.hh:903
virtual kj::Promise< Result< BuildResult > > buildDerivation(const StorePath &drvPath, const BasicDerivation &drv, BuildMode buildMode=bmNormal)
Definition entry-points.cc:74
virtual kj::Promise< Result< StorePathSet > > queryDerivationOutputs(const StorePath &path)
Definition store-api.cc:573
virtual kj::Promise< Result< StorePathSet > > queryValidPaths(const StorePathSet &paths, SubstituteFlag maybeSubstitute=NoSubstitute)
Definition store-api.cc:821
kj::Promise< Result< StorePaths > > topoSortPaths(const StorePathSet &paths)
Definition misc.cc:376
Path followLinksToStore(std::string_view path) const
Definition store-api.cc:60
virtual std::optional< AssociatedCredentials > associatedCredentials() const
Definition store-api.hh:248
virtual kj::Promise< Result< std::optional< TrustedFlag > > > isTrustedClient()=0
virtual kj::Promise< Result< void > > setOptions()
Definition store-api.hh:933
kj::Promise< Result< OutputPathMap > > queryDerivationOutputMap(const StorePath &path, Store *evalStore=nullptr)
Definition store-api.cc:559
virtual kj::Promise< Result< std::shared_ptr< const ValidPathInfo > > > queryPathInfoUncached(const StorePath &path)=0
virtual ref< FSAccessor > getFSAccessor()=0
virtual kj::Promise< Result< void > > addTempRoot(const StorePath &path)
Definition store-api.hh:711
virtual kj::Promise< Result< void > > connect()
Definition store-api.hh:898
kj::Promise< Result< void > > substitutePaths(const StorePathSet &paths)
Definition store-api.cc:794
kj::Promise< Result< std::pair< uint64_t, uint64_t > > > getClosureSize(const StorePath &storePath)
Definition store-api.cc:1030
kj::Promise< Result< StorePaths > > importPaths(Source &source, CheckSigsFlag checkSigs=CheckSigs)
Definition export-import.cc:59
bool isInStore(PathView path) const
Definition store-api.cc:42
StorePath computeStorePathForText(std::string_view name, std::string_view s, const StorePathSet &references) const
Definition store-api.cc:272
virtual kj::Promise< Result< StorePath > > addTextToStore(std::string_view name, std::string_view s, const StorePathSet &references, RepairFlag repair=NoRepair)=0
virtual kj::Promise< Result< void > > addToStore(const ValidPathInfo &info, AsyncInputStream &narSource, RepairFlag repair=NoRepair, CheckSigsFlag checkSigs=CheckSigs)=0
std::string showPaths(const StorePathSet &paths)
Definition store-api.cc:1396
virtual kj::Promise< Result< void > > addMultipleToStore(PathsSource &pathsToCopy, Activity &act, RepairFlag repair=NoRepair, CheckSigsFlag checkSigs=CheckSigs)
Definition store-api.cc:313
virtual kj::Promise< Result< std::vector< KeyedBuildResult > > > buildPathsWithResults(const std::vector< DerivedPath > &paths, BuildMode buildMode=bmNormal, std::shared_ptr< Store > evalStore=nullptr)
Definition entry-points.cc:50
virtual kj::Promise< Result< void > > addSignatures(const StorePath &storePath, const StringSet &sigs)
Definition store-api.hh:781
kj::Promise< Result< std::string > > makeValidityRegistration(const StorePathSet &paths, bool showDerivers, bool showHash)
Definition store-api.cc:885
virtual kj::Promise< Result< std::map< std::string, std::optional< StorePath > > > > queryStaticPartialDerivationOutputMap(const StorePath &path)
Definition store-api.cc:517
virtual kj::Promise< Result< void > > init()
Definition store-api.hh:257
void unsupported(const std::string &op)
Definition store-api.hh:951
bool isStorePath(std::string_view path) const
Definition path.cc:90
kj::Promise< Result< void > > exportPaths(const StorePathSet &paths, Sink &sink)
Definition export-import.cc:12
virtual kj::Promise< Result< void > > computeFSClosure(const StorePathSet &paths, StorePathSet &out, bool flipDirection=false, bool includeOutputs=false, bool includeDerivers=false)
Definition misc.cc:17
std::vector< std::pair< ValidPathInfo, std::function< kj::Promise< Result< box_ptr< AsyncInputStream > > >()> > > PathsSource
Definition store-api.hh:542
virtual bool pathInfoIsUntrusted(const ValidPathInfo &)
Definition store-api.hh:428
virtual kj::Promise< Result< StorePathSet > > queryAllValidPaths()
Definition store-api.hh:400
StorePath computeStorePathForPathRecursive(std::string_view name, const PreparedDump &source) const
Definition store-api.cc:250
std::pair< StorePath, Path > toStorePath(PathView path) const
Definition store-api.cc:48
virtual kj::Promise< Result< box_ptr< Source > > > narFromPath(const StorePath &path)=0
virtual kj::Promise< Result< void > > queryReferrers(const StorePath &path, StorePathSet &referrers)
Definition store-api.hh:456
FileIngestionMethod
Definition content-address.hh:38
@ Recursive
Definition content-address.hh:47
const uint32_t exportMagic
Definition store-api.hh:103
Definition logging.hh:185
Definition derivations.hh:274
Definition build-result.hh:17
Definition content-address.hh:126
Definition derivations.hh:324
Definition realisation.hh:24
Definition build-result.hh:135
Definition realisation.hh:49
Definition derived-path.hh:101
Definition store-api.hh:122
virtual const std::string name()=0
virtual std::string doc()
Definition store-api.hh:141
virtual std::optional< ExperimentalFeature > experimentalFeature() const
Definition store-api.hh:150
Definition store-api.hh:1069
Definition store-api.hh:1082
Definition store-api.hh:238
Definition store-api.hh:198
std::shared_ptr< const ValidPathInfo > value
Definition store-api.hh:208
bool didExist()
Definition store-api.hh:220
bool isKnownNow()
Definition store-api.cc:507
std::chrono::time_point< std::chrono::steady_clock > time_point
Definition store-api.hh:203
Definition store-api.hh:226
Definition store-api.hh:853
Definition path-info.hh:83
Definition json-fwd.hh:21
std::string Path
Definition types.hh:28