diff --git a/src/Makefile.am b/src/Makefile.am index 3b519aa55d..a56a0ae0e8 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -1,13 +1,16 @@ bin_PROGRAMS = nix fix +noinst_PROGRAMS = test AM_CXXFLAGS = -DSYSTEM=\"@host@\" -Wall -nix_SOURCES = nix.cc db.cc util.cc md5.c +nix_SOURCES = nix.cc db.cc util.cc hash.cc md5.c nix_LDADD = -ldb_cxx-4 -lATerm -fix_SOURCES = fix.cc util.cc md5.c +fix_SOURCES = fix.cc util.cc hash.cc md5.c fix_LDADD = -lATerm +test_SOURCES = test.cc util.cc hash.cc md5.c + install-data-local: $(INSTALL) -d $(localstatedir)/nix $(INSTALL) -d $(localstatedir)/nix/descriptors diff --git a/src/fix.cc b/src/fix.cc index 30303322a1..3c4c8bf539 100644 --- a/src/fix.cc +++ b/src/fix.cc @@ -10,6 +10,7 @@ extern "C" { } #include "util.hh" +#include "hash.hh" static string nixDescriptorDir; @@ -20,7 +21,7 @@ static bool verbose = false; /* Mapping of Fix file names to the hashes of the resulting Nix descriptors. */ -typedef map DescriptorMap; +typedef map DescriptorMap; void registerFile(string filename) @@ -32,12 +33,13 @@ void registerFile(string filename) } -void registerURL(string hash, string url) +void registerURL(Hash hash, string url) { - int res = system(("nix regurl " + hash + " " + url).c_str()); + int res = system(("nix regurl " + (string) hash + " " + url).c_str()); /* !!! escape */ if (WEXITSTATUS(res) != 0) - throw Error("cannot register " + hash + " -> " + url + " with Nix"); + throw Error("cannot register " + + (string) hash + " -> " + url + " with Nix"); } @@ -61,7 +63,7 @@ struct EvalContext ATerm evaluate(ATerm e, EvalContext ctx); -string instantiateDescriptor(string filename, EvalContext ctx); +Hash instantiateDescriptor(string filename, EvalContext ctx); string evaluateStr(ATerm e, EvalContext ctx) @@ -101,7 +103,7 @@ ATerm evaluate(ATerm e, EvalContext ctx) ATmatch(e, "Pkg()", &s) || ATmatch(e, "File()", &s)) { - checkHash(s); + parseHash(s); return e; } @@ -131,7 +133,7 @@ ATerm evaluate(ATerm e, EvalContext ctx) else if (ATmatch(e, "Fix()", &e2)) { string filename = absPath(evaluateStr(e2, ctx), ctx.dir); /* !!! */ return ATmake("Pkg()", - instantiateDescriptor(filename, ctx).c_str()); + ((string) instantiateDescriptor(filename, ctx)).c_str()); } #if 0 @@ -160,19 +162,18 @@ ATerm evaluate(ATerm e, EvalContext ctx) hash. */ else if (ATmatch(e, "Local()", &e2)) { string filename = absPath(evaluateStr(e2, ctx), ctx.dir); /* !!! */ - string hash = hashFile(filename); + Hash hash = hashFile(filename); registerFile(filename); /* !!! */ - return ATmake("File()", hash.c_str()); + return ATmake("File()", ((string) hash).c_str()); } /* `Url' registers a mapping from a hash to an url with Nix, and returns the hash. */ else if (ATmatch(e, "Url(, )", &e2, &e3)) { - string hash = evaluateStr(e2, ctx); - checkHash(hash); + Hash hash = parseHash(evaluateStr(e2, ctx)); string url = evaluateStr(e3, ctx); registerURL(hash, url); - return ATmake("File()", hash.c_str()); + return ATmake("File()", ((string) hash).c_str()); } /* `If' provides conditional evaluation. */ @@ -199,7 +200,7 @@ string getStringFromMap(BindingsMap & bindingsMap, /* Instantiate a Fix descriptors into a Nix descriptor, recursively instantiating referenced descriptors as well. */ -string instantiateDescriptor(string filename, EvalContext ctx) +Hash instantiateDescriptor(string filename, EvalContext ctx) { /* Already done? */ DescriptorMap::iterator isInMap = ctx.done->find(filename); @@ -256,8 +257,9 @@ string instantiateDescriptor(string filename, EvalContext ctx) if (!ATwriteToNamedTextFile(outTerm, tmpFilename.c_str())) throw Error("cannot write aterm to " + tmpFilename); - string outHash = hashFile(tmpFilename); - string outFilename = nixDescriptorDir + "/" + id + "-" + outHash + ".nix"; + Hash outHash = hashFile(tmpFilename); + string outFilename = nixDescriptorDir + "/" + + id + "-" + (string) outHash + ".nix"; if (rename(tmpFilename.c_str(), outFilename.c_str())) throw Error("cannot rename " + tmpFilename + " to " + outFilename); @@ -265,7 +267,8 @@ string instantiateDescriptor(string filename, EvalContext ctx) registerFile(outFilename); if (verbose) - cerr << "instantiated " << outHash << " from " << filename << endl; + cerr << "instantiated " << (string) outHash + << " from " << filename << endl; (*ctx.done)[filename] = outHash; return outHash; @@ -284,7 +287,7 @@ void instantiateDescriptors(Strings filenames) it != filenames.end(); it++) { string filename = absPath(*it); - cout << instantiateDescriptor(filename, ctx) << endl; + cout << (string) instantiateDescriptor(filename, ctx) << endl; } } @@ -293,7 +296,7 @@ void instantiateDescriptors(Strings filenames) void printUsage() { cerr << -"Usage: fix ... +"Usage: fix ...\n\ "; } diff --git a/src/hash.cc b/src/hash.cc new file mode 100644 index 0000000000..25d76bd15c --- /dev/null +++ b/src/hash.cc @@ -0,0 +1,87 @@ +extern "C" { +#include "md5.h" +} + +#include "hash.hh" +#include + + +/* Create a zeroed hash object. */ +Hash::Hash() +{ + memset(hash, 0, sizeof(hash)); +} + + +/* Check whether two hash are equal. */ +bool Hash::operator == (Hash & h2) +{ + for (unsigned int i = 0; i < hashSize; i++) + if (hash[i] != h2.hash[i]) return false; + return true; +} + + +/* Check whether two hash are not equal. */ +bool Hash::operator != (Hash & h2) +{ + return !(*this == h2); +} + + +/* Convert a hash code into a hexadecimal representation. */ +Hash::operator string() const +{ + ostringstream str; + for (unsigned int i = 0; i < hashSize; i++) { + str.fill('0'); + str.width(2); + str << hex << (int) hash[i]; + } + return str.str(); +} + + +/* Parse a hexadecimal representation of a hash code. */ +Hash parseHash(const string & s) +{ + Hash hash; + for (unsigned int i = 0; i < Hash::hashSize; i++) { + string s2(s, i * 2, 2); + if (!isxdigit(s2[0]) || !isxdigit(s2[1])) + throw BadRefError("invalid hash: " + s); + istringstream str(s2); + int n; + str >> hex >> n; + hash.hash[i] = n; + } + return hash; +} + + +/* Verify that a reference is valid (that is, is a MD5 hash code). */ +bool isHash(const string & s) +{ + if (s.length() != 32) return false; + for (int i = 0; i < 32; i++) { + char c = s[i]; + if (!((c >= '0' && c <= '9') || + (c >= 'a' && c <= 'f'))) + return false; + } + return true; +} + + +/* Compute the MD5 hash of a file. */ +Hash hashFile(const string & fileName) +{ + Hash hash; + FILE * file = fopen(fileName.c_str(), "rb"); + if (!file) + throw Error("file `" + fileName + "' does not exist"); + int err = md5_stream(file, hash.hash); + fclose(file); + if (err) throw Error("cannot hash file"); + return hash; +} diff --git a/src/hash.hh b/src/hash.hh new file mode 100644 index 0000000000..162b2b1c8f --- /dev/null +++ b/src/hash.hh @@ -0,0 +1,34 @@ +#ifndef __HASH_H +#define __HASH_H + +#include + +#include "util.hh" + +using namespace std; + + +struct Hash +{ + static const unsigned int hashSize = 16; + unsigned char hash[hashSize]; + + Hash(); + bool operator == (Hash & h2); + bool operator != (Hash & h2); + operator string() const; +}; + + +class BadRefError : public Error +{ +public: + BadRefError(string _err) : Error(_err) { }; +}; + + +Hash parseHash(const string & s); +bool isHash(const string & s); +Hash hashFile(const string & fileName); + +#endif /* !__HASH_H */ diff --git a/src/nix.cc b/src/nix.cc index 7f5ca927bb..ea33e5e280 100644 --- a/src/nix.cc +++ b/src/nix.cc @@ -16,6 +16,7 @@ extern "C" { } #include "util.hh" +#include "hash.hh" #include "db.hh" using namespace std; @@ -58,7 +59,7 @@ string fetchURL(string url) database), we use that. Otherwise, we attempt to fetch it from the network (using dbNetSources). We verify that the file has the right hash. */ -string getFile(string hash) +string getFile(Hash hash) { bool checkedNet = false; @@ -77,10 +78,10 @@ string getFile(string hash) if (checkedNet) throw Error("consistency problem: file fetched from " + url + - " should have hash " + hash + ", but it doesn't"); + " should have hash " + (string) hash + ", but it doesn't"); if (!queryDB(nixDB, dbNetSources, hash, url)) - throw Error("a file with hash " + hash + " is requested, " + throw Error("a file with hash " + (string) hash + " is requested, " "but it is not known to exist locally or on the network"); checkedNet = true; @@ -95,7 +96,7 @@ string getFile(string hash) typedef map Params; -void readPkgDescr(const string & hash, +void readPkgDescr(Hash hash, Params & pkgImports, Params & fileImports, Params & arguments) { string pkgfile; @@ -112,15 +113,15 @@ void readPkgDescr(const string & hash, char * cname; ATerm value; while (ATmatch(bindings, "[Bind(, ), ]", - &cname, &value, &bindings)) + &cname, &value, &bindings)) { string name(cname); char * arg; if (ATmatch(value, "Pkg()", &arg)) { - checkHash(arg); + parseHash(arg); pkgImports[name] = arg; } else if (ATmatch(value, "File()", &arg)) { - checkHash(arg); + parseHash(arg); fileImports[name] = arg; } else if (ATmatch(value, "Str()", &arg)) arguments[name] = arg; @@ -136,13 +137,13 @@ void readPkgDescr(const string & hash, } -string getPkg(string hash); +string getPkg(Hash hash); typedef map Environment; -void fetchDeps(string hash, Environment & env) +void fetchDeps(Hash hash, Environment & env) { /* Read the package description file. */ Params pkgImports, fileImports, arguments; @@ -156,7 +157,7 @@ void fetchDeps(string hash, Environment & env) cerr << "fetching package dependency " << it->first << " <- " << it->second << endl; - env[it->first] = getPkg(it->second); + env[it->first] = getPkg(parseHash(it->second)); } for (Params::iterator it = fileImports.begin(); @@ -168,7 +169,7 @@ void fetchDeps(string hash, Environment & env) string file; - file = getFile(it->second); + file = getFile(parseHash(it->second)); env[it->first] = file; } @@ -198,7 +199,7 @@ string getFromEnv(const Environment & env, const string & key) } -string queryPkgId(const string & hash) +string queryPkgId(Hash hash) { Params pkgImports, fileImports, arguments; readPkgDescr(hash, pkgImports, fileImports, arguments); @@ -206,7 +207,7 @@ string queryPkgId(const string & hash) } -void installPkg(string hash) +void installPkg(Hash hash) { string pkgfile; string src; @@ -223,14 +224,15 @@ void installPkg(string hash) string id = getFromEnv(env, "id"); /* Construct a path for the installed package. */ - path = nixHomeDir + "/pkg/" + id + "-" + hash; + path = nixHomeDir + "/pkg/" + id + "-" + (string) hash; /* Create the path. */ if (mkdir(path.c_str(), 0777)) throw Error("unable to create directory " + path); /* Create a log file. */ - string logFileName = nixLogDir + "/" + id + "-" + hash + ".log"; + string logFileName = + nixLogDir + "/" + id + "-" + (string) hash + ".log"; /* !!! auto-pclose on exit */ FILE * logFile = popen(("tee " + logFileName + " >&2").c_str(), "w"); /* !!! escaping */ if (!logFile) @@ -256,11 +258,11 @@ void installPkg(string hash) } /* Try to use a prebuilt. */ - string prebuiltHash, prebuiltFile; - if (queryDB(nixDB, dbPrebuilts, hash, prebuiltHash)) { + string prebuiltHashS, prebuiltFile; + if (queryDB(nixDB, dbPrebuilts, hash, prebuiltHashS)) { try { - prebuiltFile = getFile(prebuiltHash); + prebuiltFile = getFile(parseHash(prebuiltHashS)); } catch (Error e) { cerr << "cannot obtain prebuilt (ignoring): " << e.what() << endl; goto build; @@ -339,17 +341,16 @@ void installPkg(string hash) } -string getPkg(string hash) +string getPkg(Hash hash) { string path; - checkHash(hash); while (!queryDB(nixDB, dbInstPkgs, hash, path)) installPkg(hash); return path; } -void runPkg(string hash, +void runPkg(Hash hash, Strings::iterator firstArg, Strings::iterator lastArg) { @@ -389,7 +390,7 @@ void runPkg(string hash, } -void ensurePkg(string hash) +void ensurePkg(Hash hash) { Params pkgImports, fileImports, arguments; readPkgDescr(hash, pkgImports, fileImports, arguments); @@ -403,10 +404,9 @@ void ensurePkg(string hash) } -void delPkg(string hash) +void delPkg(Hash hash) { string path; - checkHash(hash); if (queryDB(nixDB, dbInstPkgs, hash, path)) { int res = system(("chmod -R +w " + path + " && rm -rf " + path).c_str()); // !!! escaping delDB(nixDB, dbInstPkgs, hash); // not a bug ??? @@ -423,7 +423,7 @@ void exportPkgs(string outDir, outDir = absPath(outDir); for (Strings::iterator it = firstHash; it != lastHash; it++) { - string hash = *it; + Hash hash = parseHash(*it); string pkgDir = getPkg(hash); string tmpFile = outDir + "/export_tmp"; @@ -435,42 +435,38 @@ void exportPkgs(string outDir, string prebuiltHash = hashFile(tmpFile); string pkgId = queryPkgId(hash); string prebuiltFile = outDir + "/" + - pkgId + "-" + hash + "-" + prebuiltHash + ".tar.bz2"; + pkgId + "-" + (string) hash + "-" + prebuiltHash + ".tar.bz2"; rename(tmpFile.c_str(), prebuiltFile.c_str()); } } -void registerPrebuilt(string pkgHash, string prebuiltHash) +void registerPrebuilt(Hash pkgHash, Hash prebuiltHash) { - checkHash(pkgHash); - checkHash(prebuiltHash); setDB(nixDB, dbPrebuilts, pkgHash, prebuiltHash); } -string registerFile(string filename) +Hash registerFile(string filename) { filename = absPath(filename); - string hash = hashFile(filename); + Hash hash = hashFile(filename); setDB(nixDB, dbRefs, hash, filename); return hash; } -void registerURL(string hash, string url) +void registerURL(Hash hash, string url) { - checkHash(hash); setDB(nixDB, dbNetSources, hash, url); /* !!! currently we allow only one network source per hash */ } /* This is primarily used for bootstrapping. */ -void registerInstalledPkg(string hash, string path) +void registerInstalledPkg(Hash hash, string path) { - checkHash(hash); if (path == "") delDB(nixDB, dbInstPkgs, hash); else @@ -498,12 +494,13 @@ void verifyDB() it != fileRefs.end(); it++) { try { - if (hashFile(it->second) != it->first) { + Hash hash = parseHash(it->first); + if (hashFile(it->second) != hash) { cerr << "file " << it->second << " has changed\n"; delDB(nixDB, dbRefs, it->first); } - } catch (BadRefError e) { /* !!! better error check */ - cerr << "file " << it->second << " has disappeared\n"; + } catch (Error e) { /* !!! better error check */ + cerr << "error: " << e.what() << endl; delDB(nixDB, dbRefs, it->first); } } @@ -544,7 +541,7 @@ void printInfo(Strings::iterator first, Strings::iterator last) { for (Strings::iterator it = first; it != last; it++) { try { - cout << *it << " " << queryPkgId(*it) << endl; + cout << *it << " " << queryPkgId(parseHash(*it)) << endl; } catch (Error & e) { // !!! more specific cout << *it << " (descriptor missing)\n"; } @@ -559,7 +556,7 @@ void computeClosure(Strings::iterator first, Strings::iterator last, set doneSet; while (!workList.empty()) { - string hash = workList.front(); + Hash hash = parseHash(workList.front()); workList.pop_front(); if (doneSet.find(hash) == doneSet.end()) { @@ -605,7 +602,7 @@ void printGraph(Strings::iterator first, Strings::iterator last) it != allHashes.end(); it++) { Params pkgImports, fileImports, arguments; - readPkgDescr(*it, pkgImports, fileImports, arguments); + readPkgDescr(parseHash(*it), pkgImports, fileImports, arguments); cout << dotQuote(*it) << "[label = \"" << getFromEnv(arguments, "id") @@ -633,8 +630,8 @@ void fetch(string id) } /* Register it by hash. */ - string hash = registerFile(fn); - cout << hash << endl; + Hash hash = registerFile(fn); + cout << (string) hash << endl; } @@ -648,60 +645,60 @@ void fetch(Strings::iterator first, Strings::iterator last) void printUsage() { cerr << -"Usage: nix SUBCOMMAND OPTIONS... - -Subcommands: - - init - Initialize the database. - - verify - Remove stale entries from the database. - - regfile FILENAME... - Register each FILENAME keyed by its hash. - - reginst HASH PATH - Register an installed package. - - getpkg HASH... - For each HASH, ensure that the package referenced by HASH is - installed. Print out the path of the installation on stdout. - - delpkg HASH... - Uninstall the package referenced by each HASH, disregarding any - dependencies that other packages may have on HASH. - - listinst - Prints a list of installed packages. - - run HASH ARGS... - Run the descriptor referenced by HASH with the given arguments. - - ensure HASH... - Like getpkg, but if HASH refers to a run descriptor, fetch only - the dependencies. - - export DIR HASH... - Export installed packages to DIR. - - regprebuilt HASH1 HASH2 - Inform Nix that an export HASH2 can be used to fast-build HASH1. - - info HASH... - Print information about the specified descriptors. - - closure HASH... - Determine the closure of the set of descriptors under the import - relation, starting at the given roots. - - graph HASH... - Like closure, but print a dot graph specification. - - fetch ID... - Fetch the objects identified by ID and place them in the Nix - sources directory. ID can be a hash or URL. Print out the hash - of the object. +"Usage: nix SUBCOMMAND OPTIONS...\n\ +\n\ +Subcommands:\n\ +\n\ + init\n\ + Initialize the database.\n\ +\n\ + verify\n\ + Remove stale entries from the database.\n\ +\n\ + regfile FILENAME...\n\ + Register each FILENAME keyed by its hash.\n\ +\n\ + reginst HASH PATH\n\ + Register an installed package.\n\ +\n\ + getpkg HASH...\n\ + For each HASH, ensure that the package referenced by HASH is\n\ + installed. Print out the path of the installation on stdout.\n\ +\n\ + delpkg HASH...\n\ + Uninstall the package referenced by each HASH, disregarding any\n\ + dependencies that other packages may have on HASH.\n\ +\n\ + listinst\n\ + Prints a list of installed packages.\n\ +\n\ + run HASH ARGS...\n\ + Run the descriptor referenced by HASH with the given arguments.\n\ +\n\ + ensure HASH...\n\ + Like getpkg, but if HASH refers to a run descriptor, fetch only\n\ + the dependencies.\n\ +\n\ + export DIR HASH...\n\ + Export installed packages to DIR.\n\ +\n\ + regprebuilt HASH1 HASH2\n\ + Inform Nix that an export HASH2 can be used to fast-build HASH1.\n\ +\n\ + info HASH...\n\ + Print information about the specified descriptors.\n\ +\n\ + closure HASH...\n\ + Determine the closure of the set of descriptors under the import\n\ + relation, starting at the given roots.\n\ +\n\ + graph HASH...\n\ + Like closure, but print a dot graph specification.\n\ +\n\ + fetch ID...\n\ + Fetch the objects identified by ID and place them in the Nix\n\ + sources directory. ID can be a hash or URL. Print out the hash\n\ + of the object.\n\ "; } @@ -743,29 +740,31 @@ void run(Strings::iterator argCur, Strings::iterator argEnd) verifyDB(); } else if (cmd == "getpkg") { for (Strings::iterator it = argCur; it != argEnd; it++) { - string path = getPkg(*it); + string path = getPkg(parseHash(*it)); cout << path << endl; } } else if (cmd == "delpkg") { - for_each(argCur, argEnd, delPkg); + for (Strings::iterator it = argCur; it != argEnd; it++) + delPkg(parseHash(*it)); } else if (cmd == "run") { if (argc < 1) throw argcError; - runPkg(*argCur, argCur + 1, argEnd); + runPkg(parseHash(*argCur), argCur + 1, argEnd); } else if (cmd == "ensure") { - for_each(argCur, argEnd, ensurePkg); + for (Strings::iterator it = argCur; it != argEnd; it++) + ensurePkg(parseHash(*it)); } else if (cmd == "export") { if (argc < 1) throw argcError; exportPkgs(*argCur, argCur + 1, argEnd); } else if (cmd == "regprebuilt") { if (argc != 2) throw argcError; - registerPrebuilt(*argCur, argCur[1]); + registerPrebuilt(parseHash(argCur[0]), parseHash(argCur[1])); } else if (cmd == "regfile") { for_each(argCur, argEnd, registerFile); } else if (cmd == "regurl") { - registerURL(argCur[0], argCur[1]); + registerURL(parseHash(argCur[0]), argCur[1]); } else if (cmd == "reginst") { if (argc != 2) throw argcError; - registerInstalledPkg(*argCur, argCur[1]); + registerInstalledPkg(parseHash(argCur[0]), argCur[1]); } else if (cmd == "listinst") { if (argc != 0) throw argcError; listInstalledPkgs(); diff --git a/src/test.cc b/src/test.cc new file mode 100644 index 0000000000..cce226ba92 --- /dev/null +++ b/src/test.cc @@ -0,0 +1,16 @@ +#include + +#include "hash.hh" + +int main(int argc, char * * argv) +{ + Hash h = hashFile("/etc/passwd"); + + cout << (string) h << endl; + + h = parseHash("0b0ffd0538622bfe20b92c4aa57254d9"); + + cout << (string) h << endl; + + return 0; +} diff --git a/src/util.cc b/src/util.cc index 4b7bbac3dd..299fc942f2 100644 --- a/src/util.cc +++ b/src/util.cc @@ -27,53 +27,6 @@ string absPath(string filename, string dir) } -static string printHash(unsigned char * buf) -{ - ostringstream str; - for (int i = 0; i < 16; i++) { - str.fill('0'); - str.width(2); - str << hex << (int) buf[i]; - } - return str.str(); -} - - -/* Verify that a reference is valid (that is, is a MD5 hash code). */ -bool isHash(const string & s) -{ - if (s.length() != 32) return false; - for (int i = 0; i < 32; i++) { - char c = s[i]; - if (!((c >= '0' && c <= '9') || - (c >= 'a' && c <= 'f'))) - return false; - } - return true; -} - - -void checkHash(const string & s) -{ - if (!isHash(s)) throw BadRefError("invalid reference: " + s); -} - - -/* Compute the MD5 hash of a file. */ -string hashFile(string filename) -{ - unsigned char hash[16]; - FILE * file = fopen(filename.c_str(), "rb"); - if (!file) - throw BadRefError("file `" + filename + "' does not exist"); - int err = md5_stream(file, hash); - fclose(file); - if (err) throw BadRefError("cannot hash file"); - return printHash(hash); -} - - - /* Return the directory part of the given path, i.e., everything before the final `/'. */ string dirOf(string s) diff --git a/src/util.hh b/src/util.hh index 2c09efc4d8..d1a1956095 100644 --- a/src/util.hh +++ b/src/util.hh @@ -7,10 +7,6 @@ #include -extern "C" { -#include "md5.h" -} - using namespace std; @@ -29,12 +25,6 @@ public: UsageError(string _err) : Error(_err) { }; }; -class BadRefError : public Error -{ -public: - BadRefError(string _err) : Error(_err) { }; -}; - typedef vector Strings; @@ -50,9 +40,6 @@ extern string nixHomeDirEnvVar; string absPath(string filename, string dir = ""); -bool isHash(const string & s); -void checkHash(const string & s); -string hashFile(string filename); string dirOf(string s); string baseNameOf(string s);