msi: Honor msidbFeatureAttributesFollowParent.
[wine] / dlls / msi / helpers.c
1 /*
2  * Implementation of the Microsoft Installer (msi.dll)
3  *
4  * Copyright 2005 Aric Stewart for CodeWeavers
5  *
6  * This library is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU Lesser General Public
8  * License as published by the Free Software Foundation; either
9  * version 2.1 of the License, or (at your option) any later version.
10  *
11  * This library is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14  * Lesser General Public License for more details.
15  *
16  * You should have received a copy of the GNU Lesser General Public
17  * License along with this library; if not, write to the Free Software
18  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
19  */
20
21 /*
22  * Here are helper functions formally in action.c that are used by a variety of
23  * actions and functions.
24  */
25
26 #include <stdarg.h>
27
28 #include "windef.h"
29 #include "wine/debug.h"
30 #include "msipriv.h"
31 #include "winuser.h"
32 #include "wine/unicode.h"
33 #include "msidefs.h"
34
35 WINE_DEFAULT_DEBUG_CHANNEL(msi);
36
37 static const WCHAR cszTargetDir[] = {'T','A','R','G','E','T','D','I','R',0};
38 static const WCHAR cszDatabase[]={'D','A','T','A','B','A','S','E',0};
39
40 LPWSTR build_icon_path(MSIPACKAGE *package, LPCWSTR icon_name )
41 {
42     LPWSTR SystemFolder, dest, FilePath;
43
44     static const WCHAR szMicrosoft[] =
45         {'M','i','c','r','o','s','o','f','t','\\',0};
46     static const WCHAR szInstaller[] = 
47         {'I','n','s','t','a','l','l','e','r','\\',0};
48     static const WCHAR szADFolder[] =
49         {'A','p','p','D','a','t','a','F','o','l','d','e','r',0};
50     static const WCHAR szWFolder[] =
51         {'W','i','n','d','o','w','s','F','o','l','d','e','r',0};
52
53     if(package->Context == MSIINSTALLCONTEXT_MACHINE)
54         SystemFolder = msi_dup_property( package->db, szWFolder );
55     else
56     {
57         LPWSTR ADTgt = msi_dup_property( package->db, szADFolder );
58         SystemFolder = build_directory_name(2, ADTgt, szMicrosoft);
59         msi_free(ADTgt);
60     }
61
62     dest = build_directory_name(3, SystemFolder, szInstaller, package->ProductCode);
63
64     create_full_pathW(dest);
65
66     FilePath = build_directory_name(2, dest, icon_name);
67
68     msi_free(SystemFolder);
69     msi_free(dest);
70     return FilePath;
71 }
72
73 LPWSTR msi_dup_record_field( MSIRECORD *rec, INT field )
74 {
75     DWORD sz = 0;
76     LPWSTR str;
77     UINT r;
78
79     if (MSI_RecordIsNull( rec, field ))
80         return NULL;
81
82     r = MSI_RecordGetStringW( rec, field, NULL, &sz );
83     if (r != ERROR_SUCCESS)
84         return NULL;
85
86     sz ++;
87     str = msi_alloc( sz * sizeof (WCHAR) );
88     if (!str)
89         return str;
90     str[0] = 0;
91     r = MSI_RecordGetStringW( rec, field, str, &sz );
92     if (r != ERROR_SUCCESS)
93     {
94         ERR("failed to get string!\n");
95         msi_free( str );
96         return NULL;
97     }
98     return str;
99 }
100
101 MSICOMPONENT* get_loaded_component( MSIPACKAGE* package, LPCWSTR Component )
102 {
103     MSICOMPONENT *comp;
104
105     LIST_FOR_EACH_ENTRY( comp, &package->components, MSICOMPONENT, entry )
106     {
107         if (!strcmpW( Component, comp->Component ))
108             return comp;
109     }
110     return NULL;
111 }
112
113 MSIFEATURE* get_loaded_feature(MSIPACKAGE* package, LPCWSTR Feature )
114 {
115     MSIFEATURE *feature;
116
117     LIST_FOR_EACH_ENTRY( feature, &package->features, MSIFEATURE, entry )
118     {
119         if (!strcmpW( Feature, feature->Feature ))
120             return feature;
121     }
122     return NULL;
123 }
124
125 MSIFILE* get_loaded_file( MSIPACKAGE* package, LPCWSTR key )
126 {
127     MSIFILE *file;
128
129     LIST_FOR_EACH_ENTRY( file, &package->files, MSIFILE, entry )
130     {
131         if (!strcmpW( key, file->File ))
132             return file;
133     }
134     return NULL;
135 }
136
137 MSIFILEPATCH* get_loaded_filepatch( MSIPACKAGE* package, LPCWSTR key )
138 {
139     MSIFILEPATCH *patch;
140
141     /* FIXME: There might be more than one patch */
142
143     LIST_FOR_EACH_ENTRY( patch, &package->filepatches, MSIFILEPATCH, entry )
144     {
145         if (!strcmpW( key, patch->File->File ))
146             return patch;
147     }
148     return NULL;
149 }
150
151 int track_tempfile( MSIPACKAGE *package, LPCWSTR path )
152 {
153     MSITEMPFILE *temp;
154
155     TRACE("%s\n", debugstr_w(path));
156
157     LIST_FOR_EACH_ENTRY( temp, &package->tempfiles, MSITEMPFILE, entry )
158         if (!strcmpW( path, temp->Path ))
159             return 0;
160
161     temp = msi_alloc_zero( sizeof (MSITEMPFILE) );
162     if (!temp)
163         return -1;
164
165     list_add_head( &package->tempfiles, &temp->entry );
166     temp->Path = strdupW( path );
167
168     return 0;
169 }
170
171 MSIFOLDER *get_loaded_folder( MSIPACKAGE *package, LPCWSTR dir )
172 {
173     MSIFOLDER *folder;
174
175     LIST_FOR_EACH_ENTRY( folder, &package->folders, MSIFOLDER, entry )
176     {
177         if (!strcmpW( dir, folder->Directory ))
178             return folder;
179     }
180     return NULL;
181 }
182
183 static LPWSTR get_source_root( MSIPACKAGE *package )
184 {
185     LPWSTR path, p;
186
187     path = msi_dup_property( package->db, cszSourceDir );
188     if (path)
189         return path;
190
191     path = msi_dup_property( package->db, cszDatabase );
192     if (path)
193     {
194         p = strrchrW(path,'\\');
195         if (p)
196             *(p+1) = 0;
197     }
198     return path;
199 }
200
201 /*
202  * clean_spaces_from_path()
203  *
204  * removes spaces from the beginning and end of path segments
205  * removes multiple \\ characters
206  */
207 static void clean_spaces_from_path( LPWSTR p )
208 {
209     LPWSTR q = p;
210     int n, len = 0;
211
212     while (1)
213     {
214         /* copy until the end of the string or a space */
215         while (*p != ' ' && (*q = *p))
216         {
217             p++, len++;
218             /* reduce many backslashes to one */
219             if (*p != '\\' || *q != '\\')
220                 q++;
221         }
222
223         /* quit at the end of the string */
224         if (!*p)
225             break;
226
227         /* count the number of spaces */
228         n = 0;
229         while (p[n] == ' ')
230             n++;
231
232         /* if it's leading or trailing space, skip it */
233         if ( len == 0 || p[-1] == '\\' || p[n] == '\\' )
234             p += n;
235         else  /* copy n spaces */
236             while (n && (*q++ = *p++)) n--;
237     }
238 }
239
240 LPWSTR resolve_file_source(MSIPACKAGE *package, MSIFILE *file)
241 {
242     LPWSTR p, path;
243
244     TRACE("Working to resolve source of file %s\n", debugstr_w(file->File));
245
246     if (file->IsCompressed)
247         return NULL;
248
249     p = resolve_source_folder( package, file->Component->Directory, NULL );
250     path = build_directory_name(2, p, file->ShortName);
251
252     if (file->LongName &&
253         GetFileAttributesW(path) == INVALID_FILE_ATTRIBUTES)
254     {
255         msi_free(path);
256         path = build_directory_name(2, p, file->LongName);
257     }
258
259     msi_free(p);
260
261     TRACE("file %s source resolves to %s\n", debugstr_w(file->File), debugstr_w(path));
262     return path;
263 }
264
265 LPWSTR resolve_source_folder( MSIPACKAGE *package, LPCWSTR name, MSIFOLDER **folder )
266 {
267     MSIFOLDER *f;
268     LPWSTR p, path = NULL, parent;
269
270     TRACE("working to resolve %s\n", debugstr_w(name));
271
272     if (!strcmpW( name, cszSourceDir ))
273         name = cszTargetDir;
274
275     f = get_loaded_folder( package, name );
276     if (!f)
277         return NULL;
278
279     /* special resolving for Target and Source root dir */
280     if (!strcmpW( name, cszTargetDir ))
281     {
282         if (!f->ResolvedSource)
283             f->ResolvedSource = get_source_root( package );
284     }
285
286     if (folder)
287         *folder = f;
288
289     if (f->ResolvedSource)
290     {
291         path = strdupW( f->ResolvedSource );
292         TRACE("   already resolved to %s\n", debugstr_w(path));
293         return path;
294     }
295
296     if (!f->Parent)
297         return path;
298
299     parent = f->Parent;
300     TRACE(" ! parent is %s\n", debugstr_w(parent));
301
302     p = resolve_source_folder( package, parent, NULL );
303
304     if (package->WordCount & msidbSumInfoSourceTypeCompressed)
305         path = get_source_root( package );
306     else if (package->WordCount & msidbSumInfoSourceTypeSFN)
307         path = build_directory_name( 3, p, f->SourceShortPath, NULL );
308     else
309         path = build_directory_name( 3, p, f->SourceLongPath, NULL );
310
311     TRACE("-> %s\n", debugstr_w(path));
312     f->ResolvedSource = strdupW( path );
313     msi_free( p );
314
315     return path;
316 }
317
318 LPWSTR resolve_target_folder( MSIPACKAGE *package, LPCWSTR name, BOOL set_prop, BOOL load_prop,
319                               MSIFOLDER **folder )
320 {
321     MSIFOLDER *f;
322     LPWSTR p, path = NULL, parent;
323
324     TRACE("working to resolve %s\n", debugstr_w(name));
325
326     f = get_loaded_folder( package, name );
327     if (!f)
328         return NULL;
329
330     /* special resolving for Target and Source root dir */
331     if (!strcmpW( name, cszTargetDir ))
332     {
333         if (!f->ResolvedTarget && !f->Property)
334         {
335             LPWSTR check_path;
336             check_path = msi_dup_property( package->db, cszTargetDir );
337             if (!check_path)
338             {
339                 check_path = msi_dup_property( package->db, cszRootDrive );
340                 if (set_prop)
341                     msi_set_property( package->db, cszTargetDir, check_path );
342             }
343
344             /* correct misbuilt target dir */
345             path = build_directory_name(2, check_path, NULL);
346             clean_spaces_from_path( path );
347             if (strcmpiW( path, check_path ))
348                 msi_set_property( package->db, cszTargetDir, path );
349             msi_free(check_path);
350
351             f->ResolvedTarget = path;
352         }
353     }
354
355     if (folder)
356         *folder = f;
357
358     if (f->ResolvedTarget)
359     {
360         path = strdupW( f->ResolvedTarget );
361         TRACE("   already resolved to %s\n", debugstr_w(path));
362         return path;
363     }
364
365     if (f->Property)
366     {
367         path = build_directory_name( 2, f->Property, NULL );
368         TRACE("   internally set to %s\n", debugstr_w(path));
369         if (set_prop) msi_set_property( package->db, name, path );
370         return path;
371     }
372
373     if (load_prop && (path = msi_dup_property( package->db, name )))
374     {
375         f->ResolvedTarget = strdupW( path );
376         TRACE("   property set to %s\n", debugstr_w(path));
377         return path;
378     }
379
380     if (!f->Parent)
381         return path;
382
383     parent = f->Parent;
384
385     TRACE(" ! parent is %s\n", debugstr_w(parent));
386
387     p = resolve_target_folder( package, parent, set_prop, load_prop, NULL );
388
389     TRACE("   TargetDefault = %s\n", debugstr_w(f->TargetDefault));
390     path = build_directory_name( 3, p, f->TargetDefault, NULL );
391     clean_spaces_from_path( path );
392     f->ResolvedTarget = strdupW( path );
393
394     TRACE("-> %s\n", debugstr_w(path));
395     if (set_prop) msi_set_property( package->db, name, path );
396     msi_free( p );
397
398     return path;
399 }
400
401 /* wrapper to resist a need for a full rewrite right now */
402 DWORD deformat_string(MSIPACKAGE *package, LPCWSTR ptr, WCHAR** data )
403 {
404     if (ptr)
405     {
406         MSIRECORD *rec = MSI_CreateRecord(1);
407         DWORD size = 0;
408
409         MSI_RecordSetStringW(rec,0,ptr);
410         MSI_FormatRecordW(package,rec,NULL,&size);
411
412         size++;
413         *data = msi_alloc(size*sizeof(WCHAR));
414         if (size > 1)
415             MSI_FormatRecordW(package,rec,*data,&size);
416         else
417             *data[0] = 0;
418
419         msiobj_release( &rec->hdr );
420         return sizeof(WCHAR)*size;
421     }
422
423     *data = NULL;
424     return 0;
425 }
426
427 UINT schedule_action(MSIPACKAGE *package, UINT script, LPCWSTR action)
428 {
429     UINT count;
430     LPWSTR *newbuf = NULL;
431     if (script >= TOTAL_SCRIPTS)
432     {
433         FIXME("Unknown script requested %i\n",script);
434         return ERROR_FUNCTION_FAILED;
435     }
436     TRACE("Scheduling Action %s in script %i\n",debugstr_w(action), script);
437     
438     count = package->script->ActionCount[script];
439     package->script->ActionCount[script]++;
440     if (count != 0)
441         newbuf = msi_realloc( package->script->Actions[script],
442                         package->script->ActionCount[script]* sizeof(LPWSTR));
443     else
444         newbuf = msi_alloc( sizeof(LPWSTR));
445
446     newbuf[count] = strdupW(action);
447     package->script->Actions[script] = newbuf;
448
449    return ERROR_SUCCESS;
450 }
451
452 void msi_free_action_script(MSIPACKAGE *package, UINT script)
453 {
454     UINT i;
455     for (i = 0; i < package->script->ActionCount[script]; i++)
456         msi_free(package->script->Actions[script][i]);
457
458     msi_free(package->script->Actions[script]);
459     package->script->Actions[script] = NULL;
460     package->script->ActionCount[script] = 0;
461 }
462
463 /*
464  *  build_directory_name()
465  *
466  *  This function is to save messing round with directory names
467  *  It handles adding backslashes between path segments, 
468  *   and can add \ at the end of the directory name if told to.
469  *
470  *  It takes a variable number of arguments.
471  *  It always allocates a new string for the result, so make sure
472  *   to free the return value when finished with it.
473  *
474  *  The first arg is the number of path segments that follow.
475  *  The arguments following count are a list of path segments.
476  *  A path segment may be NULL.
477  *
478  *  Path segments will be added with a \ separating them.
479  *  A \ will not be added after the last segment, however if the
480  *    last segment is NULL, then the last character will be a \
481  * 
482  */
483 LPWSTR build_directory_name(DWORD count, ...)
484 {
485     DWORD sz = 1, i;
486     LPWSTR dir;
487     va_list va;
488
489     va_start(va,count);
490     for(i=0; i<count; i++)
491     {
492         LPCWSTR str = va_arg(va,LPCWSTR);
493         if (str)
494             sz += strlenW(str) + 1;
495     }
496     va_end(va);
497
498     dir = msi_alloc(sz*sizeof(WCHAR));
499     dir[0]=0;
500
501     va_start(va,count);
502     for(i=0; i<count; i++)
503     {
504         LPCWSTR str = va_arg(va,LPCWSTR);
505         if (!str)
506             continue;
507         strcatW(dir, str);
508         if( ((i+1)!=count) && dir[strlenW(dir)-1]!='\\')
509             strcatW(dir, szBackSlash);
510     }
511     va_end(va);
512
513     return dir;
514 }
515
516 /***********************************************************************
517  *            create_full_pathW
518  *
519  * Recursively create all directories in the path.
520  *
521  * shamelessly stolen from setupapi/queue.c
522  */
523 BOOL create_full_pathW(const WCHAR *path)
524 {
525     BOOL ret = TRUE;
526     int len;
527     WCHAR *new_path;
528
529     new_path = msi_alloc( (strlenW(path) + 1) * sizeof(WCHAR));
530
531     strcpyW(new_path, path);
532
533     while((len = strlenW(new_path)) && new_path[len - 1] == '\\')
534     new_path[len - 1] = 0;
535
536     while(!CreateDirectoryW(new_path, NULL))
537     {
538         WCHAR *slash;
539         DWORD last_error = GetLastError();
540         if(last_error == ERROR_ALREADY_EXISTS)
541             break;
542
543         if(last_error != ERROR_PATH_NOT_FOUND)
544         {
545             ret = FALSE;
546             break;
547         }
548
549         if(!(slash = strrchrW(new_path, '\\')))
550         {
551             ret = FALSE;
552             break;
553         }
554
555         len = slash - new_path;
556         new_path[len] = 0;
557         if(!create_full_pathW(new_path))
558         {
559             ret = FALSE;
560             break;
561         }
562         new_path[len] = '\\';
563     }
564
565     msi_free(new_path);
566     return ret;
567 }
568
569 void ui_progress(MSIPACKAGE *package, int a, int b, int c, int d )
570 {
571     MSIRECORD * row;
572
573     row = MSI_CreateRecord(4);
574     MSI_RecordSetInteger(row,1,a);
575     MSI_RecordSetInteger(row,2,b);
576     MSI_RecordSetInteger(row,3,c);
577     MSI_RecordSetInteger(row,4,d);
578     MSI_ProcessMessage(package, INSTALLMESSAGE_PROGRESS, row);
579     msiobj_release(&row->hdr);
580
581     msi_dialog_check_messages(NULL);
582 }
583
584 void ui_actiondata(MSIPACKAGE *package, LPCWSTR action, MSIRECORD * record)
585 {
586     static const WCHAR Query_t[] = 
587         {'S','E','L','E','C','T',' ','*',' ','F','R','O','M',' ',
588          '`','A','c','t','i','o', 'n','T','e','x','t','`',' ',
589          'W','H','E','R','E',' ', '`','A','c','t','i','o','n','`',' ','=', 
590          ' ','\'','%','s','\'',0};
591     WCHAR message[1024];
592     MSIRECORD * row = 0;
593     DWORD size;
594
595     if (!package->LastAction || strcmpW(package->LastAction, action))
596     {
597         row = MSI_QueryGetRecord(package->db, Query_t, action);
598         if (!row)
599             return;
600
601         if (MSI_RecordIsNull(row,3))
602         {
603             msiobj_release(&row->hdr);
604             return;
605         }
606
607         /* update the cached actionformat */
608         msi_free(package->ActionFormat);
609         package->ActionFormat = msi_dup_record_field(row,3);
610
611         msi_free(package->LastAction);
612         package->LastAction = strdupW(action);
613
614         msiobj_release(&row->hdr);
615     }
616
617     MSI_RecordSetStringW(record,0,package->ActionFormat);
618     size = 1024;
619     MSI_FormatRecordW(package,record,message,&size);
620
621     row = MSI_CreateRecord(1);
622     MSI_RecordSetStringW(row,1,message);
623  
624     MSI_ProcessMessage(package, INSTALLMESSAGE_ACTIONDATA, row);
625
626     msiobj_release(&row->hdr);
627 }
628
629 void reduce_to_longfilename(WCHAR* filename)
630 {
631     LPWSTR p = strchrW(filename,'|');
632     if (p)
633         memmove(filename, p+1, (strlenW(p+1)+1)*sizeof(WCHAR));
634 }
635
636 LPWSTR create_component_advertise_string(MSIPACKAGE* package, 
637                 MSICOMPONENT* component, LPCWSTR feature)
638 {
639     static const WCHAR fmt[] = {'%','s','%','s','%','c','%','s',0};
640     WCHAR productid_85[21], component_85[21];
641     LPWSTR output = NULL;
642     DWORD sz = 0;
643     GUID clsid;
644
645     /* > is used if there is a component GUID and < if not.  */
646
647     productid_85[0] = 0;
648     component_85[0] = 0;
649
650     CLSIDFromString(package->ProductCode, &clsid);
651     encode_base85_guid(&clsid, productid_85);
652
653     if (component)
654     {
655         CLSIDFromString(component->ComponentId, &clsid);
656         encode_base85_guid(&clsid, component_85);
657     }
658
659     TRACE("prod=%s feat=%s comp=%s\n", debugstr_w(productid_85),
660           debugstr_w(feature), debugstr_w(component_85));
661  
662     sz = 20 + lstrlenW(feature) + 20 + 3;
663
664     output = msi_alloc_zero(sz*sizeof(WCHAR));
665
666     sprintfW(output, fmt, productid_85, feature,
667              component?'>':'<', component_85);
668     
669     return output;
670 }
671
672 /* update component state based on a feature change */
673 void ACTION_UpdateComponentStates( MSIPACKAGE *package, MSIFEATURE *feature )
674 {
675     INSTALLSTATE newstate;
676     ComponentList *cl;
677
678     newstate = feature->ActionRequest;
679
680     if (newstate == INSTALLSTATE_ABSENT)
681         newstate = INSTALLSTATE_UNKNOWN;
682
683     LIST_FOR_EACH_ENTRY( cl, &feature->Components, ComponentList, entry )
684     {
685         MSICOMPONENT* component = cl->component;
686     
687         if (!component->Enabled) continue;
688
689         TRACE("MODIFYING(%i): Component %s (Installed %i, Action %i, Request %i)\n",
690             newstate, debugstr_w(component->Component), component->Installed, 
691             component->Action, component->ActionRequest);
692         
693         if (newstate == INSTALLSTATE_LOCAL)
694         {
695             component->Action = INSTALLSTATE_LOCAL;
696             component->ActionRequest = INSTALLSTATE_LOCAL;
697         }
698         else 
699         {
700             ComponentList *clist;
701             MSIFEATURE *f;
702
703             component->hasLocalFeature = FALSE;
704
705             component->Action = newstate;
706             component->ActionRequest = newstate;
707
708             /* if any other feature wants it local we need to set it local */
709             LIST_FOR_EACH_ENTRY( f, &package->features, MSIFEATURE, entry )
710             {
711                 if ( f->ActionRequest != INSTALLSTATE_LOCAL &&
712                      f->ActionRequest != INSTALLSTATE_SOURCE )
713                 {
714                     continue;
715                 }
716
717                 LIST_FOR_EACH_ENTRY( clist, &f->Components, ComponentList, entry )
718                 {
719                     if ( clist->component == component &&
720                          (f->ActionRequest == INSTALLSTATE_LOCAL ||
721                           f->ActionRequest == INSTALLSTATE_SOURCE) )
722                     {
723                         TRACE("Saved by %s\n", debugstr_w(f->Feature));
724                         component->hasLocalFeature = TRUE;
725
726                         if (component->Attributes & msidbComponentAttributesOptional)
727                         {
728                             if (f->Attributes & msidbFeatureAttributesFavorSource)
729                             {
730                                 component->Action = INSTALLSTATE_SOURCE;
731                                 component->ActionRequest = INSTALLSTATE_SOURCE;
732                             }
733                             else
734                             {
735                                 component->Action = INSTALLSTATE_LOCAL;
736                                 component->ActionRequest = INSTALLSTATE_LOCAL;
737                             }
738                         }
739                         else if (component->Attributes & msidbComponentAttributesSourceOnly)
740                         {
741                             component->Action = INSTALLSTATE_SOURCE;
742                             component->ActionRequest = INSTALLSTATE_SOURCE;
743                         }
744                         else
745                         {
746                             component->Action = INSTALLSTATE_LOCAL;
747                             component->ActionRequest = INSTALLSTATE_LOCAL;
748                         }
749                     }
750                 }
751             }
752         }
753         TRACE("Result (%i): Component %s (Installed %i, Action %i, Request %i)\n",
754             newstate, debugstr_w(component->Component), component->Installed, 
755             component->Action, component->ActionRequest);
756     } 
757 }
758
759 UINT register_unique_action(MSIPACKAGE *package, LPCWSTR action)
760 {
761     UINT count;
762     LPWSTR *newbuf = NULL;
763
764     if (!package->script)
765         return FALSE;
766
767     TRACE("Registering %s as unique action\n", debugstr_w(action));
768     
769     count = package->script->UniqueActionsCount;
770     package->script->UniqueActionsCount++;
771     if (count != 0)
772         newbuf = msi_realloc( package->script->UniqueActions,
773                         package->script->UniqueActionsCount* sizeof(LPWSTR));
774     else
775         newbuf = msi_alloc( sizeof(LPWSTR));
776
777     newbuf[count] = strdupW(action);
778     package->script->UniqueActions = newbuf;
779
780     return ERROR_SUCCESS;
781 }
782
783 BOOL check_unique_action(const MSIPACKAGE *package, LPCWSTR action)
784 {
785     UINT i;
786
787     if (!package->script)
788         return FALSE;
789
790     for (i = 0; i < package->script->UniqueActionsCount; i++)
791         if (!strcmpW(package->script->UniqueActions[i], action))
792             return TRUE;
793
794     return FALSE;
795 }
796
797 WCHAR* generate_error_string(MSIPACKAGE *package, UINT error, DWORD count, ... )
798 {
799     static const WCHAR query[] = {'S','E','L','E','C','T',' ','`','M','e','s','s','a','g','e','`',' ','F','R','O','M',' ','`','E','r','r','o','r','`',' ','W','H','E','R','E',' ','`','E','r','r','o','r','`',' ','=',' ','%','i',0};
800
801     MSIRECORD *rec;
802     MSIRECORD *row;
803     DWORD size = 0;
804     DWORD i;
805     va_list va;
806     LPCWSTR str;
807     LPWSTR data;
808
809     row = MSI_QueryGetRecord(package->db, query, error);
810     if (!row)
811         return 0;
812
813     rec = MSI_CreateRecord(count+2);
814
815     str = MSI_RecordGetString(row,1);
816     MSI_RecordSetStringW(rec,0,str);
817     msiobj_release( &row->hdr );
818     MSI_RecordSetInteger(rec,1,error);
819
820     va_start(va,count);
821     for (i = 0; i < count; i++)
822     {
823         str = va_arg(va,LPCWSTR);
824         MSI_RecordSetStringW(rec,(i+2),str);
825     }
826     va_end(va);
827
828     MSI_FormatRecordW(package,rec,NULL,&size);
829
830     size++;
831     data = msi_alloc(size*sizeof(WCHAR));
832     if (size > 1)
833         MSI_FormatRecordW(package,rec,data,&size);
834     else
835         data[0] = 0;
836     msiobj_release( &rec->hdr );
837     return data;
838 }