Nansen

Today, I began the long journey of practicing Leetcode problems. Previously, I only did a few problems to maintain familiarity, but today, I officially started preparing for interviews.

I have been thinking about how to efficiently solve Leetcode problems. In my opinion, to be efficient, one must memorize problems. Just as reading a book a hundred times reveals its meaning, training a language model through extensive practice hones its coding skills. Similarly, with Leetcode, through repeated practice, the answers will come naturally; quantity brings quality.

53. Maximum Subarray

The solution can be approached using Kadane’s Algorithm. The code is as follows:

1
2
3
4
5
6
def maxSubArray(nums):
max_current = max_global = nums[0]
for num in nums[1:]:
max_current = max(num, max_current + num)
max_global = max(max_global, max_current)
return max_global

This is a classic dynamic programming problem, and the above algorithm actually hides the essence of dynamic programming.

1
2
3
4
5
6
7
8
9
10
11
class Solution {
public:
int maxSubArray(vector<int>& nums) {
vector<int> dp(nums.size());
dp[0] = nums[0];
for (int i = 1; i < nums.size(); i++) {
dp[i] = max(dp[i - 1] + nums[i], nums[i]);
}
return *max_element(dp.begin(), dp.end());
}
};

This code better reflects the essence of dynamic programming.

To understand the formula dp[i] = max(nums[i] + dp[i-1], nums[i]), we can analyze it from a dynamic programming perspective. The core idea here is to make the optimal choice at each position. Here is a detailed explanation:

1. What does dp[i] represent?

  • dp[i] represents the maximum subarray sum ending at position i.

2. Why compare nums[i] + dp[i-1] and nums[i]?

  • The key question is: Should the current maximum subarray include the previous part (dp[i-1])?
    • nums[i] + dp[i-1]: If dp[i-1] is positive, adding the current nums[i] will increase the subarray sum, so we choose to add it.
    • nums[i]: If dp[i-1] is negative, we choose to start a new subarray from the current position, as a negative sum will only drag down the current sum.

3. Why not compare subsequent numbers?

  • When making the comparison, we assume the subarray stops at position i. In other words, we consider the maximum value within the range [0:i]. At position i, we either add the previous subarray or abandon it and only use the current number.
  • We then traverse the entire array, finding the maximum value at each position, and finally return the largest value.

57. Insert Interval

This is a classic interval merging problem, where we need to merge a new interval into existing intervals.

The solution can be broken down as follows:

  • Step 1: Add all non-overlapping intervals that come before newInterval to the result.
  • Step 2: Merge all potentially overlapping intervals with newInterval. Note the conditions for merging.
  • Step 3: Add the remaining intervals to the result.

Note that the condition for merging intervals is that the start of the previous interval is less than or equal to the end of the subsequent interval, i.e., intervals[i][0] <= newInterval[1].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution:
def insert(self, intervals: List[List[int]], newInterval: List[int]) -> List[List[int]]:
ret_list = []
i = 0

while i < len(intervals) and intervals[i][1] < newInterval[0]:
ret_list.append(intervals[i])
i += 1

while i < len(intervals) and intervals[i][0] <= newInterval[1]:
newInterval[0] = min(intervals[i][0], newInterval[0])
newInterval[1] = max(intervals[i][1], newInterval[1])
i += 1

ret_list.append(newInterval)

while i < len(intervals):
ret_list.append(intervals[i])
i += 1

return ret_list

With careful attention to detail, this problem is not difficult.

300. Longest Increasing Subsequence

This is obviously a dynamic programming problem.

dp[i] represents the length of the longest increasing subsequence ending with a certain number.

Each time an element is added, we update the current dp array. If the current number is greater than the previous one, we increment the result by 1.

Note that the first dp[i] starts from index 1.

1
2
3
4
5
6
7
8
9
10
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
dp = [1] * len(nums)

for i in range(1, len(nums)):
for j in range(i):
if nums[j] < nums[i]:
dp[i] = max(dp[i], dp[j] + 1)

return max(dp)

