luci-app-filemanager: Editing hex files improvements
authorDmitry R <rdmitry0911@gmail.com>
Wed, 1 Jan 2025 09:46:12 +0000 (04:46 -0500)
committerPaul Donald <newtwen+github@gmail.com>
Sun, 5 Jan 2025 14:04:29 +0000 (14:04 +0000)
 - Now it uses fs.read_direct() to retrieve the file content
 - Now it opens non-text files in hex Editor by default
 - Now the 'Toggle to ASCII mode' button is disabled in hex Editor if the file is non-text.

Signed-off-by: Dmitry R <rdmitry0911@gmail.com>
applications/luci-app-filemanager/htdocs/luci-static/resources/view/system/filemanager.js

index 9e5066ddec66805284b00094aacfde37c0075a1d..717e32eac490d091f3e40fd2ea36fcae6c4b9fd5 100644 (file)
@@ -1908,79 +1908,107 @@ return view.extend({
                });
        },
 
-       // Handler for clicking on a file to open it in the editor
-       handleFileClick: function(filePath, mode = 'text') {
-               var self = this;
-               var fileRow = document.querySelector("tr[data-file-path='" + filePath + "']");
-               var editorMessage = document.getElementById('editor-message');
-               var editorContainer = document.getElementById('editor-container');
-
-               // Set default permissions if file row is not found
-               if (fileRow) {
-                       var permissions = fileRow.getAttribute('data-numeric-permissions');
-                       self.originalFilePermissions = permissions;
-               } else {
-                       self.originalFilePermissions = '644';
-               }
-
-               // Update message to indicate loading
-               if (editorMessage) {
-                       editorMessage.textContent = _('Loading file...');
-               }
-
-               // Execute 'cat' to read the file content
-               fs.exec('cat', [filePath]).then(function(res) {
-                       var content = '';
-                       if (res.code !== 0) {
-                               if (res.stderr.trim() !== '') {
-                                       return Promise.reject(new Error(res.stderr.trim()));
+       /**
+        * Determines whether a given Uint8Array represents UTF-8 text data.
+        *
+        * @param {Uint8Array} uint8Array - The binary data to check.
+        * @returns {boolean} - Returns true if the data is UTF-8 text, false otherwise.
+        */
+       isText: function(uint8Array) {
+
+               const len = uint8Array.length;
+               let i = 0;
+
+               while (i < len) {
+                       const byte = uint8Array[i];
+
+                       if (byte === 0) return false; // Null byte indicates binary
+
+                       if (byte <= 0x7F) {
+                               // ASCII character, no action needed
+                               i++;
+                               continue;
+                       } else if ((byte & 0xE0) === 0xC0) {
+                               // 2-byte sequence
+                               if (i + 1 >= len || (uint8Array[i + 1] & 0xC0) !== 0x80) return false;
+                               i += 2;
+                       } else if ((byte & 0xF0) === 0xE0) {
+                               // 3-byte sequence
+                               if (
+                                       i + 2 >= len ||
+                                       (uint8Array[i + 1] & 0xC0) !== 0x80 ||
+                                       (uint8Array[i + 2] & 0xC0) !== 0x80
+                               ) {
+                                       return false;
                                }
+                               i += 3;
+                       } else if ((byte & 0xF8) === 0xF0) {
+                               // 4-byte sequence
+                               if (
+                                       i + 3 >= len ||
+                                       (uint8Array[i + 1] & 0xC0) !== 0x80 ||
+                                       (uint8Array[i + 2] & 0xC0) !== 0x80 ||
+                                       (uint8Array[i + 3] & 0xC0) !== 0x80
+                               ) {
+                                       return false;
+                               }
+                               i += 4;
                        } else {
-                               content = res.stdout || '';
+                               // Invalid UTF-8 byte
+                               return false;
                        }
+               }
 
-                       // Store the content as a string
-                       self.fileContent = content;
+               return true;
+       },
 
-                       // Convert content to Uint8Array in chunks not exceeding 8KB
-                       var CHUNK_SIZE = 8 * 1024; // 8KB
-                       var totalLength = content.length;
-                       var chunks = [];
-                       for (var i = 0; i < totalLength; i += CHUNK_SIZE) {
-                               var chunkStr = content.slice(i, i + CHUNK_SIZE);
-                               var chunkBytes = new TextEncoder().encode(chunkStr);
-                               chunks.push(chunkBytes);
-                       }
-                       // Concatenate chunks into a single Uint8Array
-                       var totalBytes = chunks.reduce(function(prev, curr) {
-                               return prev + curr.length;
-                       }, 0);
-                       var dataArray = new Uint8Array(totalBytes);
-                       var offset = 0;
-                       chunks.forEach(function(chunk) {
-                               dataArray.set(chunk, offset);
-                               offset += chunk.length;
+       // Function to handle clicking on a file to open it in the editor
+       handleFileClick: function(filePath, mode) {
+               const self = this;
+               const fileRow = document.querySelector(`tr[data-file-path='${filePath}']`);
+               const editorMessage = document.getElementById('editor-message');
+
+               // Set original file permissions
+               self.originalFilePermissions = fileRow ? fileRow.getAttribute('data-numeric-permissions') : '644';
+               self.editorMode = mode;
+
+               // Display loading message
+               if (editorMessage) editorMessage.textContent = _('Loading file...');
+
+               // Read the file as binary data
+               fs.read_direct(filePath, 'blob')
+                       .then(blob => blob.arrayBuffer())
+                       .then(arrayBuffer => {
+                               const uint8Array = new Uint8Array(arrayBuffer);
+                               self.fileData = uint8Array;
+                               self.fileContent = ''; // Can be used for display or left empty
+                               self.editorMode = 'hex';
+                               self.textType = self.isText(uint8Array) ? 'text' : 'hex';
+                               if (mode === 'text') {
+                                       // Determine if the file is text
+                                       if (self.textType === 'text') {
+                                               // If text, decode the content
+                                               self.fileContent = new TextDecoder().decode(uint8Array);
+                                               self.editorMode = 'text';
+                                       } else {
+                                               // If not text, show a warning and set mode to hex
+                                               if (editorMessage) {
+                                                       editorMessage.textContent = _('The file does not contain valid text data. Opening in hex mode...');
+                                               }
+                                               pop(null, E('p', _('Opening file in hex mode since it is not a text file.')), 'warning');
+                                       }
+                               }
+                       })
+                       .then(() => {
+                               // Render the editor and switch to the editor tab
+                               self.renderEditor(filePath);
+                               self.switchToTab('editor');
+                       })
+                       .catch(err => {
+                               // Handle errors during file reading
+                               pop(null, E('p', _('Failed to open file: %s').format(err.message)), 'error');
                        });
-
-                       self.fileData = dataArray; // Store binary data as Uint8Array
-
-                       self.editorMode = mode; // Set the initial editor mode to 'text'
-
-                       // Render the editor
-                       self.renderEditor(filePath);
-
-                       // Switch to the editor tab
-                       self.switchToTab('editor');
-
-               }).catch(function(err) {
-                       // Handle file read errors
-                       pop(null, E('p', _('Failed to open file: %s').format(err.message)), 'error');
-                       if (editorMessage) {
-                               editorMessage.textContent = _('Failed to open file: %s').format(err.message);
-                       }
-               });
        },
-
        // Adjust padding for line numbers in the editor
        adjustLineNumbersPadding: function() {
                // Update padding based on scrollbar size
@@ -1998,25 +2026,23 @@ return view.extend({
                // Download the file to the user's local machine
                var self = this;
                var fileName = filePath.split('/').pop();
-               fs.read(filePath, {
-                       binary: true
-               }).then(function(content) {
-                       var blob = new Blob([content], {
-                               type: 'application/octet-stream'
+               // Use the read_direct method to download the file
+               fs.read_direct(filePath, 'blob')
+                       .then(function(blob) {
+                               if (!(blob instanceof Blob)) {
+                                       throw new Error(_('Response is not a Blob'));
+                               }
+                               var url = window.URL.createObjectURL(blob);
+                               var a = document.createElement('a');
+                               a.href = url;
+                               a.download = fileName;
+                               document.body.appendChild(a);
+                               a.click();
+                               a.remove();
+                               window.URL.revokeObjectURL(url);
+                       }).catch(function(err) {
+                               pop(null, E('p', _('Failed to download file "%s": %s').format(fileName, err.message)), 'error');
                        });
-                       var downloadLink = document.createElement('a');
-                       downloadLink.href = URL.createObjectURL(blob);
-                       downloadLink.download = fileName;
-                       document.body.appendChild(downloadLink);
-                       downloadLink.click();
-                       document.body.removeChild(downloadLink);
-                       var statusInfo = document.getElementById('status-info');
-                       if (statusInfo) {
-                               statusInfo.textContent = _('Downloaded file: "%s".').format(fileName);
-                       }
-               }).catch(function(err) {
-                       pop(null, E('p', _('Failed to download file "%s": %s').format(fileName, err.message)), 'error');
-               });
        },
 
        // Handler for deleting a file
@@ -2753,14 +2779,16 @@ return view.extend({
                                                self.handleSaveFile(filePath);
                                        }
                                }, _('Save')),
-                               E('button', {
-                                       'class': 'btn',
-                                       'id': 'toggle-text-mode',
-                                       'style': 'margin-left: 10px;',
-                                       'click': function() {
-                                               self.toggleHexMode(filePath);
-                                       }
-                               }, _('Toggle to ASCII Mode'))
+                               ...(self.textType !== 'hex' ? [
+                                       E('button', {
+                                               'class': 'btn',
+                                               'id': 'toggle-text-mode',
+                                               'style': 'margin-left: 10px;',
+                                               'click': function() {
+                                                       self.toggleHexMode(filePath);
+                                               }
+                                       }, _('Toggle to ASCII Mode'))
+                               ] : [])
                        ];
                }
 
@@ -2821,35 +2849,57 @@ return view.extend({
                }
        },
 
+       /**
+        * Toggles the editor mode between text and hex.
+        *
+        * @param {string} filePath - The path of the file to be edited.
+        */
        toggleHexMode: function(filePath) {
-               var self = this;
+               const self = this;
 
                if (self.editorMode === 'text') {
                        // Before switching to hex mode, update self.fileData from the textarea
-                       var textarea = document.querySelector('#editor-container textarea');
+                       const textarea = document.querySelector('#editor-container textarea');
                        if (textarea) {
-                               var content = textarea.value;
+                               const content = textarea.value;
                                self.fileContent = content;
 
                                // Convert content to Uint8Array
-                               var encoder = new TextEncoder();
+                               const encoder = new TextEncoder();
                                self.fileData = encoder.encode(content);
                        }
                        self.editorMode = 'hex';
                } else {
-                       // Before switching to text mode, update self.fileData from the HexEditor
+                       // Before switching to text mode, check if the file is textual
+                       if (self.textType !== 'text') {
+                               pop(null, E('p', _('This file is not a text file and cannot be edited in text mode.')), 'error');
+                               return; // Abort the toggle
+                       }
+
+                       // Before switching to text mode, update self.fileData from HexEditor
                        if (self.hexEditorInstance) {
-                               self.fileData = self.hexEditorInstance.getData();
+                               const hexData = self.hexEditorInstance.getData();
+                               if (hexData instanceof Uint8Array) {
+                                       self.fileData = hexData;
+                               } else {
+                                       pop(null, E('p', _('Failed to retrieve data from Hex Editor.')), 'error');
+                                       return; // Abort the toggle if data retrieval fails
+                               }
                        }
+
                        // Convert self.fileData to string
-                       var decoder = new TextDecoder();
-                       self.fileContent = decoder.decode(self.fileData);
+                       const decoder = new TextDecoder();
+                       try {
+                               self.fileContent = decoder.decode(self.fileData);
+                       } catch (error) {
+                               pop(null, E('p', _('Failed to decode file data to text: %s').format(error.message)), 'error');
+                               return; // Abort the toggle if decoding fails
+                       }
                        self.editorMode = 'text';
                }
 
-               // Re-render the editor
+               // Re-render the editor with the updated mode and content
                self.renderEditor(filePath);
        }
 
-
 });