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