Skip to content

Commit 5ed2cbc

Browse files
committed
add BOM support, fixes #1
1 parent 2cbf065 commit 5ed2cbc

File tree

3 files changed

+301
-20
lines changed

3 files changed

+301
-20
lines changed

lib_ini.sh

Lines changed: 128 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ function ini_validate_path() {
147147
fi
148148

149149
# Check for path traversal attempts
150-
if [[ "$file" =~ \.\./ ]] || [[ "$file" =~ \.\.\\ ]]; then
150+
if [[ "$file" =~ \.\./ ]]; then
151151
ini_error "Path traversal detected: $file"
152152
return 1
153153
fi
@@ -156,18 +156,24 @@ function ini_validate_path() {
156156
local normalized
157157
if command -v realpath >/dev/null 2>&1; then
158158
normalized=$(realpath -m "$file" 2>/dev/null || echo "$file")
159+
# realpath returns absolute paths, so .. in the middle is valid (e.g., /home/../home)
160+
# Only reject if .. appears at the start suggesting traversal
161+
if [[ "$normalized" =~ ^\.\./ ]]; then
162+
ini_error "Invalid path after normalization: $normalized"
163+
return 1
164+
fi
159165
else
160166
# Fallback: basic normalization
161167
normalized="$file"
162168
# Remove leading ./ if present
163169
normalized="${normalized#./}"
164-
fi
165-
166-
# Additional check: ensure path doesn't escape expected boundaries
167-
# This is a basic check - in production, you might want more sophisticated validation
168-
if [[ "$normalized" =~ \.\. ]]; then
169-
ini_error "Invalid path after normalization: $normalized"
170-
return 1
170+
171+
# Additional check: ensure path doesn't escape expected boundaries
172+
# Only check for .. if it's part of a path traversal pattern
173+
if [[ "$normalized" =~ \.\./ ]] || [[ "$normalized" =~ /\.\. ]] || [[ "$normalized" =~ ^\.\.$ ]]; then
174+
ini_error "Invalid path after normalization: $normalized"
175+
return 1
176+
fi
171177
fi
172178

173179
return 0
@@ -248,6 +254,24 @@ function ini_validate_env_var_name() {
248254
return 0
249255
}
250256

257+
# Remove UTF-8 BOM (Byte Order Mark) from a line if present
258+
# BOM in UTF-8 is the byte sequence EF BB BF (appears as U+FEFF)
259+
# This function is safe to call on any line - it only removes BOM if present
260+
function _ini_remove_bom() {
261+
local line="$1"
262+
263+
# Check if line starts with UTF-8 BOM (U+FEFF = \xEF\xBB\xBF)
264+
# In bash, we check for the BOM character directly
265+
if [[ "$line" =~ ^$'\xEF\xBB\xBF' ]]; then
266+
# Remove BOM from the beginning
267+
line="${line#$'\xEF\xBB\xBF'}"
268+
ini_debug "Removed UTF-8 BOM from line"
269+
fi
270+
271+
echo "$line"
272+
}
273+
274+
251275
# Lock file using flock
252276
function ini_lock_file() {
253277
local file="$1"
@@ -380,7 +404,14 @@ function ini_read() {
380404

381405
ini_debug "Reading key '$key' from section '$section' in file: $file"
382406

407+
local first_line=1
383408
while IFS= read -r line; do
409+
# Remove BOM from first line if present
410+
if [ "$first_line" -eq 1 ]; then
411+
line=$(_ini_remove_bom "$line")
412+
first_line=0
413+
fi
414+
384415
# Skip comments and empty lines
385416
if [[ -z "$line" || "$line" =~ ^[[:space:]]*[#\;] ]]; then
386417
continue
@@ -461,8 +492,20 @@ function ini_list_sections() {
461492

462493
ini_debug "Listing sections in file: $file"
463494

464-
# Extract section names
465-
grep -o '^\[[^]]*\]' "$file" 2>/dev/null | sed 's/^\[\(.*\)\]$/\1/'
495+
# Extract section names, handling BOM on first line
496+
local first_line=1
497+
while IFS= read -r line; do
498+
# Remove BOM from first line if present
499+
if [ "$first_line" -eq 1 ]; then
500+
line=$(_ini_remove_bom "$line")
501+
first_line=0
502+
fi
503+
504+
# Check for section header
505+
if [[ "$line" =~ ^\[([^]]+)\] ]]; then
506+
echo "${BASH_REMATCH[1]}"
507+
fi
508+
done < "$file"
466509
return 0
467510
}
468511

@@ -513,7 +556,14 @@ function ini_list_keys() {
513556

514557
ini_debug "Listing keys in section '$section' in file: $file"
515558

559+
local first_line=1
516560
while IFS= read -r line; do
561+
# Remove BOM from first line if present
562+
if [ "$first_line" -eq 1 ]; then
563+
line=$(_ini_remove_bom "$line")
564+
first_line=0
565+
fi
566+
517567
# Skip comments and empty lines
518568
if [[ -z "$line" || "$line" =~ ^[[:space:]]*[#\;] ]]; then
519569
continue
@@ -588,17 +638,30 @@ function ini_section_exists() {
588638

589639
ini_debug "Checking if section '$section' exists in file: $file"
590640

591-
# Check if section exists
592-
grep -q "^\[$escaped_section\]" "$file"
593-
local result=$?
641+
# Check if section exists, handling BOM on first line
642+
local first_line=1
643+
local found=0
644+
while IFS= read -r line; do
645+
# Remove BOM from first line if present
646+
if [ "$first_line" -eq 1 ]; then
647+
line=$(_ini_remove_bom "$line")
648+
first_line=0
649+
fi
650+
651+
# Check for section header
652+
if [[ "$line" =~ ^\[$escaped_section\] ]]; then
653+
found=1
654+
break
655+
fi
656+
done < "$file"
594657

595-
if [ $result -eq 0 ]; then
658+
if [ $found -eq 1 ]; then
596659
ini_debug "Section found: $section"
660+
return 0
597661
else
598662
ini_debug "Section not found: $section"
663+
return 1
599664
fi
600-
601-
return $result
602665
}
603666

604667
function ini_add_section() {
@@ -697,7 +760,14 @@ function ini_write() {
697760
fi
698761

699762
# Process the file line by line
763+
local first_line=1
700764
while IFS= read -r line || [ -n "$line" ]; do
765+
# Remove BOM from first line if present
766+
if [ "$first_line" -eq 1 ]; then
767+
line=$(_ini_remove_bom "$line")
768+
first_line=0
769+
fi
770+
701771
# Check for section
702772
if [[ "$line" =~ $section_pattern ]]; then
703773
in_section=1
@@ -826,7 +896,14 @@ function ini_remove_section() {
826896
ini_debug "Removing section '$section' from file: $file"
827897

828898
# Process the file line by line
899+
local first_line=1
829900
while IFS= read -r line; do
901+
# Remove BOM from first line if present
902+
if [ "$first_line" -eq 1 ]; then
903+
line=$(_ini_remove_bom "$line")
904+
first_line=0
905+
fi
906+
830907
# Check for section
831908
if [[ "$line" =~ $section_pattern ]]; then
832909
in_section=1
@@ -945,7 +1022,14 @@ function ini_remove_key() {
9451022
ini_debug "Removing key '$key' from section '$section' in file: $file"
9461023

9471024
# Process the file line by line
1025+
local first_line=1
9481026
while IFS= read -r line; do
1027+
# Remove BOM from first line if present
1028+
if [ "$first_line" -eq 1 ]; then
1029+
line=$(_ini_remove_bom "$line")
1030+
first_line=0
1031+
fi
1032+
9491033
# Check for section
9501034
if [[ "$line" =~ $section_pattern ]]; then
9511035
in_section=1
@@ -1425,8 +1509,15 @@ function ini_validate() {
14251509
local in_section=0
14261510
local last_section=""
14271511
local sections_found=0
1512+
local first_line=1
14281513

14291514
while IFS= read -r line || [ -n "$line" ]; do
1515+
# Remove BOM from first line if present
1516+
if [ "$first_line" -eq 1 ]; then
1517+
line=$(_ini_remove_bom "$line")
1518+
first_line=0
1519+
fi
1520+
14301521
line_num=$((line_num + 1))
14311522
local trimmed_line
14321523
trimmed_line=$(ini_trim "$line")
@@ -1533,7 +1624,14 @@ function ini_get_all() {
15331624

15341625
ini_debug "Getting all keys from section '$section' in file: $file"
15351626

1627+
local first_line=1
15361628
while IFS= read -r line; do
1629+
# Remove BOM from first line if present
1630+
if [ "$first_line" -eq 1 ]; then
1631+
line=$(_ini_remove_bom "$line")
1632+
first_line=0
1633+
fi
1634+
15371635
# Skip comments and empty lines
15381636
if [[ -z "$line" || "$line" =~ ^[[:space:]]*[#\;] ]]; then
15391637
continue
@@ -1628,7 +1726,14 @@ function ini_rename_section() {
16281726
ini_debug "Renaming section '$old_section' to '$new_section' in file: $file"
16291727

16301728
# Process the file line by line
1729+
local first_line=1
16311730
while IFS= read -r line || [ -n "$line" ]; do
1731+
# Remove BOM from first line if present
1732+
if [ "$first_line" -eq 1 ]; then
1733+
line=$(_ini_remove_bom "$line")
1734+
first_line=0
1735+
fi
1736+
16321737
# Check for old section header
16331738
if [[ "$line" =~ $old_section_pattern ]]; then
16341739
in_section=1
@@ -1781,7 +1886,14 @@ function ini_format() {
17811886
local comments_before_section=()
17821887

17831888
# First pass: collect all data
1889+
local first_line=1
17841890
while IFS= read -r line || [ -n "$line" ]; do
1891+
# Remove BOM from first line if present
1892+
if [ "$first_line" -eq 1 ]; then
1893+
line=$(_ini_remove_bom "$line")
1894+
first_line=0
1895+
fi
1896+
17851897
local trimmed_line
17861898
trimmed_line=$(ini_trim "$line")
17871899

run_tests.sh

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ bash tests/lib_ini_advanced_features_tests.sh
4040

4141
ADVANCED_EXIT=$?
4242

43+
# Now run the BOM support tests
44+
echo -e "\n${YELLOW}Running BOM Support Tests...${NC}"
45+
bash tests/lib_ini_bom_tests.sh
46+
47+
BOM_EXIT=$?
48+
4349
# Extract test counts from each test suite
4450
BASIC_TOTAL=$(bash tests/lib_ini_tests.sh 2>&1 | grep -oP 'Total tests executed: \K\d+' || echo "0")
4551
BASIC_PASSED=$(bash tests/lib_ini_tests.sh 2>&1 | grep -oP 'Tests passed: \K\d+' || echo "0")
@@ -61,10 +67,14 @@ ADVANCED_TOTAL=$(bash tests/lib_ini_advanced_features_tests.sh 2>&1 | grep -oP '
6167
ADVANCED_PASSED=$(bash tests/lib_ini_advanced_features_tests.sh 2>&1 | grep -oP 'Tests passed: \K\d+' || echo "0")
6268
ADVANCED_FAILED=$(bash tests/lib_ini_advanced_features_tests.sh 2>&1 | grep -oP 'Tests failed: \K\d+' || echo "0")
6369

70+
BOM_TOTAL=$(bash tests/lib_ini_bom_tests.sh 2>&1 | grep -oP 'Total BOM tests executed: \K\d+' || echo "0")
71+
BOM_PASSED=$(bash tests/lib_ini_bom_tests.sh 2>&1 | grep -oP 'Tests passed: \K\d+' || echo "0")
72+
BOM_FAILED=$(bash tests/lib_ini_bom_tests.sh 2>&1 | grep -oP 'Tests failed: \K\d+' || echo "0")
73+
6474
# Calculate totals
65-
TOTAL_TESTS=$((BASIC_TOTAL + EXTENDED_TOTAL + ENV_TOTAL + SECURITY_TOTAL + ADVANCED_TOTAL))
66-
TOTAL_PASSED=$((BASIC_PASSED + EXTENDED_PASSED + ENV_PASSED + SECURITY_PASSED + ADVANCED_PASSED))
67-
TOTAL_FAILED=$((BASIC_FAILED + EXTENDED_FAILED + ENV_FAILED + SECURITY_FAILED + ADVANCED_FAILED))
75+
TOTAL_TESTS=$((BASIC_TOTAL + EXTENDED_TOTAL + ENV_TOTAL + SECURITY_TOTAL + ADVANCED_TOTAL + BOM_TOTAL))
76+
TOTAL_PASSED=$((BASIC_PASSED + EXTENDED_PASSED + ENV_PASSED + SECURITY_PASSED + ADVANCED_PASSED + BOM_PASSED))
77+
TOTAL_FAILED=$((BASIC_FAILED + EXTENDED_FAILED + ENV_FAILED + SECURITY_FAILED + ADVANCED_FAILED + BOM_FAILED))
6878

6979
echo -e "\n${YELLOW}=====================================${NC}"
7080
echo -e "${YELLOW} Test Summary ${NC}"
@@ -74,10 +84,11 @@ echo -e "Extended tests: ${EXTENDED_TOTAL} executed, ${GREEN}${EXTENDED_PASS
7484
echo -e "Environment tests: ${ENV_TOTAL} executed, ${GREEN}${ENV_PASSED} passed${NC}, ${RED}${ENV_FAILED} failed${NC}"
7585
echo -e "Security tests: ${SECURITY_TOTAL} executed, ${GREEN}${SECURITY_PASSED} passed${NC}, ${RED}${SECURITY_FAILED} failed${NC}"
7686
echo -e "Advanced tests: ${ADVANCED_TOTAL} executed, ${GREEN}${ADVANCED_PASSED} passed${NC}, ${RED}${ADVANCED_FAILED} failed${NC}"
87+
echo -e "BOM support tests: ${BOM_TOTAL} executed, ${GREEN}${BOM_PASSED} passed${NC}, ${RED}${BOM_FAILED} failed${NC}"
7788
echo -e "${YELLOW}-------------------------------------${NC}"
7889
echo -e "Total: ${TOTAL_TESTS} executed, ${GREEN}${TOTAL_PASSED} passed${NC}, ${RED}${TOTAL_FAILED} failed${NC}"
7990

80-
if [ $BASIC_EXIT -eq 0 ] && [ $EXTENDED_EXIT -eq 0 ] && [ $ENV_OVERRIDE_EXIT -eq 0 ] && [ $SECURITY_EXIT -eq 0 ] && [ $ADVANCED_EXIT -eq 0 ]; then
91+
if [ $BASIC_EXIT -eq 0 ] && [ $EXTENDED_EXIT -eq 0 ] && [ $ENV_OVERRIDE_EXIT -eq 0 ] && [ $SECURITY_EXIT -eq 0 ] && [ $ADVANCED_EXIT -eq 0 ] && [ $BOM_EXIT -eq 0 ]; then
8192
echo -e "\n${GREEN}ALL TESTS PASSED!${NC}"
8293
exit 0
8394
else
@@ -87,5 +98,6 @@ else
8798
[ $ENV_OVERRIDE_EXIT -ne 0 ] && echo -e "${RED}Environment override tests failed.${NC}"
8899
[ $SECURITY_EXIT -ne 0 ] && echo -e "${RED}Security tests failed.${NC}"
89100
[ $ADVANCED_EXIT -ne 0 ] && echo -e "${RED}Advanced features tests failed.${NC}"
101+
[ $BOM_EXIT -ne 0 ] && echo -e "${RED}BOM support tests failed.${NC}"
90102
exit 1
91103
fi

0 commit comments

Comments
 (0)