/*============================================================================== Copyright (C) 2007 Martin Furter This file is part of svntar svntar is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2, or (at your option) any later version. svntar is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with svntar; see the file COPYING. If not, write to the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA. ==============================================================================*/ #include "svn_tar.h" #include "svn_time.h" #include "apr_errno.h" #include "apr_file_info.h" #include "apr_strings.h" #include /* property defines used by FSVS */ #define SVN_PROP_TEXT_TIME SVN_PROP_PREFIX "text-time" #define SVN_PROP_OWNER SVN_PROP_PREFIX "owner" #define SVN_PROP_GROUP SVN_PROP_PREFIX "group" #define SVN_PROP_UNIX_MODE SVN_PROP_PREFIX "unix-mode" typedef struct { char name[100]; char mode[8]; char uid[8]; char gid[8]; char size[12]; char mtime[12]; char checksum[8]; char typeflag; char linkdest[100]; char ustarind[6]; char ustarvers[2]; char user[32]; char group[32]; char major[8]; char minor[8]; char prefix[155]; char fill[12]; } ustar_t; #define TAR_SET_NUM(f,n)sprintf( f,"%0*o",( int )sizeof( f )-1,n ) #define TAR_BLOCK_SIZE 512 typedef enum FileType { ft_file = '0', /* ft_hardlink = '1', */ ft_symlink = '2', ft_chardev = '3', ft_blockdev = '4', ft_dir = '5' /* ft_fifo = '6' */ } FileType; struct svn_tar_t { /** Callback function for reporting added files. */ svn_tar_add_fn_t add_fn; /** Baton for add_fn. */ void* add_baton; /** The 'global' pool. */ apr_pool_t* pool; /** Prefix for filenames. */ const char* prefix; /** Length of the prefix. */ unsigned int prefixlen; /** Mode for files. */ unsigned short file_mode; /** Mode for executables. */ unsigned short exe_mode; /** Mode for directories. */ unsigned short dir_mode; /** Error flag. */ svn_boolean_t error; /** Template tar header. */ ustar_t template; /** Null bytes for padding. */ char nulls[2*TAR_BLOCK_SIZE]; /** Output stream to the tar file. */ svn_stream_t* tar_stream; /** Output stream to the files. */ svn_stream_t* file_stream; /** Name of the current file to write. */ const char* filename; /** Remaining count of bytes to write for the file. */ apr_size_t bytes; /** Count of padding bytes needed to fill the last block. */ apr_size_t padding; }; static svn_error_t* file_read( void* baton, char* buffer, apr_size_t* len ) { return svn_error_create( 0, NULL, _( "Can't read from tar file stream" ) ); } /* Write and count bytes. */ static svn_error_t* file_write( void* baton, const char* buffer, apr_size_t* len ) { svn_tar_t* tar = baton; svn_error_t* err; if( tar->error ) { return svn_error_create( 0, NULL, _( "Error writing tar file, can't continue" ) ); } if( *len > tar->bytes ) { return svn_error_createf( 0, NULL, _( "Too many bytes for file '%s'" ), tar->filename ); } err = svn_stream_write( tar->tar_stream, buffer, len ); if( err ) { tar->error = TRUE; return svn_error_createf( 0, err, _( "Error writing bytes for file '%s'" ), tar->filename ); } tar->bytes -= *len; return SVN_NO_ERROR; } /* Check that all bytes have been written and write padding bytes. */ static svn_error_t* file_close( void* baton ) { svn_tar_t* tar = baton; svn_error_t* err; apr_size_t sz; if( tar->error ) { return svn_error_create( 0, NULL, _( "Error writing tar file, can't continue" ) ); } if( tar->bytes != 0 ) { tar->error = TRUE; return svn_error_createf( 0, NULL, _( "Not enough bytes for file '%s'" ), tar->filename ); } if( tar->padding > 0 ) { sz = tar->padding; err = svn_stream_write( tar->tar_stream, tar->nulls, &sz ); if( err ) { tar->error = TRUE; return svn_error_createf( 0, err, _( "Error writing padding bytes for file '%s'" ), tar->filename ); } if( tar->padding != sz ) { tar->error = TRUE; return svn_error_createf( 0, NULL, _( "Error writing padding bytes for file '%s'" ), tar->filename ); } tar->padding = 0; } tar->filename = 0; return SVN_NO_ERROR; } svn_error_t* svn_tar_create( svn_tar_t** ptar, apr_pool_t* pool ) { svn_tar_t* tar; tar = apr_pcalloc( pool, sizeof(svn_tar_t) ); tar->add_fn = 0; tar->tar_stream = 0; tar->file_mode = 0644; tar->exe_mode = 0755; tar->dir_mode = 0755; tar->pool = pool; tar->error = FALSE; tar->prefix = ""; tar->prefixlen = 0; TAR_SET_NUM( tar->template.uid, 1000 ); TAR_SET_NUM( tar->template.gid, 1000 ); TAR_SET_NUM( tar->template.size, 0 ); strcpy( tar->template.ustarind, "ustar " ); strcpy( tar->template.user, "svntar" ); strcpy( tar->template.group, "svntar" ); TAR_SET_NUM( tar->template.mtime, 0 ); memset( tar->template.checksum, ' ', sizeof(tar->template.checksum) ); *ptar = tar; return SVN_NO_ERROR; } svn_error_t* svn_tar_close( svn_tar_t* tar ) { svn_boolean_t write_eof = tar->file_stream && !tar->error; /* can't reuse tar struct after close */ tar->error = TRUE; if( write_eof ) { svn_error_t* err; apr_size_t sz; sz = sizeof(tar->nulls); err = svn_stream_write( tar->tar_stream, tar->nulls, &sz ); if( err ) { return svn_error_create( 0, err, _( "Error writing EOF blocks" ) ); } if( sizeof(tar->nulls) != sz ) { return svn_error_create( 0, NULL, _( "Error writing EOF blocks" ) ); } err = svn_stream_close( tar->tar_stream ); if( err ) { return svn_error_create( 0, err, _( "Error closing tar stream" ) ); } } return SVN_NO_ERROR; } void svn_tar_set_output( svn_tar_t* tar, svn_stream_t* output ) { tar->tar_stream = output; } void svn_tar_set_add_callback( svn_tar_t* tar, svn_tar_add_fn_t add_fn, void* add_baton ) { tar->add_fn = add_fn; tar->add_baton = add_baton; } void svn_tar_set_prefix( svn_tar_t* tar, const char* prefix, apr_pool_t* pool ) { int n = strlen( prefix ); if( n > 0 && prefix[n-1] != '/' ) { prefix = apr_pstrcat( pool, prefix, "/", NULL ); n++; } else { prefix = apr_pstrdup( pool, prefix ); } tar->prefix = prefix; tar->prefixlen = n; } void svn_tar_set_user_id( svn_tar_t* tar, unsigned short user_id ) { TAR_SET_NUM( tar->template.uid, user_id ); } svn_error_t* svn_tar_set_user_name( svn_tar_t* tar, const char* user_name ) { if( strlen( user_name ) >= sizeof( tar->template.user ) ) { return svn_error_create( 0, NULL, _( "User name too long" ) ); } strcpy( tar->template.user, user_name ); return SVN_NO_ERROR; } void svn_tar_set_group_id( svn_tar_t* tar, unsigned short group_id ) { TAR_SET_NUM( tar->template.gid, group_id ); } svn_error_t* svn_tar_set_group_name( svn_tar_t* tar, const char* group_name ) { if( strlen( group_name ) >= sizeof( tar->template.group ) ) { return svn_error_create( 0, NULL, _( "Group name too long" ) ); } strcpy( tar->template.group, group_name ); return SVN_NO_ERROR; } void svn_tar_set_modes( svn_tar_t* tar, unsigned short file_mode, unsigned short exe_mode, unsigned short dir_mode ) { tar->file_mode = file_mode; tar->exe_mode = exe_mode; tar->dir_mode = dir_mode; } void svn_tar_set_mtime( svn_tar_t* tar, int mtime ) { TAR_SET_NUM( tar->template.mtime, mtime ); } /* open tar file and initialize streams */ static svn_error_t* open_tar_file( svn_tar_t* tar ) { tar->file_stream = svn_stream_create( tar, tar->pool ); svn_stream_set_read( tar->file_stream, file_read ); svn_stream_set_write( tar->file_stream, file_write ); svn_stream_set_close( tar->file_stream, file_close ); if( tar->prefixlen > 0 ) { // write prefix directory... } return SVN_NO_ERROR; } /* initialize header, open tar stream if not already done */ static svn_error_t* init_header( ustar_t* header, const char* name, svn_tar_t* tar ) { int namelen; if( tar->error ) { return svn_error_create( 0, NULL, _( "Error writing tar file, can't continue" ) ); } if( !tar->file_stream ) { SVN_ERR( open_tar_file( tar ) ); } if( tar->bytes != 0 || tar->padding != 0 ) { tar->error = TRUE; return svn_error_createf( 0, NULL, _( "File '%s' has not been closed" ), tar->filename ); } namelen = strlen( name ); if( ( namelen + tar->prefixlen ) > ( sizeof( header->name )+ sizeof(header->prefix)- 2 ) ) { return svn_error_createf( 0, NULL, _( "Filename '%s%s' is too long" ), tar->prefix, name ); } memcpy( header, &tar->template, sizeof(ustar_t) ); if( namelen > ( sizeof(header->name) - 1 ) ) { /* the first n bytes of name go into header->prefix */ int n = namelen - ( sizeof(header->name) - 1 ); strcpy( header->name, name + n ); strcpy( header->prefix, tar->prefix ); strcpy( header->prefix + tar->prefixlen, name ); } else if( ( tar->prefixlen + namelen ) > ( sizeof(header->name) - 1 ) ) { /* the first n bytes of tar->prefix go into header->prefix */ int n = tar->prefixlen + namelen - ( sizeof(header->name) - 1 ); strncpy( header->prefix, tar->prefix, n ); strcpy( header->name, tar->prefix + n ); strcpy( header->name + n, name ); } else { /* there is enough space for prefix and name in header->name */ strcpy( header->name, tar->prefix ); strcpy( header->name + tar->prefixlen, name ); } tar->filename = name; return SVN_NO_ERROR; } /* calculate checksum and write header to tar stream */ static svn_error_t* write_header( ustar_t* header, svn_tar_t* tar ) { svn_error_t* err; apr_size_t sz; int chksum = 0; int i; for( i=0; ichecksum, chksum & 0xFFFF ); sz = sizeof(*header); err = svn_stream_write( tar->tar_stream, (char*)header, &sz ); if( err ) { tar->error = TRUE; return svn_error_createf( 0, err, _( "Error writing header for file '%s'" ), tar->filename ); } if( sizeof(*header) != sz ) { tar->error = TRUE; return svn_error_createf( 0, NULL, _( "Error writing header for file '%s'" ), tar->filename ); } return SVN_NO_ERROR; } svn_error_t* svn_tar_add_dir( const char* name, svn_tar_t* tar, apr_pool_t* pool ) { ustar_t header; if( tar->add_fn ) { SVN_ERR( tar->add_fn( tar->add_baton, 'D', name, pool ) ); } name = apr_pstrcat( pool, name, "/", NULL ); SVN_ERR( init_header( &header, name, tar ) ); header.typeflag = ft_dir; TAR_SET_NUM( header.mode, tar->dir_mode ); SVN_ERR( write_header( &header, tar ) ); return SVN_NO_ERROR; } #if 0 /* unused functions */ svn_error_t* svn_tar_add_file_stream( svn_stream_t** filestream, const char* name, unsigned int size, svn_boolean_t executable, svn_tar_t* tar, apr_pool_t* pool ) { ustar_t header; if( tar->add_fn ) { SVN_ERR( tar->add_fn( tar->add_baton, 'F', name, pool ) ); } SVN_ERR( init_header( &header, name, tar ) ); header.typeflag = ft_file; TAR_SET_NUM( header.mode, executable ? tar->exe_mode : tar->file_mode ); TAR_SET_NUM( header.size, size ); tar->bytes = size; tar->padding = size % TAR_BLOCK_SIZE; if( tar->padding > 0 ) { tar->padding = TAR_BLOCK_SIZE - tar->padding; } SVN_ERR( write_header( &header, tar ) ); *filestream = tar->file_stream; return SVN_NO_ERROR; } svn_error_t* svn_tar_add_file( const char* name, const char* filename, svn_boolean_t ignore_missing, svn_tar_t* tar, apr_pool_t* pool ) { svn_error_t* err = SVN_NO_ERROR; svn_error_t* err2; apr_status_t ast; apr_finfo_t finfo; ast = apr_stat( &finfo, filename, APR_FINFO_LINK | APR_FINFO_MTIME | APR_FINFO_SIZE | APR_FINFO_TYPE | APR_FINFO_UPROT, pool ); if( APR_STATUS_IS_ENOENT( ast ) ) { if( !ignore_missing ) { err = svn_error_wrap_apr( ast, _( "lstat of '%s' failed" ), filename ); } return err; } if( finfo.filetype == APR_REG ) { apr_file_t* file; svn_stream_t* istream; svn_stream_t* ostream; SVN_ERR( svn_io_file_open( &file, filename, APR_READ | APR_BINARY, APR_OS_DEFAULT, pool ) ); istream = svn_stream_from_aprfile2( file, FALSE, pool ); // +++ finfo.mtime err = svn_tar_add_file_stream( &ostream, name, finfo.size, finfo.protection & APR_UEXECUTE, tar, pool ); if( !err ) { err = svn_stream_copy( istream, ostream, pool ); } if( !err ) { err = svn_stream_close( ostream ); } err2 = svn_stream_close( istream ); if( err2 ) { svn_error_clear( err2 ); } } else { err = svn_error_wrap_apr( ast, _( "'%s' is not a regular file" ), filename ); } return err; } svn_error_t* svn_tar_add_link( const char* name, const char* link_target, svn_tar_t* tar, apr_pool_t* pool ) { ustar_t header; if( tar->add_fn ) { SVN_ERR( tar->add_fn( tar->add_baton, 'L', name, pool ) ); } SVN_ERR( init_header( &header, name, tar ) ); header.typeflag = ft_symlink; TAR_SET_NUM( header.mode, tar->dir_mode ); if( strlen( link_target ) > ( sizeof( header.linkdest )- 1 ) ) { return svn_error_createf( 0, NULL, _( "Symlink '%s' target '%s' too long" ), name, link_target ); } strcpy( header.linkdest, link_target ); SVN_ERR( write_header( &header, tar ) ); return SVN_NO_ERROR; } #ifndef WIN32 svn_error_t* svn_tar_add_file_or_link( const char* name, const char* filename, svn_boolean_t ignore_missing, svn_tar_t* tar, apr_pool_t* pool ) { svn_error_t* err = SVN_NO_ERROR; svn_error_t* err2; apr_status_t ast; apr_finfo_t finfo; ast = apr_stat( &finfo, filename, APR_FINFO_LINK | APR_FINFO_MTIME | APR_FINFO_SIZE | APR_FINFO_TYPE | APR_FINFO_UPROT, pool ); if( APR_STATUS_IS_ENOENT( ast ) ) { if( !ignore_missing ) { err = svn_error_wrap_apr( ast, _( "lstat of '%s' failed" ), filename ); } return err; } if( finfo.filetype == APR_REG ) { apr_file_t* file; svn_stream_t* istream; svn_stream_t* ostream; SVN_ERR( svn_io_file_open( &file, filename, APR_READ | APR_BINARY, APR_OS_DEFAULT, pool ) ); istream = svn_stream_from_aprfile2( file, FALSE, pool ); // +++ finfo.mtime err = svn_tar_add_file_stream( &ostream, name, finfo.size, finfo.protection & APR_UEXECUTE, tar, pool ); if( !err ) { err = svn_stream_copy( istream, ostream, pool ); } if( !err ) { err = svn_stream_close( ostream ); } err2 = svn_stream_close( istream ); if( err2 ) { svn_error_clear( err2 ); } } else if( finfo.filetype == APR_LNK ) { char linkdest[100+4]; if( readlink( filename, linkdest, sizeof( linkdest ) )< 0 ) { return svn_error_createf( 0, NULL, _( "Couldn't read link for '%s'" ), filename ); } err = svn_tar_add_link( name, linkdest, tar, pool ); } return err; } #endif /* !WIN32 */ #endif /* unused functions */ svn_error_t* parse_device_numbers( ustar_t* header, const char* devnrs ) { int major; int minor; char* end; major = strtoul( devnrs, &end, 0 ); if( *end != ':' ) { return svn_error_createf( 0, NULL, _( "Missing colon in device numbers" ) ); } minor = strtoul( end+1, &end, 0 ); if( *end != 0 ) { return svn_error_createf( 0, NULL, _( "Garbage after device numbers" ) ); } TAR_SET_NUM( header->major, major ); TAR_SET_NUM( header->minor, minor ); return SVN_NO_ERROR; } svn_error_t* parse_user_or_group( ustar_t* header, int group, const char* str ) { char* name; int id; int n; id = strtoul( str, &name, 0 ); if( id >> 16 ) { return svn_error_createf( 0, NULL, _( "Invalid UID/GID" ) ); } if( name[0] != ' ' || name[1] == 0 ) { return svn_error_createf( 0, NULL, _( "Missing user/group name" ) ); } name++; n = strlen( name ); if( group ) { TAR_SET_NUM( header->gid, id ); if( n < sizeof( header->group ) ) { strncpy( header->group, name, sizeof( header->group ) ); header->group[sizeof( header->group )-1] = 0; } else { strcpy( header->group, name ); memset( header->group+n, 0, sizeof( header->group )-n ); } } else { TAR_SET_NUM( header->uid, id ); strcpy( header->user, name+1 ); if( n < sizeof( header->user ) ) { strncpy( header->user, name, sizeof( header->user ) ); header->user[sizeof( header->user )-1] = 0; } else { strcpy( header->user, name ); memset( header->user+n, 0, sizeof( header->user )-n ); } } return SVN_NO_ERROR; } svn_error_t* svn_tar_add( const char* name, svn_node_kind_t kind, int size, svn_tar_propget_callback_t propget_cb, void* propget_baton, svn_tar_file_stream_callback_t filestream_cb, void* filestream_baton, svn_tar_t* tar, apr_pool_t* pool ) { const char* text_time; const char* owner; const char* group; const char* unix_mode; const char* special; const char* executable; ustar_t header; SVN_ERR( propget_cb( SVN_PROP_TEXT_TIME, &text_time, propget_baton, pool ) ); SVN_ERR( propget_cb( SVN_PROP_OWNER, &owner, propget_baton, pool ) ); SVN_ERR( propget_cb( SVN_PROP_GROUP, &group, propget_baton, pool ) ); SVN_ERR( propget_cb( SVN_PROP_UNIX_MODE, &unix_mode, propget_baton, pool ) ); if( kind == svn_node_dir ) { /* diretories must have a trailing slash */ name = apr_pstrcat( pool, name, "/", NULL ); special = 0; executable = 0; } else { /* only files can have special or executable properties */ SVN_ERR( propget_cb( SVN_PROP_SPECIAL, &special, propget_baton, pool ) ); SVN_ERR( propget_cb( SVN_PROP_EXECUTABLE, &executable, propget_baton, pool ) ); } SVN_ERR( init_header( &header, name, tar ) ); if( text_time ) { apr_time_t aprmtime; int mtime; SVN_ERR( svn_time_from_cstring( &aprmtime, text_time, pool ) ); mtime = apr_time_sec( aprmtime ); TAR_SET_NUM( header.mtime, mtime ); } if( owner ) { SVN_ERR( parse_user_or_group( &header, 0, owner ) ); } if( group ) { SVN_ERR( parse_user_or_group( &header, 1, group ) ); } if( unix_mode ) { int mode = strtoul( unix_mode, 0, 0 ); TAR_SET_NUM( header.mode, mode ); } else if( executable ) { TAR_SET_NUM( header.mode, tar->exe_mode ); } else if( kind != svn_node_dir ) { TAR_SET_NUM( header.mode, tar->file_mode ); } else { TAR_SET_NUM( header.mode, tar->dir_mode ); } if( kind == svn_node_dir ) { /* directory */ if( tar->add_fn ) { SVN_ERR( tar->add_fn( tar->add_baton, 'D', name, pool ) ); } header.typeflag = ft_dir; return write_header( &header, tar ); } else if( special ) { /* special: symlink, char/block device */ svn_stringbuf_t* strbuf; svn_stream_t* strstream; char kindchar; strbuf = svn_stringbuf_create( "", pool ); strstream = svn_stream_from_stringbuf( strbuf, pool ); SVN_ERR( ( *filestream_cb )( strstream, filestream_baton, pool ) ); if( strncmp( strbuf->data, "link ", 5 ) == 0 ) { header.typeflag = ft_symlink; kindchar = 'L'; if( strlen( strbuf->data ) > ( sizeof(header.linkdest) - 1 + 5 ) ) return svn_error_createf( 0, NULL, _( "Symlink '%s' target '%s' too long" ), name, strbuf->data+5 ); strcpy( header.linkdest, strbuf->data+5 ); } else if( strncmp( strbuf->data, "cdev ", 5 )== 0 ) { header.typeflag = ft_chardev; kindchar = 'C'; SVN_ERR( parse_device_numbers( &header, strbuf->data+5 ) ); } else if( strncmp( strbuf->data, "bdev ", 5 )== 0 ) { header.typeflag = ft_blockdev; kindchar = 'B'; SVN_ERR( parse_device_numbers( &header, strbuf->data+5 ) ); } else { return svn_error_createf( 0, NULL, _( "Unknown special file '%s'" ), name ); } if( tar->add_fn ) { SVN_ERR( tar->add_fn( tar->add_baton, kindchar, name, pool ) ); } return write_header( &header, tar ); } else { /* normal file */ if( tar->add_fn ) { SVN_ERR( tar->add_fn( tar->add_baton, 'F', name, pool ) ); } header.typeflag = ft_file; TAR_SET_NUM( header.size, size ); tar->bytes = size; tar->padding = size % TAR_BLOCK_SIZE; if( tar->padding > 0 ) { tar->padding = TAR_BLOCK_SIZE - tar->padding; } SVN_ERR( write_header( &header, tar ) ); return ( *filestream_cb )( tar->file_stream, filestream_baton, pool ); } }