1/* Handle so called `shell archives'.
2   Copyright (C) 1994, 1995 Free Software Foundation, 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, or (at your option)
7   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 Foundation,
16   Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17*/
18
19/* Unpackage one or more shell archive files.  The `unshar' program is a
20   filter which removes the front part of a file and passes the rest to
21   the `sh' command.  It understands phrases like "cut here", and also
22   knows about shell comment characters and the Unix commands `echo',
23   `cat', and `sed'.  */
24
25#include "system.h"
26#include "getopt.h"
27
28/* Buffer size for holding a file name.  FIXME: No fix limit in GNU... */
29#define NAME_BUFFER_SIZE 1024
30
31/* Buffer size for shell process input.  */
32#define SHELL_BUFFER_SIZE 8196
33
34#define EOL '\n'
35
36/* The name this program was run with. */
37const char *program_name;
38
39/* If non-zero, display usage information and exit.  */
40static int show_help = 0;
41
42/* If non-zero, print the version on standard output and exit.  */
43static int show_version = 0;
44
45static int pass_c_flag = 0;
46static int continue_reading = 0;
47static const char *exit_string = "exit 0";
48static size_t exit_string_length;
49static char *current_directory;
50
51/*-------------------------------------------------------------------.
52| Match the leftmost part of a string.  Returns 1 if initial	     |
53| characters of DATA match PATTERN exactly; else 0.  This was	     |
54| formerly a function.  But because we always have a constant string |
55| as the seconf argument and the length of the second argument is a  |
56| lot of shorter than the buffer the first argument is pointing at,  |
57| we simply use `memcmp'.  And one more point: even if the `memcmp'  |
58| function does not work correct for 8 bit characters it does not    |
59| matter here.  We are only interested in equal or not equal	     |
60| information.							     |
61`-------------------------------------------------------------------*/
62
63#define starting_with(data, pattern)					    \
64  (memcmp (data, pattern, sizeof (pattern) - 1) == 0)
65
66
67/*-------------------------------------------------------------------------.
68| For a DATA string and a PATTERN containing one or more embedded	   |
69| asterisks (matching any number of characters), return non-zero if the	   |
70| match succeeds, and set RESULT_ARRAY[I] to the characters matched by the |
71| I'th *.								   |
72`-------------------------------------------------------------------------*/
73
74static int
75matched_by (data, pattern, result_array)
76     const char *data;
77     const char *pattern;
78     char **result_array;
79{
80  const char *pattern_cursor = NULL;
81  const char *data_cursor = NULL;
82  char *result_cursor = NULL;
83  int number_of_results = 0;
84
85  while (1)
86    if (*pattern == '*')
87      {
88	pattern_cursor = ++pattern;
89	data_cursor = data;
90	result_cursor = result_array[number_of_results++];
91	*result_cursor = '\0';
92      }
93    else if (*data == *pattern)
94      {
95	if (*pattern == '\0')
96	  /* The pattern matches.  */
97	  return 1;
98
99	pattern++;
100	data++;
101      }
102    else
103      {
104	if (*data == '\0')
105	  /* The pattern fails: no more data.  */
106	  return 0;
107
108	if (pattern_cursor == NULL)
109	  /* The pattern fails: no star to adjust.  */
110	  return 0;
111
112	/* Restart pattern after star.  */
113
114	pattern = pattern_cursor;
115	*result_cursor++ = *data_cursor;
116	*result_cursor = '\0';
117
118	/* Rescan after copied char.  */
119
120	data = ++data_cursor;
121      }
122}
123
124/*------------------------------------------------------------------------.
125| Associated with a given file NAME, position FILE at the start of the	  |
126| shell command portion of a shell archive file.  Scan file from position |
127| START.								  |
128`------------------------------------------------------------------------*/
129
130static int
131find_archive (name, file, start)
132     const char *name;
133     FILE *file;
134     off_t start;
135{
136  char buffer[BUFSIZ];
137  off_t position;
138
139  /* Results from star matcher.  */
140
141  static char res1[BUFSIZ], res2[BUFSIZ], res3[BUFSIZ], res4[BUFSIZ];
142  static char *result[] = {res1, res2, res3, res4};
143
144  fseek (file, start, 0);
145
146  while (1)
147    {
148
149      /* Record position of the start of this line.  */
150
151      position = ftell (file);
152
153      /* Read next line, fail if no more and no previous process.  */
154
155      if (!fgets (buffer, BUFSIZ, file))
156	{
157	  if (!start)
158	    error (0, 0, _("Found no shell commands in %s"), name);
159	  return 0;
160	}
161
162      /* Bail out if we see C preprocessor commands or C comments.  */
163
164      if (starting_with (buffer, "#include")
165	  || starting_with (buffer, "# include")
166	  || starting_with (buffer, "#define")
167	  || starting_with (buffer, "# define")
168	  || starting_with (buffer, "#ifdef")
169	  || starting_with (buffer, "# ifdef")
170	  || starting_with (buffer, "#ifndef")
171	  || starting_with (buffer, "# ifndef")
172	  || starting_with (buffer, "/*"))
173	{
174	  error (0, 0, _("%s looks like raw C code, not a shell archive"),
175		 name);
176	  return 0;
177	}
178
179      /* Does this line start with a shell command or comment.  */
180
181      if (starting_with (buffer, "#")
182	  || starting_with (buffer, ":")
183	  || starting_with (buffer, "echo ")
184	  || starting_with (buffer, "sed ")
185	  || starting_with (buffer, "cat ")
186	  || starting_with (buffer, "if "))
187	{
188	  fseek (file, position, 0);
189	  return 1;
190	}
191
192      /* Does this line say "Cut here".  */
193
194      if (matched_by (buffer, "*CUT*HERE*", result) ||
195	  matched_by (buffer, "*cut*here*", result) ||
196	  matched_by (buffer, "*TEAR*HERE*", result) ||
197	  matched_by (buffer, "*tear*here*", result) ||
198	  matched_by (buffer, "*CUT*CUT*", result) ||
199	  matched_by (buffer, "*cut*cut*", result))
200	{
201
202	  /* Read next line after "cut here", skipping blank lines.  */
203
204	  while (1)
205	    {
206	      position = ftell (file);
207
208	      if (!fgets (buffer, BUFSIZ, file))
209		{
210		  error (0, 0, _("Found no shell commands after `cut' in %s"),
211			 name);
212		  return 0;
213		}
214
215	      if (*buffer != '\n')
216		break;
217	    }
218
219	  /* Win if line starts with a comment character of lower case
220	     letter.  */
221
222	  if (*buffer == '#' || *buffer == ':'
223	      || (('a' <= *buffer) && ('z' >= *buffer)))
224	    {
225	      fseek (file, position, 0);
226	      return 1;
227	    }
228
229	  /* Cut here message lied to us.  */
230
231	  error (0, 0, _("%s is probably not a shell archive"), name);
232	  error (0, 0, _("The `cut' line was followed by: %s"), buffer);
233	  return 0;
234	}
235    }
236}
237
238/*-----------------------------------------------------------------.
239| Unarchive a shar file provided on file NAME.  The file itself is |
240| provided on the already opened FILE.				   |
241`-----------------------------------------------------------------*/
242
243static void
244unarchive_shar_file (name, file)
245     const char *name;
246     FILE *file;
247{
248  char buffer[SHELL_BUFFER_SIZE];
249  FILE *shell_process;
250  off_t current_position = 0;
251  char *more_to_read;
252
253  while (find_archive (name, file, current_position))
254    {
255      printf ("%s:\n", name);
256      shell_process = popen (pass_c_flag ? "sh -s - -c" : "sh", "w");
257      if (!shell_process)
258	error (EXIT_FAILURE, errno, _("Starting `sh' process"));
259
260      if (!continue_reading)
261	{
262	  size_t len;
263
264	  while ((len = fread (buffer, 1, SHELL_BUFFER_SIZE, file)) != 0)
265	    fwrite (buffer, 1, len, shell_process);
266#if 0
267	  /* Don't know whether a test is appropriate here.  */
268	  if (ferror (shell_process) != 0)
269	    fwrite (buffer, length, 1, shell_process);
270#endif
271	  pclose (shell_process);
272	  break;
273	}
274      else
275	{
276	  while (more_to_read = fgets (buffer, SHELL_BUFFER_SIZE, file),
277		 more_to_read != NULL)
278	    {
279	      fputs (buffer, shell_process);
280	      if (!strncmp (exit_string, buffer, exit_string_length))
281		break;
282	    }
283	  pclose (shell_process);
284
285	  if (more_to_read)
286	    current_position = ftell (file);
287	  else
288	    break;
289	}
290    }
291}
292
293/*-----------------------------.
294| Explain how to use program.  |
295`-----------------------------*/
296
297static void
298usage (status)
299     int status;
300{
301  if (status != EXIT_SUCCESS)
302    fprintf (stderr, _("Try `%s --help' for more information.\n"),
303	     program_name);
304  else
305    {
306      printf (_("Usage: %s [OPTION]... [FILE]...\n"), program_name);
307      fputs (_("\
308Mandatory arguments to long options are mandatory for short options too.\n\
309\n\
310  -d, --directory=DIRECTORY   change to DIRECTORY before unpacking\n\
311  -c, --overwrite             pass -c to shar script for overwriting files\n\
312  -e, --exit-0                same as `--split-at=\"exit 0\"'\n\
313  -E, --split-at=STRING       split concatenated shars after STRING\n\
314  -f, --force                 same as `-c'\n\
315      --help                  display this help and exit\n\
316      --version               output version information and exit\n\
317\n\
318If no FILE, standard input is read.\n"),
319	     stdout);
320    }
321  exit (status);
322}
323
324/*--------------------------------------.
325| Decode options and launch execution.  |
326`--------------------------------------*/
327
328static const struct option long_options[] =
329{
330  {"directory", required_argument, NULL, 'd'},
331  {"exit-0", no_argument, NULL, 'e'},
332  {"force", no_argument, NULL, 'f'},
333  {"overwrite", no_argument, NULL, 'c'},
334  {"split-at", required_argument, NULL, 'E'},
335
336  {"help", no_argument, &show_help, 1},
337  {"version", no_argument, &show_version, 1},
338
339  { NULL, 0, NULL, 0 },
340};
341
342int
343main (argc, argv)
344     int argc;
345     char *const *argv;
346{
347  size_t size_read;
348  FILE *file;
349  char name_buffer[NAME_BUFFER_SIZE];
350  char copy_buffer[NAME_BUFFER_SIZE];
351  int optchar;
352
353  program_name = argv[0];
354  setlocale (LC_ALL, "");
355
356  /* Set the text message domain.  */
357  bindtextdomain (PACKAGE, LOCALEDIR);
358  textdomain (PACKAGE);
359
360#ifdef __MSDOS__
361  setbuf (stdout, NULL);
362  setbuf (stderr, NULL);
363#endif
364
365  if (current_directory = xgetcwd (), !current_directory)
366    error (EXIT_FAILURE, errno, _("Cannot get current directory name"));
367
368  /* Process options.  */
369
370  while (optchar = getopt_long (argc, argv, "E:cd:ef", long_options, NULL),
371	 optchar != EOF)
372    switch (optchar)
373      {
374      case '\0':
375	break;
376
377      case 'c':
378      case 'f':
379	pass_c_flag = 1;
380	break;
381
382      case 'd':
383	if (chdir (optarg) == -1)
384	  error (2, 0, _("Cannot chdir to `%s'"), optarg);
385	break;
386
387      case 'E':
388	exit_string = optarg;
389	/* Fall through.  */
390
391      case 'e':
392	continue_reading = 1;
393	exit_string_length = strlen (exit_string);
394	break;
395
396      default:
397	usage (EXIT_FAILURE);
398      }
399
400  if (show_version)
401    {
402      printf ("%s - GNU %s %s\n", program_name, PACKAGE, VERSION);
403      exit (EXIT_SUCCESS);
404    }
405
406  if (show_help)
407    usage (EXIT_SUCCESS);
408
409  if (optind < argc)
410    for (; optind < argc; optind++)
411      {
412	if (argv[optind][0] == '/')
413	  stpcpy (name_buffer, argv[optind]);
414	else
415	  {
416	    char *cp = stpcpy (name_buffer, current_directory);
417	    *cp++ = '/';
418	    stpcpy (cp, argv[optind]);
419	  }
420	if (file = fopen (name_buffer, "r"), !file)
421	  error (EXIT_FAILURE, errno, name_buffer);
422	unarchive_shar_file (name_buffer, file);
423	fclose (file);
424      }
425  else
426    {
427      sprintf (name_buffer, "/tmp/unsh.%05d", (int) getpid ());
428      unlink (name_buffer);
429
430      if (file = fopen (name_buffer, "w+"), !file)
431	error (EXIT_FAILURE, errno, name_buffer);
432#ifndef __MSDOS__
433      unlink (name_buffer);	/* will be deleted on fclose */
434#endif
435
436      while (size_read = fread (copy_buffer, 1, sizeof (copy_buffer), stdin),
437	     size_read != 0)
438	fwrite (copy_buffer, size_read, 1, file);
439      rewind (file);
440
441      unarchive_shar_file (_("standard input"), file);
442
443      fclose (file);
444#ifdef __MSDOS__
445      unlink (name_buffer);
446#endif
447    }
448
449  exit (EXIT_SUCCESS);
450}
451