1 /* ide-vcs-uri.c
2 *
3 * Copyright 2015-2019 Christian Hergert <christian@hergert.me>
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 *
18 * SPDX-License-Identifier: GPL-3.0-or-later
19 */
20
21 #define G_LOG_DOMAIN "ide-vcs-uri"
22
23 #include "config.h"
24
25 #include <stdlib.h>
26 #include <string.h>
27
28 #include "ide-vcs-uri.h"
29
30 G_DEFINE_BOXED_TYPE (IdeVcsUri, ide_vcs_uri, ide_vcs_uri_ref, ide_vcs_uri_unref)
31
32 struct _IdeVcsUri
33 {
34 volatile gint ref_count;
35
36 /*
37 * If the URI string was created and has not been changed, we try extra
38 * hard to provide the same URI back from ide_vcs_uri_to_string(). This
39 * field is cleared any time any of the other fields are changed.
40 */
41 gchar *non_destructive_uri;
42
43 gchar *scheme;
44 gchar *user;
45 gchar *host;
46 gchar *path;
47 guint port;
48 };
49
50 static inline void
ide_vcs_uri_set_dirty(IdeVcsUri * self)51 ide_vcs_uri_set_dirty (IdeVcsUri *self)
52 {
53 g_clear_pointer (&self->non_destructive_uri, g_free);
54 }
55
56 static gboolean
ide_vcs_uri_validate(const IdeVcsUri * self)57 ide_vcs_uri_validate (const IdeVcsUri *self)
58 {
59 g_assert (self != NULL);
60
61 if (g_strcmp0 (self->scheme, "file") == 0)
62 return ((self->path != NULL) &&
63 (self->port == 0) &&
64 (self->host == NULL) &&
65 (self->user == NULL));
66
67 if ((g_strcmp0 (self->scheme, "http") == 0) ||
68 (g_strcmp0 (self->scheme, "ssh") == 0) ||
69 (g_strcmp0 (self->scheme, "git") == 0) ||
70 (g_strcmp0 (self->scheme, "https") == 0) ||
71 (g_strcmp0 (self->scheme, "rsync") == 0))
72 return ((self->path != NULL) && (self->host != NULL));
73
74 return TRUE;
75 }
76
77 static gboolean
ide_vcs_uri_parse(IdeVcsUri * self,const gchar * str)78 ide_vcs_uri_parse (IdeVcsUri *self,
79 const gchar *str)
80 {
81 static GRegex *regex1;
82 static GRegex *regex2;
83 static GRegex *regex3;
84 static gsize initialized;
85 GMatchInfo *match_info = NULL;
86 gboolean ret = FALSE;
87
88 if (g_once_init_enter (&initialized))
89 {
90 /* http://stackoverflow.com/questions/2514859/regular-expression-for-git-repository */
91
92 regex1 = g_regex_new ("file://(.*)", 0, 0, NULL);
93 g_assert (regex1);
94
95 regex2 = g_regex_new ("(\\w+://)(.+@)*([\\w\\d\\.]+)(:[\\d]+){0,1}/*(.*)", 0, 0, NULL);
96 g_assert (regex2);
97
98 regex3 = g_regex_new ("(.+@)*([\\w\\d\\.]+):(.*)", 0, 0, NULL);
99 g_assert (regex3);
100
101 g_once_init_leave (&initialized, TRUE);
102 }
103
104 if (str == NULL)
105 return FALSE;
106
107 /* check for local file:// style uris */
108 g_regex_match (regex1, str, 0, &match_info);
109 if (g_match_info_matches (match_info))
110 {
111 g_autofree gchar *path = NULL;
112
113 path = g_match_info_fetch (match_info, 1);
114
115 ide_vcs_uri_set_scheme (self, "file://");
116 ide_vcs_uri_set_user (self, NULL);
117 ide_vcs_uri_set_host (self, NULL);
118 ide_vcs_uri_set_port (self, 0);
119 ide_vcs_uri_set_path (self, path);
120
121 ret = TRUE;
122 }
123 g_clear_pointer (&match_info, g_match_info_free);
124
125 if (ret)
126 return ret;
127
128 /* check for ssh:// style network uris */
129 g_regex_match (regex2, str, 0, &match_info);
130 if (g_match_info_matches (match_info))
131 {
132 g_autofree gchar *scheme = NULL;
133 g_autofree gchar *user = NULL;
134 g_autofree gchar *host = NULL;
135 g_autofree gchar *path = NULL;
136 g_autofree gchar *portstr = NULL;
137 gint start_pos;
138 gint end_pos;
139 guint port = 0;
140
141 scheme = g_match_info_fetch (match_info, 1);
142 user = g_match_info_fetch (match_info, 2);
143 host = g_match_info_fetch (match_info, 3);
144 portstr = g_match_info_fetch (match_info, 4);
145 path = g_match_info_fetch (match_info, 5);
146
147 g_match_info_fetch_pos (match_info, 5, &start_pos, &end_pos);
148
149 if (*path != '~' && (start_pos > 0) && str [start_pos-1] == '/')
150 {
151 gchar *tmp;
152
153 tmp = path;
154 path = g_strdup_printf ("/%s", path);
155 g_free (tmp);
156 }
157
158 if (!ide_str_empty0 (portstr) && g_ascii_isdigit (portstr [1]))
159 port = CLAMP (atoi (&portstr [1]), 1, G_MAXINT16);
160
161 ide_vcs_uri_set_scheme (self, scheme);
162 ide_vcs_uri_set_user (self, user);
163 ide_vcs_uri_set_host (self, host);
164 ide_vcs_uri_set_port (self, port);
165 ide_vcs_uri_set_path (self, path);
166
167 ret = TRUE;
168 }
169 g_clear_pointer (&match_info, g_match_info_free);
170
171 if (ret)
172 return ret;
173
174 /* check for user@host style uris */
175 g_regex_match (regex3, str, 0, &match_info);
176 if (g_match_info_matches (match_info))
177 {
178 g_autofree gchar *user = NULL;
179 g_autofree gchar *host = NULL;
180 g_autofree gchar *path = NULL;
181
182 user = g_match_info_fetch (match_info, 1);
183 host = g_match_info_fetch (match_info, 2);
184 path = g_match_info_fetch (match_info, 3);
185
186 if (path && path[0] != '~' && path[0] != '/')
187 {
188 g_autofree gchar *tmp = path;
189 path = g_strdup_printf ("~/%s", tmp);
190 }
191
192 ide_vcs_uri_set_user (self, user);
193 ide_vcs_uri_set_host (self, host);
194 ide_vcs_uri_set_path (self, path);
195 ide_vcs_uri_set_scheme (self, "ssh://");
196
197 ret = TRUE;
198 }
199 g_clear_pointer (&match_info, g_match_info_free);
200
201 if (ret)
202 return ret;
203
204 /* try to avoid some in-progress schemes */
205 if (strstr (str, "://"))
206 return FALSE;
207
208 ide_vcs_uri_set_scheme (self, "file://");
209 ide_vcs_uri_set_user (self, NULL);
210 ide_vcs_uri_set_host (self, NULL);
211 ide_vcs_uri_set_port (self, 0);
212 ide_vcs_uri_set_path (self, str);
213
214 return TRUE;
215 }
216
217 IdeVcsUri *
ide_vcs_uri_new(const gchar * uri)218 ide_vcs_uri_new (const gchar *uri)
219 {
220 IdeVcsUri *self;
221
222 self = g_slice_new0 (IdeVcsUri);
223 self->ref_count = 1;
224
225 if (ide_vcs_uri_parse (self, uri) && ide_vcs_uri_validate (self))
226 {
227 self->non_destructive_uri = g_strdup (uri);
228 return self;
229 }
230
231 ide_vcs_uri_unref (self);
232
233 return NULL;
234 }
235
236 static void
ide_vcs_uri_finalize(IdeVcsUri * self)237 ide_vcs_uri_finalize (IdeVcsUri *self)
238 {
239 g_free (self->non_destructive_uri);
240 g_free (self->scheme);
241 g_free (self->user);
242 g_free (self->host);
243 g_free (self->path);
244 g_slice_free (IdeVcsUri, self);
245 }
246
247 IdeVcsUri *
ide_vcs_uri_ref(IdeVcsUri * self)248 ide_vcs_uri_ref (IdeVcsUri *self)
249 {
250 g_return_val_if_fail (self, NULL);
251 g_return_val_if_fail (self->ref_count > 0, NULL);
252
253 g_atomic_int_inc (&self->ref_count);
254
255 return self;
256 }
257
258 void
ide_vcs_uri_unref(IdeVcsUri * self)259 ide_vcs_uri_unref (IdeVcsUri *self)
260 {
261 g_return_if_fail (self);
262 g_return_if_fail (self->ref_count > 0);
263
264 if (g_atomic_int_dec_and_test (&self->ref_count))
265 ide_vcs_uri_finalize (self);
266 }
267
268 const gchar *
ide_vcs_uri_get_scheme(const IdeVcsUri * self)269 ide_vcs_uri_get_scheme (const IdeVcsUri *self)
270 {
271 g_return_val_if_fail (self, NULL);
272
273 return self->scheme;
274 }
275
276 const gchar *
ide_vcs_uri_get_user(const IdeVcsUri * self)277 ide_vcs_uri_get_user (const IdeVcsUri *self)
278 {
279 g_return_val_if_fail (self, NULL);
280
281 return self->user;
282 }
283
284 const gchar *
ide_vcs_uri_get_host(const IdeVcsUri * self)285 ide_vcs_uri_get_host (const IdeVcsUri *self)
286 {
287 g_return_val_if_fail (self, NULL);
288
289 return self->host;
290 }
291
292 guint
ide_vcs_uri_get_port(const IdeVcsUri * self)293 ide_vcs_uri_get_port (const IdeVcsUri *self)
294 {
295 g_return_val_if_fail (self, 0);
296
297 return self->port;
298 }
299
300 const gchar *
ide_vcs_uri_get_path(const IdeVcsUri * self)301 ide_vcs_uri_get_path (const IdeVcsUri *self)
302 {
303 g_return_val_if_fail (self, NULL);
304
305 return self->path;
306 }
307
308 void
ide_vcs_uri_set_scheme(IdeVcsUri * self,const gchar * scheme)309 ide_vcs_uri_set_scheme (IdeVcsUri *self,
310 const gchar *scheme)
311 {
312 g_return_if_fail (self);
313
314 if (ide_str_empty0 (scheme))
315 scheme = NULL;
316
317 if (scheme != self->scheme)
318 {
319 const gchar *tmp;
320
321 g_clear_pointer (&self->scheme, g_free);
322
323 if (scheme != NULL && (tmp = strchr (scheme, ':')))
324 self->scheme = g_strndup (scheme, tmp - scheme);
325 else
326 self->scheme = g_strdup (scheme);
327 }
328
329 ide_vcs_uri_set_dirty (self);
330 }
331
332 void
ide_vcs_uri_set_user(IdeVcsUri * self,const gchar * user)333 ide_vcs_uri_set_user (IdeVcsUri *self,
334 const gchar *user)
335 {
336 g_return_if_fail (self);
337
338 if (ide_str_empty0 (user))
339 user = NULL;
340
341 if (user != self->user)
342 {
343 const gchar *tmp;
344
345 g_clear_pointer (&self->user, g_free);
346
347 if (user != NULL && (tmp = strchr (user, '@')))
348 self->user = g_strndup (user, tmp - user);
349 else
350 self->user = g_strdup (user);
351 }
352
353 ide_vcs_uri_set_dirty (self);
354 }
355
356 void
ide_vcs_uri_set_host(IdeVcsUri * self,const gchar * host)357 ide_vcs_uri_set_host (IdeVcsUri *self,
358 const gchar *host)
359 {
360 g_return_if_fail (self);
361
362 if (ide_str_empty0 (host))
363 host = NULL;
364
365 if (host != self->host)
366 {
367 g_free (self->host);
368 self->host = g_strdup (host);
369 }
370
371 ide_vcs_uri_set_dirty (self);
372 }
373
374 void
ide_vcs_uri_set_port(IdeVcsUri * self,guint port)375 ide_vcs_uri_set_port (IdeVcsUri *self,
376 guint port)
377 {
378 g_return_if_fail (self);
379 g_return_if_fail (port <= G_MAXINT16);
380
381 self->port = port;
382
383 ide_vcs_uri_set_dirty (self);
384 }
385
386 void
ide_vcs_uri_set_path(IdeVcsUri * self,const gchar * path)387 ide_vcs_uri_set_path (IdeVcsUri *self,
388 const gchar *path)
389 {
390 g_return_if_fail (self);
391
392 if (ide_str_empty0 (path))
393 path = NULL;
394
395 if (path != self->path)
396 {
397 if (path != NULL && (*path == ':'))
398 path++;
399 g_free (self->path);
400 self->path = g_strdup (path);
401 }
402
403 ide_vcs_uri_set_dirty (self);
404 }
405
406 gchar *
ide_vcs_uri_to_string(const IdeVcsUri * self)407 ide_vcs_uri_to_string (const IdeVcsUri *self)
408 {
409 GString *str;
410
411 g_return_val_if_fail (self, NULL);
412
413 if (self->non_destructive_uri != NULL)
414 return g_strdup (self->non_destructive_uri);
415
416 str = g_string_new (NULL);
417
418 g_string_append_printf (str, "%s://", self->scheme);
419
420 if (0 == g_strcmp0 (self->scheme, "file"))
421 {
422 g_string_append (str, self->path);
423 return g_string_free (str, FALSE);
424 }
425
426 if (self->user != NULL)
427 g_string_append_printf (str, "%s@", self->user);
428
429 g_string_append (str, self->host);
430
431 if (self->port != 0)
432 g_string_append_printf (str, ":%u", self->port);
433
434 if (self->path == NULL)
435 g_string_append (str, "/");
436 else if (self->path [0] == '~')
437 g_string_append_printf (str, "/%s", self->path);
438 else if (self->path [0] != '/')
439 g_string_append_printf (str, "/%s", self->path);
440 else
441 g_string_append (str, self->path);
442
443 return g_string_free (str, FALSE);
444 }
445
446 gboolean
ide_vcs_uri_is_valid(const gchar * uri_string)447 ide_vcs_uri_is_valid (const gchar *uri_string)
448 {
449 gboolean ret = FALSE;
450
451 if (uri_string != NULL)
452 {
453 IdeVcsUri *uri;
454
455 uri = ide_vcs_uri_new (uri_string);
456 ret = !!uri;
457 g_clear_pointer (&uri, ide_vcs_uri_unref);
458 }
459
460 return ret;
461 }
462
463 /**
464 * ide_vcs_uri_get_clone_name:
465 * @self: an #ideVcsUri
466 *
467 * Determines a suggested name for the checkout directory. Some special
468 * handling of suffixes such as ".git" are performed to improve the the
469 * quality of results.
470 *
471 * Returns: (transfer full) (nullable): a string containing the suggested
472 * clone directory name, or %NULL.
473 *
474 * Since: 3.32
475 */
476 gchar *
ide_vcs_uri_get_clone_name(const IdeVcsUri * self)477 ide_vcs_uri_get_clone_name (const IdeVcsUri *self)
478 {
479 g_autofree gchar *name = NULL;
480 const gchar *path;
481
482 g_return_val_if_fail (self != NULL, NULL);
483
484 if (!(path = ide_vcs_uri_get_path (self)))
485 return NULL;
486
487 if (ide_str_empty0 (path))
488 return NULL;
489
490 if (!(name = g_path_get_basename (path)))
491 return NULL;
492
493 /* Trim trailing ".git" */
494 if (g_str_has_suffix (name, ".git"))
495 *(strrchr (name, '.')) = '\0';
496
497 if (!g_str_equal (name, "/") && !g_str_equal (name, "~"))
498 return g_steal_pointer (&name);
499
500 return NULL;
501 }
502