@@ -9,7 +9,18 @@ pub(crate) struct PartName(String);
99impl PartName {
1010 pub ( crate ) fn new ( value : impl Into < String > ) -> XlsxResult < Self > {
1111 let value = value. into ( ) ;
12- validate_part_name ( & value) ?;
12+ validate_structure ( & value) ?;
13+ validate_conformance ( & value) ?;
14+ Ok ( Self ( value) )
15+ }
16+
17+ /// Accept a name that addresses a part unambiguously but breaks
18+ /// ECMA-376 Part 2 §6.2.2.2 character rules, so Compatible mode can
19+ /// still read parts that non-conforming producers emit (spaces,
20+ /// brackets, redundant percent-encoding).
21+ pub ( crate ) fn new_lenient ( value : impl Into < String > ) -> XlsxResult < Self > {
22+ let value = value. into ( ) ;
23+ validate_structure ( & value) ?;
1324 Ok ( Self ( value) )
1425 }
1526
@@ -24,7 +35,18 @@ impl PartName {
2435 ) ) ) ;
2536 }
2637 let normalized = value. replace ( '\\' , "/" ) ;
27- Self :: new ( format ! ( "/{}" , normalized. trim_start_matches( '/' ) ) )
38+ let name = format ! ( "/{}" , normalized. trim_start_matches( '/' ) ) ;
39+ if compatible {
40+ Self :: new_lenient ( name)
41+ } else {
42+ Self :: new ( name)
43+ }
44+ }
45+
46+ /// Describe how this name breaks the OPC character rules, for
47+ /// diagnostics when it was accepted leniently.
48+ pub ( crate ) fn conformance_error ( & self ) -> Option < String > {
49+ validate_conformance ( & self . 0 ) . err ( ) . map ( |e| e. to_string ( ) )
2850 }
2951
3052 pub ( crate ) fn as_str ( & self ) -> & str {
@@ -57,7 +79,9 @@ impl PartName {
5779 } else {
5880 format ! ( "{parent}/_rels/{file}.rels" )
5981 } ;
60- Self :: new ( path)
82+ // Conformance was already decided for the owner; the added
83+ // segments are always valid, so never re-reject here.
84+ Self :: new_lenient ( path)
6185 }
6286}
6387
@@ -83,19 +107,33 @@ impl std::fmt::Display for PartName {
83107 }
84108}
85109
86- fn validate_part_name ( value : & str ) -> XlsxResult < ( ) > {
110+ /// Rules that must hold for a name to address one part unambiguously.
111+ /// Violating these is unrecoverable in any policy.
112+ fn validate_structure ( value : & str ) -> XlsxResult < ( ) > {
87113 if value == "/" || !value. starts_with ( '/' ) || value. contains ( [ '?' , '#' , '\\' ] ) {
88114 return Err ( XlsxError :: InvalidFormat ( format ! (
89115 "invalid OPC part name: {value}"
90116 ) ) ) ;
91117 }
92-
93118 for segment in value[ 1 ..] . split ( '/' ) {
94- if segment. is_empty ( ) || matches ! ( segment, "." | ".." ) || segment . ends_with ( '.' ) {
119+ if segment. is_empty ( ) || matches ! ( segment, "." | ".." ) {
95120 return Err ( XlsxError :: InvalidFormat ( format ! (
96121 "invalid OPC part name segment in {value}"
97122 ) ) ) ;
98123 }
124+ }
125+ Ok ( ( ) )
126+ }
127+
128+ /// ECMA-376 Part 2 §6.2.2.2 character and encoding rules. Breaking these
129+ /// makes a package non-conforming without making the name ambiguous.
130+ fn validate_conformance ( value : & str ) -> XlsxResult < ( ) > {
131+ for segment in value[ 1 ..] . split ( '/' ) {
132+ if segment. ends_with ( '.' ) {
133+ return Err ( XlsxError :: InvalidFormat ( format ! (
134+ "OPC part name segment ends with a dot in {value}"
135+ ) ) ) ;
136+ }
99137 if segment. chars ( ) . any ( |character| {
100138 character. is_control ( )
101139 || character == ' '
@@ -236,7 +274,12 @@ pub(crate) fn resolve_internal_target_with_policy(
236274 "relationship target does not name a package part: target={target}"
237275 ) ) ) ;
238276 }
239- PartName :: new ( format ! ( "/{}" , parts. join( "/" ) ) )
277+ let resolved = format ! ( "/{}" , parts. join( "/" ) ) ;
278+ if compatible {
279+ PartName :: new_lenient ( resolved)
280+ } else {
281+ PartName :: new ( resolved)
282+ }
240283}
241284
242285fn decode_percent_encoded_unreserved ( value : & str ) -> String {
0 commit comments