Complexity Analysis:

  • Time Complexity: $O(n^2)$, due to the two nested loops.
  • Space Complexity: $O(n)$, as we need a dp array of length n.

674. Longest Continuous Increasing Subsequence

This is a simple problem, but still worth understanding.

1
2
3
4
5
6
7
class Solution:
def findLengthOfLCIS(self, nums: List[int]) -> int:
dp = [1] * len(nums)
for i in range(1, len(nums)):
if nums[i] > nums[i-1]:
dp[i] = max(dp[i], dp[i-1] + 1)
return max(dp)

392. Is Subsequence

1
2
3
4
5
6
7
8
9
class Solution:
def isSubsequence(self, s: str, t: str) -> bool:
i = 0
j = 0
while i < len(s) and j < len(t):
if s[i] == t[j]:
i += 1
j += 1
return i == len(s)

This problem can be solved using a two-pointer technique, with t as the base. If s contains matching characters, we move forward; if we reach the end of s, it means the match is complete.

115. Distinct Subsequences

This problem asks us to find how many distinct subsequences of string s equal string t.

First, we need to define dp, where dp[i][j] represents the number of distinct subsequences that can be formed from the first i characters of s to form the first j characters of t.

  • dp[i][0] represents when t is an empty string, the result is 1.
  • dp[0][j] represents forming t from an empty string, which results in 0.

For dp[i][j], the result depends on the characters at positions i and j:

  • If they are equal, the result is the sum of the cases where s[i-1] is not matched (dp[i-1][j]) and the cases where it is matched (dp[i-1][j-1]).
  • If they are not equal, the result is equal to dp[i-1][j].

Note that i, j refer to the first i and j characters.

Additionally, dp[0][0] is initialized to 1, as an empty string is a subsequence of any string.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution:
def numDistinct(self, s: str, t: str) -> int:
dp = [[0 for _ in range(len(t) + 1)] for _ in range(len(s) + 1)]
for i in range(len(s) + 1):
dp[i][0] = 1

for j in range(1, len(t) + 1):
for i in range(1, len(s) + 1):
if s[i-1] == t[j-1]:
dp[i][j] = dp[i-1][j] + dp[i-1][j-1]
else:
dp[i][j] = dp[i-1][j]

return dp[-1][-1]

今天开始了漫长的刷题流程。之前只是简单做了一些题目,保持手感,但今天开始,就要为面试正式准备了。

之前一直在思考一个问题:如何高效地刷 Leetcode。我认为,要高效地刷题,首先就得背题。书读百遍,其义自见,大语言模型经过大量训练,也锻炼出了写代码的能力。同样地,我相信 Leetcode 也一样,通过多次练习,答案自然会浮现,量变产生质变。

53. Maximum Subarray

解题思路可以使用 Kadane’s Algorithm,代码如下:

1
2
3
4
5
6
def maxSubArray(nums):
max_current = max_global = nums[0]
for num in nums[1:]:
max_current = max(num, max_current + num)
max_global = max(max_global, max_current)
return max_global

这是一个典型的动态规划题目,上面的算法其实隐藏了动态规划的本质。

1
2
3
4
5
6
7
8
9
10
11
class Solution {
public:
int maxSubArray(vector<int>& nums) {
vector<int> dp(nums.size());
dp[0] = nums[0];
for (int i = 1; i < nums.size(); i++) {
dp[i] = max(dp[i - 1] + nums[i], nums[i]);
}
return *max_element(dp.begin(), dp.end());
}
};

这段代码更好地体现了动态规划的思想。

理解 dp[i] = max(nums[i] + dp[i-1], nums[i]) 的公式可以从动态规划的角度来解析。这个公式的核心在于“在每一个位置上,我们要做出一个最优选择”。以下是详细解释:

1. dp[i] 的含义是什么?

  • dp[i] 表示以位置 i 结尾的 最大子数组和

