Skip to content

RemoveRow corrupts file (calcChain.xml) leading to nil pointer dereference in adjustCalcChain #2130

Closed
@htnosakura

Description

@htnosakura

Description

When using excelize/v2@v2.9.0 with Go 1.24.2, performing RemoveRow operations on a sheet containing formulas (e.g., =DATEDIF(A_cell,TODAY(),"d"),other untested) and then saving the file can lead to a corrupted XLSX file. Microsoft Excel reports issues with the file content, often related to /xl/calcChain.xml (Calculation Properties) and sometimes shared formulas.

Subsequently,if you open the file with Excel and repair&save it,then attempting to open this corrupted file with excelize and perform further RemoveRow operations on it causes a panic: runtime error: invalid memory address or nil pointer dereference. The panic occurs within the excelize.(*File).adjustCalcChain method, specifically at the loop header for i := 0; i < len(f.CalcChain.C); i++ { (which corresponds to line 850 in adjust.go for v2.9.0 in my pkg/mod directory), indicating f.CalcChain is likely nil at that point due to the prior file corruption.

This issue demonstrates a two-stage problem:

Initial file corruption caused by RemoveRow + SaveAs.
Subsequent panic when excelize attempts to process this self-corrupted file which recovered from excel.

当在 Go 1.24.2 环境下使用 excelize/v2@v2.9.0 时,对包含公式(例如 =DATEDIF(A_cell,TODAY(),"d"),其它未测试)的工作表执行 RemoveRow 操作并保存文件后,可能导致生成的 XLSX 文件损坏。Microsoft Excel 会报告文件内容存在问题,通常与 /xl/calcChain.xml(计算属性)相关,有时也与共享公式相关。

随后,如果此时使用excel打开该文件并进行修复保存,再尝试使用 excelize 打开这个已损坏的文件并对其执行进一步的 RemoveRow 操作时,会引发一个 panic: runtime error: invalid memory address or nil pointer dereference(运行时错误:无效的内存地址或空指针解引用)。此 panic发生在 excelize.(*File).adjustCalcChain 方法内部,具体是在循环头 for i := 0; i < len(f.CalcChain.C); i++ { 处(根据我 pkg/mod 目录中 v2.9.0 版本的 adjust.go 文件,这对应第 850 行),表明由于之前的文件损坏,f.CalcChain 在此时很可能为 nil。

此问题表现为一个两阶段过程:

首先,RemoveRow + SaveAs 操作导致了初始的文件损坏。
然后,当 excelize 尝试处理被excel修复的文件时,发生后续的 panic。

Steps to reproduce the issue

1. Environment / 环境:

Go version / Go 版本: 1.24.2
excelize version / excelize 版本: github.com/xuri/excelize/v2@v2.9.0
Operating System / 操作系统: Windows 11 24H2

2. Prepare Source Excel File / 准备源 Excel 文件:
Create an Excel file named test_source_AB.xlsx with the following structure on a sheet named Sheet1:

创建一个名为 test_source_AB.xlsx 的 Excel 文件,在名为 Sheet1 的工作表上包含以下结构:

Column A (Date) / A列 (日期) Column B (Formula using DATEDIF & TODAY) / B列 (使用DATEDIF和TODAY的公式)
(Header) Date / (表头) 日期 (Header) Days Diff / (表头) 天数差
2024-01-15 =DATEDIF(A2,TODAY(),"d")
2024-02-20 =DATEDIF(A3,TODAY(),"d")
2024-03-25 =DATEDIF(A4,TODAY(),"d")
2024-04-30 =DATEDIF(A5,TODAY(),"d")

Ensure cells in Column A are formatted as dates.

请确保 A 列的单元格在 Excel 中已格式化为日期。

test_source_AB.xlsx

3. Go Code to Reproduce / 用于复现的 Go 代码:
Save the following Go code (e.g., as excelize_bug_test.go) in the same directory as test_source_AB.xlsx:

将以下 Go 代码(例如,保存为 excelize_bug_test.go)与 test_source_AB.xlsx 文件放置在同一目录下:

package main

import (
	"fmt"
	"log"
	"os"
	"time"

	"github.com/xuri/excelize/v2"//2.9.0
)

const (
	manualSourceFile        = "test_source_AB.xlsx" // Manually created file / 手动创建的文件
	intermediateCorruptedFile = "intermediate_corrupted_AB.xlsx"
	finalOutputFile         = "final_output_AB.xlsx"
	sheetName               = "Sheet1"
)

func main() {
	log.Println("--- Starting single-thread test for excelize bug ---")
	log.Println("--- 开始 excelize bug 的单线程测试 ---")
	log.Printf("Excelize target version: v2.9.0 (confirm via go.mod)")
	log.Printf("Excelize 目标版本: v2.9.0 (请通过 go.mod 确认)")
	log.Printf("Current date for TODAY(): %s", time.Now().Format(time.DateOnly))
     log.Printf("TODAY() 使用的当前日期: %s", time.Now().Format(time.DateOnly))


	log.Printf("IMPORTANT: Ensure '%s' is manually created as per instructions.", manualSourceFile)
	log.Printf("重要提示: 请确保按照说明手动创建了 '%s' 文件。", manualSourceFile)
	if _, err := os.Stat(manualSourceFile); os.IsNotExist(err) {
		log.Fatalf("!!! Source file '%s' not found. Please create it and re-run.", manualSourceFile)
		log.Fatalf("!!! 源文件 '%s' 未找到。请创建该文件后重新运行。", manualSourceFile)
	}

	// --- Phase 1: Generate the intermediate (likely corrupted) file ---
	// --- 阶段 1: 生成中间(可能已损坏的)文件 ---
	log.Println("\n--- Phase 1: Generating intermediate file (expect Excel to report corruption) ---")
	log.Println("\n--- 阶段 1: 生成中间文件 (预期 Excel 会报告文件损坏) ---")
	var successPhase1 bool
	func() {
		f1, err := excelize.OpenFile(manualSourceFile)
		if err != nil {
			log.Fatalf("Phase 1: Failed to open source file '%s': %v", manualSourceFile, err)
			log.Fatalf("阶段 1: 打开源文件 '%s' 失败: %v", manualSourceFile, err)
			return
		}
		defer f1.Close()

		_, sheetCheckErr := f1.GetSheetIndex(sheetName)
		if sheetCheckErr != nil {
			log.Printf("!!! Phase 1: Sheet '%s' not found in '%s'.", sheetName, manualSourceFile)
			log.Printf("!!! 阶段 1: 在 '%s' 文件中未找到工作表 '%s'。", sheetName, manualSourceFile)
			return
		}

		rowToRemoveInPhase1 := 3 // Attempt to remove the 2nd data row (Excel row 3) / 尝试删除第2条数据行 (Excel 中的第3行)
		log.Printf("Phase 1: Removing row %d from sheet '%s' in '%s'...", rowToRemoveInPhase1, sheetName, manualSourceFile)
		log.Printf("阶段 1: 从 '%s' 的 '%s' 工作表中删除第 %d 行...", rowToRemoveInPhase1, sheetName, manualSourceFile)
		if err := f1.RemoveRow(sheetName, rowToRemoveInPhase1); err != nil {
			log.Printf("!!! Phase 1: f1.RemoveRow() returned an error: %v. This might indicate internal state issues.", err)
			log.Printf("!!! 阶段 1: f1.RemoveRow() 调用返回错误: %v。这可能表明存在内部状态问题。", err)
		} else {
			log.Printf("Phase 1: f1.RemoveRow() call completed without direct error.")
			log.Printf("阶段 1: f1.RemoveRow() 调用成功完成 (未直接返回错误)。")
		}

		log.Printf("Phase 1: Saving intermediate file as '%s'...", intermediateCorruptedFile)
		log.Printf("阶段 1: 将中间文件保存为 '%s'...", intermediateCorruptedFile)
		if err := f1.SaveAs(intermediateCorruptedFile); err != nil {
			log.Fatalf("Phase 1: Failed to save intermediate file '%s': %v", intermediateCorruptedFile, err)
			log.Fatalf("阶段 1: 保存中间文件 '%s' 失败: %v", intermediateCorruptedFile, err)
			return
		}
		log.Printf("Phase 1: Intermediate file '%s' saved.", intermediateCorruptedFile)
		log.Printf("阶段 1: 中间文件 '%s' 已保存。", intermediateCorruptedFile)
		log.Println(">>> After Phase 1, try manually opening this intermediate file with Excel; expect a corruption warning.")
		log.Println(">>> 阶段 1 完成后, 请尝试用 Excel 手动打开此中间文件;预期会看到损坏警告。")
		successPhase1 = true
	}()

	if !successPhase1 {
		log.Println("Phase 1 did not complete successfully. Test aborted.")
		log.Println("阶段 1 未成功完成。测试中止。")
		return
	}

	// --- Phase 2: Attempt to process the (likely corrupted) intermediate file ---
	// --- 阶段 2: 尝试处理(可能已损坏的)中间文件 ---
	log.Println("\n--- Phase 2: Processing intermediate file (expect potential panic) ---")
	log.Println("\n--- 阶段 2: 处理中间文件 (预期可能发生 panic) ---")
	func() {

**//Before executing this section, use Excel to open and repair the saved "test_datedif_intermediate_corrupted.xlsx" file.
//在此段执行前使用excel打开并修复保存“test_datedif_intermediate_corrupted.xlsx”文件。**

		log.Printf("Phase 2: Opening intermediate file '%s'", intermediateCorruptedFile)
		log.Printf("阶段 2: 打开中间文件 '%s'", intermediateCorruptedFile)
		f2, err := excelize.OpenFile(intermediateCorruptedFile)
		if err != nil {
			log.Printf("!!! Phase 2: Failed to open intermediate file '%s': %v. This might be due to severe corruption.", intermediateCorruptedFile, err)
			log.Printf("!!! 阶段 2: 打开中间文件 '%s' 失败: %v。这可能是由于文件严重损坏。", intermediateCorruptedFile, err)
			return
		}
		defer f2.Close()
 		
		// After removing row 3 in Phase 1 (original Excel row 3, which was the 2nd data row),
		// the sheet now has Header (row 1), original Data1 (now row 2), original Data3 (now row 3), etc.
		// Let's try to remove the current row 2 (which was original Data1).
		// 阶段 1 中删除了第 3 行(原始 Excel 的第3行,即第2条数据行)之后,
		// 工作表现有表头(第1行)、原始数据1(现为第2行)、原始数据3(现为第3行)等。
		// 我们尝试删除当前的第 2 行(即原始数据1所在行)。
		rowToRemoveInPhase2 := 2 
		log.Printf("Phase 2: Attempting to remove row %d from sheet '%s' in '%s'...", rowToRemoveInPhase2, sheetName, intermediateCorruptedFile)
		log.Printf("阶段 2: 尝试从 '%s' 的 '%s' 工作表中删除第 %d 行...", rowToRemoveInPhase2, sheetName, intermediateCorruptedFile)
		
		// *****************************************************************************
		// *** EXPECTED PANIC LOCATION: nil pointer dereference in adjustCalcChain ***
		// *** 预期发生 PANIC 的位置: adjustCalcChain 中的空指针解引用 ***
		// *****************************************************************************
		if err := f2.RemoveRow(sheetName, rowToRemoveInPhase2); err != nil {
			log.Printf("!!! Phase 2: f2.RemoveRow() returned an error: %v", err)
			log.Printf("!!! 阶段 2: f2.RemoveRow() 调用返回错误: %v", err)
		} else {
			log.Printf("Phase 2: f2.RemoveRow() call completed without direct error.")
			log.Printf("阶段 2: f2.RemoveRow() 调用成功完成 (未直接返回错误)。")
		}

		log.Printf("Phase 2: Attempting to save final output file '%s'...", finalOutputFile)
		log.Printf("阶段 2: 尝试保存最终输出文件 '%s'...", finalOutputFile)
		if err := f2.SaveAs(finalOutputFile); err != nil {
			log.Printf("!!! Phase 2: Failed to save final output file '%s': %v", finalOutputFile, err)
			log.Printf("!!! 阶段 2: 保存最终输出文件 '%s' 失败: %v", finalOutputFile, err)
		} else {
			log.Printf("Phase 2: Final output file '%s' saved.", finalOutputFile)
			log.Printf("阶段 2: 最终输出文件 '%s' 已保存。", finalOutputFile)
			log.Println(">>> Please manually inspect this final file with Excel as well.")
			log.Println(">>> 请同时用 Excel 手动检查此最终文件。")
		}
	}()

	log.Println("\n--- Test finished ---")
	log.Println("\n--- 测试结束 ---")
	log.Println(">>> Review console output for errors or panic stack traces.")
	log.Println(">>> 请检查控制台输出的错误或 panic 堆栈跟踪。")
	log.Println(">>> Manually check 'intermediate_corrupted_AB.xlsx' with Excel for corruption warnings.")
	log.Println(">>> 请用 Excel 手动检查 'intermediate_corrupted_AB.xlsx' 文件是否有损坏警告。")
}

4. Run the Go Program / 运行 Go 程序:
go run excelize_bug_test.go

Describe the results you received

Phase 1 Output (intermediate_corrupted_AB.xlsx) / 阶段1输出 (intermediate_corrupted_AB.xlsx):

When intermediate_corrupted_AB.xlsx is opened with Microsoft Excel, a warning dialog appears: "Excel found a problem with some content in 'intermediate_corrupted_AB.xlsx'. Do you want us to try to recover as much as we can? If you trust the source of this workbook, click Yes."
当用 Microsoft Excel 打开 intermediate_corrupted_AB.xlsx 文件时,会弹出一个警告对话框:“Excel发现在 'intermediate_corrupted_AB.xlsx' 文件中存在一些内容问题。是否希望我们尽可能尝试恢复?如果您信任此工作簿的源,请单击‘是’。”
If "Yes" is clicked, Excel's recovery log (e.g., errorXXXXX_XX.xml) may indicate issues such as:
已删除的记录: /xl/calcChain.xml 部分的 公式 (计算属性) (Deleted record: Formula (Calculation properties) from /xl/calcChain.xml part)
如果单击“是”,Excel 的恢复日志(例如 errorXXXXX_XX.xml)可能会指出如下问题:
已删除的记录: /xl/calcChain.xml 部分的 公式 (计算属性)

Phase 2 Behavior (Go Program Console Output) / 阶段2行为 (Go程序控制台输出):

The program panics during the f2.RemoveRow() call in Phase 2.

程序在阶段2调用 f2.RemoveRow() 时发生 panic。

Panic Message / Panic 信息: panic: runtime error: invalid memory address or nil pointer dereference

Stack Trace includes / 堆栈跟踪包含:

[signal 0xc0000005 code=0x0 addr=0x0 pc=0x...]
goroutine 1 [running]:
github.com/xuri/excelize/v2.(*File).adjustCalcChain({0x...}, {0x...}, {0x...}, 0x1, 0x2, 0xffffffffffffffff, 0x...)
	C:/Users/.../go/pkg/mod/github.com/xuri/excelize/v2@v2.9.0/adjust.go:850 +0x... 
github.com/xuri/excelize/v2.init.func30({0x...}, ...)
	C:/Users/.../go/pkg/mod/github.com/xuri/excelize/v2@v2.9.0/adjust.go:53 +0x...
github.com/xuri/excelize/v2.(*File).adjustHelper({0x...}, {0x...}, 0x1, 0x2, 0xffffffffffffffff)
	C:/Users/.../go/pkg/mod/github.com/xuri/excelize/v2@v2.9.0/adjust.go:91 +0x...
github.com/xuri/excelize/v2.(*File).RemoveRow({0x...}, {0x...}, 0x2)
	C:/Users/.../go/pkg/mod/github.com/xuri/excelize/v2@v2.9.0/rows.go:590 +0x...
main.main.func2()
	.../excelize_bug_test.go:LINE_OF_F2_REMOVEROW +0x... 
...

(Note: The adjust.go:850 refers to the line for i := 0; i < len(f.CalcChain.C); i++ { in excelize/v2@v2.9.0/adjust.go as loaded in my Go module cache, indicating f.CalcChain is likely nil.)

(注意:adjust.go:850 指的是在我 Go 模块缓存中加载的 excelize/v2@v2.9.0/adjust.go 文件中的 for i := 0; i < len(f.CalcChain.C); i++ { 这一行,表明 f.CalcChain 很可能为 nil。)

Describe the results you expected

Phase 1 / 阶段 1:
excelize should save intermediate_corrupted_AB.xlsx as a valid, non-corrupted Excel file. Microsoft Excel should open this file without any warnings or need for recovery.

excelize 应该将 intermediate_corrupted_AB.xlsx 保存为一个有效的、未损坏的 Excel 文件。Microsoft Excel 打开此文件时不应有任何警告或恢复提示。

Phase 2 / 阶段 2:
excelize should be able to successfully open the intermediate_corrupted_AB.xlsx file (which should not have been corrupted in the first place).

excelize 应该能够成功打开 intermediate_corrupted_AB.xlsx 文件(该文件首先就不应该被损坏)。

The subsequent f2.RemoveRow() call should execute without errors or panics.

后续的 f2.RemoveRow() 调用应该无错误或 panic 地执行。

The final_output_AB.xlsx file should be saved as a valid Excel file.

test_datedif_source.xlsx

final_output_AB.xlsx 文件应该被保存为一个有效的 Excel 文件。

Go version

1.23.2

Excelize version or commit ID

2.9.0

Environment

Windows 11 24H2
Microsoft® Excel® 适用于 Microsoft 365MSO (版本 2504 Build 16.0.18730.20030) 64 位

Validations

  • Check that there isn't already an issue that reports the same bug to avoid creating a duplicate.
  • The provided reproduction is a minimal reproducible example of the bug.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions