ReadCustomerData: Difference between revisions

From NSIS Wiki
Jump to navigationJump to search
(fix broken relative jumps)
Line 29: Line 29:
;  Call ReadCustomerData
;  Call ReadCustomerData
;  Pop $1
;  Pop $1
;  StrCmp $1 "" 0 +2
;  StrCmp $1 "" 0 +3
;  MessageBox MB_OK "No data found"
;  MessageBox MB_OK "No data found"
;  Abort
;  Abort
Line 98: Line 98:
;  Call ReadCustomerData
;  Call ReadCustomerData
;  Pop $1
;  Pop $1
;  StrCmp $1 "" 0 +2
;  StrCmp $1 "" 0 +3
;  MessageBox MB_OK "No data found"
;  MessageBox MB_OK "No data found"
;  Abort
;  Abort

Revision as of 23:44, 9 July 2009

Author: andreyvit (talk, contrib)


The problem

You want to personalize (customize, preconfigure etc) each copy of installer you give out. But, of course, you do not want to recompile the installer every time.

The solution

Append the customization data directly to the installer EXE file, prefixed with some fixed string. For example, you can use "CUSTDATA:" prefix, and then the whole customization data could read like "CUSTDATA:12;Andrey Tarantsov;www.somesite.com/myhandler".

Implementing this solution

ReadCustomerData function does exactly what you expect: it scans through the end of the installer EXE file, finds your magic string ("CUSTDATA:"), and then returns everything after this string up to the end of the file.

; ReadCustomerData ( data_prefix -> customer_data )
;   Reads string data appended to the end of the installer EXE.
;   The data must be preceded by a known string.
;   Only last 1Kb of EXE is searched for the prefix
;   (but this can be easily change, see comment below).
; Inputs:
;   data_prefix (string) -- the string after which customer data begins
; Outputs:
;   customer_data (string) -- the data after the prefix (does NOT include the prefix),
;                             empty if prefix not found
; Author:
;   Andrey Tarantsov <andreyvit@gmail.com> -- please e-mail me useful modifications you make
; Example:
;   Push "CUSTDATA:"
;   Call ReadCustomerData
;   Pop $1
;   StrCmp $1 "" 0 +3
;   MessageBox MB_OK "No data found"
;   Abort
;   MessageBox MB_OK "Customer data: '$1'"
Function ReadCustomerData
  ; arguments
  Exch $R1            ; customer data magic value
  ; locals
  Push $1             ; file name or (later) file handle
  Push $2             ; current trial offset
  Push $3             ; current trial string (which will match $R1 when customer data is found)
  Push $4             ; length of $R1
 
  ; get intaller file name
  System::Call 'kernel32::GetModuleFileNameA(i 0, t .r1, i 1024) i r2'
 
  FileOpen $1 $1 r
 
; change 1024 here to, e.g., 2048 to scan the last 2Kb of EXE file
  IntOp $2 0 - 1024
  StrLen $4 $R1
 
loop:
  FileSeek $1 $2 END
  FileRead $1 $3 $4
  StrCmp $3 $R1 found
  IntOp $2 $2 + 1
  IntCmp $2 0 loop loop
 
  StrCpy $R1 ""
  goto fin
 
found:
  IntOp $2 $2 + $4
  FileSeek $1 $2 END
  FileRead $1 $3
  StrCpy $R1 $3
 
fin:
  Pop $4
  Pop $3
  Pop $2
  Pop $1
  Exch $R1
FunctionEnd

Performance

The simplest implementation of ReadCustomerData, provided by Andrey, searches the end of the file 1 byte at a time for the magic data prefix. It is often possible to advance the scanning process more quickly, by checking for substrings of the magic data prefix within the trial buffer. A more complex implementation can therefore progress more quickly and so may be approprate if it is necessary to scan a larger chunk of the EXE.

; ReadCustomerData ( data_prefix -> customer_data )
;   Reads string data appended to the end of the installer EXE.
;   The data must be preceded by a known string.
;   Only last 4Kb of EXE is searched for the prefix
;   (but this can be easily changed, see comment below).
; Inputs:
;   data_prefix (string) -- the string after which customer data begins
; Outputs:
;   customer_data (string) -- the data after the prefix (does NOT include the prefix),
;                             empty if prefix not found
; Author:
;   Andrey Tarantsov <andreyvit@gmail.com> -- please e-mail me useful modifications you make
;   Stephen White <swhite-nsiswiki@corefiling.com>
; Example:
;   Push "CUSTDATA:"
;   Call ReadCustomerData
;   Pop $1
;   StrCmp $1 "" 0 +3
;   MessageBox MB_OK "No data found"
;   Abort
;   MessageBox MB_OK "Customer data: '$1'"
Function ReadCustomerData
  ; arguments
  Exch $R1            ; customer data magic value
  ; locals
  Push $1             ; file name or (later) file handle
  Push $2             ; current trial offset
  Push $3             ; current trial string (which will match $R1 when customer data is found)
  Push $4             ; length of $R1
  Push $5             ; half length of $R1
  Push $6             ; first half of $R1
  Push $7             ; tmp
 
  ; get intaller file name
  System::Call 'kernel32::GetModuleFileNameA(i 0, t .r1, i 1024) i r2'
 
  FileOpen $1 $1 r
 