2. 为什么比较 nums[i] + dp[i-1]nums[i]

  • 核心问题是:当前的最大子数组和是否应该包括之前的部分(dp[i-1])?
    • **nums[i] + dp[i-1]**:如果 dp[i-1] 是正数,加上当前的 nums[i] 会让子数组和变大,那么我们选择累加。
    • **nums[i]**:如果 dp[i-1] 是负数,我们选择从当前位置重新开始一个新的子数组,因为负数只会拖累当前和。

3. 为什么不用比较后续的数?

  • 我们在比较时,是假设数组在位置 i 停止。也就是说,我们考虑 [0:i] 这个范围内的最大值,对于位置 i 来说,要么加上之前的子数组,要么放弃之前的子数组,只采用当前的数。
  • 然后,我们遍历整个数组,找出在每个位置停下来的最大值,最终返回最大的那个值。

57. Insert Interval

这是一个经典的区间合并问题,需要将新插入的区间合并到已有的区间中。

可以使用分解法来解决:

  • 第一步,将不重叠且在 newInterval 之前的区间添加到结果中。
  • 第二步,合并所有可能重叠的区间到 newInterval 中,注意这里的判断条件。
  • 第三步,将剩余的区间添加到结果中。

注意,合并区间的判断条件是前一个区间的开始小于等于后一个区间的结束,即 intervals[i][0] <= newInterval[1]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution:
def insert(self, intervals: List[List[int]], newInterval: List[int]) -> List[List[int]]:
ret_list = []
i = 0

while i < len(intervals) and intervals[i][1] < newInterval[0]:
ret_list.append(intervals[i])
i += 1

while i < len(intervals) and intervals[i][0] <= newInterval[1]:
newInterval[0] = min(intervals[i][0], newInterval[0])
newInterval[1] = max(intervals[i][1], newInterval[1])
i += 1

ret_list.append(newInterval)

while i < len(intervals):
ret_list.append(intervals[i])
i += 1

return ret_list

只要注意细节,这个题目其实并不难。

300. Longest Increasing Subsequence

显然是一个动态规划问题。

dp[i] 表示以某个数字结尾的最长递增子序列的长度。

每次加入一个元素,我们更新当前的 dp 数组,如果当前数字比之前的小,它们的结果就加 1。

注意,第一个 dp[i] 是从下标 1 开始的。

1
2
3
4
5
6
7
8
9
10
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
dp = [1] * len(nums)

for i in range(1, len(nums)):
for j in range(i):
if nums[j] < nums[i]:
dp[i] = max(dp[i], dp[j] + 1)

return max(dp)

复杂度分析

  • 时间复杂度:$O(n^2)$,因为有两个嵌套的循环。
  • 空间复杂度:$O(n)$,因为需要一个长度为 ndp 数组。

674. Longest Continuous Increasing Subsequence

这是一个简单题目,但仍然值得理解。

1
2
3
4
5
6
7
class Solution:
def findLengthOfLCIS(self, nums: List[int]) -> int:
dp = [1] * len(nums)
for i in range(1, len(nums)):
if nums[i] > nums[i-1]:
dp[i] = max(dp[i], dp[i-1] + 1)
return max(dp)

392. Is Subsequence

1
2
3
4
5
6
7
8
9
class Solution:
def isSubsequence(self, s: str, t: str) -> bool:
i = 0
j = 0
while i < len(s) and j < len(t):
if s[i] == t[j]:
i += 1
j += 1
return i == len(s)

这个题目可以用双指针法,以 t 为基准,如果 s 中有相同字符就向前推进,如果 s 到结尾了,说明匹配完成。

115. Distinct Subsequences

这个问题要求我们找到字符串 s 中有多少不同的子序列等于字符串 t

首先,我们需要定义 dp,其中 dp[i][j] 表示从 s 的前 i 个字符中形成 t 的前 j 个字符的不同子序列数。

  • dp[i][0] 表示当 t 为空串时,结果为 1。
  • dp[0][j] 表示从空串中形成 t,结果为 0。

对于 dp[i][j],其结果取决于位置 ij 的字符:

  • 如果两者相等,结果等于不匹配 s[i-1] 的情况(dp[i-1][j])加上匹配的情况(dp[i-1][j-1])。
  • 如果不相等,则结果等于 dp[i-1][j]

