Friday, December 21, 2007

Code Signing Your VFP EXEs

John Robbins has a great post on why and how to digitally sign your EXEs. As he mentions, this is very important, especially on Windows Vista, if you want to have a professional looking application.

I've been using code signing for most of this year. We actually not only sign our application's EXE but also the installer so the user doesn't get the "unknown publisher" dialog when they install it.

Thursday, December 13, 2007

Sorting in Natural Order

Jeff Atwood discussed "natural sorting" yesterday in his Coding Horror blog (a blog I highly recommend, by the way). This is something I've needed to do every now and then, so I decided it was time to write a function to do that. The code is shown below. Note that this isn't necessarily the most elegant solution; I used the "brute force" method, so I appreciate any comments about tightening it up.
*==============================================================================
* Function: NaturalSort
* Purpose: Sorts an array in natural rather than ASCII order
* Author: Doug Hennig
* Last Revision: 12/13/2007
* Parameters: taArray - the array to sort (passed by reference)
* tnColumn - the column to sort on (optional: if it isn't
* specified, column 1 is used)
* tlDescending - .T. to sort in descending order; if .F. or
* not specified, ascending order is used
* Returns: .T. if it succeeded or .F. (and an error is raised) if
* invalid parameters are passed or the specified column
* doesn't contain a homogeneous data type
* Environment in: none
* Environment out: none
*==============================================================================
 

lparameters taArray, ;
tnColumn, ;
tlDescending
local lnColumn, ;
lnRows, ;
lnCols, ;
lnOrder, ;
laArray[1], ;
lnI, ;
lcKey, ;
llInNumeric, ;
lcString, ;
lnJ, ;
lcChar, ;
llNumeric, ;
lcNumeric, ;
laClone[1], ;
lnIndex
 

* Define some constants.
 

#define cnLENGTH 20
&& the length to pad numeric sections to
#define cnERR_ARGUMENT_INVALID 11
&& Function argument value, type, or count is invalid
#define cnERR_DATA_TYPE_MISMATCH 9
&& Data type mismatch
 

* Ensure taArray is an array and tlDescending is logical if it's specified.
 

if type('taArray', 1) <> 'A' or ;
(pcount() = 3 and vartype(tlDescending) <> 'L')
error cnERR_ARGUMENT_INVALID
return .F.
endif type('taArray', 1) <> 'A' ...
 

* If the column to sort on wasn't specified, assume 1.
 

lnColumn = iif(pcount() = 2, tnColumn, 1)
 

* Figure out the size of the source array.
 

lnRows = alen(taArray, 1)
lnCols = alen(taArray, 2)
 

* Ensure the column to sort on is valid.
 

if vartype(lnColumn) <> 'N' or not between(lnColumn, 1, max(lnCols, 1))
error cnERR_ARGUMENT_INVALID
return .F.
endif vartype(lnColumn) <> 'N' ...
 

* Figure out the order flag for ASORT().
 

lnOrder = iif(tlDescending, 1, 0)
 

* Get the data type of the first key value. If it isn't character, we don't
* have to do anything fancy; ASORT() will take care of it for us.
 

if vartype(taArray[1, lnColumn]) = 'C'
 

* Create an array we'll sort on.
 

dimension laArray[lnRows, 2]
 

* Go through each element we're sorting on.
 

for lnI = 1 to lnRows
lcKey = taArray[lnI, lnColumn]
 

* Bug out if the data type is different.
 

if vartype(lcKey) <> 'C'
error cnERR_DATA_TYPE_MISMATCH
return .F.
endif vartype(lcKey) <> 'C'
 

* Create a key that will sort properly by looking for numeric sections and
* left-padding them with zeros.
 

llInNumeric = .F.
lcString = ''
for lnJ = 1 to len(lcKey)
lcChar = substr(lcKey, lnJ, 1)
llNumeric = isdigit(lcChar) or ;
(lcChar = '.' and isdigit(substr(lcKey, lnJ + 1, 1)))
do case
case llNumeric and llInNumeric
&& if we have a digit and we're already in a numeric
&& section, add to the numeric part
lcNumeric = lcNumeric + lcChar
case llNumeric
&& if we have a digit and we're not in a numeric
&& section, flag that we are in such a section and add to
&& the numeric part
llInNumeric = .T.
lcNumeric = lcChar
case llInNumeric
&& we don't have a digit and we were in a numeric section
&& so pad the section and add it to our string
llInNumeric = .F.
lcString = lcString + padl(lcNumeric, cnLENGTH, '0') + ;
lcChar
otherwise
lcString = lcString + lcChar
endcase
next lnJ
 

* Finish the string if we were still processing a numeric section.
 

if llInNumeric
lcString = lcString + padl(lcNumeric, cnLENGTH, '0')
endif llInNumeric
 

* Store the new key and the original index in our sort array.
 

laArray[lnI, 1] = lcString
laArray[lnI, 2] = lnI
next lnI
 

* Now sort the array and reorder the values in the source array by cloning it
* and copying the values from the each row in the clone to the new row in the
* source array.
 

asort(laArray, 1, -1, lnOrder, 1)
acopy(taArray, laClone)
for lnI = 1 to lnRows
lnIndex = laArray[lnI, 2]
if lnCols > 0
for lnJ = 1 to lnCols
taArray[lnI, lnJ] = laClone[lnIndex, lnJ]
next lnJ
else
taArray[lnI] = laClone[lnIndex]
endif lnCols > 0
next lnI
 

* Just use ASORT to do the job.
 

else
asort(taArray, lnColumn, -1, lnOrder, 0)
endif vartype(taArray[1, lnColumn]) = 'C'
return .T.

Wednesday, December 05, 2007

Where Should Data go in Vista?

In part 1 of my two-part Advisor article on Windows Vista (http://advisor.com/doc/18897), I discussed some of the recommended places writable files should go in Vista. (To refresh your memory, your program files folder is most likely read-only even for administrators, so you can't store anything that needs to be updated there, such as your application's tables, INI files, etc.) However, I've recently changed where I store some writable data to a Data subdirectory of the program folder. The reason: I need a consistent location to look for global (that is, not user-specific) data. If the user installs the app on a server, I can't use a location like {CommonAppData} because that's on the user's hard drive. I need a location I can always count on regardless of whether the app's on a local or network drive.

"Wait a minute," you're probably thinking. "That's (1) bad practice and (2) not allowed." Although I agree it's not an ideal location, it's not a completely bad practice. The reason for the program folder being read-only is to prevent malware from attacking your EXE and DLL files. Putting only data files into a Data subdirectory doesn't open any security holes because no executables go there.

As for not being allowed, that's true by default. However, you can easily change permissions on the folder when your installer creates it. In Inno Setup, simply create your directory like this:
[Dirs]
Name: "{app}\Data"; Permissions: everyone-modify
This gives all users the ability to write to the folder.

I don't store much in Data, certainly not the data files my application uses. The main thing stored there is an INI file that contains a setting specifying the location of the data files. When the user starts the app for the first time, a dialog prompts them for the data file location and writes the response to the INI file (which it can do because the INI file is in the writable Data folder). Our app looks in a consistent writable location for the INI file, making the code easy.