/******************************************************************************* Copyright (c) 1997-2004, Perforce Software, Inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTR IBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL PERFORCE SOFTWARE, INC. BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *******************************************************************************/ /******************************************************************************* * Name : clientuserruby.cc * * Author : Tony Smith or * * Description : Ruby bindings for the Perforce API. User interface class * for getting Perforce results into Ruby. * ******************************************************************************/ #include #include #include #include #include #include "gc_hack.h" #include "p4result.h" #include "clientuserruby.h" /******************************************************************************* * ClientUserRuby - the user interface part. Gets responses from the Perforce * server, and converts the data to Ruby form for returning to the caller. ******************************************************************************/ ClientUserRuby::ClientUserRuby() { debug = 0; input = Qnil; } void ClientUserRuby::Reset() { results.Reset(); // Leave input alone. } void ClientUserRuby::Finished() { // Reset input coz we should be done with it now. Keeping hold of // it just prevents GC from sweeping it if possible if ( debug && input != Qnil ) fprintf( stderr, "[P4] Cleaning up saved input\n" ); input = Qnil; } void ClientUserRuby::HandleError( Error *e ) { if ( debug ) fprintf( stderr, "[P4] HandleError()\n" ); results.AddError( e ); } void ClientUserRuby::OutputText( const_char *data, int length ) { if ( debug ) fprintf( stderr, "[P4] OutputText()\n" ); results.AddOutput( data ); } void ClientUserRuby::OutputInfo( char level, const_char *data ) { if ( debug ) fprintf( stderr, "[P4] OutputInfo()\n" ); results.AddOutput( data ); } void ClientUserRuby::OutputBinary( const_char *data, int length ) { if ( debug ) fprintf( stderr, "[P4] OutputBinary() %d bytes\n", length ); // // Binary is just stored in a string. Since the char * version of // P4Result::AddOutput() assumes it can strlen() to find the length, // we'll make the String object here. // results.AddOutput( rb_str_new( data, length) ); } void ClientUserRuby::OutputStat( StrDict *values ) { StrPtr *spec, *data; // If both specdef and data are set, then we need to parse the form // and return the results. If not, then we just convert it as is. spec = values->GetVar( "specdef" ); data = values->GetVar( "data" ); if ( spec && data ) { if ( debug ) fprintf( stderr, "[P4] OutputStat() - parsing form\n" ); // Parse up the form. Use the ParseNoValid() interface to prevent // errors caused by the use of invalid defaults for select items in // jobspecs. SpecDataTable specData; Spec s( spec->Text(), "" ); Error e; s.ParseNoValid( data->Text(), &specData, &e ); if ( e.Test() ) { HandleError( &e ); return; } results.AddOutput( DictToHash( specData.Dict(), spec ) ); } else { if ( debug ) fprintf( stderr, "[P4] OutputStat() - converting StrDict to hash\n" ); results.AddOutput( DictToHash( values, NULL ) ); } } /* * Diff support for Ruby API. Since the Diff class only writes its output * to files, we run the requested diff putting the output into a temporary * file. Then we read the file in and add its contents line by line to the * results. */ void ClientUserRuby::Diff( FileSys *f1, FileSys *f2, int doPage, char *diffFlags, Error *e ) { if ( debug ) fprintf( stderr, "[P4] Diff() - comparing files\n" ); // // Duck binary files. Much the same as ClientUser::Diff, we just // put the output into Ruby space rather than stdout. // if( !f1->IsTextual() || !f2->IsTextual() ) { if ( f1->Compare( f2, e ) ) results.AddOutput( "(... files differ ...)" ); return; } // Time to diff the two text files. Need to ensure that the // files are in binary mode, so we have to create new FileSys // objects to do this. FileSys *f1_bin = FileSys::Create( FST_BINARY ); FileSys *f2_bin = FileSys::Create( FST_BINARY ); FileSys *t = FileSys::CreateGlobalTemp( f1->GetType() ); f1_bin->Set( f1->Name() ); f2_bin->Set( f2->Name() ); { // // In its own block to make sure that the diff object is deleted // before we delete the FileSys objects. // #ifndef OS_NEXT :: #endif Diff d; d.SetInput( f1_bin, f2_bin, diffFlags, e ); if ( ! e->Test() ) d.SetOutput( t->Name(), e ); if ( ! e->Test() ) d.DiffWithFlags( diffFlags ); d.CloseOutput( e ); // OK, now we have the diff output, read it in and add it to // the output. if ( ! e->Test() ) t->Open( FOM_READ, e ); if ( ! e->Test() ) { StrBuf b; while( t->ReadLine( &b, e ) ) results.AddOutput( b.Text() ); } } delete t; delete f1_bin; delete f2_bin; if ( e->Test() ) HandleError( e ); } /* * convert input from the user into a form digestible to Perforce. This * involves either (a) converting any supplied hash to a Perforce form, or * (b) running to_s on whatever we were given. */ void ClientUserRuby::InputData( StrBuf *strbuf, Error *e ) { if ( debug ) fprintf( stderr, "[P4] InputData(). Using supplied input\n" ); if ( Qtrue == rb_obj_is_kind_of( input, rb_cHash ) ) { HashToForm( input, strbuf ); return; } ID to_s = rb_intern( "to_s" ); VALUE str = rb_funcall( input, to_s, 0 ); strbuf->Set( STR2CSTR( str ) ); } /* * Accept input from Ruby and convert to a StrBuf for Perforce * purposes. We just save what we're given here because we may not * have the specdef available to parse it with at this time. */ VALUE ClientUserRuby::SetInput( VALUE i ) { if ( debug ) fprintf( stderr, "[P4] SetInput()\n" ); input = i; return Qtrue; } /* * Convert a Perforce StrDict into a Ruby hash. Convert multi-level * data (Files0, Files1 etc. ) into (nested) array members of the hash. If * specDef is NULL, then the specDef member will be skipped over, other * wise it will be saved as a wrapped structure in the hash. */ VALUE ClientUserRuby::DictToHash( StrDict *dict, StrPtr *specDef ) { StrRef var, val; int i; VALUE hash = rb_hash_new(); VALUE hkey; VALUE sdv; StrBuf *sd = new StrBuf; for ( i = 0; dict->GetVar( i, var, val ); i++ ) { if ( var == "specdef" && ! specDef ) continue; if ( var == "func" ) continue; InsertItem( hash, &var, &val ); } return hash; } /* * Convert a Ruby hash - possibly containing array members into a * formatted StrBuf ready for sending to Perforce. Return Qtrue or * Qfalse to indicate success or failure. The form itself is saved in * this->input */ int ClientUserRuby::HashToForm( VALUE hash, StrBuf *strbuf ) { StrPtr *specDef; specDef = varList->GetVar( "specdef" ); if ( ! specDef ) { rb_warn( "No specdef available. Cannot convert hash to a " "Perforce form" ); return 0; } SpecDataTable specData; Spec s( specDef->Text(), "" ); ID idKeys = rb_intern( "keys" ); ID idLength = rb_intern( "length" ); ID idToS = rb_intern( "to_s" ); VALUE keys = rb_funcall( hash, idKeys, 0 ); int keyCount = NUM2INT( rb_funcall( keys, idLength, 0 ) ); for ( int idx = 0; idx < keyCount; idx++ ) { VALUE key; VALUE val; StrBuf keyStr; key = rb_ary_entry( keys, idx ); if ( key == Qnil ) break; keyStr.Set( STR2CSTR( rb_funcall( key, idToS, 0 ) ) ); val = rb_hash_aref( hash, key ); if ( rb_obj_is_kind_of( val, rb_cArray ) ) { // Need to flatten the array VALUE subVal; for( int idx2 = 0; subVal = rb_ary_entry( val, idx2 ) ; idx2++ ) { if ( subVal == Qnil ) break; StrBuf tKey; tKey.Alloc( 32 ); sprintf( tKey.Text(), "%s%d", keyStr.Text(), idx2 ); specData.Dict()->SetVar( tKey.Text(), STR2CSTR( rb_funcall( subVal, idToS, 0 ))); } } else { specData.Dict()->SetVar( keyStr.Text(), STR2CSTR( rb_funcall( val, idToS, 0 ) ) ); } } s.Format( &specData, strbuf ); return 1; } /* * Split a key into its base name and its index. i.e. for a key "how1,0" * the base name is "how" and they index is "1,0". We work backwards from * the end of the key looking for the first char that is neither a * digit, nor a comma. */ void ClientUserRuby::SplitKey( const StrPtr *key, StrBuf &base, StrBuf &index ) { int i = 0; base = *key; index = ""; for ( i = key->Length(); i; i-- ) { char prev = (*key)[ i-1 ]; if ( !isdigit( prev ) && prev != ',' ) { base.Set( key->Text(), i ); index.Set( key->Text() + i ); break; } } } /* * Insert an element into the response structure. The element may need to * be inserted into an array nested deeply within the enclosing hash. */ void ClientUserRuby::InsertItem( VALUE hash, const StrPtr *var, const StrPtr *val ) { VALUE ary = 0; VALUE tary = 0; VALUE key; StrBuf base, index; StrRef comma( "," ); SplitKey( var, base, index ); // If there's no index, then we insert into the top level hash // but if the key is already defined then we need to rename the key. This // is probably one of those special keys like otherOpen which can be // both an array element and a scalar. The scalar comes last, so we // just rename it to "otherOpens" to avoid trashing the previous key // value if ( index == "" ) { ID idHasKey = rb_intern( "has_key?"); ID idPlus = rb_intern( "+" ); key = rb_str_new2( var->Text() ); if ( rb_funcall( hash, idHasKey, 1, key ) == Qtrue ) key = rb_funcall( key, idPlus, 1, rb_str_new2( "s" ) ); rb_hash_aset( hash, key, rb_str_new2( val->Text() ) ); return; } // // Get or create the parent array from the hash. // key = rb_str_new2( base.Text() ); ary = rb_hash_aref( hash, key ); if ( Qnil == ary ) { ary = rb_ary_new(); rb_hash_aset( hash, key, ary ); } // The index may be a simple digit, or it could be a comma separated // list of digits. For each "level" in the index, we need a containing // array. for( const char *c = 0 ; c = index.Contains( comma ); ) { StrBuf level; level.Set( index.Text(), c - index.Text() ); index.Set( c + 1 ); // Found another level so we need to get/create a nested array // under the current entry. We use the level as an index so that // missing entries are left empty deliberately. tary = rb_ary_entry( ary, level.Atoi() ); if ( ! RTEST( tary ) ) { tary = rb_ary_new(); rb_ary_store( ary, level.Atoi(), tary ); } ary = tary; } rb_ary_push( ary, rb_str_new2( val->Text() ) ); } void ClientUserRuby::GCMark() { if ( debug >= 2 ) fprintf( stderr, "[P4] Marking results and errors for garbage collection\n" ); if ( input != Qnil ) rb_gc_mark( input ); results.GCMark(); }