注意,这里 i, j 指的是前 i 个和前 j 个字符。

此外,dp[0][0] 初始化为 1,因为空字符串是任何字符串的子序列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution:
def numDistinct(self, s: str, t: str) -> int:
dp = [[0 for _ in range(len(t) + 1)] for _ in range(len(s) + 1)]
for i in range(len(s) + 1):
dp[i][0] = 1

for j in range(1, len(t) + 1):
for i in range(1, len(s) + 1):
if s[i-1] == t[j-1]:
dp[i][j] = dp[i-1][j] + dp[i-1][j-1]
else:
dp[i][j] = dp[i-1][j]

return dp[-1][-1]

Off-by-One 溢出攻击分析

背景

上周,我参加了一门安全课程,其中包含了一个 off-by-one 溢出漏洞的示例。以下是原始代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* 简单的 off-by-one 溢出示例 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void foo(char *input) {
char buf[1024];
strncpy(buf, input, sizeof(buf));
buf[sizeof(buf)] = '\0';
}

void bar(void) {
printf("I've been hacked\n");
}

int main(int argc, char **argv) {
if (argc != 2) {
printf("Usage: %s input_string\n", argv[0]);
exit(EXIT_FAILURE);
}
foo(argv[1]);
return 0;
}

利用此漏洞的攻击命令是:

1
perl -e 'system "./obo", "\x38\x84\x04\x08"x256'

运行此命令的结果是屏幕上打印出多行 I've been hacked

分析

当程序进入 foo 函数时,内存布局如下(使用 GDB 观察到):

从上到下,布局包含返回地址、保存的帧指针(saved frame pointer)和缓冲区 (buf)。

当执行 buf[sizeof(buf)] = '\0'; 时,保存的帧指针 (ebp) 的最低有效字节被设置为 0。为了确保 ebp 在被部分覆盖后仍然指向 buf 区域内,需要至少 1024 字节的缓冲区。具体来说,需要覆盖 ebp,使其保持在合理范围内(最高到 0xff),这就是为什么缓冲区设置为 0xff * 4 字节的原因。

理解 foo 返回时的汇编命令

foo 函数返回时,通常会执行以下关键的汇编指令:

1. leave 指令

leave 指令等价于:

1
2
mov esp, ebp
pop ebp
  • mov esp, ebp:将栈指针 (esp) 设置为帧指针 (ebp) 的值,恢复栈指针到当前栈帧的顶部,从而释放当前函数占用的空间。
  • pop ebp:将栈顶的值弹出并赋给帧指针 (ebp),从而恢复调用者的帧指针。实际上,相当于将返回地址写入 ebp,这意味着将栈中的值(通常是调用者的帧地址)赋给 ebp,从而恢复调用者的栈帧。

leave 的作用是将 esp 恢复到函数调用前的位置,并弹出保存的 ebp。如果 ebp 被覆盖为指向某个特殊地址(例如缓冲区内的地址),在函数返回时可能导致错误的栈指针位置。

2. ret 指令

ret 指令从栈顶弹出一个地址并跳转到该地址:

1
pop eip

如果返回地址被覆盖为 bar 函数的地址,执行流将跳转到 bar,允许攻击者执行任意代码。本质上,ret 会将一个地址弹出到指令指针 (eip) 并跳转到该地址继续执行。

攻击步骤

  • 当执行命令 perl -e 'system "./obo", "\x38\x84\x04\x08"x256' 时,程序会将这些重复的字节作为输入传递给 ./obo
  • foo 函数返回时,执行了 leaveret 指令,由于返回地址被覆盖,程序跳转到 bar 函数,打印出成功的消息多次。

进一步分析:确定有效的覆盖位置

为了成功执行攻击,关键在于精确确定需要覆盖哪些字节,以有效地操控控制流。在这个例子中,溢出发生在执行 buf[sizeof(buf)] = '\0' 时,导致保存的帧指针 (ebp) 的最低有效字节被设置为 0。因此,需要调整 ebp 的值,确保其指向缓冲区区域,从而使执行流程按预期进行,最终跳转到 bar 函数。

