[AHK v2] JSON Object, Compressed File Attachment with Autoloading and Autosaving

Post your working scripts, libraries and tools.
oif2003
Posts: 214
Joined: 17 Oct 2018, 11:43
Contact:

[AHK v2] JSON Object, Compressed File Attachment with Autoloading and Autosaving

20 Nov 2018, 05:11

Hi,

Here's a script that will autoload a JSON string from the end of the script's comment block. I also included methods to insert/extract files. Files are compressed and converted to string using Windows API (compression and cryptto). On exit, the JSON object is saved again as string to the end of the script file.

EDIT: See https://autohotkey.com/boards/viewtopic ... 67#p249588 for a newer version used in the libtcc (Tiny C Compiler) script.

Code: Select all

/*
	Storing variables/files in JSON format inside AHK Script Demo
	created by oif2003 for AutoHotkey v2 a100 on 20 Nov 2018
*/
#singleinstance force

;To use the demo simply run it twice going through the prompts each time.
;The json object is loaded automatically when the script starts and is
;saved automatically on exit.

doc.jso.run++
if Mod(doc.jso.run, 2) {
	doc.jso.firstName := InputBox("Enter your name: ")
	
	msgbox("Select file to attach")
	file := FileSelect()
	description := InputBox("Enter a file description: ")
	doc.pack(file, description)
	
	msgbox("Once script exits, view script file to see attachment.")
} else {
	msgbox("Hello " doc.jso.firstName "!")
	
	msgbox("Now we we unpack.")
	target := FileSelect(,doc.jso.file[1].2_dir "\" doc.jso.file[1].1_fileName ".test")
	doc.unpack(doc.jso.file[1].8_data, target)
	if doc.fileSHA256(target) == doc.jso.file[1].7_sha256 {
		msgbox("Success! Hash (SHA256) matches!")
	}
	
	doc.jso.file := ""
	doc.jso.firstName := ""
}

;----------------------------------------------------------------------------------------------
;by oif2003 on 19 Nov 2018, written for AutoHotkey v2 a100
;----------------------------------------------------------------------------------------------
;Basic json class, call json(x) to convert between json string and ahk Object
;Note: proper json escape sequences have not been implmented, this version only
;escapes `" (escaped double quote, AHK style)
;----------------------------------------------------------------------------------------------
json(x)=>json.auto(x)	;if x is an object it returns a json string and vice versa
;----------------------------------------------------------------------------------------------
;This json converter operates as follows:
;	If given a json string:
;	1)	Replace all string literals with serialized tokens
;	2) 	Convert json string to ahk function string consisting of array(...) and object(...)
;	3)	Replace tokens with original strings
;	4)	Evaluate the ahk function expression string and return the resulting object
;	
;	If given an ahk object:
;	1)	Recursively print output using obj2str
;	2)	isNumber and isArray are used to determine the format of the output (numbers are not quoted
;		and arrays sit inside brackets inside of braces)
;	
class json {
	;auto input parser json string <=> ahk object
	auto(input) {
		if IsObject(input) {
			return this.obj2str(input)
		} else {
			return this.str2obj(input)
		}
	}
	
	;------------------------------------------------------------------------------------------
	;convert object to json string
	obj2str(obj, firstRun := true) {
		static output := ""
		static level := 0
		static noTab := false
		if firstRun {
			output := ""
		}
		
		if isObject(obj) {
			if obj.Count() {
				if isArray(obj) {	;if this is an array (based on A_Index == key)
					output .= (noTab ? "" : tabs(level)) "[`n"
					level++
					noTab := false
					for k, v in obj {
						this.obj2str(v, false)
						output .= (k != obj.Count() ? "," : "") "`n"
					}
					output .= tabs(--level) "]"
				} else {	;otherwise output as object
					output .= (noTab ? "" : tabs(level)) "{`n"
					level++
					noTab := false
					for k, v in obj {
						output .= tabs(level) '"' k '": '
						noTab := true
						this.obj2str(v,  false)
						noTab := false
						output .= (A_Index != obj.Count() ? "," : "") "`n"
					}
					output .= tabs(--level) "}"
				}
			} else {
				output .= "[]"
			}
		} else {
			if obj == "" {
				obj := "null"
			} else if !(obj is "Number") || !isNumber(obj) {	;don't put quotes around numbers
				obj := '"' obj '"'
			}
			output .= (noTab ? "" : tabs(level)) obj
		}
		
		return output
		
		isNumber(x) {	;quickly and dirty check.  Other ideas: use is Type first then do this
			return NumGet(&x, "UInt")   == x 
				|| NumGet(&x, "Int")    == x || NumGet(&x, "Int64") == x 
				|| NumGet(&x, "Double") == x || NumGet(&x, "Float") == x 
				|| NumGet(&x, "Ptr")    == x || NumGet(&x, "UPtr") == x
				|| NumGet(&x, "Short")  == x || NumGet(&x, "UShort") == x 
				|| NumGet(&x, "Char")   == x || NumGet(&x, "UChar") == x 
		}
		
		isArray(arr) {	;another quick and dirty check: A_Index == Current Key ?
			if !IsObject(arr) {
				return false
			} else {
				for k, v in arr {
					if k != A_Index {
						return false
					}
				}
				return true
			}
		}
		
		tabs(n) {	;create tab string
			loop n {
				tab .= "`t"
			}
			return tab
		}
	}

	;------------------------------------------------------------------------------------------
	;covert json string to ahk function string then feed it thru the function parser
	str2obj(jstr) {
		funcStr := this.jstr2func(jstr)
		o := json.funcParser(strreplace(funcstr, '"'))
		return o
	}
	
	;recursively evaluate the function string
	funcParser(funcStr) {
		paren := InStr(funcStr, "(")
		if paren {
			innerStr := SubStr(funcStr, paren + 1, -1)
			parenCount := 0
			argStart := 1
			args := []
			loop parse innerStr {
				if A_LoopField == "(" {
					parenCount++
				} else if A_LoopField == ")" {
					parenCount--
				}
				
				if !parenCount && A_LoopField == "," {
					args.push(SubStr(innerStr, argStart, A_Index - argStart))
					argStart := A_Index + 1
				} else if A_Index == StrLen(innerStr) {
					args.push(SubStr(innerStr, argStart, A_Index - argStart + 1))
				}
			}
			
			for k, v in args {
				args[k] := this.funcParser(v)
			}

			return func(SubStr(funcStr, 1, paren - 1)).Call(args*)
		} else {
			this.restoreString(funcStr, this.dictionary)	;replace string tokens with their originals
			if funcStr is "Number" {
				return funcStr + 0
			} else {
				if funcStr == "null" {
					return
				} else if funcStr == "true" {
					return true
				} else if funcStr == "false" {
					return false
				} else if SubStr(funcStr, 1, 1) == '"' && SubStr(funcStr, -1, 1) == '"' {
					funcStr := SubStr(funcStr, 2, -1)	;remove quotation marks on strings
					return funcStr
				} else {
					return "Unhandled exception, check " A_ScriptName ", " A_ThisFunc ", " A_LineNumber
				}
			}
		}
	}

	;convert json string to function string in ahk. namely, arrary() and object()
	jstr2func(jstr, firstRun := true) {
		static tokenBase := 0x1abf - 1
		static commaToken := Chr(tokenBase + 1)
		static colonToken := Chr(tokenBase + 2)
		
		if firstRun {
			this.dictionary := this.tokenizeString(jstr)
			jstr := StrReplace(jstr, "`n")				;remove newline, tab, and space
			jstr := StrReplace(jstr, "`r")
			jstr := StrReplace(jstr, "`t")
			jstr := StrReplace(jstr, " ")
		}
			
		if !InStr(jstr, "[") && !InStr(jstr, "{") {
			return jstr
		} else {
			inner := findInnerMost(jstr)
			innerStr := SubStr(jstr, inner[1], inner[2] - inner[1] + 1)
			jstr := SubStr(jstr, 1, inner[1] - 1) str2func(innerStr) SubStr(jstr, inner[2] + 1)
			jstr := this.jstr2func(jstr, firstRun := false)
			
			return jstr
		}
		
		str2func(str) {		;convert string to function
			brace := InStr(str, "{")
			brack := InStr(str, "[")
			if brack {
				
				funcStr := 'Array('
				funcStrArr := StrSplit(SubStr(str, 2, -1), commaToken)
				for k, v in funcStrArr {
					funcStr .= str2func(v) (k != funcStrArr.Length() ? "," : "")
				}
				funcStr .= ")"
			} else if brace {
				funcStr := 'Object(' StrReplace(SubStr(str, 2), "}", ")") 		
				funcStr := StrReplace(funcStr, ':', ',')
			} else {
				return str
			}
			return funcStr
		}
		
		findInnerMost(str) {	;find innermost object/array
			braceCount := brackCount := maxCount := maxStart := maxEnd := 0

			loop parse str {
				if 			A_LoopField == "{" {
					braceCount++
				} else if 	A_LoopField == "}" {
					braceCount--
				} else if 	A_LoopField == "[" {
					brackCount++
				} else if 	A_LoopField == "]" {
					brackCount--
				}
				
				if braceCount + brackCount > maxCount {						;Find max cumulative count of [ and {
					maxCount := braceCount + brackCount
					maxStart := A_Index										;Update max [ / { location
					closing := SubStr(str, A_Index, 1) == "[" ? "]" : "}"
					maxEnd := InStr(str, closing, , A_Index)				;update where it ends
				}
			}
			return [maxStart, maxEnd]
		}
	}
	
	;------------------------------------------------------------------------------------------
	;helper methods
	restoreString(ByRef str, dictionary) {
		static tokenBase := 0x1abf - 1
		static strToken  := Chr(tokenBase + 3)
		static escapedQuote := Chr(tokenBase + 4)
		for k, v in dictionary {
			str := StrReplace(str, strToken k strToken, v, , 1)
		}
		str := StrReplace(str, escapedQuote, '"')
	}

	tokenizeString(ByRef str) {
		static tokenBase := 0x1abf - 1
		static strToken  := Chr(tokenBase + 3)
		static escapedQuote := Chr(tokenBase + 4)
		
		quoteCount :=  quoteStart := 0
		dictionary := []
		
		str := StrReplace(str, '``"', escapedQuote)
		
		loop parse str {
			if A_LoopField == '"' {
				quoteCount++

				if Mod(quoteCount, 2) {
					quoteStart := A_Index
				} else {
					quoteEnd := A_Index
					dictionary.push(SubStr(str, quoteStart, quoteEnd - quoteStart + 1))
				}
			}
		}
		
		for k, v in dictionary {
			str := StrReplace(str, v, strToken k strToken, , 1)
		}
		return dictionary
	}
}



;----------------------------------------------------------------------------------------------
;by oif2003 on 20 Nov 2018, written for AutoHotkey v2 a100
;----------------------------------------------------------------------------------------------
;Compress/Decompress file and converts them from binary to string so they can be stored as
;plaintext inside a script file.  This class will autoload the json object at the end of 
;this file to doc.jso.  On exit, it will save to the same string automatically (provided you
;don't crash out, of course.
;	Methods: savejson, loadjson
class document {
	static label := "/* json" " " "attachements"
	static script
	
	__New() {
		global
		static init := new document()
		if init {
			return init
		}
		this.jso:= this.loadjson()
		onExit(()=>this.savejson())
		doc := this
	}
	
	savejson() {	;dumps the current doc.jso object into json string and saves it
		this.script := SubStr(this.script, 1, InStr(this.script, this.label) - 1)
		this.script := this.script this.label "`n" json(this.jso) 
		FileOpen(A_ScriptFullPath, "w").Write(this.script)
	}
	
	loadjson() {	;loads the json string at the end of this file
		this.script := FileRead(A_ScriptFullPath)
		jstr := SubStr(this.script, InStr(this.script, this.label, true, -1) + StrLen(this.label) + 1)
		return json(jstr)
	}

	unpack(str, target) {	;unpack a file that has been stored inside the doc.jso object
		cryptString := str	;str is doc.jso.file[n].8_data 
		
		;https://docs.microsoft.com/en-us/windows/desktop/api/compressapi/nf-compressapi-createcompressor
		COMPRESS_ALGORITHM_MSZIP        := 2    ;MSZIP compression algorithm
		COMPRESS_ALGORITHM_XPRESS       := 3 	;XPRESS compression algorithm 
		COMPRESS_ALGORITHM_XPRESS_HUFF  := 4	;XPRESS compression algorithm with Huffman encoding
		COMPRESS_ALGORITHM_LZMS         := 5	;LZMS compression algorithm
		
		;first call to get buffer size
		DllCall("crypt32\CryptStringToBinary"
		   , "str", cryptString	            ;pszString
		   , "uint", 0			            ;cchString
		   , "uint", 1			            ;dwFlags
		   , "ptr", 0		    	        ;pbBinary
		   , "uint*", s	    	            ;pcbBinary
		   , "ptr", 0			            ;pdwSkip
		   , "ptr", 0			            ;pdwFlags
		)

		;set buffer size based on previous call (*2 for UTF)
		VarSetCapacity(buffer, s*2)
		DllCall("crypt32\CryptStringToBinary"
		   , "str", cryptString	            ;pszString
		   , "uint", 0			            ;cchString
		   , "uint", 1			            ;dwFlags
		   , "ptr", &buffer	    	        ;pbBinary
		   , "uint*", s	    	            ;pcbBinary
		   , "ptr", 0			            ;pdwSkip
		   , "ptr", 0			            ;pdwFlags
		)

		;Create Decompressor Handle
		DllCall("Cabinet.dll\CreateDecompressor"
		  , "UInt", COMPRESS_ALGORITHM_LZMS	    ;Algorithm,
		  , "Ptr",  0		                    ;AllocationRoutines,
		  , "Ptr*", dHandle                     ;CompressorHandle
		)

		size := s
		;first call to get buffer size (s)
		DllCall("Cabinet.dll\Decompress"
		  ,"Ptr", dHandle                   ;DecompressorHandle,
		  ,"Ptr", &buffer                   ;CompressedData,
		  ,"UInt", size                     ;CompressedDataSize,
		  ,"Ptr", &dBuffer                  ;UncompressedBuffer,
		  ,"UInt", 0                        ;UncompressedBufferSize,
		  ,"UInt*", s                       ;UncompressedDataSize
		)

		;set buffer size based on previous call
		_s := VarSetCapacity(dBuffer, s)
		DllCall("Cabinet.dll\Decompress"
		  ,"Ptr", dHandle                   ;DecompressorHandle,
		  ,"Ptr", &buffer                   ;CompressedData,
		  ,"UInt", size                     ;CompressedDataSize,
		  ,"Ptr", &dBuffer                  ;UncompressedBuffer,
		  ,"UInt", _s                       ;UncompressedBufferSize,
		  ,"UInt*", s                       ;UncompressedDataSize
		)

		DllCall("Cabinet.dll\CloseDecompressor", "Ptr", dHandle)

		FileDelete(target)
		FileOpen(target, "w").RawWrite(dbuffer,s)
		
	}

	pack(file, description) {	; packs and compresses a file, it is then stored as plaintext
		size    := FileGetSize(file)
		sha256  := this.fileSHA256(file)
		dlltext := FileRead(file, "RAW")
		_size 	:= size
		
		;https://docs.microsoft.com/en-us/windows/desktop/api/compressapi/nf-compressapi-createcompressor
		COMPRESS_ALGORITHM_MSZIP        := 2    ;MSZIP compression algorithm
		COMPRESS_ALGORITHM_XPRESS       := 3 	;XPRESS compression algorithm 
		COMPRESS_ALGORITHM_XPRESS_HUFF  := 4	;XPRESS compression algorithm with Huffman encoding
		COMPRESS_ALGORITHM_LZMS         := 5	;LZMS compression algorithm

		DllCall("Cabinet.dll\CreateCompressor"
		  , "UInt", COMPRESS_ALGORITHM_LZMS	    ;Algorithm,
		  , "Ptr",  0		                    ;AllocationRoutines,
		  , "Ptr*", cHandle                     ;CompressorHandle
		)

		;https://docs.microsoft.com/en-us/windows/desktop/api/compressapi/nf-compressapi-compress
		;first call to get buffer size (s)
		DllCall("Cabinet.dll\Compress"
		  ,"Ptr", cHandle                   ;CompressorHandle,
		  ,"Ptr", &dlltext                  ;UncompressedData,
		  ,"UInt", size                     ;UncompressedDataSize,
		  ,"Ptr", &cBuffer                  ;CompressedBuffer,
		  ,"UInt", 0                        ;CompressedBufferSize,
		  ,"UInt*", s                       ;CompressedDataSize
		)

		;set buffer size based on previous call
		_s := VarSetCapacity(cBuffer, s)
		DllCall("Cabinet.dll\Compress"
		  ,"Ptr", cHandle                   ;CompressorHandle,
		  ,"Ptr", &dlltext                  ;UncompressedData,
		  ,"UInt", size                     ;UncompressedDataSize,
		  ,"Ptr", &cBuffer                  ;CompressedBuffer,
		  ,"UInt", _s                       ;CompressedBufferSize,
		  ,"UInt*", s                       ;CompressedDataSize
		)
		
		DllCall("Cabinet.dll\CloseCompressor", "Ptr", cHandle)

		size := s
		;https://docs.microsoft.com/en-us/windows/desktop/api/wincrypt/nf-wincrypt-cryptbinarytostringw
		;first call to find size (s) needed for our crypt string
		DllCall("crypt32\CryptBinaryToString"
		   , "Ptr", &cBuffer			    ;pbBinary   ptr to array of bytes
		   , "uint", size            	    ;cbBinary   length of array
		   , "uint", 1						;dwFlags    flags: 1 = 64 bit without headers
		   , "ptr", 0				        ;pszString  when this is 0, pccString returns needed size
		   , "uint*", s			            ;pccString
		)

		VarSetCapacity(cryptString, s := s * 2)     ;*2 for unicode
		;second call to get the actual string
		DllCall("crypt32\CryptBinaryToString"
		   , "Ptr", &cBuffer			    ;pbBinary   ptr to array of bytes
		   , "uint", size            	    ;cbBinary   length of array
		   , "uint", 1						;dwFlags    flags: 1 = 64 bit without headers
		   , "str", cryptString		        ;pszString  now this is ptr to buffer of string
		   , "uint*", s     		        ;pccString  size of buffer as previously determined
		)
		
		str .= cryptString
		SplitPath(file, fileName, dir)
		this.jso.file := []
		this.jso.file[1] :=	{1_fileName:fileName, 4_uncompressedSize: _size
							, 3_compressedSize: size, 5_added: formattime(A_Now), 6_description:description
							, 2_dir:dir, 7_sha256:sha256, 8_data:str}
	}
	
	fileSHA256(file) {	;using CertUtil to get SHA256
		static cPid := 0
		if !cPid {
			_A_DetectHiddenWindows := A_DetectHiddenWindows
			A_DetectHiddenWindows := true
			Run(A_ComSpec " /k ",,"Hide", cPid)
			WinWait("ahk_pid" cPid,, 10)
			DllCall("AttachConsole","uint",cPid)
			A_DetectHiddenWindows := _A_DetectHiddenWindows
			OnExit(()=>cleanUp(cPid))
		}

		objShell := ComObjCreate("WScript.Shell")
		objExec := objShell.Exec('certutil -hashfile "' file '" SHA256')
		strStdOut:=strStdErr:=""
		while !objExec.StdOut.AtEndOfStream
			 strStdOut := objExec.StdOut.ReadAll()
		while !objExec.StdErr.AtEndOfStream
			 strStdErr := objExec.StdErr.ReadAll()

		r := strStdOut strStdErr
		SplitPath(file, fileName)
		RegExMatch(r, "(?<=" fileName ":)(.|`r|`n)*(?=CertUtil)", match)
		return match.Value(0)
		
		;cleanUp function called on script exit (OnExit)
		cleanUp(_cPid) {
			_A_DetectHiddenWindows := A_DetectHiddenWindows
			A_DetectHiddenWindows := true
			DllCall("FreeConsole", "UInt")
			WinKill("ahk_pid" _cPid)
			A_DetectHiddenWindows := _A_DetectHiddenWindows
		}
	}
}

;===========================================================================================
/* json attachements
{
	"firstName": FirstRun,
	"run": 0
}
https://github.com/oif2003/AHK_v2_64bit ... h.ahk?ts=4

Return to “Scripts and Functions (v2)”

Who is online

Users browsing this forum: wilkster and 31 guests