~akopytov/percona-xtrabackup/bug1166888-2.1

« back to all changes in this revision

Viewing changes to src/libarchive/libarchive/archive_write_set_format_ar.c

  • Committer: Alexey Kopytov
  • Date: 2012-02-10 20:05:56 UTC
  • mto: (391.1.5 staging)
  • mto: This revision was merged to the branch mainline in revision 390.
  • Revision ID: akopytov@gmail.com-20120210200556-6kx41z8wwrqfucro
Rebase of the parallel compression patch on new trunk + post-review
fixes.

Implementation of parallel compression and streaming for XtraBackup.

This revision implements the following changes:

* InnoDB files are now streamed by the xtrabackup binary rather than
innobackupex. As a result, integrity is now verified by xtrabackup and
thus tar4ibd is no longer needed, so it was removed.

* xtrabackup binary now accepts the new '--stream' option which has
exactly the same semantics as the '--stream' option in
innobackupex: it tells xtrabackup to stream all files to the standard
output in the specified format rather than storing them locally.

* The xtrabackup binary can now do parallel compression using the
quicklz library. Two new options were added to xtrabackup to support
this feature:

- '--compress' tells xtrabackup to compress all output data, including
the transaction log file and meta data files, using the specified
compression algorithm. The only currently supported algorithm is
'quicklz'. The resulting files have the qpress archive format,
i.e. every *.qp file produced by xtrabackup is essentially a one-file
qpress archive and can be extracted and uncompressed by the qpress
file archiver (http://www.quicklz.com/).

- '--compress-threads' specifies the number of worker threads used by
xtrabackup for parallel data compression. This option defaults to 1.

Parallel compression ('--compress-threads') can be used together with
parallel file copying ('--parallel'). For example, '--parallel=4
--compress --compress-threads=2' will create 4 IO threads that will
read the data and pipe it to 2 compression threads.

* To support simultaneous compression and streaming, a new custom
streaming format called 'xbstream' was introduced to XtraBackup in
addition to the 'tar' format. That was required to overcome some
limitations of traditional archive formats such as 'tar', 'cpio' and
others that do not allow streaming dynamically generated files, for
example dynamically compressed files.  Other advantages of xbstream over
traditional streaming/archive formats include ability to stream multiple
files concurrently (so it is possible to use streaming in the xbstream
format together with the --parallel option) and more compact data
storage.

* To allow streaming and extracting files to/from the xbstream format
produced by xtrabackup, a new utility aptly called 'xbstream' was
added to the XtraBackup distribution. This utility has a tar-like
interface:

- with the '-x' option it extracts files from the stream read from its
standard input to the current directory unless specified otherwise
with the '-C' option.

- with the '-c' option it streams files specified on the command line
to its standard output.

The utility also tries to minimize its impact on the OS page cache by
using the appropriate posix_fadvise() calls when available.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
/*-
 
2
 * Copyright (c) 2007 Kai Wang
 
3
 * Copyright (c) 2007 Tim Kientzle
 
4
 * All rights reserved.
 
5
 *
 
6
 * Redistribution and use in source and binary forms, with or without
 
7
 * modification, are permitted provided that the following conditions
 
8
 * are met:
 
9
 * 1. Redistributions of source code must retain the above copyright
 
10
 *    notice, this list of conditions and the following disclaimer
 
11
 *    in this position and unchanged.
 
12
 * 2. Redistributions in binary form must reproduce the above copyright
 
13
 *    notice, this list of conditions and the following disclaimer in the
 
14
 *    documentation and/or other materials provided with the distribution.
 
15
 *
 
16
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR
 
17
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 
18
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 
19
 * IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT,
 
20
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 
21
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 
25
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
26
 */
 
27
 
 
28
#include "archive_platform.h"
 
29
__FBSDID("$FreeBSD: head/lib/libarchive/archive_write_set_format_ar.c 201108 2009-12-28 03:28:21Z kientzle $");
 
30
 
 
31
#ifdef HAVE_ERRNO_H
 
32
#include <errno.h>
 
33
#endif
 
34
#ifdef HAVE_STDLIB_H
 
35
#include <stdlib.h>
 
36
#endif
 
37
#ifdef HAVE_STRING_H
 
38
#include <string.h>
 
39
#endif
 
40
 
 
41
#include "archive.h"
 
42
#include "archive_entry.h"
 
43
#include "archive_private.h"
 
44
#include "archive_write_private.h"
 
45
 
 
46
struct ar_w {
 
47
        uint64_t         entry_bytes_remaining;
 
48
        uint64_t         entry_padding;
 
49
        int              is_strtab;
 
50
        int              has_strtab;
 
51
        char            *strtab;
 
52
};
 
53
 
 
54
/*
 
55
 * Define structure of the "ar" header.
 
56
 */
 
57
#define AR_name_offset 0
 
58
#define AR_name_size 16
 
59
#define AR_date_offset 16
 
60
#define AR_date_size 12
 
61
#define AR_uid_offset 28
 
62
#define AR_uid_size 6
 
63
#define AR_gid_offset 34
 
64
#define AR_gid_size 6
 
65
#define AR_mode_offset 40
 
66
#define AR_mode_size 8
 
67
#define AR_size_offset 48
 
68
#define AR_size_size 10
 
69
#define AR_fmag_offset 58
 
70
#define AR_fmag_size 2
 
71
 
 
72
static int               archive_write_set_format_ar(struct archive_write *);
 
73
static int               archive_write_ar_header(struct archive_write *,
 
74
                             struct archive_entry *);
 
75
static ssize_t           archive_write_ar_data(struct archive_write *,
 
76
                             const void *buff, size_t s);
 
77
static int               archive_write_ar_destroy(struct archive_write *);
 
78
static int               archive_write_ar_finish(struct archive_write *);
 
79
static int               archive_write_ar_finish_entry(struct archive_write *);
 
80
static const char       *ar_basename(const char *path);
 
81
static int               format_octal(int64_t v, char *p, int s);
 
82
static int               format_decimal(int64_t v, char *p, int s);
 
83
 
 
84
int
 
85
archive_write_set_format_ar_bsd(struct archive *_a)
 
86
{
 
87
        struct archive_write *a = (struct archive_write *)_a;
 
88
        int r = archive_write_set_format_ar(a);
 
89
        if (r == ARCHIVE_OK) {
 
90
                a->archive.archive_format = ARCHIVE_FORMAT_AR_BSD;
 
91
                a->archive.archive_format_name = "ar (BSD)";
 
92
        }
 
93
        return (r);
 
94
}
 
95
 
 
96
int
 
97
archive_write_set_format_ar_svr4(struct archive *_a)
 
98
{
 
99
        struct archive_write *a = (struct archive_write *)_a;
 
100
        int r = archive_write_set_format_ar(a);
 
101
        if (r == ARCHIVE_OK) {
 
102
                a->archive.archive_format = ARCHIVE_FORMAT_AR_GNU;
 
103
                a->archive.archive_format_name = "ar (GNU/SVR4)";
 
104
        }
 
105
        return (r);
 
106
}
 
107
 
 
108
/*
 
109
 * Generic initialization.
 
110
 */
 
111
static int
 
112
archive_write_set_format_ar(struct archive_write *a)
 
113
{
 
114
        struct ar_w *ar;
 
115
 
 
116
        /* If someone else was already registered, unregister them. */
 
117
        if (a->format_destroy != NULL)
 
118
                (a->format_destroy)(a);
 
119
 
 
120
        ar = (struct ar_w *)malloc(sizeof(*ar));
 
121
        if (ar == NULL) {
 
122
                archive_set_error(&a->archive, ENOMEM, "Can't allocate ar data");
 
123
                return (ARCHIVE_FATAL);
 
124
        }
 
125
        memset(ar, 0, sizeof(*ar));
 
126
        a->format_data = ar;
 
127
 
 
128
        a->format_name = "ar";
 
129
        a->format_write_header = archive_write_ar_header;
 
130
        a->format_write_data = archive_write_ar_data;
 
131
        a->format_finish = archive_write_ar_finish;
 
132
        a->format_destroy = archive_write_ar_destroy;
 
133
        a->format_finish_entry = archive_write_ar_finish_entry;
 
134
        return (ARCHIVE_OK);
 
135
}
 
136
 
 
137
static int
 
138
archive_write_ar_header(struct archive_write *a, struct archive_entry *entry)
 
139
{
 
140
        int ret, append_fn;
 
141
        char buff[60];
 
142
        char *ss, *se;
 
143
        struct ar_w *ar;
 
144
        const char *pathname;
 
145
        const char *filename;
 
146
        int64_t size;
 
147
 
 
148
        append_fn = 0;
 
149
        ar = (struct ar_w *)a->format_data;
 
150
        ar->is_strtab = 0;
 
151
        filename = NULL;
 
152
        size = archive_entry_size(entry);
 
153
 
 
154
 
 
155
        /*
 
156
         * Reject files with empty name.
 
157
         */
 
158
        pathname = archive_entry_pathname(entry);
 
159
        if (*pathname == '\0') {
 
160
                archive_set_error(&a->archive, EINVAL,
 
161
                    "Invalid filename");
 
162
                return (ARCHIVE_WARN);
 
163
        }
 
164
 
 
165
        /*
 
166
         * If we are now at the beginning of the archive,
 
167
         * we need first write the ar global header.
 
168
         */
 
169
        if (a->archive.file_position == 0)
 
170
                (a->compressor.write)(a, "!<arch>\n", 8);
 
171
 
 
172
        memset(buff, ' ', 60);
 
173
        strncpy(&buff[AR_fmag_offset], "`\n", 2);
 
174
 
 
175
        if (strcmp(pathname, "/") == 0 ) {
 
176
                /* Entry is archive symbol table in GNU format */
 
177
                buff[AR_name_offset] = '/';
 
178
                goto stat;
 
179
        }
 
180
        if (strcmp(pathname, "__.SYMDEF") == 0) {
 
181
                /* Entry is archive symbol table in BSD format */
 
182
                strncpy(buff + AR_name_offset, "__.SYMDEF", 9);
 
183
                goto stat;
 
184
        }
 
185
        if (strcmp(pathname, "//") == 0) {
 
186
                /*
 
187
                 * Entry is archive filename table, inform that we should
 
188
                 * collect strtab in next _data call.
 
189
                 */
 
190
                ar->is_strtab = 1;
 
191
                buff[AR_name_offset] = buff[AR_name_offset + 1] = '/';
 
192
                /*
 
193
                 * For archive string table, only ar_size filed should
 
194
                 * be set.
 
195
                 */
 
196
                goto size;
 
197
        }
 
198
 
 
199
        /*
 
200
         * Otherwise, entry is a normal archive member.
 
201
         * Strip leading paths from filenames, if any.
 
202
         */
 
203
        if ((filename = ar_basename(pathname)) == NULL) {
 
204
                /* Reject filenames with trailing "/" */
 
205
                archive_set_error(&a->archive, EINVAL,
 
206
                    "Invalid filename");
 
207
                return (ARCHIVE_WARN);
 
208
        }
 
209
 
 
210
        if (a->archive.archive_format == ARCHIVE_FORMAT_AR_GNU) {
 
211
                /*
 
212
                 * SVR4/GNU variant use a "/" to mark then end of the filename,
 
213
                 * make it possible to have embedded spaces in the filename.
 
214
                 * So, the longest filename here (without extension) is
 
215
                 * actually 15 bytes.
 
216
                 */
 
217
                if (strlen(filename) <= 15) {
 
218
                        strncpy(&buff[AR_name_offset], 
 
219
                            filename, strlen(filename));
 
220
                        buff[AR_name_offset + strlen(filename)] = '/';
 
221
                } else {
 
222
                        /*
 
223
                         * For filename longer than 15 bytes, GNU variant
 
224
                         * makes use of a string table and instead stores the
 
225
                         * offset of the real filename to in the ar_name field.
 
226
                         * The string table should have been written before.
 
227
                         */
 
228
                        if (ar->has_strtab <= 0) {
 
229
                                archive_set_error(&a->archive, EINVAL,
 
230
                                    "Can't find string table");
 
231
                                return (ARCHIVE_WARN);
 
232
                        }
 
233
 
 
234
                        se = (char *)malloc(strlen(filename) + 3);
 
235
                        if (se == NULL) {
 
236
                                archive_set_error(&a->archive, ENOMEM,
 
237
                                    "Can't allocate filename buffer");
 
238
                                return (ARCHIVE_FATAL);
 
239
                        }
 
240
 
 
241
                        strncpy(se, filename, strlen(filename));
 
242
                        strcpy(se + strlen(filename), "/\n");
 
243
 
 
244
                        ss = strstr(ar->strtab, se);
 
245
                        free(se);
 
246
 
 
247
                        if (ss == NULL) {
 
248
                                archive_set_error(&a->archive, EINVAL,
 
249
                                    "Invalid string table");
 
250
                                return (ARCHIVE_WARN);
 
251
                        }
 
252
 
 
253
                        /*
 
254
                         * GNU variant puts "/" followed by digits into
 
255
                         * ar_name field. These digits indicates the real
 
256
                         * filename string's offset to the string table.
 
257
                         */
 
258
                        buff[AR_name_offset] = '/';
 
259
                        if (format_decimal(ss - ar->strtab,
 
260
                            buff + AR_name_offset + 1,
 
261
                            AR_name_size - 1)) {
 
262
                                archive_set_error(&a->archive, ERANGE,
 
263
                                    "string table offset too large");
 
264
                                return (ARCHIVE_WARN);
 
265
                        }
 
266
                }
 
267
        } else if (a->archive.archive_format == ARCHIVE_FORMAT_AR_BSD) {
 
268
                /*
 
269
                 * BSD variant: for any file name which is more than
 
270
                 * 16 chars or contains one or more embedded space(s), the
 
271
                 * string "#1/" followed by the ASCII length of the name is
 
272
                 * put into the ar_name field. The file size (stored in the
 
273
                 * ar_size field) is incremented by the length of the name.
 
274
                 * The name is then written immediately following the
 
275
                 * archive header.
 
276
                 */
 
277
                if (strlen(filename) <= 16 && strchr(filename, ' ') == NULL) {
 
278
                        strncpy(&buff[AR_name_offset], filename, strlen(filename));
 
279
                        buff[AR_name_offset + strlen(filename)] = ' ';
 
280
                }
 
281
                else {
 
282
                        strncpy(buff + AR_name_offset, "#1/", 3);
 
283
                        if (format_decimal(strlen(filename),
 
284
                            buff + AR_name_offset + 3,
 
285
                            AR_name_size - 3)) {
 
286
                                archive_set_error(&a->archive, ERANGE,
 
287
                                    "File name too long");
 
288
                                return (ARCHIVE_WARN);
 
289
                        }
 
290
                        append_fn = 1;
 
291
                        size += strlen(filename);
 
292
                }
 
293
        }
 
294
 
 
295
stat:
 
296
        if (format_decimal(archive_entry_mtime(entry), buff + AR_date_offset, AR_date_size)) {
 
297
                archive_set_error(&a->archive, ERANGE,
 
298
                    "File modification time too large");
 
299
                return (ARCHIVE_WARN);
 
300
        }
 
301
        if (format_decimal(archive_entry_uid(entry), buff + AR_uid_offset, AR_uid_size)) {
 
302
                archive_set_error(&a->archive, ERANGE,
 
303
                    "Numeric user ID too large");
 
304
                return (ARCHIVE_WARN);
 
305
        }
 
306
        if (format_decimal(archive_entry_gid(entry), buff + AR_gid_offset, AR_gid_size)) {
 
307
                archive_set_error(&a->archive, ERANGE,
 
308
                    "Numeric group ID too large");
 
309
                return (ARCHIVE_WARN);
 
310
        }
 
311
        if (format_octal(archive_entry_mode(entry), buff + AR_mode_offset, AR_mode_size)) {
 
312
                archive_set_error(&a->archive, ERANGE,
 
313
                    "Numeric mode too large");
 
314
                return (ARCHIVE_WARN);
 
315
        }
 
316
        /*
 
317
         * Sanity Check: A non-pseudo archive member should always be
 
318
         * a regular file.
 
319
         */
 
320
        if (filename != NULL && archive_entry_filetype(entry) != AE_IFREG) {
 
321
                archive_set_error(&a->archive, EINVAL,
 
322
                    "Regular file required for non-pseudo member");
 
323
                return (ARCHIVE_WARN);
 
324
        }
 
325
 
 
326
size:
 
327
        if (format_decimal(size, buff + AR_size_offset, AR_size_size)) {
 
328
                archive_set_error(&a->archive, ERANGE,
 
329
                    "File size out of range");
 
330
                return (ARCHIVE_WARN);
 
331
        }
 
332
 
 
333
        ret = (a->compressor.write)(a, buff, 60);
 
334
        if (ret != ARCHIVE_OK)
 
335
                return (ret);
 
336
 
 
337
        ar->entry_bytes_remaining = size;
 
338
        ar->entry_padding = ar->entry_bytes_remaining % 2;
 
339
 
 
340
        if (append_fn > 0) {
 
341
                ret = (a->compressor.write)(a, filename, strlen(filename));
 
342
                if (ret != ARCHIVE_OK)
 
343
                        return (ret);
 
344
                ar->entry_bytes_remaining -= strlen(filename);
 
345
        }
 
346
 
 
347
        return (ARCHIVE_OK);
 
348
}
 
349
 
 
350
static ssize_t
 
351
archive_write_ar_data(struct archive_write *a, const void *buff, size_t s)
 
352
{
 
353
        struct ar_w *ar;
 
354
        int ret;
 
355
 
 
356
        ar = (struct ar_w *)a->format_data;
 
357
        if (s > ar->entry_bytes_remaining)
 
358
                s = ar->entry_bytes_remaining;
 
359
 
 
360
        if (ar->is_strtab > 0) {
 
361
                if (ar->has_strtab > 0) {
 
362
                        archive_set_error(&a->archive, EINVAL,
 
363
                            "More than one string tables exist");
 
364
                        return (ARCHIVE_WARN);
 
365
                }
 
366
 
 
367
                ar->strtab = (char *)malloc(s);
 
368
                if (ar->strtab == NULL) {
 
369
                        archive_set_error(&a->archive, ENOMEM,
 
370
                            "Can't allocate strtab buffer");
 
371
                        return (ARCHIVE_FATAL);
 
372
                }
 
373
                strncpy(ar->strtab, buff, s);
 
374
                ar->has_strtab = 1;
 
375
        }
 
376
 
 
377
        ret = (a->compressor.write)(a, buff, s);
 
378
        if (ret != ARCHIVE_OK)
 
379
                return (ret);
 
380
 
 
381
        ar->entry_bytes_remaining -= s;
 
382
        return (s);
 
383
}
 
384
 
 
385
static int
 
386
archive_write_ar_destroy(struct archive_write *a)
 
387
{
 
388
        struct ar_w *ar;
 
389
 
 
390
        ar = (struct ar_w *)a->format_data;
 
391
 
 
392
        if (ar == NULL)
 
393
                return (ARCHIVE_OK);
 
394
 
 
395
        if (ar->has_strtab > 0) {
 
396
                free(ar->strtab);
 
397
                ar->strtab = NULL;
 
398
        }
 
399
 
 
400
        free(ar);
 
401
        a->format_data = NULL;
 
402
        return (ARCHIVE_OK);
 
403
}
 
404
 
 
405
static int
 
406
archive_write_ar_finish(struct archive_write *a)
 
407
{
 
408
        int ret;
 
409
 
 
410
        /*
 
411
         * If we haven't written anything yet, we need to write
 
412
         * the ar global header now to make it a valid ar archive.
 
413
         */
 
414
        if (a->archive.file_position == 0) {
 
415
                ret = (a->compressor.write)(a, "!<arch>\n", 8);
 
416
                return (ret);
 
417
        }
 
418
 
 
419
        return (ARCHIVE_OK);
 
420
}
 
421
 
 
422
static int
 
423
archive_write_ar_finish_entry(struct archive_write *a)
 
424
{
 
425
        struct ar_w *ar;
 
426
        int ret;
 
427
 
 
428
        ar = (struct ar_w *)a->format_data;
 
429
 
 
430
        if (ar->entry_bytes_remaining != 0) {
 
431
                archive_set_error(&a->archive, ARCHIVE_ERRNO_MISC,
 
432
                    "Entry remaining bytes larger than 0");
 
433
                return (ARCHIVE_WARN);
 
434
        }
 
435
 
 
436
        if (ar->entry_padding == 0) {
 
437
                return (ARCHIVE_OK);
 
438
        }
 
439
 
 
440
        if (ar->entry_padding != 1) {
 
441
                archive_set_error(&a->archive, ARCHIVE_ERRNO_MISC,
 
442
                    "Padding wrong size: %d should be 1 or 0",
 
443
                    ar->entry_padding);
 
444
                return (ARCHIVE_WARN);
 
445
        }
 
446
 
 
447
        ret = (a->compressor.write)(a, "\n", 1);
 
448
        return (ret);
 
449
}
 
450
 
 
451
/*
 
452
 * Format a number into the specified field using base-8.
 
453
 * NB: This version is slightly different from the one in
 
454
 * _ustar.c
 
455
 */
 
456
static int
 
457
format_octal(int64_t v, char *p, int s)
 
458
{
 
459
        int len;
 
460
        char *h;
 
461
 
 
462
        len = s;
 
463
        h = p;
 
464
 
 
465
        /* Octal values can't be negative, so use 0. */
 
466
        if (v < 0) {
 
467
                while (len-- > 0)
 
468
                        *p++ = '0';
 
469
                return (-1);
 
470
        }
 
471
 
 
472
        p += s;         /* Start at the end and work backwards. */
 
473
        do {
 
474
                *--p = (char)('0' + (v & 7));
 
475
                v >>= 3;
 
476
        } while (--s > 0 && v > 0);
 
477
 
 
478
        if (v == 0) {
 
479
                memmove(h, p, len - s);
 
480
                p = h + len - s;
 
481
                while (s-- > 0)
 
482
                        *p++ = ' ';
 
483
                return (0);
 
484
        }
 
485
        /* If it overflowed, fill field with max value. */
 
486
        while (len-- > 0)
 
487
                *p++ = '7';
 
488
 
 
489
        return (-1);
 
490
}
 
491
 
 
492
/*
 
493
 * Format a number into the specified field using base-10.
 
494
 */
 
495
static int
 
496
format_decimal(int64_t v, char *p, int s)
 
497
{
 
498
        int len;
 
499
        char *h;
 
500
 
 
501
        len = s;
 
502
        h = p;
 
503
 
 
504
        /* Negative values in ar header are meaningless , so use 0. */
 
505
        if (v < 0) {
 
506
                while (len-- > 0)
 
507
                        *p++ = '0';
 
508
                return (-1);
 
509
        }
 
510
 
 
511
        p += s;
 
512
        do {
 
513
                *--p = (char)('0' + (v % 10));
 
514
                v /= 10;
 
515
        } while (--s > 0 && v > 0);
 
516
 
 
517
        if (v == 0) {
 
518
                memmove(h, p, len - s);
 
519
                p = h + len - s;
 
520
                while (s-- > 0)
 
521
                        *p++ = ' ';
 
522
                return (0);
 
523
        }
 
524
        /* If it overflowed, fill field with max value. */
 
525
        while (len-- > 0)
 
526
                *p++ = '9';
 
527
 
 
528
        return (-1);
 
529
}
 
530
 
 
531
static const char *
 
532
ar_basename(const char *path)
 
533
{
 
534
        const char *endp, *startp;
 
535
 
 
536
        endp = path + strlen(path) - 1;
 
537
        /*
 
538
         * For filename with trailing slash(es), we return
 
539
         * NULL indicating an error.
 
540
         */
 
541
        if (*endp == '/')
 
542
                return (NULL);
 
543
 
 
544
        /* Find the start of the base */
 
545
        startp = endp;
 
546
        while (startp > path && *(startp - 1) != '/')
 
547
                startp--;
 
548
        
 
549
        return (startp);
 
550
}