基于进一步的分析和测试,得出以下见解:

  • 要准确确定覆盖位置,ebp 的值至关重要。然而,获得这个值是有挑战的,因为:

    1. GDB 调试会影响地址布局。
    2. 输入参数的长度会影响地址布局。
  • 在 GDB 调试下,foo 函数内的布局如下:

  • 执行 buf[sizeof(buf)] = '\0'; 后,ebp 被修改,使得返回地址实际上取的是 ebp + 1 处的值,即地址 0xbfffed00 + 1,也就是 0xbfffed04

  • 对应的偏移位置在 buf 的第 255 个位置,这意味着可以通过只在该特定位置填入返回地址来构造攻击。在 GDB 中使用以下命令进行验证:

    1
    r $(perl -e 'print  "\x01\x01\x01\x01"x254 . "\x38\x84\x04\x08"x1 . "\x01\x01\x01\x01"x1')
  • 经过验证,这在 GDB 调试下是有效的,但需要注意以下细节:

    1. 输入参数的长度必须始终为 256 字节,否则 ebp 的值会发生变化,因为输入参数会占用栈空间,从而影响帧的起始位置,进而影响 ebp 的值。
    2. 填充必须使用非零值,如 0x01,因为如果遇到 0 值,strncpy 会提前终止。
  • 直接执行程序时(即不使用 GDB),内存布局不同,导致偏移位置也不同。通过实验,发现偏移位置在第 235 个位置。对应的攻击命令是:

    1
    ./obo $(perl -e 'print  "\x01\x01\x01\x01"x234 . "\x38\x84\x04\x08"x1 . "\x01\x01\x01\x01"x21')

这样就实现了找到准确覆盖位置并成功执行攻击的效果。

Off-by-One Overflow Attack Analysis

Background

Last week, I attended a security course that included an example of an off-by-one overflow vulnerability. Here is the original code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* Simple off-by-one overflow example */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void foo(char *input) {
char buf[1024];
strncpy(buf, input, sizeof(buf));
buf[sizeof(buf)] = '\0';
}

void bar(void) {
printf("I've been hacked\n");
}

int main(int argc, char **argv) {
if (argc != 2) {
printf("Usage: %s input_string\n", argv[0]);
exit(EXIT_FAILURE);
}
foo(argv[1]);
return 0;
}

The answer provided for exploiting this vulnerability is:

1
perl -e 'system "./obo", "\x38\x84\x04\x08"x256'

The result of running this command is that multiple lines of I've been hacked are printed on the screen.

Analysis

When the program enters the foo function, the memory layout looks like this (as observed using GDB):

From top to bottom, the layout contains the return address, the saved frame pointer, and the buffer (buf).

When the line buf[sizeof(buf)] = '\0'; is executed, the least significant byte of the saved frame pointer (ebp) is set to 0. To ensure that ebp still points within the buf region after being partially overwritten, a buffer of at least 1024 bytes is required. Specifically, ebp needs to be overwritten such that it remains within a reasonable range (— up to 0xff), which is why the buffer is set to 0xff * 4 bytes.

Understanding Assembly Commands on foo Return

When the foo function returns, it typically executes the following key assembly instructions:

1. leave Instruction

The leave instruction is equivalent to:

1
2
mov esp, ebp
pop ebp
  • mov esp, ebp: This sets the stack pointer (esp) to the value of the frame pointer (ebp), restoring the stack pointer to the top of the current stack frame and effectively releasing the space occupied by the current function.
  • pop ebp: This pops the value at the top of the stack and assigns it to the frame pointer (ebp), thereby restoring the caller’s frame pointer. Essentially, it writes the return address into ebp, meaning it assigns the stack value (usually the caller’s frame address) to ebp, restoring the caller’s stack frame.

The effect of leave is to restore esp to its state before the function was called and to pop the saved ebp. If ebp has been overwritten to point to a special address (such as an address within the buffer), it can result in an incorrect stack pointer location during function return.

