diff options
-rw-r--r-- | Jamroot.jam | 0 | ||||
-rw-r--r-- | project2/Jamfile.jam | 20 | ||||
-rw-r--r-- | project2/envproc.cpp | 78 | ||||
-rw-r--r-- | project2/envproc.h | 33 | ||||
-rw-r--r-- | project2/p2web.cpp | 278 | ||||
-rw-r--r-- | project2/p2webMain.cpp | 42 | ||||
-rw-r--r-- | project2/rdbmsDataSource.cpp | 18 | ||||
-rw-r--r-- | project2/rdbmsDataSource.h | 23 | ||||
-rw-r--r-- | project2/sourceObject.cpp | 8 | ||||
-rw-r--r-- | project2/sourceObject.h | 13 | ||||
-rw-r--r-- | project2/sqlView.cpp | 78 | ||||
-rw-r--r-- | project2/sqlView.h | 23 | ||||
-rw-r--r-- | project2/ustring.h | 9 |
13 files changed, 623 insertions, 0 deletions
diff --git a/Jamroot.jam b/Jamroot.jam new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/Jamroot.jam diff --git a/project2/Jamfile.jam b/project2/Jamfile.jam new file mode 100644 index 0000000..7505f92 --- /dev/null +++ b/project2/Jamfile.jam @@ -0,0 +1,20 @@ +alias libxml2 : : : : + <cflags>"`pkg-config --cflags libxml-2.0`" + <linkflags>"`pkg-config --libs libxml-2.0`" ; + +lib fcgi : : <name>fcgi ; +lib odbc : : <name>odbc ; + +exe p2web : + p2webMain.cpp + sourceObject.cpp + rdbmsDataSource.cpp + sqlView.cpp + envproc.cpp + libxml2 + ../libmisc + ../libodbcpp : + <include>../libmisc/ + <include>../libodbcpp/ + <library>odbc + <library>fcgi ; diff --git a/project2/envproc.cpp b/project2/envproc.cpp new file mode 100644 index 0000000..0313c0f --- /dev/null +++ b/project2/envproc.cpp @@ -0,0 +1,78 @@ +#include "envproc.h" +#include "rdbmsDataSource.h" +#include "sqlView.h" +#include <map> +#include <libxml/tree.h> +#include <libxml/xinclude.h> +#include <boost/shared_ptr.hpp> +#include <boost/any.hpp> +#include <boost/foreach.hpp> + +void +EnvironmentProcessor::init() +{ + elems = regexExtractSet("/([^/?]+)", request_uri.c_str()); + params = regexExtractMulti("&?([^=]+)=?([^&]*)", query_string.c_str(), 2); + page = elems.size() > 0 ? elems[0] : "index"; +} + +template <class X, class Y> +void +collectAll(std::map<ustring, Y> & objs, xmlNodePtr node, const ustring & name) +{ + if (name == node->name) { + fprintf(stderr, "Found a %s\n", name.c_str()); + try { + objs[xmlGetProp(node, BAD_CAST "name")] = Y(new X(node)); + fprintf(stderr, "Load succeeded\n"); + } + catch (const std::exception & e) { + // Assume the XML node is what we thought it was + fprintf(stderr, "Load failed (%s)\n", e.what()); + } + catch (...) { + // Assume the XML node is what we thought it was + fprintf(stderr, "Load failed\n"); + } + } + else { + for (xmlNodePtr child = node->children; child; child = child->next) { + collectAll<X, Y>(objs, child, name); + } + } +} + +xmlDocPtr +EnvironmentProcessor::process() +{ + xmlDocPtr doc = xmlNewDoc(BAD_CAST "1.0"); + xmlDocPtr pageXDoc = xmlReadFile(("present/" + page + ".xml").c_str(), NULL, XML_PARSE_XINCLUDE); + while (xmlXIncludeProcess(pageXDoc) > 0); + xmlNodePtr root = xmlDocGetRootElement(pageXDoc); + // Collect datasources + RdbmsDataSources rdbmsDataSources; + collectAll<_RdbmsDataSource>(rdbmsDataSources, root, BAD_CAST "rdbmsdatasource"); + // Collect views + SqlViews sqlViews; + collectAll<_SqlView>(sqlViews, root, BAD_CAST "sqlview"); + // + xmlNodePtr root_node = xmlNewNode(NULL, xmlGetProp(root, BAD_CAST "root")); + try { + BOOST_FOREACH(SqlViews::value_type s, sqlViews) { + s.second->execute(rdbmsDataSources, root_node); + } + } + catch (...) { + } + xmlDocSetRootElement(doc, root_node); + char * buf; + if (asprintf(&buf, "type=\"text/xsl\" href=\"%s\"", + xmlGetProp(root, BAD_CAST "style")) > 0) { + xmlAddPrevSibling(root_node, + xmlNewDocPI(doc, BAD_CAST "xml-stylesheet", BAD_CAST buf)); + } + free(buf); + xmlFreeDoc(pageXDoc); + return doc; +} + diff --git a/project2/envproc.h b/project2/envproc.h new file mode 100644 index 0000000..64e13c1 --- /dev/null +++ b/project2/envproc.h @@ -0,0 +1,33 @@ +#ifndef ENVPROC_H +#define ENVPROC_H + +#include <string> +#include <libxml/tree.h> +#include "regexex.h" + +class EnvironmentProcessor { + public: + template <class getenvFunc> + EnvironmentProcessor(const getenvFunc & getenv) : + request_uri(getenv("REQUEST_URI")), + query_string(getenv("QUERY_STRING")), + http_host(getenv("HTTP_HOST")), + request_method(getenv("REQUEST_METHOD")) + { + init(); + } + virtual xmlDoc * process(); + private: + void init(); + + std::string request_uri; + std::string query_string; + std::string http_host; + std::string request_method; + + StringSet elems; + RegMultiMatch params; + std::string page; +}; + +#endif diff --git a/project2/p2web.cpp b/project2/p2web.cpp new file mode 100644 index 0000000..3d1b320 --- /dev/null +++ b/project2/p2web.cpp @@ -0,0 +1,278 @@ +#include <stdio.h> +#include <string.h> +#include <map> +#include <stdlib.h> +#include <libxml/tree.h> +#include "connection.h" +#include "selectcommand.h" +#include "column.h" +#include "smartpointer.h" +#include "dsn.h" +#include "xml.h" +#include "regexex.h" + +template <class X> +class Handle : public X, public IsRefCounted { + public: + template <class P1> + Handle(const P1 & p1) : X(p1) + { + } + Handle() + { + } +}; + +typedef Handle<ODBC::Connection> ConnHandle; +typedef SmartPointer<ConnHandle> ConnHandlePtr; +typedef std::map<int, ConnHandlePtr> ConnectionPool; + +void bindQueryParams(ODBC::Connection & db, ODBC::SelectCommand & sel, int pqid, ODBC::SelectCommand * parent, + const StringSet & elems, const RegMultiMatch & qs); + +void +addQueryResultsToXml(ODBC::Connection & wdb, ConnectionPool cp, xmlNode * root_node, int dsn, const ODBC::String & sql, + const ODBC::String & name, const ODBC::String & recordName, int pqid, ODBC::SelectCommand * parent, + const StringSet & elems, const RegMultiMatch & qs) +{ + typedef std::map<ODBC::String, xmlNode *> Columns; + xmlNode * resultXml = xmlNewChild(root_node, NULL, name.c_str(), NULL); + ODBC::SelectCommand query(*cp[dsn], sql); + bindQueryParams(wdb, query, pqid, parent, elems, qs); + while (query.fetch()) { + Columns columns; + unsigned int cols = query.columnCount(); + xmlNode * record = xmlNewChild(resultXml, NULL, recordName.c_str(), NULL); + for (unsigned int col = 0; col < cols; col += 1) { + const unsigned char * nameattr = query[col].name.c_str(); + unsigned char * name, * attr = NULL; + switch (sscanf((const char *)nameattr, "%a[^_]_%as", &name, &attr)) + { + case 0: + return; // Make me an exception + break; + case 1: + attr = NULL; + break; + } + char * buf = NULL; + query[col].writeToBuf(&buf); + if (buf) { + if (attr) { + if (strcmp((const char *)attr, ".") == 0) { + xmlNewProp(record, name, BAD_CAST buf); + } + else { + Columns::iterator i = columns.find(attr); + if (i != columns.end()) { + xmlNewProp(i->second, name, BAD_CAST buf); + } + } + } + else if (strcmp((const char *)name, "value") == 0) { + xmlNodeAddContent(record, BAD_CAST buf); + } + else { + columns[name] = xmlNewTextChild(record, NULL, name, BAD_CAST buf); + } + free(buf); + } + free(name); + free(attr); + } + ODBC::SelectCommand queries(wdb, + "SELECT d.id AS dsn, q.id AS query, q.sql, q.name, q.recordname, pq.id \ + FROM dsn d, page_query pq, query q \ + WHERE pq.parent = ? \ + AND q.id = pq.query \ + AND d.id = q.dsn"); + queries.bindParamI(0, pqid); + while (queries.fetch()) { + addQueryResultsToXml(wdb, cp, record, queries["dsn"], + queries["sql"], queries["name"], queries["recordname"], queries["id"], &query, elems, qs); + } + } +} + +void +bindQueryParams(ODBC::Connection & db, ODBC::SelectCommand & sel, int pqid, ODBC::SelectCommand * parent, + const StringSet & elems, const RegMultiMatch & qs) +{ + ODBC::SelectCommand params(db, + "SELECT paramidx, source, key \ + FROM page_query_params \ + WHERE pqid = ?"); + params.bindParamI(0, pqid); + while (params.fetch()) { + const char * key = params["key"]; + const char * src = params["source"]; + int idx = params["paramidx"]; + switch (*src) { + case 'v': // Value + sel.bindParamS(idx, key); + break; + case 'u': // URL token + sel.bindParamS(idx, elems[atoi(key)]); + break; + case 's': // Session variable + break; + case 'q': // Query string param + for(RegMultiMatch::const_iterator u = qs.begin(); u != qs.end(); u++) { + if ((*u)[0] == key) { + sel.bindParamS(idx, (*u)[1]); + } + } + break; + case 'p': // Parent column value + if (parent) { + (*parent)[key].rebind(&sel, idx); + } + break; + default: + ; + } + } +} + + +int +main(int, char**) +{ + try { + std::string request_uri(getenv("REQUEST_URI")); + std::string query_string(getenv("QUERY_STRING")); + StringSet elems(regexExtractSet("/([^/?]+)", request_uri.c_str())); + RegMultiMatch params(regexExtractMulti("&?([^=]+)=?([^&]*)", query_string.c_str(), 2)); + xmlDoc * doc = xmlNewDoc(BAD_CAST "1.0"); + xmlNode * root_node = NULL; + ODBC::String fqdn(getenv("HTTP_HOST")); + ODBC::DSN main("dbweb", "dbweb", "dbweb"); + ODBC::Connection db(main); + int pageId = 0; + std::string page(elems.size() > 0 ? elems[0] : "index"); + { + ODBC::SelectCommand pageHeaders(db, + "SELECT p.id, p.name, p.style, p.documentname \ + FROM site s, domainname dn, page p \ + WHERE s.id = dn.site \ + AND p.pattern = ? \ + AND dn.fqdn = ? \ + AND p.site = s.id"); + pageHeaders.bindParamS(0, page); + pageHeaders.bindParamS(1, fqdn); + while (pageHeaders.fetch()) { + pageId = pageHeaders["id"]; + root_node = xmlNewNode(NULL, pageHeaders["documentname"]); + xmlDocSetRootElement(doc, root_node); + if (!pageHeaders["style"].isNull()) { + printf("Content-type: text/xml-xslt\r\n"); + char * buf; + if (asprintf(&buf, "type=\"text/xsl\" href=\"%s\"", + pageHeaders["style"].operator const char *()) > 0) { + xmlAddPrevSibling(root_node, + xmlNewDocPI(doc, BAD_CAST "xml-stylesheet", BAD_CAST buf)); + } + free(buf); + } + else { + printf("Content-type: text/xml\r\n"); + } + xmlNewProp(root_node, BAD_CAST "title", pageHeaders["name"]); + } + } + // These were for debug... but why not pass them on? + xmlNsPtr dbwebns = xmlNewNs(root_node, BAD_CAST "http://dbweb.randomdan.homeip.net/", BAD_CAST "dbweb"); + xmlNewChild(root_node, dbwebns, BAD_CAST "fqdn", fqdn.c_str()); + xmlNewChild(root_node, dbwebns, BAD_CAST "requesturi", BAD_CAST request_uri.c_str()); + // URL elements + xmlNode * uriElems = xmlNewChild(root_node, dbwebns, BAD_CAST "uriElems", NULL); + for(StringSet::const_iterator u = elems.begin(); u != elems.end(); u++) { + xmlNewChild(uriElems, dbwebns, BAD_CAST "uriElem", BAD_CAST u->c_str()); + } + // Parameters + xmlNode * paramsXml = xmlNewChild(root_node, dbwebns, BAD_CAST "params", NULL); + for(RegMultiMatch::const_iterator u = params.begin(); u != params.end(); u++) { + xmlNode * param = xmlNewChild(paramsXml, NULL, BAD_CAST "param", BAD_CAST (*u)[1].c_str()); + xmlNewProp(param, BAD_CAST "name", BAD_CAST (*u)[0].c_str()); + } + ConnectionPool cp; + // Load DSNs +#ifdef DEBUG + xmlNode * dsnsXml = xmlNewChild(root_node, dbwebns, BAD_CAST "DSNs", NULL); +#endif + { + ODBC::SelectCommand dsns(db, + "SELECT DISTINCT d.id AS id, d.connectionstring AS connstr \ + FROM dsn d, page_query pq, query q \ + WHERE pq.page = ? \ + AND q.id = pq.query \ + AND d.id = q.dsn"); + dsns.bindParamI(0, pageId); + while (dsns.fetch()) { + int id = dsns["id"]; + ODBC::String connstr = dsns["connstr"]; +#ifdef DEBUG + xmlNode * dsnXml = xmlNewChild(dsnsXml, NULL, BAD_CAST "DSN", NULL); + xmlNewTextChildf(dsnXml, "id", "%d", id); + xmlNewTextChildf(dsnXml, "connectionstring", "%s", connstr.c_str()); +#endif + cp[id] = new ConnHandle(connstr); + } + } + // Exec queries + { + ODBC::SelectCommand queries(db, + "SELECT d.id AS dsn, q.id AS query, q.sql, q.name, q.recordname, pq.id \ + FROM dsn d, page_query pq, query q \ + WHERE pq.page = ? \ + AND q.id = pq.query \ + AND d.id = q.dsn \ + AND pq.parent IS NULL"); + queries.bindParamI(0, pageId); +#ifdef DEBUG + xmlNode * queriesXml = xmlNewChild(root_node, dbwebns, BAD_CAST "queries", NULL); +#endif + while (queries.fetch()) { +#ifdef DEBUG + int query = queries["query"]; +#endif + int dsn = queries["dsn"]; + ODBC::String sql = queries["sql"]; + ODBC::String name = queries["name"]; + ODBC::String recordName = queries["recordname"]; +#ifdef DEBUG + xmlNode * queryXml = xmlNewChild(queriesXml, NULL, BAD_CAST "query", NULL); + xmlNewTextChildf(queryXml, "query", "%d", query); + xmlNewTextChildf(queryXml, "name", "%s", name.c_str()); + xmlNewTextChildf(queryXml, "dsn", "%d", dsn); + xmlNewTextChildf(queryXml, "sql", "%s", sql.c_str()); +#endif + addQueryResultsToXml(db, cp, root_node, dsn, sql, name, recordName, queries["id"], NULL, elems, params); + } + } + printf("\r\n"); + xmlDocFormatDump(stdout, doc, 1); + xmlFreeDoc(doc); + } + catch (std::exception & e) { + printf("Content-type: text/xml\r\n"); + printf("\r\n"); + xmlDoc * doc = xmlNewDoc(BAD_CAST "1.0"); + xmlNode * root_node = xmlNewNode(NULL, BAD_CAST "error"); + xmlDocSetRootElement(doc, root_node); + xmlNewTextChildf(root_node, "what", "%s", e.what()); + xmlDocFormatDump(stdout, doc, 1); + } + catch (...) { + printf("Content-type: text/xml\r\n"); + printf("\r\n"); + xmlDoc * doc = xmlNewDoc(BAD_CAST "1.0"); + xmlNode * root_node = xmlNewNode(NULL, BAD_CAST "error"); + xmlNewTextChildf(root_node, "what", "Unknown"); + xmlNewTextChildf(root_node, "detail", "Exception thrown does not inherit from std::exception"); + xmlDocSetRootElement(doc, root_node); + xmlDocFormatDump(stdout, doc, 1); + } + return 0; +} + diff --git a/project2/p2webMain.cpp b/project2/p2webMain.cpp new file mode 100644 index 0000000..716495f --- /dev/null +++ b/project2/p2webMain.cpp @@ -0,0 +1,42 @@ +#include <libxml/tree.h> +#include <fcgi_stdio.h> +#include "envproc.h" +#include <boost/bind.hpp> + +int +xmlWrite(void * _out, const char * buf, int len) +{ + return FCGX_PutStr(buf, len, (FCGX_Stream*)_out); +} + +int main(void) +{ + FCGX_Stream *in, *_out, *err; + FCGX_ParamArray envp; + + while (FCGX_Accept(&in, &_out, &err, &envp) >= 0) + { + try { + EnvironmentProcessor ep(boost::bind(FCGX_GetParam, _1, envp)); + + xmlDoc * doc = ep.process(); + + FCGX_FPrintF(_out, "Content-type: text/xml-xslt\r\n\r\n"); + xmlOutputBufferPtr out = xmlOutputBufferCreateIO( + xmlWrite, NULL, _out, xmlGetCharEncodingHandler(XML_CHAR_ENCODING_UTF8)); + xmlSaveFileTo(out, doc, NULL); + xmlFreeDoc(doc); + } + catch (const std::exception & e) { + FCGX_FPrintF(_out, "Content-type: text/plain\r\n\r\n"); + FCGX_FPrintF(_out, "Kaboom!\r\n\r\n"); + FCGX_FPrintF(_out, "%s\r\n\r\n", e.what()); + } + catch (...) { + FCGX_FPrintF(_out, "Content-type: text/plain\r\n\r\n"); + FCGX_FPrintF(_out, "Kaboom!\r\n\r\n"); + FCGX_FPrintF(_out, "Unknown exception.\r\n\r\n"); + } + } + return 0; +} diff --git a/project2/rdbmsDataSource.cpp b/project2/rdbmsDataSource.cpp new file mode 100644 index 0000000..f4b3b4e --- /dev/null +++ b/project2/rdbmsDataSource.cpp @@ -0,0 +1,18 @@ +#include "rdbmsDataSource.h" +#include "xml.h" + +_RdbmsDataSource::_RdbmsDataSource(xmlNodePtr p) : + _Project2SourceObject(p), + masterDsn(xmlGetNodeValue<ustring>(p, BAD_CAST "masterdsn", BAD_CAST "")) +{ + fprintf(stderr, "Created RDBMS Datasource %s (%s)\n", name.c_str(), masterDsn.c_str()); +} + +_RdbmsDataSource::operator ODBC::Connection &() +{ + if (!database) { + database = boost::shared_ptr<ODBC::Connection>(new ODBC::Connection(masterDsn)); + } + return *database; +} + diff --git a/project2/rdbmsDataSource.h b/project2/rdbmsDataSource.h new file mode 100644 index 0000000..74ed829 --- /dev/null +++ b/project2/rdbmsDataSource.h @@ -0,0 +1,23 @@ +#ifndef RDBMSDATASOURCE_H +#define RDBMSDATASOURCE_H + +#include <libxml/tree.h> +#include <boost/shared_ptr.hpp> +#include <map> +#include "sourceObject.h" +#include "connection.h" +#include "ustring.h" + +class _RdbmsDataSource : public _Project2SourceObject { + public: + _RdbmsDataSource(xmlNodePtr p); + operator ODBC::Connection &(); + const ustring masterDsn; + private: + boost::shared_ptr<ODBC::Connection> database; +}; +typedef boost::shared_ptr<_RdbmsDataSource> RdbmsDataSource; +typedef std::map<ustring, RdbmsDataSource> RdbmsDataSources; + +#endif + diff --git a/project2/sourceObject.cpp b/project2/sourceObject.cpp new file mode 100644 index 0000000..a441e77 --- /dev/null +++ b/project2/sourceObject.cpp @@ -0,0 +1,8 @@ +#include "sourceObject.h" + +_Project2SourceObject::_Project2SourceObject(xmlNodePtr p) : + name(xmlGetProp(p, BAD_CAST "name")) +{ + fprintf(stderr, "Created object %s\n", name.c_str()); +} + diff --git a/project2/sourceObject.h b/project2/sourceObject.h new file mode 100644 index 0000000..d6531ff --- /dev/null +++ b/project2/sourceObject.h @@ -0,0 +1,13 @@ +#ifndef SOURCEOBJECT_H +#define SOURCEOBJECT_H + +#include "ustring.h" +#include <libxml/tree.h> + +class _Project2SourceObject { + public: + _Project2SourceObject(xmlNodePtr p); + const ustring name; +}; + +#endif diff --git a/project2/sqlView.cpp b/project2/sqlView.cpp new file mode 100644 index 0000000..ca1c135 --- /dev/null +++ b/project2/sqlView.cpp @@ -0,0 +1,78 @@ +#include "sqlView.h" +#include "xml.h" +#include "selectcommand.h" +#include "column.h" +#include <string.h> + +_SqlView::_SqlView(xmlNodePtr p) : + _Project2SourceObject(p), + sql(xmlGetNodeValue<ustring>(p, BAD_CAST "sql", BAD_CAST "")), + dataSource(xmlGetProp(p, BAD_CAST "datasource")), + recordName(xmlGetProp(p, BAD_CAST "recordname")) +{ +} + +void _SqlView::execute(RdbmsDataSources s, xmlNodePtr par) const +{ + typedef std::map<ODBC::String, xmlNode *> Columns; + fprintf(stderr, "executing\n"); + ODBC::SelectCommand query(*s[dataSource], sql); + xmlNode * set = xmlNewChild(par, NULL, name.c_str(), NULL); + while (query.fetch()) { + Columns columns; + unsigned int cols = query.columnCount(); + xmlNode * record = xmlNewChild(set, NULL, recordName.c_str(), NULL); + for (unsigned int col = 0; col < cols; col += 1) { + const unsigned char * nameattr = query[col].name.c_str(); + unsigned char * name, * attr = NULL; + switch (sscanf((const char *)nameattr, "%a[^_]_%as", &name, &attr)) + { + case 0: + return; // Make me an exception + break; + case 1: + attr = NULL; + break; + } + char * buf = NULL; + query[col].writeToBuf(&buf); + if (buf) { + if (attr) { + if (strcmp((const char *)attr, ".") == 0) { + xmlNewProp(record, name, BAD_CAST buf); + } + else { + Columns::iterator i = columns.find(attr); + if (i != columns.end()) { + xmlNewProp(i->second, name, BAD_CAST buf); + } + } + } + else if (strcmp((const char *)name, "value") == 0) { + xmlNodeAddContent(record, BAD_CAST buf); + } + else { + columns[name] = xmlNewTextChild(record, NULL, name, BAD_CAST buf); + } + free(buf); + } + free(name); + free(attr); + } + /* + ODBC::SelectCommand queries(wdb, + "SELECT d.id AS dsn, q.id AS query, q.sql, q.name, q.recordname, pq.id \ + FROM dsn d, page_query pq, query q \ + WHERE pq.parent = ? \ + AND q.id = pq.query \ + AND d.id = q.dsn"); + queries.bindParamI(0, pqid); + while (queries.fetch()) { + addQueryResultsToXml(wdb, cp, record, queries["dsn"], + queries["sql"], queries["name"], queries["recordname"], queries["id"], &query, elems, qs); + } + */ + } + fprintf(stderr, "finished\n"); +} + diff --git a/project2/sqlView.h b/project2/sqlView.h new file mode 100644 index 0000000..16812d2 --- /dev/null +++ b/project2/sqlView.h @@ -0,0 +1,23 @@ +#ifndef SQLVIEW_H +#define SQLVIEW_H + +#include <libxml/tree.h> +#include <boost/shared_ptr.hpp> +#include <map> +#include "sourceObject.h" +#include "ustring.h" +#include "rdbmsDataSource.h" + +class _SqlView : public _Project2SourceObject { + public: + _SqlView(xmlNodePtr p); + void execute(RdbmsDataSources s, xmlNodePtr) const; + const ustring sql; + const ustring dataSource; + const ustring recordName; +}; +typedef boost::shared_ptr<_SqlView> SqlView; +typedef std::map<ustring, SqlView> SqlViews; + +#endif + diff --git a/project2/ustring.h b/project2/ustring.h new file mode 100644 index 0000000..cfed0c4 --- /dev/null +++ b/project2/ustring.h @@ -0,0 +1,9 @@ +#ifndef USTRING_H +#define USTRING_H + +#include <string> + +typedef std::basic_string<unsigned char> ustring; + +#endif + |