New API: inspect-get-windows-current-control-set
[libguestfs.git] / inspector / virt-inspector.c
1 /* virt-inspector
2  * Copyright (C) 2010-2011 Red Hat Inc.
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; either version 2 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program; if not, write to the Free Software
16  * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
17  */
18
19 #include <config.h>
20
21 #include <stdio.h>
22 #include <stdlib.h>
23 #include <inttypes.h>
24 #include <unistd.h>
25 #include <getopt.h>
26 #include <locale.h>
27 #include <assert.h>
28
29 #include <libxml/xmlIO.h>
30 #include <libxml/xmlwriter.h>
31
32 #include "progname.h"
33 #include "c-ctype.h"
34
35 #include "guestfs.h"
36 #include "options.h"
37
38 /* Currently open libguestfs handle. */
39 guestfs_h *g;
40
41 int read_only = 1;
42 int live = 0;
43 int verbose = 0;
44 int keys_from_stdin = 0;
45 int echo_keys = 0;
46 const char *libvirt_uri = NULL;
47 int inspector = 1;
48
49 static void output (char **roots);
50 static void output_roots (xmlTextWriterPtr xo, char **roots);
51 static void output_root (xmlTextWriterPtr xo, char *root);
52 static void output_mountpoints (xmlTextWriterPtr xo, char *root);
53 static void output_filesystems (xmlTextWriterPtr xo, char *root);
54 static void output_applications (xmlTextWriterPtr xo, char *root);
55 static void canonicalize (char *dev);
56 static void free_strings (char **argv);
57 static int count_strings (char *const*argv);
58
59 static inline char *
60 bad_cast (char const *s)
61 {
62   return (char *) s;
63 }
64
65 static void __attribute__((noreturn))
66 usage (int status)
67 {
68   if (status != EXIT_SUCCESS)
69     fprintf (stderr, _("Try `%s --help' for more information.\n"),
70              program_name);
71   else {
72     fprintf (stdout,
73            _("%s: display information about a virtual machine\n"
74              "Copyright (C) 2010 Red Hat Inc.\n"
75              "Usage:\n"
76              "  %s [--options] -d domname file [file ...]\n"
77              "  %s [--options] -a disk.img [-a disk.img ...] file [file ...]\n"
78              "Options:\n"
79              "  -a|--add image       Add image\n"
80              "  -c|--connect uri     Specify libvirt URI for -d option\n"
81              "  -d|--domain guest    Add disks from libvirt guest\n"
82              "  --echo-keys          Don't turn off echo for passphrases\n"
83              "  --format[=raw|..]    Force disk format for -a option\n"
84              "  --help               Display brief help\n"
85              "  --keys-from-stdin    Read passphrases from stdin\n"
86              "  -v|--verbose         Verbose messages\n"
87              "  -V|--version         Display version and exit\n"
88              "  -x                   Trace libguestfs API calls\n"
89              "For more information, see the manpage %s(1).\n"),
90              program_name, program_name, program_name,
91              program_name);
92   }
93   exit (status);
94 }
95
96 int
97 main (int argc, char *argv[])
98 {
99   /* Set global program name that is not polluted with libtool artifacts.  */
100   set_program_name (argv[0]);
101
102   setlocale (LC_ALL, "");
103   bindtextdomain (PACKAGE, LOCALEBASEDIR);
104   textdomain (PACKAGE);
105
106   enum { HELP_OPTION = CHAR_MAX + 1 };
107
108   static const char *options = "a:c:d:vVx";
109   static const struct option long_options[] = {
110     { "add", 1, 0, 'a' },
111     { "connect", 1, 0, 'c' },
112     { "domain", 1, 0, 'd' },
113     { "echo-keys", 0, 0, 0 },
114     { "format", 2, 0, 0 },
115     { "help", 0, 0, HELP_OPTION },
116     { "keys-from-stdin", 0, 0, 0 },
117     { "verbose", 0, 0, 'v' },
118     { "version", 0, 0, 'V' },
119     { 0, 0, 0, 0 }
120   };
121   struct drv *drvs = NULL;
122   struct drv *drv;
123   const char *format = NULL;
124   int c;
125   int option_index;
126
127   g = guestfs_create ();
128   if (g == NULL) {
129     fprintf (stderr, _("guestfs_create: failed to create handle\n"));
130     exit (EXIT_FAILURE);
131   }
132
133   argv[0] = bad_cast (program_name);
134
135   for (;;) {
136     c = getopt_long (argc, argv, options, long_options, &option_index);
137     if (c == -1) break;
138
139     switch (c) {
140     case 0:                     /* options which are long only */
141       if (STREQ (long_options[option_index].name, "keys-from-stdin")) {
142         keys_from_stdin = 1;
143       } else if (STREQ (long_options[option_index].name, "echo-keys")) {
144         echo_keys = 1;
145       } else if (STREQ (long_options[option_index].name, "format")) {
146         if (!optarg || STREQ (optarg, ""))
147           format = NULL;
148         else
149           format = optarg;
150       } else {
151         fprintf (stderr, _("%s: unknown long option: %s (%d)\n"),
152                  program_name, long_options[option_index].name, option_index);
153         exit (EXIT_FAILURE);
154       }
155       break;
156
157     case 'a':
158       OPTION_a;
159       break;
160
161     case 'c':
162       OPTION_c;
163       break;
164
165     case 'd':
166       OPTION_d;
167       break;
168
169     case 'h':
170       usage (EXIT_SUCCESS);
171
172     case 'v':
173       OPTION_v;
174       break;
175
176     case 'V':
177       OPTION_V;
178       break;
179
180     case 'x':
181       OPTION_x;
182       break;
183
184     case HELP_OPTION:
185       usage (EXIT_SUCCESS);
186
187     default:
188       usage (EXIT_FAILURE);
189     }
190   }
191
192   /* Old-style syntax?  There were no -a or -d options in the old
193    * virt-inspector which is how we detect this.
194    */
195   if (drvs == NULL) {
196     while (optind < argc) {
197       if (strchr (argv[optind], '/') ||
198           access (argv[optind], F_OK) == 0) { /* simulate -a option */
199         drv = malloc (sizeof (struct drv));
200         if (!drv) {
201           perror ("malloc");
202           exit (EXIT_FAILURE);
203         }
204         drv->type = drv_a;
205         drv->a.filename = argv[optind];
206         drv->a.format = NULL;
207         drv->next = drvs;
208         drvs = drv;
209       } else {                  /* simulate -d option */
210         drv = malloc (sizeof (struct drv));
211         if (!drv) {
212           perror ("malloc");
213           exit (EXIT_FAILURE);
214         }
215         drv->type = drv_d;
216         drv->d.guest = argv[optind];
217         drv->next = drvs;
218         drvs = drv;
219       }
220
221       optind++;
222     }
223   }
224
225   /* These are really constants, but they have to be variables for the
226    * options parsing code.  Assert here that they have known-good
227    * values.
228    */
229   assert (read_only == 1);
230   assert (inspector == 1);
231   assert (live == 0);
232
233   /* Must be no extra arguments on the command line. */
234   if (optind != argc)
235     usage (EXIT_FAILURE);
236
237   /* User must have specified some drives. */
238   if (drvs == NULL)
239     usage (EXIT_FAILURE);
240
241   /* Add drives, inspect and mount.  Note that inspector is always true,
242    * and there is no -m option.
243    */
244   add_drives (drvs, 'a');
245
246   if (guestfs_launch (g) == -1)
247     exit (EXIT_FAILURE);
248
249   /* Free up data structures, no longer needed after this point. */
250   free_drives (drvs);
251
252   /* NB. Can't call inspect_mount () here (ie. normal processing of
253    * the -i option) because it can only handle a single root.  So we
254    * use low-level APIs.
255    */
256   inspect_do_decrypt ();
257
258   char **roots = guestfs_inspect_os (g);
259   if (roots == NULL) {
260     fprintf (stderr, _("%s: no operating system could be detected inside this disk image.\n\nThis may be because the file is not a disk image, or is not a virtual machine\nimage, or because the OS type is not understood by libguestfs.\n\nNOTE for Red Hat Enterprise Linux 6 users: for Windows guest support you must\ninstall the separate libguestfs-winsupport package.\n\nIf you feel this is an error, please file a bug report including as much\ninformation about the disk image as possible.\n"),
261              program_name);
262     exit (EXIT_FAILURE);
263   }
264
265   output (roots);
266
267   free_strings (roots);
268
269   guestfs_close (g);
270
271   exit (EXIT_SUCCESS);
272 }
273
274 #define DISABLE_GUESTFS_ERRORS_FOR(stmt) do {                           \
275     guestfs_error_handler_cb old_error_cb;                              \
276     void *old_error_data;                                               \
277     old_error_cb = guestfs_get_error_handler (g, &old_error_data);      \
278     guestfs_set_error_handler (g, NULL, NULL);                          \
279     stmt;                                                               \
280     guestfs_set_error_handler (g, old_error_cb, old_error_data);        \
281   } while (0)
282
283 #define XMLERROR(code,e) do {                                           \
284     if ((e) == (code)) {                                                \
285       fprintf (stderr, _("%s: XML write error at \"%s\": %m\n"),        \
286                #e, program_name);                                       \
287       exit (EXIT_FAILURE);                                              \
288     }                                                                   \
289   } while (0)
290
291 static void
292 output (char **roots)
293 {
294   xmlOutputBufferPtr ob = xmlOutputBufferCreateFd (1, NULL);
295   if (ob == NULL) {
296     fprintf (stderr,
297              _("%s: xmlOutputBufferCreateFd: failed to open stdout\n"),
298              program_name);
299     exit (EXIT_FAILURE);
300   }
301
302   xmlTextWriterPtr xo = xmlNewTextWriter (ob);
303   if (xo == NULL) {
304     fprintf (stderr,
305              _("%s: xmlNewTextWriter: failed to create libxml2 writer\n"),
306              program_name);
307     exit (EXIT_FAILURE);
308   }
309
310   /* Pretty-print the output. */
311   XMLERROR (-1, xmlTextWriterSetIndent (xo, 1));
312   XMLERROR (-1, xmlTextWriterSetIndentString (xo, BAD_CAST "  "));
313
314   XMLERROR (-1, xmlTextWriterStartDocument (xo, NULL, NULL, NULL));
315   output_roots (xo, roots);
316   XMLERROR (-1, xmlTextWriterEndDocument (xo));
317
318   /* 'ob' is freed by this too. */
319   xmlFreeTextWriter (xo);
320 }
321
322 static void
323 output_roots (xmlTextWriterPtr xo, char **roots)
324 {
325   size_t i;
326
327   XMLERROR (-1, xmlTextWriterStartElement (xo, BAD_CAST "operatingsystems"));
328   for (i = 0; roots[i] != NULL; ++i)
329     output_root (xo, roots[i]);
330   XMLERROR (-1, xmlTextWriterEndElement (xo));
331 }
332
333 static void
334 output_root (xmlTextWriterPtr xo, char *root)
335 {
336   char *str;
337   int i, r;
338   char buf[32];
339   char canonical_root[strlen (root) + 1];
340
341   XMLERROR (-1, xmlTextWriterStartElement (xo, BAD_CAST "operatingsystem"));
342
343   strcpy (canonical_root, root);
344   canonicalize (canonical_root);
345   XMLERROR (-1,
346     xmlTextWriterWriteElement (xo, BAD_CAST "root", BAD_CAST canonical_root));
347
348   str = guestfs_inspect_get_type (g, root);
349   if (!str) exit (EXIT_FAILURE);
350   if (STRNEQ (str, "unknown"))
351     XMLERROR (-1,
352       xmlTextWriterWriteElement (xo, BAD_CAST "name", BAD_CAST str));
353   free (str);
354
355   str = guestfs_inspect_get_arch (g, root);
356   if (!str) exit (EXIT_FAILURE);
357   if (STRNEQ (str, "unknown"))
358     XMLERROR (-1,
359       xmlTextWriterWriteElement (xo, BAD_CAST "arch", BAD_CAST str));
360   free (str);
361
362   str = guestfs_inspect_get_distro (g, root);
363   if (!str) exit (EXIT_FAILURE);
364   if (STRNEQ (str, "unknown"))
365     XMLERROR (-1,
366       xmlTextWriterWriteElement (xo, BAD_CAST "distro", BAD_CAST str));
367   free (str);
368
369   str = guestfs_inspect_get_product_name (g, root);
370   if (!str) exit (EXIT_FAILURE);
371   if (STRNEQ (str, "unknown"))
372     XMLERROR (-1,
373       xmlTextWriterWriteElement (xo, BAD_CAST "product_name", BAD_CAST str));
374   free (str);
375
376   str = guestfs_inspect_get_product_variant (g, root);
377   if (!str) exit (EXIT_FAILURE);
378   if (STRNEQ (str, "unknown"))
379     XMLERROR (-1,
380       xmlTextWriterWriteElement (xo, BAD_CAST "product_variant", BAD_CAST str));
381   free (str);
382
383   i = guestfs_inspect_get_major_version (g, root);
384   snprintf (buf, sizeof buf, "%d", i);
385   XMLERROR (-1,
386     xmlTextWriterWriteElement (xo, BAD_CAST "major_version", BAD_CAST buf));
387   i = guestfs_inspect_get_minor_version (g, root);
388   snprintf (buf, sizeof buf, "%d", i);
389   XMLERROR (-1,
390     xmlTextWriterWriteElement (xo, BAD_CAST "minor_version", BAD_CAST buf));
391
392   str = guestfs_inspect_get_package_format (g, root);
393   if (!str) exit (EXIT_FAILURE);
394   if (STRNEQ (str, "unknown"))
395     XMLERROR (-1,
396       xmlTextWriterWriteElement (xo, BAD_CAST "package_format", BAD_CAST str));
397   free (str);
398
399   str = guestfs_inspect_get_package_management (g, root);
400   if (!str) exit (EXIT_FAILURE);
401   if (STRNEQ (str, "unknown"))
402     XMLERROR (-1,
403       xmlTextWriterWriteElement (xo, BAD_CAST "package_management",
404                                  BAD_CAST str));
405   free (str);
406
407   /* inspect-get-windows-systemroot will fail with non-windows guests,
408    * or if the systemroot could not be determined for a windows guest.
409    * Disable error output around this call.
410    */
411   DISABLE_GUESTFS_ERRORS_FOR (
412     str = guestfs_inspect_get_windows_systemroot (g, root);
413     if (str)
414       XMLERROR (-1,
415                 xmlTextWriterWriteElement (xo, BAD_CAST "windows_systemroot",
416                                            BAD_CAST str));
417     free (str);
418   );
419   DISABLE_GUESTFS_ERRORS_FOR (
420     str = guestfs_inspect_get_windows_current_control_set (g, root);
421     if (str)
422       XMLERROR (-1,
423                 xmlTextWriterWriteElement (xo, BAD_CAST "windows_current_control_set",
424                                            BAD_CAST str));
425     free (str);
426   );
427
428   str = guestfs_inspect_get_format (g, root);
429   if (!str) exit (EXIT_FAILURE);
430   if (STRNEQ (str, "unknown"))
431     XMLERROR (-1,
432       xmlTextWriterWriteElement (xo, BAD_CAST "format",
433                                  BAD_CAST str));
434   free (str);
435
436   r = guestfs_inspect_is_live (g, root);
437   if (r > 0) {
438     XMLERROR (-1,
439               xmlTextWriterStartElement (xo, BAD_CAST "live"));
440     XMLERROR (-1, xmlTextWriterEndElement (xo));
441   }
442
443   r = guestfs_inspect_is_netinst (g, root);
444   if (r > 0) {
445     XMLERROR (-1,
446               xmlTextWriterStartElement (xo, BAD_CAST "netinst"));
447     XMLERROR (-1, xmlTextWriterEndElement (xo));
448   }
449
450   r = guestfs_inspect_is_multipart (g, root);
451   if (r > 0) {
452     XMLERROR (-1,
453               xmlTextWriterStartElement (xo, BAD_CAST "multipart"));
454     XMLERROR (-1, xmlTextWriterEndElement (xo));
455   }
456
457   output_mountpoints (xo, root);
458
459   output_filesystems (xo, root);
460
461   output_applications (xo, root);
462
463   XMLERROR (-1, xmlTextWriterEndElement (xo));
464 }
465
466 static int
467 compare_keys (const void *p1, const void *p2)
468 {
469   const char *key1 = * (char * const *) p1;
470   const char *key2 = * (char * const *) p2;
471
472   return strcmp (key1, key2);
473 }
474
475 static int
476 compare_keys_len (const void *p1, const void *p2)
477 {
478   const char *key1 = * (char * const *) p1;
479   const char *key2 = * (char * const *) p2;
480   int c;
481
482   c = strlen (key1) - strlen (key2);
483   if (c != 0)
484     return c;
485
486   return compare_keys (p1, p2);
487 }
488
489 static void
490 output_mountpoints (xmlTextWriterPtr xo, char *root)
491 {
492   char **mountpoints;
493   size_t i;
494
495   mountpoints = guestfs_inspect_get_mountpoints (g, root);
496   if (mountpoints == NULL)
497     exit (EXIT_FAILURE);
498
499   /* Sort by key length, shortest key first, and then name, so the
500    * output is stable.
501    */
502   qsort (mountpoints, count_strings (mountpoints) / 2, 2 * sizeof (char *),
503          compare_keys_len);
504
505   XMLERROR (-1, xmlTextWriterStartElement (xo, BAD_CAST "mountpoints"));
506
507   for (i = 0; mountpoints[i] != NULL; i += 2) {
508     canonicalize (mountpoints[i+1]);
509
510     XMLERROR (-1,
511               xmlTextWriterStartElement (xo, BAD_CAST "mountpoint"));
512     XMLERROR (-1,
513               xmlTextWriterWriteAttribute (xo, BAD_CAST "dev",
514                                            BAD_CAST mountpoints[i+1]));
515     XMLERROR (-1,
516               xmlTextWriterWriteString (xo, BAD_CAST mountpoints[i]));
517     XMLERROR (-1, xmlTextWriterEndElement (xo));
518   }
519
520   XMLERROR (-1, xmlTextWriterEndElement (xo));
521
522   free_strings (mountpoints);
523 }
524
525 static void
526 output_filesystems (xmlTextWriterPtr xo, char *root)
527 {
528   char **filesystems;
529   char *str;
530   size_t i;
531
532   filesystems = guestfs_inspect_get_filesystems (g, root);
533   if (filesystems == NULL)
534     exit (EXIT_FAILURE);
535
536   /* Sort by name so the output is stable. */
537   qsort (filesystems, count_strings (filesystems), sizeof (char *),
538          compare_keys);
539
540   XMLERROR (-1, xmlTextWriterStartElement (xo, BAD_CAST "filesystems"));
541
542   for (i = 0; filesystems[i] != NULL; ++i) {
543     canonicalize (filesystems[i]);
544
545     XMLERROR (-1, xmlTextWriterStartElement (xo, BAD_CAST "filesystem"));
546     XMLERROR (-1,
547               xmlTextWriterWriteAttribute (xo, BAD_CAST "dev",
548                                            BAD_CAST filesystems[i]));
549
550     DISABLE_GUESTFS_ERRORS_FOR (
551       str = guestfs_vfs_type (g, filesystems[i]);
552       if (str && str[0])
553         XMLERROR (-1,
554                   xmlTextWriterWriteElement (xo, BAD_CAST "type",
555                                              BAD_CAST str));
556       free (str);
557     );
558
559     DISABLE_GUESTFS_ERRORS_FOR (
560       str = guestfs_vfs_label (g, filesystems[i]);
561       if (str && str[0])
562         XMLERROR (-1,
563                   xmlTextWriterWriteElement (xo, BAD_CAST "label",
564                                              BAD_CAST str));
565       free (str);
566     );
567
568     DISABLE_GUESTFS_ERRORS_FOR (
569       str = guestfs_vfs_uuid (g, filesystems[i]);
570       if (str && str[0])
571         XMLERROR (-1,
572                   xmlTextWriterWriteElement (xo, BAD_CAST "uuid",
573                                              BAD_CAST str));
574       free (str);
575     );
576
577     XMLERROR (-1, xmlTextWriterEndElement (xo));
578   }
579
580   XMLERROR (-1, xmlTextWriterEndElement (xo));
581
582   free_strings (filesystems);
583 }
584
585 static void
586 output_applications (xmlTextWriterPtr xo, char *root)
587 {
588   struct guestfs_application_list *apps;
589   size_t i;
590
591   /* We need to mount everything up in order to read out the list of
592    * applications.
593    */
594   inspect_mount_root (root);
595
596   /* This returns an empty list if we simply couldn't determine the
597    * applications, so if it returns NULL then it's a real error.
598    */
599   apps = guestfs_inspect_list_applications (g, root);
600   if (apps == NULL)
601     exit (EXIT_FAILURE);
602   if (guestfs_umount_all (g) == -1)
603     exit (EXIT_FAILURE);
604
605   XMLERROR (-1, xmlTextWriterStartElement (xo, BAD_CAST "applications"));
606
607   for (i = 0; i < apps->len; ++i) {
608     XMLERROR (-1, xmlTextWriterStartElement (xo, BAD_CAST "application"));
609
610     assert (apps->val[i].app_name && apps->val[i].app_name[0]);
611     XMLERROR (-1,
612               xmlTextWriterWriteElement (xo, BAD_CAST "name",
613                                          BAD_CAST apps->val[i].app_name));
614
615     if (apps->val[i].app_display_name && apps->val[i].app_display_name[0])
616       XMLERROR (-1,
617         xmlTextWriterWriteElement (xo, BAD_CAST "display_name",
618                                    BAD_CAST apps->val[i].app_display_name));
619
620     if (apps->val[i].app_epoch != 0) {
621       char buf[32];
622
623       snprintf (buf, sizeof buf, "%d", apps->val[i].app_epoch);
624
625       XMLERROR (-1,
626         xmlTextWriterWriteElement (xo, BAD_CAST "epoch", BAD_CAST buf));
627     }
628
629     if (apps->val[i].app_version && apps->val[i].app_version[0])
630       XMLERROR (-1,
631         xmlTextWriterWriteElement (xo, BAD_CAST "version",
632                                    BAD_CAST apps->val[i].app_version));
633     if (apps->val[i].app_release && apps->val[i].app_release[0])
634       XMLERROR (-1,
635         xmlTextWriterWriteElement (xo, BAD_CAST "release",
636                                    BAD_CAST apps->val[i].app_release));
637     if (apps->val[i].app_install_path && apps->val[i].app_install_path[0])
638       XMLERROR (-1,
639         xmlTextWriterWriteElement (xo, BAD_CAST "install_path",
640                                    BAD_CAST apps->val[i].app_install_path));
641     if (apps->val[i].app_publisher && apps->val[i].app_publisher[0])
642       XMLERROR (-1,
643         xmlTextWriterWriteElement (xo, BAD_CAST "publisher",
644                                    BAD_CAST apps->val[i].app_publisher));
645     if (apps->val[i].app_url && apps->val[i].app_url[0])
646       XMLERROR (-1,
647         xmlTextWriterWriteElement (xo, BAD_CAST "url",
648                                    BAD_CAST apps->val[i].app_url));
649     if (apps->val[i].app_source_package && apps->val[i].app_source_package[0])
650       XMLERROR (-1,
651         xmlTextWriterWriteElement (xo, BAD_CAST "source_package",
652                                    BAD_CAST apps->val[i].app_source_package));
653     if (apps->val[i].app_summary && apps->val[i].app_summary[0])
654       XMLERROR (-1,
655         xmlTextWriterWriteElement (xo, BAD_CAST "summary",
656                                    BAD_CAST apps->val[i].app_summary));
657     if (apps->val[i].app_description && apps->val[i].app_description[0])
658       XMLERROR (-1,
659         xmlTextWriterWriteElement (xo, BAD_CAST "description",
660                                    BAD_CAST apps->val[i].app_description));
661
662     XMLERROR (-1, xmlTextWriterEndElement (xo));
663   }
664
665   XMLERROR (-1, xmlTextWriterEndElement (xo));
666
667   guestfs_free_application_list (apps);
668 }
669
670 /* "/dev/vda1" -> "/dev/sda1"
671  * See BLOCK DEVICE NAMING in guestfs(3).
672  */
673 static void
674 canonicalize (char *dev)
675 {
676   if (STRPREFIX (dev, "/dev/") &&
677       (dev[5] == 'h' || dev[5] == 'v') &&
678       dev[6] == 'd' &&
679       c_isalpha (dev[7]) &&
680       (c_isdigit (dev[8]) || dev[8] == '\0'))
681     dev[5] = 's';
682 }
683
684 static void
685 free_strings (char **argv)
686 {
687   int argc;
688
689   for (argc = 0; argv[argc] != NULL; ++argc)
690     free (argv[argc]);
691   free (argv);
692 }
693
694 static int
695 count_strings (char *const *argv)
696 {
697   int c;
698
699   for (c = 0; argv[c]; ++c)
700     ;
701   return c;
702 }