2. ret Instruction

The ret instruction pops an address off the top of the stack and jumps to that address:

1
pop eip

If the return address has been overwritten with the address of the bar function, the execution flow will jump to bar, allowing an attacker to run arbitrary code. Essentially, ret pops an address into the instruction pointer (eip) and jumps to that address to continue execution.

Attack Steps

  • When the command perl -e 'system "./obo", "\x38\x84\x04\x08"x256' is executed, the program takes these repeated bytes as the input to ./obo.
  • As the foo function returns, the leave and ret instructions are executed, leading to the return address being overwritten. This causes the program to jump to the bar function, printing the success message multiple times.

Further Analysis: Determining Effective Overwrite Locations

Stack Frame Layout Explanation

During the GDB debugging session, the memory layout for the stack frame of the foo function looks like this:

1
2
3
4
5
6
7
8
9
10
0xbfffed10   return address
0xbfffed0c saved frame pointer (ebp)
0xbfffed0b buf[1023]
...
0xbfffed03 buf[1015]
0xbfffed02 buf[1014]
0xbfffed01 buf[1013]
0xbfffed00 buf[1012]
...
0xbfffe90c buf[0]
  • Return address: Located at 0xbfffed10, this is the address that the program will jump to after the foo function finishes executing. Overwriting this address can control the flow of the program and potentially redirect it to malicious code (e.g., the bar function).

  • Saved frame pointer (ebp): Stored at 0xbfffed0c, this value is used to restore the calling function’s stack frame after foo finishes. In this example, we can see how the off-by-one overflow can overwrite the least significant byte of ebp.

  • Buffer (buf): The buffer starts at address 0xbfffe90c and extends to 0xbfffed0b, with buf[0] located at 0xbfffe90c and buf[1023] at 0xbfffed0b. The vulnerable line in the code, buf[sizeof(buf)] = '\0';, writes a null terminator (\0) just outside the bounds of this buffer, affecting the saved frame pointer.

In the off-by-one overflow scenario, the write operation overwrites the least significant byte of ebp, which is stored at 0xbfffed0c. By manipulating the value of ebp, we can influence the stack behavior when the leave and ret instructions are executed, eventually allowing us to control the program flow and redirect execution to the bar function.

To perform a successful attack, it’s crucial to determine precisely which bytes need to be overwritten in order to manipulate the control flow effectively. In this example, the overflow occurs when buf[sizeof(buf)] = '\0' is executed, causing the least significant byte of the saved frame pointer (ebp) to be set to 0. Thus, the value of ebp needs to be adjusted to ensure it points back into the buffer area, allowing the execution to proceed in the desired way and eventually jump to the bar function.

Based on further analysis and testing, the following insights were obtained:

  • To accurately determine the overwrite location, the value of ebp is crucial. However, obtaining this value is challenging because:

    1. GDB debugging affects address layout.
    2. The length of the input parameter affects the address layout.
  • Under GDB debugging, the layout within the foo function looks like this:

  • After executing buf[sizeof(buf)] = '\0';, ebp is modified such that the return address effectively takes the value at ebp + 1, which is the address 0xbfffed00 + 1, or 0xbfffed04.

  • The corresponding offset is at position 255 in buf, meaning the attack can be constructed by filling in the return address only at that specific location. The following command was used for verification in GDB:

    1
    r $(perl -e 'print  "\x01\x01\x01\x01"x254 . "\x38\x84\x04\x08"x1 . "\x01\x01\x01\x01"x1')
  • This was verified to work under GDB debugging, with some details to note:

    1. The input parameter length must always be 256 bytes; otherwise, the value of ebp will change, as the input parameter occupies stack space, affecting the starting position of the frame and thereby affecting the value of ebp.
    2. Padding must use non-zero values such as 0x01, because strncpy will terminate early if it encounters a 0 value.
  • When executing the program directly (i.e., without GDB), the memory layout differs, resulting in a different offset position. Through experimentation, it was found that the offset is at position 235. The corresponding attack command is:

    1
    ./obo $(perl -e 'print  "\x01\x01\x01\x01"x234 . "\x38\x84\x04\x08"x1 . "\x01\x01\x01\x01"x21')

