Skip to content

Commit ead8015

Browse files
Bug fix: Special characters escape in database names.
1 parent daabf9b commit ead8015

File tree

7 files changed

+202
-30
lines changed

7 files changed

+202
-30
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -328,3 +328,6 @@ ASALocalRun/
328328

329329
# MFractors (Xamarin productivity tool) working folder
330330
.mfractor/
331+
332+
# VsCode
333+
.vscode

src/CouchDB.Driver/CouchClient.cs

+27-24
Original file line numberDiff line numberDiff line change
@@ -66,18 +66,15 @@ public CouchClient(string connectionString, Action<CouchSettings> couchSettingsF
6666
#region CRUD
6767

6868
/// <summary>
69-
/// Returns an instance of the CouchDB database with the given name.
69+
/// Returns an instance of the CouchDB database with the given name.
7070
/// If EnsureDatabaseExists is configured, it creates the database if it doesn't exists.
7171
/// </summary>
7272
/// <typeparam name="TSource">The type of database documents.</typeparam>
7373
/// <param name="database">The database name.</param>
7474
/// <returns>An instance of the CouchDB database with given name.</returns>
7575
public CouchDatabase<TSource> GetDatabase<TSource>(string database) where TSource : CouchDocument
7676
{
77-
if (database == null)
78-
{
79-
throw new ArgumentNullException(nameof(database));
80-
}
77+
database = EscapeDatabaseName(database);
8178

8279
if (_settings.CheckDatabaseExists)
8380
{
@@ -102,15 +99,7 @@ public CouchDatabase<TSource> GetDatabase<TSource>(string database) where TSourc
10299
/// <returns>A task that represents the asynchronous operation. The task result contains the newly created CouchDB database.</returns>
103100
public async Task<CouchDatabase<TSource>> CreateDatabaseAsync<TSource>(string database, int? shards = null, int? replicas = null) where TSource : CouchDocument
104101
{
105-
if (database == null)
106-
{
107-
throw new ArgumentNullException(nameof(database));
108-
}
109-
110-
if (!_systemDatabases.Contains(database) && !new Regex(@"^[a-z][a-z0-9_$()+/-]*$").IsMatch(database))
111-
{
112-
throw new ArgumentException($"Name {database} contains invalid characters. Please visit: https://docs.couchdb.org/en/stable/api/database/common.html#put--db", nameof(database));
113-
}
102+
database = EscapeDatabaseName(database);
114103

115104
IFlurlRequest request = NewRequest()
116105
.AppendPathSegment(database);
@@ -119,6 +108,7 @@ public async Task<CouchDatabase<TSource>> CreateDatabaseAsync<TSource>(string da
119108
{
120109
request = request.SetQueryParam("q", shards.Value);
121110
}
111+
122112
if (replicas.HasValue)
123113
{
124114
request = request.SetQueryParam("n", replicas.Value);
@@ -146,10 +136,7 @@ public async Task<CouchDatabase<TSource>> CreateDatabaseAsync<TSource>(string da
146136
/// <returns>A task that represents the asynchronous operation.</returns>
147137
public async Task DeleteDatabaseAsync<TSource>(string database) where TSource : CouchDocument
148138
{
149-
if (database == null)
150-
{
151-
throw new ArgumentNullException(nameof(database));
152-
}
139+
database = EscapeDatabaseName(database);
153140

154141
OperationResult result = await NewRequest()
155142
.AppendPathSegment(database)
@@ -158,7 +145,8 @@ public async Task DeleteDatabaseAsync<TSource>(string database) where TSource :
158145
.SendRequestAsync()
159146
.ConfigureAwait(false);
160147

161-
if (!result.Ok) {
148+
if (!result.Ok)
149+
{
162150
throw new CouchException("Something went wrong during the delete.", "S");
163151
}
164152
}
@@ -168,7 +156,7 @@ public async Task DeleteDatabaseAsync<TSource>(string database) where TSource :
168156
#region CRUD reflection
169157

170158
/// <summary>
171-
/// Returns an instance of the CouchDB database of the given type.
159+
/// Returns an instance of the CouchDB database of the given type.
172160
/// If EnsureDatabaseExists is configured, it creates the database if it doesn't exists.
173161
/// </summary>
174162
/// <typeparam name="TSource">The type of database documents.</typeparam>
@@ -179,7 +167,7 @@ public CouchDatabase<TSource> GetDatabase<TSource>() where TSource : CouchDocume
179167
}
180168

181169
/// <summary>
182-
/// Creates a new database of the given type in the server.
170+
/// Creates a new database of the given type in the server.
183171
/// The name must begin with a lowercase letter and can contains only lowercase characters, digits or _, $, (, ), +, - and /.s
184172
/// </summary>
185173
/// <typeparam name="TSource">The type of database documents.</typeparam>
@@ -235,7 +223,7 @@ public CouchDatabase<TUser> GetUsersDatabase<TUser>() where TUser : CouchUser
235223
#region Utils
236224

237225
/// <summary>
238-
/// Determines whether the server is up, running, and ready to respond to requests.
226+
/// Determines whether the server is up, running, and ready to respond to requests.
239227
/// </summary>
240228
/// <returns>true is the server is not in maintenance_mode; otherwise, false.</returns>
241229
public async Task<bool> IsUpAsync()
@@ -249,7 +237,7 @@ public async Task<bool> IsUpAsync()
249237
.ConfigureAwait(false);
250238
return result.Status == "ok";
251239
}
252-
catch(CouchNotFoundException)
240+
catch (CouchNotFoundException)
253241
{
254242
return false;
255243
}
@@ -292,6 +280,21 @@ private IFlurlRequest NewRequest()
292280
return _flurlClient.Request(ConnectionString);
293281
}
294282

283+
private string EscapeDatabaseName(string database)
284+
{
285+
if (database == null)
286+
{
287+
throw new ArgumentNullException(nameof(database));
288+
}
289+
290+
if (!_systemDatabases.Contains(database) && !new Regex(@"^[a-z][a-z0-9_$()+/-]*$").IsMatch(database))
291+
{
292+
throw new ArgumentException($"Name {database} contains invalid characters. Please visit: https://docs.couchdb.org/en/stable/api/database/common.html#put--db", nameof(database));
293+
}
294+
295+
return Uri.EscapeDataString(database);
296+
}
297+
295298
/// <summary>
296299
/// Performs the logout and disposes the HTTP client.
297300
/// </summary>
@@ -305,7 +308,7 @@ protected virtual void Dispose(bool disposing)
305308
{
306309
if (_settings.AuthenticationType == AuthenticationType.Cookie && _settings.LogOutOnDispose)
307310
{
308-
AsyncContext.Run(() => LogoutAsync().ConfigureAwait(false));
311+
_ = AsyncContext.Run(() => LogoutAsync().ConfigureAwait(false));
309312
}
310313
_flurlClient.Dispose();
311314
}

src/CouchDB.Driver/CouchDatabase.cs

+6-4
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public class CouchDatabase<TSource> where TSource : CouchDocument
2525
private readonly IFlurlClient _flurlClient;
2626
private readonly CouchSettings _settings;
2727
private readonly string _connectionString;
28+
private string _database;
2829

2930
/// <summary>
3031
/// The database name.
@@ -41,9 +42,10 @@ internal CouchDatabase(IFlurlClient flurlClient, CouchSettings settings, string
4142
_flurlClient = flurlClient ?? throw new ArgumentNullException(nameof(flurlClient));
4243
_settings = settings ?? throw new ArgumentNullException(nameof(settings));
4344
_connectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString));
44-
Database = db ?? throw new ArgumentNullException(nameof(db));
45-
_queryProvider = new CouchQueryProvider(flurlClient, _settings, connectionString, Database);
45+
_database = db ?? throw new ArgumentNullException(nameof(db));
46+
_queryProvider = new CouchQueryProvider(flurlClient, _settings, connectionString, _database);
4647

48+
Database = Uri.UnescapeDataString(_database);
4749
Security = new CouchSecurity(NewRequest);
4850
}
4951

@@ -274,7 +276,7 @@ private async Task<List<TSource>> SendQueryAsync(Func<IFlurlRequest, Task<HttpRe
274276

275277
return findResult.Docs.ToList();
276278
}
277-
279+
278280
/// Finds all documents with given IDs.
279281
/// </summary>
280282
/// <param name="docIds">The collection of documents IDs.</param>
@@ -487,7 +489,7 @@ public override string ToString()
487489

488490
private IFlurlRequest NewRequest()
489491
{
490-
return _flurlClient.Request(_connectionString).AppendPathSegment(Database);
492+
return _flurlClient.Request(_connectionString).AppendPathSegment(_database);
491493
}
492494

493495
#endregion

src/CouchDB.Driver/CouchQueryProvider.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ private object InvokeUnsupportedMethodCallExpression(object result, MethodCallEx
142142
MethodInfo queryableMethodInfo = methodCallExpression.Method;
143143
Expression[] queryableMethodArguments = methodCallExpression.Arguments.ToArray();
144144

145-
// Since Max and Min are not map 1 to 1 from Queryable to Enumerable
145+
// Since Max and Min are not map 1 to 1 from Queryable to Enumerable
146146
// they need to be handled differently
147147
MethodInfo FindEnumerableMethod()
148148
{

src/CouchDB.Driver/QueryTranslator.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
#pragma warning disable IDE0058 // Expression value is never used
88
namespace CouchDB.Driver
9-
{
9+
{
1010
internal partial class QueryTranslator : ExpressionVisitor
1111
{
1212
private readonly CouchSettings _settings;

tests/CouchDB.Driver.E2ETests/Client_Tests.cs

+36
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,42 @@ public async Task Crud()
4343
await client.DeleteDatabaseAsync<Rebel>().ConfigureAwait(false);
4444
}
4545
}
46+
47+
[Fact]
48+
public async Task Crud_SpecialCharacters()
49+
{
50+
var databaseName = "rebel0_$()+/-";
51+
52+
using (var client = new CouchClient("http://localhost:5984"))
53+
{
54+
IEnumerable<string> dbs = await client.GetDatabasesNamesAsync().ConfigureAwait(false);
55+
CouchDatabase<Rebel> rebels = client.GetDatabase<Rebel>(databaseName);
56+
57+
if (dbs.Contains(rebels.Database))
58+
{
59+
await client.DeleteDatabaseAsync<Rebel>(databaseName).ConfigureAwait(false);
60+
}
61+
62+
rebels = await client.CreateDatabaseAsync<Rebel>(databaseName).ConfigureAwait(false);
63+
64+
Rebel luke = await rebels.CreateAsync(new Rebel { Name = "Luke", Age = 19 }).ConfigureAwait(false);
65+
Assert.Equal("Luke", luke.Name);
66+
67+
luke.Surname = "Skywalker";
68+
luke = await rebels.CreateOrUpdateAsync(luke).ConfigureAwait(false);
69+
Assert.Equal("Skywalker", luke.Surname);
70+
71+
luke = await rebels.FindAsync(luke.Id).ConfigureAwait(false);
72+
Assert.Equal(19, luke.Age);
73+
74+
await rebels.DeleteAsync(luke).ConfigureAwait(false);
75+
luke = await rebels.FindAsync(luke.Id).ConfigureAwait(false);
76+
Assert.Null(luke);
77+
78+
await client.DeleteDatabaseAsync<Rebel>(databaseName).ConfigureAwait(false);
79+
}
80+
}
81+
4682
[Fact]
4783
public async Task Users()
4884
{

0 commit comments

Comments
 (0)