I'd like to write a very tricky elisp code that does the following in a non-compiled .el file. If we are being byte compiled, use the secure-hash function with sha-256 to compute the whole file's sha256, and emit code into the byte compiled result, that when will be read in later, will compute the sha256 of the .el that is belonging to the .elc being loaded and errors on difference. E.g. we solve the problem of not loading outdated result without depending on timestamps (that is incompatible with git commits). We obviously have to define a defmacro for this at least, since we want to have difference between initial .el and compiled .elc. Can you design this macro for me, and show me the result with a minimal usage example? Make sure to think a lot about this and be concise in presenting your results.
Let's break down the problem:
- We need to create a macro that, when a file is byte-compiled, computes the SHA-256 hash of the entire file.
- The byte-compiled .elc file will include this hash.
- When the .elc file is loaded, it checks if the corresponding .el file's hash matches the stored hash.
- If they don't match, it throws an error.
This is a security feature to ensure that the .elc file corresponds to the same .el file, especially useful when working with version control systems like git, where timestamps might not reflect the actual changes.
Let's start by thinking about the macro structure. We need to have different behavior during compilation versus execution. We can use eval-when-compile
and similar constructs.
Here's a sketch:
(defmacro ensure-el-file-integrity ()
"Ensure that the .el file has not changed since byte compilation."
(if (bound-and-true-p byte-compile-current-file)
;; During byte compilation
(let ((hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents byte-compile-current-file)
(buffer-string)))))
`(unless (string= ,hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents (substring load-file-name 0 -1))
(buffer-string))))
(error "The .el file has been modified since byte compilation!")))
;; During normal execution
nil))
However, there's a problem: we need to know the path of the .el file when the .elc file is loaded. This can be tricky because we don't always have the .el file's path directly available.
Let's revise our approach. During compilation, we can determine the .el file's path from byte-compile-current-file
. But during runtime, we need to infer the .el file's path from the loaded .elc file.
Let's assume that the .el file is in the same directory and has the same name as the .elc file, but with a .el extension. We can use load-file-name
to get the path of the .elc file and then derive the .el file path.
But I think there's a more direct problem: load-file-name
might be nil
if the file was loaded with require
rather than load
. Instead, let's try using (locate-library (file-name-sans-extension (file-name-nondirectory load-file-name)) t)
to find the .el file.
Another complication is ensuring that we're only checking this when the file is loaded, not byte-compiled (we don't want to check integrity during byte compilation).
Let's revise the macro:
(defmacro ensure-el-file-integrity ()
"Ensure that the .el file has not changed since byte compilation."
(if (bound-and-true-p byte-compile-current-file)
;; During byte compilation
(let ((hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents byte-compile-current-file)
(buffer-string)))))
`(unless (or noninteractive (null load-file-name))
(let ((el-file (locate-library (file-name-sans-extension (file-name-nondirectory load-file-name)) t)))
(when el-file
(unless (string= ,hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string))))
(error "The .el file has been modified since byte compilation!"))))))
;; During normal execution
nil))
Wait, there's still an issue here. We want to ensure that the integrity check happens during the loading of the .elc file. But the macro expansion will only occur during the loading of the file the macro is defined in. So we need to design our solution differently.
Let's approach it with a different technique. Instead of a macro, let's use a variable and a function. The variable will hold the hash of the .el file, and the function will check if the current .el file's hash matches the stored hash.
(defvar my-file-hash nil
"SHA-256 hash of the .el file, computed during byte compilation.")
(defun my-ensure-el-file-integrity ()
"Ensure that the .el file has not changed since byte compilation."
(let ((el-file (locate-library (file-name-sans-extension (file-name-nondirectory load-file-name)) t)))
(when el-file
(let ((current-hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string)))))
(unless (string= my-file-hash current-hash)
(error "The .el file has been modified since byte compilation!"))))))
;; During byte compilation, set the hash
(eval-when-compile
(when (bound-and-true-p byte-compile-current-file)
(setq my-file-hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents byte-compile-current-file)
(buffer-string))))))
;; During runtime, check the integrity
(eval-after-load (file-name-sans-extension (file-name-nondirectory (or load-file-name (buffer-file-name))))
'(my-ensure-el-file-integrity))
But there's still an issue: We want the hash computed during byte compilation to be available during runtime. The problem is that when the .elc file is loaded, it will only define the variable my-file-hash
as nil
, not with the hash that was computed during byte compilation.
Ok, let's totally revamp our approach. Let's go back to the macro idea. The trick is to ensure that when the macro is expanded during byte compilation, it will produce code that will execute during file loading.
Here's the idea:
- Create a macro that, when expanded during byte compilation, computes the hash of the .el file.
- The macro expansion will return code that, when executed, checks if the current .el file's hash matches the hash computed during byte compilation.
Let's try it:
(defmacro ensure-el-file-integrity ()
"Ensure that the .el file has not changed since byte compilation."
(if (and (boundp 'byte-compile-current-file) byte-compile-current-file)
;; During byte compilation
(let* ((el-file byte-compile-current-file)
(hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string)))))
`(let* ((el-file (concat (file-name-sans-extension (or load-file-name buffer-file-name)) ".el"))
(current-hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string)))))
(unless (string= ,hash current-hash)
(error "The .el file has been modified since byte compilation!"))))
;; During normal execution (not byte compiling)
nil))
I think we're getting closer, but there's still an issue. ensure-el-file-integrity
will expand into the integrity check during byte compilation, but we need to ensure that the integrity check is executed when the .elc file is loaded.
Let's try yet another approach. During byte compilation, we'll append to the byte-compile-output-buffer the code that checks integrity. This way, the integrity check will be executed when the .elc file is loaded.
Wait, I don't think we can do this directly. Let's try a simpler approach. Let's put the macro at the end of the .el file, and have it expand to a wrapped version of the file's entire content, along with an integrity check.
No, that would be too complex. Let's go back to a direct approach.
We'll create a macro that, when used in a .el file, will:
- During byte compilation, compute the hash of the current .el file and include it in the .elc file.
- During runtime, check the hash of the source .el file against the stored hash.
(defmacro check-source-file-integrity ()
"Ensure that the source .el file has not changed since byte compilation."
(if (and (boundp 'byte-compile-current-file) byte-compile-current-file)
;; During byte compilation
(let* ((el-file byte-compile-current-file)
(hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string)))))
`(let* ((source-file (concat (file-name-sans-extension (or load-file-name buffer-file-name)) ".el"))
(current-hash (and (file-exists-p source-file)
(secure-hash 'sha256 (with-temp-buffer
(insert-file-contents source-file)
(buffer-string))))))
(unless (or (null current-hash) (string= ,hash current-hash))
(error "Source file %s has been modified since byte compilation" source-file))))
;; No action needed during normal execution without byte compilation
nil))
Actually, I think still have an issue to overcome: the macro is expanded during byte compilation, but we want the check to occur when the .elc file is loaded (i.e., at runtime). This seems to be our main challenge.
Let me rethink this more.
What I'm trying to achieve is similar to having a global variable (like my-file-hash
) defined to nil
in the .el file, and when the file is byte-compiled, the variable's definition in the .elc file should be set to the actual hash. This is a form of cross-stage persistence.
Ok, let's define a macro verify-source-integrity
such that, when the byte-compiler sees the macro, it includes the computed hash in the expanded code. Then, during runtime, the expanded code (now with the hash built in) will perform the verification.
(defmacro verify-source-integrity ()
"Ensure that the .el file has not been modified since byte compilation."
(if (and (boundp 'byte-compile-current-file) byte-compile-current-file)
;; During byte compilation, compute the hash and include it in the expanded code
(let* ((el-file byte-compile-current-file)
(hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string)))))
`(let* ((el-file (concat (file-name-sans-extension load-file-name) ".el"))
(current-hash (and (file-exists-p el-file)
(secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string))))))
(unless (or (null current-hash) (string= ,hash current-hash))
(error "The .el file %s has been modified since byte compilation" el-file))))
;; During normal execution (not byte compiling), just return nil
nil))
Now, if we put this macro at the end of our .el file, it should ensure that the hash of the .el file at byte compilation time matches the hash at runtime.
Let's create a small example to demonstrate its use:
;;; test-integrity.el --- Test file integrity checking
;; Define the macro for verifying file integrity
(defmacro verify-source-integrity ()
"Ensure that the .el file has not been modified since byte compilation."
(if (and (boundp 'byte-compile-current-file) byte-compile-current-file)
;; During byte compilation, compute the hash and include it in the expanded code
(let* ((el-file byte-compile-current-file)
(hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string)))))
`(let* ((el-file (concat (file-name-sans-extension load-file-name) ".el"))
(current-hash (and (file-exists-p el-file)
(secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string))))))
(unless (or (null current-hash) (string= ,hash current-hash))
(error "The .el file %s has been modified since byte compilation" el-file))))
;; During normal execution (not byte compiling), just return nil
nil))
;; Some example function
(defun test-function ()
(message "This is a test function."))
;; Verify the file integrity at the end of the file
(verify-source-integrity)
;;; test-integrity.el ends here
Actually, I need to correct a possible issue in our logic: load-file-name
might be nil
if the file was loaded with require
rather than load
, so we should handle that case.
(defmacro verify-source-integrity ()
"Ensure that the .el file has not been modified since byte compilation."
(if (and (boundp 'byte-compile-current-file) byte-compile-current-file)
;; During byte compilation, compute the hash and include it in the expanded code
(let* ((el-file byte-compile-current-file)
(hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string)))))
`(let* ((elc-file (or load-file-name (symbol-file ',(intern (file-name-base byte-compile-current-file)))))
(el-file (and elc-file (concat (file-name-sans-extension elc-file) ".el")))
(current-hash (and el-file (file-exists-p el-file)
(secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string))))))
(unless (or (null current-hash) (string= ,hash current-hash))
(error "The .el file %s has been modified since byte compilation" el-file))))
;; During normal execution (not byte compiling), just return nil
nil))
Actually, I realized I'm making this overly complex. Let's simplify the problem a bit and approach it differently. The goal is to embed a hash in the byte-compiled file and check it when the file is loaded. The macro is still the right way to go, but I need to be more precise about how it works.
Let's try:
(defmacro verify-source-integrity ()
"Ensure that the .el file has not been modified since byte compilation."
(if (and (boundp 'byte-compile-current-file) byte-compile-current-file)
;; During byte compilation, compute the hash and include it in the expanded code
(let* ((el-file byte-compile-current-file)
(hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string)))))
`(let* ((el-file (concat (file-name-sans-extension (or load-file-name (buffer-file-name))) ".el"))
(current-hash (and (file-exists-p el-file)
(secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string))))))
(unless (or (null current-hash) (string= ,hash current-hash))
(error "The .el file %s has been modified since byte compilation" el-file))))
;; During normal execution (not byte compiling), just return nil
nil))
Reflecting on this, I'm still not confident this will work in all situations. Let's reapproach. First, let's understand how file loading and byte-compilation works in Emacs.
- When you load a file with
load
, Emacs looks for a .elc file first, and if it exists, it loads that. Otherwise, it loads the .el file. require
does the same thing, but it also checks if the feature has already been loaded.- When a file is byte-compiled, it is read in its entirety, and then the byte-compiler produces a .elc file.
Let me describe a simpler approach:
- Create a macro that, when expanded during byte compilation, computes the hash of the .el file.
- The macro expansion will include this hash in a variable definition within the .elc file.
- At the end of the .elc file, include a function that, when the file is loaded, checks the .el file's hash against the stored hash.
This should work regardless of whether the file is loaded with load
or require
.
(defmacro ensure-file-integrity ()
"Ensure that the source .el file has not been modified since byte compilation."
(if (and (boundp 'byte-compile-current-file) byte-compile-current-file)
;; During byte compilation, compute the hash and include it in the expanded code
(let* ((el-file byte-compile-current-file)
(hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string)))))
`(progn
(defvar ,(intern (concat (file-name-base el-file) "-source-hash")) ,hash
"SHA-256 hash of the source .el file at byte compilation time.")
(defun ,(intern (concat (file-name-base el-file) "-ensure-integrity")) ()
"Ensure that the source .el file has not been modified since byte compilation."
(let* ((elc-file (or load-file-name (buffer-file-name)))
(el-file (and elc-file (concat (file-name-sans-extension elc-file) ".el")))
(current-hash (and el-file (file-exists-p el-file)
(secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string))))))
(unless (or (null current-hash)
(string= ,(intern (concat (file-name-base el-file) "-source-hash")) current-hash))
(error "The .el file %s has been modified since byte compilation" el-file))))
(add-hook 'after-load-functions
(lambda (file)
(when (string= (file-name-sans-extension file) ,(file-name-sans-extension el-file))
(,(intern (concat (file-name-base el-file) "-ensure-integrity"))))))))
;; During normal execution (not byte compiling), just return nil
nil))
Hmm, after thinking about this once more, I think I'm overcomplicating things. Let's simplify the approach:
- At the end of our .el file, we'll include a macro call.
- When the file is byte-compiled, the macro expands into code that: a. Defines a variable with the hash of the .el file at compile time. b. Defines a function that checks the current .el file's hash against the stored hash. c. Calls this function immediately upon loading the .elc file.
(defmacro ensure-file-integrity ()
"Ensure that the source .el file has not been modified since byte compilation."
(if (and (boundp 'byte-compile-current-file) byte-compile-current-file)
;; During byte compilation, compute the hash and include it in the expanded code
(let* ((el-file byte-compile-current-file)
(hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string)))))
`(let* ((elc-file (or load-file-name (buffer-file-name)))
(el-file (and elc-file (concat (file-name-sans-extension elc-file) ".el")))
(current-hash (and el-file (file-exists-p el-file)
(secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string))))))
(unless (or (null current-hash) (string= ,hash current-hash))
(error "The .el file %s has been modified since byte compilation" el-file))))
;; During normal execution (not byte compiling), just return nil
nil))
Ok, let me revise our approach one more time for clarity. When the .el file is byte-compiled, we want our macro to insert code into the .elc file that, when loaded, will check the integrity of the .el file. Let's be more explicit and simple.
(defmacro verify-source-integrity ()
"Verify the integrity of the source .el file when the compiled .elc file is loaded."
(if (and (boundp 'byte-compile-current-file) byte-compile-current-file)
;; During byte compilation, store the .el file's hash in the .elc file
(let* ((el-file byte-compile-current-file)
(hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string)))))
`(progn
;; This code will be evaluated when the .elc file is loaded
(let* ((elc-file (or load-file-name (buffer-file-name)))
(el-file (and elc-file (concat (file-name-sans-extension elc-file) ".el")))
(current-hash (and el-file (file-exists-p el-file)
(secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string))))))
(unless (or (null current-hash) (string= ,hash current-hash))
(error "The .el file %s has been modified since byte compilation" el-file)))))
;; During normal execution, do nothing
nil))
There are some error cases I'm still not sure about. Let me think about them:
- What if the .el file doesn't exist anymore when the .elc file is loaded? I've added a check for this with
(and el-file (file-exists-p el-file))
. - What if
load-file-name
andbuffer-file-name
are bothnil
? This case might need a better solution.
Let me restructure the code to be clearer:
(defmacro verify-source-integrity ()
"Verify the integrity of the source .el file when the compiled .elc file is loaded."
(if (and (boundp 'byte-compile-current-file) byte-compile-current-file)
;; During byte compilation, store the .el file's hash in the .elc file
(let* ((el-file byte-compile-current-file)
(hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string)))))
`(progn
;; This code will be evaluated when the .elc file is loaded
(let* ((elc-file (or load-file-name (buffer-file-name)))
(el-file (and elc-file (concat (file-name-sans-extension elc-file) ".el"))))
(when (and el-file (file-exists-p el-file))
(let ((current-hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string)))))
(unless (string= ,hash current-hash)
(error "The .el file %s has been modified since byte compilation" el-file)))))))
;; During normal execution, do nothing
nil))
Actually, let's also guard against the case that load-file-name or buffer-file-name is nil. This could happen in some unusual situations. In that case, we just won't be able to verify the source file.
(defmacro verify-source-integrity ()
"Verify the integrity of the source .el file when the compiled .elc file is loaded."
(if (and (boundp 'byte-compile-current-file) byte-compile-current-file)
;; During byte compilation, store the .el file's hash in the .elc file
(let* ((el-file byte-compile-current-file)
(hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string)))))
`(progn
;; This code will be evaluated when the .elc file is loaded
(let* ((elc-file (or load-file-name (buffer-file-name)))
(el-file (and elc-file (concat (file-name-sans-extension elc-file) ".el"))))
(if (and el-file (file-exists-p el-file))
(let ((current-hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string)))))
(unless (string= ,hash current-hash)
(error "The .el file %s has been modified since byte compilation" el-file)))
(message "Could not verify the integrity of the source file for %s" (or elc-file "this file"))))))
;; During normal execution, do nothing
nil))
Actually let's add a defensive guard against not being able to locate our .elc file. My understanding is buffer-file-name
should be nil when loading:
(defmacro verify-source-integrity ()
"Verify the integrity of the source .el file when the compiled .elc file is loaded."
(if (and (boundp 'byte-compile-current-file) byte-compile-current-file)
;; During byte compilation, store the .el file's hash in the .elc file
(let* ((el-file byte-compile-current-file)
(hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string)))))
`(progn
;; This code will be evaluated when the .elc file is loaded
(let* ((elc-file (or load-file-name (symbol-file ',(intern (file-name-base byte-compile-current-file)))))
(el-file (and elc-file (concat (file-name-sans-extension elc-file) ".el"))))
(if (and el-file (file-exists-p el-file))
(let ((current-hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string)))))
(unless (string= ,hash current-hash)
(error "The .el file %s has been modified since byte compilation" el-file)))
(message "Could not verify the integrity of the source file for %s" (or elc-file "this file"))))))
;; During normal execution, do nothing
nil))
Ok, I think this is getting overly complex and there might still be edge cases I'm not considering. Let's simplify and create a cleaner version.
Let's go back to fundamentals and think logically about this: During byte compilation, we want to compute the hash of the source file. We need to make sure this hash is included in the byte-compiled file, and when the byte-compiled file is loaded, we want to check if the source file's hash still matches.
So, in the simplest terms, our macro expands differently at compile-time vs. runtime.
(defmacro verify-source-integrity ()
"Verify the integrity of the source .el file when the compiled .elc file is loaded."
(if (boundp 'byte-compile-current-file)
;; During byte compilation, compute the hash and include it in the expanded code
(let ((hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents byte-compile-current-file)
(buffer-string)))))
;; Return code that will verify integrity
`(let* ((elc-file load-file-name)
(el-file (and elc-file (concat (file-name-sans-extension elc-file) ".el"))))
(if (and el-file (file-exists-p el-file))
(let ((current-hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string)))))
(unless (string= ,hash current-hash)
(error "The .el file %s has been modified since byte compilation" el-file)))
(message "Could not verify the integrity of the source file for %s" (or elc-file "this file")))))
;; During normal execution, do nothing
nil))
Ok. Another possible issue is that the load-file-name might be nil. For that case, let's use some other ways to determine the el file.
Here's a simple one: in the macro expansion, we can include the full path of the .el file. So even if load-file-name is not available during loading of the elc, we can still verify.
Let's try this approach:
(defmacro verify-source-integrity ()
"Verify the integrity of the source .el file when the compiled .elc file is loaded."
(if (and (boundp 'byte-compile-current-file) byte-compile-current-file)
;; During byte compilation, compute the hash and include it in the expanded code
(let* ((el-file byte-compile-current-file)
(hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string)))))
;; Return code that will verify integrity
`(when (file-exists-p ,el-file)
(let ((current-hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents ,el-file)
(buffer-string)))))
(unless (string= ,hash current-hash)
(error "The .el file %s has been modified since byte compilation" ,el-file)))))
;; During normal execution, do nothing
nil))
Wait, this seems a bit too simple. Let me think again and be precise. The issue might be to handle the case where the file is loaded from a different directory than where it was compiled. In that case, the direct path to the el file from byte-compile-current-file might not work.
Let's see if we can handle different loading directories. We need to find the corresponding .el file regardless of the directory it's loaded from.
(defmacro verify-source-integrity ()
"Verify the integrity of the source .el file when the compiled .elc file is loaded."
(if (and (boundp 'byte-compile-current-file) byte-compile-current-file)
;; During byte compilation, compute the hash and include it in the expanded code
(let* ((el-file-name (file-name-nondirectory byte-compile-current-file))
(hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents byte-compile-current-file)
(buffer-string)))))
;; Return code that will verify integrity
`(progn
(let* ((elc-file (or load-file-name (buffer-file-name)))
(el-file (and elc-file (concat (file-name-sans-extension elc-file) ".el"))))
(if (and el-file (file-exists-p el-file))
(let ((current-hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string)))))
(unless (string= ,hash current-hash)
(error "The .el file %s has been modified since byte compilation" el-file)))
(message "Could not find the source .el file for verification")))))
;; During normal execution, do nothing
nil))
I've simplified it once more. It now computes the hash of the source file during byte compilation and then verifies it at runtime. It uses load-file-name
to find the .elc file, and from there, it tries to find the .el file. If it can't find the .el file, it just prints a message.
But I'm starting to realize a flaw in our approach. Even if we find the .el file, we can't be sure it's the exact same .el file that was byte-compiled. It might have the same name but be in a different location. So our verification might not be foolproof.
Actually, there's another way we can deal with this. We can look at the current feature/library. This basically would take the file-name-base of the current el file, and use feature-file. So here's what to do:
(defmacro verify-source-integrity ()
"Verify the integrity of the source .el file when the compiled .elc file is loaded."
(if (and (boundp 'byte-compile-current-file) byte-compile-current-file)
;; During byte compilation, compute the hash and include it in the expanded code
(let* ((el-file byte-compile-current-file)
(feature (intern (file-name-base el-file)))
(hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string)))))
;; Return code that will verify integrity
`(progn
(let* ((elc-file (if load-file-name
load-file-name
(locate-library ,(file-name-nondirectory el-file) 'nosuffix 'path-only)))
(el-file (and elc-file (concat (file-name-sans-extension elc-file) ".el"))))
(if (and el-file (file-exists-p el-file))
(let ((current-hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string)))))
(unless (string= ,hash current-hash)
(error "The .el file %s has been modified since byte compilation" el-file)))
(message "Could not find the source .el file for verification")))))
;; During normal execution, do nothing
nil))
This seems a lot more reliable now. It tries to find the .el file corresponding to the .elc file being loaded. If it finds the .el file, it computes its hash and checks if it matches the one computed during byte compilation.
Let me try to make a final check. The intent is to have a macro that, when called in a .el file, adds integrity checking to the compiled .elc file. This macro should work in any .el file and ensure that the .elc file checks its corresponding .el file integrity.
Here's a simplified and more robust version:
(defmacro verify-source-integrity ()
"Verify the integrity of the source .el file when the compiled .elc file is loaded."
(if (and (boundp 'byte-compile-current-file) byte-compile-current-file)
;; During byte compilation, compute the hash and include it in the expanded code
(let* ((el-file byte-compile-current-file)
(hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string)))))
;; Return code that will verify integrity at runtime
`(progn
(let* ((elc-file (or load-file-name (buffer-file-name)))
(el-file (and elc-file (concat (file-name-sans-extension elc-file) ".el"))))
(if (and el-file (file-exists-p el-file))
(let ((current-hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string)))))
(unless (string= ,hash current-hash)
(error "The .el file %s has been modified since byte compilation" el-file)))
(message "Could not find the source .el file for verification")))))
;; During normal execution, do nothing
nil))
I think I've simplified it enough now. It should work for most cases, and I've included fallbacks for the edge cases.
Let's make a minimal example to show its usage:
;;; test-integrity.el --- Test file integrity checking
;; Define the macro for verifying file integrity
(defmacro verify-source-integrity ()
"Verify the integrity of the source .el file when the compiled .elc file is loaded."
(if (and (boundp 'byte-compile-current-file) byte-compile-current-file)
;; During byte compilation, compute the hash and include it in the expanded code
(let* ((el-file byte-compile-current-file)
(hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string)))))
;; Return code that will verify integrity at runtime
`(progn
(let* ((elc-file (or load-file-name (buffer-file-name)))
(el-file (and elc-file (concat (file-name-sans-extension elc-file) ".el"))))
(if (and el-file (file-exists-p el-file))
(let ((current-hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string)))))
(unless (string= ,hash current-hash)
(error "The .el file %s has been modified since byte compilation" el-file)))
(message "Could not find the source .el file for verification")))))
;; During normal execution, do nothing
nil))
;; Some example function
(defun test-function ()
(message "This is a test function."))
;; Verify the file integrity at the end of the file
(verify-source-integrity)
;;; test-integrity.el ends here
Usage would be as follows:
- Save the above content as "test-integrity.el".
- Byte-compile the file:
M-x byte-compile-file RET test-integrity.el RET
. - Load the compiled file:
M-x load-file RET test-integrity.elc RET
. - If the source file has not been modified, it should load without errors. If you modify the source file and then try to load the .elc file again, you should get an error.
Just to really solidify the implementation, let's walk through step by step:
- Define a macro
verify-source-integrity
that behaves differently during byte compilation versus normal execution. - During byte compilation, the macro expands to compute the source file's hash and include code in the .elc file to verify the source file's integrity.
- During regular execution, the macro expands to nil (does nothing).
- When the .elc file (which has the integrity check embedded in it) is loaded, it verifies that the source .el file hasn't been modified since byte compilation.
Let me sanity check the implementation:
- Are there any edge cases I haven't handled? possibly loading from a place where the .el file doesn't exist. Our implementation gracefully handles this.
- What if the .el file exists but has been modified? An error will be raised.
- What if the file isn't loaded with
load-file-name
? If we can't find the source .el file, a message will be printed, but no error will be thrown.
I think I've been as careful as possible and things look solid. Here's the final implementation:
(defmacro verify-source-integrity ()
"Verify the integrity of the source .el file when the compiled .elc file is loaded."
(if (and (boundp 'byte-compile-current-file) byte-compile-current-file)
;; During byte compilation, compute the hash and include it in the expanded code
(let* ((el-file byte-compile-current-file)
(hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string)))))
;; Return code that will verify integrity at runtime
`(progn
(let* ((elc-file (or load-file-name (buffer-file-name)))
(el-file (and elc-file (concat (file-name-sans-extension elc-file) ".el"))))
(if (and el-file (file-exists-p el-file))
(let ((current-hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string)))))
(unless (string= ,hash current-hash)
(error "The .el file %s has been modified since byte compilation" el-file)))
(message "Could not find the source .el file for verification")))))
;; During normal execution, do nothing
nil))
All right so let's do a small test. I've reduced our code to the minimum, making sure to wrap the code in a safety check to ensure we get proper behavior in both compilation and load time:
(defmacro verify-source-integrity ()
"Verify the integrity of the source .el file when the compiled .elc file is loaded."
(if (and (boundp 'byte-compile-current-file) byte-compile-current-file)
;; During byte compilation, compute the hash and include it in the expanded code
(let* ((el-file byte-compile-current-file)
(hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string)))))
;; Return code that will verify integrity at runtime
`(progn
(let* ((elc-file (or load-file-name (buffer-file-name)))
(el-file (and elc-file (concat (file-name-sans-extension elc-file) ".el"))))
(if (and el-file (file-exists-p el-file))
(let ((current-hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string)))))
(unless (string= ,hash current-hash)
(error "The .el file %s has been modified since byte compilation" el-file)))
(message "Could not find the source .el file for verification")))))
;; During normal execution, do nothing
nil))
(provide 'test-integrity)
Let me trace through the behavior:
- When we compile test-integrity.el to .elc, the
verify-source-integrity
macro is expanded. - During compilation,
(boundp 'byte-compile-current-file)
will be true, so the macro expands to our integrity verification code. - The hash of the file is computed using secure-hash.
- The expanded code includes this hash and the verification logic.
- When the .elc file is later loaded, this verification code will run.
- It will find the .el file and compute its hash.
- If the hashes match, the file will load normally. If not, it will throw an error.
I believe the implementation is now correct and robust. We've thought through the edge cases and have a clean, simple macro that does what we want.
In order to make sure I'm solving the user's problem, let's check what the user is asking for:
- A macro that computes the SHA-256 hash of a .el file during byte compilation.
- The hash is embedded in the compiled .elc file.
- When the .elc file is loaded, it verifies that the .el file's current hash matches the one computed during byte compilation.
- If they don't match, it throws an error.
And this is exactly what our code does.
Perfect, let's polish our solution:
- Make sure the macro has a clear, descriptive name and docstring.
- Include brief explanations as comments to make the code easy to understand.
- Make the usage example clear.
(defmacro verify-source-integrity ()
"Verify the integrity of the source .el file when the compiled .elc file is loaded.
When this macro is placed in an Emacs Lisp file and the file is byte-compiled,
it embeds the SHA-256 hash of the source .el file within the compiled .elc file.
Then, when the .elc file is loaded, it verifies that the source .el file
hasn't been modified since compilation. If the source has been modified,
an error is thrown. This ensures that the .elc file corresponds exactly to
the .el file, which is useful for version control systems like git where
timestamps might not reflect actual changes."
(if (and (boundp 'byte-compile-current-file) byte-compile-current-file)
;; During byte compilation, compute the hash and include it in the expanded code
(let* ((el-file byte-compile-current-file)
(hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string)))))
;; Return code that will verify integrity at runtime
`(progn
(let* ((elc-file (or load-file-name (buffer-file-name)))
(el-file (and elc-file (concat (file-name-sans-extension elc-file) ".el"))))
(if (and el-file (file-exists-p el-file))
(let ((current-hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string)))))
(unless (string= ,hash current-hash)
(error "The .el file %s has been modified since byte compilation" el-file)))
(message "Could not find the source .el file for verification")))))
;; During normal execution, do nothing
nil))
And here's a simple example of how to use it:
;;; my-package.el --- A simple package
;; Author: Your Name
;; Version: 0.1
;;; Code:
;; Your Emacs Lisp code goes here
(defun my-function ()
"This is just a sample function."
(message "Hello, world!"))
;; Verify source integrity
(verify-source-integrity)
(provide 'my-package)
;;; my-package.el ends here
When you byte-compile this file, the hash of the source will be embedded in the .elc. Then, when you load the .elc file, it'll check if the source file still matches that hash. If it doesn't, it'll throw an error.
This approach provides integrity checking without relying on timestamps, which is especially useful when using version control systems like git.
I hope this satisfies the original request. The code is concise, easy to understand, and should work robustly in various scenarios.
Just to summarize, this macro:
- Computes the SHA-256 hash of the source .el file during byte compilation.
- Embeds this hash in the compiled .elc file.
- Verifies that the .el file hasn't been modified when the .elc file is loaded.
- Throws an error if the file has been modified, or a warning if the .el file can't be found.
This ensures that .elc files loaded are always in sync with their source .el files, even if the timestamps are misleading (as is often the case with version control systems).
Actually there's one remaining edge case: what if we're compiling a file that requires a file that has already been loaded. In that case, we won't be able to guarantee that the .el file hasn't been modified. But that's outside the scope of the macro's responsibility. If a file has already been loaded, then its integrity has already been verified. If the file isn't using our macro, then we won't be able to verify its integrity anyway. So I think we're good as is.
Actually, I think that we could run in to issues if the .el file is being compiled because that might update the .el file without updating the .elc file. Let's guard against that by checking if the macro is being executed during compilation-time, so we don't compute the hash:
(defmacro verify-source-integrity ()
"Verify the integrity of the source .el file when the compiled .elc file is loaded.
When this macro is placed in an Emacs Lisp file and the file is byte-compiled,
it embeds the SHA-256 hash of the source .el file within the compiled .elc file.
Then, when the .elc file is loaded, it verifies that the source .el file
hasn't been modified since compilation. If the source has been modified,
an error is thrown. This ensures that the .elc file corresponds exactly to
the .el file, which is useful for version control systems like git where
timestamps might not reflect actual changes."
(if (and (boundp 'byte-compile-current-file) byte-compile-current-file)
;; During byte compilation, compute the hash and include it in the expanded code
(let* ((el-file byte-compile-current-file)
(hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string)))))
;; Return code that will verify integrity at runtime
`(progn
(unless (bound-and-true-p byte-compile-current-file) ; Don't check when compiling
(let* ((elc-file (or load-file-name (buffer-file-name)))
(el-file (and elc-file (concat (file-name-sans-extension elc-file) ".el"))))
(if (and el-file (file-exists-p el-file))
(let ((current-hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string)))))
(unless (string= ,hash current-hash)
(error "The .el file %s has been modified since byte compilation" el-file)))
(message "Could not find the source .el file for verification"))))))
;; During normal execution, do nothing
nil))
But here's another scenario: what if we've byte-compiled a file but then are loading the .el file directly (either because the .elc file was deleted or we're explicitly loading the .el file). In that case the verify-source-integrity
as a macro will be evaluated but there's no stored hash to check against since we didn't load the .elc file. But we just expand to nil, so even if the macro were to be evaluated directly, it wouldn't run the integrity check, and I think that's fine.
Source Code Integrity Verification for Emacs Lisp
Here's a macro that verifies the integrity of an .el file during .elc loading by comparing SHA-256 hashes:
(defmacro verify-source-integrity ()
"Verify the integrity of the source .el file when its compiled .elc is loaded.
This macro computes the SHA-256 hash of the .el file during byte-compilation
and embeds it in the resulting .elc file. When the .elc file is loaded later,
it compares the original hash with the current hash of the corresponding .el file.
If they differ, it signals an error, ensuring that outdated bytecode is not used."
(if (and (boundp 'byte-compile-current-file) byte-compile-current-file)
;; During byte compilation: compute the source file hash and embed it
(let* ((el-file byte-compile-current-file)
(hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string)))))
;; Generate code to verify integrity at runtime
`(unless (bound-and-true-p byte-compile-current-file) ; Skip during compilation
(let* ((elc-file (or load-file-name (buffer-file-name)))
(el-file (and elc-file (concat (file-name-sans-extension elc-file) ".el"))))
(when (and el-file (file-exists-p el-file))
(let ((current-hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string)))))
(unless (string= ,hash current-hash)
(error "The .el file %s has been modified since byte compilation" el-file)))))))
;; During normal execution (not byte-compiling): do nothing
nil))
Here's a minimal example showing how to use the macro in your Emacs Lisp files:
;;; example.el --- Demonstrates source integrity verification
;;; Code:
;; Your package code here
(defun example-function ()
"A simple example function."
(message "This is an example function"))
;; Place this at the end of your file
(verify-source-integrity)
(provide 'example)
;;; example.el ends here
-
During byte-compilation:
- The macro calculates the SHA-256 hash of the source file
- It embeds this hash in the compiled code
-
When the .elc file is loaded:
- It calculates the current hash of the corresponding .el file
- It compares this with the embedded hash
- If they differ, it signals an error
This solution is version control friendly as it doesn't rely on timestamps, making it compatible with git operations.
Looks good, please fix these small issues, but first think about it and tell me if I'm correct in my review in each of these cases, maybe I'm asking for incorrect things:
- use bound-and-true-p in the first line too for consistency,
- error out on the case of load-file-name not defined (forget about this or buffer-file-name, that makes no sense)
- check that load-file-name ends with the string ".elc"
- compute the el-file by simply substringing the elc-file with the last character missing (no need for file-name-sans-extension)
- error out on el-file not existing
- the big if around the whole function can be replaced with a when, since the else branch is nil anyway
Let me go through each of your suggested changes and consider if they're correct:
-
Use bound-and-true-p in the first line too for consistency
Original:
(if (and (boundp 'byte-compile-current-file) byte-compile-current-file)
Suggested change would be:
(if (and (bound-and-true-p byte-compile-current-file) byte-compile-current-file)
Analysis: This suggestion is actually redundant. The function
bound-and-true-p
checks both if a variable is bound and if its value is non-nil. So(bound-and-true-p byte-compile-current-file)
is equivalent to(and (boundp 'byte-compile-current-file) byte-compile-current-file)
.However, for consistency in coding style, it makes sense to use
bound-and-true-p
in both places. So this change is reasonable. -
Error out on the case of load-file-name not defined
Original:
(let* ((elc-file (or load-file-name (buffer-file-name)))
Analysis: You're right that we should focus on
load-file-name
since we're specifically concerned with loading .elc files. The fallback tobuffer-file-name
doesn't make much sense in this context because we're dealing with loading files, not editing them. Ifload-file-name
is nil, we should indeed signal an error as the verification can't proceed. -
Check that load-file-name ends with the string ".elc"
Analysis: This is a valid suggestion. We want to ensure that we're working with a compiled Elisp file. Checking the file extension helps validate that assumption.
-
Compute the el-file by simply substringing the elc-file with the last character missing
Original:
(el-file (and elc-file (concat (file-name-sans-extension elc-file) ".el")))
Suggested:
(el-file (and elc-file (substring elc-file 0 -1)))
Analysis: This assumes that the file extension is exactly ".elc" (which we're checking in point 3), so we can just remove the "c" at the end. While
file-name-sans-extension
is more generic, your approach is more direct for this specific case where we know the exact pattern. It's slightly less robust (if the filename had unusual extensions it might fail), but given we're checking for ".elc" ending, this approach is reasonable. -
Error out on el-file not existing
Original:
(when (and el-file (file-exists-p el-file))
Analysis: Currently, we only proceed with the hash verification if the .el file exists. If it doesn't, we silently skip the verification. This could be a security concern as it means modified .elc files would load without verification if the .el file is missing. Erroring out in this case is a better approach for ensuring integrity.
-
Replace if with when since the else branch is nil
Original:
(if (and (boundp 'byte-compile-current-file) byte-compile-current-file) ;; During byte compilation: code... nil)
Suggested:
(when (and (boundp 'byte-compile-current-file) byte-compile-current-file) ;; During byte compilation: code... )
Analysis: This is a valid simplification. When the else clause of an
if
isnil
, usingwhen
is more idiomatic and cleaner in Elisp.
All of your suggestions appear to be valid improvements to the code. Let me revise the macro with these changes:
(defmacro verify-source-integrity ()
"Verify the integrity of the source .el file when its compiled .elc is loaded.
This macro computes the SHA-256 hash of the .el file during byte-compilation
and embeds it in the resulting .elc file. When the .elc file is loaded later,
it compares the original hash with the current hash of the corresponding .el file.
If they differ, it signals an error, ensuring that outdated bytecode is not used."
(when (bound-and-true-p byte-compile-current-file)
;; During byte compilation: compute the source file hash and embed it
(let* ((el-file byte-compile-current-file)
(hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string)))))
;; Generate code to verify integrity at runtime
`(unless (bound-and-true-p byte-compile-current-file) ; Skip during compilation
(let* ((elc-file (or load-file-name
(error "Cannot verify source integrity: load-file-name is not set"))))
(unless (string-suffix-p ".elc" elc-file)
(error "Expected a .elc file, but got: %s" elc-file))
(let ((el-file (substring elc-file 0 -1)))
(unless (file-exists-p el-file)
(error "Source file %s not found - cannot verify integrity" el-file))
(let ((current-hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string)))))
(unless (string= ,hash current-hash)
(error "The .el file %s has been modified since byte compilation" el-file)))))))))
Let me review this revision once more:
- Changed the outer
if
to awhen
since the else branch was nil - Used
bound-and-true-p
for checkingbyte-compile-current-file
- Added an error if
load-file-name
is not set - Added a check to ensure
elc-file
ends with ".elc" - Simplified the computation of
el-file
by just removing the last character ("c") - Added an error if the
el-file
doesn't exist
These changes look good to me and should address all the points you raised. The code is now more robust and will fail explicitly in error conditions rather than silently skipping verification steps.
Actually, let me check one more thing. In the original code we had:
(el-file (and elc-file (concat (file-name-sans-extension elc-file) ".el")))
And now we have:
(let ((el-file (substring elc-file 0 -1))))
This assumes that we're just removing the "c" from ".elc". But if we look more carefully, file-name-sans-extension
removes the whole extension (the ".elc") and then we add ".el". The new approach just removes the last character ("c"), assuming the file ends in ".elc".
Since we're now checking that the file ends with ".elc", the substring approach should be fine, but to be extra safe, we could use:
(let ((el-file (concat (substring elc-file 0 -4) ".el"))))
This removes the ".elc" and adds ".el", which is functionally equivalent to the original but more direct.
However, your suggestion of just removing the last character seems reasonable and straightforward given that we're already checking for the ".elc" suffix. So I'll stick with that in the final version.
Review of Your Suggested Changes
Let me analyze each of your proposed changes:
-
Use bound-and-true-p in the first line - Correct. This is more concise and consistent.
-
Error out when load-file-name is not defined - Correct. We need a valid file path to verify integrity.
-
Check that load-file-name ends with ".elc" - Correct. This ensures we're working with a compiled file.
-
Compute el-file by substringing - Mostly correct, but we should be careful. Removing just the "c" assumes the file ends exactly with ".elc" (which we're now checking). This is more direct than file-name-sans-extension.
-
Error out when el-file doesn't exist - Correct. Silently skipping verification when the source is missing would create a security hole.
-
Replace if with when - Correct. When the else branch returns nil, using
when
is more idiomatic in Elisp.
Revised Implementation
(defmacro verify-source-integrity ()
"Verify the integrity of the source .el file when its compiled .elc is loaded.
This macro computes the SHA-256 hash of the .el file during byte-compilation
and embeds it in the resulting .elc file. When the .elc file is loaded later,
it compares the original hash with the current hash of the corresponding .el file.
If they differ, it signals an error, ensuring that outdated bytecode is not used."
(when (bound-and-true-p byte-compile-current-file)
;; During byte compilation: compute the source file hash and embed it
(let* ((el-file byte-compile-current-file)
(hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string)))))
;; Generate code to verify integrity at runtime
`(unless (bound-and-true-p byte-compile-current-file) ; Skip during compilation
(let ((elc-file (or load-file-name
(error "Cannot verify source integrity: load-file-name is not set"))))
(unless (string-suffix-p ".elc" elc-file)
(error "Expected a .elc file, but got: %s" elc-file))
(let ((el-file (substring elc-file 0 -1)))
(unless (file-exists-p el-file)
(error "Source file %s not found - cannot verify integrity" el-file))
(let ((current-hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string)))))
(unless (string= ,hash current-hash)
(error "The .el file %s has been modified since byte compilation" el-file)))))))))
;;; example.el --- Demonstrates source integrity verification
;;; Code:
;; Your package code here
(defun example-function ()
"A simple example function."
(message "This is an example function"))
;; Place this at the end of your file
(verify-source-integrity)
(provide 'example)
;;; example.el ends here
This version addresses all the specified concerns while maintaining the core functionality of ensuring .elc files are only loaded when their source .el files match what was compiled.
Okay, we are getting there, my final comments:
- the name of the "hash" variable is too generic, young engineers who read this and not familiar with the whole macro concept will wonder what's happening here. To help their life, rename this variable to "compile-time-hash" or something similar, so the splicing with comma makes more immediate sense for a reviewer
- the 3 level embedded let is really hard to read, we could use a trick: use let* and you can still embed the unless+error checks by assigning those checks to unused underscore prefixed let* values, this way the whole cheking part will look like a sequential program, that finally does the unless check with the hashes
You've raised two excellent points about improving the readability and maintainability of the code:
-
Rename "hash" to "compile-time-hash" for better clarity - this makes the purpose of the variable more immediately obvious, especially when it's spliced into the runtime code with the comma.
-
Restructure the nested
let
statements to uselet*
with dummy assignments for conditional checks - this is a clever technique to make the code more linear and easier to follow.
Let me rewrite the macro with these improvements:
(defmacro verify-source-integrity ()
"Verify the integrity of the source .el file when its compiled .elc is loaded.
This macro computes the SHA-256 hash of the .el file during byte-compilation
and embeds it in the resulting .elc file. When the .elc file is loaded later,
it compares the original hash with the current hash of the corresponding .el file.
If they differ, it signals an error, ensuring that outdated bytecode is not used."
(when (bound-and-true-p byte-compile-current-file)
;; During byte compilation: compute the source file hash and embed it
(let* ((el-file byte-compile-current-file)
(compile-time-hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string)))))
;; Generate code to verify integrity at runtime
`(unless (bound-and-true-p byte-compile-current-file) ; Skip during compilation
(let* ((elc-file (or load-file-name
(error "Cannot verify source integrity: load-file-name is not set")))
(_check-elc (unless (string-suffix-p ".elc" elc-file)
(error "Expected a .elc file, but got: %s" elc-file)))
(el-file (substring elc-file 0 -1))
(_check-el (unless (file-exists-p el-file)
(error "Source file %s not found - cannot verify integrity" el-file)))
(runtime-hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string))))
(_verify (unless (string= ,compile-time-hash runtime-hash)
(error "The .el file %s has been modified since byte compilation" el-file))))
nil)))))
I've made the following changes:
- Renamed
hash
tocompile-time-hash
to make its purpose clearer. - Restructured the runtime code to use a single
let*
with dummy variables (_check-elc
,_check-el
, and_verify
) that perform the checks. This makes the code more sequential and easier to follow. - Also renamed the hash computed at runtime to
runtime-hash
for symmetry withcompile-time-hash
.
The final nil
isn't strictly necessary, but it makes explicit that the function returns nil after successful verification. It could be removed or replaced with something meaningful if needed.
This version should be more readable while maintaining all the functionality of the previous version.
Wait, let me double check if there's anything I missed:
- We're using
bound-and-true-p
to check forbyte-compile-current-file
- We're checking that
load-file-name
is defined - We're verifying that
load-file-name
ends with ".elc" - We're computing
el-file
by removing the last character fromelc-file
- We're checking that
el-file
exists - We're comparing the hashes and raising an error if they don't match
Everything looks correct. The macro should work as intended and now has clearer variable names and a more sequential structure.
Improved Source Integrity Verification Macro
Here's the revised implementation with better variable naming and more linear structure:
(defmacro verify-source-integrity ()
"Verify the integrity of the source .el file when its compiled .elc is loaded.
This macro computes the SHA-256 hash of the .el file during byte-compilation
and embeds it in the resulting .elc file. When the .elc file is loaded later,
it compares the original hash with the current hash of the corresponding .el file.
If they differ, it signals an error, ensuring that outdated bytecode is not used."
(when (bound-and-true-p byte-compile-current-file)
;; During byte compilation: compute the source file hash and embed it
(let* ((el-file byte-compile-current-file)
(compile-time-hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string)))))
;; Generate code to verify integrity at runtime
`(unless (bound-and-true-p byte-compile-current-file) ; Skip during compilation
(let* ((elc-file (or load-file-name
(error "Cannot verify source integrity: load-file-name is not set")))
(_check-elc (unless (string-suffix-p ".elc" elc-file)
(error "Expected a .elc file, but got: %s" elc-file)))
(el-file (substring elc-file 0 -1))
(_check-el (unless (file-exists-p el-file)
(error "Source file %s not found - cannot verify integrity" el-file)))
(runtime-hash (secure-hash 'sha256 (with-temp-buffer
(insert-file-contents el-file)
(buffer-string))))
(_verify (unless (string= ,compile-time-hash runtime-hash)
(error "The .el file %s has been modified since byte compilation" el-file))))
nil)))))
;;; example.el --- Demonstrates source integrity verification
;;; Code:
;; Your package code here
(defun example-function ()
"A simple example function."
(message "This is an example function"))
;; Place this at the end of your file
(verify-source-integrity)
(provide 'example)
;;; example.el ends here
-
Better variable naming: Using
compile-time-hash
andruntime-hash
makes the purpose of each value clearer, particularly when the compile-time hash is spliced into the runtime code. -
Linear structure: The nested conditionals have been flattened into a single
let*
with dummy variables (prefixed with underscore) that perform validations. This makes the code read more like a sequential program:- Get the .elc file path
- Verify it ends with ".elc"
- Compute the corresponding .el path
- Verify the .el file exists
- Compute the current hash
- Verify it matches the original hash
This approach maintains the functionality while making the code more maintainable and easier to comprehend.