This achieves the desired effect of accurately finding the overwrite location and successfully executing the attack.

ChatGPT生成的公式使用了以下格式:

1
2
3
\[
公式内容
\]

而Obsidian中的公式渲染使用的是以下格式:

1
2
3
$$
公式内容
$$

当我们将ChatGPT的公式复制到Obsidian中时,这种差异会导致无法正确渲染。

解决方案

我们可以创建一个Obsidian脚本,在粘贴操作时自动替换公式的格式。

1. 创建脚本

可以使用Obsidian中的插件来解决这个问题。

在你的库中,在template目录下创建一个文件fixlatex.js,并输入以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
module.exports = async (params) => {
const { quickAddApi } = params;

// 获取剪贴板内容
const clipboardContent = await quickAddApi.utility.getClipboard();

// 检查是否成功获取内容
if (!clipboardContent) {
new Notice("剪贴板为空或无法访问。");
return;
}

const modifiedContent = clipboardContent
.replace(/\\\[|\\\]/g, '$$$$') // 转换 \[ \] 为 $$ $$
.replace(/\\\(\s*|\s*\\\)/g, '$$'); // 转换 \( \) 为 $

// 将修改后的内容写回剪贴板
await navigator.clipboard.writeText(modifiedContent);

new Notice("剪贴板内容已处理并修改!");
};

2. 在QuickAdd中设置脚本

安装QuickAdd插件,并创建一个Macro,按如下图中的配置进行设置并保存。Macro的第一步是执行我们刚刚创建的用户脚本fixlatex.js,第二步是等待100毫秒,第三步是执行粘贴操作。

3. 在Commander中设置侧边快捷键

安装Commander插件,并将刚刚创建的QuickAdd操作设置为侧边栏的快捷键。你也可以跳过这一步,直接使用Obsidian命令执行这个操作。

4. 验证效果

现在,在ChatGPT的网页中(目前在APP中点击复制按钮似乎有点问题),点击复制按钮后,在Obsidian中点击侧边栏快捷键,或者手动执行QuickAdd命令,就可以将ChatGPT中的内容复制到Obsidian中,并自动转换Latex格式。

The formulas generated by ChatGPT use the following format:

1
2
3
\[
Formula Content
\]

However, Obsidian renders formulas using the following format:

1
2
3
$$
Formula Content
$$

When copying a formula from ChatGPT to Obsidian, this difference prevents proper rendering.

Solution

We can create a script for Obsidian to automatically replace the formula format when pasting.

1. Create the Script

We can solve this issue using a plugin in Obsidian.

In your vault, create a file named fixlatex.js under the template directory, and input the following content:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
module.exports = async (params) => {
const { quickAddApi } = params;

// Get clipboard content
const clipboardContent = await quickAddApi.utility.getClipboard();

// Check if content was successfully retrieved
if (!clipboardContent) {
new Notice("Clipboard is empty or inaccessible.");
return;
}

const modifiedContent = clipboardContent
.replace(/\\\[|\\\]/g, '$$$$') // Convert \[ \] to $$ $$
.replace(/\\\(\s*|\s*\\\)/g, '$$'); // Convert \( \) to $

// Write the modified content back to the clipboard
await navigator.clipboard.writeText(modifiedContent);

new Notice("Clipboard content has been processed and modified!");
};

2. Set Up the Script in QuickAdd

Install the QuickAdd plugin and create a Macro, configuring it as shown below, then save it. The first step in the Macro is to execute our user script fixlatex.js, the second step is to wait for 100 milliseconds, and the third step is to execute the paste action.

3. Set Sidebar Shortcut in Commander

Install the Commander plugin and set up the QuickAdd action we just created as a sidebar shortcut. You can also skip this step and directly use an Obsidian command to execute this action.

4. Verify the Effect

Now, on the ChatGPT webpage (currently there seems to be an issue when clicking the copy button in the app), click the copy button, then in Obsidian, click the sidebar shortcut or manually execute the QuickAdd command. This will copy the content from ChatGPT to Obsidian and automatically convert the LaTeX format.

About Me

Written on October 11, 2024, in Dublin, Ireland.


Self Introduction

Hello everyone, my name is Nansen Li, previously known as Nan Li. I’m from China and a technology-loving engineer. I hold a bachelor’s degree in Mechatronic Engineering and two master’s degrees in Computer Science. I previously worked as a Go Language Engineer at a major internet company in China, and I am currently pursuing a master’s degree at a university in Dublin.

I completed my undergraduate studies in Mechanical and Electronic Engineering, during which I participated in the National College Students Electronic Design Contest and the Intelligent Vehicle Competition held in China. Over time, I became increasingly interested in programming, which led me to pursue a Master’s degree in Computer Engineering to follow my true passion. During my Master’s studies, I continued to engage in programming-related competitions and interned at a major Internet company. I was fortunate to secure a full-time position at the company as a Software Engineer, where I worked for three years.

During my time at work, I had the privilege of developing software for hundreds of millions of users and directly serving customers. Every time I deployed a service, I was extremely careful because I knew that what I was releasing was not just cold lines of code but something that would impact the experience of thousands of users.

To help my family business recover from the pandemic, I left the company to support my family. At the same time, I began strengthening my foreign language skills and applied for an overseas study opportunity. Now, I have arrived in Ireland to further deepen my knowledge in computer science.

I am deeply interested in frontend and backend development, system architecture, algorithms, game development, and generative AI (e.g., Stable Diffusion and ChatGPT). I’ve always been dedicated to improving my programming skills and enjoy experimenting with new technologies, keeping myself sensitive to industry trends.

If you share similar interests in technology or lifestyle, feel free to reach out to me. We could discuss and exchange ideas together.

关于我

本文书写于 2024年10月11日,都柏林,爱尔兰。


自我介绍

大家好,我叫李楠森,曾用名李楠。我来自中国,是一名热爱科技的工程师。我拥有机械电子的本科学位和两个计算机科学的硕士学位。我曾在中国一家大型互联网公司担任过 Go 语言工程师,目前正在都柏林一所大学攻读硕士学位。

我本科学习机械电子工程,并且此期间参与了组织的中国大学生电子设计竞赛和在中国举办的智能汽车比赛。我在此期间逐渐对编程更感兴趣,为此,我开始攻读一个计算机工程的硕士学位,以追寻我真正热爱的东西。在硕士期间,我继续参与了和编程相关的比赛,并且在一家大型互联网公司实习。我有幸取得了该公司的正式职位,作为软件工程师,工作了三年。

在工作期间,我有幸为上亿用户开发软件并直接服务客户。每当我发布服务,我都很小心翼翼,我知道我所发布的不仅仅是冰冷的代码,还影响着成千上万用户的体验。

为了帮助家族企业从疫情中恢复,我离开了公司,去帮助我的家人。同时,我开始巩固我的外语技能,申请一段海外的学习机会。现在,我来到爱尔兰,开始继续深入学习计算机科学相关的知识。

我对前端和后端开发、系统架构、算法、游戏开发,以及生成式 AI(例如 Stable Diffusion 和 ChatGPT)有着浓厚的兴趣。我一直致力于提高自己的编程能力,也喜欢尝试新技术,保持对行业发展的敏感度。

如果你对技术或者生活方式有相同的兴趣,欢迎联系我,我们可以一起讨论和交流。

我是李楠森,这里是我的第一篇笔记,我不知道我能记录多少内容,也不知道能记录多久,但是我会尽力坚持去记录。

于 2024年10月11日凌晨 1点20分 爱尔兰 都柏林

I am Nansen Li, and this is my first note. I don’t know how much I can document, nor how long I can keep this up, but I will do my best to stay consistent with my entries.

At 1:20 AM on October 11, 2024, Dublin, Ireland

0%