; change 4096 here to, e.g., 2048 to scan just the last 2Kb of EXE file
  IntOp $2 0 - 4096
 
  StrLen $4 $R1
 
  IntOp $5 $4 / 2
  StrCpy $6 $R1 $5
 
 
loop:
  FileSeek $1 $2 END
  FileRead $1 $3 $4
  StrCmpS $3 $R1 found
 
  ${StrLoc} $7 $3 $6 ">"
  StrCmpS $7 "" NotFound
    IntCmp $7 0 FoundAtStart
      ; We can jump forwards to the position at which we found the partial match
      IntOp $2 $2 + $7
      IntCmp $2 0 loop loop
FoundAtStart:
    ; We should make progress
    IntOp $2 $2 + 1
    IntCmp $2 0 loop loop
NotFound:
    ; We can safely jump forward half the length of the magic
    IntOp $2 $2 + $5
    IntCmp $2 0 loop loop
 
  StrCpy $R1 ""
  goto fin
 
found:
  IntOp $2 $2 + $4
  FileSeek $1 $2 END
  FileRead $1 $3
  StrCpy $R1 $3
 
fin:
  Pop $7
  Pop $6
  Pop $5
  Pop $4
  Pop $3
  Pop $2
  Pop $1
  Exch $R1
FunctionEnd

How to parse the returned data

ReadCustomerData returns simply a string. When I need to split it into individual fields, I first declare variables to hold these fields:

var customer_name
var customer_account
var customer_cookie
var customer_role
var server_url

and then use the following code. (It calls ReadCSV and Trim functions, more on them below.)

; ParseCustomerData ( customer_data -> )
;   Parses semicolon-separated customer data into individual fields
Function ParseCustomerData
  Exch $R1          ; customer data, then item count
  Push $1           ; current item index (0-based)
  Push $2           ; current item
 
  Push $R1
  Call ReadCSV
  Pop $R1
 
  StrCpy $1 0
loop:
  IntCmp $1 $R1 done
  Call Trim
  Pop $2
 
; TODO: add dispatching code here
  IntCmp $1 0 item0
  IntCmp $1 1 item1
  IntCmp $1 2 item2
  IntCmp $1 3 item3
  IntCmp $1 4 item4
 
  MessageBox MB_OK|MB_ICONEXCLAMATION "There are too many items in customer data; extra items ignored."
  goto done
 
; TODO: add saving code here
item0:
  StrCpy $customer_name $2
  goto ok
item1:
  StrCpy $customer_account $2
  goto ok
item2:
  StrCpy $customer_cookie $2
  goto ok
item3:
  StrCpy $customer_role $2
  goto ok
item4:
  StrCpy $server_url $2
  goto ok
 
ok:
  IntOp $1 $1 + 1
  Goto loop
done:
  Pop $2
  Pop $1
  Pop $R1
FunctionEnd

ReadCSV and Trim functions

They used to be described in this Wiki, but somehow seem to be gone now. This is very unfortunate, as these are very convenient functions. So I just reproduce them here. I'm am NOT an author of these functions, and I would be glad to remove the code and replace it with links to them.

; Trim
;   Removes leading & trailing whitespace from a string
; Usage:
;   Push
;   Call Trim
;   Pop
Function Trim
	Exch $R1 ; Original string
	Push $R2
 
Loop:
	StrCpy $R2 "$R1" 1
	StrCmp "$R2" " " TrimLeft
	StrCmp "$R2" "$\r" TrimLeft
	StrCmp "$R2" "$\n" TrimLeft
	StrCmp "$R2" "	" TrimLeft ; this is a tab.
	GoTo Loop2
TrimLeft:
	StrCpy $R1 "$R1" "" 1
	Goto Loop
 
Loop2:
	StrCpy $R2 "$R1" 1 -1
	StrCmp "$R2" " " TrimRight
	StrCmp "$R2" "$\r" TrimRight
	StrCmp "$R2" "$\n" TrimRight
	StrCmp "$R2" "	" TrimRight ; this is a tab
	GoTo Done
TrimRight:
	StrCpy $R1 "$R1" -1
	Goto Loop2
 
Done:
	Pop $R2
	Exch $R1
FunctionEnd
Function  ReadCSV
		Exch	$1	# input string (csv)
		Push	$2	# substring of $1: length 1, checked for commata
		Push	$3	# substring of $1: single value, returned to stack (below $r2)
		Push	$r1	# counter: length of $1, number letters to check
		Push	$r2	# counter: values found, returned to top of stack
		Push	$r3	# length: to determinate length of current value
		StrLen	$r1  $1
		StrCpy	$r2  0
		StrLen	$r3  $1
	loop:
		IntOp	$r1  $r1 - 1
		IntCmp	$r1  -1  text  done
		StrCpy	$2  $1  1  $r1
		StrCmp	$2  ";"  text
		Goto	loop
	text:
		Push	$r1
		IntOp	$r1  $r1 + 1
		IntOp	$r3  $r3 - $r1
		StrCpy	$3  $1  $r3  $r1
		Pop	$r1
		StrCpy	$r3  $r1
		IntOp	$r2  $r2 + 1
		Push	$3
		Exch	6
		Exch	5
		Exch	4
		Exch	3
		Exch
		Goto	loop
	done:
		StrCpy	$1  $r2
		Pop	$r3
		Pop	$r2
		Pop	$r1
		Pop	$3
		Pop	$2
		Exch	$1
FunctionEnd