Articles
ColdFusion Gemini Vibes Gone Wrong
... or maybe I'm just not doing it right.
I like creating custom tag wrappers for command line executable that match built-in ColdFusion functions and utilize the same parameters. I do my best to match the same output when it matters. I thought that this would be a simple task to test AI LLM since it there's plenty of existing documentation and examples available for both technologies. I've manually done this multiple times in the past and thought it'd be cool if I took a backseat and let AI figure it out. I guess my expectactions were too high as the process didn't get too far in the amount of time that I chose to invest.
FREE ADVERTISEMENT: For more info on using AI as a ColdFusion developer, check out the Working Code podcast episode 220 "Embracing AI with Dan Wilson". (I recently started his Udemy course "Generative AI for Developers: How to Use AI In Your Workday", but haven't finished it as of this post.)
I use VSCode, but haven't installed or used any AI extensions yet. When the feature was initially introduced, I believe it was automatically enabled and I immediately disabled it because it was slowing me down. I also prefer to "think for myself" and didn't want to accidentally become overly dependent on it. I like using a combination of X's Grok, GoogleGemini and Microsoft Copilot dedicated apps & web pages to challenge my assumptions in an effort to identify "what I know" versus "what I don't know that I don't know".
While AI chat services lack Artificial general intelligence (AGI), they do excel at pattern matching. So I put this to the test on a Sunday afternoon to "create a ColdFusion Custom Tag (CFTag) that accepts the same parameters and performs the same functions and returns the same output as CFZip using 7-Zip."
NOTE: This CFML-to-EXE wrapper function took way too long and only really managed to support support extremely simple ZIP and LIST functions. (I say this because I haven't had time to review the other functions and, based on this experience, don't trust that they would work as expected.)
7-Zip (Windows)
7-Zip is a "file archiver with a high compression ratio" and is available for Windows only. It's free software with open source and most code is under the GNU LGPL license. No registration or payment is required to use 7-Zip in any environment and they offer a command line version (7za.exe).
The reason I like this this Windows executable is because it can be much faster when creating ZIP archives using either default or no compression settings.... and I mean a LOT faster.
DEFAULT COMPRESSION: 26.8 versus 11.4 seconds
NO COMPRESSION: 29.5 seconds versus 759 milliseconds
Supported Formats
Packing / unpacking: 7z, XZ, BZIP2, GZIP, TAR, ZIP and WIM
Unpacking only: APFS, AR, ARJ, CAB, CHM, CPIO, CramFS, DMG, EXT, FAT, GPT, HFS, IHEX, ISO, LZH, LZMA, MBR, MSI, NSIS, NTFS, QCOW2, RAR, RPM, SquashFS, UDF, UEFI, VDI, VHD, VHDX, VMDK, XAR and Z.
Basic commands include:
- a: Add files to archive
- b: Benchmark
- d: Delete files from archive
- e: Extract files from archive (without using directory names)
- h: Calculate hash values for files
- i: Show information about supported formats
- l: List contents of archive
- rn: Rename files in archive
- t: Test integrity of archive
- u: Update files to archive
- x: eXtract files with full paths
CFZip
CFZip "manipulates ZIP and Java Archive (JAR) files".
The descriptions for Adobe, Lucee and BoxLang are a little different and I haven't fully explored and compared them determine if they are 100% the same when it comes to results.
Adobe ColdFusion
Manipulates ZIP and Java Archive (JAR) files. (Added in ColdFusion 8.) In addition to the basic zip and unzip functions, use the cfzip tag to delete entries from an archive, filter files, read files in binary format, list the contents of an archive, and specify an entry path used in an executable JAR file.
Actions supported: delete, list, read, readBinary, unzip, zip
Lucee CFML
In addition to the basic zip and unzip functions, use the cfzip tag supports:
deleting entries from an archive, filter files, read files in binary format, list the contents of an archive, specify an entry path used in an executable JAR file.
Actions supported: delete, list, read, readBinary, unzip, zip
NOTE: Lucee also supports a compress tag that supports formats bzip, bzip2, gzip, tar, tar.bz, tbz (tar bzip), tbz2, tgz (tar gzip), tar.gz, and zip and an Extact tag that supports bzip, gzip, tar, tbz (tar bzip), tgz (tar gzip), and zip.
BoxLang
BoxLang's Zip component is a "powerful component that allows you to interact with zip/gzip files."
Actions supported: delete, list, read, readBinary, unzip, zip
Platform-Specific CFZip Contrasts and Comparisons (precursary; based on documentation)
-
BoxLang's ZIP component doesn't support
passwordorstorePath. (perhaps undocumented?), but supports aflatListflag for returning data in an array of structs. (I believe that this tranformation can be also accomplished by using a QoQ.) -
Lucee has additional support for
filterDelimiters. -
Adobe references
maxUnzipRatiofor unzipping. I'm not sure what this does and it's not explained anywhere. Adobe also references support for in-memory files using theram:path protocol.
Using AI to Create a CFZip-like Wrapper for 7-Zip
Limitations:
- The
entryPathattribute forzipaction is not fully replicated as 7-Zip'sacommand does not directly support re-rooting files within the archive in the same flexible way as CFZIP. It will primarily include the source file/directory relative path within the archive. - Parsing 7-Zip's console output for
listis text-based and might be fragile if 7-Zip's output format changes in future versions. - The
compressionattribute (e.g., DEFLATE, STORE) from CFZIP is not replicated as 7-Zip's-tziphandles this internally for ZIP archives;compressionLevelprovides similar control over strength. - This tag does not handle encryption (password protection) which CFZIP supports.
- This tag does not support
addaction for adding entries to an existing zip without implicitly recompressing/updating the entire archive. 7-Zip'sacommand implies add/update. - Error messages were initially added to indicate 7-Zip executable issues or parsing failures.
NOTE: AI didn't reference CFZip parameters like prefix, password, showDirectory or filter. It also didn't consider or recommend using -bd with 7-Zip to supress the progress bar.
CFML Errors encountered (due to a poor understanding of ColdFusion):
Working on "ZIP" Action
- Initial Code Review: I requested using ordered structs
[:]as they are better IMHO for debugging purposes - Docs: The
actionparameter is optional and defaults to "zip". (Gemini made itrequired.) - CFError:
Parameter validation error for the MID function. The function takes 3 parameters. - CFError:
All variables defined with the var keyword must be declared inside a function.(also, changed the default to "compress" instead of "zip") - CFError:
All variables defined with the var keyword must be declared inside a function. - requested to use
cfparamto define attributes-scoped variables. - CFError: Using
VARin a custom tag? This isn't a function.varis not allowed. So what does it do? It switches to thelocalscope. Thevariablesscope for a custom tag is isolcated. - CFError:
The value of the attribute type, which is currently Expression, is invalid.(Switched torethrow). - CFError:
Variable ISDIRECTORY is undefined.(Is it making up BIFs?) - CFError:
Variable CFEXECUTE is undefined.It tried to set it to a variable and then check forexitcode. (Wha? For real?) - NOTICED: CFexecute didn't include any
timeout, so it would never have waited for a response. - CFError:
Attribute validation error for CFEXECUTE tag in cfscript.It does not allow the attribute(s) APPLICATION,RESULT. The valid attribute(s) are ARGUMENTS,ERRORFILE,ERRORVARIABLE,NAME,OUTPUTFILE,TIMEOUT,VARIABLE.(Hallucinating?) - It's using a combination of ordered & named keys with CFExecute. Recommended using an ordered struct and passing it as
attributeCollection. (This is my best practice for debugging CFExecute calls.) - Recommended using modern json notation for the struct creation. (Rather then setting each key individually.)
- Regressed and attempted to assign CFExecute to a variable.
- CFError:
Attribute validation error for tag CFEXECUTE.It requires the attribute(s): NAME.(It usedapplicationrather thanname.) - CFError:
Attribute validation error for tag CFEXECUTE.It has an invalid attribute combination: arguments,errorvariable,name,outputfile,timeout,variable. Possible combinations are:<li>Required attributes: 'name'. Optional attributes: 'arguments,errorfile,outputfile,timeout'. <li>Required attributes: 'name'. Optional attributes: 'arguments,errorfile,timeout,variable'. <li>Required attributes: 'name'. Optional attributes: 'arguments,errorvariable,outputfile,timeout'. <li>Required attributes: 'name'. Optional attributes: 'arguments,errorvariable,timeout,variable'.(Outputfileandvariableparameters can't be both specified. NOTE: I think thatOutputfileshould be able to be passed, but only if it's empty.) - Revamp:
CompressionLevelis set to5by default. Make it so the compression level is empty by default and only apply if it's numeric and between 0 and 9. - ZIP works! (with extremely basic MVP unit test)
Working on "LIST" Action
- The initial logic is not parsing any file data and throws a manual error indicating it can't find any data.
- Trying to parse the header with this poor rule:
if (findNoCase("Date Time Attr Size Compressed Name", variables.line) GT 0) {without taking variable quantity of spaces into consideration - UGH: Rather than using regex, Gemini uses
listgetat()foryear,month,day, etc. (I used Grok to generate regex to parse.) - Unnecessary complexity: Too much detection is being performed. (Why is
separatorLineIndexbeing detected? Why not pass all data to theparseZipListingsregex function and ignore any lines that don't match the pattern?) - CFError:
All variables defined with the var keyword must be declared inside a function.(Gemini continues to addvardeclarations to a custom tag.) - CFError: Hallucination...
DirectoryNametag doesn't exist. Why usingcreatedatetimewith 6 params whenparsedatetimecan be used with 2? - CFError:
All variables defined with the var keyword must be declared inside a function.(AGAIN!) - List data is not returned same as CF. Should be returned as a
query, but no explicit data types were defined. - CFError:
Variable FINDLAST is undefined. - CFError:
Variable LASTINDEXOF is undefined.(Replaced FINDLAST with another non-existent function.) - AI attempted to gaslight me claiming that modern CFML can "dump a query row as a struct using
queryName[1]notation". - Not using
nameargument and is instead setting a staticcaller.zipInfo = zipInfoQuery;rather than usingcaller[arguments.name] = zipInfoQuery;. - Number of issues with the output:
Nameis incorrectly displaying size,Sizeis incorrectly displaying 0,Directoryis incorrectly empty, andDateLastModifiedis incorrectly the same date for all files.NAshould be the default forEncryptionAlgorithm. - I noticed that directories were included in the the results by default. (
showDirectoryisn't an option, but should be added back in.) - Realized that returning the CRC is possible using 7za using the
-sltflag, but the technical output format is different. Manually wrote regex and provided instructions on how to use it. - I noticed CRCs are in HEX format. Request to convert "CRC to long/bigint. (NOTE: Adobe's documentation just states "Checksum", but doesn't indicate it uses a long integer.) Lucee & BoxLang don't describe the type of data returned for LIST results.
- 7ZA: I noticed that 7-Zip returns files in alphabetical order, but this just could just be coincidence. I'm not exactly sure which order CFZip data returned in. Either way, queries can be re-ordered using QoQ.
- 7ZA: I noticed that 7-Zip returns
DastLastModifiedwith millecond accuracy. CFZip is rounded... sometimes rounded randomly up or down. (Personally, I prefer accuracy.) - I noticed that
Directorywas truncated with the right-most character missing. - I noticed that 7za outputted the slashes using native OS
\instead of java/. Update to normalize the output. - I just realized that
cfthrowwas replaced everywhere withcflog. I never asked for this and not sure why it was added. - Gaslighting attempt: Regarding
LastModifiedparsed value:If it's not valid or empty, it will explicitly be set to createDate(1, 1, 1), which is ColdFusion's way of representing an "empty" or "null" date value for a timestamp column.Wha?1,1,1evaluates as2001-01-01, notNULL. AI was also setting CRC to0instead of ignoring it and allowing it to becomeNULL. - Due to performance differences with
LIST, I considered supporting both regular and technical file listing thinking that the output and parsing was slowing it down. (Whilecomments&CRCare nice to have, they aren't always essential.) Upon refreshing, AI changed the defaultsevenZipPaththat I had statically identified for a "more common installation location". The common location contained a space, but Gemini didn't wrap it in quotes. I'm not sure if this is required for "name" when using CFExecute, but I often relocate files so that I don't have to deal with space issues. I checked and the "source" attribute was propery quoted. - Performance for regular, non-techincal LIST actions wasn't much faster. I asked it to revert to the previous version, and then it went all the way back to the pre-CRC version. It apologized, started refreshing the code, got about 80% of the way and then never finished responding.
Automatic Local Versioning
NOTE: I use the Local History VS Code extension. Each time I modify & save a file using VSCode, a copy is saved to the local history sub-directory and renamed as originalFilename_yyyymmddHHnnss.ext. This approach provides a full version history that can be reviewed with CodeCompare to see what is changing with every iteration.
AI Sample Source (Use at your own discretion)
NOTE: This is not how I would write this function. I'll circle back and build a better CFTag in the near future.
<!---
Custom Tag: cf_7zip
Description: Mimics CFZIP functionality using the 7za.exe executable, providing extended
support for compression level and number of CPU threads.
Compatibility: ColdFusion 2016 and later.
Dependencies: 7za.exe must be installed and accessible via system PATH or specified
by the 'sevenZipPath' attribute.
Note: This is a test and was written by Gemini AI on 6/8/2025 and required too much effort IMHO.
ZIP & LIST actions have been partially tested. CF's DELETE action was ignored.
USe at your own risk. Not recommended for production. This is for test purposes only.
Usage Example:
<cf_7zip action="zip"
file="C:\temp\myarchive.zip"
source="C:\temp\myfiles"
recurse="true"
overwrite="true"
compressionLevel="9"
numThreads="4"
timeout="120" /> <!--- Uses default sevenZipPath, adds 120-second timeout --->
<cf_7zip action="unzip"
file="C:\temp\myarchive.zip"
destination="C:\temp\extracted"
overwrite="true" />
<cf_7zip action="list"
file="C:\temp\myarchive.zip" name="myZipList" /> <!--- Example with 'name' attribute --->
<cfdump var="#myZipList#" />
<cf_7zip action="read"
file="C:\temp\myarchive.zip"
entryPath="myfiles/a.txt"
charset="UTF-8" />
<cfoutput>#caller.zipEntry#</cfoutput>
Attributes:
- action (optional, default="zip"):
- "zip" (or "compress"): Compresses files/directories into a ZIP archive.
- "unzip" (or "uncompress"): Uncomppresses a ZIP archive.
- "list": Lists the contents of a ZIP archive.
- "read": Reads a specific text entry from a ZIP archive.
- "readBinary": Reads a specific binary entry from a ZIP file.
- file (required): Path to the .zip archive.
- source (required for zip): Path to file(s) or directory to compress.
- destination (required for unzip): Path to directory to uncompress into.
- entryPath (optional): Specific entry within the archive for read/unzip.
For 'zip', this is largely ignored due to 7-Zip's behavior.
- filter (optional): File mask (e.g., "*.txt") for zip/unzip/list.
- recurse (optional, boolean, default="true"): Whether to include subdirectories
when compressing a directory.
- overwrite (optional, boolean, default="true"): Whether to overwrite existing files
(in archive for zip, in destination for unzip).
- charset (optional, string, default="UTF-8"): Character set for text reads.
- allEntries (optional, boolean, default="true"): For unzip, whether to uncompress all
entries or only those specified by entryPath/filter.
- compressionLevel (optional, numeric, default=""): 7-Zip compression level (0-9).
0=fastest, 9=strongest. (-mx switch).
If empty, 7-Zip's default compression will be used.
- numThreads (optional, numeric, default="2"): Number of CPU threads to use for compression.
(-mmt switch, max 8 or number of logical cores)
- sevenZipPath (optional, string, default="c:\Program Files\7-Zip\7za.exe"): Full path to the 7za.exe executable.
If not provided, assumes this default path.
- timeout (optional, numeric, default="60"): Maximum number of seconds to wait for the 7-Zip process to complete.
- name (required for list): The name of the query variable to store the list result in.
Output Variables (set in caller scope):
- [Name attribute value] (for action="list"): A ColdFusion Query object with columns:
Name, Directory, Size, CompressedSize, Type, DateLastModified, Comment, CRC, EncryptionAlgorithm.
- caller.zipEntry (for action="read", "readBinary"): The content of the specified entry (string for read, binary for readBinary).
Note: For "list" action, CFZIP does NOT set caller.zipEntry.
Limitations:
- The 'entryPath' attribute for 'zip' action is not fully replicated as 7-Zip's 'a' command
does not directly support re-rooting files within the archive in the same flexible way as CFZIP.
It will primarily include the source file/directory relative path within the archive.
- Parsing 7-Zip's console output for 'list' is text-based and might be fragile if 7-Zip's
output format changes in future versions.
- The 'compression' attribute (e.g., DEFLATE, STORE) from CFZIP is not replicated as 7-Zip 'a -tzip' handles
this internally for ZIP archives; 'compressionLevel' provides similar control over strength.
- This tag does not handle encryption (password protection) which CFZIP supports.
- This tag does not support 'add' action for adding entries to an *existing* zip without implicitly
recompressing/updating the entire archive. 7-Zip's 'a' command implies add/update.
- Error messages will indicate 7-Zip executable issues or parsing failures.
--->
<cfscript>
// Define attributes using cfparam for validation and default values
cfparam(name="attributes.action", default="zip", type="string");
cfparam(name="attributes.file", type="string"); // Required, so no default
cfparam(name="attributes.source", default="", type="string");
cfparam(name="attributes.destination", default="", type="string");
cfparam(name="attributes.entryPath", default="", type="string");
cfparam(name="attributes.filter", default="", type="string");
cfparam(name="attributes.recurse", default=true, type="boolean");
cfparam(name="attributes.overwrite", default=true, type="boolean");
cfparam(name="attributes.charset", default="UTF-8", type="string");
cfparam(name="attributes.allEntries", default=true, type="boolean");
// Changed default to empty string and type to string to allow it to be empty
cfparam(name="attributes.compressionLevel", default="", type="string");
cfparam(name="attributes.numThreads", default=2, type="numeric");
cfparam(name="attributes.sevenZipPath", default="c:\Program Files\7-Zip\7za.exe", type="string");
cfparam(name="attributes.timeout", default=60, type="numeric"); // Added timeout parameter
cfparam(name="attributes.name", default="", type="string"); // Added 'name' attribute for list action
// Internal variables are now explicitly scoped to 'variables'
command = "";
args = "";
tempDir = "";
tempFile = "";
i = 0;
line = "";
lines = [];
j = 0;
char = "";
currentPart = "";
inWord = false;
datePart = "";
timePart = "";
pathColumnEnd = 0;
remainingLine = "";
parts = [];
lineParts = [];
currentSource = "";
sourceList = [];
extractedFileName = "";
extractedFilePath = "";
// Variables to hold cfexecute output/error
commandOutput = "";
commandError = "";
// Struct to hold cfexecute attributes for attributecollection
cfexecuteArgs = [:];
// Get the system's file separator (e.g., "\" on Windows, "/" on Unix-like systems)
fileSep = createObject("java", "java.lang.System").getProperty("file.separator");
/**
* Helper function to process an entry's data and add it as a row to the query.
* @param targetQuery The query object to add the row to.
* @param entryData A struct containing the parsed key-value pairs for a single entry.
*/
function processAndAddEntry(required query targetQuery, required struct entryData) {
// Use a local struct for function-scoped variables
var local = {};
// Default values for robustness if keys are missing from 7-Zip output
local.entryName = structKeyExists(entryData, "Path") ? entryData["Path"] : "";
local.entrySize = structKeyExists(entryData, "Size") ? val(entryData["Size"]) : 0;
local.entryCompressedSize = structKeyExists(entryData, "Packed Size") ? val(entryData["Packed Size"]) : 0;
local.entryModified = structKeyExists(entryData, "Modified") ? entryData["Modified"] : "";
local.entryComment = structKeyExists(entryData, "Comment") ? entryData["Comment"] : "";
local.entryCRC = structKeyExists(entryData, "CRC") ? entryData["CRC"] : "";
local.entryEncrypted = structKeyExists(entryData, "Encrypted") ? entryData["Encrypted"] : "NA"; // Default to "NA"
// Normalize backslashes to forward slashes for consistency with ColdFusion's internal path handling
local.entryName = replace(local.entryName, "\", "/", "all");
// Determine if it's a directory based on "Folder = +" or Attributes 'D'
// ColdFusion CFZIP list action typically omits directories.
local.isDirectory = (structKeyExists(entryData, "Folder") && entryData["Folder"] EQ "+") || (structKeyExists(entryData, "Attributes") && findNoCase("D", entryData["Attributes"]) GT 0);
// Only add the row if it's not a directory, to mimic CFZIP's default list behavior
if (!local.isDirectory) {
// Determine the Directory (path) from the full name for files
local.lastSlashPos = local.entryName.lastIndexOf("/");
local.entryDirectory = "";
if (local.lastSlashPos > 0) {
// Extract everything before the last slash, INCLUDING the slash itself
local.entryDirectory = left(local.entryName, local.lastSlashPos);
}
// Type is always "File" if we are excluding directories
local.entryType = "File";
// Add a new row to the query
queryAddRow(targetQuery);
querySetCell(targetQuery, "Name", local.entryName);
querySetCell(targetQuery, "Directory", local.entryDirectory);
querySetCell(targetQuery, "Size", javacast("long", local.entrySize));
querySetCell(targetQuery, "CompressedSize", javacast("long", local.entryCompressedSize));
querySetCell(targetQuery, "Type", local.entryType);
// Parse DateLastModified: If valid, set it; otherwise, do not set the cell
if (len(local.entryModified) && isDate(local.entryModified)) {
querySetCell(targetQuery, "DateLastModified", parseDateTime(local.entryModified));
}
querySetCell(targetQuery, "Comment", local.entryComment);
// Convert CRC from hexadecimal string to numeric (bigint): If valid hex, set it; otherwise, do not set the cell
if (len(local.entryCRC) && reFindNoCase("^[0-9A-F]+$", local.entryCRC) GT 0) {
querySetCell(targetQuery, "CRC", javacast("long", inputBaseN(local.entryCRC, 16)));
}
querySetCell(targetQuery, "EncryptionAlgorithm", (local.entryEncrypted EQ "-" ? "NA" : local.entryEncrypted));
}
}
// --- Input Validation and Setup ---
// 'attributes.file' is already validated by cfparam's lack of a default,
// which means it *must* be provided by the caller.
// Check if the target zip file exists for actions other than 'zip'
if (attributes.action NEQ "zip" AND attributes.action NEQ "compress" AND !fileExists(attributes.file)) {
throw(type="7Zip.FileDoesNotExist", message="The specified zip file '#attributes.file#' does not exist for action '#attributes.action#'.");
}
// Ensure the 7za.exe executable exists if a full path is explicitly provided.
if (!fileExists(attributes.sevenZipPath)) {
throw(type="7Zip.ExecutableNotFound", message="The 7-Zip executable '#attributes.sevenZipPath#' was not found. Please verify the path or ensure it's in your system's PATH.");
}
// --- Action Logic using a switch statement ---
try {
switch (attributes.action) {
case "zip": // CFZIP standard action
case "compress": // Alias for backward compatibility with this tag
// Validate 'source' attribute for compression
if (!len(attributes.source)) {
throw(type="7Zip.MissingArgument", message="The 'source' attribute is required for action '#attributes.action#'.");
}
command = attributes.sevenZipPath;
// 'a': Add files to archive. If archive doesn't exist, it's created.
// '-tzip': Force creation of a ZIP format archive (mimicking CFZIP's default).
args = "a -tzip";
if (attributes.overwrite) {
args &= " -y"; // '-y': Assume Yes on all queries (useful for overwriting existing files in archive).
}
// Apply compression level if provided and valid
if (len(attributes.compressionLevel)) {
if (isNumeric(attributes.compressionLevel) && attributes.compressionLevel GTE 0 && attributes.compressionLevel LTE 9) {
args &= " -mx" & attributes.compressionLevel; // '-mx': Set compression method/level (0=fastest, 9=strongest).
} else {
throw(type="7Zip.InvalidArgument", message="Invalid 'compressionLevel'. Must be a number between 0 and 9, or left empty.");
}
}
// Apply number of threads if valid
if (isNumeric(attributes.numThreads) && attributes.numThreads GTE 1 && attributes.numThreads LTE 8) { // 7-Zip typically supports up to 8 threads effectively
args &= " -mmt" & attributes.numThreads; // '-mmt': Set number of CPU threads.
} else {
throw(type="7Zip.InvalidArgument", message="Invalid 'numThreads'. Must be a number between 1 and 8 (inclusive).");
}
// Add the output archive path (quoted for spaces)
args &= " " & chr(34) & attributes.file & chr(34);
// Corrected to use directoryExists()
if (len(attributes.source) && directoryExists(attributes.source)) {
if (attributes.recurse) {
// For directories with recursion, 7-Zip 'a' command includes the base directory name.
// Example: 7za a archive.zip C:\MyDir\* will add contents of MyDir under MyDir/
args &= " " & chr(34) & attributes.source & fileSep & "*" & chr(34);
// CFZIP's 'entryPath' allows re-rooting, which 7-Zip's 'a' command doesn't easily support.
// This implementation preserves the source directory's relative path within the archive.
} else {
// 7-Zip's 'a' command fundamentally includes directory contents recursively by default.
// Non-recursive compression of a directory itself (i.e., just the directory entry, not its contents)
// is not directly equivalent to CFZIP's non-recursive directory handling with 'a'.
throw(type="7Zip.UnsupportedFeature", message="Compression of a directory without recursion ('recurse=false') is not directly supported by 7-Zip's 'a' command like CFZIP. Please set recurse='true' or compress individual files within the directory.");
}
} else {
// Source is a file or a comma-separated list of files
sourceList = listToArray(attributes.source);
for (i=1; i <= arrayLen(sourceList); i++) {
currentSource = trim(sourceList[i]);
if (!fileExists(currentSource)) {
throw(type="7Zip.FileDoesNotExist", message="Source file '#currentSource#' does not exist.");
}
args &= " " & chr(34) & currentSource & chr(34);
}
// Similar to directories, entryPath for files is complex with 7-Zip 'a' command
// if it implies renaming the file inside the archive.
}
// Prepare arguments for cfexecute using attributecollection
cfexecuteArgs = [
"name": '"' & command & '"',
"arguments": args,
"variable": "variables.commandOutput", // Still needs to be variables.commandOutput for output variable
"errorvariable": "variables.commandError", // Still needs to be variables.commandError for error variable
"timeout": attributes.timeout
];
// Execute the 7-Zip command (no assignment as it returns void in CFScript)
cfexecute(attributecollection=cfexecuteArgs);
// Check for errors based on content in the error output stream
if (len(commandError)) {
throw(type="7Zip.ExecutionError", message="7-Zip compression failed. Error: #commandError#. Output: #commandOutput#");
}
break;
case "unzip": // CFZIP standard action
case "uncompress": // Alias for backward compatibility with this tag
// Validate 'destination' attribute for uncompression
if (!len(attributes.destination)) {
throw(type="7Zip.MissingArgument", message="The 'destination' attribute is required for action '#attributes.action#'.");
}
// Create destination directory if it's specified and doesn't exist
if (len(attributes.destination) && !directoryExists(attributes.destination)) {
try {
cfdirectory(action="create", directory=attributes.destination);
} catch (any e) {
throw(type="7Zip.DirectoryError", message="Could not create destination directory '#attributes.destination#'. Error: #e.message#.");
}
}
command = attributes.sevenZipPath;
// 'x': Extract with full path names. This is generally preferred for preserving directory structure.
args = "x " & chr(34) & attributes.file & chr(34);
// Specify output directory
args &= " -o" & chr(34) & attributes.destination & chr(34);
if (attributes.overwrite) {
args &= " -y"; // '-y': Assume Yes on all queries (overwrite existing files).
}
// Handle 'filter' or 'entryPath' for selective uncompression
if (len(attributes.filter) && len(attributes.entryPath)) {
throw(type="7Zip.InvalidArgument", message="Cannot specify both 'filter' and 'entryPath' for action '#attributes.action#'. Please choose one.");
}
if (len(attributes.filter)) {
args &= " " & chr(34) & attributes.filter & chr(34); // Apply file mask
} else if (len(attributes.entryPath)) {
args &= " " & chr(34) & attributes.entryPath & chr(34); // Extract specific entry
} else if (!attributes.allEntries) {
// If allEntries is false, either filter or entryPath must be provided, otherwise it's ambiguous.
throw(type="7Zip.InvalidArgument", message="When 'allEntries' is false for action '#attributes.action#', either 'filter' or 'entryPath' must be specified to indicate which entries to uncompress.");
}
// Prepare arguments for cfexecute using attributecollection
cfexecuteArgs = [
"name": '"' & command & '"',
"arguments": args,
"variable": "variables.commandOutput",
"errorvariable": "variables.commandError",
"timeout": attributes.timeout
];
// Execute the 7-Zip command (no assignment as it returns void in CFScript)
cfexecute(attributecollection=cfexecuteArgs);
if (len(commandError)) {
throw(type="7Zip.ExecutionError", message="7-Zip uncompression failed. Error: #commandError#. Output: #commandOutput#");
}
break;
case "list":
// Validate 'name' attribute for list action
if (!len(attributes.name)) {
throw(type="7Zip.MissingArgument", message="The 'name' attribute is required for action '#attributes.action#'.");
}
command = attributes.sevenZipPath;
// Add the -slt flag for technical information
args = "l -slt " & chr(34) & attributes.file & chr(34); // 'l -slt': List contents with technical info
// Prepare arguments for cfexecute using attributecollection
cfexecuteArgs = [
"name": '"' & command & '"',
"arguments": args,
"variable": "variables.commandOutput",
"errorvariable": "variables.commandError",
"timeout": attributes.timeout
];
// Execute the 7-Zip command
cfexecute(attributecollection=cfexecuteArgs);
// Check for errors based on content in the error output stream
if (len(commandError)) {
throw(type="7Zip.ExecutionError", message="7-Zip list failed. Error: #commandError#. Output: #commandOutput#");
}
// Create a new ColdFusion Query object for the output
zipInfoQuery = queryNew(
"Name,Directory,Size,CompressedSize,Type,DateLastModified,Comment,CRC,EncryptionAlgorithm",
"varchar,varchar,bigint,bigint,varchar,timestamp,varchar,bigint,varchar" // CRC is now bigint
);
// Variables for parsing -slt output
local.currentEntryData = {};
local.lines = listToArray(commandOutput, chr(10));
// Regex to capture key-value pairs from -slt output
// Group 1: Key (e.g., Path, Size, Modified)
// Group 2: Value
local.sltRegex = "^\s*([a-zA-Z\s]+)\s*=\s*(.*?)\s*$";
local.isFirstEntry = true; // Flag to skip initial archive metadata (archive comment, physical size etc.)
for (i=1; i <= arrayLen(local.lines); i++) {
local.line = trim(local.lines[i]);
// An empty line usually signals the end of an entry's data block
if (len(local.line) EQ 0) {
// If we have accumulated data for an entry and it's not the initial archive metadata
// Also, skip the archive's own metadata block (which is the first one encountered)
if (!structIsEmpty(local.currentEntryData) && !local.isFirstEntry) {
processAndAddEntry(zipInfoQuery, local.currentEntryData);
}
// Reset for next entry
local.currentEntryData = {};
continue;
}
// The "----------" line indicates the end of the archive's general metadata
// and the start of individual file/folder entries.
if (local.line EQ "----------") {
local.isFirstEntry = false; // Next blocks will be actual entries
local.currentEntryData = {}; // Clear any partial archive metadata that might have been picked up before the separator
continue;
}
// Parse key-value pairs
local.matches = reFindNoCase(local.sltRegex, local.line, 1, true);
if (local.matches.len[1] > 0) {
local.key = trim(mid(local.line, local.matches.pos[2], local.matches.len[2]));
local.value = trim(mid(local.line, local.matches.pos[3], local.matches.len[3]));
local.currentEntryData[local.key] = local.value;
}
}
// After the loop, process any remaining data for the last entry
// This handles the case where the last entry is not followed by an empty line
if (!structIsEmpty(local.currentEntryData) && !local.isFirstEntry) {
processAndAddEntry(zipInfoQuery, local.currentEntryData);
}
// Set output variable in the caller scope using the 'name' attribute value
caller[attributes.name] = zipInfoQuery;
// caller.zipEntry is ONLY set for read/readBinary actions, not list.
break;
case "read":
case "readBinary":
// Validate 'entryPath' for reading specific content
if (!len(attributes.entryPath)) {
throw(type="7Zip.MissingArgument", message="The 'entryPath' attribute is required for action '#attributes.action#'.");
}
tempDir = getTempDirectory(); // Get a temporary directory path
// 7-Zip's 'e' (extract) command strips the path from the extracted file,
// so we just get the filename part from the entryPath.
extractedFileName = getFileFromPath(attributes.entryPath);
// Create a unique temporary file path to avoid conflicts
extractedFilePath = tempDir & fileSep & createUUID() & "_" & extractedFileName;
command = attributes.sevenZipPath;
// 'e': Extract without full path names (flattens archive structure into target dir).
// This is suitable for extracting a single file.
args = "e " & chr(34) & attributes.file & chr(34) & " " & chr(34) & attributes.entryPath & chr(34) & " -o" & chr(34) & tempDir & chr(34);
if (attributes.overwrite) {
args &= " -y"; // '-y': Overwrite if the temporary file already exists.
}
// Prepare arguments for cfexecute using attributecollection
cfexecuteArgs = [
"name": '"' & command & '"',
"arguments": args,
"variable": "variables.commandOutput",
"errorvariable": "variables.commandError",
"timeout": attributes.timeout
];
// Execute the 7-Zip command (no assignment as it returns void in CFScript)
cfexecute(attributecollection=cfexecuteArgs);
if (len(commandError)) {
throw(type="7Zip.ExecutionError", message="7-Zip extraction for '#attributes.action#' failed. Error: #commandError#. Output: #commandOutput#");
}
// Verify that the file was actually extracted
if (!fileExists(extractedFilePath)) {
throw(type="7Zip.ExtractionError", message="Extracted file '#extractedFilePath#' not found in temp directory after 7-Zip extraction. This might indicate an issue with the archive or entryPath. 7-Zip output: #commandOutput#");
}
try {
if (attributes.action EQ "read") {
// Read as text with specified character set
caller.zipEntry = fileRead(extractedFilePath, attributes.charset);
} else { // action EQ "readBinary"
caller.zipEntry = fileReadBinary(extractedFilePath);
}
} catch (any e) {
throw(type="7Zip.FileReadError", message="Failed to read extracted file '#extractedFilePath#'. Error: #e.message#. Detail: #e.detail#.");
} finally {
// Always attempt to delete the temporary file after reading
try {
cffile(action="delete", file=extractedFilePath);
} catch (any e) {
// Removed cflog warning as requested
}
}
break;
default:
// Handle unsupported actions
throw(type="7Zip.InvalidAction", message="Invalid action '#attributes.action#'. Valid actions are: zip, unzip, list, read, readBinary.");
}
} catch (any e) {
// Rethrow the caught exception to preserve the original error context
rethrow;
}
</cfscript>