mirror of https://github.com/adamdruppe/arsd.git
Compare commits
No commits in common. "master" and "v12.1.0" have entirely different histories.
|
|
@ -98,7 +98,6 @@ import arsd.rtf;
|
|||
// import arsd.screen; // D1 or 2.098
|
||||
import arsd.script;
|
||||
import arsd.sha;
|
||||
import arsd.shell;
|
||||
import arsd.simpleaudio;
|
||||
import arsd.simpledisplay;
|
||||
import arsd.sqlite;
|
||||
|
|
|
|||
524
database.d
524
database.d
|
|
@ -109,8 +109,6 @@ interface Database {
|
|||
import arsd.core;
|
||||
Variant[] vargs;
|
||||
string sql;
|
||||
|
||||
// FIXME: use the new helper functions sqlFromInterpolatedArgs and variantsFromInterpolatedArgs
|
||||
foreach(arg; args) {
|
||||
static if(is(typeof(arg) == InterpolatedLiteral!str, string str)) {
|
||||
sql ~= str;
|
||||
|
|
@ -125,19 +123,10 @@ interface Database {
|
|||
}
|
||||
return queryImpl(sql, vargs);
|
||||
}
|
||||
// see test/dbis.d
|
||||
|
||||
/// turns a systime into a value understandable by the target database as a timestamp to be concated into a query. so it should be quoted and escaped etc as necessary
|
||||
string sysTimeToValue(SysTime);
|
||||
|
||||
/++
|
||||
Return true if the connection appears to be alive
|
||||
|
||||
History:
|
||||
Added October 30, 2025
|
||||
+/
|
||||
bool isAlive();
|
||||
|
||||
/// Prepared statement api
|
||||
/*
|
||||
PreparedStatement prepareStatement(string sql, int numberOfArguments);
|
||||
|
|
@ -360,519 +349,6 @@ interface ResultSet {
|
|||
/* deprecated */ final ResultSet byAssoc() { return this; }
|
||||
}
|
||||
|
||||
/++
|
||||
Converts a database result set to a html table, using [arsd.dom].
|
||||
|
||||
History:
|
||||
Added October 29, 2025
|
||||
+/
|
||||
auto resultSetToHtmlTable()(ResultSet resultSet) {
|
||||
import arsd.dom;
|
||||
|
||||
Table table = cast(Table) Element.make("table");
|
||||
table.appendHeaderRow(resultSet.fieldNames);
|
||||
foreach(row; resultSet) {
|
||||
table.appendRow(row.toStringArray());
|
||||
}
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
abstract class ConnectionPoolBase : arsd.core.SynchronizableObject {
|
||||
protected struct DatabaseListItem {
|
||||
Database db;
|
||||
DatabaseListItem* nextAvailable;
|
||||
}
|
||||
|
||||
private DatabaseListItem* firstAvailable;
|
||||
|
||||
// FIXME: add a connection count limit and some kind of wait mechanism for one to become available
|
||||
|
||||
final protected void makeAvailable(DatabaseListItem* what) {
|
||||
synchronized(this) {
|
||||
auto keep = this.firstAvailable;
|
||||
what.nextAvailable = keep;
|
||||
this.firstAvailable = what;
|
||||
}
|
||||
}
|
||||
|
||||
final protected DatabaseListItem* getNext() {
|
||||
DatabaseListItem* toUse;
|
||||
synchronized(this) {
|
||||
if(this.firstAvailable !is null) {
|
||||
toUse = this.firstAvailable;
|
||||
this.firstAvailable = this.firstAvailable.nextAvailable;
|
||||
}
|
||||
}
|
||||
|
||||
return toUse;
|
||||
}
|
||||
}
|
||||
|
||||
/++
|
||||
PooledConnection is an RAII holder for a database connection that is automatically recycled to the pool it came from (unless you [discard] it).
|
||||
|
||||
History:
|
||||
Added October 29, 2025
|
||||
+/
|
||||
struct PooledConnection(ConnectionPoolType) {
|
||||
private ConnectionPoolType.DatabaseListItem* dli;
|
||||
private ConnectionPoolType pool;
|
||||
private bool discarded;
|
||||
private this(ConnectionPoolType.DatabaseListItem* dli, ConnectionPoolType pool) {
|
||||
this.dli = dli;
|
||||
this.pool = pool;
|
||||
}
|
||||
|
||||
@disable this(this);
|
||||
|
||||
/++
|
||||
Indicates you want the connection discarded instead of returned to the pool when you're finished with it.
|
||||
|
||||
You should call this if you know the connection is dead.
|
||||
+/
|
||||
void discard() {
|
||||
this.discarded = true;
|
||||
}
|
||||
|
||||
~this() {
|
||||
import core.memory;
|
||||
if(GC.inFinalizer)
|
||||
return;
|
||||
if(!discarded && dli.db.isAlive) {
|
||||
// FIXME: a connection must not be returned to the pool unless it is both alive and idle; any pending query work would screw up the next user
|
||||
// it is the user's responsibility to live with other state though like prepared statements or whatever saved per-connection.
|
||||
pool.makeAvailable(dli);
|
||||
}
|
||||
}
|
||||
|
||||
/++
|
||||
|
||||
+/
|
||||
ConnectionPoolType.DriverType borrow() @system return {
|
||||
return cast(ConnectionPoolType.DriverType) dli.db; // static_cast
|
||||
}
|
||||
|
||||
/++
|
||||
+/
|
||||
ResultSet rtQuery(T...)(T t) {
|
||||
return dli.db.query(t);
|
||||
}
|
||||
|
||||
/++
|
||||
|
||||
+/
|
||||
template query(string file = __FILE__, size_t line = __LINE__, Args...) {
|
||||
enum asSql = sqlFromInterpolatedArgs!(Args);
|
||||
__gshared queryMetadata = new QueryMetadata!(asSql, file, line);
|
||||
@(arsd.core.standalone) @system shared static this() {
|
||||
ConnectionPoolType.registeredQueries_ ~= queryMetadata;
|
||||
}
|
||||
|
||||
auto query(arsd.core.InterpolationHeader ihead, Args args, arsd.core.InterpolationFooter ifoot) {
|
||||
return new QueryResult!queryMetadata(dli.db.queryImpl(asSql, variantsFromInterpolatedArgs(args)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/++
|
||||
|
||||
+/
|
||||
unittest {
|
||||
import arsd.database;
|
||||
|
||||
shared dbPool = new shared ConnectionPool!(() => new MockDatabase())();
|
||||
|
||||
void main() {
|
||||
auto db = dbPool.get();
|
||||
foreach(row; db.query(i"SELECT * FROM test")) {
|
||||
if(row.id.isNull)
|
||||
continue;
|
||||
auto id = row.id.get!int;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
main(); // remove from docs
|
||||
}
|
||||
|
||||
private Variant[] variantsFromInterpolatedArgs(Args...)(Args args) {
|
||||
Variant[] ret;
|
||||
|
||||
import arsd.core;
|
||||
|
||||
foreach(arg; args) {
|
||||
static if(is(typeof(arg) == InterpolationHeader))
|
||||
{}
|
||||
else
|
||||
static if(is(typeof(arg) == InterpolationFooter))
|
||||
{}
|
||||
else
|
||||
static if(is(typeof(arg) == InterpolatedLiteral!sql, string sql))
|
||||
{}
|
||||
else
|
||||
static if(is(typeof(arg) == InterpolatedExpression!code, string code))
|
||||
{}
|
||||
else
|
||||
static if(is(typeof(arg) == AdHocBuiltStruct!(tag, names, Values), string tag, string[] names, Values...)) {
|
||||
static if(tag == "VALUES") {
|
||||
foreach(value; arg.values) {
|
||||
static if(is(value == sql_!code, string code)) {
|
||||
// intentionally blank
|
||||
} else {
|
||||
ret ~= Variant(value);
|
||||
}
|
||||
}
|
||||
|
||||
} else static assert(0);
|
||||
}
|
||||
// FIXME: iraw and sql!"" too and VALUES
|
||||
else
|
||||
ret ~= Variant(arg);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private string sqlFromInterpolatedArgs(Args...)() {
|
||||
string ret;
|
||||
|
||||
import arsd.core;
|
||||
|
||||
foreach(arg; Args) {
|
||||
static if(is(arg == InterpolationHeader))
|
||||
{}
|
||||
else
|
||||
static if(is(arg == InterpolationFooter))
|
||||
{}
|
||||
else
|
||||
static if(is(arg == InterpolatedLiteral!sql, string sql))
|
||||
ret ~= sql;
|
||||
else
|
||||
static if(is(arg == InterpolatedExpression!code, string code))
|
||||
{}
|
||||
else
|
||||
static if(is(arg == AdHocBuiltStruct!(tag, names, values), string tag, string[] names, values...)) {
|
||||
static if(tag == "VALUES") {
|
||||
ret ~= "(";
|
||||
foreach(idx, name; names) {
|
||||
if(idx) ret ~= ", ";
|
||||
ret ~= name;
|
||||
}
|
||||
ret ~= ") VALUES (";
|
||||
foreach(idx, value; values) {
|
||||
if(idx) ret ~= ", ";
|
||||
|
||||
static if(is(value == sql_!code, string code)) {
|
||||
ret ~= code;
|
||||
} else {
|
||||
ret ~= "?";
|
||||
}
|
||||
}
|
||||
ret ~= ")";
|
||||
}
|
||||
else static assert(0);
|
||||
}
|
||||
// FIXME: iraw and sql_!"" too
|
||||
else
|
||||
ret ~= "?";
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
/+
|
||||
+/
|
||||
|
||||
struct AssociatedDatabaseDatum(alias queryMetadata, string name, string file, size_t line) {
|
||||
@(arsd.core.standalone) @system shared static this() {
|
||||
queryMetadata.registerName(name, file, line);
|
||||
}
|
||||
|
||||
template get(T, string file = __FILE__, size_t line = __LINE__) {
|
||||
shared static this() {
|
||||
// FIXME: empty string and null must be distinguishable in arsd.core
|
||||
static if(is(T == string))
|
||||
T t = "sample";
|
||||
else
|
||||
T t = T.init;
|
||||
queryMetadata.registerType(name, LimitedVariant(t), T.stringof, file, line);
|
||||
}
|
||||
|
||||
T get() {
|
||||
import std.conv;
|
||||
return datum.toString().to!T;
|
||||
}
|
||||
}
|
||||
|
||||
DatabaseDatum datum;
|
||||
|
||||
bool isNull() {
|
||||
return datum.isNull();
|
||||
}
|
||||
|
||||
string toString() {
|
||||
if(isNull)
|
||||
return null;
|
||||
else
|
||||
return datum.toString();
|
||||
}
|
||||
|
||||
alias toString this;
|
||||
}
|
||||
|
||||
private abstract class QueryResultBase {
|
||||
|
||||
}
|
||||
|
||||
class QueryResult(alias queryMetadata) : QueryResultBase {
|
||||
private ResultSet resultSet;
|
||||
|
||||
this(ResultSet resultSet) {
|
||||
this.resultSet = resultSet;
|
||||
}
|
||||
|
||||
QueryResultRow!queryMetadata front() {
|
||||
return new QueryResultRow!queryMetadata(resultSet.front);
|
||||
}
|
||||
|
||||
bool empty() {
|
||||
return resultSet.empty;
|
||||
}
|
||||
|
||||
void popFront() {
|
||||
resultSet.popFront();
|
||||
}
|
||||
}
|
||||
|
||||
class QueryResultRow(alias queryMetadata) {
|
||||
Row row;
|
||||
this(Row row) {
|
||||
this.row = row;
|
||||
}
|
||||
|
||||
AssociatedDatabaseDatum!(queryMetadata, name, file, line) opDispatch(string name, string file = __FILE__, size_t line = __LINE__)() if(name != "__dtor") {
|
||||
return typeof(return)(row[name]);
|
||||
}
|
||||
|
||||
// i could support an opSlice. maybe opIndex tho it won't be CT bound checked w/o a ct!0 thing
|
||||
// also want opApply which discards type check prolly and just gives you the datum.
|
||||
|
||||
int opApply(int delegate(string, DatabaseDatum) dg) {
|
||||
string[] fn = row.resultSet.fieldNames();
|
||||
foreach(idx, item; row.row)
|
||||
mixin(yield("fn[idx], item"));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// ditto
|
||||
int opApply(int delegate(DatabaseDatum) dg) {
|
||||
foreach(item; row.row)
|
||||
mixin(yield("item"));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
struct ReferencedColumn {
|
||||
string name;
|
||||
LimitedVariant sampleData;
|
||||
string assumedType;
|
||||
string actualType;
|
||||
string file;
|
||||
size_t line;
|
||||
}
|
||||
|
||||
class QueryMetadataBase {
|
||||
ReferencedColumn[] names;
|
||||
void registerName(string name, string file, size_t line) {
|
||||
names ~= ReferencedColumn(name, LimitedVariant.init, null, null, file, line);
|
||||
}
|
||||
void registerType(string name, LimitedVariant sample, string type, string file, size_t line) {
|
||||
foreach(ref n; names)
|
||||
if(n.name == name) {
|
||||
if(n.assumedType.length && type.length) {
|
||||
n.actualType = type;
|
||||
}
|
||||
n.assumedType = type;
|
||||
n.sampleData = sample;
|
||||
n.file = file;
|
||||
n.line = line;
|
||||
return;
|
||||
}
|
||||
names ~= ReferencedColumn(name, sample, type, type, file, line);
|
||||
}
|
||||
|
||||
abstract string sql() const;
|
||||
abstract string file() const;
|
||||
abstract size_t line() const;
|
||||
}
|
||||
|
||||
class QueryMetadata(string q, string file_, size_t line_) : QueryMetadataBase {
|
||||
override string sql() const { return q; }
|
||||
override string file() const { return file_; }
|
||||
override size_t line() const { return line_; }
|
||||
}
|
||||
|
||||
version(unittest)
|
||||
class MockDatabase : Database {
|
||||
void startTransaction() {}
|
||||
string sysTimeToValue(SysTime s) { return null; }
|
||||
bool isAlive() { return true; }
|
||||
|
||||
ResultSet queryImpl(string sql, Variant[] args...) {
|
||||
return new PredefinedResultSet(null, null);
|
||||
}
|
||||
string escape(string sqlData) {
|
||||
return null;
|
||||
}
|
||||
string escapeBinaryString(const(ubyte)[] sqlData) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/++
|
||||
Helpers for interpolated queries.
|
||||
|
||||
History:
|
||||
Added October 31, 2025
|
||||
|
||||
See_Also:
|
||||
[arsd.core.iraw]
|
||||
+/
|
||||
auto VALUES() {
|
||||
import arsd.core;
|
||||
return AdHocBuiltStruct!"VALUES"();
|
||||
}
|
||||
|
||||
/// ditto
|
||||
auto sql(string s)() {
|
||||
return sql_!s();
|
||||
}
|
||||
|
||||
private struct sql_(string s) { }
|
||||
|
||||
/++
|
||||
A ConnectionPool manages a set of shared connections to a database.
|
||||
|
||||
|
||||
Create one like this:
|
||||
|
||||
---
|
||||
// at top level
|
||||
shared dbPool = new shared ConnectionPool!(() => new PostgreSql("dbname=me"))();
|
||||
|
||||
void main() {
|
||||
auto db = dbPool.get(); // in the function, get it and use it temporarily
|
||||
}
|
||||
---
|
||||
|
||||
History:
|
||||
Added October 29, 2025
|
||||
+/
|
||||
class ConnectionPool(alias connectionFactory) : ConnectionPoolBase {
|
||||
private alias unsharedThis = ConnectionPool!connectionFactory;
|
||||
|
||||
static if(is(typeof(connectionFactory) DriverType == return)) {
|
||||
static if(!is(DriverType : Database))
|
||||
static assert(0, "unusable connectionFactory - it needs to return an instance of Database");
|
||||
} else {
|
||||
static assert(0, "unusable connectionFactory - it needs to be a function");
|
||||
}
|
||||
|
||||
private __gshared QueryMetadataBase[] registeredQueries_;
|
||||
|
||||
immutable(QueryMetadataBase[]) registeredQueries() shared {
|
||||
return cast(immutable(QueryMetadataBase[])) registeredQueries_;
|
||||
}
|
||||
|
||||
bool checkQueries()(DriverType db) shared {
|
||||
bool succeeded = true;
|
||||
|
||||
import arsd.postgres; // FIXME is this really postgres only? looks like sqlite has no similar function... maybe make a view then sqlite3_table_column_metadata ?
|
||||
static assert(is(DriverType == PostgreSql), "Only implemented for postgres right now");
|
||||
|
||||
int count;
|
||||
import arsd.core;
|
||||
import arsd.conv;
|
||||
foreach(q; registeredQueries) {
|
||||
//writeln(q.file, ":", q.line, " ", q.sql);
|
||||
try {
|
||||
try {
|
||||
string dbSpecificSql;
|
||||
int placeholderNumber = 1;
|
||||
size_t lastCopied = 0;
|
||||
foreach(idx, ch; q.sql) {
|
||||
if(ch == '?') {
|
||||
dbSpecificSql ~= q.sql[lastCopied .. idx];
|
||||
lastCopied = idx + 1;
|
||||
dbSpecificSql ~= "$" ~ to!string(placeholderNumber);
|
||||
placeholderNumber++;
|
||||
}
|
||||
}
|
||||
dbSpecificSql ~= q.sql[lastCopied .. $];
|
||||
// FIXME: pipeline this
|
||||
db.query("PREPARE thing_"~to!string(++count)~" AS " ~ dbSpecificSql);
|
||||
} catch(Exception e) {
|
||||
e.file = q.file;
|
||||
e.line = q.line;
|
||||
throw e;
|
||||
// continue;
|
||||
}
|
||||
// this mysql function looks about right: https://dev.mysql.com/doc/c-api/8.0/en/mysql-stmt-result-metadata.html
|
||||
// could maybe emulate by trying it in a rolled back transaction though.
|
||||
auto desca = describePrepared(db,"thing_"~arsd.conv.to!string(count));
|
||||
LimitedVariant[string] byName;
|
||||
foreach(col; desca.result) {
|
||||
byName[col.fieldName] = col.type.storage;
|
||||
}
|
||||
|
||||
foreach(name; q.names) {
|
||||
if(name.name !in byName)
|
||||
throw ArsdException!"you reference unknown field"(name.name, name.file, name.line);
|
||||
if(name.assumedType.length == 0)
|
||||
continue;
|
||||
if(byName[name.name].contains != name.sampleData.contains)
|
||||
throw ArsdException!"type mismatch"(
|
||||
name.name,
|
||||
arsd.conv.to!string(byName[name.name].contains),
|
||||
arsd.conv.to!string(name.sampleData.contains),
|
||||
name.file,
|
||||
name.line,
|
||||
);
|
||||
|
||||
// i think this is redundant
|
||||
if(name.assumedType.length && name.actualType.length && name.actualType != name.assumedType) {
|
||||
throw ArsdException!"usage mismatch"(name.assumedType, name.actualType, name.file, name.line);
|
||||
}
|
||||
}
|
||||
} catch(Exception e) {
|
||||
writeln(e.toString());
|
||||
succeeded = false;
|
||||
}
|
||||
}
|
||||
|
||||
if(!succeeded)
|
||||
writeln("db check failed.");
|
||||
|
||||
return succeeded;
|
||||
}
|
||||
|
||||
/++
|
||||
|
||||
+/
|
||||
public PooledConnection!(unsharedThis) get() shared {
|
||||
auto toUse = (cast(unsharedThis) this).getNext();
|
||||
|
||||
if(toUse is null)
|
||||
toUse = new DatabaseListItem(connectionFactory());
|
||||
|
||||
return PooledConnection!(unsharedThis)(toUse, cast(unsharedThis) this);
|
||||
}
|
||||
}
|
||||
|
||||
class DatabaseException : Exception {
|
||||
this(string msg, string file = __FILE__, size_t line = __LINE__) {
|
||||
super(msg, file, line);
|
||||
|
|
|
|||
43
dom.d
43
dom.d
|
|
@ -1,5 +1,3 @@
|
|||
// FIXME: i want css nesting via the new standard now.
|
||||
|
||||
// FIXME: xml namespace support???
|
||||
// FIXME: https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentHTML
|
||||
// FIXME: parentElement is parentNode that skips DocumentFragment etc but will be hard to work in with my compatibility...
|
||||
|
|
@ -2697,47 +2695,6 @@ class Element : DomParent {
|
|||
return m;
|
||||
}
|
||||
|
||||
/++
|
||||
Makes an element from an interpolated sequence.
|
||||
|
||||
FIXME: add a type interpolator thing that can be replaced
|
||||
FIXME: syntax check at compile time?
|
||||
FIXME: allow a DocumentFragment in some cases
|
||||
+/
|
||||
static Element make(Args...)(arsd.core.InterpolationHeader head, Args args, arsd.core.InterpolationFooter foot) {
|
||||
string html;
|
||||
|
||||
import arsd.core;
|
||||
foreach(arg; args) {
|
||||
static if(is(typeof(arg) == InterpolationHeader))
|
||||
{}
|
||||
else
|
||||
static if(is(typeof(arg) == InterpolationFooter))
|
||||
{}
|
||||
else
|
||||
static if(is(typeof(arg) == InterpolatedLiteral!h, string h))
|
||||
html ~= h;
|
||||
else
|
||||
static if(is(typeof(arg) == InterpolatedExpression!code, string code))
|
||||
{}
|
||||
else
|
||||
static if(is(typeof(arg) : iraw))
|
||||
html ~= arg.s;
|
||||
else
|
||||
// FIXME: what if we are inside a <script> ? or an attribute etc
|
||||
static if(is(typeof(arg) : Html))
|
||||
html ~= arg.source;
|
||||
else
|
||||
static if(is(typeof(arg) : Element))
|
||||
html ~= arg.toString();
|
||||
else
|
||||
html ~= htmlEntitiesEncode(toStringInternal(arg));
|
||||
}
|
||||
|
||||
auto root = Element.make("root");
|
||||
root.innerHTML(html, true /* strict mode */);
|
||||
return root.querySelector(" > *");
|
||||
}
|
||||
|
||||
/// Generally, you don't want to call this yourself - use Element.make or document.createElement instead.
|
||||
this(Document _parentDocument, string _tagName, string[string] _attributes = null, bool _selfClosed = false) {
|
||||
|
|
|
|||
28
dub.json
28
dub.json
|
|
@ -11,30 +11,10 @@
|
|||
"description": "Shared components across other arsd modules",
|
||||
"targetType": "library",
|
||||
"libs-windows": ["user32", "ws2_32"],
|
||||
"dflags-dmd": [
|
||||
"-mv=arsd.core=$PACKAGE_DIR/core.d",
|
||||
"-mv=arsd.string=$PACKAGE_DIR/string.d",
|
||||
"-mv=arsd.uri=$PACKAGE_DIR/uri.d",
|
||||
"-mv=arsd.conv=$PACKAGE_DIR/conv.d"
|
||||
],
|
||||
"dflags-ldc": [
|
||||
"--mv=arsd.core=$PACKAGE_DIR/core.d",
|
||||
"--mv=arsd.string=$PACKAGE_DIR/string.d",
|
||||
"--mv=arsd.uri=$PACKAGE_DIR/uri.d",
|
||||
"--mv=arsd.conv=$PACKAGE_DIR/conv.d"
|
||||
],
|
||||
"dflags-gdc": [
|
||||
"-fmodule-file=arsd.core=$PACKAGE_DIR/core.d",
|
||||
"-fmodule-file=arsd.string=$PACKAGE_DIR/string.d",
|
||||
"-fmodule-file=arsd.uri=$PACKAGE_DIR/uri.d",
|
||||
"-fmodule-file=arsd.conv=$PACKAGE_DIR/conv.d"
|
||||
],
|
||||
"sourceFiles": [
|
||||
"core.d",
|
||||
"string.d",
|
||||
"uri.d",
|
||||
"conv.d"
|
||||
]
|
||||
"dflags-dmd": ["-mv=arsd.core=$PACKAGE_DIR/core.d"],
|
||||
"dflags-ldc": ["--mv=arsd.core=$PACKAGE_DIR/core.d"],
|
||||
"dflags-gdc": ["-fmodule-file=arsd.core=$PACKAGE_DIR/core.d"],
|
||||
"sourceFiles": ["core.d"]
|
||||
},
|
||||
{
|
||||
"name": "simpledisplay",
|
||||
|
|
|
|||
14
minigui.d
14
minigui.d
|
|
@ -17879,26 +17879,15 @@ class FilePicker : Dialog {
|
|||
return -1;
|
||||
|
||||
enum specialZoneSize = 1;
|
||||
string originalString = whole;
|
||||
bool fallback;
|
||||
|
||||
start_over:
|
||||
|
||||
char current = whole[0];
|
||||
if(!fallback && current >= '0' && current <= '9') {
|
||||
// if this overflows, it can mess up the sort, so it will fallback to string sort in that event
|
||||
if(current >= '0' && current <= '9') {
|
||||
int accumulator;
|
||||
do {
|
||||
auto before = accumulator;
|
||||
whole = whole[1 .. $];
|
||||
accumulator *= 10;
|
||||
accumulator += current - '0';
|
||||
current = whole.length ? whole[0] : 0;
|
||||
if(accumulator < before) {
|
||||
fallback = true;
|
||||
whole = originalString;
|
||||
goto start_over;
|
||||
}
|
||||
} while (current >= '0' && current <= '9');
|
||||
|
||||
return accumulator + specialZoneSize + cast(int) char.max; // leave room for symbols
|
||||
|
|
@ -18288,7 +18277,6 @@ private FileType getFileType(string name) {
|
|||
auto ret = stat((name ~ '\0').ptr, &buf);
|
||||
if(ret == -1)
|
||||
return FileType.error;
|
||||
// FIXME: what about a symlink to a dir? S_IFLNK then readlink then i believe stat it again.
|
||||
return ((buf.st_mode & S_IFMT) == S_IFDIR) ? FileType.dir : FileType.other;
|
||||
} else assert(0, "Not implemented");
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
4
mssql.d
4
mssql.d
|
|
@ -88,10 +88,6 @@ class MsSql : Database {
|
|||
return null; // FIXME
|
||||
}
|
||||
|
||||
override bool isAlive() {
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
SQLHENV env;
|
||||
SQLHDBC conn;
|
||||
|
|
|
|||
3
mysql.d
3
mysql.d
|
|
@ -219,9 +219,6 @@ class MySql : Database {
|
|||
query("START TRANSACTION");
|
||||
}
|
||||
|
||||
override bool isAlive() {
|
||||
return true;
|
||||
}
|
||||
|
||||
string sysTimeToValue(SysTime s) {
|
||||
return "cast('" ~ escape(s.toISOExtString()) ~ "' as datetime)";
|
||||
|
|
|
|||
3
oauth.d
3
oauth.d
|
|
@ -2,8 +2,7 @@
|
|||
module arsd.oauth;
|
||||
|
||||
import arsd.curl;
|
||||
import arsd.uri;
|
||||
import arsd.cgi : Cgi;
|
||||
import arsd.cgi; // for decodeVariables
|
||||
import std.array;
|
||||
static import std.uri;
|
||||
static import std.algorithm;
|
||||
|
|
|
|||
106
postgres.d
106
postgres.d
|
|
@ -60,12 +60,9 @@ class PostgreSql : Database {
|
|||
conn = PQconnectdb(toStringz(connectionString));
|
||||
if(conn is null)
|
||||
throw new DatabaseException("Unable to allocate PG connection object");
|
||||
if(PQstatus(conn) != CONNECTION_OK) {
|
||||
this.connectionOk = false;
|
||||
if(PQstatus(conn) != CONNECTION_OK)
|
||||
throw new DatabaseException(error());
|
||||
}
|
||||
query("SET NAMES 'utf8'"); // D does everything with utf8
|
||||
this.connectionOk = true;
|
||||
}
|
||||
|
||||
string connectionString;
|
||||
|
|
@ -78,11 +75,6 @@ class PostgreSql : Database {
|
|||
return "'" ~ escape(s.toISOExtString()) ~ "'::timestamptz";
|
||||
}
|
||||
|
||||
private bool connectionOk;
|
||||
override bool isAlive() {
|
||||
return connectionOk;
|
||||
}
|
||||
|
||||
/**
|
||||
Prepared statement support
|
||||
|
||||
|
|
@ -140,10 +132,8 @@ class PostgreSql : Database {
|
|||
conn = PQconnectdb(toStringz(connectionString));
|
||||
if(conn is null)
|
||||
throw new DatabaseException("Unable to allocate PG connection object");
|
||||
if(PQstatus(conn) != CONNECTION_OK) {
|
||||
this.connectionOk = false;
|
||||
if(PQstatus(conn) != CONNECTION_OK)
|
||||
throw new DatabaseException(error());
|
||||
}
|
||||
goto retry;
|
||||
}
|
||||
throw new DatabaseException(error());
|
||||
|
|
@ -189,82 +179,6 @@ class PostgreSql : Database {
|
|||
PGconn* conn;
|
||||
}
|
||||
|
||||
/+
|
||||
# when it changes from lowercase to upper case, call that a new word. or when it goes to/from anything else and underscore or dashes.
|
||||
+/
|
||||
|
||||
struct PreparedStatementDescription {
|
||||
PreparedStatementResult[] result;
|
||||
}
|
||||
|
||||
struct PreparedStatementResult {
|
||||
string fieldName;
|
||||
DatabaseDatum type;
|
||||
}
|
||||
|
||||
PreparedStatementDescription describePrepared(PostgreSql db, string name) {
|
||||
auto res = PQdescribePrepared(db.conn, name.toStringz);
|
||||
|
||||
PreparedStatementResult[] ret;
|
||||
|
||||
// PQnparams PQparamtype for params
|
||||
auto numFields = PQnfields(res);
|
||||
foreach(num; 0 .. numFields) {
|
||||
auto typeId = PQftype(res, num);
|
||||
DatabaseDatum dd;
|
||||
dd.platformSpecificTag = typeId;
|
||||
dd.storage = sampleForOid(typeId);
|
||||
ret ~= PreparedStatementResult(
|
||||
copyCString(PQfname(res, num)),
|
||||
dd,
|
||||
);
|
||||
}
|
||||
|
||||
PQclear(res);
|
||||
|
||||
return PreparedStatementDescription(ret);
|
||||
}
|
||||
|
||||
import arsd.core : LimitedVariant, PackedDateTime, SimplifiedUtcTimestamp;
|
||||
LimitedVariant sampleForOid(int platformSpecificTag) {
|
||||
switch(platformSpecificTag) {
|
||||
case BOOLOID:
|
||||
return LimitedVariant(false);
|
||||
case BYTEAOID:
|
||||
return LimitedVariant(cast(const(ubyte)[]) null);
|
||||
case TEXTOID:
|
||||
case VARCHAROID:
|
||||
return LimitedVariant("");
|
||||
case INT4OID:
|
||||
return LimitedVariant(0);
|
||||
case INT8OID:
|
||||
return LimitedVariant(0L);
|
||||
case FLOAT4OID:
|
||||
return LimitedVariant(0.0f);
|
||||
case FLOAT8OID:
|
||||
return LimitedVariant(0.0);
|
||||
case TIMESTAMPOID:
|
||||
case TIMESTAMPTZOID:
|
||||
return LimitedVariant(SimplifiedUtcTimestamp(0));
|
||||
case DATEOID:
|
||||
PackedDateTime d;
|
||||
d.hasDate = true;
|
||||
return LimitedVariant(d); // might want a different type so contains shows the thing without checking hasDate and hasTime
|
||||
case TIMETZOID: // possibly wrong... the tz isn't in my packed thing
|
||||
case TIMEOID:
|
||||
PackedDateTime d;
|
||||
d.hasTime = true;
|
||||
return LimitedVariant(d);
|
||||
case INTERVALOID:
|
||||
// months, days, and microseconds
|
||||
|
||||
case NUMERICOID: // aka decimal
|
||||
default:
|
||||
// when in doubt, assume it is just a string
|
||||
return LimitedVariant("sample");
|
||||
}
|
||||
}
|
||||
|
||||
private string toLowerFast(string s) {
|
||||
import std.ascii : isUpper;
|
||||
foreach (c; s)
|
||||
|
|
@ -460,22 +374,7 @@ extern(C) {
|
|||
int PQfformat(const PGresult *res, int column_number);
|
||||
|
||||
alias Oid = int;
|
||||
enum BOOLOID = 16;
|
||||
enum BYTEAOID = 17;
|
||||
enum TEXTOID = 25;
|
||||
enum INT4OID = 23; // integer
|
||||
enum INT8OID = 20; // bigint
|
||||
enum NUMERICOID = 1700;
|
||||
enum FLOAT4OID = 700;
|
||||
enum FLOAT8OID = 701;
|
||||
enum VARCHAROID = 1043;
|
||||
enum DATEOID = 1082;
|
||||
enum TIMEOID = 1083;
|
||||
enum TIMESTAMPOID = 1114;
|
||||
enum TIMESTAMPTZOID = 1184;
|
||||
enum INTERVALOID = 1186;
|
||||
enum TIMETZOID = 1266;
|
||||
|
||||
Oid PQftype(const PGresult* res, int column_number);
|
||||
|
||||
char *PQescapeByteaConn(PGconn *conn,
|
||||
|
|
@ -487,7 +386,6 @@ extern(C) {
|
|||
|
||||
char* PQcmdTuples(PGresult *res);
|
||||
|
||||
PGresult *PQdescribePrepared(PGconn *conn, const char *stmtName);
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
|||
839
shell.d
839
shell.d
|
|
@ -1,839 +0,0 @@
|
|||
/++
|
||||
Support functions to build a custom unix-style shell.
|
||||
|
||||
|
||||
$(PITFALL
|
||||
Do NOT use this to try to sanitize, escape, or otherwise parse what another shell would do with a string! Every shell is different and this implements my rules which may differ in subtle ways from any other common shell.
|
||||
|
||||
If you want to use this to understand a command, also use it to execute that command so you get what you expect.
|
||||
)
|
||||
|
||||
History:
|
||||
Added October 18, 2025
|
||||
|
||||
Bugs:
|
||||
$(LIST
|
||||
* < and > redirections are not implemented at all
|
||||
* >> not implemented
|
||||
* | on Windows is not implemented
|
||||
* glob expansion is minimal - * works, but no ?, no {item,other}, no {start..end}
|
||||
* ~ expansion is not implemented
|
||||
* `substitution` and $(...) is not implemented
|
||||
* variable expansion is not implemented. can do $IDENT and ${IDENT} i think
|
||||
* built-ins don't exist - `set`, want `for` and then like `export` and a way to hook in basic utilities polyfills especially on Windows (ls, rm, grep, etc)
|
||||
* built-ins should have a pipe they can read/write to and return an int. integrate with arsd.cli?
|
||||
* no !history recall. or history command in general
|
||||
* job control is rudimentary - no fg, bg, jobs, &, ctrl+z, etc.
|
||||
* set -o ignoreeof
|
||||
* the path search is hardcoded
|
||||
* prompt could be cooler
|
||||
PS1 = normal prompt
|
||||
PS2 = continuation prompt
|
||||
Bash shell executes the content of the PROMPT_COMMAND just before displaying the PS1 variable.
|
||||
|
||||
bash does it with `\u` and stuff but i kinda thiink using `$USER` and such might make more sense.
|
||||
* it prints command return values when you might not want that
|
||||
* LS_COLORS env var is not set
|
||||
* && and || is not implemented
|
||||
* the api is not very good
|
||||
* ulimit? sourcing things too. aliases.
|
||||
* see my bash rc for other little things. maybe i want a deeshrc
|
||||
* permission denied when hitting tab on Windows
|
||||
* tab complete of available commands not implemented - get it from path search.
|
||||
* some vars dynamic like $_ being the most recent command, $? being its return value, etc
|
||||
)
|
||||
|
||||
Questionable_ideas:
|
||||
$(LIST
|
||||
* separate stdout and stderr more by default, allow stderr pipes.
|
||||
* custom completion scripts? prolly not bash compatible since the scripts would be more involved
|
||||
* some kind of scriptable cmdlet? a full on script language with shell stuff embeddable?
|
||||
see https://hush-shell.github.io/cmd/index.html for some ok ideas
|
||||
* do something fun with job control. idk what tho really.
|
||||
* can terminal emulators get notifications when the foreground process group changes? i don't think so but i could make a "poll again now" sequence since i control shell and possibly terminal emulator now.
|
||||
* change DISPLAY and such when attaching remote sessions
|
||||
)
|
||||
+/
|
||||
module arsd.shell;
|
||||
|
||||
import arsd.core;
|
||||
|
||||
/++
|
||||
Holds some context needed for shell expansions.
|
||||
+/
|
||||
struct ShellContext {
|
||||
string[string] vars;
|
||||
string cwd;
|
||||
string[string] userHomes;
|
||||
}
|
||||
|
||||
enum QuoteStyle {
|
||||
none,
|
||||
nonExpanding, // 'thing'
|
||||
expanding, // "thing"
|
||||
subcommand, // `thing`
|
||||
}
|
||||
|
||||
/++
|
||||
|
||||
+/
|
||||
alias Globber = string[] delegate(string str, ShellContext context);
|
||||
|
||||
/++
|
||||
Represents one component of a shell command line as a precursor to parsing.
|
||||
+/
|
||||
struct ShellLexeme {
|
||||
string l;
|
||||
QuoteStyle quoteStyle;
|
||||
|
||||
string toEscapedFormat() {
|
||||
if(quoteStyle == QuoteStyle.nonExpanding) {
|
||||
string ret;
|
||||
ret.reserve(l.length);
|
||||
foreach(ch; l) {
|
||||
if(ch == '*')
|
||||
ret ~= "\\*";
|
||||
else
|
||||
ret ~= ch;
|
||||
}
|
||||
|
||||
return ret;
|
||||
} else {
|
||||
return l;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/+
|
||||
/++
|
||||
The second thing should be have toSingleArg called on it
|
||||
+/
|
||||
EnvironmentPair toEnvironmentPair(ShellLexeme context) {
|
||||
assert(quoteStyle == QuoteStyle.none);
|
||||
|
||||
size_t splitPoint = l.length;
|
||||
foreach(size_t idx, char ch; l) {
|
||||
if(ch == '=') {
|
||||
splitPoint = idx;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(splitPoint != l.length) {
|
||||
return EnvironmentPair(l[0 .. splitPoint], ShellLexeme(l[splitPoint + 1 .. $]));
|
||||
} else {
|
||||
return EnvironmentPair(null, ShellLexeme.init);
|
||||
}
|
||||
}
|
||||
|
||||
/++
|
||||
Expands variables but not globs while replacing quotes and such. Note it is NOT safe to pass an expanded single arg to another shell
|
||||
+/
|
||||
string toExpandedSingleArg(ShellContext context) {
|
||||
return l;
|
||||
}
|
||||
|
||||
/++
|
||||
Returns the value as an argv array, after shell expansion of variables, tildes, and globs
|
||||
|
||||
Does NOT attempt to execute `subcommands`.
|
||||
+/
|
||||
string[] toExpandedArgs(ShellContext context, Globber globber) {
|
||||
return null;
|
||||
}
|
||||
+/
|
||||
|
||||
/++
|
||||
This function in pure in all but formal annotation; it does not interact with the outside world.
|
||||
+/
|
||||
ShellLexeme[] lexShellCommandLine(string commandLine) {
|
||||
ShellLexeme[] ret;
|
||||
|
||||
enum State {
|
||||
consumingWhitespace,
|
||||
readingWord,
|
||||
readingSingleQuoted,
|
||||
readingEscaped,
|
||||
readingExpandingContextEscaped,
|
||||
readingDoubleQuoted,
|
||||
// FIXME: readingSubcommand for `thing`
|
||||
readingComment,
|
||||
}
|
||||
|
||||
State state = State.consumingWhitespace;
|
||||
size_t first = commandLine.length;
|
||||
|
||||
void endWord() {
|
||||
state = State.consumingWhitespace;
|
||||
first = commandLine.length; // we'll rewind upon encountering the next word, if there is one
|
||||
}
|
||||
|
||||
foreach(size_t idx, char ch; commandLine) {
|
||||
again:
|
||||
final switch(state) {
|
||||
case State.consumingWhitespace:
|
||||
switch(ch) {
|
||||
case ' ':
|
||||
// the arg separators should all be collapsed to exactly one
|
||||
if(ret.length && !(ret[$-1].quoteStyle == QuoteStyle.none && ret[$-1].l == " "))
|
||||
ret ~= ShellLexeme(" ");
|
||||
continue;
|
||||
case '#':
|
||||
state = State.readingComment;
|
||||
continue;
|
||||
default:
|
||||
first = idx;
|
||||
state = State.readingWord;
|
||||
goto again;
|
||||
}
|
||||
case State.readingWord:
|
||||
switch(ch) {
|
||||
case '\'':
|
||||
if(first != idx)
|
||||
ret ~= ShellLexeme(commandLine[first .. idx]);
|
||||
first = idx + 1;
|
||||
state = State.readingSingleQuoted;
|
||||
break;
|
||||
case '\\':
|
||||
// a \ch can be treated as just a single quoted single char...
|
||||
if(first != idx)
|
||||
ret ~= ShellLexeme(commandLine[first .. idx]);
|
||||
first = idx + 1;
|
||||
state = State.readingEscaped;
|
||||
break;
|
||||
case '"':
|
||||
if(first != idx)
|
||||
ret ~= ShellLexeme(commandLine[first .. idx]);
|
||||
first = idx + 1;
|
||||
state = State.readingDoubleQuoted;
|
||||
break;
|
||||
case ' ':
|
||||
ret ~= ShellLexeme(commandLine[first .. idx]);
|
||||
ret ~= ShellLexeme(" "); // an argument separator
|
||||
endWord();
|
||||
continue;
|
||||
case '|', '<', '>', '&':
|
||||
if(first != idx)
|
||||
ret ~= ShellLexeme(commandLine[first .. idx]);
|
||||
ret ~= ShellLexeme(commandLine[idx .. idx + 1]); // shell special symbol
|
||||
endWord();
|
||||
continue;
|
||||
default:
|
||||
// keep searching
|
||||
}
|
||||
break;
|
||||
case State.readingComment:
|
||||
if(ch == '\n') {
|
||||
endWord();
|
||||
}
|
||||
break;
|
||||
case State.readingSingleQuoted:
|
||||
switch(ch) {
|
||||
case '\'':
|
||||
ret ~= ShellLexeme(commandLine[first .. idx], QuoteStyle.nonExpanding);
|
||||
endWord();
|
||||
break;
|
||||
default:
|
||||
}
|
||||
break;
|
||||
case State.readingDoubleQuoted:
|
||||
switch(ch) {
|
||||
case '"':
|
||||
ret ~= ShellLexeme(commandLine[first .. idx], QuoteStyle.expanding);
|
||||
endWord();
|
||||
break;
|
||||
case '\\':
|
||||
state = State.readingExpandingContextEscaped;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
break;
|
||||
case State.readingEscaped:
|
||||
if(ch >= 0x80 && ch <= 0xBF) {
|
||||
// continuation byte
|
||||
continue;
|
||||
} else if(first == idx) {
|
||||
// first byte, keep searching for continuations
|
||||
continue;
|
||||
} else {
|
||||
// same as if the user wrote the escaped character in single quotes
|
||||
ret ~= ShellLexeme(commandLine[first .. idx], QuoteStyle.nonExpanding);
|
||||
|
||||
if(state == State.readingExpandingContextEscaped) {
|
||||
state = State.readingDoubleQuoted;
|
||||
first = idx;
|
||||
} else {
|
||||
endWord();
|
||||
}
|
||||
goto again;
|
||||
}
|
||||
case State.readingExpandingContextEscaped:
|
||||
if(ch == '"') {
|
||||
// the -1 trims out the \
|
||||
ret ~= ShellLexeme(commandLine[first .. idx - 1], QuoteStyle.expanding);
|
||||
state = State.readingDoubleQuoted;
|
||||
first = idx; // we need to INCLUDE the " itself
|
||||
} else {
|
||||
// this was actually nothing special, the backslash is kept in the double quotes
|
||||
state = State.readingDoubleQuoted;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(first != commandLine.length) {
|
||||
if(state != State.readingWord && state != State.readingComment)
|
||||
throw new Exception("ran out of data in inappropriate state");
|
||||
ret ~= ShellLexeme(commandLine[first .. $]);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
unittest {
|
||||
ShellLexeme[] got;
|
||||
|
||||
got = lexShellCommandLine("FOO=bar");
|
||||
assert(got.length == 1);
|
||||
assert(got[0].l == "FOO=bar");
|
||||
|
||||
// comments can only happen at whitespace contexts, not at the end of a single word
|
||||
got = lexShellCommandLine("FOO=bar#commentspam");
|
||||
assert(got.length == 1);
|
||||
assert(got[0].l == "FOO=bar#commentspam");
|
||||
|
||||
got = lexShellCommandLine("FOO=bar #commentspam");
|
||||
assert(got.length == 2);
|
||||
assert(got[0].l == "FOO=bar");
|
||||
assert(got[1].l == " "); // arg separator still there even tho there is no arg cuz of the comment, but that's semantic
|
||||
|
||||
got = lexShellCommandLine("#commentspam");
|
||||
assert(got.length == 0, got[0].l);
|
||||
|
||||
got = lexShellCommandLine("FOO=bar ./prog");
|
||||
assert(got.length == 3);
|
||||
assert(got[0].l == "FOO=bar");
|
||||
assert(got[1].l == " "); // argument separator
|
||||
assert(got[2].l == "./prog");
|
||||
|
||||
// all whitespace should be collapsed to a single argument separator
|
||||
got = lexShellCommandLine("FOO=bar ./prog");
|
||||
assert(got.length == 3);
|
||||
assert(got[0].l == "FOO=bar");
|
||||
assert(got[1].l == " "); // argument separator
|
||||
assert(got[2].l == "./prog");
|
||||
|
||||
got = lexShellCommandLine("'foo'bar");
|
||||
assert(got.length == 2);
|
||||
assert(got[0].l == "foo");
|
||||
assert(got[0].quoteStyle == QuoteStyle.nonExpanding);
|
||||
assert(got[1].l == "bar");
|
||||
assert(got[1].quoteStyle == QuoteStyle.none);
|
||||
|
||||
// escaped single char works as if you wrote it in single quotes
|
||||
got = lexShellCommandLine("test\\'bar");
|
||||
assert(got.length == 3);
|
||||
assert(got[0].l == "test");
|
||||
assert(got[1].l == "'");
|
||||
assert(got[2].l == "bar");
|
||||
|
||||
// checking for utf-8 decode of escaped char
|
||||
got = lexShellCommandLine("test\\\»bar");
|
||||
assert(got.length == 3);
|
||||
assert(got[0].l == "test");
|
||||
assert(got[1].l == "\»");
|
||||
assert(got[2].l == "bar");
|
||||
|
||||
got = lexShellCommandLine(`"ok"`);
|
||||
assert(got.length == 1);
|
||||
assert(got[0].l == "ok");
|
||||
assert(got[0].quoteStyle == QuoteStyle.expanding);
|
||||
|
||||
got = lexShellCommandLine(`"ok\"after"`);
|
||||
assert(got.length == 2);
|
||||
assert(got[0].l == "ok");
|
||||
assert(got[0].quoteStyle == QuoteStyle.expanding);
|
||||
assert(got[1].l == "\"after");
|
||||
assert(got[1].quoteStyle == QuoteStyle.expanding);
|
||||
|
||||
got = lexShellCommandLine(`FOO=bar ./thing 'my ard' second_arg "quoted\"thing"`);
|
||||
assert(got.length == 10); // because quoted\"thing is two in this weird system
|
||||
assert(got[0].l == "FOO=bar");
|
||||
assert(got[1].l == " ");
|
||||
assert(got[2].l == "./thing");
|
||||
assert(got[3].l == " ");
|
||||
assert(got[4].l == "my ard");
|
||||
assert(got[5].l == " ");
|
||||
assert(got[6].l == "second_arg");
|
||||
assert(got[7].l == " ");
|
||||
assert(got[8].l == "quoted");
|
||||
assert(got[9].l == "\"thing");
|
||||
|
||||
got = lexShellCommandLine("a | b c");
|
||||
assert(got.length == 7);
|
||||
|
||||
}
|
||||
|
||||
struct ShellIo {
|
||||
enum Kind {
|
||||
inherit,
|
||||
fd,
|
||||
filename,
|
||||
pipedCommand,
|
||||
memoryBuffer
|
||||
}
|
||||
|
||||
Kind kind;
|
||||
int fd;
|
||||
string filename;
|
||||
ShellCommand pipedCommand;
|
||||
}
|
||||
|
||||
class ShellCommand {
|
||||
ShellIo stdin;
|
||||
ShellIo stdout;
|
||||
ShellIo stderr;
|
||||
// yes i know in unix you can do other fds too. do i care?
|
||||
|
||||
private ExternalProcess externalProcess;
|
||||
|
||||
string exePath;
|
||||
string cwd;
|
||||
string[] argv;
|
||||
EnvironmentPair[] environmentPairs;
|
||||
|
||||
/++
|
||||
The return value may be null! Some things can be executed without external processes.
|
||||
|
||||
This function is absolutely NOT pure. It may modify your shell context, run external processes, and generally carry out operations outside the shell.
|
||||
+/
|
||||
ExternalProcess execute(ref ShellContext context) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/++
|
||||
A shell component - which is likely an argument, but that is a semantic distinction we can't make until parsing - may be made up of several lexemes. Think `foo'bar'`. This will extract them from the given array up to and including the next unquoted space or newline char.
|
||||
+/
|
||||
ShellLexeme[] nextComponent(ref ShellLexeme[] lexemes) {
|
||||
if(lexemes.length == 0)
|
||||
return lexemes[$ .. $];
|
||||
|
||||
int pos;
|
||||
while(
|
||||
pos < lexemes.length &&
|
||||
!(
|
||||
// identify an arg or command separator
|
||||
lexemes[pos].quoteStyle == QuoteStyle.none &&
|
||||
(
|
||||
lexemes[pos].l == " " ||
|
||||
lexemes[pos].l == "\n"
|
||||
)
|
||||
)
|
||||
) {
|
||||
pos++;
|
||||
}
|
||||
|
||||
if(pos == 0)
|
||||
pos++; // include the termination condition as its own component
|
||||
|
||||
auto ret = lexemes[0 .. pos];
|
||||
lexemes = lexemes[pos .. $];
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
struct EnvironmentPair {
|
||||
string environmentVariableName;
|
||||
string assignedValue;
|
||||
|
||||
string toString() {
|
||||
return environmentVariableName ~ "=" ~ assignedValue;
|
||||
}
|
||||
}
|
||||
|
||||
string expandSingleArg(string escapedArg, ShellContext context) {
|
||||
return escapedArg;
|
||||
}
|
||||
|
||||
/++
|
||||
Parses a set of lexemes into set of command objects.
|
||||
|
||||
This function in pure in all but formal annotation; it does not interact with the outside world, except through the globber delegate you provide (which should not make any changes to the outside world!).
|
||||
+/
|
||||
ShellCommand[] parseShellCommand(ShellLexeme[] lexemes, ShellContext context, Globber globber) {
|
||||
ShellCommand[] ret;
|
||||
|
||||
ShellCommand currentCommand;
|
||||
ShellCommand firstCommand;
|
||||
|
||||
enum ParseState {
|
||||
lookingForVarAssignment,
|
||||
lookingForArg,
|
||||
}
|
||||
ParseState parseState = ParseState.lookingForVarAssignment;
|
||||
|
||||
while(lexemes.length) {
|
||||
auto component = nextComponent(lexemes);
|
||||
if(component.length) {
|
||||
if(currentCommand is null)
|
||||
currentCommand = new ShellCommand();
|
||||
if(firstCommand is null)
|
||||
firstCommand = currentCommand;
|
||||
|
||||
/+
|
||||
Command syntax in bash is basically:
|
||||
|
||||
Zero or more `ENV=value` sets, separated by whitespace, followed by zero or more arg things.
|
||||
OR
|
||||
a shell builtin which does special things to the rest of the command, and may even require subsequent commands
|
||||
|
||||
Argv[0] can be a shell built in which reads the rest of argv separately. It may even require subsequent commands!
|
||||
|
||||
For some shell built in keywords, you should not actually do expansion:
|
||||
$ for $i in one two; do ls $i; done
|
||||
bash: `$i': not a valid identifier
|
||||
|
||||
So there must be some kind of intermediate representation of possible expansions.
|
||||
|
||||
|
||||
BUT THIS IS MY SHELL I CAN DO WHAT I WANT!!!!!!!!!!!!
|
||||
+/
|
||||
|
||||
bool thisWasEnvironmentPair = false;
|
||||
EnvironmentPair environmentPair;
|
||||
bool thisWasRedirection = false;
|
||||
bool thisWasPipe = false;
|
||||
string arg;
|
||||
|
||||
if(component.length == 0) {
|
||||
// nothing left, should never happen
|
||||
break;
|
||||
}
|
||||
if(component.length == 1) {
|
||||
if(component[0].quoteStyle == QuoteStyle.none && component[0].l == " ") {
|
||||
// just an arg separator
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
foreach(lexeme; component) {
|
||||
again:
|
||||
final switch(parseState) {
|
||||
case ParseState.lookingForVarAssignment:
|
||||
if(thisWasEnvironmentPair) {
|
||||
environmentPair.assignedValue ~= lexeme.toEscapedFormat();
|
||||
} else {
|
||||
// assume there is no var until we prove otherwise
|
||||
parseState = ParseState.lookingForArg;
|
||||
if(lexeme.quoteStyle == QuoteStyle.none) {
|
||||
foreach(idx, ch; lexeme.l) {
|
||||
if(ch == '=') {
|
||||
// actually found one!
|
||||
thisWasEnvironmentPair = true;
|
||||
environmentPair.environmentVariableName = lexeme.l[0 .. idx];
|
||||
environmentPair.assignedValue = lexeme.l[idx + 1 .. $];
|
||||
parseState = ParseState.lookingForVarAssignment;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(parseState == ParseState.lookingForArg)
|
||||
goto case;
|
||||
}
|
||||
break;
|
||||
case ParseState.lookingForArg:
|
||||
if(lexeme.quoteStyle == QuoteStyle.none) {
|
||||
if(lexeme.l == "<" || lexeme.l == ">")
|
||||
thisWasRedirection = true;
|
||||
if(lexeme.l == "|")
|
||||
thisWasPipe = true;
|
||||
}
|
||||
arg ~= lexeme.toEscapedFormat();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(thisWasEnvironmentPair) {
|
||||
environmentPair.assignedValue = expandSingleArg(environmentPair.assignedValue, context);
|
||||
currentCommand.environmentPairs ~= environmentPair;
|
||||
} else if(thisWasRedirection) {
|
||||
// FIXME: read the fd off this arg
|
||||
// FIXME: read the filename off the next arg, new parse state
|
||||
assert(0);
|
||||
} else if(thisWasPipe) {
|
||||
// FIXME: read the fd? i kinda wanna support 2| and such
|
||||
auto newCommand = new ShellCommand();
|
||||
currentCommand.stdout.kind = ShellIo.Kind.pipedCommand;
|
||||
currentCommand.stdout.pipedCommand = newCommand;
|
||||
newCommand.stdin.kind = ShellIo.Kind.pipedCommand;
|
||||
newCommand.stdin.pipedCommand = currentCommand;
|
||||
|
||||
currentCommand = newCommand;
|
||||
} else {
|
||||
currentCommand.argv ~= globber(arg, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(firstCommand)
|
||||
ret ~= firstCommand;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
unittest {
|
||||
string[] globber(string s, ShellContext context) {
|
||||
return [s];
|
||||
}
|
||||
ShellContext context;
|
||||
ShellCommand[] commands;
|
||||
|
||||
commands = parseShellCommand(lexShellCommandLine("foo bar"), context, &globber);
|
||||
assert(commands.length == 1);
|
||||
assert(commands[0].argv.length == 2);
|
||||
assert(commands[0].argv[0] == "foo");
|
||||
assert(commands[0].argv[1] == "bar");
|
||||
|
||||
commands = parseShellCommand(lexShellCommandLine("foo bar'baz'"), context, &globber);
|
||||
assert(commands.length == 1);
|
||||
assert(commands[0].argv.length == 2);
|
||||
assert(commands[0].argv[0] == "foo");
|
||||
assert(commands[0].argv[1] == "barbaz");
|
||||
|
||||
}
|
||||
|
||||
/+
|
||||
interface OSInterface {
|
||||
setEnv
|
||||
getEnv
|
||||
getAllEnv
|
||||
|
||||
runCommand
|
||||
waitForCommand
|
||||
}
|
||||
+/
|
||||
|
||||
class Shell {
|
||||
protected ShellContext context;
|
||||
|
||||
this() {
|
||||
context.cwd = getCurrentWorkingDirectory().toString;
|
||||
prompt = "[deesh]" ~ context.cwd ~ "$ ";
|
||||
}
|
||||
|
||||
public string prompt;
|
||||
|
||||
protected string[] glob(string s) {
|
||||
string[] ret;
|
||||
getFiles(context.cwd, (string name, bool isDirectory) {
|
||||
if(name.length && name[0] == '.' && (s.length == 0 || s[0] != '.'))
|
||||
return; // skip hidden unless specifically requested
|
||||
if(name.matchesFilePattern(s))
|
||||
ret ~= name;
|
||||
});
|
||||
if(ret.length == 0)
|
||||
return [s];
|
||||
else
|
||||
return ret;
|
||||
}
|
||||
|
||||
private final string[] globberForwarder(string s, ShellContext context) {
|
||||
return glob(s);
|
||||
}
|
||||
|
||||
void dumpCommand(ShellCommand command) {
|
||||
foreach(ep; command.environmentPairs)
|
||||
writeln(ep.toString());
|
||||
writeln(command.argv);
|
||||
if(command.stdout.kind == ShellIo.Kind.pipedCommand) {
|
||||
writeln(" | ");
|
||||
dumpCommand(command.stdout.pipedCommand);
|
||||
}
|
||||
}
|
||||
|
||||
FilePath searchPathForCommand(string arg0) {
|
||||
if(arg0.indexOf("/") != -1)
|
||||
return FilePath(arg0);
|
||||
// could also be built-ins and cmdlets...
|
||||
// and on Windows we should check .exe, .com, .bat, or ask the OS maybe
|
||||
|
||||
version(Posix) { // FIXME
|
||||
immutable searchPaths = ["/usr/bin", "/bin", "/usr/local/bin", "/home/me/bin"]; // FIXME
|
||||
foreach(path; searchPaths) {
|
||||
auto t = FilePath(arg0).makeAbsolute(FilePath(path));
|
||||
import core.sys.posix.sys.stat;
|
||||
stat_t sbuf;
|
||||
|
||||
CharzBuffer buf = t.toString();
|
||||
auto ret = stat(buf.ptr, &sbuf);
|
||||
if(ret != -1)
|
||||
return t;
|
||||
}
|
||||
}
|
||||
return FilePath(null);
|
||||
}
|
||||
|
||||
version(Windows)
|
||||
private ExternalProcess startCommand(ShellCommand command, int inheritedPipe, int pgid) {
|
||||
string windowsCommandLine;
|
||||
foreach(arg; command.argv) {
|
||||
if(windowsCommandLine.length)
|
||||
windowsCommandLine ~= " ";
|
||||
windowsCommandLine ~= arg;
|
||||
}
|
||||
|
||||
auto fp = searchPathForCommand(command.argv[0]);
|
||||
|
||||
auto proc = new ExternalProcess(fp, windowsCommandLine);
|
||||
command.externalProcess = proc;
|
||||
proc.start;
|
||||
return proc;
|
||||
}
|
||||
|
||||
version(Posix)
|
||||
private ExternalProcess startCommand(ShellCommand command, int inheritedPipe, int pgid) {
|
||||
auto fp = searchPathForCommand(command.argv[0]);
|
||||
if(fp.isNull()) {
|
||||
throw new Exception("Command not found");
|
||||
}
|
||||
|
||||
import core.sys.posix.unistd;
|
||||
|
||||
int[2] pipes;
|
||||
if(command.stdout.pipedCommand) {
|
||||
auto ret = pipe(pipes);
|
||||
|
||||
setCloExec(pipes[0]);
|
||||
setCloExec(pipes[1]);
|
||||
|
||||
import core.stdc.errno;
|
||||
|
||||
if(ret == -1)
|
||||
throw new ErrnoApiException("stdin pipe", errno);
|
||||
} else {
|
||||
pipes[0] = inheritedPipe;
|
||||
pipes[1] = 1;
|
||||
}
|
||||
|
||||
auto proc = new ExternalProcess(fp, command.argv);
|
||||
if(command.stdout.pipedCommand) {
|
||||
proc.beforeExec = () {
|
||||
// reset ignored signals to default behavior
|
||||
import core.sys.posix.signal;
|
||||
signal (SIGINT, SIG_DFL);
|
||||
signal (SIGQUIT, SIG_DFL);
|
||||
signal (SIGTSTP, SIG_DFL);
|
||||
signal (SIGTTIN, SIG_DFL);
|
||||
signal (SIGTTOU, SIG_DFL);
|
||||
signal (SIGCHLD, SIG_DFL);
|
||||
};
|
||||
}
|
||||
proc.pgid = pgid; // 0 here means to lead the group, all subsequent pipe programs should inherit the leader
|
||||
// and inherit the standard handles
|
||||
proc.overrideStdinFd = inheritedPipe;
|
||||
proc.overrideStdoutFd = pipes[1];
|
||||
proc.overrideStderrFd = 2;
|
||||
command.externalProcess = proc;
|
||||
proc.start;
|
||||
|
||||
if(command.stdout.pipedCommand) {
|
||||
startCommand(command.stdout.pipedCommand, pipes[0], pgid ? pgid : proc.pid);
|
||||
|
||||
// we're done with them now
|
||||
close(pipes[0]);
|
||||
close(pipes[1]);
|
||||
pipes[] = -1;
|
||||
}
|
||||
|
||||
return proc;
|
||||
}
|
||||
|
||||
int waitForCommand(ShellCommand command) {
|
||||
command.externalProcess.waitForCompletion();
|
||||
writeln(command.externalProcess.status);
|
||||
if(auto cmd = command.stdout.pipedCommand)
|
||||
waitForCommand(cmd);
|
||||
return command.externalProcess.status;
|
||||
}
|
||||
|
||||
public void execute(string commandLine) {
|
||||
auto commands = parseShellCommand(lexShellCommandLine(commandLine), context, &globberForwarder);
|
||||
foreach(command; commands)
|
||||
try {
|
||||
dumpCommand(command);
|
||||
|
||||
version(Posix) {
|
||||
import core.sys.posix.unistd;
|
||||
import core.sys.posix.signal;
|
||||
|
||||
auto proc = startCommand(command, 0, 0);
|
||||
|
||||
// put the child group in control of the tty
|
||||
ErrnoEnforce!tcsetpgrp(1, proc.pid);
|
||||
kill(-proc.pid, SIGCONT); // and if it beat us to the punch and is waiting, go ahead and wake it up (this is harmless if it is already running
|
||||
waitForCommand(command);
|
||||
// reassert control of the tty to the shell
|
||||
ErrnoEnforce!tcsetpgrp(1, getpid());
|
||||
}
|
||||
|
||||
version(Windows) {
|
||||
auto proc = startCommand(command, 0, 0);
|
||||
waitForCommand(command);
|
||||
}
|
||||
} catch(ArsdExceptionBase e) {
|
||||
string more;
|
||||
e.getAdditionalPrintableInformation((string name, in char[] value) {
|
||||
more ~= ", ";
|
||||
more ~= name ~ ": " ~ value;
|
||||
});
|
||||
writeln("deesh: ", command.argv.length ? command.argv[0] : "", ": ", e.message, more);
|
||||
} catch(Exception e) {
|
||||
writeln("deesh: ", command.argv.length ? command.argv[0] : "", ": ", e.msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/++
|
||||
Constructs an instance of [arsd.terminal.LineGetter] appropriate for use in a repl for this shell.
|
||||
+/
|
||||
auto constructLineGetter()() {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/+
|
||||
Parts of bash I like:
|
||||
|
||||
glob expansion
|
||||
! command recall
|
||||
redirection
|
||||
f{1..3} expand to f1 f2 f3. can add ..incr btw
|
||||
f{a,b,c} expand to fa fb fc
|
||||
for i in *; do cmd; done
|
||||
`command expansion`. also $( cmd ) is a thing
|
||||
~ expansion
|
||||
|
||||
foo && bar
|
||||
foo || bar
|
||||
|
||||
$(( maybe arithmetic but idk ))
|
||||
|
||||
ENV=whatever cmd.
|
||||
$ENV ...?
|
||||
|
||||
tab complete!
|
||||
|
||||
PATH lookup. if requested.
|
||||
|
||||
Globbing could insert -- before if there's any - in there.
|
||||
|
||||
Or better yet all the commands must either start with ./
|
||||
or be found internally. Internal can be expanded by definition
|
||||
files that tell how to expand the real thing.
|
||||
|
||||
* flags
|
||||
* arguments
|
||||
* command line
|
||||
* i/o
|
||||
+/
|
||||
|
|
@ -1164,19 +1164,17 @@ unittest {
|
|||
|
||||
import arsd.core;
|
||||
|
||||
version(ArsdNoCocoa) {
|
||||
} else {
|
||||
version(D_OpenD) {
|
||||
version(D_OpenD) {
|
||||
version(OSX)
|
||||
version=OSXCocoa;
|
||||
version(iOS)
|
||||
version=OSXCocoa;
|
||||
} else {
|
||||
} else {
|
||||
version(OSX) version(DigitalMars) version=OSXCocoa;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
version(Emscripten) {
|
||||
version=allow_unimplemented_features;
|
||||
version=without_opengl;
|
||||
|
|
@ -1418,7 +1416,7 @@ version(Emscripten) {
|
|||
}
|
||||
|
||||
version(libnotify) {
|
||||
//pragma(lib, "dl");
|
||||
pragma(lib, "dl");
|
||||
import core.sys.posix.dlfcn;
|
||||
|
||||
void delegate()[int] libnotify_action_delegates;
|
||||
|
|
@ -2912,7 +2910,6 @@ class SimpleWindow : CapableOfHandlingNativeEvent, CapableOfBeingDrawnUpon {
|
|||
else
|
||||
XMapWindow(impl.display, impl.window);
|
||||
} else version(OSXCocoa) {
|
||||
if(impl.window)
|
||||
impl.window.setIsVisible = !b;
|
||||
if(!hidden)
|
||||
impl.view.setNeedsDisplay(true);
|
||||
|
|
@ -4417,10 +4414,6 @@ struct EventLoopImpl {
|
|||
|
||||
void delegate(int) signalHandler;
|
||||
}
|
||||
else
|
||||
version(Posix) {
|
||||
static import unix = core.sys.posix.unistd;
|
||||
}
|
||||
|
||||
version(X11) {
|
||||
int pulseFd = -1;
|
||||
|
|
@ -4630,7 +4623,7 @@ struct EventLoopImpl {
|
|||
ref int customEventFDRead() { return SimpleWindow.customEventFDRead; }
|
||||
version(Posix)
|
||||
ref int customEventFDWrite() { return SimpleWindow.customEventFDWrite; }
|
||||
version(Posix)
|
||||
version(linux)
|
||||
ref int customSignalFD() { return SimpleWindow.customSignalFD; }
|
||||
version(Windows)
|
||||
ref auto customEventH() { return SimpleWindow.customEventH; }
|
||||
|
|
@ -7362,7 +7355,7 @@ version(Windows) {
|
|||
}
|
||||
|
||||
version (X11) {
|
||||
//pragma(lib, "dl"); // already done by the standard compiler build and specifying it again messes up zig cross compile
|
||||
pragma(lib, "dl");
|
||||
import core.sys.posix.dlfcn;
|
||||
}
|
||||
|
||||
|
|
@ -8423,8 +8416,6 @@ struct Pen {
|
|||
/++
|
||||
Represents an in-memory image in the format that the GUI expects, but with its raw data available to your program.
|
||||
|
||||
You can load an image with an alpha channel, but you cannot draw that in the current implementation. If you want alpha blending when drawing, use [Sprite] instead.
|
||||
|
||||
|
||||
On Windows, this means a device-independent bitmap. On X11, it is an XImage.
|
||||
|
||||
|
|
@ -9961,14 +9952,7 @@ struct ScreenPainter {
|
|||
impl.drawPixmap(s, upperLeft.x, upperLeft.y, imageUpperLeft.x, imageUpperLeft.y, sliceSize.width, sliceSize.height);
|
||||
}
|
||||
|
||||
/++
|
||||
Draws an [Image] to the window.
|
||||
|
||||
$(WARNING
|
||||
Even if the Image was loaded with `enableAlpha`, drawing may not work!
|
||||
|
||||
Use [Sprite.fromMemoryImage] and [drawPixmap] instead if you want alpha blending to work better.
|
||||
+/
|
||||
///
|
||||
void drawImage(Point upperLeft, Image i, Point upperLeftOfImage = Point(0, 0), int w = 0, int h = 0) {
|
||||
if(impl is null) return;
|
||||
//if(isClipped(upperLeft, w, h)) return; // FIXME
|
||||
|
|
@ -9982,8 +9966,6 @@ struct ScreenPainter {
|
|||
if(upperLeftOfImage.y < 0)
|
||||
upperLeftOfImage.y = 0;
|
||||
|
||||
assert(i.enableAlpha == false, "Alpha blending is not implemented for Image drawing - use Sprite and drawPixmap instead");
|
||||
|
||||
impl.drawImage(upperLeft.x, upperLeft.y, i, upperLeftOfImage.x, upperLeftOfImage.y, w, h);
|
||||
}
|
||||
|
||||
|
|
@ -19471,7 +19453,7 @@ version(OSXCocoa) {
|
|||
|
||||
auto contentRect = NSRect(NSPoint(0, 0), NSSize(width, height));
|
||||
|
||||
window = NSWindow.alloc.initWithContentRect(
|
||||
auto window = NSWindow.alloc.initWithContentRect(
|
||||
contentRect,
|
||||
NSWindowStyleMask.resizable | NSWindowStyleMask.closable | NSWindowStyleMask.miniaturizable | NSWindowStyleMask.titled,
|
||||
NSBackingStoreType.buffered,
|
||||
|
|
|
|||
4
sqlite.d
4
sqlite.d
|
|
@ -124,10 +124,6 @@ class Sqlite : Database {
|
|||
}
|
||||
}
|
||||
|
||||
override bool isAlive() {
|
||||
return true;
|
||||
}
|
||||
|
||||
///
|
||||
override void startTransaction() {
|
||||
query("BEGIN TRANSACTION");
|
||||
|
|
|
|||
73
string.d
73
string.d
|
|
@ -53,76 +53,3 @@ deprecated("D calls this `stripRight` instead") alias trimRight = stripRight;
|
|||
alias stringz = arsd.core.stringz;
|
||||
// CharzBuffer
|
||||
// WCharzBuffer
|
||||
|
||||
// ********* UTILITIES **************
|
||||
|
||||
string[] split(string s, string onWhat) {
|
||||
assert(onWhat.length);
|
||||
string[] ret;
|
||||
more:
|
||||
auto idx = s.indexOf(onWhat);
|
||||
if(idx == -1) {
|
||||
ret ~= s;
|
||||
return ret;
|
||||
}
|
||||
ret ~= s[0 .. idx];
|
||||
s = s[idx + onWhat.length .. $];
|
||||
goto more;
|
||||
}
|
||||
|
||||
unittest {
|
||||
assert("foo.bar".split(".") == ["foo", "bar"]);
|
||||
}
|
||||
|
||||
ptrdiff_t lastIndexOf(string s, string what) {
|
||||
assert(what.length);
|
||||
if(s.length < what.length)
|
||||
return -1;
|
||||
ptrdiff_t checking = s.length - what.length;
|
||||
while(checking >= 0) {
|
||||
if(s[checking .. checking + what.length] == what)
|
||||
return checking;
|
||||
|
||||
checking--;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
unittest {
|
||||
assert("31234".lastIndexOf("3") == 3);
|
||||
}
|
||||
|
||||
string join(string[] str, string w) {
|
||||
string ret;
|
||||
foreach(i, s; str) {
|
||||
if(i)
|
||||
ret ~= w;
|
||||
ret ~= s;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
unittest {
|
||||
assert(["a", "b"].join(" ") == "a b");
|
||||
}
|
||||
|
||||
string replace(string str, string find, string repacement) {
|
||||
assert(find.length);
|
||||
|
||||
string ret;
|
||||
more:
|
||||
auto idx = str.indexOf(find);
|
||||
if(idx == -1) {
|
||||
ret ~= str;
|
||||
return ret;
|
||||
}
|
||||
ret ~= str[0 .. idx];
|
||||
ret ~= repacement;
|
||||
str = str[idx + find.length .. $];
|
||||
goto more;
|
||||
}
|
||||
|
||||
unittest {
|
||||
assert("foobarfoo".replace("foo", "bar") == "barbarbar");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9268,9 +9268,6 @@ version(TerminalDirectToEmulator) {
|
|||
|
||||
private class TerminalEmulatorInsideWidget : TerminalEmulator {
|
||||
|
||||
import arsd.core : EnableSynchronization;
|
||||
mixin EnableSynchronization;
|
||||
|
||||
private ScrollbackBuffer sbb() { return scrollbackBuffer; }
|
||||
|
||||
void resized(int w, int h) {
|
||||
|
|
@ -9470,8 +9467,8 @@ version(TerminalDirectToEmulator) {
|
|||
if(this.font.isNull) {
|
||||
// carry on, it will try a default later
|
||||
} else if(this.font.isMonospace) {
|
||||
this.fontWidth = castFnumToCnum(font.averageWidth);
|
||||
this.fontHeight = castFnumToCnum(font.height);
|
||||
this.fontWidth = font.averageWidth;
|
||||
this.fontHeight = font.height;
|
||||
} else {
|
||||
this.font.unload(); // can't really use a non-monospace font, so just going to unload it so the default font loads again
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,13 +111,6 @@ struct ScopeBuffer(T, size_t maxSize, bool allowGrowth = false) {
|
|||
size_t length;
|
||||
bool isNull = true;
|
||||
T[] opSlice() { return isNull ? null : buffer[0 .. length]; }
|
||||
|
||||
static if(is(T == char))
|
||||
void appendIntAsString(int n) {
|
||||
import std.conv;
|
||||
this ~= to!string(n);
|
||||
}
|
||||
|
||||
void opOpAssign(string op : "~")(in T rhs) {
|
||||
if(buffer is null) buffer = bufferInternal[];
|
||||
isNull = false;
|
||||
|
|
@ -334,7 +327,6 @@ class TerminalEmulator {
|
|||
if(termY >= screenHeight)
|
||||
termY = screenHeight - 1;
|
||||
|
||||
/+
|
||||
version(Windows) {
|
||||
// I'm swapping these because my laptop doesn't have a middle button,
|
||||
// and putty swaps them too by default so whatevs.
|
||||
|
|
@ -343,7 +335,6 @@ class TerminalEmulator {
|
|||
else if(button == MouseButton.middle)
|
||||
button = MouseButton.right;
|
||||
}
|
||||
+/
|
||||
|
||||
int baseEventCode() {
|
||||
int b;
|
||||
|
|
@ -369,9 +360,6 @@ class TerminalEmulator {
|
|||
if(alt) // sending alt as meta
|
||||
b |= 8;
|
||||
|
||||
if(!sgrMouseMode)
|
||||
b |= 32; // it just always does this
|
||||
|
||||
return b;
|
||||
}
|
||||
|
||||
|
|
@ -423,9 +411,14 @@ class TerminalEmulator {
|
|||
dragging = false;
|
||||
if(mouseButtonReleaseTracking) {
|
||||
int b = baseEventCode;
|
||||
if(!sgrMouseMode)
|
||||
b |= 3; // always send none / button released
|
||||
sendMouseEvent(b, termX, termY, true);
|
||||
ScopeBuffer!(char, 16) buffer;
|
||||
buffer ~= "\033[M";
|
||||
buffer ~= cast(char) (b | 32);
|
||||
addMouseCoordinates(buffer, termX, termY);
|
||||
//buffer ~= cast(char) (termX+1 + 32);
|
||||
//buffer ~= cast(char) (termY+1 + 32);
|
||||
sendToApplication(buffer[]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -435,7 +428,13 @@ class TerminalEmulator {
|
|||
lastDragX = termX;
|
||||
if(mouseMotionTracking || (mouseButtonMotionTracking && button)) {
|
||||
int b = baseEventCode;
|
||||
sendMouseEvent(b + 32, termX, termY);
|
||||
ScopeBuffer!(char, 16) buffer;
|
||||
buffer ~= "\033[M";
|
||||
buffer ~= cast(char) ((b | 32) + 32);
|
||||
addMouseCoordinates(buffer, termX, termY);
|
||||
//buffer ~= cast(char) (termX+1 + 32);
|
||||
//buffer ~= cast(char) (termY+1 + 32);
|
||||
sendToApplication(buffer[]);
|
||||
}
|
||||
|
||||
if(dragging) {
|
||||
|
|
@ -512,9 +511,18 @@ class TerminalEmulator {
|
|||
|
||||
int b = baseEventCode;
|
||||
|
||||
sendMouseEvent(b, termX, termY);
|
||||
int x = termX;
|
||||
int y = termY;
|
||||
x++; y++; // applications expect it to be one-based
|
||||
|
||||
ScopeBuffer!(char, 16) buffer;
|
||||
buffer ~= "\033[M";
|
||||
buffer ~= cast(char) (b | 32);
|
||||
addMouseCoordinates(buffer, termX, termY);
|
||||
//buffer ~= cast(char) (x + 32);
|
||||
//buffer ~= cast(char) (y + 32);
|
||||
|
||||
sendToApplication(buffer[]);
|
||||
} else {
|
||||
do_default_behavior:
|
||||
if(button == MouseButton.middle) {
|
||||
|
|
@ -615,22 +623,7 @@ class TerminalEmulator {
|
|||
return false;
|
||||
}
|
||||
|
||||
private void sendMouseEvent(int b, int x, int y, bool isRelease = false) {
|
||||
|
||||
ScopeBuffer!(char, 16) buffer;
|
||||
|
||||
if(sgrMouseMode) {
|
||||
buffer ~= "\033[<";
|
||||
buffer.appendIntAsString(b);
|
||||
buffer ~= ";";
|
||||
buffer.appendIntAsString(x + 1);
|
||||
buffer ~= ";";
|
||||
buffer.appendIntAsString(y + 1);
|
||||
buffer ~= isRelease ? "m" : "M";
|
||||
} else {
|
||||
buffer ~= "\033[M";
|
||||
buffer ~= cast(char) b;
|
||||
|
||||
private void addMouseCoordinates(ref ScopeBuffer!(char, 16) buffer, int x, int y) {
|
||||
// 1-based stuff and 32 is the base value
|
||||
x += 1 + 32;
|
||||
y += 1 + 32;
|
||||
|
|
@ -650,9 +643,6 @@ class TerminalEmulator {
|
|||
}
|
||||
}
|
||||
|
||||
sendToApplication(buffer[]);
|
||||
}
|
||||
|
||||
protected void returnToNormalScreen() {
|
||||
alternateScreenActive = false;
|
||||
|
||||
|
|
@ -1993,7 +1983,6 @@ class TerminalEmulator {
|
|||
bool mouseButtonTracking;
|
||||
private bool _mouseMotionTracking;
|
||||
bool utf8MouseMode;
|
||||
bool sgrMouseMode;
|
||||
bool mouseButtonReleaseTracking;
|
||||
bool mouseButtonMotionTracking;
|
||||
bool selectiveMouseTracking;
|
||||
|
|
@ -3234,10 +3223,7 @@ SGR (1006)
|
|||
The highlight tracking responses are also modified to an SGR-
|
||||
like format, using the same SGR-style scheme and button-encod-
|
||||
ings.
|
||||
|
||||
Note that M is used for motion; m is only release
|
||||
*/
|
||||
sgrMouseMode = true;
|
||||
break;
|
||||
case 1014:
|
||||
// ARSD extension: it is 1002 but selective, only
|
||||
|
|
@ -3314,7 +3300,6 @@ URXVT (1015)
|
|||
break;
|
||||
case 1006:
|
||||
// turn off sgr mouse
|
||||
sgrMouseMode = false;
|
||||
break;
|
||||
case 1015:
|
||||
// turn off urxvt mouse
|
||||
|
|
|
|||
|
|
@ -1503,10 +1503,6 @@ class TextLayouter {
|
|||
|
||||
auto bb = boundingBoxOfGlyph(segmentIndex, selection.focus);
|
||||
|
||||
// the y is added elsewhere already. i think.
|
||||
bb.left += segment.upperLeft.x;
|
||||
bb.right += segment.upperLeft.x;
|
||||
|
||||
bb.top += castFnumToCnum(glyphStyle.font.ascent);
|
||||
bb.bottom -= castFnumToCnum(glyphStyle.font.descent);
|
||||
|
||||
|
|
|
|||
628
uri.d
628
uri.d
|
|
@ -8,9 +8,6 @@ module arsd.uri;
|
|||
|
||||
import arsd.core;
|
||||
|
||||
import arsd.conv;
|
||||
import arsd.string;
|
||||
|
||||
alias encodeUriComponent = arsd.core.encodeUriComponent;
|
||||
alias decodeUriComponent = arsd.core.decodeUriComponent;
|
||||
|
||||
|
|
@ -21,628 +18,3 @@ alias decodeComponent = decodeUriComponent;
|
|||
// FIXME: merge and pull Uri struct from http2 and cgi. maybe via core.
|
||||
|
||||
// might also put base64 in here....
|
||||
|
||||
|
||||
|
||||
/++
|
||||
Represents a URI. It offers named access to the components and relative uri resolution, though as a user of the library, you'd mostly just construct it like `Uri("http://example.com/index.html")`.
|
||||
|
||||
History:
|
||||
Moved from duplication in [arsd.cgi] and [arsd.http2] to arsd.uri on November 2, 2025.
|
||||
+/
|
||||
struct Uri {
|
||||
UriString toUriString() {
|
||||
return UriString(toString());
|
||||
}
|
||||
|
||||
alias toUriString this; // blargh idk a url really is a string, but should it be implicit?
|
||||
|
||||
// scheme://userinfo@host:port/path?query#fragment
|
||||
|
||||
string scheme; /// e.g. "http" in "http://example.com/"
|
||||
string userinfo; /// the username (and possibly a password) in the uri
|
||||
string host; /// the domain name. note it may be an ip address or have percent encoding too.
|
||||
int port; /// port number, if given. Will be zero if a port was not explicitly given
|
||||
string path; /// e.g. "/folder/file.html" in "http://example.com/folder/file.html"
|
||||
string query; /// the stuff after the ? in a uri
|
||||
string fragment; /// the stuff after the # in a uri.
|
||||
|
||||
// cgi.d specific.......
|
||||
// idk if i want to keep these, since the functions they wrap are used many, many, many times in existing code, so this is either an unnecessary alias or a gratuitous break of compatibility
|
||||
// the decode ones need to keep different names anyway because we can't overload on return values...
|
||||
static string encode(string s) { return encodeUriComponent(s); }
|
||||
static string encode(string[string] s) { return encodeVariables(s); }
|
||||
static string encode(string[][string] s) { return encodeVariables(s); }
|
||||
|
||||
/++
|
||||
Parses an existing uri string (which should be pre-validated) into this further detailed structure.
|
||||
|
||||
History:
|
||||
Added November 2, 2025.
|
||||
+/
|
||||
this(UriString uriString) {
|
||||
this(uriString.toString());
|
||||
}
|
||||
|
||||
/++
|
||||
Transforms an interpolated expression sequence into a uri, encoding as appropriate as it reads.
|
||||
|
||||
History:
|
||||
Added November 2, 2025.
|
||||
+/
|
||||
this(Args...)(InterpolationHeader header, Args args, InterpolationFooter footer) {
|
||||
// will need to use iraw here for some cases. paths may partially encoded but still allow slashes, prolly needs a type.
|
||||
// so like $(path(x)) or $(queryString(x)) or maybe isemi or something. or make user split it into a string[] then recombine here....
|
||||
string thing;
|
||||
foreach(arg; args) {
|
||||
static if(is(typeof(arg) == InterpolationHeader))
|
||||
{}
|
||||
else
|
||||
static if(is(typeof(arg) == InterpolationFooter))
|
||||
{}
|
||||
else
|
||||
static if(is(typeof(arg) == InterpolatedLiteral!part, string part))
|
||||
thing ~= part;
|
||||
else
|
||||
static if(is(typeof(arg) == InterpolatedExpression!code, string code))
|
||||
{}
|
||||
else
|
||||
static if(is(typeof(arg) == iraw))
|
||||
thing ~= iraw.s;
|
||||
else
|
||||
thing ~= encodeUriComponent(to!string(arg));
|
||||
|
||||
}
|
||||
|
||||
this(thing);
|
||||
}
|
||||
|
||||
unittest {
|
||||
string bar = "12/";
|
||||
string baz = "&omg";
|
||||
auto uri = Uri(i"http://example.com/foo/$bar?thing=$baz");
|
||||
|
||||
assert(uri.toString() == "http://example.com/foo/12%2F?thing=%26omg");
|
||||
}
|
||||
|
||||
/// Breaks down a uri string to its components
|
||||
this(string uri) {
|
||||
size_t lastGoodIndex;
|
||||
foreach(char ch; uri) {
|
||||
if(ch > 127) {
|
||||
break;
|
||||
}
|
||||
lastGoodIndex++;
|
||||
}
|
||||
|
||||
string replacement = uri[0 .. lastGoodIndex];
|
||||
foreach(char ch; uri[lastGoodIndex .. $]) {
|
||||
if(ch > 127) {
|
||||
// need to percent-encode any non-ascii in it
|
||||
char[3] buffer;
|
||||
buffer[0] = '%';
|
||||
|
||||
auto first = ch / 16;
|
||||
auto second = ch % 16;
|
||||
first += (first >= 10) ? ('A'-10) : '0';
|
||||
second += (second >= 10) ? ('A'-10) : '0';
|
||||
|
||||
buffer[1] = cast(char) first;
|
||||
buffer[2] = cast(char) second;
|
||||
|
||||
replacement ~= buffer[];
|
||||
} else {
|
||||
replacement ~= ch;
|
||||
}
|
||||
}
|
||||
|
||||
reparse(replacement);
|
||||
}
|
||||
|
||||
/// Returns `port` if set, otherwise if scheme is https 443, otherwise always 80
|
||||
int effectivePort() const @property nothrow pure @safe @nogc {
|
||||
return port != 0 ? port
|
||||
: scheme == "https" ? 443 : 80;
|
||||
}
|
||||
|
||||
package string unixSocketPath = null;
|
||||
/// Indicates it should be accessed through a unix socket instead of regular tcp. Returns new version without modifying this object.
|
||||
Uri viaUnixSocket(string path) const {
|
||||
Uri copy = this;
|
||||
copy.unixSocketPath = path;
|
||||
return copy;
|
||||
}
|
||||
|
||||
/// Goes through a unix socket in the abstract namespace (linux only). Returns new version without modifying this object.
|
||||
version(linux)
|
||||
Uri viaAbstractSocket(string path) const {
|
||||
Uri copy = this;
|
||||
copy.unixSocketPath = "\0" ~ path;
|
||||
return copy;
|
||||
}
|
||||
|
||||
// these are like javascript's location.search and location.hash
|
||||
string search() const {
|
||||
return query.length ? ("?" ~ query) : "";
|
||||
}
|
||||
string hash() const {
|
||||
return fragment.length ? ("#" ~ fragment) : "";
|
||||
}
|
||||
|
||||
|
||||
private void reparse(string uri) {
|
||||
// from RFC 3986
|
||||
// the ctRegex triples the compile time and makes ugly errors for no real benefit
|
||||
// it was a nice experiment but just not worth it.
|
||||
// enum ctr = ctRegex!r"^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?";
|
||||
/*
|
||||
Captures:
|
||||
0 = whole url
|
||||
1 = scheme, with :
|
||||
2 = scheme, no :
|
||||
3 = authority, with //
|
||||
4 = authority, no //
|
||||
5 = path
|
||||
6 = query string, with ?
|
||||
7 = query string, no ?
|
||||
8 = anchor, with #
|
||||
9 = anchor, no #
|
||||
*/
|
||||
// Yikes, even regular, non-CT regex is also unacceptably slow to compile. 1.9s on my computer!
|
||||
// instead, I will DIY and cut that down to 0.6s on the same computer.
|
||||
/*
|
||||
|
||||
Note that authority is
|
||||
user:password@domain:port
|
||||
where the user:password@ part is optional, and the :port is optional.
|
||||
|
||||
Regex translation:
|
||||
|
||||
Scheme cannot have :, /, ?, or # in it, and must have one or more chars and end in a :. It is optional, but must be first.
|
||||
Authority must start with //, but cannot have any other /, ?, or # in it. It is optional.
|
||||
Path cannot have any ? or # in it. It is optional.
|
||||
Query must start with ? and must not have # in it. It is optional.
|
||||
Anchor must start with # and can have anything else in it to end of string. It is optional.
|
||||
*/
|
||||
|
||||
this = Uri.init; // reset all state
|
||||
|
||||
// empty uri = nothing special
|
||||
if(uri.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
size_t idx;
|
||||
|
||||
scheme_loop: foreach(char c; uri[idx .. $]) {
|
||||
switch(c) {
|
||||
case ':':
|
||||
case '/':
|
||||
case '?':
|
||||
case '#':
|
||||
break scheme_loop;
|
||||
default:
|
||||
}
|
||||
idx++;
|
||||
}
|
||||
|
||||
if(idx == 0 && uri[idx] == ':') {
|
||||
// this is actually a path! we skip way ahead
|
||||
goto path_loop;
|
||||
}
|
||||
|
||||
if(idx == uri.length) {
|
||||
// the whole thing is a path, apparently
|
||||
path = uri;
|
||||
return;
|
||||
}
|
||||
|
||||
if(idx > 0 && uri[idx] == ':') {
|
||||
scheme = uri[0 .. idx];
|
||||
idx++;
|
||||
} else {
|
||||
// we need to rewind; it found a / but no :, so the whole thing is prolly a path...
|
||||
idx = 0;
|
||||
}
|
||||
|
||||
if(idx + 2 < uri.length && uri[idx .. idx + 2] == "//") {
|
||||
// we have an authority....
|
||||
idx += 2;
|
||||
|
||||
auto authority_start = idx;
|
||||
authority_loop: foreach(char c; uri[idx .. $]) {
|
||||
switch(c) {
|
||||
case '/':
|
||||
case '?':
|
||||
case '#':
|
||||
break authority_loop;
|
||||
default:
|
||||
}
|
||||
idx++;
|
||||
}
|
||||
|
||||
auto authority = uri[authority_start .. idx];
|
||||
|
||||
auto idx2 = authority.indexOf("@");
|
||||
if(idx2 != -1) {
|
||||
userinfo = authority[0 .. idx2];
|
||||
authority = authority[idx2 + 1 .. $];
|
||||
}
|
||||
|
||||
if(authority.length && authority[0] == '[') {
|
||||
// ipv6 address special casing
|
||||
idx2 = authority.indexOf("]");
|
||||
if(idx2 != -1) {
|
||||
auto end = authority[idx2 + 1 .. $];
|
||||
if(end.length && end[0] == ':')
|
||||
idx2 = idx2 + 1;
|
||||
else
|
||||
idx2 = -1;
|
||||
}
|
||||
} else {
|
||||
idx2 = authority.indexOf(":");
|
||||
}
|
||||
|
||||
if(idx2 == -1) {
|
||||
port = 0; // 0 means not specified; we should use the default for the scheme
|
||||
host = authority;
|
||||
} else {
|
||||
host = authority[0 .. idx2];
|
||||
if(idx2 + 1 < authority.length)
|
||||
port = to!int(authority[idx2 + 1 .. $]);
|
||||
else
|
||||
port = 0;
|
||||
}
|
||||
}
|
||||
|
||||
path_loop:
|
||||
auto path_start = idx;
|
||||
|
||||
foreach(char c; uri[idx .. $]) {
|
||||
if(c == '?' || c == '#')
|
||||
break;
|
||||
idx++;
|
||||
}
|
||||
|
||||
path = uri[path_start .. idx];
|
||||
|
||||
if(idx == uri.length)
|
||||
return; // nothing more to examine...
|
||||
|
||||
if(uri[idx] == '?') {
|
||||
idx++;
|
||||
auto query_start = idx;
|
||||
foreach(char c; uri[idx .. $]) {
|
||||
if(c == '#')
|
||||
break;
|
||||
idx++;
|
||||
}
|
||||
query = uri[query_start .. idx];
|
||||
}
|
||||
|
||||
if(idx < uri.length && uri[idx] == '#') {
|
||||
idx++;
|
||||
fragment = uri[idx .. $];
|
||||
}
|
||||
|
||||
// uriInvalidated = false;
|
||||
}
|
||||
|
||||
private string rebuildUri() const {
|
||||
string ret;
|
||||
if(scheme.length)
|
||||
ret ~= scheme ~ ":";
|
||||
if(userinfo.length || host.length)
|
||||
ret ~= "//";
|
||||
if(userinfo.length)
|
||||
ret ~= userinfo ~ "@";
|
||||
if(host.length)
|
||||
ret ~= host;
|
||||
if(port)
|
||||
ret ~= ":" ~ to!string(port);
|
||||
|
||||
ret ~= path;
|
||||
|
||||
if(query.length)
|
||||
ret ~= "?" ~ query;
|
||||
|
||||
if(fragment.length)
|
||||
ret ~= "#" ~ fragment;
|
||||
|
||||
// uri = ret;
|
||||
// uriInvalidated = false;
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// Converts the broken down parts back into a complete string
|
||||
string toString() const {
|
||||
// if(uriInvalidated)
|
||||
return rebuildUri();
|
||||
}
|
||||
|
||||
/// Returns a new absolute Uri given a base. It treats this one as
|
||||
/// relative where possible, but absolute if not. (If protocol, domain, or
|
||||
/// other info is not set, the new one inherits it from the base.)
|
||||
///
|
||||
/// Browsers use a function like this to figure out links in html.
|
||||
Uri basedOn(in Uri baseUrl) const {
|
||||
Uri n = this; // copies
|
||||
if(n.scheme == "data")
|
||||
return n;
|
||||
// n.uriInvalidated = true; // make sure we regenerate...
|
||||
|
||||
// userinfo is not inherited... is this wrong?
|
||||
|
||||
// if anything is given in the existing url, we don't use the base anymore.
|
||||
if(n.scheme.length == 0) {
|
||||
n.scheme = baseUrl.scheme;
|
||||
if(n.host.length == 0) {
|
||||
n.host = baseUrl.host;
|
||||
if(n.port == 0) {
|
||||
n.port = baseUrl.port;
|
||||
if(n.path.length > 0 && n.path[0] != '/') {
|
||||
auto b = baseUrl.path[0 .. baseUrl.path.lastIndexOf("/") + 1];
|
||||
if(b.length == 0)
|
||||
b = "/";
|
||||
n.path = b ~ n.path;
|
||||
} else if(n.path.length == 0) {
|
||||
n.path = baseUrl.path;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
n.removeDots();
|
||||
|
||||
// if still basically talking to the same thing, we should inherit the unix path
|
||||
// too since basically the unix path is saying for this service, always use this override.
|
||||
if(n.host == baseUrl.host && n.scheme == baseUrl.scheme && n.port == baseUrl.port)
|
||||
n.unixSocketPath = baseUrl.unixSocketPath;
|
||||
|
||||
return n;
|
||||
}
|
||||
|
||||
/++
|
||||
Resolves ../ and ./ parts of the path. Used in the implementation of [basedOn] and you could also use it to normalize things.
|
||||
+/
|
||||
void removeDots() {
|
||||
auto parts = this.path.split("/");
|
||||
string[] toKeep;
|
||||
foreach(part; parts) {
|
||||
if(part == ".") {
|
||||
continue;
|
||||
} else if(part == "..") {
|
||||
//if(toKeep.length > 1)
|
||||
toKeep = toKeep[0 .. $-1];
|
||||
//else
|
||||
//toKeep = [""];
|
||||
continue;
|
||||
} else {
|
||||
//if(toKeep.length && toKeep[$-1].length == 0 && part.length == 0)
|
||||
//continue; // skip a `//` situation
|
||||
toKeep ~= part;
|
||||
}
|
||||
}
|
||||
|
||||
auto path = toKeep.join("/");
|
||||
if(path.length && path[0] != '/')
|
||||
path = "/" ~ path;
|
||||
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
unittest {
|
||||
auto uri = Uri("test.html");
|
||||
assert(uri.path == "test.html");
|
||||
uri = Uri("path/1/lol");
|
||||
assert(uri.path == "path/1/lol");
|
||||
uri = Uri("http://me@example.com");
|
||||
assert(uri.scheme == "http");
|
||||
assert(uri.userinfo == "me");
|
||||
assert(uri.host == "example.com");
|
||||
uri = Uri("http://example.com/#a");
|
||||
assert(uri.scheme == "http");
|
||||
assert(uri.host == "example.com");
|
||||
assert(uri.fragment == "a");
|
||||
uri = Uri("#foo");
|
||||
assert(uri.fragment == "foo");
|
||||
uri = Uri("?lol");
|
||||
assert(uri.query == "lol");
|
||||
uri = Uri("#foo?lol");
|
||||
assert(uri.fragment == "foo?lol");
|
||||
uri = Uri("?lol#foo");
|
||||
assert(uri.fragment == "foo");
|
||||
assert(uri.query == "lol");
|
||||
|
||||
uri = Uri("http://127.0.0.1/");
|
||||
assert(uri.host == "127.0.0.1");
|
||||
assert(uri.port == 0);
|
||||
|
||||
uri = Uri("http://127.0.0.1:123/");
|
||||
assert(uri.host == "127.0.0.1");
|
||||
assert(uri.port == 123);
|
||||
|
||||
uri = Uri("http://[ff:ff::0]/");
|
||||
assert(uri.host == "[ff:ff::0]");
|
||||
|
||||
uri = Uri("http://[ff:ff::0]:123/");
|
||||
assert(uri.host == "[ff:ff::0]");
|
||||
assert(uri.port == 123);
|
||||
}
|
||||
|
||||
// This can sometimes be a big pain in the butt for me, so lots of copy/paste here to cover
|
||||
// the possibilities.
|
||||
unittest {
|
||||
auto url = Uri("cool.html"); // checking relative links
|
||||
|
||||
assert(url.basedOn(Uri("http://test.com/what/test.html")) == "http://test.com/what/cool.html");
|
||||
assert(url.basedOn(Uri("https://test.com/what/test.html")) == "https://test.com/what/cool.html");
|
||||
assert(url.basedOn(Uri("http://test.com/what/")) == "http://test.com/what/cool.html");
|
||||
assert(url.basedOn(Uri("http://test.com/")) == "http://test.com/cool.html");
|
||||
assert(url.basedOn(Uri("http://test.com")) == "http://test.com/cool.html");
|
||||
assert(url.basedOn(Uri("http://test.com/what/test.html?a=b")) == "http://test.com/what/cool.html");
|
||||
assert(url.basedOn(Uri("http://test.com/what/test.html?a=b&c=d")) == "http://test.com/what/cool.html");
|
||||
assert(url.basedOn(Uri("http://test.com/what/test.html?a=b&c=d#what")) == "http://test.com/what/cool.html");
|
||||
assert(url.basedOn(Uri("http://test.com")) == "http://test.com/cool.html");
|
||||
|
||||
url = Uri("/something/cool.html"); // same server, different path
|
||||
assert(url.basedOn(Uri("http://test.com/what/test.html")) == "http://test.com/something/cool.html");
|
||||
assert(url.basedOn(Uri("https://test.com/what/test.html")) == "https://test.com/something/cool.html");
|
||||
assert(url.basedOn(Uri("http://test.com/what/")) == "http://test.com/something/cool.html");
|
||||
assert(url.basedOn(Uri("http://test.com/")) == "http://test.com/something/cool.html");
|
||||
assert(url.basedOn(Uri("http://test.com")) == "http://test.com/something/cool.html");
|
||||
assert(url.basedOn(Uri("http://test.com/what/test.html?a=b")) == "http://test.com/something/cool.html");
|
||||
assert(url.basedOn(Uri("http://test.com/what/test.html?a=b&c=d")) == "http://test.com/something/cool.html");
|
||||
assert(url.basedOn(Uri("http://test.com/what/test.html?a=b&c=d#what")) == "http://test.com/something/cool.html");
|
||||
assert(url.basedOn(Uri("http://test.com")) == "http://test.com/something/cool.html");
|
||||
|
||||
url = Uri("?query=answer"); // same path. server, protocol, and port, just different query string and fragment
|
||||
assert(url.basedOn(Uri("http://test.com/what/test.html")) == "http://test.com/what/test.html?query=answer");
|
||||
assert(url.basedOn(Uri("https://test.com/what/test.html")) == "https://test.com/what/test.html?query=answer");
|
||||
assert(url.basedOn(Uri("http://test.com/what/")) == "http://test.com/what/?query=answer");
|
||||
assert(url.basedOn(Uri("http://test.com/")) == "http://test.com/?query=answer");
|
||||
assert(url.basedOn(Uri("http://test.com")) == "http://test.com?query=answer");
|
||||
assert(url.basedOn(Uri("http://test.com/what/test.html?a=b")) == "http://test.com/what/test.html?query=answer");
|
||||
assert(url.basedOn(Uri("http://test.com/what/test.html?a=b&c=d")) == "http://test.com/what/test.html?query=answer");
|
||||
assert(url.basedOn(Uri("http://test.com/what/test.html?a=b&c=d#what")) == "http://test.com/what/test.html?query=answer");
|
||||
assert(url.basedOn(Uri("http://test.com")) == "http://test.com?query=answer");
|
||||
|
||||
url = Uri("/test/bar");
|
||||
assert(Uri("./").basedOn(url) == "/test/", Uri("./").basedOn(url));
|
||||
assert(Uri("../").basedOn(url) == "/");
|
||||
|
||||
url = Uri("http://example.com/");
|
||||
assert(Uri("../foo").basedOn(url) == "http://example.com/foo");
|
||||
|
||||
//auto uriBefore = url;
|
||||
url = Uri("#anchor"); // everything should remain the same except the anchor
|
||||
//uriBefore.anchor = "anchor");
|
||||
//assert(url == uriBefore);
|
||||
|
||||
url = Uri("//example.com"); // same protocol, but different server. the path here should be blank.
|
||||
|
||||
url = Uri("//example.com/example.html"); // same protocol, but different server and path
|
||||
|
||||
url = Uri("http://example.com/test.html"); // completely absolute link should never be modified
|
||||
|
||||
url = Uri("http://example.com"); // completely absolute link should never be modified, even if it has no path
|
||||
|
||||
// FIXME: add something for port too
|
||||
}
|
||||
}
|
||||
|
||||
/// Makes a data:// uri that can be used as links in most newer browsers (IE8+).
|
||||
string makeDataUrl()(string mimeType, in void[] data) {
|
||||
import std.base64; // FIXME then i can remove the () template
|
||||
auto data64 = Base64.encode(cast(const(ubyte[])) data);
|
||||
return "data:" ~ mimeType ~ ";base64," ~ cast(string)(data64);
|
||||
}
|
||||
|
||||
/// breaks down a url encoded string
|
||||
string[][string] decodeVariables(string data, string separator = "&", string[]* namesInOrder = null, string[]* valuesInOrder = null) {
|
||||
auto vars = data.split(separator);
|
||||
string[][string] _get;
|
||||
foreach(var; vars) {
|
||||
auto equal = var.indexOf("=");
|
||||
string name;
|
||||
string value;
|
||||
if(equal == -1) {
|
||||
name = decodeUriComponent(var);
|
||||
value = "";
|
||||
} else {
|
||||
//_get[decodeUriComponent(var[0..equal])] ~= decodeUriComponent(var[equal + 1 .. $].replace("+", " "));
|
||||
// stupid + -> space conversion.
|
||||
name = decodeUriComponent(var[0..equal].replace("+", " "));
|
||||
value = decodeUriComponent(var[equal + 1 .. $].replace("+", " "));
|
||||
}
|
||||
|
||||
_get[name] ~= value;
|
||||
if(namesInOrder)
|
||||
(*namesInOrder) ~= name;
|
||||
if(valuesInOrder)
|
||||
(*valuesInOrder) ~= value;
|
||||
}
|
||||
return _get;
|
||||
}
|
||||
|
||||
/// breaks down a url encoded string, but only returns the last value of any array
|
||||
string[string] decodeVariablesSingle(string data) {
|
||||
string[string] va;
|
||||
auto varArray = decodeVariables(data);
|
||||
foreach(k, v; varArray)
|
||||
va[k] = v[$-1];
|
||||
|
||||
return va;
|
||||
}
|
||||
|
||||
|
||||
/// url encodes the whole string
|
||||
string encodeVariables(in string[string] data) {
|
||||
string ret;
|
||||
|
||||
bool outputted = false;
|
||||
foreach(k, v; data) {
|
||||
if(outputted)
|
||||
ret ~= "&";
|
||||
else
|
||||
outputted = true;
|
||||
|
||||
ret ~= encodeUriComponent(k) ~ "=" ~ encodeUriComponent(v);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// url encodes a whole string
|
||||
string encodeVariables(in string[][string] data) {
|
||||
string ret;
|
||||
|
||||
bool outputted = false;
|
||||
foreach(k, arr; data) {
|
||||
foreach(v; arr) {
|
||||
if(outputted)
|
||||
ret ~= "&";
|
||||
else
|
||||
outputted = true;
|
||||
ret ~= encodeUriComponent(k) ~ "=" ~ encodeUriComponent(v);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// Encodes all but the explicitly unreserved characters per rfc 3986
|
||||
/// Alphanumeric and -_.~ are the only ones left unencoded
|
||||
/// name is borrowed from php
|
||||
string rawurlencode(in char[] data) {
|
||||
string ret;
|
||||
ret.reserve(data.length * 2);
|
||||
foreach(char c; data) {
|
||||
if(
|
||||
(c >= 'a' && c <= 'z') ||
|
||||
(c >= 'A' && c <= 'Z') ||
|
||||
(c >= '0' && c <= '9') ||
|
||||
c == '-' || c == '_' || c == '.' || c == '~')
|
||||
{
|
||||
ret ~= c;
|
||||
} else {
|
||||
ret ~= '%';
|
||||
// since we iterate on char, this should give us the octets of the full utf8 string
|
||||
ret ~= toHexUpper(c);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
char[2] toHexUpper(ubyte num) {
|
||||
char[2] ret = 0;
|
||||
ret[0] = num / 16;
|
||||
ret[1] = num % 16;
|
||||
ret[0] += cast(char)(ret[0] >= 10 ? 'A' : '0');
|
||||
ret[1] += cast(char)(ret[1] >= 10 ? 'A' : '0');
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
2
web.d
2
web.d
|
|
@ -11,8 +11,6 @@
|
|||
+/
|
||||
module arsd.web;
|
||||
|
||||
import arsd.uri : decodeVariablesSingle, encodeVariables;
|
||||
|
||||
static if(__VERSION__ <= 2076) {
|
||||
// compatibility shims with gdc
|
||||
enum JSONType {
|
||||
|
|
|
|||
|
|
@ -51,7 +51,6 @@ module arsd.webtemplate;
|
|||
|
||||
import arsd.script;
|
||||
import arsd.dom;
|
||||
import arsd.uri;
|
||||
|
||||
public import arsd.jsvar : var;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue